import Fastify from 'fastify';
import type { FastifyInstance } from 'fastify';
import cors from '@fastify/cors';
import { APP_VERSION } from '@mcpctl/shared';
import type { HttpConfig } from './config.js';
import { setProviderDisabledInConfig } from './config.js';
import { McpdClient } from './mcpd-client.js';
import { registerProxyRoutes } from './routes/proxy.js';
import { registerMcpEndpoint } from './mcp-endpoint.js';
import { registerProjectMcpEndpoint } from './project-mcp-endpoint.js';
import { registerInspectEndpoint } from './inspect-endpoint.js';
import { registerProxymodelEndpoint } from './proxymodel-endpoint.js';
import { registerReplayEndpoint } from './replay-endpoint.js';
import { registerCacheEndpoint } from './cache-endpoint.js';
import { registerPauseEndpoint } from './pause-endpoint.js';
import { TrafficCapture } from './traffic.js';
import type { McpRouter } from '../router.js';
import type { HealthMonitor } from '../health.js';
import type { TieredHealthMonitor } from '../health/tiered.js';
import type { ProviderRegistry } from '../providers/registry.js';
import type { ManagedVllmProvider } from '../providers/vllm-managed.js';

export interface HttpServerDeps {
  router: McpRouter;
  healthMonitor?: HealthMonitor | undefined;
  tieredHealthMonitor?: TieredHealthMonitor | undefined;
  providerRegistry?: ProviderRegistry | null | undefined;
}

