import {
  CanActivate,
  ExecutionContext,
  ForbiddenException,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import type { Request } from 'express';
import { AuditWriter } from '../audit/audit.service';
import { readSessionPrincipal } from '../auth/principal-extractor';

/**
 * Single Entra app role that gates the entire `/api/admin/*` surface
 * per ADR-0020 §"Auth — `Portal.Admin` role claim". The value matches
 * the `value` field declared on the app role in the Entra app
 * registration manifest. Defined as a constant so spec assertions
 * can refer to the same source of truth.
 */
export const ADMIN_ROLE = 'Portal.Admin';

/**
 * `AdminRoleGuard` — enforces ADR-0020's role-based admin gate.
 *
 * Reads from `principal.privileges` per [ADR-0025 §"Guard surface"](../../../../docs/decisions/0025-authorization-model-privileges-roles-scopes.md)
 * — the migration thinned the implementation but kept the
 * `@RequireAdmin()` public API and the `admin.access_denied`
 * audit event type unchanged.
 *
 * Contract
 * --------
 * - **No session → 401.** The user is not authenticated at all; the
 *   SPA should kick off the login flow. We do not audit anonymous
 *   probes here because the route family is not a target — any
 *   unauthenticated 401 is normal traffic the absolute-timeout
 *   middleware would surface anyway.
 *
 * - **Session but missing `Portal.Admin` privilege → 403 + audit.**
 *   The user *is* authenticated; they just are not authorised
 *   for admin. This is the privilege-escalation attempt audit
 *   signal — every denial lands in `audit.events` with
 *   `outcome=denied`, the attempted route as `subject`, and the
 *   `Portal.*` privileges the principal did hold in the payload's
 *   `rolesHeld` field (kept named `rolesHeld` for backward
 *   compatibility with the existing audit shape; the values are
 *   privileges per ADR-0025's renaming of the axis). Per
 *   ADR-0013 §"Blocking writes": no audit ⇒ no action.
 *
 * - **Session with `Portal.Admin` privilege → pass through.**
 *   Downstream controllers see `req.session.user` *and*
 *   `req.session.principal` populated and can rely on the gate
 *   having happened.
 */
@Injectable()
export class AdminRoleGuard implements CanActivate {
  constructor(private readonly audit: AuditWriter) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest<Request>();
    const principal = readSessionPrincipal(req);

    if (principal === null) {
      throw new UnauthorizedException();
    }

    if (!principal.privileges.includes(ADMIN_ROLE)) {
      await this.audit.adminAccessDenied({
        actor: { oid: principal.user.entraOid },
        attemptedRoute: `${req.method} ${req.originalUrl}`,
        rolesHeld: [...principal.privileges],
      });
      throw new ForbiddenException();
    }

    return true;
  }
}
