/**
 * Public type surface for the audit module — kept narrow so the
 * generated Prisma types do not leak through the rest of the BFF.
 */

export type AuditAudience = 'workforce' | 'customer';
export type AuditOutcome = 'success' | 'failure' | 'denied';

export interface AuditEventInput {
  /**
   * Free-form categorical identifier. Convention: lower-case dotted
   * verb-on-noun, e.g. `auth.login`, `dossier.update`. We keep it
   * free-form in v1; the catalogue is formalised when we have N
   * stable event types in production.
   */
  eventType: string;

  audience: AuditAudience;
  outcome: AuditOutcome;

  /**
   * What the event is about — typically a domain entity URI like
   * `user:42` or `dossier:xyz`. Optional when the event is
   * system-wide and has no clear subject.
   */
  subject?: string;

  /**
   * Event-specific structured detail. **Caller-redacted PII** —
   * the audit module does not run a redact pass; it is the writer's
   * responsibility. Mirror of the Pino redact list in scope.
   */
  payload?: Record<string, unknown>;

  /**
   * Override of the auto-resolved actor id hash. Normally left
   * undefined — the service reads from the CLS context (set by
   * future auth interceptors per ADR-0009 / ADR-0010). Pass this
   * only when emitting events from a context where CLS is not
   * populated (background jobs, future cron).
   */
  actorIdHash?: string;
}

/**
 * Identity payload accepted by the typed audit methods. The raw
 * Entra `oid` is hashed by `AuditWriter` itself so the salt stays
 * inside the module. `amr` is captured for the sign-in record.
 */
export interface SignInActor {
  oid: string;
  amr: readonly string[];
}

export interface SignInFailedInput {
  failureKind: string;
  /**
   * Optional — pass only when the identity was actually resolved
   * before the rejection (rare). Most failure paths (state
   * mismatch, expired flow, token-exchange error) reject before any
   * user claim is parsed and should leave `actor` undefined.
   */
  actor?: { oid: string };
  /**
   * Extra fields to merge into the audit row's payload alongside
   * `failureKind`. PII is the caller's responsibility, same posture
   * as `AuditEventInput.payload`.
   */
  payload?: Record<string, unknown>;
}

export interface SignOutInput {
  actor: { oid: string };
  sessionId: string;
}

export interface SessionExpiredInput {
  actor: { oid: string };
  sessionId: string;
  /** `absolute` is the only reason emitted in v1; `idle` is silent. */
  reason: 'absolute';
  /** Age of the session at the moment of expiry, in milliseconds. */
  ageMs: number;
}

export interface AdminAccessDeniedInput {
  actor: { oid: string };
  /**
   * Canonical "{METHOD} {originalUrl}" of the rejected request, e.g.
   * `GET /api/admin/audit`. Persisted into the audit row's `subject`
   * column so auditors can pivot on it without joining anything.
   */
  attemptedRoute: string;
  /**
   * The roles the user *did* hold at the moment of denial — empty
   * array when no app role was assigned. Stored in the payload so
   * an auditor can tell apart "tried admin while logged in as a
   * regular user" from "tried admin with an unrelated role like
   * `auditor` (future)" without parsing the deny pattern.
   */
  rolesHeld: readonly string[];
}

export interface AdminAuditQueryInput {
  actor: { oid: string };
  /**
   * Filters the admin used on the audit-viewer query. Captured into
   * the payload so a reviewer auditing audit-log reads can see what
   * the admin was searching for — per ADR-0020 §"Read actions are
   * also captured ... to deter fishing expeditions". Empty object
   * is meaningful (the admin asked for everything).
   */
  filters: Record<string, unknown>;
  /**
   * Result-set size returned to the admin. Bounded by the DTO's
   * `MAX_LIMIT`; persisted so an auditor can spot oversized scans.
   */
  resultCount: number;
}

/**
 * Same shape as {@link AdminAuditQueryInput} but targets the
 * `admin.users.query` event type. Per ADR-0020's deterrent posture,
 * the admin user-list read is auditable just like the audit-log
 * read — same payload contract, different `eventType`. A future
 * refactor could fold both into a single generic typed method, but
 * v1 keeps them split so the call sites declare intent at the type
 * system.
 */
