import {
  CanActivate,
  ExecutionContext,
  ForbiddenException,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import type { Request } from 'express';
import { type Privilege, principalHasAnyPrivilege } from 'shared-auth';
import { AuditWriter } from '../audit/audit.service';
import { readSessionPrincipal } from './principal-extractor';
import { REQUIRE_PRIVILEGE_METADATA } from './require-privilege.decorator';

/**
 * `RequirePrivilegeGuard` — enforces ADR-0025 privilege gates.
 *
 * Contract (matches `AdminRoleGuard` for symmetry):
 *
 * - **No session → 401 `unauthenticated`.** Anonymous traffic is
 *   noise; no audit row, the SPA kicks off the login flow.
 *
 * - **Session but missing every required privilege → 403 + audit.**
 *   The user *is* authenticated; they just are not authorised.
 *   Audit row: `auth.authorization_denied`, `kind=privilege`,
 *   `required` = what the route asked for, `held` = what the
 *   principal actually carries.
 *
 * - **Session with at least one required privilege → pass.**
 *
 * The structured error envelope per [ADR-0021](../../../../docs/decisions/0021-phase-2-security-baseline.md):
 * the body is `{ error: { code: 'forbidden', message, traceId } }`
 * with a generic message — no role / privilege hint, so an
 * attacker cannot use 403 responses to enumerate which privilege
 * a route gates.
 */
@Injectable()
export class RequirePrivilegeGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    private readonly audit: AuditWriter,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const required = this.reflector.getAllAndOverride<readonly Privilege[]>(
      REQUIRE_PRIVILEGE_METADATA,
      [context.getHandler(), context.getClass()],
    );
    if (required === undefined || required.length === 0) {
      // No metadata = guard wired but route un-annotated. Treat as
      // pass-through rather than guess at the intent — the decorator
      // is what carries the privilege list.
      return true;
    }

    const req = context.switchToHttp().getRequest<Request>();
    const principal = readSessionPrincipal(req);
    if (principal === null) {
      throw new UnauthorizedException({
        code: 'unauthenticated',
        message: 'Unauthenticated',
      });
    }

    if (principalHasAnyPrivilege(principal, required)) {
      return true;
    }

    await this.audit.authorizationDenied({
      actor: { oid: principal.user.entraOid },
      attemptedRoute: `${req.method} ${req.originalUrl}`,
      kind: 'privilege',
      required,
      held: [...principal.privileges],
    });
    throw new ForbiddenException({
      code: 'forbidden',
      message: 'Forbidden',
    });
  }
}
