// Geolocation — the device position as a typed service plugin (docs/service-channel-plan.md).
// A wrapper over the host-registered "geolocation" service, exactly like QRScanner wraps the
// "qrScanner" NativeView — except services have no UI surface, so there is nothing to present:
// just `getCurrent()` and `watch()`.
//
// Host-OPTIONAL, like every plugin: it exists where the host registered a "geolocation" service
// (web hosts do out of the box; Android via MainActivity) — gate on `Geolocation.isSupported`.
// The OS permission prompt happens on the first call that needs it (host-side); denial is a
// rejection. Both methods reject immediately on unsupported hosts, BEFORE any permission path.
//
// Service contract ("geolocation"): calls `getCurrent(options) → GeoPosition`,
// `startWatch(options)` (resolves once watching), `stopWatch()`; event `position` → GeoPosition.

import { ServiceClient } from "../runtime/service"

export interface GeoPosition {
  latitude: number
  longitude: number
  /** Horizontal accuracy radius, meters. */
  accuracy: number
  /** Meters above sea level, or null when the provider doesn't know. */
  altitude: number | null
  /** Direction of travel in degrees clockwise from north, or null when stationary/unknown. */
  heading: number | null
  /** Ground speed in m/s, or null when unknown. */
  speed: number | null
  /** When this fix was taken, ms epoch. */
  timestamp: number
}

export interface GeoOptions {
  /** Prefer the precise (GPS) provider — slower first fix, more battery. Default false. */
  highAccuracy?: boolean
  /** getCurrent only: reject with "timeout" if no fix arrives in this many ms. */
  timeout?: number
}

/** A live `Geolocation.watch()` subscription — `stop()` it when the screen goes away. */
export interface GeoWatch {
  stop(): void
}

/**
 * The device position. `Geolocation.isSupported` reports whether this host registered the
 * service — check it before offering location features.
 */
// PURE IIFE so an app that never uses location tree-shakes the whole plugin away (see NativeView).
export const Geolocation: {
  /** Whether this host registered a "geolocation" service. */
  readonly isSupported: boolean
  /** One position fix. Prompts for permission on first use; rejects on denial ("denied"), no
   *  provider ("unavailable"), or `options.timeout` elapsing ("timeout"). */
  getCurrent(options?: GeoOptions): Promise<GeoPosition>
  /** Continuous updates. Resolves once watching (permission granted + provider started) — so a
   *  denial is a rejection, never a silently dead callback. One host watch serves every
   *  subscriber; the options of the watch that starts it win. Always `stop()` when done. */
  watch(callback: (position: GeoPosition) => void, options?: GeoOptions): Promise<GeoWatch>
} = /*#__PURE__*/ (() => {
  let client: ServiceClient | null = null
  const service = (): ServiceClient => client ??= new ServiceClient("geolocation")

  // Watch state: one host watch fans out to N subscribers (SDK-side), the last stop() releases it.
  const watchers: ((position: GeoPosition) => void)[] = []
  let positionHooked = false
  let startPromise: Promise<void> | null = null

  const assertSupported = (): void => {
    // Fail before the host is touched — a permission dialog for a feature that can't exist
    // would be worse than the error (same ordering rule as QRScanner.open).
    if (!ServiceClient.isSupported("geolocation")) {
      throw new Error('Geolocation isn\'t available on this host — it needs a registered "geolocation" service.')
    }
  }

  const removeWatcher = (callback: (position: GeoPosition) => void): void => {
    const i = watchers.indexOf(callback)
    if (i >= 0) watchers.splice(i, 1)
    if (watchers.length === 0 && startPromise !== null) {
      startPromise = null
      service().call("stopWatch").catch(() => {})
    }
  }

  const geolocation = {
    async getCurrent(options?: GeoOptions): Promise<GeoPosition> {
      assertSupported()
      return await service().call("getCurrent", options ?? {})
    },

    async watch(callback: (position: GeoPosition) => void, options?: GeoOptions): Promise<GeoWatch> {
      assertSupported()
      if (!positionHooked) {
        positionHooked = true
        service().on("position", (position?: GeoPosition) => {
          if (position) for (const cb of [...watchers]) cb(position)
        })
      }
      watchers.push(callback)
      startPromise ??= service().call("startWatch", options ?? {})
      try {
        await startPromise
      } catch (e) {
        // The watch never started — unwind without the stopWatch a live watch would need.
        const i = watchers.indexOf(callback)
        if (i >= 0) watchers.splice(i, 1)
        if (watchers.length === 0) startPromise = null
        throw e
      }
      let stopped = false
      return {
        stop(): void {
          if (stopped) return
          stopped = true
          removeWatcher(callback)
        },
      }
    },
  }
  Object.defineProperty(geolocation, "isSupported", {
    get: (): boolean => ServiceClient.isSupported("geolocation"),
  })
  return geolocation as any
})()
