import { createServer as createTcpServer } from 'node:net'
import { createServer as createTlsServer } from 'node:tls'
import { readFileSync, existsSync } from 'node:fs'
import { updateFromCot } from '../utils/cotStore.js'
import { parseTakStreamFrame, parseTraditionalXmlFrame, parseCotPayload } from '../utils/cotParser.js'
import { validateCotAuth } from '../utils/cotAuth.js'
import { getCotSslPaths, getCotPort } from '../utils/cotSsl.js'
import { registerCleanup } from '../utils/shutdown.js'
import { COT_AUTH_TIMEOUT_MS } from '../utils/constants.js'
import { acquire } from '../utils/asyncLock.js'

const serverState = {
  tcpServer: null,
  tlsServer: null,
}
const relaySet = new Set()
const allSockets = new Set()
const socketBuffers = new WeakMap()
const socketAuthTimeout = new WeakMap()

function clearAuthTimeout(socket) {
  const t = socketAuthTimeout.get(socket)
  if (t) {
    clearTimeout(t)
    socketAuthTimeout.delete(socket)
  }
}

function removeFromRelay(socket) {
  relaySet.delete(socket)
  allSockets.delete(socket)
  clearAuthTimeout(socket)
  socketBuffers.delete(socket)
}

function broadcast(senderSocket, rawMessage) {
  for (const s of relaySet) {
    if (s !== senderSocket && !s.destroyed && s.writable) {
      try {
        s.write(rawMessage)
      }
      catch (err) {
        console.error('[cot] Broadcast write error:', err?.message)
      }
    }
  }
}

const createPreview = (payload) => {
  try {
    const str = payload.toString('utf8')
    if (str.startsWith('<')) {
      const s = str.length <= 120 ? str : str.slice(0, 120) + '...'
      // eslint-disable-next-line no-control-regex -- sanitize control chars for log preview
      return s.replace(/[\u0000-\u0008\v\f\u000E-\u001F]/g, '.')
    }
    return 'hex:' + payload.subarray(0, Math.min(40, payload.length)).toString('hex')
  }
  catch {
    return 'hex:' + payload.subarray(0, Math.min(40, payload.length)).toString('hex')
  }
}

async function processFrame(socket, rawMessage, payload, authenticated) {
  const requireAuth = socket._cotRequireAuth !== false
  const debug = socket._cotDebug === true
  const parsed = parseCotPayload(payload)
  if (debug) {
    const preview = createPreview(payload)
    console.log('[cot] payload length:', payload.length, 'parsed:', parsed ? parsed.type : null, 'preview:', preview)
  }
  if (!parsed) return

  if (parsed.type === 'auth') {
    if (authenticated) return
    console.log('[cot] auth attempt username=', parsed.username)
    // Use lock per socket to prevent concurrent auth attempts
    const socketKey = `cot-auth-${socket.remoteAddress || 'unknown'}-${socket.remotePort || 0}`
    await acquire(socketKey, async () => {
      // Re-check authentication state after acquiring lock
      if (socket._cotAuthenticated || socket.destroyed) return
      try {
        const valid = await validateCotAuth(parsed.username, parsed.password)
        console.log('[cot] auth result valid=', valid, 'for username=', parsed.username)
        if (!socket.writable || socket.destroyed) return
        if (valid) {
          clearAuthTimeout(socket)
          relaySet.add(socket)
          socket._cotAuthenticated = true
        }
        else {
          socket.destroy()
        }
      }
      catch (err) {
        console.log('[cot] auth validation error:', err?.message)
        if (!socket.destroyed) socket.destroy()
      }
    }).catch((err) => {
      console.log('[cot] auth lock error:', err?.message)
      if (!socket.destroyed) socket.destroy()
    })
    return
  }

  if (parsed.type === 'cot') {
    if (requireAuth && !authenticated) {
      socket.destroy()
      return
    }
    updateFromCot({ ...parsed, type: parsed.eventType }).catch((err) => {
      console.error('[cot] Error updating from CoT:', err?.message)
    })
    if (authenticated) broadcast(socket, rawMessage)
  }
}

const parseFrame = (buf) => {
  const takResult = parseTakStreamFrame(buf)
  if (takResult) return { result: takResult, frameType: 'tak' }
  if (buf[0] === 0x3C) {
    const xmlResult = parseTraditionalXmlFrame(buf)
    if (xmlResult) return { result: xmlResult, frameType: 'traditional' }
  }
  return { result: null, frameType: null }
}

