/**
 * T8 · Upgrade authorization edge — a THIN ADAPTER over P5 (FIX 6a). Holds NO independent authz
 * logic: it (a) parses the upgrade (subdomain + FIX-5 token extraction + salted remoteAddr hash),
 * (b) resolves the subdomain → requestedHostId via P3's RouteResolver, (c) builds P5's
 * UpgradeContext, (d) calls the injected P5 Authorizer, (e) maps AuthzOutcome → §4.1 MuxOpen or a
 * 401/403. Every accept/reject verdict is P5's; the only LOCAL failures are the three parse guards.
 *
 * Security: capability token rides ONLY the two FIX-5 carriers (subprotocol PREFERRED, cookie
 * FALLBACK) — NEVER the query string (bearer-equivalent secrets must not leak into logs/history/
 * Referer). The accepted subprotocol echoed back is ALWAYS `APP_SUBPROTOCOL`, never the token
 * entry (INV15). Identity (`host_id`/`account_id`) derives only from the signed token + registry
 * via P5 (INV3). Logging discipline (INV9): callers MUST NOT log req.url / Authorization /
 * Sec-WebSocket-Protocol — only `{subdomain, hostId, jti, decision, ts}`.
 */
import { createHmac } from 'node:crypto'
import { APP_SUBPROTOCOL, extractTokenFromSubprotocols, type MuxOpen, type CapabilityRight } from 'relay-contracts'
import { extractSubdomain, type RouteResolver } from './subdomain-router.js'
import type { Authorizer, UpgradeContext } from './authz-port.js'

/** Short-lived HttpOnly; Secure; SameSite=Strict cookie carrying the capability token (fallback). */
export const TOKEN_COOKIE_NAME = 'term_cap'

export interface UpgradeRequest {
  readonly host: string
  readonly origin: string | undefined
  readonly url: string // requestPath ONLY (opaque passthrough) — NEVER a token source
  readonly subprotocols: readonly string[] // parsed Sec-WebSocket-Protocol values (FIX-5 transport)
  readonly cookies: Readonly<Record<string, string>> // parsed Cookie header (fallback transport)
  readonly remoteAddr: string
  readonly dpop: UpgradeContext['dpop']
  readonly activeSessionCount: number
  readonly sessionId?: string // present ⇒ reattach path (→ onReattach)
}

export type UpgradeDecision =
  | {
      readonly ok: true
      readonly open: MuxOpen
      readonly hostId: string
      readonly acceptedSubprotocol: string
    }
  | { readonly ok: false; readonly status: 401 | 403 }

export interface AuthorizeDeps {
  authorizer: Authorizer
  resolver: RouteResolver
  baseDomain: string
  now: () => number
  remoteAddrSalt: string
  requiredRight: CapabilityRight
}

/**
 * Extract the capability token from EXACTLY the two allowed FIX-5 carriers, in order:
 * (1) `Sec-WebSocket-Protocol` `term.token.<b64u>` entry, (2) the `term_cap` cookie. Returns the
 * opaque token + which carrier, or null. NEVER reads `req.url` (query-string tokens forbidden).
 */
export function extractCapabilityToken(
  req: UpgradeRequest,
): { token: string; via: 'subprotocol' | 'cookie' } | null {
  const fromSub = extractTokenFromSubprotocols(req.subprotocols)
  if (fromSub !== null && fromSub.length > 0) return { token: fromSub, via: 'subprotocol' }
  const fromCookie = req.cookies[TOKEN_COOKIE_NAME]
  if (fromCookie !== undefined && fromCookie.length > 0) return { token: fromCookie, via: 'cookie' }
  return null
}

/** Salted HMAC-SHA256 of the client IP, truncated — audit-only, never reversible (INV10). */
function hashRemoteAddr(salt: string, remoteAddr: string): string {
  return createHmac('sha256', salt).update(remoteAddr).digest('hex').slice(0, 32)
}

/** The opaque request path (everything the relay forwards but NEVER parses for authz). */
function pathOf(url: string): string {
  return url
}

/**
 * Parse → resolve → build ctx → delegate to P5 → map. No decision branches beyond the three
 * LOCAL parse guards (unparseable Host → 401, no token carrier → 401, unknown subdomain → 403).
 */
export async function authorizeUpgrade(
  req: UpgradeRequest,
  deps: AuthorizeDeps,
): Promise<UpgradeDecision> {
  const subdomain = extractSubdomain(req.host, deps.baseDomain)
  if (subdomain === null) return { ok: false, status: 401 } // unparseable Host (local)

  const extracted = extractCapabilityToken(req)
  if (extracted === null) return { ok: false, status: 401 } // no token carrier present (local)

  const resolved = await deps.resolver.resolveSubdomain(subdomain)
  if (resolved === null) return { ok: false, status: 403 } // unknown subdomain (local)

  const remoteAddrHash = hashRemoteAddr(deps.remoteAddrSalt, req.remoteAddr)
  const ctx: UpgradeContext = {
    capabilityRaw: extracted.token,
    originHeader: req.origin ?? '',
    expectedAud: subdomain,
    requestedHostId: resolved.hostId, // P5 gates token.host against this (INV1)
    requiredRight: deps.requiredRight,
    remoteAddrHash,
    activeSessionCount: req.activeSessionCount,
    dpop: req.dpop,
    principal: null, // P5 resolves from the signed token (INV3)
  }

  const outcome =
    req.sessionId !== undefined
      ? await deps.authorizer.onReattach({ ...ctx, sessionId: req.sessionId }, deps.now())
      : await deps.authorizer.onUpgrade(ctx, deps.now())

  if (!outcome.ok) return { ok: false, status: outcome.status }

  const open: MuxOpen = {
    streamId: 0, // allocated by MuxSession
    subdomain,
    requestPath: pathOf(req.url),
    originHeader: req.origin ?? '',
    remoteAddrHash,
    capabilityTokenRef: outcome.jti, // OQ5 resolved: P5 surfaces the verified jti
  }
  return { ok: true, open, hostId: outcome.hostId, acceptedSubprotocol: APP_SUBPROTOCOL }
}
