/**
 * Agent chat + threads HTTP surface.
 *
 *   POST /api/v1/agents/:name/chat            — chat (non-streaming + SSE)
 *   POST /api/v1/agents/:name/threads         — explicit thread create
 *   GET  /api/v1/agents/:name/threads         — list threads (caller-scoped)
 *   GET  /api/v1/threads/:id/messages         — replay thread history
 *
 * RBAC: chat + threads on a named agent route through `run:agents:<name>` so
 * a viewer can list them but only callers with `run` rights can drive a turn.
 * History under `/threads/:id` checks `view:agents` (best we can do without a
 * thread→agent reverse lookup in the URL) plus a service-level owner check.
 *
 * The SSE pattern mirrors `llm-infer.ts` — same headers, same `data: ...\n\n`
 * frame format, same `[DONE]` terminator. Each ChatService chunk becomes one
 * frame; final/error chunks close the stream.
 */
import type { FastifyInstance, FastifyReply } from 'fastify';
import type { ChatService, ChatStreamChunk } from '../services/chat.service.js';
import { AgentChatRequestSchema } from '../validation/agent.schema.js';
import { NotFoundError } from '../services/mcp-server.service.js';

export function registerAgentChatRoutes(
  app: FastifyInstance,
  chat: ChatService,
): void {
  app.post<{ Params: { name: string } }>(
    '/api/v1/agents/:name/chat',
    async (request, reply) => {
      const ownerId = request.userId ?? 'system';
      let parsed;
      try {
        parsed = AgentChatRequestSchema.parse(request.body ?? {});
      } catch (err) {
        reply.code(400);
        return { error: (err as Error).message };
      }

      const {
        threadId, message, messages: messagesOverride, stream, personality,
        ...paramsRest
      } = parsed;

      const args = {
        agentName: request.params.name,
        ownerId,
        ...(threadId !== undefined ? { threadId } : {}),
        ...(message !== undefined ? { userMessage: message } : {}),
        ...(messagesOverride !== undefined
          ? { messagesOverride: messagesOverride.map((m) => ({ role: m.role, content: m.content, ...(m.tool_call_id !== undefined ? { tool_call_id: m.tool_call_id } : {}) })) }
          : {}),
        ...(personality !== undefined ? { personalityName: personality } : {}),
        params: paramsRest,
      };

      if (stream !== true) {
        try {
          return await chat.chat(args);
        } catch (err) {
          if (err instanceof NotFoundError) {
            reply.code(404);
            return { error: err.message };
          }
          reply.code(502);
          return { error: (err as Error).message };
        }
      }

      // Streaming — exact same headers as llm-infer.
      reply.raw.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        Connection: 'keep-alive',
        'X-Accel-Buffering': 'no',
      });
      try {
        for await (const chunk of chat.chatStream(args)) {
          writeSseChunk(reply, JSON.stringify(chunk));
          if (chunk.type === 'final' || chunk.type === 'error') break;
        }
        writeSseChunk(reply, '[DONE]');
      } catch (err) {
        const payload: ChatStreamChunk = { type: 'error', message: (err as Error).message };
        writeSseChunk(reply, JSON.stringify(payload));
        writeSseChunk(reply, '[DONE]');
      } finally {
        reply.raw.end();
      }
      return reply;
    },
  );

  app.post<{ Params: { name: string }; Body: { title?: string } }>(
    '/api/v1/agents/:name/threads',
    async (request, reply) => {
      const ownerId = request.userId ?? 'system';
      try {
        const t = await chat.createThread(request.params.name, ownerId, request.body?.title);
        reply.code(201);
        return t;
      } catch (err) {
        if (err instanceof NotFoundError) {
          reply.code(404);
          return { error: err.message };
        }
        throw err;
      }
    },
  );

  app.get<{ Params: { name: string } }>(
    '/api/v1/agents/:name/threads',
    async (request, reply) => {
      const ownerId = request.userId ?? undefined;
      try {
        return await chat.listThreads(request.params.name, ownerId);
      } catch (err) {
        if (err instanceof NotFoundError) {
          reply.code(404);
          return { error: err.message };
        }
        throw err;
      }
    },
  );

  app.get<{ Params: { id: string } }>(
    '/api/v1/threads/:id/messages',
    async (request, reply) => {
      const ownerId = request.userId ?? 'system';
      try {
        return await chat.listMessages(request.params.id, ownerId);
      } catch (err) {
        if (err instanceof NotFoundError) {
          reply.code(404);
          return { error: err.message };
        }
        throw err;
      }
    },
  );
}

function writeSseChunk(reply: FastifyReply, data: string): void {
  // Guard against writing to a destroyed/closed socket — happens when the
  // client disconnects mid-stream and we still try to flush an error payload
  // from the catch path. Without the guard, Node emits an unhandled 'error'
  // on the response and the noise pollutes logs.
  if (reply.raw.destroyed || reply.raw.writableEnded) return;
  reply.raw.write(`data: ${data}\n\n`);
}
