import { XMLParser } from 'fast-xml-parser'
import { MAX_PAYLOAD_BYTES } from './constants.js'

// CoT protocol detection constants
export const COT_FIRST_BYTE_TAK = 0xBF
export const COT_FIRST_BYTE_XML = 0x3C

/** @param {number} byte - First byte of stream. @returns {boolean} */
export function isCotFirstByte(byte) {
  return byte === COT_FIRST_BYTE_TAK || byte === COT_FIRST_BYTE_XML
}

const TRADITIONAL_DELIMITER = Buffer.from('</event>', 'utf8')

/**
 * @param {Buffer} buf
 * @param {number} offset
 * @param {number} value - Accumulated value
 * @param {number} shift - Current bit shift
 * @param {number} bytesRead - Bytes consumed so far
 * @returns {{ value: number, bytesRead: number }} Decoded varint and bytes consumed.
 */
function readVarint(buf, offset, value = 0, shift = 0, bytesRead = 0) {
  if (offset + bytesRead >= buf.length) return { value, bytesRead }
  const b = buf[offset + bytesRead]
  const newValue = value + ((b & 0x7F) << shift)
  const newBytesRead = bytesRead + 1
  if ((b & 0x80) === 0) return { value: newValue, bytesRead: newBytesRead }
  const newShift = shift + 7
  if (newShift > 28) return { value: 0, bytesRead: 0 }
  return readVarint(buf, offset, newValue, newShift, newBytesRead)
}

/**
 * TAK stream frame: 0xBF, varint length, payload.
 * @param {Buffer} buf
 * @returns {{ payload: Buffer, bytesConsumed: number } | null} Frame or null if incomplete/invalid.
 */
export function parseTakStreamFrame(buf) {
  if (!buf || buf.length < 2 || buf[0] !== COT_FIRST_BYTE_TAK) return null
  const { value: length, bytesRead } = readVarint(buf, 1)
  if (length < 0 || length > MAX_PAYLOAD_BYTES) return null
  const bytesConsumed = 1 + bytesRead + length
  if (buf.length < bytesConsumed) return null
  return { payload: buf.subarray(1 + bytesRead, bytesConsumed), bytesConsumed }
}

/**
 * Traditional CoT: one XML message delimited by </event>.
 * @param {Buffer} buf
 * @returns {{ payload: Buffer, bytesConsumed: number } | null} Frame or null if incomplete.
 */
export function parseTraditionalXmlFrame(buf) {
  if (!buf || buf.length < 8 || buf[0] !== COT_FIRST_BYTE_XML) return null
  const idx = buf.indexOf(TRADITIONAL_DELIMITER)
  if (idx === -1) return null
  const bytesConsumed = idx + TRADITIONAL_DELIMITER.length
  if (bytesConsumed > MAX_PAYLOAD_BYTES) return null
  return { payload: buf.subarray(0, bytesConsumed), bytesConsumed }
}

const xmlParser = new XMLParser({
  ignoreAttributes: false,
  attributeNamePrefix: '@_',
  parseTagValue: false,
  ignoreDeclaration: true,
  ignorePiTags: true,
  processEntities: false, // Disable entity expansion to prevent XML bomb attacks
  maxAttributes: 100,
  parseAttributeValue: false,
  trimValues: true,
  parseTrueNumberOnly: false,
  arrayMode: false,
  stopNodes: [], // Could add depth limit here if needed
})

/**
 * Case-insensitive key lookup in nested object.
 * @returns {unknown} Found value or undefined.
 */
function findInObject(obj, key) {
  if (!obj || typeof obj !== 'object') return undefined
  const k = key.toLowerCase()
  for (const [name, val] of Object.entries(obj)) {
    if (name.toLowerCase() === k) return val
    if (typeof val === 'object' && val !== null) {
      const found = findInObject(val, key)
      if (found !== undefined) return found
    }
  }
  return undefined
}

/**
 * Extract { username, password } from detail.auth (or __auth / credentials).
 * @returns {{ username: string, password: string } | null} Credentials or null if missing/invalid.
 */
function extractAuth(parsed) {
  const detail = findInObject(parsed, 'detail')
  if (!detail || typeof detail !== 'object') return null
  const auth = findInObject(detail, 'auth') ?? findInObject(detail, '__auth') ?? findInObject(detail, 'credentials')
  if (!auth || typeof auth !== 'object') return null
  const username = auth['@_username'] ?? auth['@_Username'] ?? auth.username
  const password = auth['@_password'] ?? auth['@_Password'] ?? auth.password
  if (typeof username !== 'string' || typeof password !== 'string' || !username.trim()) return null
  return { username: username.trim(), password }
}

/**
 * Parse CoT XML payload into auth or position. Does not mutate payload.
 * @param {Buffer} payload - UTF-8 XML
 * @returns {{ type: 'auth', username: string, password: string } | { type: 'cot', id: string, lat: number, lng: number, label: string, eventType: string } | null} Auth or position, or null.
 */
export function parseCotPayload(payload) {
  if (!payload?.length) return null
  const str = payload.toString('utf8').trim()
  if (!str.startsWith('<')) return null
  try {
    const parsed = xmlParser.parse(str)
    const event = findInObject(parsed, 'event')
    if (!event || typeof event !== 'object') return null

    const auth = extractAuth(parsed)
    if (auth) return { type: 'auth', username: auth.username, password: auth.password }

    const uid = String(event['@_uid'] ?? event.uid ?? '')
    const eventType = String(event['@_type'] ?? event.type ?? '')
    const point = findInObject(parsed, 'point') ?? findInObject(event, 'point')
    const extractCoords = (pt) => {
      if (!pt || typeof pt !== 'object') return { lat: Number.NaN, lng: Number.NaN }
      return {
        lat: Number(pt['@_lat'] ?? pt.lat),
        lng: Number(pt['@_lon'] ?? pt.lon ?? pt['@_lng'] ?? pt.lng),
      }
    }
    const { lat, lng } = extractCoords(point)
    if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null

    const detail = findInObject(parsed, 'detail')
    const contact = detail && typeof detail === 'object' ? (findInObject(detail, 'contact') ?? detail) : null
    const callsign = contact && typeof contact === 'object'
      ? (contact['@_callsign'] ?? contact.callsign ?? contact['@_Callsign'])
      : ''
    const label = typeof callsign === 'string' ? callsign.trim() || uid : uid

    return { type: 'cot', id: uid, lat, lng, label, eventType }
  }
  catch {
    return null
  }
}
