/**
 * T8 — client + host handshake state machines (§4.4 `client_hello` → `host_hello`).
 *
 * Anti-MITM (MUST be explicit): the pin string alone is NOT the defense — `enrollFpr` hashes a
 * PUBLIC key a malicious relay also knows. `onHostHello` receives the raw `agentPubkey` from P3's
 * INDEPENDENT TLS channel (never from the relay-carried HostHello), recomputes the fpr locally,
 * asserts it equals BOTH `msg.enrollFpr` AND the pin outcome, and only then verifies the signature
 * against that registry pubkey. Any disagreement ⇒ FingerprintMismatchError, no key derived.
 *
 * Anti-proof-replay: `deviceAuthProof` is bound to THIS handshake's `clientEphPub`/`clientNonce`
 * (issued by P5, injected). The host's `verifyDeviceProof` receives that binding so a captured
 * bearer proof replayed into a different ephemeral handshake is rejected (INV3).
 */
import type {
  AeadAlg,
  ClientHandshake,
  ClientHello,
  DeviceAuthProofProvider,
  DevicePinStore,
  HandshakeResult,
  HostHello,
} from 'relay-contracts'
import { ClientHelloSchema, HostHelloSchema } from 'relay-contracts'
import { randomBytes } from './crypto-provider.js'
import { computeEnrollFpr, resolvePin, verifyPinnedFingerprint } from './fingerprint.js'
import { deriveSessionKeys } from './hkdf.js'
import { FingerprintMismatchError, HandshakeStateError } from './errors.js'
import { buildTranscript } from './transcript.js'
import { deriveSharedSecret, generateEphemeralKeyPair, type EphemeralKeyPair } from './x25519.js'

const NONCE_BYTES = 32

/** Fixed AEAD strength preference (strongest first). No fallback to an unoffered alg. */
export const AEAD_PREFERENCE: readonly AeadAlg[] = ['xchacha20-poly1305', 'aes-256-gcm']

export type HandshakePhase =
  | 'init'
  | 'awaitHostHello'
  | 'awaitClientHello'
  | 'established'
  | 'aborted'

export interface HostSigner {
  sign(transcript: Uint8Array): Promise<Uint8Array>
}
export interface HostVerifier {
  verify(pubkey: Uint8Array, transcript: Uint8Array, sig: Uint8Array): Promise<boolean>
}

/** Pick the strongest alg present in both offers; null if the intersection is empty. */
function negotiate(offer: readonly AeadAlg[], supported: readonly AeadAlg[]): AeadAlg | null {
  for (const alg of AEAD_PREFERENCE) {
    if (offer.includes(alg) && supported.includes(alg)) return alg
  }
  return null
}

// ---------------------------------------------------------------------------
// CLIENT (browser, P6)
// ---------------------------------------------------------------------------

export interface ClientHandshakeWithPhase extends ClientHandshake {
  readonly phase: HandshakePhase
}

export interface CreateClientHandshakeDeps {
  readonly aeadOffer: readonly AeadAlg[]
  readonly deviceAuthProofProvider: DeviceAuthProofProvider
  readonly verifier: HostVerifier
  readonly pinStore: DevicePinStore
  readonly hostId: string
}

class ClientHandshakeImpl implements ClientHandshakeWithPhase {
  #phase: HandshakePhase = 'init'
  #eph: EphemeralKeyPair | null = null
  #clientNonce: Uint8Array | null = null
  constructor(private readonly deps: CreateClientHandshakeDeps) {}

  get phase(): HandshakePhase {
    return this.#phase
  }

  async start(): Promise<ClientHello> {
    if (this.#phase !== 'init') {
      throw new HandshakeStateError(`start() illegal in phase ${this.#phase}`)
    }
    const eph = await generateEphemeralKeyPair()
    const clientNonce = randomBytes(NONCE_BYTES)
    const deviceAuthProof = await this.deps.deviceAuthProofProvider.proofFor(this.deps.hostId, {
      clientEphPub: eph.publicKey,
      clientNonce,
    })
    this.#eph = eph
    this.#clientNonce = clientNonce
    this.#phase = 'awaitHostHello'
    return {
      clientEphPub: eph.publicKey,
      clientNonce,
      aeadOffer: this.deps.aeadOffer,
      deviceAuthProof,
    }
  }

