import { cameraToFeature, featureCollection } from './alprGeo.js'

const OVERPASS_URL = 'https://overpass.deflock.org/api/interpreter'
const MAX_BBOX_DEGREES = 0.5
const CACHE_TTL_MS = 86_400_000
const TILE_SCALE = 10

function httpError(statusCode, message) {
  const err = new Error(message)
  err.statusCode = statusCode
  return err
}

/**
 * @param {unknown} tags
 * @returns {Record<string, string>} String OSM tags only
 */
function normalizeTags(tags) {
  if (!tags || typeof tags !== 'object') return {}
  return Object.fromEntries(
    Object.entries(/** @type {Record<string, unknown>} */ (tags))
      .filter(([, v]) => typeof v === 'string')
      .map(([k, v]) => [k, /** @type {string} */ (v)]),
  )
}

/**
 * @param {unknown} element
 * @returns {{ osmId: number, lat: number, lng: number, manufacturer: string | null, direction: number | null, tags: Record<string, string> } | null} Parsed camera or null
 */
export function parseOverpassElement(element) {
  if (!element || typeof element !== 'object') return null
  const el = /** @type {Record<string, unknown>} */ (element)
  if (el.type !== 'node' || typeof el.id !== 'number') return null
  const lat = Number(el.lat)
  const lng = Number(el.lon ?? el.lng)
  if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null
  const tags = normalizeTags(el.tags)
  const directionRaw = tags['camera:direction'] ?? tags.direction
  const direction = directionRaw != null && directionRaw !== '' ? Number(directionRaw) : null
  const fovRaw = tags['camera:angle'] ?? tags['surveillance:angle']
  const fov = fovRaw != null && fovRaw !== '' ? Number(fovRaw) : null
  return {
    osmId: el.id,
    lat,
    lng,
    manufacturer: tags.manufacturer ?? tags.brand ?? null,
    direction: Number.isFinite(direction) ? direction : null,
    fov: Number.isFinite(fov) ? fov : null,
    tags,
  }
}

export function parseBbox(query) {
  const south = Number(query.south)
  const west = Number(query.west)
  const north = Number(query.north)
  const east = Number(query.east)
  if (![south, west, north, east].every(Number.isFinite)) {
    throw httpError(400, 'south, west, north, east required')
  }
  if (south >= north || west >= east) {
    throw httpError(400, 'invalid bbox')
  }
  if (north - south > MAX_BBOX_DEGREES || east - west > MAX_BBOX_DEGREES) {
    throw httpError(400, `bbox exceeds ${MAX_BBOX_DEGREES}°`)
  }
  return { south, west, north, east }
}

/** @param {{ south: number, west: number, north: number, east: number }} bbox */
export function tileKeysForBbox(bbox) {
  const latMin = Math.floor(bbox.south * TILE_SCALE)
  const latMax = Math.floor(bbox.north * TILE_SCALE)
  const lngMin = Math.floor(bbox.west * TILE_SCALE)
  const lngMax = Math.floor(bbox.east * TILE_SCALE)
  const keys = []
  for (let lat = latMin; lat <= latMax; lat++) {
    for (let lng = lngMin; lng <= lngMax; lng++) {
      keys.push(`${lat}:${lng}`)
    }
  }
  return keys
}

export function buildBboxQuery(bbox) {
  const { south, west, north, east } = bbox
  return `[out:json][timeout:25];node["surveillance:type"="ALPR"](${south},${west},${north},${east});out body;`
}

export function buildGlobalQuery() {
  return '[out:json][timeout:300];node["surveillance:type"="ALPR"];out body;'
}

function rowToCamera(row, source) {
  const tags = (() => {
    try {
      return JSON.parse(row.tags)
    }
    catch {
      return {}
    }
  })()
  const fovRaw = tags['camera:angle'] ?? tags['surveillance:angle']
  const fov = fovRaw != null && fovRaw !== '' ? Number(fovRaw) : null
  return {
    osmId: row.osm_id,
    lat: row.lat,
    lng: row.lng,
    manufacturer: row.manufacturer ?? null,
    direction: row.direction ?? null,
    fov: Number.isFinite(fov) ? fov : null,
    tags,
    source,
  }
}

