import Fastify from 'fastify';
import '@fastify/websocket';
import { loadConfig } from './config.js';
import { getSql, applySchema, pingDb, waitForTable } from './db.js';
import { createFleetState, ensureHostState } from "./services/fleet-state.js";
import { registerControlWebSocket } from './routes/ws.js';
import { startFleetConnector } from "./services/fleet-connector.js";
import { buildRetentionConfig, runRollup, pruneRawSamples, pruneActivity, pruneModelEvents } from './services/retention.js';
import { ActionQueue } from './services/action-queue.js';
import { LogRelay } from './services/log-relay.js';
import { registerActionRoutes } from './routes/actions.js';
import { registerCaptureRoutes } from './routes/captures.js';
import { registerBenchRoutes, setBenchApp } from './routes/bench.js';
import { registerPlaygroundRoutes } from './routes/playground.js';
import { registerEvalRoutes } from './routes/evals.js';
import { registerRoutingRoutes } from './routes/routing.js';
import { registerReportRoutes, startReportScheduler } from './routes/reports.js';
import { registerGatewayRoutes } from './routes/gateway.js';
import { registerPolicyRoutes } from './routes/policies.js';
import { registerSshConfigRoutes } from './routes/ssh-config.js';
import { loadLlamaProviders } from "./services/llama-providers.js";
import { GATEWAY_KIND } from "@boocode/contracts/gateway";
import { createDeltaEmitter } from "./services/delta-emitter.js";
import type { AppContext } from './app-context.js';
export type { DeltaEmitter } from './services/delta-emitter.js';
import { handleLlamaSweepEvent } from './services/sse-pipeline.js';
import { pollPerformance } from './services/perf-poller.js';
import { rebuildFleetFromDB } from './services/fleet-rebuild.js';

