// v2.6.x: read_tab_by_number tool. Reads the conversation transcript of the
// chat that occupies a given session-scoped tab number. Stable tab numbers are
// stored in the session's workspace_panes envelope (WorkspaceState.tabNumbers),
// keyed by chat id. Lives in its own file (not appended to tools.ts) so tests
// can import the executor directly without dragging in the whole tool registry.
// Registered in tools.ts ALL_TOOLS.

import { z } from 'zod';
import type { Sql } from '../db.js';
// type-only import to dodge the runtime cycle (tools.ts re-exports this tool
// via ALL_TOOLS; importing ToolDef/ToolExecCtx at type level keeps the dep
// one-way).
import type { ToolDef, ToolExecCtx } from './tools.js';

const ReadTabByNumberInput = z.object({
  number: z.number().int().positive(),
});
export type ReadTabByNumberInputT = z.infer<typeof ReadTabByNumberInput>;

// Cap total transcript size so a long conversation can't blow the context
// window. The model gets a clear truncation marker when the cap is hit.
const MAX_TRANSCRIPT_CHARS = 20_000;

// WorkspaceState envelope shape (panes omitted — we only need tabNumbers here).
interface WorkspaceStateLike {
  panes?: unknown;
  tabNumbers?: Record<string, number>;
  nextTabNumber?: number;
  closedPaneStack?: unknown[];
}

// MIGRATION: the stored workspace_panes value may be the legacy bare
// WorkspacePane[] OR the WorkspaceState envelope. Normalize to an envelope so
// tabNumbers is always available (empty for the legacy shape — no tab numbers
// were tracked before the envelope landed).
function normalizeWorkspaceState(v: unknown): {
  tabNumbers: Record<string, number>;
} {
  if (Array.isArray(v)) {
    return { tabNumbers: {} };
  }
  if (v && typeof v === 'object' && Array.isArray((v as WorkspaceStateLike).panes)) {
    const env = v as WorkspaceStateLike;
    return { tabNumbers: env.tabNumbers ?? {} };
  }
  return { tabNumbers: {} };
}

// Pure executor split out from the ToolDef wrapper so tests can call it with a
// mocked Sql. Returns a transcript string (read-only — never writes).
export async function executeReadTabByNumber(
  input: ReadTabByNumberInputT,
  sql: Sql,
  sessionId: string,
): Promise<string> {
  const sessionRows = await sql<{ workspace_panes: unknown }[]>`
    SELECT workspace_panes FROM sessions WHERE id = ${sessionId}
  `;
  if (sessionRows.length === 0) {
    return `Session not found.`;
  }
  const { tabNumbers } = normalizeWorkspaceState(sessionRows[0]!.workspace_panes);

  // Reverse-lookup: find the chat id whose stable tab number equals the input.
  let chatId: string | null = null;
  for (const [cid, num] of Object.entries(tabNumbers)) {
    if (num === input.number) {
      chatId = cid;
      break;
    }
  }
  if (chatId === null) {
    return `No tab is numbered ${input.number} in this session.`;
  }

  // Read the conversation: skip system sentinels (role='system') and empty
  // content rows. Oldest first.
  const messages = await sql<{ role: string; content: string }[]>`
    SELECT role, content
      FROM messages
     WHERE chat_id = ${chatId}
       AND role <> 'system'
       AND content <> ''
     ORDER BY created_at ASC
  `;
  if (messages.length === 0) {
    return `Tab ${input.number} (chat ${chatId}) has no messages yet.`;
  }

  // Format a compact transcript, capping total output size.
  const parts: string[] = [];
  let total = 0;
  let truncated = false;
  for (const m of messages) {
    const block = `### ${m.role}\n${m.content}`;
    // +2 accounts for the "\n\n" joiner between blocks.
    if (total + block.length + 2 > MAX_TRANSCRIPT_CHARS) {
      truncated = true;
      break;
    }
    parts.push(block);
    total += block.length + 2;
  }

  let out = parts.join('\n\n');
  if (truncated) {
    out += `\n\n[transcript truncated at ${MAX_TRANSCRIPT_CHARS} chars]`;
  }
  return out;
}

export const readTabByNumber: ToolDef<ReadTabByNumberInputT> = {
  name: 'read_tab_by_number',
  description:
    'Read the conversation transcript of the tab with the given session-scoped tab number. Tab numbers are stable per session (shown in the workspace tab strip). Returns the messages of that tab oldest-first as a compact transcript. Read-only.',
  inputSchema: ReadTabByNumberInput,
  jsonSchema: {
    type: 'function',
    function: {
      name: 'read_tab_by_number',
      description:
        'Read the conversation transcript of the tab with the given session-scoped tab number. Read-only.',
      parameters: {
        type: 'object',
        properties: {
          number: {
            type: 'integer',
            description: 'The session-scoped tab number (positive integer).',
          },
        },
        required: ['number'],
        additionalProperties: false,
      },
    },
  },
  async execute(input, _projectRoot, _extraRoots, toolCtx?: ToolExecCtx) {
    if (!toolCtx) {
      return 'read_tab_by_number unavailable: no session context';
    }
    return await executeReadTabByNumber(input, toolCtx.sql, toolCtx.sessionId);
  },
};
