/**
 * Conversation recall tools — SearchTaskConversation / ReadTaskConversation.
 *
 * META tools (always available) that let the agent retrieve older parts of the
 * CURRENT task's conversation instead of asking the user to repeat themselves
 * or stuffing the whole transcript into prompt context.
 *
 * Sources:
 *   - DB local task comments (user requests / interjections / agent progress),
 *     via the per-task `listComments()` accessor.
 *   - `transcript.jsonl` (the raw ReAct thread), read line-by-line.
 *
 * Scoping is enforced by the bound `ctx.taskConversation` IO: comments come from
 * `listLocalTaskComments(taskId)` (WHERE task_id = ?), and the transcript path is
 * bound by the worker to a location a tool call can never use to read another
 * task's logs. For a top-level local task that is the per-task `runtimeDir`
 * (never the space-shared `workspacePath/logs` fallback). A sub-task run instead
 * gets a transcript-only IO (no comments — see `makeTranscriptOnlyConversationIO`)
 * bound to the sub-task's OWN isolated workspace transcript. When the transcript
 * path is unset the transcript source yields nothing (comments still work); when
 * the whole IO is unset (gitea-issue runs) the tools degrade to a friendly
 * non-error message.
 */

import { existsSync, readFileSync } from 'node:fs';
import { ToolDef } from '../../llm/openai-compat.js';
import type { ToolContext, ToolResult, TaskConversationComment } from './core.js';
import { excerpt, tokenize } from './text-excerpt.js';

const SEARCH_DEF: ToolDef = {
  type: 'function',
  function: {
    name: 'SearchTaskConversation',
    description:
      'このタスクの過去の会話 (コメント + transcript) をキーワードで検索し、出典 ref 付きの短い抜粋を返す。長い/継続タスクでユーザーに聞き直す前に過去の制約・判断を思い出すために使う。詳細は ReadToolDoc({ name: "SearchTaskConversation" })。',
    parameters: {
      type: 'object',
      properties: {
        query: { type: 'string', description: '検索キーワード。スペース区切りで複数指定すると AND 検索 (すべての語を含む発言にヒット)。部分一致・大文字小文字無視。' },
        source: { type: 'string', enum: ['comments', 'transcript', 'both'], description: '検索対象。既定 both。' },
        author: { type: 'string', enum: ['user', 'agent', 'system'], description: '発言者で絞り込む。' },
        kind: { type: 'string', description: 'コメント種別で絞り込む (request/comment/interjection/result/ask/progress/handoff)。transcript には適用されない。' },
        limit: { type: 'number', description: '最大件数。既定 10、最大 50。' },
      },
      required: ['query'],
    },
  },
};

const READ_DEF: ToolDef = {
  type: 'function',
  function: {
    name: 'ReadTaskConversation',
    description:
      'SearchTaskConversation が返した ref (comment:<id> または transcript:<index>) の前後を数件だけ読み、当時の文脈を確認する。詳細は ReadToolDoc({ name: "ReadTaskConversation" })。',
    parameters: {
      type: 'object',
      properties: {
        ref: { type: 'string', description: '"comment:123" または "transcript:42"。' },
        before: { type: 'number', description: '前に含める件数。既定 2、最大 5。' },
        after: { type: 'number', description: '後に含める件数。既定 2、最大 5。' },
      },
      required: ['ref'],
    },
  },
};

export const TOOL_DEFS: Record<string, ToolDef> = {
  SearchTaskConversation: SEARCH_DEF,
  ReadTaskConversation: READ_DEF,
};

const NO_CONTEXT_MSG =
  '会話検索はこのコンテキストでは利用できません (subtask など local_task と紐付かない実行)。現在見えている文脈で判断してください。';

const EXCERPT_MAX = 200;
const READ_EXCERPT_MAX = 500;

type Author = 'user' | 'agent' | 'system';

interface TranscriptEntry {
  index: number;
  author: Author;
  content: string;
}

/** Map a transcript message role to a coarse author bucket. */
function roleToAuthor(role: unknown): Author {
  if (role === 'user') return 'user';
  if (role === 'assistant' || role === 'tool') return 'agent';
  return 'system';
}

function loadTranscript(path: string | undefined): TranscriptEntry[] {
  if (!path || !existsSync(path)) return [];
  const out: TranscriptEntry[] = [];
  const lines = readFileSync(path, 'utf8').split('\n');
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i]!;
    if (!line.trim()) continue;
    try {
      const parsed = JSON.parse(line) as { role?: unknown; content?: unknown };
      const content = typeof parsed.content === 'string' ? parsed.content : '';
      out.push({ index: i, author: roleToAuthor(parsed.role), content });
    } catch {
      /* skip malformed line */
    }
  }
  return out;
}

function clampLimit(v: unknown, def: number, max: number): number {
  const n = typeof v === 'number' && Number.isFinite(v) ? Math.floor(v) : def;
  return Math.min(Math.max(n, 1), max);
}

