// A host-registered headless service (geolocation, bluetooth, contacts, …) — the UI-less sibling
// of NativeView (docs/service-channel-plan.md). The host registers a *capability*
// (`registerService("geolocation") { params, channel -> ... }`); the SDK talks to it over the
// shared JSON call/event protocol (runtime/channel.ts) via `_creatorUtils.service*`.
//
// Not a public global itself: apps meet services through typed plugin wrappers (Geolocation —
// src/plugins/) or the raw `Service(name)` global (src/plugins/service.ts), both thin layers
// over this exactly like QRScanner wraps NativeViewElement.

import { _channelCall, _channelEmit, _channelOff, _channelOn, type ChannelListeners } from "./channel"

/** @internal Base for typed service plugins. The host session opens lazily on the first `call()`
 *  (`on()` only records listeners), and `close()` releases it — a later `call()` reopens a fresh
 *  session. Session ids are host-allocated (websocket model): the `onEvent` callback is handed
 *  over at open, so no SDK-side ref registry is needed. */
export class ServiceClient {
  readonly serviceName: string
  readonly params: any
  private _serviceId: number | null = null
  private readonly _listeners: ChannelListeners = {}

  constructor(serviceName: string, params: any = {}) {
    this.serviceName = serviceName
    this.params = params
  }

  /** Whether this host registered a `registerService` factory under `name`. */
  static isSupported(name: string): boolean {
    return typeof _creatorUtils !== "undefined" && typeof _creatorUtils.isServiceSupported === "function"
      && _creatorUtils.isServiceSupported(name)
  }

  /** @internal open (or reuse) the host session; throws when the host has no channel/factory. */
  private _ensureOpen(): number {
    if (this._serviceId !== null) return this._serviceId
    if (typeof _creatorUtils === "undefined" || typeof _creatorUtils.serviceOpen !== "function") {
      throw new Error(`Service("${this.serviceName}") is not supported on this host (requires _creatorUtils.serviceOpen)`)
    }
    const id = _creatorUtils.serviceOpen(this.serviceName, JSON.stringify(this.params ?? {}),
      (event: string, dataJson?: string) => _channelEmit(this._listeners, event, dataJson))
    if (id < 0) {
      throw new Error(`Service("${this.serviceName}") is not registered on this host`)
    }
    this._serviceId = id
    return id
  }

  /** Invoke a method on the service (`geo.call("getCurrent", options)`). Args and the result are
   *  JSON-serialized. Rejects if the host has no such service/method. */
  call(method: string, ...args: any[]): Promise<any> {
    let id: number
    try {
      id = this._ensureOpen()
    } catch (e) {
      return Promise.reject(e)
    }
    return _channelCall(
      typeof _creatorUtils !== "undefined" ? _creatorUtils : undefined, "serviceCall",
      `Service("${this.serviceName}") is not supported on this host (requires _creatorUtils.serviceCall)`,
      id, method, args)
  }

  /** Subscribe to an event the service emits (`geo.on("position", cb)`). */
  on(event: string, callback: (data?: any) => void): this {
    _channelOn(this._listeners, event, callback)
    return this
  }
  off(event: string, callback: (data?: any) => void): this {
    _channelOff(this._listeners, event, callback)
    return this
  }

  /** Close the host session (stops whatever the service was doing). Listeners stay registered —
   *  a later `call()` opens a fresh session that delivers to them again. */
  close(): void {
    if (this._serviceId === null) return
    const id = this._serviceId
    this._serviceId = null
    if (typeof _creatorUtils !== "undefined" && typeof _creatorUtils.serviceClose === "function") {
      _creatorUtils.serviceClose(id)
    }
  }
}
