import type { FunctionalRole, Principal, Privilege } from './authorization.types';

/**
 * Pure matchers used by the BFF route guards (and, in the future,
 * by SPA-side UI predicates) to evaluate a `Principal` against an
 * authorization requirement per [ADR-0025 §"Guard surface"](../../../../../docs/decisions/0025-authorization-model-privileges-roles-scopes.md).
 *
 * The matchers live in the shared lib so the projection contract
 * between BFF and SPA stays single-sourced — the SPA can pre-check
 * a privilege/role to decide whether to render a UI element with
 * the exact same logic the BFF guards apply server-side. The SPA
 * check is a UX optimisation (avoid rendering a button that 403s);
 * the BFF guard is the authoritative gate.
 *
 * **Composition.** Each matcher takes a list of required values;
 * a `Principal` matches when it holds **any** of them (OR). Decorator
 * stacking is AND-combined at the Nest level (separate guards run
 * sequentially), so the BFF can express
 * `@RequireRole('rh') @RequirePrivilege('Portal.Admin')` to mean
 * "RH **and** Admin"; within a single decorator the values are OR.
 */

export function principalHasAnyPrivilege(
  principal: Principal,
  required: ReadonlyArray<Privilege>,
): boolean {
  if (required.length === 0) {
    return true;
  }
  for (const want of required) {
    if (principal.privileges.includes(want)) {
      return true;
    }
  }
  return false;
}

export function principalHasAnyRole(
  principal: Principal,
  required: ReadonlyArray<FunctionalRole>,
): boolean {
  if (required.length === 0) {
    return true;
  }
  for (const want of required) {
    if (principal.roles.includes(want)) {
      return true;
    }
  }
  return false;
}

/**
 * Shape an `@RequireScope` handler hands to the matcher. Mirrors
 * the parentage chain of a `Person`-scoped resource without
 * pulling Prisma types into the shared lib — guards extract the
 * subset their resource exposes (a route operating on an
 * `Etablissement` populates `etablissementFiness`,
 * `delegationCode`, `regionCode`; one operating on a `Dossier`
 * populates the etablissement chain via the dossier's
 * `etablissement` parent; one operating on a `Person` populates
 * `personId` for the `self` rule).
 *
 * Every field is optional — the matcher iterates over the
 * principal's scopes and returns true on the first scope that
 * matches a present field. Missing parentage simply makes
 * coarser-grained scopes fail to match, which is the safe
 * direction (deny on doubt).
 */
export interface ScopableResource {
  /**
   * The resource's owner-`Person` id, matched against `self` scope.
   * Present only on resources owned by a single Person (a `Dossier`
   * the Person is the beneficiary of, a `NoteDeFrais` they
   * submitted). Absent on collective resources (an `Etablissement`,
   * a `Delegation`).
   */
  readonly personId?: string;
  /** FINESS code, matched against `etablissement:<finess>` scope. */
  readonly etablissementFiness?: string;
  /** French department code, matched against `delegation:<dept>` scope. */
  readonly delegationCode?: string;
  /** INSEE region code, matched against `region:<insee>` scope. */
  readonly regionCode?: string;
  /**
   * True for resources that belong to the national head office.
   * Matched against `siege` scope. Distinct from "lives in Paris" —
   * a Paris-based délégation is `delegation:75`, not `siege`.
   */
  readonly isSiege?: boolean;
}

/**
 * Helper for `@RequireScope`. Returns `true` if at least one of
 * the principal's scopes covers the resource per ADR-0025 §"Scope
 * expansion at check time". Containment rules:
 *
 *   - `unrestricted` covers every resource.
 *   - `self` covers a resource whose `personId` matches the
 *     principal's user — caller supplies `personId` when this
 *     comparison is meaningful for the route.
 *   - `etablissement:<finess>` covers a resource whose
 *     `etablissementFiness` equals the scope value.
 *   - `delegation:<dept>` covers a resource whose
 *     `delegationCode` equals the scope value.
 *   - `region:<insee>` covers a resource whose `regionCode`
 *     equals the scope value.
 *   - `siege` covers resources flagged `isSiege: true`.
 *
 * Empty scope list ⇒ no coverage (returns false). The BFF's
 * `PrincipalBuilder` always populates *something* (in v1, every
 * principal carries `[{ kind: 'unrestricted' }]` via the stub
 * resolver), but the matcher does not assume that — a future
 * Prisma-backed resolver returning an empty list for a user must
 * deny safely.
 */
export function principalCoversResource(principal: Principal, resource: ScopableResource): boolean {
  for (const scope of principal.scopes) {
    switch (scope.kind) {
      case 'unrestricted':
        return true;
      case 'self':
        if (resource.personId !== undefined && resource.personId === principal.user.personId) {
          return true;
        }
        break;
      case 'etablissement':
        if (
          resource.etablissementFiness !== undefined &&
          scope.value === resource.etablissementFiness
        ) {
          return true;
        }
        break;
      case 'delegation':
        if (resource.delegationCode !== undefined && scope.value === resource.delegationCode) {
          return true;
        }
        break;
      case 'region':
        if (resource.regionCode !== undefined && scope.value === resource.regionCode) {
          return true;
        }
        break;
      case 'siege':
        if (resource.isSiege === true) {
          return true;
        }
        break;
    }
  }
  return false;
}
