/**
 * Outbound mTLS dial — PLAN_RELAY_AGENT T12 (INV14/INV4). Builds a wss:// client authenticated by
 * the client cert + IN-PROCESS Ed25519 key + pinned CA chain, `rejectUnauthorized: true`, and NO
 * bearer/agent token (mTLS IS the auth). Absent/expired cert ⇒ fail-fast, no dial.
 */
import { X509Certificate } from 'node:crypto'
import type { Keystore } from '../keys/keystore.js'
import type { AgentConfig } from '../config/agentConfig.js'
import type { WsLike } from './seams.js'

export class NotEnrolledError extends Error {
  constructor() {
    super('agent is not enrolled (no identity/cert in keystore)')
    this.name = 'NotEnrolledError'
  }
}
export class CertExpiredError extends Error {
  constructor() {
    super('client certificate has expired; renew before dialling')
    this.name = 'CertExpiredError'
  }
}

/** TLS material for the wss client. NOTE: `rejectUnauthorized` is ALWAYS true (anti-MITM). */
export interface TlsClientOptions {
  readonly cert: string
  readonly key: string
  /**
   * CA(s) to verify the SERVER cert against. Set to the private tunnel CA for the relay dial; left
   * undefined for a request whose server is publicly trusted (the LE-fronted control-plane /renew) so
   * Node verifies against the system roots — pinning the private CA there fails with "unable to get
   * local issuer certificate".
   */
  readonly ca?: string
  readonly rejectUnauthorized: true
}

export interface CertInfo {
  readonly validTo: Date
}
export type CertParser = (certPem: string) => CertInfo
const defaultCertParser: CertParser = (pem) => ({ validTo: new Date(new X509Certificate(pem).validTo) })

/**
 * Assemble the mTLS options from the keystore. Throws NotEnrolledError if key/cert are missing,
 * CertExpiredError if the cert is past `validTo`. There is NO token field by construction (INV4).
 */
export function buildTlsOptions(
  ks: Keystore,
  opts: { now?: Date; certParser?: CertParser } = {},
): TlsClientOptions {
  const id = ks.loadIdentity()
  const certs = ks.loadCert()
  if (id === null || certs === null) throw new NotEnrolledError()
  const parse = opts.certParser ?? defaultCertParser
  const now = opts.now ?? new Date()
  if (parse(certs.certPem).validTo.getTime() < now.getTime()) throw new CertExpiredError()
  return {
    cert: certs.certPem,
    key: id.exportPrivatePkcs8Pem(),
    ca: certs.caChainPem,
    rejectUnauthorized: true,
  }
}

export interface RawTlsWs {
  send(data: Uint8Array): void
  close(): void
  on(event: string, cb: (...args: unknown[]) => void): void
  once(event: string, cb: (...args: unknown[]) => void): void
}
export type TlsWsConstructor = new (url: string, opts: TlsClientOptions) => RawTlsWs

function toU8(data: unknown): Uint8Array | null {
  if (data instanceof Uint8Array) return data
  if (data instanceof ArrayBuffer) return new Uint8Array(data)
  return null
}

function adapt(raw: RawTlsWs): WsLike {
  return {
    send: (d) => raw.send(d),
    on(ev, cb) {
      if (ev === 'message') {
        raw.on('message', (data: unknown) => {
          const bytes = toU8(data)
          if (bytes !== null) cb(bytes)
        })
      } else {
        raw.on(ev, cb)
      }
    },
    close: () => raw.close(),
  }
}

/** Dial the relay's /agent endpoint over mTLS wss. Resolves the connected WsLike on open. */
export function dialRelay(
  cfg: AgentConfig,
  ks: Keystore,
  opts: { Ctor: TlsWsConstructor; now?: Date; certParser?: CertParser },
): Promise<WsLike> {
  const tls = buildTlsOptions(ks, { ...(opts.now ? { now: opts.now } : {}), ...(opts.certParser ? { certParser: opts.certParser } : {}) })
  return new Promise<WsLike>((resolve, reject) => {
    const raw = new opts.Ctor(cfg.relayUrl, tls)
    raw.once('open', () => resolve(adapt(raw)))
    raw.once('error', (err: unknown) => reject(err instanceof Error ? err : new Error(String(err))))
  })
}
