/**
 * P5 signing-key setup + capability-token / DPoP bundle builder. These wire the REAL
 * relay-auth issue + DPoP primitives. `resetVerifyKeyForTest`, `resetDpopCacheForTest`,
 * `buildDpopProof`, `jwkThumbprint`, and the WebCrypto Ed25519 helpers are TEST-ONLY / internal,
 * so they are deep-imported from `relay-auth/src/...` (relay-auth has no `exports` map, so subpath
 * imports resolve through the e2e node_modules symlink).
 */
import { randomUUID } from 'node:crypto'
import { issueCapabilityToken, type DpopContext } from 'relay-auth'
import type { CapabilityRight } from 'relay-contracts'
import { encodeBase64UrlBytes } from 'relay-contracts'
import {
  configureVerifyKey,
  resetVerifyKeyForTest,
} from 'relay-auth/src/config/keys.js'
import {
  generateEd25519KeyPair,
  exportEd25519PublicRaw,
} from 'relay-auth/src/crypto/ed25519.js'
import { buildDpopProof, resetDpopCacheForTest } from 'relay-auth/src/capability/verify.js'
import { jwkThumbprint } from 'relay-auth/src/crypto/thumbprint.js'
import { principal } from './fakes.js'

export { resetDpopCacheForTest, jwkThumbprint }

export interface P5Key {
  readonly signingKey: CryptoKey
  readonly publicRaw: Uint8Array
}

/** Configure the P5 verifying key globally and return the matching private signing key. */
export async function setupP5SigningKey(): Promise<P5Key> {
  resetVerifyKeyForTest()
  const pair = await generateEd25519KeyPair()
  configureVerifyKey(pair.publicKey)
  const publicRaw = await exportEd25519PublicRaw(pair.publicKey)
  return { signingKey: pair.privateKey, publicRaw }
}

export interface Ephemeral {
  readonly privateKey: CryptoKey
  readonly publicRaw: Uint8Array
}

export async function makeEphemeral(): Promise<Ephemeral> {
  const pair = await generateEd25519KeyPair()
  return { privateKey: pair.privateKey, publicRaw: await exportEd25519PublicRaw(pair.publicKey) }
}

export interface IssueOpts {
  readonly accountId: string
  readonly host: string
  readonly aud: string
  readonly rights?: readonly CapabilityRight[]
  readonly ttl?: number
  readonly now: number
  readonly htu?: string
  readonly htm?: string
}

/**
 * A capability token bundled with a matching DPoP proof. `newDpop(at?)` mints a FRESH DPoP proof
 * (new jti) bound to the SAME ephemeral key — needed to re-present the same token under a distinct
 * DPoP (single-use jti / revocation tests) without tripping the DPoP replay cache.
 */
export interface CapBundle {
  readonly raw: string
  readonly dpop: DpopContext
  readonly newDpop: (at?: number) => Promise<DpopContext>
}

export async function issueCapBundle(signingKey: CryptoKey, o: IssueOpts): Promise<CapBundle> {
  const eph = await makeEphemeral()
  const cnfJkt = await jwkThumbprint(eph.publicRaw)
  const raw = await issueCapabilityToken(
    {
      principal: principal(o.accountId),
      aud: o.aud,
      host: o.host,
      rights: o.rights ?? ['attach'],
      ttlSeconds: o.ttl ?? 45,
      cnfJkt,
    },
    signingKey,
    o.now,
  )
  const htu = o.htu ?? `https://${o.aud}/ws`
  const htm = o.htm ?? 'GET'
  const newDpop = async (at: number = o.now): Promise<DpopContext> => ({
    proofJws: await buildDpopProof(eph.privateKey, eph.publicRaw, {
      htu,
      htm,
      jti: randomUUID(),
      iat: at,
    }),
    htu,
    htm,
  })
  return { raw, dpop: await newDpop(o.now), newDpop }
}

/**
 * F4 crafting: a capability whose `cnf.jkt` is the thumbprint of a NON-32-byte blob, plus a DPoP
 * proof whose `header.jwk.x` decodes to that same blob. The thumbprint check therefore PASSES and
 * execution reaches `importEd25519PublicRaw(blob)`, which throws on the bad length — exercising the
 * fail-safe (must resolve to false, never reject).
 */
export interface MalformedOpts {
  readonly accountId: string
  readonly host: string
  readonly aud: string
  readonly now: number
  readonly blobLen?: number
}

export async function craftMalformedDpopBundle(
  signingKey: CryptoKey,
  o: MalformedOpts,
): Promise<{ raw: string; dpop: DpopContext }> {
  const blob = new Uint8Array(o.blobLen ?? 16).fill(7)
  const cnfJkt = await jwkThumbprint(blob) // 43-char base64url regardless of blob length
  const raw = await issueCapabilityToken(
    {
      principal: principal(o.accountId),
      aud: o.aud,
      host: o.host,
      rights: ['attach'],
      ttlSeconds: 45,
      cnfJkt,
    },
    signingKey,
    o.now,
  )
  const htu = `https://${o.aud}/ws`
  const enc = (x: unknown): string =>
    encodeBase64UrlBytes(new TextEncoder().encode(JSON.stringify(x)))
  const h = enc({ typ: 'dpop+ed25519', jwk: { crv: 'Ed25519', kty: 'OKP', x: encodeBase64UrlBytes(blob) } })
  const p = enc({ htu, htm: 'GET', jti: randomUUID(), iat: o.now })
  const s = encodeBase64UrlBytes(new Uint8Array(64)) // arbitrary signature bytes
  return { raw, dpop: { proofJws: `${h}.${p}.${s}`, htu, htm: 'GET' } }
}
