import { HttpError } from "@batch-cooking/error-tools";
import { ErrorCode, type SafeUserProfile } from "@batch-cooking/shared";
import type { NextFunction, Request, Response } from "express";
import { env } from "../config/env.js";
import { prisma } from "../db/prisma.js";
import { verifyAuthToken } from "../lib/jwt.js";
import { toSafeProfile } from "../lib/safe-profile.js";

/**
 * Shape of `res.locals` once {@link requireAuth} has run successfully. Type
 * a route handler's response as `Response<unknown, AuthLocals>` (see
 * `auth.routes.ts`'s `/me` handler) to read `res.locals.userProfile` fully
 * typed, no cast needed.
 */
export interface AuthLocals {
  /** The authenticated profile, resolved from the session cookie's JWT. */
  userProfile: SafeUserProfile;
}

/**
 * Express middleware guarding routes that require an authenticated
 * profile. Reads the session cookie, verifies the JWT, and re-checks
 * `tokenVersion` against the database — so a stateless JWT can still be
 * invalidated server-side (e.g. on password change / logout-everywhere,
 * once that feature exists) despite carrying no server-side session.
 *
 * On success, attaches the resolved profile to `res.locals.userProfile`
 * (typed via {@link AuthLocals}) for downstream handlers to use.
 * Deliberately `res.locals` rather than augmenting Express's global
 * `Request` type via `declare global`: `res.locals` is Express's own
 * built-in mechanism for exactly this (passing data from a middleware to
 * the next handler), typed per-route through a generic parameter — no
 * project-wide ambient augmentation silently changing every `Request` in
 * the codebase, whether or not it went through this middleware.
 *
 * @throws {HttpError} `401 NOT_AUTHENTICATED` for any failure — missing
 *   cookie, malformed/expired JWT, unknown profile, or stale tokenVersion.
 *   Never distinguishes the reason to the client.
 */
export async function requireAuth(
  req: Request,
  res: Response<unknown, AuthLocals>,
  next: NextFunction,
) {
  try {
    const token = req.cookies?.[env.AUTH_COOKIE_NAME];
    if (typeof token !== "string") {
      throw new HttpError(401, ErrorCode.NOT_AUTHENTICATED, "Not authenticated");
    }

    const payload = verifyAuthToken(token);
    const profile = await prisma.userProfile.findUnique({ where: { id: payload.userProfileId } });

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

    res.locals.userProfile = toSafeProfile(profile);
    next();
  } catch (err) {
    if (err instanceof HttpError) {
      next(err);
    } else {
      // Covers jwt.verify failures (expired/invalid/malformed token).
      next(new HttpError(401, ErrorCode.NOT_AUTHENTICATED, "Not authenticated"));
    }
  }
}
