import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import type { Sql } from '../db.js';
import type { Chat } from '../types/api.js';
import { getSkillBody, listSkills } from '../services/skills.js';
import {
  buildSkillInvokeSyntheticFrames,
  DEFAULT_SKILL_USER_MESSAGE,
  runSkillInvokeTransaction,
} from '../services/skill-invoke.js';

// Batch 9.6 slash-invoke handlers. Mirrors the MessageHandlers shape in
// routes/messages.ts so index.ts can pass thin adapters around broker +
// inference runner without skills.ts importing them directly.
export interface SkillInvokeHandlers {
  enqueueInference: (
    sessionId: string,
    chatId: string,
    assistantMessageId: string,
    user: string,
  ) => void;
  publishUserMessage: (
    sessionId: string,
    chatId: string,
    userMessageId: string,
    content: string,
  ) => void;
  publishSessionFrame: (
    sessionId: string,
    frame: Record<string, unknown> & { type: string },
  ) => void;
}

const SkillInvokeBody = z.object({
  skill_name: z.string().min(1),
  // Optional — server fills in a default if absent or whitespace-only so the
  // model always has something to act on (matches the spec's "Apply this
  // skill." filler).
  user_message: z.string().max(64_000).nullable().optional(),
});

export function registerSkillsRoutes(
  app: FastifyInstance,
  sql: Sql,
  handlers: SkillInvokeHandlers,
): void {
  // Debug/admin surface — the model interacts with skills via the three
  // skill_* tools, not through this endpoint.
  app.get('/api/skills', async () => {
    return { skills: await listSkills() };
  });

  // POST /api/chats/:id/skill_invoke — slash-command entry point. Loads the
  // skill body server-side (clients never get to forge file content),
  // persists 4 messages in one transaction (synthetic assistant tool_use,
  // synthetic tool result, real user message, streaming assistant), and
  // enqueues inference against the updated history.
  app.post<{ Params: { id: string } }>(
    '/api/chats/:id/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 { skill_name } = parsed.data;
      const userText = parsed.data.user_message?.trim()
        ? parsed.data.user_message
        : DEFAULT_SKILL_USER_MESSAGE;

      const chatRows = await sql<Chat[]>`
        SELECT id, session_id FROM chats WHERE id = ${req.params.id} AND status = 'open'
      `;
      if (chatRows.length === 0) {
        reply.code(404);
        return { error: 'chat not found' };
      }
      const chat = chatRows[0]!;
      const sessionId = chat.session_id;

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

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

      // Synthetic frames so useSessionStream's reducer reflects the new
      // history without a refetch. Frame shapes match the streaming-inference
      // protocol (see services/inference.ts InferenceFrame).
      for (const frame of buildSkillInvokeSyntheticFrames(chat.id, result, toolCall, body)) {
        handlers.publishSessionFrame(sessionId, frame);
      }
      handlers.publishUserMessage(sessionId, chat.id, result.user_message_id, userText);
      handlers.enqueueInference(sessionId, chat.id, result.assistant_message_id, 'default');

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