/**
 * T11 — admin-API principal derivation (INV3). `accountId` comes ONLY from the VERIFIED capability
 * token (P5 owns the signing key + `verifyCapabilityToken`; we consume it as an injected verifier).
 * Any body/query field named `account_id`/`tenant_id` is IGNORED for authz decisions. Deny-by-default:
 * a missing/expired/foreign-`aud` token → 401.
 */
import { extractTokenFromSubprotocols, type CapabilityToken, type CapabilityRight } from 'relay-contracts'

export interface AdminPrincipal {
  readonly accountId: string
  readonly rights: readonly CapabilityRight[]
}

export class AuthzError extends Error {
  constructor(
    public readonly status: 401 | 403,
    message: string,
  ) {
    super(message)
  }
}

/**
 * P5's verifier, injected. Mirrors the frozen `verifyCapabilityToken` signature (§4.3), which is
 * ASYNC (Ed25519 verify over WebCrypto) — so this seam returns a Promise and all call sites await.
 */
export interface CapabilityVerifier {
  verify(raw: string, expectedAud: string, now: number): Promise<CapabilityToken>
}

/** Minimal request shape we read for the bearer token (never a body account field). */
export interface AuthRequest {
  readonly headers: Readonly<Record<string, string | string[] | undefined>>
}

export interface AuthzDeps {
  readonly verifier: CapabilityVerifier
  readonly expectedAud: string // admin audience the token must be scoped to (Host-confusion guard)
  readonly now?: () => number
}

function extractRawToken(req: AuthRequest): string | null {
  const auth = req.headers['authorization']
  if (typeof auth === 'string' && auth.startsWith('Bearer ')) return auth.slice('Bearer '.length).trim()
  // Also accept the WS-upgrade subprotocol transport (FIX 5), for parity with the relay edge.
  const proto = req.headers['sec-websocket-protocol']
  if (typeof proto === 'string') {
    const values = proto.split(',').map((v) => v.trim())
    return extractTokenFromSubprotocols(values)
  }
  return null
}

export interface Authorizer {
  principalFromRequest(req: AuthRequest): Promise<AdminPrincipal>
  requireRight(principal: AdminPrincipal, right: CapabilityRight): void
}

export function createAuthorizer(deps: AuthzDeps): Authorizer {
  const now = deps.now ?? (() => Math.floor(Date.now() / 1000))
  return {
    async principalFromRequest(req) {
      const raw = extractRawToken(req)
      if (raw === null) throw new AuthzError(401, 'missing capability token')
      let token: CapabilityToken
      try {
        token = await deps.verifier.verify(raw, deps.expectedAud, now())
      } catch (err: unknown) {
        throw new AuthzError(401, `invalid capability token: ${err instanceof Error ? err.message : 'rejected'}`)
      }
      // accountId derives ONLY from the token principal — never from a client field (INV3).
      return { accountId: token.sub, rights: token.rights }
    },
    requireRight(principal, right) {
      if (!principal.rights.includes(right)) {
        throw new AuthzError(403, `token scope lacks required right: ${right}`)
      }
    },
  }
}