const processBufferedData = async (socket, buf, authenticated) => {
  if (buf.length === 0) return buf
  const { result, frameType } = parseFrame(buf)
  if (result && socket._cotDebug) {
    console.log('[cot] frame parsed as', frameType, 'bytesConsumed=', result.bytesConsumed)
  }
  if (!result) return buf
  const { payload, bytesConsumed } = result
  const rawMessage = buf.subarray(0, bytesConsumed)
  await processFrame(socket, rawMessage, payload, authenticated)
  if (socket.destroyed) return null
  const remainingBuf = buf.subarray(bytesConsumed)
  socketBuffers.set(socket, remainingBuf)
  return processBufferedData(socket, remainingBuf, authenticated)
}

async function onData(socket, data) {
  const existingBuf = socketBuffers.get(socket)
  const buf = Buffer.concat([existingBuf || Buffer.alloc(0), data])
  socketBuffers.set(socket, buf)
  const authenticated = Boolean(socket._cotAuthenticated)

  if (socket._cotDebug && buf.length > 0 && !socket._cotFirstChunkLogged) {
    socket._cotFirstChunkLogged = true
    const hex = buf.subarray(0, Math.min(80, buf.length)).toString('hex')
    console.log('[cot] first chunk len=', buf.length, 'first bytes (hex):', hex, 'starts with 0xBF:', buf[0] === 0xBF, 'starts with <:', buf[0] === 0x3C)
  }
  await processBufferedData(socket, buf, authenticated)
}

function setupSocket(socket, tls = false) {
  const remote = socket.remoteAddress || 'unknown'
  console.log('[cot] client connected', tls ? '(TLS)' : '(TCP)', 'from', remote)
  allSockets.add(socket)
  const config = useRuntimeConfig()
  socket._cotDebug = Boolean(config.cotDebug)
  socket._cotRequireAuth = config.cotRequireAuth !== false
  if (socket._cotRequireAuth) {
    const timeout = setTimeout(() => {
      if (!socket._cotAuthenticated && !socket.destroyed) {
        console.log('[cot] auth timeout, closing connection from', remote)
        socket.destroy()
      }
    }, COT_AUTH_TIMEOUT_MS)
    socketAuthTimeout.set(socket, timeout)
  }
  else {
    socket._cotAuthenticated = true
    relaySet.add(socket)
  }

  socket.on('data', data => onData(socket, data))
  socket.on('error', (err) => {
    console.error('[cot] Socket error:', err?.message)
  })
  socket.on('close', () => {
    console.log('[cot] client disconnected', socket._cotAuthenticated ? '(was authenticated)' : '', 'from', remote)
    removeFromRelay(socket)
  })
}

function startCotServers() {
  const config = useRuntimeConfig()
  const { certPath, keyPath } = getCotSslPaths(config) || {}
  const hasTls = certPath && keyPath && existsSync(certPath) && existsSync(keyPath)
  const port = getCotPort()

  try {
    if (hasTls) {
      const tlsOpts = {
        cert: readFileSync(certPath),
        key: readFileSync(keyPath),
        rejectUnauthorized: false,
      }
      serverState.tlsServer = createTlsServer(tlsOpts, socket => setupSocket(socket, true))
      serverState.tlsServer.on('error', err => console.error('[cot] TLS server error:', err?.message))
      serverState.tlsServer.listen(port, '0.0.0.0', () => {
        console.log('[cot] CoT server listening on 0.0.0.0:' + port + ' (TLS) - use this port in ATAK/iTAK and enable SSL')
      })
    }
    else {
      serverState.tcpServer = createTcpServer(socket => setupSocket(socket, false))
      serverState.tcpServer.on('error', err => console.error('[cot] TCP server error:', err?.message))
      serverState.tcpServer.listen(port, '0.0.0.0', () => {
        console.log('[cot] CoT server listening on 0.0.0.0:' + port + ' (plain TCP) - use this port in ATAK/iTAK with SSL disabled')
      })
    }
  }
  catch (err) {
    console.error('[cot] Failed to start CoT server:', err?.message)
    if (err?.code === 'EADDRINUSE') {
      console.error('[cot] Port', port, 'is already in use. Stop the other process or set COT_PORT to a different port.')
    }
  }
}

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('ready', startCotServers)
  // Start immediately so CoT is up before first request in dev; ready may fire late in some setups.
  setImmediate(startCotServers)

  const cleanupServers = () => {
    if (serverState.tcpServer) {
      serverState.tcpServer.close()
      serverState.tcpServer = null
    }
    if (serverState.tlsServer) {
      serverState.tlsServer.close()
      serverState.tlsServer = null
    }
  }

  const cleanupSockets = () => {
    for (const s of allSockets) {
      try {
        s.destroy()
      }
      catch {
        /* ignore */
      }
    }
    allSockets.clear()
    relaySet.clear()
  }

  registerCleanup(async () => {
    cleanupSockets()
    cleanupServers()
  })

  nitroApp.hooks.hook('close', async () => {
    cleanupSockets()
    cleanupServers()
  })
})
