import { errorHandlerService } from "@batch-cooking/error-tools";
import { createErrorMiddleware, ExpressServer } from "@batch-cooking/express-tools";
import { ErrorCode } from "@batch-cooking/shared";
import type { Express, Request, Response } from "express";
import { env } from "./config/env.js";
import { errorLogger } from "./middlewares/error-logger.js";
import { requestLogger } from "./middlewares/request-logger.js";
import { adminRouter } from "./modules/admin/admin.routes.js";
import { authRouter } from "./modules/auth/auth.routes.js";
import { cookingSessionRouter } from "./modules/cooking-session/cooking-session.routes.js";
import { houseRouter } from "./modules/house/house.routes.js";
import { techStepWorkerRouter } from "./modules/internal/tech-step-worker.routes.js";
import { planningRouter } from "./modules/planning/planning.routes.js";
import { preferencesRouter } from "./modules/preferences/preferences.routes.js";
import { profileRouter } from "./modules/profile/profile.routes.js";
import { recipeRouter } from "./modules/recipe/recipe.routes.js";
import { referenceRouter } from "./modules/reference/reference.routes.js";
import { shoppingListRouter } from "./modules/shopping-list/shopping-list.routes.js";
import { sourcesRouter } from "./modules/sources/sources.routes.js";

/**
 * Builds the API's `ExpressServer`: standard middleware, routes, and the
 * final error handler, in that order. Returns the `ExpressServer` wrapper
 * (not just the raw Express app) so `server.ts` can call `.listen()` on
 * it — {@link createApp} below is the thinner entry point that exposes
 * just the raw `Express` instance, for test tooling (supertest) that
 * expects one.
 */
export function createServer(): ExpressServer {
  const server = new ExpressServer();
  // First middleware registered, before even setupCore's own (CORS/JSON
  // body parsing/cookies) — it only reads `req`/`res`, so it doesn't need
  // to run after them, and mounting it first means it wraps the *whole*
  // pipeline (its "finish" listener still fires for a request that never
  // makes it past CORS/body-parsing, not just ones that reach a route).
  server.addMiddleware(requestLogger);
  // One allowed origin: the app (`CORS_ORIGIN`). The admin surface
  // (`/admin/*`) is served by this same API and consumed by `apps/web`'s
  // own `/admin/*` routes — same origin as the rest of the app, so no
  // extra CORS entry.
  server.setupCore({ corsOrigin: env.CORS_ORIGIN });

  server.addRoute("get", "/health", (_req: Request, res: Response) => {
    res.status(200).json({ status: "ok" });
  });

  server.mountRouter("/auth", authRouter);
  // Admin application surface (`apps/web`'s `/admin/*` routes) — its own
  // auth (`requireAdmin`, distinct cookie/secret), never the end-user
  // session.
  server.mountRouter("/admin", adminRouter);
  server.mountRouter("/house", houseRouter);
  // Not user-facing — `services/tech-step-llm-worker` only, guarded by
  // `requireInternalWorker` on every route within (see that router's own
  // doc comment), never `requireAuth`. Mounted alongside the other routers
  // rather than nested under one of them since it isn't scoped to a single
  // recipe/step the way `recipeRouter`'s own correction routes are.
  server.mountRouter("/internal/tech-steps", techStepWorkerRouter);
  server.mountRouter("/planning", planningRouter);
  server.mountRouter("/preferences", preferencesRouter);
  server.mountRouter("/profile", profileRouter);
  server.mountRouter("/recipes", recipeRouter);
  server.mountRouter("/reference", referenceRouter);
  server.mountRouter("/shopping-list", shoppingListRouter);
  server.mountRouter("/cooking-session", cookingSessionRouter);
  server.mountRouter("/sources", sourcesRouter);

  // Serves the built frontend (production Docker image only — see
  // FRONTEND_DIST_DIR's doc comment in config/env.ts). Must come after
  // every API route above (so they always win) and before the catch-all
  // 404 below (so unmatched GETs fall through to the SPA's index.html
  // instead of a JSON 404).
  if (env.FRONTEND_DIST_DIR) {
    server.serveStaticFrontend(env.FRONTEND_DIST_DIR);
  }

  // No route matched — same shape as every other error response, via the
  // shared ErrorCode contract, so clients never special-case 404s.
  server.addMiddleware((_req: Request, res: Response) => {
    res.status(404).json({ code: ErrorCode.NOT_FOUND, message: "Not found" });
  });

  // Two error-handling middlewares in a row (Express runs them in
  // registration order, same as regular middleware) — errorLogger logs the
  // error, then hands it on (`next(err)`) to the real one: all the "what
  // status/body does this error map to" logic lives in ErrorHandlerService,
  // from @batch-cooking/error-tools — this stays a thin adapter.
  server.setErrorHandler(errorLogger);
  server.setErrorHandler(createErrorMiddleware(errorHandlerService));

  return server;
}

/**
 * Builds a fresh Express application instance (no shared mutable state
 * between calls — used both indirectly by the real server entrypoint
 * (`server.ts`, via {@link createServer}) and directly by tests, which
 * each get their own app via supertest).
 */
export function createApp(): Express {
  return createServer().instance;
}
