import { HttpError } from "@batch-cooking/error-tools";
import { type AdminUserView, ErrorCode } from "@batch-cooking/shared";
import type { NextFunction, Request, Response } from "express";
import { env } from "../config/env.js";
import { prisma } from "../db/prisma.js";
import { verifyAdminToken } from "../lib/admin-jwt.js";
import { toSafeAdmin } from "../lib/safe-admin.js";

/**
 * Shape of `res.locals` once {@link requireAdmin} has run successfully.
 * Type a handler's response as `Response<unknown, AdminLocals>` to read
 * `res.locals.adminUser` fully typed, no cast — same `res.locals` (not
 * global `Request` augmentation) approach as {@link AuthLocals}
 * (`require-auth.ts`).
 */
export interface AdminLocals {
  /** The authenticated admin operator, resolved from the admin session cookie's JWT. */
  adminUser: AdminUserView;
}

/**
 * Express middleware guarding every `/admin/*` route — the operations UI
 * (`apps/web`'s `/admin/*` routes) authenticating as an `AdminUser`. Reads the admin
 * session cookie (`ADMIN_COOKIE_NAME`, deliberately **not** the same cookie
 * as end-user sessions), verifies the JWT against `ADMIN_JWT_SECRET`
 * (a different secret than `JWT_SECRET`), and re-checks `tokenVersion`
 * against the database so a stateless JWT can still be invalidated
 * server-side.
 *
 * A completely separate mechanism from {@link requireAuth}, not layered on
 * it: an end-user session token and an admin session token are never
 * interchangeable in either direction.
 *
 * Fails closed: an unset `ADMIN_JWT_SECRET` (the default for any instance
 * that doesn't run the admin app) makes {@link verifyAdminToken} throw, so
 * every request is rejected rather than the surface left open — same
 * posture as `requireInternalWorker`.
 *
 * @throws {HttpError} `401 NOT_AUTHENTICATED` for any failure — missing
 *   cookie, malformed/expired JWT, unknown admin, stale tokenVersion, or
 *   no secret configured. Never distinguishes the reason.
 */
export async function requireAdmin(
  req: Request,
  res: Response<unknown, AdminLocals>,
  next: NextFunction,
) {
  try {
    const token = req.cookies?.[env.ADMIN_COOKIE_NAME];
    if (typeof token !== "string") {
      throw new HttpError(401, ErrorCode.NOT_AUTHENTICATED, "Not authenticated");
    }

    const payload = verifyAdminToken(token);
    const admin = await prisma.adminUser.findUnique({ where: { id: payload.adminUserId } });

    if (!admin || admin.tokenVersion !== payload.tokenVersion) {
      throw new HttpError(401, ErrorCode.NOT_AUTHENTICATED, "Not authenticated");
    }

    res.locals.adminUser = toSafeAdmin(admin);
    next();
  } catch (err) {
    if (err instanceof HttpError) {
      next(err);
    } else {
      // Covers jwt.verify failures and the unset-secret throw from verifyAdminToken.
      next(new HttpError(401, ErrorCode.NOT_AUTHENTICATED, "Not authenticated"));
    }
  }
}
