import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import type { Sql } from '../db.js';
import type { Broker } from '@boocode/server/broker';
import type { WsFrame } from '@boocode/contracts/ws-frames';
import { getSkillBody } from '@boocode/server/skills';
import {
  buildSkillInvokeSyntheticFrames,
  buildSkillInvokeUserFrames,
  DEFAULT_SKILL_USER_MESSAGE,
  runSkillInvokeTransaction,
} from '@boocode/server/skill-invoke';
import type { FlowRunner } from '../services/flow-runner.js';
import { flowForSkill } from '../services/skill-flow-map.js';
import { resolveChatId } from './chat-resolve.js';

const SkillInvokeBody = z.object({
  pane_id: z.string().min(1).max(200),
  skill_name: z.string().min(1),
  user_message: z.string().max(64_000).nullable().optional(),
  // v2.5.9: when set to an external provider, the skill runs UNDER that agent —
  // its body is injected into a dispatched task instead of native inference.
  provider: 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(),
  // Flow-dispatch band; only used when the skill maps to a conductor flow.
  band: z.enum(['small', 'medium', 'large']).optional(),
});

interface InferenceApi {
  enqueue: (sessionId: string, chatId: string, assistantId: string, user: string) => void;
  hasActive: (chatId: string) => boolean;
}

export function registerSkillRoutes(
  app: FastifyInstance,
  sql: Sql,
  broker: Broker,
  inference: InferenceApi,
  flowRunner: FlowRunner,
): void {
  app.post<{ Params: { sessionId: string } }>(
    '/api/sessions/:sessionId/skill_invoke',
    async (req, reply) => {
      const parsed = SkillInvokeBody.safeParse(req.body);
      if (!parsed.success) {
        reply.code(400);
        return { error: 'invalid body', details: parsed.error.flatten() };
      }

      const sessionId = req.params.sessionId;
      const { pane_id, skill_name, provider, model, mode_id, thinking_option_id } = parsed.data;
      const sessionRows = await sql<{ id: string; project_id: string }[]>`
        SELECT id, project_id FROM sessions WHERE id = ${sessionId}
      `;
      if (sessionRows.length === 0) {
        reply.code(404);
        return { error: 'session not found' };
      }

      const chatId = await resolveChatId(sql, sessionId, pane_id);
      if (!chatId) {
        reply.code(404);
        return { error: 'pane not found' };
      }

      if (inference.hasActive(chatId)) {
        reply.code(409);
        return { error: 'inference already running on this chat' };
      }

      const userText = parsed.data.user_message?.trim()
        ? parsed.data.user_message
        : DEFAULT_SKILL_USER_MESSAGE;

      const body = await getSkillBody(skill_name);
      if (body === null) {
        reply.code(404);
        return { error: 'unknown_skill', message: `unknown skill: ${skill_name}` };
      }

      // Native path: if the skill maps to a conductor flow, launch the full
      // fan-out (personas → fold → synthesizer → adversarial gate) instead of
      // single-context body injection. External-provider invocations bypass
      // this — they run the skill body under the chosen external agent.
      const flowName = (!provider || provider === 'boocode') ? flowForSkill(skill_name) : undefined;
      if (flowName) {
        const { runId } = await flowRunner.launch({
          projectId: sessionRows[0]!.project_id,
          flowName,
          band: parsed.data.band ?? 'small',
          input: { question: userText },
          model: model ?? undefined,
        });
        reply.code(202);
        return { run_id: runId, flow_name: flowName, dispatched: true };
      }

      // v2.5.9: external agent → run the skill UNDER that agent. The skill body
      // stays server-side (like the native path's tool message) and is injected
      // into a dispatched task; the agent receives the skill instructions + the
      // user's text. Mirrors the messages-route external-provider dispatch.
      if (provider && provider !== 'boocode') {
        const [userMsg] = await sql<{ id: string }[]>`
          INSERT INTO messages (session_id, chat_id, role, content, status, created_at)
          VALUES (${sessionId}, ${chatId}, 'user', ${userText}, 'complete', clock_timestamp())
          RETURNING id
        `;
        broker.publishFrame(sessionId, { type: 'message_started', message_id: userMsg!.id, chat_id: chatId, role: 'user' } as WsFrame);
        broker.publishFrame(sessionId, { type: 'delta', message_id: userMsg!.id, chat_id: chatId, content: userText } as WsFrame);
        broker.publishFrame(sessionId, { type: 'message_complete', message_id: userMsg!.id, chat_id: chatId } as WsFrame);

        const taskInput = `${body}\n\n---\n\n${userText}`;
        const [task] = await sql<{ id: string; state: string }[]>`
          INSERT INTO tasks (project_id, input, agent, model, mode_id, thinking_option_id, session_id, chat_id)
          VALUES (${sessionRows[0]!.project_id}, ${taskInput}, ${provider}, ${model ?? null}, ${mode_id ?? null}, ${thinking_option_id ?? null}, ${sessionId}, ${chatId})
          RETURNING id, state
        `;
        await sql`UPDATE chats SET updated_at = clock_timestamp() WHERE id = ${chatId}`;
        reply.code(202);
        return { user_message_id: userMsg!.id, task_id: task!.id, dispatched: true };
      }

      const { result, toolCall } = await runSkillInvokeTransaction(sql, {
        sessionId,
        chatId,
        skillName: skill_name,
        skillBody: body,
        userText,
      });

      for (const frame of buildSkillInvokeSyntheticFrames(chatId, result, toolCall, body)) {
        broker.publishFrame(sessionId, frame as WsFrame);
      }
      for (const frame of buildSkillInvokeUserFrames(chatId, result.user_message_id, userText)) {
        broker.publishFrame(sessionId, frame as WsFrame);
      }

      inference.enqueue(sessionId, chatId, result.assistant_message_id, 'default');

      reply.code(202);
      return result;
    },
  );
}