async function main() {
  const config = loadConfig();
  const app = Fastify({ logger: { level: config.LOG_LEVEL } });

  app.removeContentTypeParser(['application/json']);
  app.addContentTypeParser('application/json', { parseAs: 'string' }, (_req: unknown, body: unknown, done: (err: Error | null, body: unknown) => void) => {
    const str = (body as string) ?? '';
    if (str.trim().length === 0) {
      done(null, {});
      return;
    }
    try {
      done(null, JSON.parse(str));
    } catch (err) {
      done(err as Error, undefined);
    }
  });

  const sql = getSql(config);

  // Startup ordering guard: wait for server-owned tables before applying schema.
  await waitForTable(sql, 'sessions', 30_000);
  await applySchema(sql);
  app.log.info('database schema applied');

  // Register WebSocket endpoint.
  const fleet = createFleetState();
  const emitter = createDeltaEmitter();

  // P2: Action queue + log relay
  const actionQueue = new ActionQueue();
  const logRelay = new LogRelay();
  const ctx: AppContext = { sql, config, fleet, emitter, actionQueue, logRelay };
  registerControlWebSocket(app, ctx);
  registerActionRoutes(app, ctx);
  registerCaptureRoutes(app, sql);
  setBenchApp(app.log);
  registerBenchRoutes(app, ctx);
  registerPlaygroundRoutes(app);
  registerEvalRoutes(app, ctx);
  registerRoutingRoutes(app, sql, fleet);
  registerReportRoutes(app, sql);
  registerGatewayRoutes(app, ctx);
  registerPolicyRoutes(app, sql);
  registerSshConfigRoutes(app, ctx);

  // Health endpoint.
  app.get('/api/health', async (_req: unknown, reply: import('fastify').FastifyReply) => {
    const dbOk = await pingDb(sql);
    const status = dbOk ? 200 : 503;
    return reply.status(status).send({
      ok: dbOk,
      db: dbOk,
    });
  });

  // Rebuild fleet state from DB on startup (A1/F2 fix).
  await rebuildFleetFromDB(fleet, sql).catch((err) => {
    app.log.warn({ err: (err as Error).message }, 'fleet: rebuild from DB failed');
  });

  // Load the provider registry — baseUrl comes from the registry, never from ssh_host.
  const registry = loadLlamaProviders(config.LLAMA_PROVIDERS_PATH, config.LLAMA_SWAP_URL);
  app.log.info({ count: registry.providers.length }, 'fleet: provider registry loaded');

  const fleetProviders = registry.providers.filter((p) => p.kind !== GATEWAY_KIND);

  // JOIN registry providers with control_hosts for the enabled flag.
  // Insert a control_hosts row ON CONFLICT DO NOTHING for any registry provider
  // missing one, so the fleet state has a row to key off.
  const enabledHosts = await sql<{ provider_id: string; enabled: boolean }[]>`
    SELECT provider_id, enabled FROM control_hosts
    WHERE provider_id = ANY(${fleetProviders.map((p) => p.id)}::text[])
  `;
  const enabledMap = new Map<string, boolean>();
  for (const row of enabledHosts) {
    enabledMap.set(row.provider_id, row.enabled);
  }

  // Seed missing control_hosts rows so the registry is the source of truth.
  for (const provider of fleetProviders) {
    if (!enabledMap.has(provider.id)) {
      await sql`
        INSERT INTO control_hosts (provider_id, enabled)
        VALUES (${provider.id}, true)
        ON CONFLICT (provider_id) DO NOTHING
      `;
      enabledMap.set(provider.id, true);
    }
  }

  const abortControllers = new Map<string, AbortController>();

  for (const provider of fleetProviders) {
    const enabled = enabledMap.get(provider.id) ?? true;
    if (!enabled) continue;

    const baseUrl = provider.baseUrl;

    // P2: Register host with action queue
    actionQueue.registerHost(provider.id, {
      baseUrl,
      isLivenessUp: () => {
        const hs = fleet.hosts.get(provider.id);
        return hs?.liveness !== 'down';
      },
      isInflightRequests: () => {
        // Host-level total from the SSE inflight event (per-model is not published).
        return fleet.hosts.get(provider.id)?.inflightTotal ?? 0;
      },
      log: app.log,
    });

    const abort = startFleetConnector(provider.id, baseUrl, {
      isUp: () => true,
      sql,
      log: app.log,
      onEvent: (pid, event) => handleLlamaSweepEvent(fleet, sql, config, pid, emitter, event, logRelay),
      onReconnectGiveUp: async (pid) => {
        const state = ensureHostState(fleet, pid);
        state.liveness = 'down';
      },
      sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
    });
    abortControllers.set(provider.id, abort);
  }

  // Perf poller: 5s interval per enabled provider — baseUrl from registry.
  const pollTimer = setInterval(async () => {
    for (const provider of fleetProviders) {
      const enabled = enabledMap.get(provider.id) ?? true;
      if (!enabled) continue;
      await pollPerformance(sql, config, provider.id, provider.baseUrl, fleet, emitter);
    }
  }, 5_000);

  // Retention job: daily timer — iterate registry providers.
  const retentionConfig = buildRetentionConfig(config);
  const retentionTimer = setInterval(async () => {
    // Per-provider work: rollup + raw-sample prune (both scoped to provider_id).
    for (const provider of fleetProviders) {
      const enabled = enabledMap.get(provider.id) ?? true;
      if (!enabled) continue;
      await runRollup(sql, provider.id, retentionConfig.rawHours);
      await pruneRawSamples(sql, provider.id, retentionConfig.rawHours);
    }
    // Global prunes (no provider_id filter) run ONCE, not once per provider.
    await pruneActivity(sql, retentionConfig.rawHours);
    await pruneModelEvents(sql, retentionConfig.rollupDays * 24);
  }, 24 * 3600_000); // daily

  // P6.2: Report digest scheduler (catch-up on boot, then hourly).
  const stopReportScheduler = startReportScheduler(sql, app.log);

  app.addHook('onClose', async () => {
    clearInterval(pollTimer);
    clearInterval(retentionTimer);
    stopReportScheduler();
    for (const abort of abortControllers.values()) {
      abort.abort();
    }
  });

  // Graceful shutdown.
  const shutdown = async () => {
    app.log.info('shutting down');
    await app.close();
    await sql.end({ timeout: 5 });
    process.exit(0);
  };
  process.on('SIGTERM', shutdown);
  process.on('SIGINT', shutdown);

  await app.listen({ port: config.PORT, host: config.HOST });
  app.log.info(`BooControl listening on ${config.HOST}:${config.PORT}`);
}

// P2 exports for tests
export { ActionQueue } from './services/action-queue.js';
export { LogRelay } from './services/log-relay.js';

// P3 exports for tests
export { runSingleBenchRequest, parseLlamaTimings, computeAggregates } from './services/bench-engine.js';
export { computeRegressionFlag } from './services/bench-engine.js';

// P5 exports for tests
export { loadEvalSuitesFromData } from './services/eval-suites.js';
export { runCodeEval } from './services/sandbox-runner.js';

if (!process.env.VITEST) {
  main().catch((err) => {
    console.error('fatal:', err);
    process.exit(1);
  });
}
