/**
 * Authorization catalogues + types per
 * [ADR-0025](../../../../../docs/decisions/0025-authorization-model-privileges-roles-scopes.md).
 *
 * The three axes are independent: a `Principal` carries a list per
 * axis, and guards consume the structured shape. Privileges gate
 * portal *surfaces* (admin app, audit viewer); functional roles
 * encode *what work the person does*; scopes encode *where the role
 * applies*. Composition happens at guard time, not at sign-in —
 * sign-in just builds the three lists.
 *
 * The catalogues are **closed**: every literal that appears in a
 * `@RequirePrivilege` / `@RequireRole` / `@RequireScope` call must
 * be a member of the corresponding constant array. A CI drift gate
 * (next PR per ADR-0025 §"More Information") enforces that contract
 * against the source tree.
 */

/**
 * Portal-level capabilities, materialised as Entra app roles on the
 * BFF's app registration. Each privilege gates a portal surface —
 * never a business operation in isolation. The four entries here
 * are the entire v1 set; adding a fifth is an ADR-0025 amendment.
 *
 * The exact strings are the `value` field of the Entra app-role
 * manifest and are what Entra emits in the ID-token `roles` claim.
 */
export const PRIVILEGES = [
  'Portal.Admin',
  'Portal.Auditor',
  'Portal.SecurityOfficer',
  'Portal.DPO',
] as const;

export type Privilege = (typeof PRIVILEGES)[number];

/**
 * Functional roles, materialised as Entra security groups named
 * `apf-role-<slug>`. The slug is what the BFF and SPA reason about;
 * the Entra group GUID is tenant-private and resolved to a slug at
 * the boundary (see `entra-group-to-role.ts`).
 *
 * Grouped here by APF org-chart category (workforce / governance /
 * volunteer / external) for readability, but the union below is
 * flat — guards don't care about the category.
 */
export const FUNCTIONAL_ROLES = [
  // Workforce (15) — employees on payroll.
  'collaborateur',
  'chef-equipe',
  'chef-service',
  'directeur-etablissement',
  'directeur-territorial',
  'rh',
  'responsable-paie',
  'comptable',
  'juriste',
  'dpo',
  'rssi',
  'it',
  'formation',
  'qualite',
  'communication',
  // Governance (6) — élus + local delegates.
  'elu-ca',
  'elu-cd',
  'elu-cd-president',
  'elu-cd-tresorier',
  'elu-cd-secretaire',
  'delegue',
  // Volunteer (2) — bénévoles with portal access.
  'benevole',
  'benevole-responsable',
  // External (1) — placeholder, no consumer surface yet in v1.
  'partenaire',
] as const;

export type FunctionalRole = (typeof FUNCTIONAL_ROLES)[number];

/**
 * Scope kinds — the six discriminants of `Scope`. Three carry a
 * value (`etablissement:<finess>`, `delegation:<dept>`,
 * `region:<insee>`); three are valueless (`self`, `siege`,
 * `unrestricted`).
 *
 * Adding a kind is an ADR-0025 amendment. Adding a value (a new
 * établissement opening, a new délégation) is a data operation and
 * does not touch this file.
 */
export const SCOPE_KINDS = [
  'self',
  'etablissement',
  'delegation',
  'region',
  'siege',
  'unrestricted',
] as const;

export type ScopeKind = (typeof SCOPE_KINDS)[number];

/**
 * A scope on a principal. Three kinds carry a tenant-specific
 * value, three do not. Discriminated union so consumers can
 * exhaustively switch on `kind`.
 *
 * `value` semantics per kind:
 *   - `etablissement`: APE / FINESS code (`9` characters, stable
 *     across reorgs).
 *   - `delegation`: French department code (`2` characters,
 *     INSEE / postal — `'33'`, `'2A'`, `'971'`).
 *   - `region`: INSEE region code (`2` characters).
 */
export type Scope =
  | { readonly kind: 'self' }
  | { readonly kind: 'etablissement'; readonly value: string }
  | { readonly kind: 'delegation'; readonly value: string }
  | { readonly kind: 'region'; readonly value: string }
  | { readonly kind: 'siege' }
  | { readonly kind: 'unrestricted' };

/**
 * The session-resident shape consumed by route guards. Built once
 * at sign-in by the BFF's OIDC callback, persisted as part of the
 * session payload, refreshed on every authenticated request from
 * the session ID. Guards never re-derive it from claims at request
 * time — that would re-do the Entra-group-to-slug mapping on every
 * call.
 *
 * `amr` and `mfaVerifiedAt` live on the principal (not separately
 * on the session) so the AI-service projector and any guard that
 * wants to consult MFA freshness reads a single, coherent object.
 *
 * `user.id` and `user.personId` will become real portal UUIDs once
 * the `User` + `Person` schema (proposed ADR-0026) lands. Until
 * then the BFF-side builder populates both with the Entra `oid`
 * as a stable placeholder so consumers can already key on a
 * "portal identifier" without branching on availability.
 */
export interface Principal {
  readonly user: {
    readonly id: string;
    readonly personId: string;
    readonly entraOid: string;
    readonly tenantId: string;
    readonly displayName: string;
  };
  readonly privileges: ReadonlyArray<Privilege>;
  readonly roles: ReadonlyArray<FunctionalRole>;
  readonly scopes: ReadonlyArray<Scope>;
  readonly amr: ReadonlyArray<string>;
  readonly mfaVerifiedAt?: number;
}

/* ---------------------------------------------------------------- *
 * Membership type-guards — runtime helpers callers use to validate
 * arbitrary strings (claim values, config map values, drift-gate
 * findings) against the closed catalogues. Returning a type
 * predicate makes the post-check string narrow to the catalogue
 * type, which keeps consumers honest about the closed-set rule.
 * ---------------------------------------------------------------- */

const PRIVILEGE_SET: ReadonlySet<string> = new Set(PRIVILEGES);
const FUNCTIONAL_ROLE_SET: ReadonlySet<string> = new Set(FUNCTIONAL_ROLES);
const SCOPE_KIND_SET: ReadonlySet<string> = new Set(SCOPE_KINDS);

export function isPrivilege(value: string): value is Privilege {
  return PRIVILEGE_SET.has(value);
}

export function isFunctionalRole(value: string): value is FunctionalRole {
  return FUNCTIONAL_ROLE_SET.has(value);
}

export function isScopeKind(value: string): value is ScopeKind {
  return SCOPE_KIND_SET.has(value);
}
