/**
 * Phase 0 world — composes the REAL relay-auth (P5) enforcement + relay-e2e (P4) handshake through
 * the in-memory stores, tailored for the live data-plane path: a host's capability-token `aud` is
 * the SINGLE subdomain label (what `authorizeUpgrade` derives from the Host header), not the FQDN.
 * Directly modelled on `e2e/harness/world.ts` (the reference wiring named in the build spec).
 *
 * Only the true I/O boundaries are faked; every security check (Origin/CSWSH, token verify, DPoP
 * PoP, cross-tenant gate, single-use jti) is the production relay-auth path.
 */
import { randomUUID, randomBytes } from 'node:crypto'
import type { AeadAlg, E2ESession, HostHello } from 'relay-contracts'
import {
  createClientHandshake,
  createHostHandshake,
  createE2ESession,
  MemoryDevicePinStore,
} from 'relay-e2e'
import {
  signDeviceAuthProof,
  verifyDeviceProof,
  type EnforceDeps,
  type StepUpPolicy,
} from 'relay-auth'
import {
  generateEd25519KeyPair,
  exportEd25519PublicRaw,
  importEd25519PublicRaw,
  signEd25519,
  verifyEd25519,
} from 'relay-auth/src/crypto/ed25519.js'
import type { MtlsVerifier } from 'term-relay/data-plane/agent-listener.js'
import type { Authorizer } from 'term-relay/data-plane/authz-port.js'
import type { RouteResolver } from 'term-relay/data-plane/subdomain-router.js'
import {
  fakeAuditSink,
  fakeHostRegistry,
  fakeRevocationStore,
  fakeSessionRegistry,
  fakeTokenBucket,
  makeHostRecord,
  principal,
  type AuditFake,
  type MutableSessionRegistry,
  type RevocationFake,
  type TokenBucketFake,
} from './memory-stores.js'
import { issueCapBundle, setupP5SigningKey, resetDpopCacheForTest, type CapBundle } from './capability.js'
import { createAuthorizer } from './authorizer.js'
import { memoryRouteResolver } from './data-plane.js'

export const DEFAULT_NOW = 1_700_000_000
const REPLAY_ALG: AeadAlg = 'xchacha20-poly1305'

/** Phase 0 default: step-up never required (F2: null session-principal is correct). */
export const NO_STEP_UP: StepUpPolicy = {
  required: false,
  maxAgeSeconds: Number.MAX_SAFE_INTEGER,
  requiredMethod: 'passkey',
}

/** A seeded host: authz identity (hostId/accountId/label) + §4.4 e2e identity + replay secret. */
export interface HostFixture {
  readonly label: string
  readonly hostId: string
  readonly accountId: string
  readonly subdomain: string
  readonly origin: string
  readonly agentPubkey: Uint8Array
  readonly hostContentSecret: Uint8Array
  readonly replayAlg: AeadAlg
  readonly signingPriv: CryptoKey
}

async function makeHostFixture(
  label: string,
  accountId: string,
  baseDomain: string,
): Promise<HostFixture> {
  const kp = await generateEd25519KeyPair()
  const agentPubkey = await exportEd25519PublicRaw(kp.publicKey)
  return {
    label,
    hostId: randomUUID(),
    accountId,
    subdomain: label,
    origin: `https://${label}.${baseDomain}`,
    agentPubkey,
    hostContentSecret: new Uint8Array(randomBytes(32)),
    replayAlg: REPLAY_ALG,
    signingPriv: kp.privateKey,
  }
}

export interface EstablishedSessions {
  readonly client: E2ESession
  readonly host: E2ESession
  readonly agentPubkey: Uint8Array
}