  async onHostHello(msg: HostHello, agentPubkey: Uint8Array): Promise<HandshakeResult> {
    if (this.#phase !== 'awaitHostHello' || !this.#eph || !this.#clientNonce) {
      throw new HandshakeStateError(`onHostHello() illegal in phase ${this.#phase}`)
    }
    const parsed = HostHelloSchema.parse(msg)

    // (a)+(b) Host-key sourcing: recompute fpr from the REGISTRY pubkey; assert it matches BOTH the
    // relay-carried enrollFpr string AND the browser pin. Never a string-only fallback.
    const computedFpr = computeEnrollFpr(agentPubkey)
    if (!verifyPinnedFingerprint(agentPubkey, parsed.enrollFpr)) {
      this.#phase = 'aborted'
      throw new FingerprintMismatchError('host_hello enrollFpr != recomputed registry fpr')
    }
    const pin = await resolvePin(this.deps.pinStore, this.deps.hostId, agentPubkey)
    if (pin.outcome === 'mismatch') {
      this.#phase = 'aborted'
      throw new FingerprintMismatchError('pinned fingerprint mismatch (no auto-repin)')
    }

    if (!this.deps.aeadOffer.includes(parsed.aeadChoice)) {
      this.#phase = 'aborted'
      throw new HandshakeStateError('host chose an AEAD alg not in the client offer')
    }

    const transcript = buildTranscript({
      clientEphPub: this.#eph.publicKey,
      clientNonce: this.#clientNonce,
      aeadOffer: this.deps.aeadOffer,
      hostEphPub: parsed.hostEphPub,
      hostNonce: parsed.hostNonce,
      aeadChoice: parsed.aeadChoice,
      enrollFpr: computedFpr,
    })

    // (c) Verify signature against the REGISTRY pubkey (never a relay-supplied value).
    const ok = await this.deps.verifier.verify(agentPubkey, transcript, parsed.sig)
    if (!ok) {
      this.#phase = 'aborted'
      throw new FingerprintMismatchError('host_hello signature invalid')
    }

    // TOFU: record the pin only after a fully valid host_hello.
    if (pin.outcome === 'tofu-first-use') {
      await this.deps.pinStore.pin(this.deps.hostId, computedFpr)
    }

    const shared = await deriveSharedSecret(this.#eph.privateKey, parsed.hostEphPub)
    const keys = await deriveSessionKeys(
      shared,
      this.#clientNonce,
      parsed.hostNonce,
      parsed.aeadChoice,
    )
    this.#phase = 'established'
    return { keys, aead: parsed.aeadChoice, transcript }
  }
}

export function createClientHandshake(deps: CreateClientHandshakeDeps): ClientHandshakeWithPhase {
  return new ClientHandshakeImpl(deps)
}

// ---------------------------------------------------------------------------
// HOST (agent, P2)
// ---------------------------------------------------------------------------

export interface HostHandshake {
  readonly phase: HandshakePhase
  readonly result: HandshakeResult | null
  onClientHello(msg: ClientHello): Promise<HostHello>
}

export interface CreateHostHandshakeDeps {
  readonly signer: HostSigner
  readonly agentPubkey: Uint8Array
  readonly supported: readonly AeadAlg[]
  readonly verifyDeviceProof: (
    proof: string,
    binding: { clientEphPub: Uint8Array; clientNonce: Uint8Array },
  ) => Promise<boolean>
}

class HostHandshakeImpl implements HostHandshake {
  #phase: HandshakePhase = 'init'
  #result: HandshakeResult | null = null
  constructor(private readonly deps: CreateHostHandshakeDeps) {}

  get phase(): HandshakePhase {
    return this.#phase
  }
  get result(): HandshakeResult | null {
    return this.#result
  }

  async onClientHello(msg: ClientHello): Promise<HostHello> {
    if (this.#phase !== 'init') {
      throw new HandshakeStateError(`onClientHello() illegal in phase ${this.#phase}`)
    }
    const parsed = ClientHelloSchema.parse(msg)

    // INV3 gate: proof MUST be bound to this handshake's ephemeral material.
    const proofOk = await this.deps.verifyDeviceProof(parsed.deviceAuthProof, {
      clientEphPub: parsed.clientEphPub,
      clientNonce: parsed.clientNonce,
    })
    if (!proofOk) {
      this.#phase = 'aborted'
      throw new HandshakeStateError('deviceAuthProof failed verification (deny-by-default)')
    }

    const aeadChoice = negotiate(parsed.aeadOffer, this.deps.supported)
    if (!aeadChoice) {
      this.#phase = 'aborted'
      throw new HandshakeStateError('no common AEAD algorithm')
    }

    const eph = await generateEphemeralKeyPair()
    const hostNonce = randomBytes(NONCE_BYTES)
    const enrollFpr = computeEnrollFpr(this.deps.agentPubkey)

    const transcript = buildTranscript({
      clientEphPub: parsed.clientEphPub,
      clientNonce: parsed.clientNonce,
      aeadOffer: parsed.aeadOffer,
      hostEphPub: eph.publicKey,
      hostNonce,
      aeadChoice,
      enrollFpr,
    })
    const sig = await this.deps.signer.sign(transcript)

    const shared = await deriveSharedSecret(eph.privateKey, parsed.clientEphPub)
    const keys = await deriveSessionKeys(shared, parsed.clientNonce, hostNonce, aeadChoice)
    this.#result = { keys, aead: aeadChoice, transcript }
    this.#phase = 'established'
    return { hostEphPub: eph.publicKey, hostNonce, aeadChoice, enrollFpr, sig }
  }
}

export function createHostHandshake(deps: CreateHostHandshakeDeps): HostHandshake {
  return new HostHandshakeImpl(deps)
}
