import { Inject, Injectable } from '@nestjs/common';
import { SignJWT } from 'jose';
import { BFF_SIGNING_KEY, type BffSigningKey } from '../bff-signing-key';

/**
 * Caller-supplied inputs. The future framework integration fills
 * these from the active request:
 *
 *   - `actorIdHash` — salted user-id hash from the audit module.
 *     Goes into `sub`. Never the raw user id (per ADR-0013 §"Salted
 *     hash actor_id"); never a user-controlled value.
 *   - `audience` — workforce or customer per ADR-0008. Drives
 *     downstream audience-aware authorization without forcing the
 *     downstream to call back to the IdP.
 *   - `downstreamName` — stable downstream identifier; becomes the
 *     JWT `aud`. The downstream rejects assertions not targeted at
 *     itself, so a leaked assertion can't be replayed at a
 *     different service.
 *   - `claims` — curated subset of session claims the downstream
 *     needs to make its authorization decision. The framework is
 *     responsible for the curation (allow-list, never the full
 *     session); this strategy treats it as opaque structured data.
 *   - `traceId` — W3C trace id of the originating request. Lets the
 *     downstream emit its own logs/audit referencing the same trace
 *     so a correlation across BFF + downstream is one-step.
 */
export interface SignedAssertionInput {
  readonly actorIdHash: string;
  readonly audience: 'workforce' | 'customer';
  readonly downstreamName: string;
  readonly claims: Readonly<Record<string, unknown>>;
  readonly traceId: string;
}

/** Issuer value baked into every assertion. Stable string, no env override. */
const ISSUER = 'portal-bff';

/** Assertion lifetime — 60 s per ADR-0014 §"Service strategy". */
const ASSERTION_TTL_SECONDS = 60;

/**
 * `SignedAssertionStrategy` per
 * [ADR-0014](../../../../docs/decisions/0014-downstream-api-access-obo-pattern.md)
 * §"Service strategy (non-Entra downstreams)".
 *
 * Mints a short-lived JWT that propagates user identity to a
 * downstream without giving the downstream a token it could replay
 * against Entra. The downstream verifies the signature against the
 * BFF's `/.well-known/jwks.json` and makes its own authZ decision
 * from the claims.
 *
 * Claim shape per the ADR:
 *
 * ```
 * {
 *   "iss": "portal-bff",
 *   "sub": "<actor_id_hash>",
 *   "aud": "<downstream-name>",
 *   "audience": "workforce" | "customer",
 *   "claims": { … },
 *   "exp": <now + 60s>,
 *   "iat": <now>,
 *   "trace_id": "<W3C trace id>"
 * }
 * ```
 *
 * Each call mints a fresh JWT — there's no caching. At ~60 s TTL
 * the savings would be negligible and a cache would create a window
 * where a replayed assertion lingers past its useful life. The
 * signing operation itself is cheap (a few hundred microseconds for
 * RS256 with a 3 KB key).
 */
@Injectable()
export class SignedAssertionStrategy {
  constructor(@Inject(BFF_SIGNING_KEY) private readonly key: BffSigningKey) {}

  async sign(input: SignedAssertionInput): Promise<string> {
    const now = Math.floor(Date.now() / 1000);
    return await new SignJWT({
      audience: input.audience,
      claims: input.claims,
      trace_id: input.traceId,
    })
      .setProtectedHeader({ alg: this.key.config.alg, kid: this.key.config.kid })
      .setIssuer(ISSUER)
      .setSubject(input.actorIdHash)
      .setAudience(input.downstreamName)
      .setIssuedAt(now)
      .setExpirationTime(now + ASSERTION_TTL_SECONDS)
      .sign(this.key.config.privateKey);
  }
}