/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
export async function upsertAlprNode(db, camera) {
  const now = new Date().toISOString()
  await db.run(
    `INSERT INTO alpr_nodes (osm_id, lat, lng, manufacturer, direction, tags, fetched_at)
     VALUES (?, ?, ?, ?, ?, ?, ?)
     ON CONFLICT(osm_id) DO UPDATE SET
       lat = excluded.lat,
       lng = excluded.lng,
       manufacturer = excluded.manufacturer,
       direction = excluded.direction,
       tags = excluded.tags,
       fetched_at = excluded.fetched_at`,
    [
      camera.osmId,
      camera.lat,
      camera.lng,
      camera.manufacturer,
      camera.direction,
      JSON.stringify(camera.tags),
      now,
    ],
  )
}

/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
export async function upsertAlprNodesBatch(db, cameras) {
  if (!cameras.length) return
  await db.run('BEGIN TRANSACTION')
  try {
    for (const camera of cameras) {
      await upsertAlprNode(db, camera)
    }
    await db.run('COMMIT')
  }
  catch (error) {
    await db.run('ROLLBACK').catch(() => {})
    throw error
  }
}

/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
export async function markTilesFetched(db, keys) {
  if (!keys.length) return
  const now = new Date().toISOString()
  await db.run('BEGIN TRANSACTION')
  try {
    for (const key of keys) {
      await db.run('INSERT OR REPLACE INTO alpr_tiles (tile_key, fetched_at) VALUES (?, ?)', [key, now])
    }
    await db.run('COMMIT')
  }
  catch (error) {
    await db.run('ROLLBACK').catch(() => {})
    throw error
  }
}

/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
async function tilesAreFresh(db, keys) {
  if (!keys.length) return false
  const placeholders = keys.map(() => '?').join(',')
  const rows = await db.all(
    `SELECT tile_key, fetched_at FROM alpr_tiles WHERE tile_key IN (${placeholders})`,
    keys,
  )
  if (rows.length !== keys.length) return false
  const cutoff = Date.now() - CACHE_TTL_MS
  return rows.every(row => Date.parse(row.fetched_at) >= cutoff)
}

/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
export async function getCachedAlprNodes(db, bbox) {
  const rows = await db.all(
    `SELECT osm_id, lat, lng, manufacturer, direction, tags
     FROM alpr_nodes
     WHERE lat >= ? AND lat <= ? AND lng >= ? AND lng <= ?`,
    [bbox.south, bbox.north, bbox.west, bbox.east],
  )
  return rows.map(row => rowToCamera(row, 'cache'))
}

export async function fetchOverpass(query) {
  const res = await fetch(OVERPASS_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: `data=${encodeURIComponent(query)}`,
    signal: AbortSignal.timeout(120_000),
  })
  if (!res.ok) {
    throw httpError(502, `Overpass error: ${res.status}`)
  }
  const data = await res.json()
  const elements = Array.isArray(data?.elements) ? data.elements : []
  return elements.map(parseOverpassElement).filter(Boolean)
}

/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
async function fetchAndCacheBbox(db, bbox, tileKeys) {
  const live = await fetchOverpass(buildBboxQuery(bbox))
  await upsertAlprNodesBatch(db, live)
  await markTilesFetched(db, tileKeys)
  return live.map(c => ({ ...c, source: 'live' }))
}

const refreshInFlight = new Map()

/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
function refreshBboxInBackground(db, bbox, tileKeys) {
  const key = tileKeys.join('|')
  if (refreshInFlight.has(key)) return
  const job = fetchAndCacheBbox(db, bbox, tileKeys)
    .catch(() => {})
    .finally(() => refreshInFlight.delete(key))
  refreshInFlight.set(key, job)
}

/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
export async function getAlprCameras(db, bbox) {
  const tileKeys = tileKeysForBbox(bbox)
  const cached = await getCachedAlprNodes(db, bbox)

  if (await tilesAreFresh(db, tileKeys)) {
    return featureCollection(cached.map(cameraToFeature), 'cache')
  }

  if (cached.length > 0) {
    refreshBboxInBackground(db, bbox, tileKeys)
    return featureCollection(cached.map(cameraToFeature), 'cache')
  }

  try {
    const live = await fetchAndCacheBbox(db, bbox, tileKeys)
    return featureCollection(live.map(cameraToFeature), 'live')
  }
  catch {
    return featureCollection(cached.map(cameraToFeature), 'cache')
  }
}

/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
export async function importAllAlprNodes(db) {
  const cameras = await fetchOverpass(buildGlobalQuery())
  await upsertAlprNodesBatch(db, cameras)
  return cameras.length
}
