import Fastify from "fastify";
import cors from "@fastify/cors";
import { ZodError } from "zod";
import { config as loadDotenv } from "dotenv";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

// Load `.env` from the repo root regardless of the cwd the process was
// started from. `import "dotenv/config"` would only look in `process.cwd()`,
// which silently breaks when the API is started from a sub-directory.
const __dirnameApi = dirname(fileURLToPath(import.meta.url));
loadDotenv({ path: resolve(__dirnameApi, "../../../.env") });

import { registerAuthRoutes } from "./auth/routes.js";
import { registerChatRoutes } from "./chat/routes.js";
import { openDatabase } from "./db/database.js";
import { migrate } from "./db/migrate.js";
import { registerDocsRoutes } from "./docs/routes.js";
import { envNumber } from "./env.js";
import { registerMcpRoutes } from "./mcp/routes.js";
import { registerModelRoutes } from "./models/routes.js";
import { registerN8nRoutes } from "./n8n/routes.js";
import { registerRagRoutes } from "./rag/routes.js";
import { registerSessionRoutes } from "./sessions/routes.js";
import { registerSkillRoutes } from "./skills/routes.js";
import { runWebhookAuditPurge, webhookAuditPurgeConfigFromEnv } from "./webhooks/audit.js";
import { registerWebhookRoutes } from "./webhooks/routes.js";
import { observeHttp, renderPrometheusText, snapshotMetrics } from "./metrics.js";

const port = envNumber(process.env.API_PORT, 8787);
const bodyLimit = envNumber(process.env.API_BODY_LIMIT_BYTES, 1_048_576);

const corsOrigin = () => {
  const configured = process.env.CORS_ALLOWED_ORIGINS;
  if (!configured) return true;

  const origins = configured
    .split(",")
    .map((origin) => origin.trim())
    .filter(Boolean);

  if (origins.includes("*")) return true;
  return origins;
};

const app = Fastify({
  logger: true,
  bodyLimit,
});

const db = openDatabase();
migrate(db);

await app.register(cors, {
  origin: corsOrigin(),
});

// Observability: track every request by route + method + status with duration.
// The route template (e.g. "/api/sessions/:id") is preferred over the raw URL
// so `/api/sessions/abc` and `/api/sessions/def` aggregate into the same bucket.
app.addHook("onResponse", async (request, reply) => {
  const route = request.routeOptions?.url ?? request.url ?? "unknown";
  observeHttp({
    route,
    method: request.method,
    status: reply.statusCode,
    durationMs: Math.round(performance.now() - (request as { sic_startedAt?: number }).sic_startedAt!),
    timestamp: Date.now(),
  });
});

app.addHook("onRequest", async (request) => {
  (request as { sic_startedAt?: number }).sic_startedAt = performance.now();
});

app.addHook("onSend", async (_request, reply, payload) => {
  reply.header("x-content-type-options", "nosniff");
  reply.header("referrer-policy", "no-referrer");
  reply.header("x-frame-options", "DENY");

  return payload;
});

app.setErrorHandler((error, _request, reply) => {
  const message = error instanceof Error ? error.message : String(error);

  if (error instanceof ZodError) {
    return reply.code(400).send({
      error: "validation_error",
      issues: error.issues,
    });
  }

  if (
    message.startsWith("auth_") ||
    message.startsWith("JWT") ||
    message.startsWith("JWKS")
  ) {
    return reply.code(401).send({ error: "unauthorized" });
  }

  app.log.error(error);
  return reply.code(500).send({ error: "internal_error" });
});

app.get("/healthz", async () => ({ status: "ok" }));
app.get("/readyz", async () => {
  db.prepare("SELECT 1").get();
  return { status: "ready" };
});

app.get("/api/version", async () => ({
  name: "pi-chat-api",
  version: "0.1.0",
}));

// Observability surface. `/metrics` returns Prometheus text (scraper-friendly);
// `/api/metrics` returns the same data as JSON for humans and the smoke test.
app.get("/metrics", async (_request, reply) => {
  reply.header("content-type", "text/plain; version=0.0.4; charset=utf-8");
  return renderPrometheusText();
});

app.get("/api/metrics", async () => snapshotMetrics());

await registerSessionRoutes(app, db);
await registerAuthRoutes(app);
await registerDocsRoutes(app);
await registerModelRoutes(app);
await registerRagRoutes(app);
await registerSkillRoutes(app);
await registerN8nRoutes(app);
await registerMcpRoutes(app);
await registerWebhookRoutes(app, db);
await registerChatRoutes(app, db);

// Audit retention: run once on boot, then on a timer. Cheap, idempotent.
const auditPurgeConfig = webhookAuditPurgeConfigFromEnv();
const initialPurge = runWebhookAuditPurge(db, auditPurgeConfig);
if (initialPurge.deletedByAge > 0 || initialPurge.deletedByCap > 0) {
  app.log.info(
    { ...initialPurge, config: auditPurgeConfig },
    "webhook audit purge (boot)",
  );
}

const auditPurgeIntervalMs = Math.max(60_000, envNumber(process.env.WEBHOOK_AUDIT_PURGE_INTERVAL_MS, 3_600_000));
const auditPurgeTimer = setInterval(() => {
  try {
    const report = runWebhookAuditPurge(db, auditPurgeConfig);
    if (report.deletedByAge > 0 || report.deletedByCap > 0) {
      app.log.info({ ...report, config: auditPurgeConfig }, "webhook audit purge (timer)");
    }
  } catch (error) {
    app.log.error({ err: error }, "webhook audit purge failed");
  }
}, auditPurgeIntervalMs);
auditPurgeTimer.unref?.();

const shutdown = async () => {
  app.log.info("shutdown requested");
  clearInterval(auditPurgeTimer);
  await app.close();
  db.close();
};

process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);

await app.listen({ port, host: "0.0.0.0" });
