import type { Request } from 'express';
import type { Principal, Scope } from 'shared-auth';

/**
 * Reads the authorization `Principal` off the express session,
 * coalescing the legacy session-shape carryover so a session that
 * was minted before the ADR-0025 implementation landed still works
 * for the duration of its 12 h absolute TTL.
 *
 * Returns `null` when no session is present at all (the guard
 * should answer 401 — the user is anonymous). Returns a
 * synthesised `Principal` derived from `session.user` when the
 * session is authenticated but predates the `principal` field —
 * the synthesised principal carries the `Portal.*` privileges
 * from the legacy `roles` claim, no functional roles (the
 * `groups` claim was unconfigured at the time), and an
 * `unrestricted` scope. After every persisted session has cycled
 * through the absolute-timeout window post-deploy, this fallback
 * is dead code.
 */
export function readSessionPrincipal(req: Request): Principal | null {
  const session = req.session;
  if (session === undefined) {
    return null;
  }

  if (session.principal !== undefined) {
    return session.principal;
  }

  const user = session.user;
  if (user === undefined) {
    return null;
  }

  // Legacy session bridge. Filter `user.roles` through a generic
  // `Portal.*` heuristic rather than re-importing `isPrivilege`
  // here — the file's whole purpose is graceful degradation; if
  // the legacy claim carries a non-catalogue Portal.X the matcher
  // downstream just returns false on it.
  const portalPrivileges = user.roles.filter((r) => r.startsWith('Portal.'));
  const fallback: Principal = {
    user: {
      id: user.oid,
      personId: user.oid,
      entraOid: user.oid,
      tenantId: user.tid,
      displayName: user.displayName,
    },
    privileges: portalPrivileges as Principal['privileges'],
    roles: [],
    scopes: [{ kind: 'unrestricted' } satisfies Scope],
    amr: user.amr,
  };
  return fallback;
}

/**
 * Format a scope list as the `string[]` shape the
 * `AuthorizationDeniedInput.held` audit field expects:
 * `unrestricted` / `self` / `siege` ride as-is;
 * `etablissement:<finess>` / `delegation:<dept>` / `region:<insee>`
 * concatenate kind and value. Mirrors `PrincipalProjector` in
 * spirit but is independent (the audit module must not depend on
 * the AI-bridge projector — different surfaces).
 */
export function scopesToAuditStrings(scopes: ReadonlyArray<Scope>): string[] {
  return scopes.map((s) => {
    if (s.kind === 'etablissement' || s.kind === 'delegation' || s.kind === 'region') {
      return `${s.kind}:${s.value}`;
    }
    return s.kind;
  });
}
