/**
 * The one server.
 *
 * One Fastify instance on `127.0.0.1:8431` serving `/api/v1/*`, behind Caddy,
 * with one systemd unit and one Caddy snippet. Three separate backends were
 * designed for this box — three ports, three frameworks, three deploy files that
 * would have overwritten each other — and this is the one that replaces them.
 * Weather, flights, markers and offices are route modules here, not services on
 * ports of their own. CONTRACT.md §5.
 *
 * `buildApp` never listens. Tests build one and use `inject()`; `index.ts` builds
 * one and binds it.
 */

import Fastify, { type FastifyInstance } from "fastify";
import { registerCachePolicy } from "./cache.ts";
import { loadConfig, type Config } from "./config.ts";
import { registerFlights } from "./routes/flights.ts";
import { registerHealth } from "./routes/health.ts";
import { registerMarkers } from "./routes/markers.ts";
import { registerOffices } from "./routes/offices.ts";
import { registerPresence } from "./routes/presence.ts";
import { registerSatellites } from "./routes/satellites.ts";
import { registerSession } from "./routes/session.ts";
import { registerWeather } from "./routes/weather.ts";
import { createServices } from "./services.ts";
import type { ErrorBody } from "../../src/server/wire.ts";

export function buildApp(config: Config = loadConfig()): FastifyInstance {
  const app = Fastify({
    logger: { level: config.logLevel },
    // Caddy is the only thing that talks to this socket, so its X-Forwarded-For
    // is the client address. Nothing else can reach the port to forge one.
    trustProxy: true,
  });

  // One loud line per demotion, at boot, before anything can be served. The
  // health body carries the same list for anyone without log access.
  for (const line of config.degraded) {
    app.log.warn(`TERA DEGRADED: ${line}`);
  }

  registerCachePolicy(app);
  registerCors(app, config.corsOrigins);

  const services = createServices(config, app.log);
  registerHealth(app, services);
  registerFlights(app, services);
  registerSatellites(app, services);
  registerWeather(app, services);
  registerMarkers(app, services);
  registerOffices(app, services);
  registerPresence(app, services);
  registerSession(app, services);

  app.setNotFoundHandler(async (_req, reply) => {
    const body: ErrorBody = { error: "not_found", message: "No such route." };
    return reply.code(404).send(body);
  });

  app.setErrorHandler(async (err, _req, reply) => {
    app.log.error({ err }, "unhandled error");
    const body: ErrorBody = { error: "internal", message: "Something went wrong." };
    return reply.code(500).send(body);
  });

  return app;
}

/**
 * CORS, by hand.
 *
 * The default is an empty allowlist — the browser build is served from the same
 * origin through Caddy, so nothing needs this until somebody runs the dev server
 * on a different port and sets `TERA_CORS_ORIGIN`. A plugin whose entire job is
 * fifteen lines of header-setting is not worth the dependency, and the fifteen
 * lines being visible here is worth something on a route that decides who may
 * read the API from where.
 */
function registerCors(app: FastifyInstance, allowed: string[]): void {
  if (allowed.length === 0) return;

  app.addHook("onRequest", async (req, reply) => {
    const origin = req.headers.origin;
    if (typeof origin !== "string" || !allowed.includes(origin)) return;

    reply.header("access-control-allow-origin", origin);
    reply.header("access-control-allow-headers", "authorization, content-type");
    reply.header("access-control-allow-methods", "GET, OPTIONS");
    // Whatever the response ends up being, it depended on the Origin header.
    reply.header("vary", "Origin");

    if (req.method === "OPTIONS") await reply.code(204).send();
  });
}
