/**
 * buildRelayWorld() — composes the REAL relay-auth (P5) + relay-e2e (P4) + agent (P2 replay)
 * exports through the in-memory seams in ./fakes and exposes a clean flow API plus the untrusted
 * "RelaySpy" attacker vantage. Only the true I/O boundaries are faked; every security check is the
 * production code path. Pass an explicit `now` everywhere (no Date.now in assertions).
 *
 * The host identity used for the §4.4 handshake transcript signature is a real WebCrypto Ed25519
 * key (relay-auth's own crypto, deep-imported) so the harness needs no @noble import of its own and
 * no relay-e2e deep import (relay-e2e's `exports` map blocks subpaths).
 */
import { randomUUID, randomBytes } from 'node:crypto'
import type { AeadAlg, E2EEnvelope, E2ESession, HostHello, HostRecord } from 'relay-contracts'
import {
  createClientHandshake,
  createHostHandshake,
  createE2ESession,
  MemoryDevicePinStore,
  deriveContentKey,
  sealReplayFrame,
  openReplayCiphertext,
  encodeEnvelope,
} from 'relay-e2e'
import {
  onUpgrade,
  onReattach,
  revoke,
  revokeToken,
  signDeviceAuthProof,
  verifyDeviceProof,
  type AuthzOutcome,
  type EnforceDeps,
  type UpgradeContext,
  type StepUpPolicy,
  type RevocationScope,
} from 'relay-auth'
import {
  generateEd25519KeyPair,
  exportEd25519PublicRaw,
  importEd25519PublicRaw,
  signEd25519,
  verifyEd25519,
} from 'relay-auth/src/crypto/ed25519.js'
import { createReplaySealer, type ReplaySealer } from 'agent'
import {
  fakeAuditSink,
  fakeHostRegistry,
  fakeRevocationBus,
  fakeRevocationStore,
  fakeSessionRegistry,
  fakeTokenBucket,
  makeHostRecord,
  principal,
  type AuditFake,
  type MutableSessionRegistry,
  type RevocationBusFake,
  type RevocationFake,
  type TokenBucketFake,
} from './fakes.js'
import {
  craftMalformedDpopBundle,
  issueCapBundle,
  resetDpopCacheForTest,
  setupP5SigningKey,
  type CapBundle,
} from './dpop.js'
import { RelaySpy } from './spy.js'

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

export const NO_STEP_UP: StepUpPolicy = {
  required: false,
  maxAgeSeconds: Number.MAX_SAFE_INTEGER,
  requiredMethod: 'passkey',
}
export const STRICT_PASSKEY: StepUpPolicy = {
  required: true,
  maxAgeSeconds: 300,
  requiredMethod: 'passkey',
}

/** A seeded host: authz identity (hostId/accountId/aud) + §4.4 e2e identity + replay secret. */
export interface HostFixture {
  readonly label: string
  readonly hostId: string
  readonly accountId: string
  readonly aud: string
  readonly origin: string
  readonly agentPubkey: Uint8Array
  readonly hostContentSecret: Uint8Array
  readonly replayAlg: AeadAlg
  /** WebCrypto private key backing the handshake transcript signer. */
  readonly signingPriv: CryptoKey
}

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

export interface UpgradeArgs {
  readonly raw: string
  readonly dpop: UpgradeContext['dpop']
  readonly host: string
  readonly aud: string
  readonly origin: string
  readonly now: number
  readonly principal?: UpgradeContext['principal']
  readonly requiredRight?: UpgradeContext['requiredRight']
  readonly activeSessionCount?: number
  readonly remoteAddrHash?: string
}

export interface ReattachArgs extends UpgradeArgs {
  readonly sessionId: string
}

export interface HandshakeOpts {
  readonly host?: HostFixture
  readonly now?: number
  /** MITM: pubkey the client verifies host_hello against (default = the real agentPubkey). */
  readonly verifyAgainstPubkey?: Uint8Array
  /** MITM: mutate the host_hello the client receives (tampered transcript / hostEphPub). */
  readonly tamperHostHello?: (h: HostHello) => HostHello
}

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

export interface RelayWorld {
  readonly now: number
  readonly signingKey: CryptoKey
  readonly publicRaw: Uint8Array
  readonly hostA: HostFixture
  readonly hostB: HostFixture
  readonly allowedOrigins: readonly string[]
  readonly deps: EnforceDeps
  readonly audit: AuditFake
  readonly revocation: RevocationFake
  readonly buckets: TokenBucketFake
  readonly bus: RevocationBusFake
  readonly sessions: MutableSessionRegistry
  principal(accountId: string, over?: Parameters<typeof principal>[1]): ReturnType<typeof principal>
  setStepUpPolicy(policy: StepUpPolicy): void
  issueCap(o: {
    accountId: string
    host: string
    aud: string
    rights?: readonly UpgradeContext['requiredRight'][]
    ttl?: number
    now?: number
  }): Promise<CapBundle>
  craftMalformedDpop(o: {
    accountId: string
    host: string
    aud: string
    now?: number
    blobLen?: number
  }): Promise<{ raw: string; dpop: UpgradeContext['dpop'] }>
  upgrade(a: UpgradeArgs): Promise<AuthzOutcome>
  reattach(a: ReattachArgs): Promise<AuthzOutcome>
  addSession(e: { sessionId: string; hostId: string; accountId: string }): void
  establishSession(opts?: HandshakeOpts): Promise<EstablishedSessions>
  newReplaySealer(sessionId: string, host?: HostFixture): ReplaySealer
  /** Browser-side re-derivation + open of a replay frame (throws on AEAD failure / wrong epoch). */
  openReplay(sessionId: string, epoch: string, env: E2EEnvelope, host?: HostFixture): Uint8Array
  revokeToken(jti: string, exp?: number): Promise<void>
  revoke(scope: RevocationScope): Promise<void>
}

