/**
 * CoT entity helpers: filters and OSINT → CoT mapping.
 */

/**
 * @param {string} id
 * @returns {'adsb' | 'ais' | 'tak'} Inferred track source.
 */
export function inferSourceFromId(id) {
  if (typeof id !== 'string') return 'tak'
  if (id.startsWith('ICAO.')) return 'adsb'
  if (id.startsWith('MMSI.')) return 'ais'
  return 'tak'
}

/**
 * @param {string} [type]
 * @returns {'air' | 'surface' | 'ground'} CoT display category.
 */
export function cotCategoryFromType(type) {
  const t = typeof type === 'string' ? type : ''
  if (t.startsWith('a-f-A-')) return 'air'
  if (t.startsWith('a-f-S-')) return 'surface'
  return 'ground'
}

/**
 * @param {{ lat: number, lng: number }} point
 * @param {{ west: number, south: number, east: number, north: number }} bbox
 */
export function isInBbox(point, bbox) {
  if (!point || !bbox) return false
  const { lat, lng } = point
  if (!Number.isFinite(lat) || !Number.isFinite(lng)) return false
  return lat >= bbox.south && lat <= bbox.north && lng >= bbox.west && lng <= bbox.east
}

/**
 * @param {Set<string> | string[] | undefined} layers
 * @param {{ type?: string, source?: string }} entity
 */
export function matchesLayerFilter(layers, entity) {
  if (!layers || layers.size === 0) return true
  const category = cotCategoryFromType(entity.type)
  if (layers.has('air') && category === 'air') return true
  if (layers.has('surface') && category === 'surface') return true
  if (layers.has('ground') && category === 'ground') return true
  return false
}

/** OpenSky emitter category → MilStd CoT air type. */
function openSkyCategoryToType(category) {
  if (category === 8) return 'a-f-A-C-H' // rotorcraft
  if (category === 14) return 'a-f-A-C-F' // UAV — plane icon
  return 'a-f-A-C-F'
}

/** OpenSky state vector → CoT upsert. */
export function openSkyStateToCot(state) {
  if (!Array.isArray(state) || state.length < 11) return null
  const icao24 = String(state[0] ?? '').trim().toLowerCase()
  if (!icao24) return null
  const lat = Number(state[6])
  const lng = Number(state[5])
  if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null
  const callsign = typeof state[1] === 'string' ? state[1].trim() : ''
  const originCountry = typeof state[2] === 'string' ? state[2].trim() : ''
  const category = Number(state[17])
  const type = Number.isFinite(category) ? openSkyCategoryToType(category) : 'a-f-A-C-F'
  const heading = Number(state[10])
  const speed = Number(state[9])
  const altitude = Number(state[7])
  const verticalRate = Number(state[11])
  const onGround = state[8] === true
  const squawk = state[14] != null ? String(state[14]).padStart(4, '0') : undefined
  return {
    id: `ICAO.${icao24}`,
    lat,
    lng,
    label: callsign || icao24.toUpperCase(),
    type,
    source: 'adsb',
    icao: icao24,
    originCountry: originCountry || undefined,
    heading: Number.isFinite(heading) ? heading : undefined,
    speed: Number.isFinite(speed) ? speed : undefined,
    altitude: Number.isFinite(altitude) ? altitude : undefined,
    verticalRate: Number.isFinite(verticalRate) ? verticalRate : undefined,
    onGround: onGround || undefined,
    squawk: squawk && squawk !== '0000' ? squawk : undefined,
  }
}

/** AISStream position report → CoT upsert. */
export function aisStreamMessageToCot(message) {
  if (!message || typeof message !== 'object') return null
  const meta = /** @type {Record<string, unknown>} */ (message.MetaData)
  const msg = /** @type {Record<string, unknown>} */ (message.Message)
  if (!msg || typeof msg !== 'object') return null
  const report = /** @type {Record<string, unknown>} */ (
    msg.PositionReport ?? msg.StandardClassBPositionReport ?? msg.ExtendedClassBPositionReport
  )
  if (!report || typeof report !== 'object') return null
  const mmsi = Number(meta?.MMSI ?? report.UserID)
  if (!Number.isFinite(mmsi)) return null
  const lat = Number(report.Latitude)
  const lng = Number(report.Longitude)
  if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null
  const shipName = typeof meta?.ShipName === 'string' ? meta.ShipName.trim() : ''
  const heading = Number(report.Cog ?? report.TrueHeading)
  const speed = Number(report.Sog)
  return {
    id: `MMSI.${mmsi}`,
    lat,
    lng,
    label: shipName || `MMSI ${mmsi}`,
    type: 'a-f-S-C',
    source: 'ais',
    mmsi: String(mmsi),
    heading: Number.isFinite(heading) ? heading : undefined,
    speed: Number.isFinite(speed) ? speed : undefined,
  }
}

/**
 * Union of subscriber bboxes.
 * @param {Array<{ west: number, south: number, east: number, north: number } | null | undefined>} boxes
 */
export function unionBboxes(boxes) {
  let west = Infinity
  let south = Infinity
  let east = -Infinity
  let north = -Infinity
  let has = false
  for (const bbox of boxes) {
    if (!bbox) continue
    has = true
    west = Math.min(west, bbox.west)
    south = Math.min(south, bbox.south)
    east = Math.max(east, bbox.east)
    north = Math.max(north, bbox.north)
  }
  return has ? { west, south, east, north } : null
}

/**
 * Shrink bbox to max span (degrees per axis), centered on midpoint.
 * @param {{ west: number, south: number, east: number, north: number } | null} bbox
 * @param {number} maxDegrees
 */
export function clampBbox(bbox, maxDegrees) {
  if (!bbox || !Number.isFinite(maxDegrees) || maxDegrees <= 0) return bbox
  const latSpan = bbox.north - bbox.south
  const lngSpan = bbox.east - bbox.west
  if (latSpan <= maxDegrees && lngSpan <= maxDegrees) return bbox
  const latMid = (bbox.north + bbox.south) / 2
  const lngMid = (bbox.east + bbox.west) / 2
  const half = maxDegrees / 2
  return {
    south: latMid - half,
    north: latMid + half,
    west: lngMid - half,
    east: lngMid + half,
  }
}

/**
 * @param {string | undefined} raw
 * @returns {Set<string>} Enabled layer names.
 */
export function parseLayersParam(raw) {
  if (!raw || typeof raw !== 'string') {
    return new Set(['air', 'surface', 'ground'])
  }
  const parts = raw.split(',').map(s => s.trim().toLowerCase()).filter(Boolean)
  if (parts.length === 0 || parts.includes('none')) return new Set()
  return new Set(parts)
}

/**
 * @param {string | undefined} raw
 * @returns {{ west: number, south: number, east: number, north: number } | null} Parsed bbox or null.
 */
export function parseBboxParam(raw) {
  if (!raw || typeof raw !== 'string') return null
  const parts = raw.split(',').map(s => Number(s.trim()))
  if (parts.length !== 4 || parts.some(n => !Number.isFinite(n))) return null
  const [west, south, east, north] = parts
  if (south > north || west > east) return null
  return { west, south, east, north }
}
