#!/usr/bin/env node
/**
 * HTTP-only entry for the containerized mcplocal (deployed behind Ingress as `mcp.ad.itaz.eu`).
 *
 * Differences from main.ts (the STDIO/systemd entry):
 *   - No StdioProxyServer (there's no stdin/stdout MCP client in a pod).
 *   - No `--upstream` flag (upstreams come from mcpd project discovery).
 *   - Host + port from env (MCPLOCAL_HTTP_HOST / MCPLOCAL_HTTP_PORT).
 *   - Requires MCPLOCAL_MCPD_URL to point at mcpd inside the cluster.
 *   - Registers a token-auth preHandler on `/projects/*` and `/mcp`.
 *   - FileCache directory honours MCPLOCAL_CACHE_DIR (wired via project-mcp-endpoint).
 *
 * Identity model: **the pod has no persistent identity to mcpd.** Every
 * inbound request's `Authorization: Bearer mcpctl_pat_…` is forwarded
 * verbatim for all downstream mcpd calls (introspect + project
 * discovery). mcpd's auth middleware dispatches on the `mcpctl_pat_`
 * prefix and resolves the McpToken principal. As a result there is
 * deliberately no MCPLOCAL_MCPD_TOKEN env var — adding one would only
 * create a rotation problem for a state we don't need.
 */
import { McpRouter } from './router.js';
import { createHttpServer } from './http/server.js';
import { loadHttpConfig, loadLlmProviders } from './http/config.js';
import { createProvidersFromConfig } from './llm-config.js';
import { createSecretStore } from '@mcpctl/shared';
import { reloadStages, startWatchers, stopWatchers } from './proxymodel/watcher.js';
import { createTokenAuthMiddleware } from './http/token-auth.js';

function requireEnv(name: string): string {
  const value = process.env[name];
  if (value === undefined || value === '') {
    throw new Error(`Required env var ${name} is not set`);
  }
  return value;
}

export async function serve(): Promise<void> {
  const mcpdUrl = requireEnv('MCPLOCAL_MCPD_URL');
  const httpHost = process.env.MCPLOCAL_HTTP_HOST ?? '0.0.0.0';
  const httpPort = Number(process.env.MCPLOCAL_HTTP_PORT ?? '3200');
  if (!Number.isFinite(httpPort) || httpPort <= 0) {
    throw new Error(`Invalid MCPLOCAL_HTTP_PORT: ${process.env.MCPLOCAL_HTTP_PORT}`);
  }
  // MCPLOCAL_CACHE_DIR is optional; FileCache reads it directly.
  const cacheDir = process.env.MCPLOCAL_CACHE_DIR;

  // loadHttpConfig reads user-level config.json; we override with env.
  const baseConfig = loadHttpConfig();
  const httpConfig = {
    ...baseConfig,
    httpHost,
    httpPort,
    mcpdUrl,
  };

  // LLM providers (configured via mounted ConfigMap at ~/.mcpctl/config.json or env).
  const llmEntries = loadLlmProviders();
  const secretStore = await createSecretStore();
  const providerRegistry = await createProvidersFromConfig(llmEntries, secretStore);

  process.stderr.write(
    `mcplocal-serve: mcpd=${mcpdUrl} host=${httpHost} port=${httpPort} cache=${cacheDir ?? '~/.mcpctl/cache'}\n`,
  );

  const router = new McpRouter();

  const httpServer = await createHttpServer(httpConfig, { router, providerRegistry });

  // Auth preHandler: only protect the MCP surfaces. /health, /healthz, /proxymodels etc stay open.
  // Introspection cache TTLs are tunable via env for operators who want a different tradeoff.
  // Default 5s for both: mcpd's introspection endpoint is a single DB lookup, so the cache
  // mainly protects against burst restart storms — not steady-state load. A higher positive
  // TTL means revoked tokens keep working for the full window after revocation; 5s aligns with
  // the negativeTtl and matches mcptoken.smoke's 7s `wait after revoke` assertion.
  const positiveTtlMs = Number(process.env.MCPLOCAL_TOKEN_POSITIVE_TTL_MS ?? '5000');
  const negativeTtlMs = Number(process.env.MCPLOCAL_TOKEN_NEGATIVE_TTL_MS ?? '5000');
  const tokenAuth = createTokenAuthMiddleware({ mcpdUrl, positiveTtlMs, negativeTtlMs });
  httpServer.addHook('preHandler', async (request, reply) => {
    const url = request.url;
    if (!url.startsWith('/projects/') && !url.startsWith('/mcp')) return;
    await tokenAuth(request, reply);
  });

  await httpServer.listen({ port: httpPort, host: httpHost });
  process.stderr.write(`mcplocal-serve listening on ${httpHost}:${httpPort}\n`);

  // Hot-reload proxymodel stages from ~/.mcpctl/stages (same as main.ts).
  await reloadStages();
  startWatchers();

  let shuttingDown = false;
  const shutdown = async () => {
    if (shuttingDown) return;
    shuttingDown = true;
    stopWatchers();
    providerRegistry.disposeAll();
    await httpServer.close();
    await router.closeAll();
    process.exit(0);
  };
  process.on('SIGTERM', () => void shutdown());
  process.on('SIGINT', () => void shutdown());
}

const isMain =
  process.argv[1]?.endsWith('serve.js') ||
  process.argv[1]?.endsWith('serve.ts');

if (isMain) {
  serve().catch((err) => {
    process.stderr.write(`Fatal: ${err}\n`);
    process.exit(1);
  });
}