export async function buildRelayWorld(now: number = DEFAULT_NOW): Promise<RelayWorld> {
  const { signingKey, publicRaw } = await setupP5SigningKey()
  resetDpopCacheForTest()

  const hostA = await makeHostFixture('alice', 'acct-A')
  const hostB = await makeHostFixture('bob', 'acct-B')

  const hostRecords: HostRecord[] = [
    makeHostRecord(hostA.accountId, hostA.hostId),
    makeHostRecord(hostB.accountId, hostB.hostId),
  ]
  const hosts = fakeHostRegistry(hostRecords)
  const sessions = fakeSessionRegistry([])
  const revocation = fakeRevocationStore()
  const buckets = fakeTokenBucket()
  const audit = fakeAuditSink()
  const bus = fakeRevocationBus()
  const allowedOrigins = [hostA.origin, hostB.origin]

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

  function ctxFor(a: UpgradeArgs): UpgradeContext {
    return {
      capabilityRaw: a.raw,
      originHeader: a.origin,
      expectedAud: a.aud,
      requestedHostId: a.host,
      requiredRight: a.requiredRight ?? 'attach',
      remoteAddrHash: a.remoteAddrHash ?? 'ip-hash',
      activeSessionCount: a.activeSessionCount ?? 0,
      dpop: a.dpop,
      principal: a.principal ?? null,
    }
  }

  const replayCrypto = { deriveContentKey, sealReplayFrame }

  return {
    now,
    signingKey,
    publicRaw,
    hostA,
    hostB,
    allowedOrigins,
    deps,
    audit,
    revocation,
    buckets,
    bus,
    sessions,
    principal,
    setStepUpPolicy(policy) {
      stepUpPolicy = policy
    },
    issueCap(o) {
      return issueCapBundle(signingKey, {
        accountId: o.accountId,
        host: o.host,
        aud: o.aud,
        rights: o.rights,
        ttl: o.ttl,
        now: o.now ?? now,
      })
    },
    craftMalformedDpop(o) {
      return craftMalformedDpopBundle(signingKey, {
        accountId: o.accountId,
        host: o.host,
        aud: o.aud,
        now: o.now ?? now,
        blobLen: o.blobLen,
      })
    },
    upgrade(a) {
      return onUpgrade(ctxFor(a), deps, allowedOrigins, a.now)
    },
    reattach(a) {
      return onReattach({ ...ctxFor(a), sessionId: a.sessionId }, deps, allowedOrigins, a.now)
    },
    addSession(e) {
      sessions.add(e)
    },
    async establishSession(opts: HandshakeOpts = {}): Promise<EstablishedSessions> {
      const h = opts.host ?? hostA
      const hsNow = opts.now ?? now
      const client = createClientHandshake({
        aeadOffer: ['xchacha20-poly1305', 'aes-256-gcm'],
        deviceAuthProofProvider: {
          proofFor: (_hostId, binding) =>
            signDeviceAuthProof(principal(h.accountId), binding, signingKey, hsNow),
        },
        verifier: {
          verify: async (pub, transcript, sig) =>
            verifyEd25519(await importEd25519PublicRaw(pub), sig, transcript),
        },
        pinStore: new MemoryDevicePinStore(),
        hostId: h.hostId,
      })
      const host = createHostHandshake({
        signer: { sign: (transcript) => signEd25519(h.signingPriv, transcript) },
        agentPubkey: h.agentPubkey,
        supported: ['xchacha20-poly1305', 'aes-256-gcm'],
        verifyDeviceProof: (proof, binding) => verifyDeviceProof(proof, binding, hsNow),
      })
      const clientHello = await client.start()
      const rawHostHello = await host.onClientHello(clientHello)
      const hostHello = opts.tamperHostHello ? opts.tamperHostHello(rawHostHello) : rawHostHello
      const clientResult = await client.onHostHello(
        hostHello,
        opts.verifyAgainstPubkey ?? h.agentPubkey,
      )
      return {
        client: createE2ESession('client', clientResult),
        host: createE2ESession('host', host.result!),
        spy: new RelaySpy(),
        agentPubkey: h.agentPubkey,
      }
    },
    newReplaySealer(sessionId, host = hostA) {
      return createReplaySealer(host.hostContentSecret, sessionId, host.replayAlg, replayCrypto)
    },
    openReplay(sessionId, epoch, env, host = hostA) {
      const key = deriveContentKey({
        hostContentSecret: host.hostContentSecret,
        sessionId,
        alg: host.replayAlg,
        epoch,
      })
      return openReplayCiphertext(key, encodeEnvelope(env))
    },
    async revokeToken(jti, exp = now + 60) {
      await revokeToken(jti, exp, revocation)
    },
    async revoke(scope) {
      await revoke(scope, revocation, hosts, bus, now)
    },
  }
}