function clampNeighbors(v: unknown, def: number, max: number): number {
  const n = typeof v === 'number' && Number.isFinite(v) ? Math.floor(v) : def;
  return Math.min(Math.max(n, 0), max);
}

async function search(input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {
  const io = ctx.taskConversation;
  if (!io) return { output: NO_CONTEXT_MSG, isError: false };

  const query = typeof input['query'] === 'string' ? (input['query'] as string) : '';
  if (!query.trim()) {
    return { output: 'query が空です。検索キーワードを指定してください。', isError: true };
  }
  const source = (input['source'] as string) ?? 'both';
  const author = input['author'] as Author | undefined;
  const kind = typeof input['kind'] === 'string' ? (input['kind'] as string) : undefined;
  const limit = clampLimit(input['limit'], 10, 50);
  const tokens = tokenize(query);
  const matches = (body: string): boolean => {
    const lower = body.toLowerCase();
    return tokens.every((t) => lower.includes(t));
  };

  type Hit = { ref: string; header: string; body: string };
  const hits: Hit[] = [];

  if (source === 'comments' || source === 'both') {
    const comments = await io.listComments();
    for (const c of comments) {
      if (author && c.author !== author) continue;
      if (kind && c.kind !== kind) continue;
      if (!matches(c.body)) continue;
      hits.push({ ref: `comment:${c.id}`, header: `${c.author}/${c.kind} ${c.createdAt}`, body: c.body });
    }
  }

  if (source === 'transcript' || source === 'both') {
    // kind is a comment concept; transcript entries have none, so a kind filter
    // excludes the transcript source entirely.
    if (!kind) {
      for (const e of loadTranscript(io.transcriptPath)) {
        if (author && e.author !== author) continue;
        if (!matches(e.content)) continue;
        hits.push({ ref: `transcript:${e.index}`, header: e.author, body: e.content });
      }
    }
  }

  if (hits.length === 0) {
    return { output: `## Conversation Search Results\n\n"${query}" に一致する会話は見つかりませんでした。`, isError: false };
  }

  const shown = hits.slice(0, limit);
  const lines = [`## Conversation Search Results (query: "${query}", ${hits.length} hit(s)${hits.length > limit ? `, showing ${limit}` : ''})`, ''];
  for (const h of shown) {
    lines.push(`- ${h.ref} ${h.header}`);
    lines.push(`  ${excerpt(h.body, tokens, EXCERPT_MAX)}`);
    lines.push('');
  }
  lines.push('前後の文脈は ReadTaskConversation({ ref }) で確認できます。');
  return { output: lines.join('\n'), isError: false };
}

async function read(input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {
  const io = ctx.taskConversation;
  if (!io) return { output: NO_CONTEXT_MSG, isError: false };

  const ref = typeof input['ref'] === 'string' ? (input['ref'] as string) : '';
  const m = /^(comment|transcript):(\d+)$/.exec(ref.trim());
  if (!m) {
    return { output: 'ref が不正です。"comment:<id>" または "transcript:<index>" を指定してください。', isError: true };
  }
  const kind = m[1] as 'comment' | 'transcript';
  const id = Number(m[2]);
  const before = clampNeighbors(input['before'], 2, 5);
  const after = clampNeighbors(input['after'], 2, 5);

  if (kind === 'comment') {
    const comments = await io.listComments();
    const pos = comments.findIndex((c) => c.id === id);
    if (pos < 0) return { output: `comment:${id} は見つかりませんでした。`, isError: true };
    const slice = comments.slice(Math.max(0, pos - before), pos + after + 1);
    const lines = [`## ReadTaskConversation — comment #${id} (前 ${before} / 後 ${after})`, ''];
    for (const c of slice) {
      const marker = c.id === id ? ' ←' : '';
      lines.push(`- comment:${c.id} ${c.author}/${c.kind} ${c.createdAt}${marker}`);
      lines.push(`  ${excerpt(c.body, [], READ_EXCERPT_MAX)}`);
    }
    return { output: lines.join('\n'), isError: false };
  }

  const entries = loadTranscript(io.transcriptPath);
  const pos = entries.findIndex((e) => e.index === id);
  if (pos < 0) return { output: `transcript:${id} は見つかりませんでした。`, isError: true };
  const slice = entries.slice(Math.max(0, pos - before), pos + after + 1);
  const lines = [`## ReadTaskConversation — transcript #${id} (前 ${before} / 後 ${after})`, ''];
  for (const e of slice) {
    const marker = e.index === id ? ' ←' : '';
    lines.push(`- transcript:${e.index} ${e.author}${marker}`);
    lines.push(`  ${excerpt(e.content, [], READ_EXCERPT_MAX)}`);
  }
  return { output: lines.join('\n'), isError: false };
}

export async function executeTool(
  name: string,
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult | null> {
  if (name === 'SearchTaskConversation') return search(input, ctx);
  if (name === 'ReadTaskConversation') return read(input, ctx);
  return null;
}

// Keep the type import referenced for consumers of this module's typings.
export type { TaskConversationComment };
