/**
 * POST /api/v1/llms/:name/infer
 *
 * OpenAI-compatible chat completions endpoint. The RBAC check runs in the
 * global hook — this URL maps to `run:llms:<name>`, not the default
 * `create:llms`. See `main.ts:mapUrlToPermission`.
 *
 * Non-streaming: resolves the Llm, dispatches to the right provider adapter,
 * returns the OpenAI chat.completion JSON.
 *
 * Streaming (`stream: true`): pipes adapter-emitted chunks back as
 * `text/event-stream`. Adapters translate provider-native SSE into OpenAI
 * `chat.completion.chunk`s so clients can use any OpenAI SDK unchanged.
 */
import type { FastifyInstance, FastifyReply } from 'fastify';
import type { LlmService } from '../services/llm.service.js';
import type { LlmAdapterRegistry } from '../services/llm/dispatcher.js';
import { NotFoundError } from '../services/mcp-server.service.js';
import type { OpenAiChatRequest, InferContext } from '../services/llm/types.js';

export interface LlmInferDeps {
  llmService: LlmService;
  adapters: LlmAdapterRegistry;
  /** Optional hook to emit audit events — consumer may ignore. */
  onInferenceEvent?: (event: InferenceAuditEvent) => void;
}

export interface InferenceAuditEvent {
  kind: 'llm_inference_call';
  llmName: string;
  model: string;
  type: string;
  userId?: string | undefined;
  tokenSha?: string | undefined;
  streaming: boolean;
  durationMs: number;
  status: number;
}

export function registerLlmInferRoutes(
  app: FastifyInstance,
  deps: LlmInferDeps,
): void {
  app.post<{ Params: { name: string }; Body: OpenAiChatRequest }>(
    '/api/v1/llms/:name/infer',
    async (request, reply) => {
      const started = Date.now();
      let llm;
      try {
        llm = await deps.llmService.getByName(request.params.name);
      } catch (err) {
        if (err instanceof NotFoundError) {
          reply.code(404);
          return { error: err.message };
        }
        throw err;
      }

      const body = (request.body ?? {}) as OpenAiChatRequest;
      if (!body.messages || body.messages.length === 0) {
        reply.code(400);
        return { error: 'messages is required' };
      }

      // Resolve API key (may be empty string for providers that don't take one).
      let apiKey = '';
      if (llm.apiKeyRef !== null) {
        try {
          apiKey = await deps.llmService.resolveApiKey(llm.name);
        } catch (err) {
          reply.code(500);
          return { error: `Failed to resolve API key: ${err instanceof Error ? err.message : String(err)}` };
        }
      }

      const ctx: InferContext = {
        body,
        modelOverride: llm.model,
        apiKey,
        url: llm.url,
        extraConfig: llm.extraConfig,
      };

      const adapter = deps.adapters.get(llm.type);
      const streaming = body.stream === true;

      const audit = (status: number): void => {
        if (deps.onInferenceEvent === undefined) return;
        deps.onInferenceEvent({
          kind: 'llm_inference_call',
          llmName: llm.name,
          model: llm.model,
          type: llm.type,
          userId: request.userId,
          tokenSha: request.mcpToken?.tokenSha,
          streaming,
          durationMs: Date.now() - started,
          status,
        });
      };

      if (!streaming) {
        try {
          const result = await adapter.infer(ctx);
          reply.code(result.status);
          audit(result.status);
          return result.body;
        } catch (err) {
          audit(502);
          reply.code(502);
          return { error: err instanceof Error ? err.message : String(err) };
        }
      }

      // Streaming path — set SSE headers and pipe chunks.
      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 adapter.stream(ctx)) {
          writeSseChunk(reply, chunk.data);
          if (chunk.done === true) break;
        }
        audit(200);
      } catch (err) {
        const payload = JSON.stringify({
          error: { message: err instanceof Error ? err.message : String(err) },
        });
        writeSseChunk(reply, payload);
        writeSseChunk(reply, '[DONE]');
        audit(502);
      } finally {
        reply.raw.end();
      }
      return reply;
    },
  );
}

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