/**
 * 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 type { VirtualLlmService } from '../services/virtual-llm.service.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. When provided, requests for `kind=virtual` Llm rows are
   * fanned through the SSE control channel rather than calling an
   * upstream URL directly. Required for v1 of the virtual-LLM feature;
   * absent in older test configurations.
   */
  virtualLlms?: VirtualLlmService;
  /** 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' };
      }

      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,
        });
      };

      // ── Virtual-provider branch ──
      // For kind=virtual rows there is no upstream URL — inference is fanned
      // through the SSE control channel back to the publishing mcplocal.
      // VirtualLlmService.enqueueInferTask handles the routing.
      if (llm.kind === 'virtual') {
        if (deps.virtualLlms === undefined) {
          reply.code(500);
          audit(500);
          return { error: 'virtual LLM dispatch unavailable (server misconfiguration)' };
        }
        try {
          // Direct infer is the v1-v4 sync path: caller pinned to a
          // specific Llm name and wants a fast 503 if the publisher is
          // offline. The async durable-queue API (Stage 3) is for callers
          // that explicitly opt into queueing.
          if (!streaming) {
            const ref = await deps.virtualLlms.enqueueInferTask(llm.name, body, false, { failFast: true });
            const result = await ref.done;
            reply.code(result.status);
            audit(result.status);
            return result.body;
          }
          // Streaming: open SSE response, fan chunks from the result stream
          // into outgoing SSE frames.
          reply.raw.writeHead(200, {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            Connection: 'keep-alive',
            'X-Accel-Buffering': 'no',
          });
          const ref = await deps.virtualLlms.enqueueInferTask(llm.name, body, true, { failFast: true });
          const unsubscribe = ref.onChunk((chunk) => writeSseChunk(reply, chunk.data));
          try {
            await ref.done;
            audit(200);
          } catch (err) {
            const payload = JSON.stringify({ error: err instanceof Error ? err.message : String(err) });
            writeSseChunk(reply, payload);
            audit(502);
          } finally {
            unsubscribe();
            if (!reply.raw.writableEnded) reply.raw.end();
          }
          return reply;
        } catch (err) {
          const status = (err as { statusCode?: number }).statusCode ?? 502;
          reply.code(status);
          audit(status);
          return { error: err instanceof Error ? err.message : String(err) };
        }
      }

      // ── Public-provider branch (existing behavior) ──

      // 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);

      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`);
}
