/**
 * BooCoder MCP Server — exposes task primitives as MCP tools.
 *
 * Started when `--mcp` flag is passed to the entry point. Runs stdio transport
 * so external tools (opencode in Termius) can drive the task queue.
 */
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import type { Sql } from '../db.js';
import { applyOne, rejectOne } from './pending_changes.js';

interface TaskRow {
  id: string;
  state: string;
}

interface PendingRow {
  id: string;
  file_path: string;
  operation: string;
  diff: string;
  session_id: string;
}

interface ProjectPathRow {
  path: string;
}

interface MessageRow {
  id: string;
  session_id: string;
  chat_id: string | null;
  role: string;
  content: string;
  status: string;
  model: string | null;
  created_at: Date;
}

function textResult(data: unknown) {
  return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };
}

export async function startMcpServer(sql: Sql): Promise<void> {
  const server = new McpServer(
    { name: 'boocoder', version: '2.0.2' },
    { capabilities: { tools: {} } },
  );

  // 1. boocoder.create_task
  server.tool(
    'boocoder.create_task',
    'Create a new task in the BooCoder task queue',
    {
      project_id: z.string().describe('Project UUID'),
      input: z.string().describe('Task description / prompt for the agent'),
      agent: z.string().optional().describe('Agent name (optional — uses default if omitted)'),
      model: z.string().optional().describe('Model override (optional)'),
      mode_id: z.string().optional().describe('Permission/mode id (optional)'),
      thinking_option_id: z.string().optional().describe('Thinking/effort option id (optional)'),
    },
    async (args) => {
      const [row] = await sql<TaskRow[]>`
        INSERT INTO tasks (project_id, input, agent, model, mode_id, thinking_option_id, state)
        VALUES (
          ${args.project_id},
          ${args.input},
          ${args.agent ?? null},
          ${args.model ?? null},
          ${args.mode_id ?? null},
          ${args.thinking_option_id ?? null},
          'pending'
        )
        RETURNING id, state
      `;
      return textResult({
        task_id: row!.id,
        state: row!.state,
        mode_id: args.mode_id ?? null,
        thinking_option_id: args.thinking_option_id ?? null,
      });
    },
  );

  // 2. boocoder.list_pending_changes
  server.tool(
    'boocoder.list_pending_changes',
    'List pending changes awaiting review',
    {
      session_id: z.string().optional().describe('Optional session filter'),
    },
    async (args) => {
      let rows: PendingRow[];
      if (args.session_id) {
        rows = await sql<PendingRow[]>`
          SELECT id, file_path, operation, diff, session_id
          FROM pending_changes
          WHERE status = 'pending' AND session_id = ${args.session_id}
          ORDER BY created_at ASC
        `;
      } else {
        rows = await sql<PendingRow[]>`
          SELECT id, file_path, operation, diff, session_id
          FROM pending_changes
          WHERE status = 'pending'
          ORDER BY created_at ASC
        `;
      }
      const items = rows.map((r) => ({
        id: r.id,
        file_path: r.file_path,
        operation: r.operation,
        diff_preview: r.diff.slice(0, 200),
      }));
      return textResult(items);
    },
  );

  // 3. boocoder.apply
  server.tool(
    'boocoder.apply',
    'Apply a pending change (write to disk)',
    {
      change_id: z.string().describe('Pending change UUID'),
    },
    async (args) => {
      // Resolve projectRoot from the change's session → project path
      const [proj] = await sql<ProjectPathRow[]>`
        SELECT p.path FROM pending_changes pc
        JOIN sessions s ON pc.session_id = s.id
        JOIN projects p ON s.project_id = p.id
        WHERE pc.id = ${args.change_id}
      `;
      if (!proj) {
        return textResult({ success: false, file_path: '', error: 'change not found or project path unresolved' });
      }
      const result = await applyOne(sql, args.change_id, proj.path);
      return textResult({ success: result.success, file_path: result.file_path, error: result.error });
    },
  );

  // 4. boocoder.reject
  server.tool(
    'boocoder.reject',
    'Reject a pending change (mark as rejected, no disk write)',
    {
      change_id: z.string().describe('Pending change UUID'),
    },
    async (args) => {
      await rejectOne(sql, args.change_id);
      return textResult({ success: true });
    },
  );

  // 5. boocoder.dispatch_external_agent
  server.tool(
    'boocoder.dispatch_external_agent',
    'Create a task targeting a specific external agent (ACP or PTY dispatch)',
    {
      project_id: z.string().describe('Project UUID'),
      input: z.string().describe('Task prompt'),
      agent: z.string().describe('Agent name (must match available_agents registry)'),
      model: z.string().optional().describe('Model override (optional)'),
      mode_id: z.string().optional().describe('Permission/mode id (optional)'),
      thinking_option_id: z.string().optional().describe('Thinking/effort option id (optional)'),
    },
    async (args) => {
      const [row] = await sql<TaskRow[]>`
        INSERT INTO tasks (project_id, input, agent, model, mode_id, thinking_option_id, state)
        VALUES (
          ${args.project_id},
          ${args.input},
          ${args.agent},
          ${args.model ?? null},
          ${args.mode_id ?? null},
          ${args.thinking_option_id ?? null},
          'pending'
        )
        RETURNING id, state
      `;

      // Determine execution path from available_agents
      const [agentRow] = await sql<{ supports_acp: boolean }[]>`
        SELECT supports_acp FROM available_agents WHERE name = ${args.agent}
      `;
      const executionPath = agentRow?.supports_acp ? 'acp' : 'pty';

      return textResult({
        task_id: row!.id,
        state: row!.state,
        execution_path: executionPath,
        mode_id: args.mode_id ?? null,
        thinking_option_id: args.thinking_option_id ?? null,
      });
    },
  );

  // 6. boocoder.view_session_history
  server.tool(
    'boocoder.view_session_history',
    'Retrieve the most-recent N messages of a session chat transcript (role != system) from messages_with_parts, returned in chronological (oldest→newest) order',
    {
      session_id: z.string().describe('Session UUID'),
      chat_id: z.string().optional().describe('Optional chat UUID — narrows to one chat tab'),
      limit: z
        .number()
        .int()
        .min(1)
        .max(200)
        .optional()
        .describe('Max messages to return (default 50, max 200)'),
    },
    async (args) => {
      const effectiveLimit = Math.min(args.limit ?? 50, 200);
      let rows: MessageRow[];
      if (args.chat_id) {
        rows = await sql<MessageRow[]>`
          SELECT id, session_id, chat_id, role, content, status, model, created_at
          FROM (
            SELECT id, session_id, chat_id, role, content, status, model, created_at
            FROM messages_with_parts
            WHERE session_id = ${args.session_id}
              AND chat_id = ${args.chat_id}
              AND role != 'system'
            ORDER BY created_at DESC
            LIMIT ${effectiveLimit}
          ) sub
          ORDER BY created_at ASC
        `;
      } else {
        rows = await sql<MessageRow[]>`
          SELECT id, session_id, chat_id, role, content, status, model, created_at
          FROM (
            SELECT id, session_id, chat_id, role, content, status, model, created_at
            FROM messages_with_parts
            WHERE session_id = ${args.session_id}
              AND role != 'system'
            ORDER BY created_at DESC
            LIMIT ${effectiveLimit}
          ) sub
          ORDER BY created_at ASC
        `;
      }
      return textResult({ session_id: args.session_id, count: rows.length, messages: rows });
    },
  );

  // Connect via stdio
  const transport = new StdioServerTransport();
  await server.connect(transport);

  // Block until stdin closes (transport handles lifecycle)
  await new Promise<void>((resolve) => {
    process.stdin.on('end', resolve);
    process.stdin.on('close', resolve);
  });

  await sql.end({ timeout: 5 });
}
