import { Injectable } from '@nestjs/common';
import { HashUserIdService } from '../../audit/hash-user-id.service';
import type { Principal } from '../gen/apf-ai/common';

/**
 * Read-only view of the session fields the mapper consumes. Decoupled
 * from `AuthenticatedUser` so callers can build a Principal in tests
 * or in non-session contexts (a future background-job consumer) by
 * supplying a plain object — without dragging the full session shape
 * along.
 *
 * Keep the shape minimal: anything beyond `oid + tid + roles` should
 * land in `attributes` rather than as a new top-level field, so the
 * proto contract stays stable as the BFF grows new claim sources.
 */
export interface PrincipalInputs {
  /** Entra Object ID. Hashed via `HashUserIdService` into the proto `subject` field. */
  readonly oid: string;
  /** Entra tenant id. Surfaced as `attributes.tenantId`. */
  readonly tid: string;
  /** Roles already resolved upstream by the auth flow. */
  readonly roles: readonly string[];
  /**
   * Free-form extra attributes (delegation slug, region, …) merged
   * into `attributes` after `tenantId`. Caller-controlled; the
   * mapper does not enrich. Reserved keys (`tenantId`) overwrite
   * the caller-provided value to keep the proto contract single-
   * sourced.
   */
  readonly extraAttributes?: Readonly<Record<string, string>>;
}

/**
 * Builds the `apf.ai.v1.Principal` proto from a portal session,
 * per ADR-0024 §"Sub-decision 4 — unsigned Principal in the proto body".
 *
 * The `subject` field MUST be the same hash the audit module writes
 * to `audit.events.actor_id_hash` (per [ADR-0013](../../docs/decisions/0013-audit-trail-separated-postgres-append-only.md))
 * and that Pino's `user_id_hash` log field carries (per [ADR-0012](../../docs/decisions/0012-observability-pino-opentelemetry.md)).
 * The two services' audit trails only join cleanly when both sides
 * compute the same value — same Entra `oid`, same salt, same algo.
 * The mapper delegates to `HashUserIdService` to make that contract
 * explicit at the wire boundary.
 *
 * The `roles` field is passed through verbatim in v1. A future ADR
 * on role hierarchy (proposed alongside the stargate migration
 * analysis) will introduce inclusive expansion (`Admin` ⇒
 * `[admin, directeur, rh, collaborateur]`). At that point the
 * mapper grows a `RoleExpander` collaborator; the wire shape and
 * the upstream contract on the AI side stay unchanged.
 */
@Injectable()
export class PrincipalMapper {
  constructor(private readonly hashUserId: HashUserIdService) {}

  fromInputs(inputs: PrincipalInputs): Principal {
    return {
      subject: this.hashUserId.hash(inputs.oid),
      roles: [...inputs.roles],
      attributes: {
        ...(inputs.extraAttributes ?? {}),
        tenantId: inputs.tid,
      },
    };
  }
}
