/**
 * @mizan/webview-transport — routes Mizan calls through a VSCode webview's
 * postMessage channel instead of HTTP fetch or Tauri IPC. Paired with the
 * extension-host-side Mizan dispatcher (e.g. `MizanHost` in the holomorphic
 * extension), which receives envelopes via `webview.onDidReceiveMessage`
 * and posts responses back via `webview.postMessage`.
 *
 * Usage (webview side, inside the bundled React/TS app):
 *
 *   import { configure } from '@mizan/base'
 *   import { webviewTransport } from '@mizan/webview-transport'
 *
 *   configure({ transport: webviewTransport() })
 *
 * The transport keeps the same protocol surface as the HTTP and Tauri
 * transports (call/fetch envelopes, {result, invalidate, merge} response
 * shape), so the codegen output and React adapter are unchanged — only
 * the wire channel differs.
 *
 * Envelope shapes (this side ↔ extension-host):
 *
 *   webview → host:
 *     { kind: 'call',  id, fn, args }
 *     { kind: 'fetch', id, context, params? }
 *
 *   host → webview:
 *     { kind: 'response', id, ok: true,  body }
 *     { kind: 'response', id, ok: false, error: { status, body } }
 *
 * Correlation by `id` lets multiple in-flight calls share the one
 * postMessage channel.
 */

import { MizanError, type MizanCallResponse, type MizanTransport } from '@mizan/base'

// VSCode's webview API — present at runtime, declared globally so the
// transport can be authored without pulling vscode types into a generic
// frontend package. Returned by acquireVsCodeApi() exactly once per
// webview load; subsequent calls throw.
declare global {
    function acquireVsCodeApi(): VsCodeApi
}

interface VsCodeApi {
    postMessage(msg: unknown): void
    setState(state: unknown): void
    getState(): unknown
}

type Pending = {
    resolve: (data: unknown) => void
    reject: (err: unknown) => void
}

type CallEnvelope = { kind: 'call'; id: string; fn: string; args: Record<string, any> }
type FetchEnvelope = { kind: 'fetch'; id: string; context: string; params?: Record<string, any> }
type Envelope = CallEnvelope | FetchEnvelope

type ResponseOk = { kind: 'response'; id: string; ok: true; body: any }
type ResponseErr = { kind: 'response'; id: string; ok: false; error: { status: number; body: string } }
type ResponseEnvelope = ResponseOk | ResponseErr

let _api: VsCodeApi | null = null

function api(): VsCodeApi {
    if (_api === null) {
        if (typeof acquireVsCodeApi !== 'function') {
            throw new Error(
                '@mizan/webview-transport: acquireVsCodeApi is not defined. ' +
                'This transport runs inside a VSCode webview only.',
            )
        }
        _api = acquireVsCodeApi()
    }
    return _api
}

const pending = new Map<string, Pending>()
let installed = false
let counter = 0

function install(): void {
    if (installed) return
    installed = true
    window.addEventListener('message', (e: MessageEvent) => {
        const msg = e.data as ResponseEnvelope | undefined
        if (!msg || msg.kind !== 'response') return
        const pen = pending.get(msg.id)
        if (!pen) return
        pending.delete(msg.id)
        if (msg.ok) {
            pen.resolve(msg.body)
        } else {
            pen.reject(new MizanError(msg.error.status, msg.error.body))
        }
    })
}

function nextId(): string {
    return `mz_${++counter}_${Date.now()}`
}

function send<T>(env: Envelope): Promise<T> {
    install()
    return new Promise<T>((resolve, reject) => {
        pending.set(env.id, {
            resolve: resolve as (data: unknown) => void,
            reject,
        })
        api().postMessage(env)
    })
}

/**
 * Build a Mizan transport that routes through a VSCode webview's
 * postMessage channel. Install via:
 *
 *   import { configure } from '@mizan/base'
 *   import { webviewTransport } from '@mizan/webview-transport'
 *   configure({ transport: webviewTransport() })
 */
export function webviewTransport(): MizanTransport {
    return {
        async call(fn, args): Promise<MizanCallResponse> {
            return send<MizanCallResponse>({
                kind: 'call',
                id: nextId(),
                fn,
                args,
            })
        },
        async fetch(context, params) {
            return send<any>({
                kind: 'fetch',
                id: nextId(),
                context,
                params,
            })
        },
    }
}