export async function createHttpServer(
  config: HttpConfig,
  deps: HttpServerDeps,
): Promise<FastifyInstance> {
  const app = Fastify({
    logger: {
      level: config.logLevel,
    },
  });

  await app.register(cors, {
    origin: true,
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  });

  // Health endpoint
  app.get('/health', async (_request, reply) => {
    const upstreams = deps.router.getUpstreamNames();
    const healthStatuses = deps.healthMonitor
      ? deps.healthMonitor.getAllStatuses()
      : undefined;

    // Include tiered summary if available
    let tieredSummary: { mcpd: string; llmProvider: string | null } | undefined;
    if (deps.tieredHealthMonitor) {
      const tiered = await deps.tieredHealthMonitor.checkHealth();
      tieredSummary = {
        mcpd: tiered.mcpd.status,
        llmProvider: tiered.mcplocal.llmProvider,
      };
    }

    reply.code(200).send({
      status: 'healthy',
      version: APP_VERSION,
      uptime: process.uptime(),
      timestamp: new Date().toISOString(),
      upstreams: upstreams.length,
      mcpdUrl: config.mcpdUrl,
      ...(healthStatuses !== undefined ? { health: healthStatuses } : {}),
      ...(tieredSummary !== undefined ? { tiered: tieredSummary } : {}),
    });
  });

  // Detailed tiered health endpoint
  app.get('/health/detailed', async (_request, reply) => {
    if (!deps.tieredHealthMonitor) {
      reply.code(503).send({
        error: 'Tiered health monitor not configured',
      });
      return;
    }

    const status = await deps.tieredHealthMonitor.checkHealth();
    reply.code(200).send(status);
  });

  // Liveness probe
  app.get('/healthz', async (_request, reply) => {
    reply.code(200).send({ status: 'ok' });
  });

  // LLM health check — cached to avoid burning tokens on every call.
  // Does a real inference call at most once per 10 minutes.
  let llmHealthCache: { result: Record<string, unknown>; expiresAt: number } | null = null;
  const LLM_HEALTH_CACHE_MS = 10 * 60 * 1000; // 10 minutes

  app.get('/llm/health', async (_request, reply) => {
    const provider = deps.providerRegistry?.getProvider('fast') ?? null;
    if (!provider) {
      reply.code(200).send({ status: 'not configured' });
      return;
    }

    // Return cached result if fresh
    if (llmHealthCache && Date.now() < llmHealthCache.expiresAt) {
      reply.code(200).send(llmHealthCache.result);
      return;
    }

    // For managed providers (e.g. vllm-managed) that are not running,
    // report their lifecycle state without triggering startup via complete().
    if ('getStatus' in provider && typeof (provider as ManagedVllmProvider).getStatus === 'function') {
      const status = (provider as ManagedVllmProvider).getStatus();
      if (status.state !== 'running') {
        const response = {
          status: status.state === 'error' ? 'error' : 'ok',
          provider: provider.name,
          state: status.state,
          ...(status.lastError ? { error: status.lastError } : {}),
        };
        reply.code(200).send(response);
        return;
      }
    }

    try {
      const result = await provider.complete({
        messages: [{ role: 'user', content: 'Respond with exactly: ok' }],
        maxTokens: 10,
      });
      const ok = result.content.trim().toLowerCase().includes('ok');
      const response = {
        status: ok ? 'ok' : 'unexpected response',
        provider: provider.name,
        response: result.content.trim().slice(0, 100),
      };
      llmHealthCache = { result: response, expiresAt: Date.now() + LLM_HEALTH_CACHE_MS };
      reply.code(200).send(response);
    } catch (err) {
      const msg = (err as Error).message ?? String(err);
      const response = {
        status: 'error',
        provider: provider.name,
        error: msg.slice(0, 200),
      };
      // Cache errors for 1 minute only (retry sooner)
      llmHealthCache = { result: response, expiresAt: Date.now() + 60_000 };
      reply.code(200).send(response);
    }
  });

  // LLM models — list available models from the active provider
  app.get<{ Querystring: { provider?: string } }>('/llm/models', async (request, reply) => {
    const registry = deps.providerRegistry;
    const providerName = request.query.provider;
    let provider;
    if (providerName && registry) {
      provider = registry.get(providerName) ?? null;
    } else {
      provider = registry?.getProvider('fast') ?? null;
    }
    if (!provider) {
      reply.code(200).send({ models: [], provider: null });
      return;
    }
    try {
      const models = await provider.listModels();
      reply.code(200).send({ models, provider: provider.name });
    } catch {
      reply.code(200).send({ models: [], provider: provider.name });
    }
  });

  // LLM providers — list all registered providers with tier assignments and health
  app.get('/llm/providers', async (_request, reply) => {
    const registry = deps.providerRegistry;
    if (!registry) {
      reply.code(200).send({ providers: [], tiers: { fast: [], heavy: [] }, health: {} });
      return;
    }

    // Run isAvailable() on all providers in parallel (lightweight, no tokens burned)
    const names = registry.list();
    const healthChecks = await Promise.all(
      names.map(async (name) => {
        const provider = registry.get(name);
        if (!provider) return { name, available: false };
        try {
          const available = await provider.isAvailable();
          return { name, available };
        } catch {
          return { name, available: false };
        }
      }),
    );
    const health: Record<string, boolean> = {};
    for (const check of healthChecks) {
      health[check.name] = check.available;
    }

    // Collect extended details for managed providers
    const details: Record<string, { managed: boolean; state?: string; lastError?: string }> = {};
    for (const name of names) {
      const provider = registry.get(name);
      if (provider && 'getStatus' in provider && typeof (provider as ManagedVllmProvider).getStatus === 'function') {
        const status = (provider as ManagedVllmProvider).getStatus();
        const detail: { managed: boolean; state?: string; lastError?: string } = { managed: true, state: status.state };
        if (status.lastError !== null) detail.lastError = status.lastError;
        details[name] = detail;
      }
    }

    reply.code(200).send({
      providers: names,
      tiers: {
        fast: registry.getTierProviders('fast'),
        heavy: registry.getTierProviders('heavy'),
      },
      health,
      ...(Object.keys(details).length > 0 ? { details } : {}),
    });
  });

  // Per-provider status (managed providers expose lifecycle state). Used by
  // `mcpctl provider <name> status` to read vllm-managed's state without
  // burning a token like /llm/health does.
  app.get<{ Params: { name: string } }>('/llm/providers/:name/status', async (request, reply) => {
    const registry = deps.providerRegistry;
    const provider = registry?.get(request.params.name) ?? null;
    if (provider === null) {
      reply.code(404).send({ error: `Provider '${request.params.name}' not found` });
      return;
    }
    if (!('getStatus' in provider) || typeof (provider as ManagedVllmProvider).getStatus !== 'function') {
      // Non-managed providers (anthropic, openai, gemini-cli) have no
      // lifecycle — they're always "ready" as long as the API key works.
      reply.code(200).send({ name: provider.name, managed: false });
      return;
    }
    const status = (provider as ManagedVllmProvider).getStatus();
    reply.code(200).send({ name: provider.name, managed: true, ...status });
  });

  // Stop a managed provider (free GPU memory). No-op on non-managed
  // providers. Returns 200 with the resulting status either way.
  app.post<{ Params: { name: string } }>('/llm/providers/:name/stop', async (request, reply) => {
    const registry = deps.providerRegistry;
    const provider = registry?.get(request.params.name) ?? null;
    if (provider === null) {
      reply.code(404).send({ error: `Provider '${request.params.name}' not found` });
      return;
    }
    if (!('dispose' in provider) || typeof (provider as ManagedVllmProvider).dispose !== 'function') {
      reply.code(400).send({ error: `Provider '${request.params.name}' is not managed (nothing to stop)` });
      return;
    }
    (provider as ManagedVllmProvider).dispose();
    const status = (provider as ManagedVllmProvider).getStatus();
    reply.code(200).send({ name: provider.name, managed: true, ...status });
  });

  // Start (warm up) a managed provider so the first chat doesn't pay
  // the model-load latency.
  app.post<{ Params: { name: string } }>('/llm/providers/:name/start', async (request, reply) => {
    const registry = deps.providerRegistry;
    const provider = registry?.get(request.params.name) ?? null;
    if (provider === null) {
      reply.code(404).send({ error: `Provider '${request.params.name}' not found` });
      return;
    }
    if (!('warmup' in provider) || typeof (provider as ManagedVllmProvider).warmup !== 'function') {
      reply.code(400).send({ error: `Provider '${request.params.name}' is not managed (nothing to start)` });
      return;
    }
    if ((provider as ManagedVllmProvider).isDisabled?.()) {
      reply.code(409).send({ error: `Provider '${request.params.name}' is disabled — run \`mcpctl provider enable\` first` });
      return;
    }
    (provider as ManagedVllmProvider).warmup();
    // warmup() is fire-and-forget — return current state immediately so
    // the CLI can show 'starting' and the user knows it's been kicked.
    const status = (provider as ManagedVllmProvider).getStatus();
    reply.code(202).send({ name: provider.name, managed: true, ...status });
  });

  // Persistent disable: dispose the running process AND set the
  // `disabled: true` flag on the provider's entry in ~/.mcpctl/config.json
  // so the next mcplocal restart doesn't auto-start it. Live: complete()
  // and ensureRunning() short-circuit immediately.
  app.post<{ Params: { name: string } }>('/llm/providers/:name/disable', async (request, reply) => {
    const registry = deps.providerRegistry;
    const provider = registry?.get(request.params.name) ?? null;
    if (provider === null) {
      reply.code(404).send({ error: `Provider '${request.params.name}' not found` });
      return;
    }
    if (!('setDisabled' in provider) || typeof (provider as ManagedVllmProvider).setDisabled !== 'function') {
      reply.code(400).send({ error: `Provider '${request.params.name}' is not managed (no enable/disable surface)` });
      return;
    }
    (provider as ManagedVllmProvider).setDisabled(true);
    // Best-effort persist. If the entry isn't in the config (e.g. the
    // provider was registered programmatically) the live disable still
    // sticks for this mcplocal lifetime; we just can't persist it.
    const persisted = setProviderDisabledInConfig(request.params.name, true);
    const status = (provider as ManagedVllmProvider).getStatus();
    reply.code(200).send({ name: provider.name, managed: true, persisted, ...status });
  });

  // Inverse of /disable: clear the flag both live and on disk. The
  // provider stays in the registry (we never removed it), so the next
  // chat triggers ensureRunning() normally.
  app.post<{ Params: { name: string } }>('/llm/providers/:name/enable', async (request, reply) => {
    const registry = deps.providerRegistry;
    const provider = registry?.get(request.params.name) ?? null;
    if (provider === null) {
      reply.code(404).send({ error: `Provider '${request.params.name}' not found` });
      return;
    }
    if (!('setDisabled' in provider) || typeof (provider as ManagedVllmProvider).setDisabled !== 'function') {
      reply.code(400).send({ error: `Provider '${request.params.name}' is not managed (no enable/disable surface)` });
      return;
    }
    (provider as ManagedVllmProvider).setDisabled(false);
    const persisted = setProviderDisabledInConfig(request.params.name, false);
    const status = (provider as ManagedVllmProvider).getStatus();
    reply.code(200).send({ name: provider.name, managed: true, persisted, ...status });
  });

  // ProxyModel discovery endpoints
  registerProxymodelEndpoint(app);

  // ProxyModel replay endpoint (stateless pipeline execution)
  registerReplayEndpoint(app, deps.providerRegistry);

  // Cache management endpoints
  registerCacheEndpoint(app);

  // Pause queue endpoints (live debugging)
  registerPauseEndpoint(app);

  // Proxy management routes to mcpd
  const mcpdClient = new McpdClient(config.mcpdUrl, config.mcpdToken);
  registerProxyRoutes(app, mcpdClient);

  // Traffic inspector
  const trafficCapture = new TrafficCapture();
  registerInspectEndpoint(app, trafficCapture);

  // Streamable HTTP MCP protocol endpoint at /mcp
  registerMcpEndpoint(app, deps.router);

  // Project-scoped MCP endpoint at /projects/:projectName/mcp
  registerProjectMcpEndpoint(app, mcpdClient, deps.providerRegistry, trafficCapture);

  return app;
}
