import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { CaslAbilityFactory } from '../../authorization/casl-ability.factory';
import { CaslAction, permissionCodeSubject } from '../../authorization/casl.constants';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
import { PERMISSION_KEY } from '../decorators/permission.decorator';
import { AUTHENTICATED_KEY } from '../decorators/authenticated.decorator';
import { CHECK_POLICIES_KEY } from '../../authorization/decorators/check-policies.decorator';
import type { AuthorizationRequest } from '../../authorization/interfaces';

/**
 * Permission guard — deny-by-default (security-critical).
 *
 * When a handler/controller has no @RequirePermission, @Authenticated,
 * @CheckPolicies, or @Public annotation, the guard denies access.
 *
 * Authorization is via CASL exact-code matching: each permission code
 * the user holds is registered as `Access PermissionCode:<code>`.
 * Checking `@RequirePermission('bill:export-excel')` verifies
 * `ability.can('access', 'PermissionCode:bill:export-excel')` — a user
 * with only `bill:view` will NOT pass.
 */
@Injectable()
export class PermissionGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private abilityFactory: CaslAbilityFactory,
  ) {}

  canActivate(context: ExecutionContext): boolean {
    // 1. @Public() exemption
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) return true;

    const request = context.switchToHttp().getRequest<AuthorizationRequest>();
    const user = request.user;

    // 2. Read @Authenticated, but only use it as a fallback after checking
    // more specific permission and policy declarations.
    const authenticatedOnly = this.reflector.getAllAndOverride<boolean>(AUTHENTICATED_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    // 3. Get required permissions (handler + class merged)
    const requiredPermissions = this.reflector.getAllAndMerge<string[]>(PERMISSION_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    // 4. Check for @CheckPolicies — defer to PoliciesGuard
    const hasPolicies = this.reflector.getAllAndMerge<unknown[]>(CHECK_POLICIES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    const hasCheckPolicies = Array.isArray(hasPolicies) && hasPolicies.length > 0;

    // 5. @Authenticated is a fallback only when no more specific authorization
    // declaration exists. This prevents controller-level @Authenticated from
    // bypassing handler-level @RequirePermission or @CheckPolicies.
    if (
      (!requiredPermissions || requiredPermissions.length === 0) &&
      !hasCheckPolicies &&
      authenticatedOnly
    ) {
      return !!user;
    }

    // No authorization declaration at any level → deny.
    if ((!requiredPermissions || requiredPermissions.length === 0) && !hasCheckPolicies) {
      return false;
    }

    // 6. @CheckPolicies present but no @RequirePermission → let PoliciesGuard handle it
    if ((!requiredPermissions || requiredPermissions.length === 0) && hasCheckPolicies) {
      return !!user; // deny unauthenticated, pass-through for PoliciesGuard
    }

    // 7. User must exist and have permissions array
    if (!user || !user.permissions || !Array.isArray(user.permissions)) return false;

    // 8. CASL exact-code check — maps each required code to an exact PermissionCode subject
    const ability = this.abilityFactory.createForUser(user);

    return requiredPermissions.some((code: string) =>
      ability.can(CaslAction.Access, permissionCodeSubject(code)),
    );
  }
}
