import type {
  Coordinate,
  PlaceRef,
  SearchNearbyOptions,
} from '@crumbs/core/schemas'
import {HTTPException} from 'hono/http-exception'

type LocationIqConfig = {
  apiKey: string
  baseUrl: string
}

type LocationIqResult = {
  place_id?: string | number
  osm_id?: string | number
  osm_type?: string
  lat?: string
  lon?: string
  display_name?: string
  class?: string
  type?: string
  name?: string
  address?: Record<string, string>
}

// a geocoder hit: the client-safe ref plus the raw payload kept as evidence
export type GeocodeHit = {
  ref: PlaceRef
  raw: LocationIqResult
}

const DEFAULT_RADIUS_METERS = 500
const DEFAULT_LIMIT = 20

// upstream bodies can leak account/quota details — log them, return generic
const upstreamError = (
  endpoint: string,
  status: number,
  body: string,
): never => {
  console.error('locationiq upstream error', {
    endpoint,
    status,
    body: body.slice(0, 500),
  })
  throw new HTTPException(502, {message: 'upstream_error'})
}

export function createLocationIqClient(config: LocationIqConfig) {
  const base = config.baseUrl.replace(/\/$/, '')
  return {
    async searchNearby(
      coord: Coordinate,
      opts: SearchNearbyOptions = {},
    ): Promise<GeocodeHit[]> {
      const params = new URLSearchParams({
        key: config.apiKey,
        lat: String(coord.latitude),
        lon: String(coord.longitude),
        radius: String(opts.radiusMeters ?? DEFAULT_RADIUS_METERS),
        limit: String(opts.limit ?? DEFAULT_LIMIT),
        format: 'json',
      })
      const res = await fetch(`${base}/nearby?${params.toString()}`, {
        headers: {Accept: 'application/json'},
      })
      if (res.status === 404) return []
      if (!res.ok)
        upstreamError('nearby', res.status, await res.text())
      const json = (await res.json()) as LocationIqResult[]
      return json.flatMap(toHit)
    },
    async search(
      query: string,
      opts: {limit?: number} = {},
    ): Promise<GeocodeHit[]> {
      const params = new URLSearchParams({
        key: config.apiKey,
        q: query,
        limit: String(opts.limit ?? 1),
        format: 'json',
        addressdetails: '1',
      })
      const res = await fetch(`${base}/search?${params.toString()}`, {
        headers: {Accept: 'application/json'},
      })
      if (res.status === 404) return []
      if (!res.ok)
        upstreamError('search', res.status, await res.text())
      const json = (await res.json()) as LocationIqResult[]
      return json.flatMap(toHit)
    },
    async reverse(coord: Coordinate): Promise<GeocodeHit | null> {
      const params = new URLSearchParams({
        key: config.apiKey,
        lat: String(coord.latitude),
        lon: String(coord.longitude),
        format: 'json',
      })
      const res = await fetch(
        `${base}/reverse?${params.toString()}`,
        {
          headers: {Accept: 'application/json'},
        },
      )
      if (res.status === 404) return null
      if (!res.ok)
        upstreamError('reverse', res.status, await res.text())
      const json = (await res.json()) as
        | LocationIqResult
        | {error: string}
      if ('error' in json) return null
      return toHit(json)[0] ?? null
    },
  }
}

function toHit(r: LocationIqResult): GeocodeHit[] {
  const latitude = r.lat ? Number(r.lat) : NaN
  const longitude = r.lon ? Number(r.lon) : NaN
  // osm type+id is the only quasi-stable key; place_id churns on reindex
  const osmType = r.osm_type
  const osmId = r.osm_id != null ? String(r.osm_id) : undefined
  const nativeId =
    osmType && osmId ? `${osmType}/${osmId}` : r.place_id
  if (
    !nativeId ||
    Number.isNaN(latitude) ||
    Number.isNaN(longitude)
  ) {
    return []
  }
  return [
    {
      ref: {
        id: `liq:${nativeId}`,
        provider: 'locationiq',
        osmType,
        osmId,
        name: r.name ?? r.display_name?.split(',')[0] ?? 'unnamed',
        category: r.type ?? r.class,
        coordinate: {latitude, longitude},
        address: r.display_name,
      },
      raw: r,
    },
  ]
}
