/**
 * Project-scoped chat + threads HTTP surface. Mirrors `agent-chat.ts`, but the
 * chat subject is a Project (its Prompts → system, its MCP servers → tools,
 * its llmProvider/llmModel → the LLM), and history is persisted as
 * project-scoped ChatThreads attributed to the caller.
 *
 *   POST   /api/v1/projects/:name/chat             — chat (non-streaming + SSE)
 *   POST   /api/v1/projects/:name/threads          — explicit thread create
 *   GET    /api/v1/projects/:name/threads          — list threads (caller-scoped)
 *   DELETE /api/v1/projects/:name/threads/:id       — delete a thread (owner or admin)
 *
 * RBAC: chat/threads route through `run:projects:<name>` (see main.ts
 * mapUrlToPermission). Thread reads/deletes add a service-level owner check;
 * delete additionally honors `delete:projects:<name>` as an admin override.
 * The SSE frame format matches agent-chat / llm-infer exactly.
 */
import type { FastifyInstance, FastifyReply } from 'fastify';
import type { ChatService, ChatStreamChunk } from '../services/chat.service.js';
import type { RbacService } from '../services/rbac.service.js';
import { ProjectChatRequestSchema } from '../validation/agent.schema.js';
import { NotFoundError } from '../services/mcp-server.service.js';

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

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

      const args = {
        projectName: 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 } : {}) })) }
          : {}),
        ...(allowSecrets !== undefined ? { allowSecrets } : {}),
        params: paramsRest,
      };

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

      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.chatProjectStream(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/projects/:name/threads',
    async (request, reply) => {
      const ownerId = request.userId ?? 'system';
      try {
        const t = await chat.createProjectThread(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/projects/:name/threads',
    async (request, reply) => {
      const ownerId = request.userId ?? undefined;
      try {
        return await chat.listProjectThreads(request.params.name, ownerId);
      } catch (err) {
        if (err instanceof NotFoundError) {
          reply.code(404);
          return { error: err.message };
        }
        throw err;
      }
    },
  );

  app.delete<{ Params: { name: string; id: string } }>(
    '/api/v1/projects/:name/threads/:id',
    async (request, reply) => {
      const ownerId = request.userId ?? 'system';
      // Admin override: a caller who can delete the project can prune anyone's
      // threads; otherwise only their own (enforced in the service, 404 on miss).
      const canDeleteOthers = request.userId !== undefined
        && await rbac.canAccess(request.userId, 'delete', 'projects', request.params.name);
      try {
        await chat.deleteThread(request.params.id, ownerId, canDeleteOthers);
        reply.code(204);
        return null;
      } catch (err) {
        if (err instanceof NotFoundError) {
          reply.code(404);
          return { error: err.message };
        }
        throw err;
      }
    },
  );
}

function writeSseChunk(reply: FastifyReply, data: string): void {
  if (reply.raw.destroyed || reply.raw.writableEnded) return;
  reply.raw.write(`data: ${data}\n\n`);
}
