import {
  CanActivate,
  ExecutionContext,
  ForbiddenException,
  Injectable,
  Logger,
  UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { ServiceCredentialValidatorService } from '../../services/service-credential-validator.service';
import { ClientJwtGuard } from '../../../auth/guards/client-jwt.guard';
import { UserJwtGuard } from '../../../auth/guards/user-jwt.guard';
import {
  ALLOW_AUTH_KEY,
  AllowedAuthType,
} from '../../decorators/allow-auth.decorator';
import { REQUIRE_SCOPES_KEY } from '../../decorators/require-scopes.decorator';
import {
  LEGACY_API_KEY_HEADER,
  SERVICE_KEY_HEADER,
} from '../../constants/service-credential.constants';

/** Matrix denials -> 403; everything else (unreachable, not_found, no-key) -> 401. */
const FORBIDDEN_REASONS = new Set([
  'route_denied',
  'method_denied',
  'scope_missing',
  'ip_denied',
  'origin_denied',
]);

@Injectable()
export class ServiceCredentialGuard implements CanActivate {
  private readonly logger = new Logger(ServiceCredentialGuard.name);

  constructor(
    private readonly validator: ServiceCredentialValidatorService,
    private readonly userJwtGuard: UserJwtGuard,
    private readonly clientJwtGuard: ClientJwtGuard,
    private readonly reflector: Reflector,
    private readonly configService: ConfigService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    if (!this.configService.get<boolean>('SERVICE_CREDENTIALS_ENABLED')) {
      throw new UnauthorizedException('Service credentials subsystem disabled');
    }

    const request = context.switchToHttp().getRequest<Request>();
    const allowed = this.allowedAuthTypes(context);
    const presentedKey = this.header(request, SERVICE_KEY_HEADER);

    // 1) Service-key path (always tried first when allowed and a key is present).
    if (allowed.includes('service_key') && presentedKey) {
      return this.validateServiceKey(context, request, presentedKey);
    }

    // 2) Legacy global x-api-key (only behind the flag; logged; not the primary path).
    if (allowed.includes('service_key') && this.tryLegacyApiKey(request)) {
      return true;
    }

    // 3) Fall through to JWT guards when the route accepts them (compose like MultiAuthGuard).
    const jwtResult = await this.tryJwtGuards(context, allowed);
    if (jwtResult) {
      return true;
    }

    throw new UnauthorizedException(
      'Acesso negado: nenhum método de autenticação válido encontrado.',
    );
  }

  private async validateServiceKey(
    context: ExecutionContext,
    request: Request,
    presentedKey: string,
  ): Promise<boolean> {
    const result = await this.validator.validate({
      presentedKey,
      route: this.matchedRoute(request),
      method: (request.method ?? 'GET').toUpperCase(),
      requiredScope: this.requiredScope(context),
      ip: request.ip ?? null,
      origin: this.header(request, 'origin') ?? null,
      correlationId: this.header(request, 'x-correlation-id') ?? null,
    });

    if (result.valid) {
      // Attach for downstream handlers (no canonical audit write — Laravel already did it).
      (request as Request & { serviceCredential?: unknown }).serviceCredential =
        result.credential;
      return true;
    }

    if (result.reason && FORBIDDEN_REASONS.has(result.reason)) {
      throw new ForbiddenException(`Service key denied: ${result.reason}`);
    }
    // not_found | revoked | expired | env_mismatch | validator_unreachable | invalid -> 401, fail-closed.
    throw new UnauthorizedException(
      `Service key rejected: ${result.reason ?? 'invalid'}`,
    );
  }

  /** Legacy global key compatibility. Constant-time compare; logged; flag-gated. */
  private tryLegacyApiKey(request: Request): boolean {
    if (!this.configService.get<boolean>('LEGACY_API_KEY_ENABLED')) {
      return false;
    }
    const presented = this.header(request, LEGACY_API_KEY_HEADER);
    const expected = this.configService.get<string>('API_KEY');
    if (!presented || !expected) {
      return false;
    }
    if (!ServiceCredentialGuard.timingSafeEqual(presented, expected)) {
      return false;
    }
    this.logger.warn(
      `legacy_api_key.used route=${this.matchedRoute(request)} method=${(request.method ?? '').toUpperCase()} ip=${request.ip ?? 'n/a'}`,
    );
    return true;
  }

  private async tryJwtGuards(
    context: ExecutionContext,
    allowed: AllowedAuthType[],
  ): Promise<boolean> {
    const guards: CanActivate[] = [];
    if (allowed.includes('user_jwt')) guards.push(this.userJwtGuard);
    if (allowed.includes('client_jwt')) guards.push(this.clientJwtGuard);

    for (const g of guards) {
      try {
        if (await g.canActivate(context)) {
          return true;
        }
      } catch {
        // try the next mechanism (mirrors MultiAuthGuard)
      }
    }
    return false;
  }

  private allowedAuthTypes(context: ExecutionContext): AllowedAuthType[] {
    const types = this.reflector.getAllAndOverride<AllowedAuthType[]>(
      ALLOW_AUTH_KEY,
      [context.getHandler(), context.getClass()],
    );
    return types && types.length > 0 ? types : ['service_key'];
  }

  private requiredScope(context: ExecutionContext): string | null {
    const scopes = this.reflector.getAllAndMerge<string[]>(REQUIRE_SCOPES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    return scopes && scopes.length > 0 ? scopes[0] : null;
  }

  /**
   * The path the matrix is checked against. With no global prefix in this app,
   * `request.route.path` is the bare controller path (e.g. `/audit-events`).
   * Fallback strips the query string from `path`/`url`.
   */
  private matchedRoute(request: Request): string {
    const routePath = (request as Request & { route?: { path?: string } }).route
      ?.path;
    if (typeof routePath === 'string' && routePath.length > 0) {
      return routePath;
    }
    const raw = request.path ?? request.url ?? '';
    const q = raw.indexOf('?');
    return q === -1 ? raw : raw.slice(0, q);
  }

  private header(request: Request, name: string): string | undefined {
    const value = request.headers?.[name];
    return typeof value === 'string' ? value : undefined;
  }

  private static timingSafeEqual(a: string, b: string): boolean {
    // Length check first; then constant-time over equal-length buffers (replaces S6's `!==`).

    const { timingSafeEqual, createHash } =
      require('crypto') as typeof import('crypto');
    const ha = createHash('sha256').update(a).digest();
    const hb = createHash('sha256').update(b).digest();
    return timingSafeEqual(ha, hb);
  }
}
