import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import type { Sql } from '../db.js';
import { getPendingPermission, respondToPermission, cancelPendingPermission } from '../services/permission-waiter.js';
import { getTaskCommands } from '../services/agent-commands-cache.js';

interface InferenceApi {
  cancel: (sessionId: string, chatId: string) => Promise<boolean>;
}

// F1: the dispatcher's reach into an in-flight external-agent run. Narrow by
// design (not the whole dispatcher) — the route only needs to fire the abort.
// Returns true when a controller was registered for the task (an external run was
// in flight), false otherwise (native boocode task, or already finished).
export type ExternalCancelFn = (taskId: string) => boolean;

const CreateBody = z.object({
  project_id: z.string().uuid(),
  input: z.string().min(1).max(64_000),
  agent: z.string().max(100).optional(),
  model: z.string().max(200).optional(),
  mode_id: z.string().max(200).optional(),
  thinking_option_id: z.string().max(200).optional(),
});

const PermissionBody = z.object({
  option_id: z.string().max(200).nullable(),
  updated_input: z.record(z.unknown()).optional(),
});

const ListQuery = z.object({
  state: z.enum(['pending', 'running', 'completed', 'failed', 'blocked', 'cancelled']).optional(),
  project_id: z.string().uuid().optional(),
});

export function registerTaskRoutes(
  app: FastifyInstance,
  sql: Sql,
  inference: InferenceApi,
  cancelExternal: ExternalCancelFn,
): void {
  // POST /api/tasks — create a new task
  app.post('/api/tasks', async (req, reply) => {
    const parsed = CreateBody.safeParse(req.body);
    if (!parsed.success) {
      reply.code(400);
      return { error: 'invalid body', details: parsed.error.flatten() };
    }

    const { project_id, input, agent, model, mode_id, thinking_option_id } = parsed.data;

    const [task] = await sql<{ id: string; state: string }[]>`
      INSERT INTO tasks (project_id, input, agent, model, mode_id, thinking_option_id)
      VALUES (${project_id}, ${input}, ${agent ?? null}, ${model ?? null}, ${mode_id ?? null}, ${thinking_option_id ?? null})
      RETURNING id, state
    `;

    reply.code(201);
    return { id: task!.id, state: task!.state };
  });

  app.get('/api/tasks', async (req, _reply) => {
    const parsed = ListQuery.safeParse(req.query);
    if (!parsed.success) {
      return { error: 'invalid query', details: parsed.error.flatten() };
    }

    const { state, project_id } = parsed.data;

    if (state && project_id) {
      return sql`
        SELECT id, project_id, state, input, output_summary, agent, model, execution_path, session_id, started_at, ended_at, created_at
        FROM tasks
        WHERE state = ${state} AND project_id = ${project_id}
        ORDER BY created_at DESC
        LIMIT 100
      `;
    } else if (state) {
      return sql`
        SELECT id, project_id, state, input, output_summary, agent, model, execution_path, session_id, started_at, ended_at, created_at
        FROM tasks
        WHERE state = ${state}
        ORDER BY created_at DESC
        LIMIT 100
      `;
    } else if (project_id) {
      return sql`
        SELECT id, project_id, state, input, output_summary, agent, model, execution_path, session_id, started_at, ended_at, created_at
        FROM tasks
        WHERE project_id = ${project_id}
        ORDER BY created_at DESC
        LIMIT 100
      `;
    } else {
      return sql`
        SELECT id, project_id, state, input, output_summary, agent, model, execution_path, session_id, started_at, ended_at, created_at
        FROM tasks
        ORDER BY created_at DESC
        LIMIT 100
      `;
    }
  });

  app.get<{ Params: { id: string } }>('/api/tasks/:id', async (req, reply) => {
    const rows = await sql`
      SELECT id, project_id, parent_task_id, state, input, output_summary, agent, model, execution_path, session_id, cost_tokens, started_at, ended_at, created_at
      FROM tasks
      WHERE id = ${req.params.id}
    `;
    if (rows.length === 0) {
      reply.code(404);
      return { error: 'task not found' };
    }
    return rows[0];
  });

  // POST /api/tasks/:id/cancel — cancel a pending or running task
  app.post<{ Params: { id: string } }>('/api/tasks/:id/cancel', async (req, reply) => {
    const taskId = req.params.id;

    const rows = await sql<{ id: string; state: string; session_id: string | null }[]>`
      SELECT id, state, session_id FROM tasks WHERE id = ${taskId}
    `;
    if (rows.length === 0) {
      reply.code(404);
      return { error: 'task not found' };
    }

    const task = rows[0]!;
    if (task.state !== 'pending' && task.state !== 'running' && task.state !== 'blocked') {
      reply.code(409);
      return { error: `cannot cancel task in state '${task.state}'` };
    }

    cancelPendingPermission(taskId);

    // F1: abort the in-flight external-agent run (opencode / goose / qwen / claude).
    // Idempotent — a double-Stop re-aborts harmlessly; a native boocode task is not
    // registered, so this returns false and the inference.cancel path below handles
    // it unchanged. The dispatcher's run-function finalizes the streaming assistant
    // message as 'cancelled' once the backend honors the signal.
    cancelExternal(taskId);

    // If running, try to cancel inference (native boocode path — unchanged).
    if ((task.state === 'running' || task.state === 'blocked') && task.session_id) {
      // Find active chat in the task's session
      const chats = await sql<{ id: string }[]>`
        SELECT id FROM chats WHERE session_id = ${task.session_id} AND status = 'open'
      `;
      for (const chat of chats) {
        await inference.cancel(task.session_id, chat.id);
      }
    }

    await sql`
      UPDATE tasks
      SET state = 'cancelled', ended_at = clock_timestamp()
      WHERE id = ${taskId} AND state IN ('pending', 'running', 'blocked')
    `;

    return { cancelled: true };
  });

  // GET /api/tasks/:id/permission — pending permission prompt (if any)
  app.get<{ Params: { id: string } }>('/api/tasks/:id/permission', async (req, reply) => {
    const prompt = getPendingPermission(req.params.id);
    if (!prompt) {
      reply.code(404);
      return { error: 'no pending permission' };
    }
    return prompt;
  });

  // POST /api/tasks/:id/permission — respond to a pending permission prompt
  app.post<{ Params: { id: string } }>('/api/tasks/:id/permission', async (req, reply) => {
    const parsed = PermissionBody.safeParse(req.body);
    if (!parsed.success) {
      reply.code(400);
      return { error: 'invalid body', details: parsed.error.flatten() };
    }

    const ok = respondToPermission(req.params.id, parsed.data.option_id, parsed.data.updated_input as Record<string, unknown> | undefined);
    if (!ok) {
      reply.code(404);
      return { error: 'no pending permission' };
    }
    return { ok: true };
  });

  // GET /api/tasks/:id/commands — cached ACP slash commands (if any)
  app.get<{ Params: { id: string } }>('/api/tasks/:id/commands', async (req, reply) => {
    const commands = getTaskCommands(req.params.id);
    if (!commands?.length) {
      reply.code(404);
      return { error: 'no commands cached' };
    }
    return { taskId: req.params.id, commands };
  });
}
