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

/**
 * `RequireRoleGuard` — enforces ADR-0025 functional-role gates.
 *
 * Same contract as `RequirePrivilegeGuard` with the role axis as
 * the source of truth: 401 if anonymous, 403 + audit if
 * authenticated but missing every required role, pass otherwise.
 * Multiple roles on the decorator are OR-combined; stacking with
 * other decorators is AND-combined at the Nest layer.
 */
@Injectable()
export class RequireRoleGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    private readonly audit: AuditWriter,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const required = this.reflector.getAllAndOverride<readonly FunctionalRole[]>(
      REQUIRE_ROLE_METADATA,
      [context.getHandler(), context.getClass()],
    );
    if (required === undefined || required.length === 0) {
      return true;
    }

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

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

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