export interface Phase0World {
  readonly now: number
  readonly signingKey: CryptoKey
  /** Raw 32-byte Ed25519 P5 verify pubkey (for CAPABILITY_SIGN_PUBKEY_B64). */
  readonly publicRaw: Uint8Array
  readonly baseDomain: string
  readonly host: HostFixture
  readonly allowedOrigins: readonly string[]
  readonly deps: EnforceDeps
  readonly audit: AuditFake
  readonly revocation: RevocationFake
  readonly buckets: TokenBucketFake
  readonly sessions: MutableSessionRegistry
  readonly resolver: RouteResolver
  readonly authorizer: Authorizer
  readonly mtls: MtlsVerifier
  issueCap(o?: { ttl?: number; now?: number }): Promise<CapBundle>
  establishSession(opts?: {
    now?: number
    verifyAgainstPubkey?: Uint8Array
    tamperHostHello?: (h: HostHello) => HostHello
  }): Promise<EstablishedSessions>
}

export async function buildPhase0World(now: number = DEFAULT_NOW): Promise<Phase0World> {
  const baseDomain = 'term.localhost'
  const { signingKey, publicRaw } = await setupP5SigningKey()
  resetDpopCacheForTest()

  const host = await makeHostFixture('alice', 'acct-alice', baseDomain)
  const hosts = fakeHostRegistry([makeHostRecord(host.accountId, host.hostId, host.subdomain)])
  const sessions = fakeSessionRegistry([])
  const revocation = fakeRevocationStore()
  const buckets = fakeTokenBucket()
  const audit = fakeAuditSink()
  const allowedOrigins = [host.origin]

  const deps: EnforceDeps = {
    hosts,
    sessions,
    revocation,
    buckets,
    audit,
    stepUpPolicyFor: () => NO_STEP_UP,
  }

  const routeMap = new Map([[host.subdomain, { hostId: host.hostId, accountId: host.accountId }]])
  const resolver = memoryRouteResolver(routeMap)
  const authorizer = createAuthorizer({ deps, allowedOrigins, now: () => now })
  const mtls: MtlsVerifier = {
    verifyPeer: () => ({ hostId: host.hostId, accountId: host.accountId }),
  }

  return {
    now,
    signingKey,
    publicRaw,
    baseDomain,
    host,
    allowedOrigins,
    deps,
    audit,
    revocation,
    buckets,
    sessions,
    resolver,
    authorizer,
    mtls,
    issueCap(o = {}) {
      return issueCapBundle(signingKey, {
        accountId: host.accountId,
        host: host.hostId,
        aud: host.subdomain,
        ttl: o.ttl,
        now: o.now ?? now,
      })
    },
    async establishSession(opts = {}): Promise<EstablishedSessions> {
      const hsNow = opts.now ?? now
      const client = createClientHandshake({
        aeadOffer: ['xchacha20-poly1305', 'aes-256-gcm'],
        deviceAuthProofProvider: {
          proofFor: (_hostId, binding) =>
            signDeviceAuthProof(principal(host.accountId), binding, signingKey, hsNow),
        },
        verifier: {
          verify: async (pub, transcript, sig) =>
            verifyEd25519(await importEd25519PublicRaw(pub), sig, transcript),
        },
        pinStore: new MemoryDevicePinStore(),
        hostId: host.hostId,
      })
      const hostHs = createHostHandshake({
        signer: { sign: (transcript) => signEd25519(host.signingPriv, transcript) },
        agentPubkey: host.agentPubkey,
        supported: ['xchacha20-poly1305', 'aes-256-gcm'],
        verifyDeviceProof: (proof, binding) => verifyDeviceProof(proof, binding, hsNow),
      })
      const clientHello = await client.start()
      const rawHostHello = await hostHs.onClientHello(clientHello)
      const hostHello = opts.tamperHostHello ? opts.tamperHostHello(rawHostHello) : rawHostHello
      const clientResult = await client.onHostHello(
        hostHello,
        opts.verifyAgainstPubkey ?? host.agentPubkey,
      )
      return {
        client: createE2ESession('client', clientResult),
        host: createE2ESession('host', hostHs.result!),
        agentPubkey: host.agentPubkey,
      }
    },
  }
}
