import { Injectable } from '@nestjs/common';
import { AbilityBuilder, createMongoAbility, detectSubjectType, MongoAbility } from '@casl/ability';
import { CaslAction, mapPermissionCode, permissionCodeSubject } from './casl.constants';
import { AppAbility, AppSubject, AuthPrincipal } from './interfaces';

/**
 * Builds a CASL {@link AppAbility} instance for a given user.
 *
 * This factory is deliberately free of any NestJS execution-context
 * dependency so it can be reused outside of HTTP (e.g. Agent Tool
 * execution, background jobs, etc.).
 *
 * ## Two-layer ability model
 *
 * | Layer | Condition | Grant |
 * |---|---|---|
 * | Exact code | Every code in `user.permissions` | `Access PermissionCode:<code>` |
 * | Domain | Strict CRUD-equivalent codes only | `(create|read|update|delete) Subject` |
 * | Super admin | `isSuperAdmin === true` | `manage('all')` |
 *
 * Unknown/custom codes get only exact-code ability — no domain ability
 * is inferred.
 */
@Injectable()
export class CaslAbilityFactory {
  /**
   * Build ability for a user loaded from the database (or JWT-refreshed).
   */
  createForUser(user: AuthPrincipal): AppAbility {
    const { can, build } = new AbilityBuilder<MongoAbility<[CaslAction, AppSubject]>>(
      createMongoAbility,
    );

    // 1. Super admin → manage everything
    if (user.isSuperAdmin) {
      can(CaslAction.Manage, 'all');
      return build({ detectSubjectType });
    }

    // 2. For every permission code the user holds:
    //    a) Always add exact-code ability (layer 1)
    //    b) If code is known, also add domain-level ability (layer 2)
    for (const code of user.permissions ?? []) {
      // Layer 1 — exact code (always)
      can(CaslAction.Access, permissionCodeSubject(code));

      // Layer 2 — domain-level (known codes only)
      const rule = mapPermissionCode(code);
      if (rule) can(rule.action, rule.subject);
    }

    return build({ detectSubjectType });
  }

  /**
   * Create an ability from raw permission codes — useful in tests.
   */
  createFromPermissions(permissions: string[], isSuperAdmin = false): AppAbility {
    return this.createForUser({ permissions, isSuperAdmin });
  }
}
