/**
 * T12 · Enforcement entry point `onUpgrade` — the SOLE owner of the WS-upgrade authorization
 * DECISION (INDEX §6a). P1's `authorizeUpgrade` is a thin adapter that DELEGATES here. The full
 * decision composes, in order: Origin/CSWSH (retained, INV15) → pre-auth throttle (Finding-5) →
 * deny-by-default authz incl. DPoP proof-of-possession + cross-tenant gate (T3/INV1) → per-tenant
 * rate → step-up gate (T8/Finding-3, v0.10) → single-use jti burn (Finding-4) → audit (T4/INV10).
 *
 * Deny-by-default: every branch that is not an explicit allow returns 401/403 and emits exactly one
 * `deny` AuditEvent. No allow path writes payload (INV10).
 */
import type { CapabilityRight, HostRecord } from 'relay-contracts'
import type {
  AuditAction,
  AuthenticatedPrincipal,
  AuditSink,
  HostRegistryPort,
  RevocationStore,
  SessionRegistryPort,
  StepUpPolicy,
  TokenBucketStore,
} from '../types.js'
import { authorizeConnect, authorizeReattach, type AuthzOutcome } from '../authz/decide.js'
import type { DpopContext } from '../capability/verify.js'
import { peekExp } from '../capability/verify.js'
import {
  checkPreAuthRate,
  checkConnectRate,
  checkConcurrentSessions,
  policyForPlan,
} from '../ratelimit/quota.js'
import { needsStepUp } from '../human/stepup/stepup.js'
import { buildAuditEvent, audit } from '../audit/log.js'

export interface UpgradeContext {
  readonly capabilityRaw: string
  readonly originHeader: string
  readonly expectedAud: string
  readonly requestedHostId: string
  readonly requiredRight: CapabilityRight
  readonly remoteAddrHash: string
  readonly activeSessionCount: number
  readonly dpop: DpopContext
  readonly principal: AuthenticatedPrincipal | null // session principal for step-up (T8)
}

export interface EnforceDeps {
  readonly hosts: HostRegistryPort
  readonly sessions: SessionRegistryPort
  readonly revocation: RevocationStore
  readonly buckets: TokenBucketStore
  readonly audit: AuditSink
  /** v0.10; v0.9 injects a "never required" policy (only consulted when a session principal exists). */
  readonly stepUpPolicyFor: (host: HostRecord) => StepUpPolicy
}

/** v0.9 default: no per-host tier lookup port here; the real tier comes from P3's account registry
 *  (INTEGRATION POINT). A conservative fixed policy bounds per-tenant abuse until then. */
const DEFAULT_TENANT_POLICY = policyForPlan('personal')

function deny(status: 401 | 403, reason: string): AuthzOutcome {
  return { ok: false, status, reason }
}

function auditActionFor(base: AuditAction, reason: string): AuditAction {
  return reason === 'cross_tenant' || reason === 'cross_tenant_session' ? 'cross-tenant-attempt' : base
}

async function emitDeny(
  deps: EnforceDeps,
  ctx: UpgradeContext,
  base: AuditAction,
  reason: string,
  now: number,
): Promise<void> {
  await audit(
    deps.audit,
    buildAuditEvent({
      action: auditActionFor(base, reason),
      principal: ctx.principal,
      hostId: ctx.requestedHostId,
      sessionId: null,
      jti: null,
      outcome: 'deny',
      reason,
      remoteAddrHash: ctx.remoteAddrHash,
      now,
    }),
  )
}

/** Shared enforcement pipeline for connect (`base='attach'`) and reattach (`base='reattach'`). */
export async function runEnforcement(
  ctx: UpgradeContext,
  deps: EnforceDeps,
  allowedOrigins: readonly string[],
  now: number,
  authorize: () => Promise<AuthzOutcome>,
  base: AuditAction,
  sessionId: string | null,
): Promise<AuthzOutcome> {
  // 1. Origin / CSWSH (retained base-app check, INV15).
  if (!allowedOrigins.includes(ctx.originHeader)) {
    await emitDeny(deps, ctx, base, 'bad_origin', now)
    return deny(401, 'bad_origin')
  }
  // 2. Pre-auth throttle BEFORE any token work (Finding-5) — keyed on the IP hash, no principal yet.
  if (!(await checkPreAuthRate(ctx.remoteAddrHash, deps.buckets, now))) {
    await emitDeny(deps, ctx, base, 'pre_auth_throttled', now)
    return deny(403, 'pre_auth_throttled')
  }
  // 3. Deny-by-default authz (token verify + DPoP PoP + INV1 cross-tenant gate).
  const outcome = await authorize()
  if (!outcome.ok) {
    await emitDeny(deps, ctx, base, outcome.reason, now)
    return outcome
  }
  const accountId = outcome.principal.accountId
  // 4. Per-tenant rate (after the principal is known).
  if (!(await checkConnectRate(accountId, DEFAULT_TENANT_POLICY, deps.buckets, now))) {
    await emitDeny(deps, ctx, base, 'rate_limited', now)
    return deny(403, 'rate_limited')
  }
  if (!checkConcurrentSessions(accountId, ctx.activeSessionCount, DEFAULT_TENANT_POLICY)) {
    await emitDeny(deps, ctx, base, 'too_many_sessions', now)
    return deny(403, 'too_many_sessions')
  }
  // 5. Step-up gate (Finding-3/F1/F2) — HOST-DRIVEN and FAIL-CLOSED on connect AND reattach.
  //    When a host REQUIRES step-up, DENY unless an authenticated principal proves a FRESH step-up
  //    of the required method. A missing host, a null principal, or a stale/wrong-method step-up all
  //    fail closed. When the policy does not require step-up, this gate is a no-op.
  const host = await deps.hosts.getById(outcome.hostId)
  const policy = deps.stepUpPolicyFor(host as HostRecord)
  if (policy.required) {
    if (host === null || ctx.principal === null || needsStepUp(ctx.principal, policy, now)) {
      await emitDeny(deps, ctx, 'stepup', 'step_up_required', now)
      return deny(403, 'step_up_required')
    }
  }
  // 6. Single-use consume (Finding-4) — burn the jti on the first fully-allowed upgrade.
  const exp = peekExp(ctx.capabilityRaw)
  if (!(await deps.revocation.consumeOnce(outcome.jti, exp))) {
    await emitDeny(deps, ctx, base, 'token_replayed', now)
    return deny(403, 'token_replayed')
  }
  // 7. Audit allow + return.
  await audit(
    deps.audit,
    buildAuditEvent({
      action: base,
      principal: outcome.principal,
      hostId: outcome.hostId,
      sessionId,
      jti: outcome.jti,
      outcome: 'allow',
      reason: 'ok',
      remoteAddrHash: ctx.remoteAddrHash,
      now,
    }),
  )
  return outcome
}

export function onUpgrade(
  ctx: UpgradeContext,
  deps: EnforceDeps,
  allowedOrigins: readonly string[],
  now: number,
): Promise<AuthzOutcome> {
  return runEnforcement(
    ctx,
    deps,
    allowedOrigins,
    now,
    () =>
      authorizeConnect(
        {
          capabilityRaw: ctx.capabilityRaw,
          expectedAud: ctx.expectedAud,
          requestedHostId: ctx.requestedHostId,
          requiredRight: ctx.requiredRight,
        },
        ctx.dpop,
        deps.hosts,
        deps.revocation,
        now,
      ),
    'attach',
    null,
  )
}