export interface AdminUsersQueryInput {
  actor: { oid: string };
  filters: Record<string, unknown>;
  resultCount: number;
}

/**
 * Admin granted a scope to a user via the ADR-0026 PR 2b admin
 * screen. The `target` field carries the affected user's Entra `oid`
 * (the URL-path identifier of the scope-management screen). Stored
 * as the audit row's `subject` (`user:<oid>`). Payload captures the
 * scope tuple + the resulting row id + the expiry so a reviewer can
 * spot operator drift without re-querying.
 */
export interface AdminScopeGrantedInput {
  actor: { oid: string };
  target: { oid: string };
  scopeId: string;
  kind: string;
  value: string;
  expiresAt: string | null;
}

/**
 * Admin revoked (deleted) a scope row from a user. Same target
 * convention as {@link AdminScopeGrantedInput}; payload carries the
 * resolved tuple at the moment of revocation so the deletion is
 * reconstructable from the audit log even after the row itself is
 * gone.
 */
export interface AdminScopeRevokedInput {
  actor: { oid: string };
  target: { oid: string };
  scopeId: string;
  kind: string;
  value: string;
}

/**
 * Audit aggregations read — `GET /api/admin/audit/stats`. Same
 * deterrent posture as {@link AdminAuditQueryInput} (every admin
 * read is auditable per ADR-0020) but the payload captures the
 * `total` event count of the aggregated set instead of the
 * paginated `resultCount` — there's no pagination on stats, the
 * value carries more "size of the scan" signal.
 */
export interface AdminAuditStatsQueryInput {
  actor: { oid: string };
  filters: Record<string, unknown>;
  total: number;
}

/**
 * Discriminator on `AuthorizationDeniedInput.kind` — which of the
 * three new ADR-0025 guards rejected the request. Stored in the
 * payload so an auditor can spot "what kind of authorization
 * failed" without parsing the route. The existing
 * `admin.access_denied` event keeps its own type for backwards
 * compatibility with the legacy `AdminRoleGuard` audit posture.
 */
export type AuthorizationDeniedKind = 'privilege' | 'role' | 'scope';

export interface AuthorizationDeniedInput {
  actor: { oid: string };
  /** Canonical "{METHOD} {originalUrl}" of the rejected request. */
  attemptedRoute: string;
  /** Which guard fired the rejection. */
  kind: AuthorizationDeniedKind;
  /**
   * For `kind: 'privilege' | 'role'`: the catalogue values the
   * guard required. For `kind: 'scope'`: the route's resource
   * descriptor serialised into a `{kind, value}` map. Stored as
   * a generic `string[]` rather than the union of `Privilege` /
   * `FunctionalRole` so the audit module stays decoupled from the
   * shared catalogue.
   */
  required: readonly string[];
  /**
   * What the principal actually held on the matching axis at the
   * moment of denial — `privileges[]` for privilege/admin denials,
   * `roles[]` for role denials, `scopes[]` (serialised as
   * `kind` or `kind:value` strings) for scope denials. Lets an
   * auditor spot "tried admin while logged in as RH" patterns
   * without joining anything.
   */
  held: readonly string[];
}

/** Reason `RequireMfaGuard` rejected a request — fed into the audit payload. */
export type MfaRequiredReason = 'no-mfa-in-amr' | 'no-mfa-verified-at' | 'mfa-stale';

export interface MfaRequiredInput {
  actor: { oid: string };
  /** Canonical "{METHOD} {originalUrl}" of the rejected request. */
  attemptedRoute: string;
  reason: MfaRequiredReason;
  /**
   * Freshness window (seconds) the guard checked against. Captured
   * so an auditor can tell apart a 10-min default rejection from a
   * tighter per-route override.
   */
  freshnessSeconds: number;
  /** Age (ms) of the session's `mfaVerifiedAt` at the moment of rejection — undefined when the session had none. */
  mfaAgeMs?: number;
}
