// The JSON wire protocol shared by the two host plugin channels — NativeView
// (`_creatorUI.viewCall`, see ui/NativeView.ts) and headless services
// (`_creatorUtils.serviceCall`, see runtime/service.ts). One protocol, two transports:
// `args`, results, and event payloads travel as JSON strings, and an absent/empty string
// decodes to `undefined`. Typed plugins (QRScanner, CameraView, Geolocation) layer methods
// over this — see src/plugins/.

/** @internal event-name → listeners. NativeView exposes it as `nvl` (host contract). */
export type ChannelListeners = Record<string, ((data?: any) => void)[]>

/** @internal */
export const _channelOn = (listeners: ChannelListeners, event: string, callback: (data?: any) => void): void => {
  (listeners[event] ??= []).push(callback)
}

/** @internal */
export const _channelOff = (listeners: ChannelListeners, event: string, callback: (data?: any) => void): void => {
  const list = listeners[event]
  if (!list) return
  const i = list.indexOf(callback)
  if (i >= 0) list.splice(i, 1)
}

/** @internal deliver a host event: decode the JSON payload, fan out (copy-before-iterate). */
export const _channelEmit = (listeners: ChannelListeners, event: string, dataJson?: string): void => {
  const list = listeners[event]
  if (!list || list.length === 0) return
  const data = dataJson === undefined || dataJson === "" ? undefined : JSON.parse(dataJson)
  for (const cb of [...list]) cb(data)
}

type ChannelCallBridge = (
  id: number, method: string, args: string,
  onComplete: (result?: string) => void, onError: (err: string) => void,
) => void

/** @internal one JSON call over a channel bridge.
 *
 *  Takes the HANDLER OBJECT plus the member name — never a plucked-off function reference. On
 *  Apple hosts the bridge members are JSExport-ed Objective-C/Swift methods, and JavaScriptCore
 *  binds those to their native receiver: invoking a detached `handler.viewCall(…)` with no `this`
 *  fails at runtime with "self type check failed for Objective-C instance method". QuickJS and web
 *  hosts are plain functions that ignore `this`, so a detached call works there and the breakage
 *  looks iOS/macOS-only. Calling through `owner` keeps the receiver attached on every host.
 *
 *  `bridgeName` may be absent from the host object (optional ABI member) — that's the
 *  unsupported-host path, not an error. */
export const _channelCall = (
  owner: Record<string, any> | undefined, bridgeName: string, unsupportedError: string,
  id: number, method: string, args: any[],
): Promise<any> =>
  new Promise((res, rej) => {
    const bridge = owner?.[bridgeName] as ChannelCallBridge | undefined
    if (typeof bridge !== "function") {
      rej(new Error(unsupportedError))
      return
    }
    bridge.call(owner, id, method, JSON.stringify(args),
      (result?: string) => res(result === undefined || result === "" ? undefined : JSON.parse(result)),
      (err: string) => rej(new Error(err)))
  })
