/**
 * Loopback forwarder — PLAN_RELAY_AGENT T8. One OPEN ⇒ one fresh ws://127.0.0.1:3000<path>
 * socket, REPLAYING the real browser `Origin` (§4.1 MuxOpen.originHeader) so the UNCHANGED base
 * app's Origin check still passes end-to-end (CSWSH protection preserved — EXPLORE §3).
 *
 * The raw `ws` constructor is injectable so the URL/Origin wiring is unit-testable without a real
 * socket. The target is always loopback (validated upstream in config).
 */
import type { WsLike } from './seams.js'

export type DialLoopback = (path: string, origin: string) => Promise<WsLike>

/** Minimal surface of a raw `ws` client the adapter needs. */
export interface RawWs {
  send(data: Uint8Array): void
  close(): void
  on(event: string, cb: (...args: unknown[]) => void): void
  once(event: string, cb: (...args: unknown[]) => void): void
}
export type WsConstructor = new (
  url: string,
  opts?: { headers?: Record<string, string> },
) => RawWs

/** Join the loopback target with the request path (avoids a double slash). */
export function buildLoopbackUrl(target: string, path: string): string {
  const base = target.endsWith('/') ? target.slice(0, -1) : target
  const suffix = path.startsWith('/') ? path : `/${path}`
  return `${base}${suffix}`
}

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: RawWs): WsLike {
  return {
    send(d: Uint8Array): void {
      raw.send(d)
    },
    on(ev: 'message' | 'close' | 'error', cb: (...a: unknown[]) => void): void {
      if (ev === 'message') {
        raw.on('message', (data: unknown) => {
          const bytes = toU8(data)
          if (bytes !== null) cb(bytes)
        })
      } else {
        raw.on(ev, cb)
      }
    },
    close(): void {
      raw.close()
    },
  }
}

/**
 * Build a DialLoopback bound to `target`. Resolves once the loopback socket is open; rejects on
 * a pre-open error (so a down base app surfaces as a typed dial failure, not a silent hang).
 */
export function dialLoopback(target: string, Ctor: WsConstructor): DialLoopback {
  return (path: string, origin: string): Promise<WsLike> =>
    new Promise<WsLike>((resolve, reject) => {
      const url = buildLoopbackUrl(target, path)
      const raw = new Ctor(url, { headers: { Origin: origin } })
      raw.once('open', () => resolve(adapt(raw)))
      raw.once('error', (err: unknown) => reject(err instanceof Error ? err : new Error(String(err))))
    })
}
