/**
 * Host E2E endpoint (§4.4) — PLAN_RELAY_AGENT T15. The HOST side of the authenticated ECDH: absorb
 * ClientHello → verify the device-auth-proof FIRST → reply HostHello → derive DirectionalKeys →
 * per-stream seal(h2c)/open(c2h). Distinct from live frames, every replay-bound output is ALSO
 * sealed under K_content via the T19 ReplaySealer (FIX 3).
 *
 * ANTI-MITM (INV2): the device-auth-proof `verifyDeviceProof` is P5-OWNED and reaches this module
 * INJECTED (FIX 6b) — this file MUST NOT `import { verifyDeviceAuthProof } from 'relay-e2e'`. The
 * proof is verified BEFORE any key derivation; a forged proof ⇒ MitmAbortError, NO HostHello, NO
 * DirectionalKeys. A no-stub guard test asserts this file imports no verifier and that swapping the
 * injected verifier for `async () => true` makes the MITM test fail.
 *
 * BLOCKING GATE (open Q#2 — DEFERRED): the real §4.4 crypto (`sealFrame`/`openFrame`/
 * `createE2ESession`) + host-handshake wiring (`createHostHandshake`) live in P4 `relay-e2e/`, and
 * the P5 verifier + forged-proof vector are a hard pre-W4 gate. P4 is NOT built yet, so those are
 * INJECTED here as seams typed to the frozen relay-contracts signatures — production passes the
 * relay-e2e impls verbatim, NEVER a stub. INV11: even after open(), bytes go to loopback OPAQUE.
 */
import type {
  ClientHello,
  E2ESession,
  HandshakeResult,
  HostHello,
} from 'relay-contracts'
import type { AgentIdentity } from '../keys/identity.js'
import type { FrameTransform } from '../transport/streamRouter.js'
import type { ReplaySealer } from './replaySeal.js'

/** Host-side device-proof verifier — INJECTED (P5 issues+verifies), bound to the ClientHello. */
export type VerifyDeviceProof = (
  proof: string,
  binding: { clientEphPub: Uint8Array; clientNonce: Uint8Array },
) => Promise<boolean>

export class MitmAbortError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'MitmAbortError'
  }
}

/** P4 host-handshake wiring seam (`createHostHandshake`), typed to §4.4 shapes. */
export interface HostHandshake {
  respond(clientHello: ClientHello): Promise<{ hello: HostHello; result: HandshakeResult }>
}
export type CreateHostHandshake = (deps: {
  verifyDeviceProof: VerifyDeviceProof
  identity: AgentIdentity
}) => HostHandshake

/** P4 §4.4 crypto core, injected (impls live in relay-e2e/). */
export interface E2ECryptoDeps {
  createHostHandshake: CreateHostHandshake
  createE2ESession(role: 'host', result: HandshakeResult): E2ESession
  /** Wire codec for the HostHello reply (P4/P6-owned framing). */
  encodeHostHello(hello: HostHello): Uint8Array
}

/**
 * Produce the HostHello + HandshakeResult for a ClientHello. The proof is verified FIRST; a forged
 * proof aborts with MitmAbortError and NO key derivation (anti-MITM). FIX 2: the result carries
 * DirectionalKeys{c2h,h2c}, never a single sessionKey.
 */
export async function makeHostHello(
  clientHello: ClientHello,
  id: AgentIdentity,
  verifyDeviceProof: VerifyDeviceProof,
  deps: Pick<E2ECryptoDeps, 'createHostHandshake'>,
): Promise<{ hello: HostHello; result: HandshakeResult }> {
  const ok = await verifyDeviceProof(clientHello.deviceAuthProof, {
    clientEphPub: clientHello.clientEphPub,
    clientNonce: clientHello.clientNonce,
  })
  if (!ok) {
    throw new MitmAbortError('device-auth-proof verification failed — aborting, no keys derived')
  }
  const handshake = deps.createHostHandshake({ verifyDeviceProof, identity: id })
  return handshake.respond(clientHello)
}

/**
 * FrameTransform + a `seedSession` seam. The wiring layer intercepts the first (ClientHello) frame,
 * runs `makeHostHello` (async, verify-first), then calls `seedSession` with the derived
 * E2ESession + the encoded HostHello (queued as a control frame the router flushes upstream).
 * After seeding: inbound = session.open(c2h) → loopback (OPAQUE, INV11); outbound = session.seal
 * (h2c) → tunnel AND replay.seal(K_content) — two DISTINCT ciphertexts (FIX 3).
 */
export interface E2ETransform extends FrameTransform {
  seedSession(streamId: number, session: E2ESession, hostHelloBytes: Uint8Array): void
}

interface StreamE2EState {
  session: E2ESession | null
  readonly control: Uint8Array[]
}

export function createE2ETransform(
  _id: AgentIdentity,
  _verifyDeviceProof: VerifyDeviceProof,
  replay: ReplaySealer,
): E2ETransform {
  const streams = new Map<number, StreamE2EState>()

  function stateFor(streamId: number): StreamE2EState {
    let s = streams.get(streamId)
    if (s === undefined) {
      s = { session: null, control: [] }
      streams.set(streamId, s)
    }
    return s
  }

  return {
    openStream(streamId: number): void {
      stateFor(streamId)
    },
    closeStream(streamId: number): void {
      streams.delete(streamId)
    },
    seedSession(streamId: number, session: E2ESession, hostHelloBytes: Uint8Array): void {
      const s = stateFor(streamId)
      s.session = session
      s.control.push(hostHelloBytes)
    },
    inbound(streamId: number, cipher: Uint8Array): Uint8Array | null {
      const s = stateFor(streamId)
      if (s.session === null) return null // handshake not yet seeded; wiring layer handles it
      return s.session.open(cipher) // opaque plaintext to loopback (INV11)
    },
    outbound(streamId: number, plain: Uint8Array): Uint8Array {
      const s = stateFor(streamId)
      if (s.session === null) {
        throw new MitmAbortError('cannot seal before the E2E session is established')
      }
      replay.seal(plain) // FIX 3: recoverable K_content seal, DISTINCT from the live h2c frame
      return s.session.seal(plain)
    },
    takeControlFrames(streamId: number): Uint8Array[] {
      const s = streams.get(streamId)
      if (s === undefined || s.control.length === 0) return []
      return s.control.splice(0, s.control.length)
    },
  }
}
