'use strict'
const snmp = require('net-snmp')
const cfg = require('./config')
const { log } = require('./helpers')

const olts = new Map()
let pollTimer = null
const POLL_INTERVAL = cfg.OLT_POLL_INTERVAL || 300000
const SNMP_TIMEOUT = 4000

// --- TP-Link DeltaStream GPON OLT MIB (enterprise 11863) ---
// ONU table: .6.100.1.7.2.1.{col}.0.{port}.{onu}
const TPLINK = {
  BASE: '1.3.6.1.4.1.11863.6.100.1.7.2.1',
  COL_PORT_NAME: 3, COL_ONU_IDX: 4, COL_DESCRIPTION: 5, COL_SERIAL: 6,
  COL_LINE_PROF: 7, COL_STATUS: 8, COL_VENDOR: 15, COL_MODEL: 16,
  COL_UPTIME: 19, COL_HW_VER: 20, COL_RX_POWER: 26, COL_TX_POWER: 27,
  COL_ONU_RX: 28, COL_DISTANCE: 30, COL_TEMPERATURE: 31, COL_LAST_DOWN: 32,
  COL_FIRMWARE: 33, COL_OFFLINE_CAUSE: 42, COL_MAC: 43,
  COL_MAP: {},
  FETCH_COLS: [],
}
TPLINK.COL_MAP = {
  [TPLINK.COL_PORT_NAME]: 'port', [TPLINK.COL_ONU_IDX]: 'onuIdx',
  [TPLINK.COL_DESCRIPTION]: 'description', [TPLINK.COL_SERIAL]: 'serial',
  [TPLINK.COL_VENDOR]: 'vendor', [TPLINK.COL_MODEL]: 'model',
  [TPLINK.COL_UPTIME]: 'uptime', [TPLINK.COL_RX_POWER]: 'rxPowerOlt',
  [TPLINK.COL_TX_POWER]: 'txPowerOlt', [TPLINK.COL_ONU_RX]: 'rxPowerOnu',
  [TPLINK.COL_DISTANCE]: 'distance', [TPLINK.COL_TEMPERATURE]: 'temperature',
  [TPLINK.COL_FIRMWARE]: 'firmware', [TPLINK.COL_OFFLINE_CAUSE]: 'lastOfflineCause',
  [TPLINK.COL_MAC]: 'mac', [TPLINK.COL_LINE_PROF]: 'lineProfile',
}
TPLINK.FETCH_COLS = [
  TPLINK.COL_PORT_NAME, TPLINK.COL_ONU_IDX, TPLINK.COL_DESCRIPTION, TPLINK.COL_SERIAL,
  TPLINK.COL_STATUS, TPLINK.COL_VENDOR, TPLINK.COL_MODEL, TPLINK.COL_UPTIME,
  TPLINK.COL_RX_POWER, TPLINK.COL_TX_POWER, TPLINK.COL_ONU_RX,
  TPLINK.COL_DISTANCE, TPLINK.COL_TEMPERATURE, TPLINK.COL_FIRMWARE,
  TPLINK.COL_OFFLINE_CAUSE, TPLINK.COL_MAC, TPLINK.COL_LINE_PROF,
]

// backward compat alias
const OID = TPLINK

// --- Raisecom ISCOM6800 GPON OLT MIB (enterprise 8886) ---
// ONU table: 1.3.6.1.4.1.8886.18.3.1.3.1.1.{col}.{onu_id}
// Signal table: 1.3.6.1.4.1.8886.18.3.1.3.3.1.1.{onu_id}  (rx_power in dBm*10)
// onu_id encoding: (slot+32)*8388608 + port*65536 + ontid
const RAISECOM = {
  BASE_TABLE: '1.3.6.1.4.1.8886.18.3.1.3.1.1',
  BASE_SIGNAL: '1.3.6.1.4.1.8886.18.3.1.3.3.1.1',
  COL_SERIAL: 2,
  COL_DISTANCE: 6,
  COL_FIRMWARE: 7,
  COL_STATUS: 8,
  COL_LINE_PROF: 9,
  COL_DEREG_COUNT: 15,
  COL_ONU_RX: 16,
  FETCH_COLS: [2, 6, 7, 8, 9, 16],
  COL_MAP: { 2: 'serial', 6: 'distance', 7: 'firmware', 9: 'lineProfile', 16: 'rxPowerOnu' },
}

function decodeRaisecomOnuId (onuId) {
  const slot = Math.floor(onuId / 8388608) - 32
  const port = Math.floor((onuId % 8388608) / 65536)
  const ontid = onuId % 65536
  return { slot, port, ontid }
}

function registerOlt ({ host, community, name, type }) {
  if (!host || !community) return
  if (olts.has(host)) return olts.get(host)
  const vendor = (type || 'tplink').toLowerCase()
  if (vendor !== 'tplink' && vendor !== 'raisecom') {
    log(`OLT ${name || host}: unknown type "${type}", defaulting to tplink`)
  }
  const olt = {
    host, community, name: name || host,
    type: (vendor === 'raisecom') ? 'raisecom' : 'tplink',
    lastPoll: null, lastError: null, lastDuration: null,
    onuCount: 0, onlineCount: 0, onus: new Map(),
  }
  olts.set(host, olt)
  log(`OLT registered: ${name || host} (${host}) [${olt.type}]`)
  return olt
}

const createSession = (host, community) =>
  snmp.createSession(host, community, { timeout: SNMP_TIMEOUT, retries: 1, version: snmp.Version2c })

function snmpWalk (session, oid) {
  return new Promise((resolve) => {
    const results = []
    const timer = setTimeout(() => {
      try { session.close() } catch {}
      resolve(results)
    }, SNMP_TIMEOUT + 2000)

    session.subtree(oid, 50, (varbinds) => {
      for (const vb of varbinds) {
        if (!snmp.isVarbindError(vb)) results.push(vb)
      }
    }, (error) => {
      clearTimeout(timer)
      resolve(results)
    })
  })
}

function snmpGet (session, oid) {
  return new Promise((resolve) => {
    const timer = setTimeout(() => { try { session.close() } catch {} resolve(null) }, SNMP_TIMEOUT + 1000)
    session.get([oid], (error, varbinds) => {
      clearTimeout(timer)
      if (error || !varbinds?.length || snmp.isVarbindError(varbinds[0])) return resolve(null)
      resolve(varbinds[0])
    })
  })
}

// Raisecom OLT WAN IP OIDs: 1.3.6.1.4.1.8886.18.3.6.6.1.1.13.{olt_id}.{wan_idx}
// olt_id = slot * 10000000 + port * 100000 + ontid
const RAISECOM_WAN_IP_OID = '1.3.6.1.4.1.8886.18.3.6.6.1.1.13'

// Get management IP from Raisecom OLT via SNMP
// Can use either cached ONU data (serial lookup) or explicit slot/port/ontid params
async function getManageIp (serial, opts = {}) {
  let oltHost, community, oltName, slot, port, ontId

  if (serial) {
    const onu = getOnuBySerial(serial)
    if (onu) {
      const olt = olts.get(onu.oltHost)
      if (!olt || olt.type !== 'raisecom') return null
      oltHost = olt.host; community = olt.community; oltName = olt.name
      // Raisecom cache stores port as "0/slot/port" and onuIdx as ontid
      const parts = (onu.port || '').split('/')
      if (parts.length === 3) { slot = parseInt(parts[1]); port = parseInt(parts[2]) }
      ontId = onu.onuIdx
    }
  }

  // Allow explicit params (from ERPNext data when ONU not in cache yet)
  if (opts.oltIp) oltHost = opts.oltIp
  if (opts.community) community = opts.community
  if (opts.slot != null) slot = opts.slot
  if (opts.port != null) port = opts.port
  if (opts.ontId != null) ontId = opts.ontId
  if (!community) community = 'targosnmp'
  if (!oltName) oltName = oltHost

  if (!oltHost || slot == null || port == null || ontId == null) return null

  const oltId = slot * 10000000 + port * 100000 + ontId
  const session = createSession(oltHost, community)
  const ips = []
  try {
    for (let wan = 1; wan <= 4; wan++) {
      const vb = await snmpGet(session, `${RAISECOM_WAN_IP_OID}.${oltId}.${wan}`)
      if (vb) {
        const ip = extractVal(vb)
        if (ip && ip !== '0.0.0.0' && ip !== '') ips.push({ wan, ip })
      }
    }
  } finally { try { session.close() } catch {} }

  if (!ips.length) return null
  const mgmt = ips.find(i => i.ip.startsWith('172.17.') || i.ip.startsWith('10.'))
  return { serial, oltName, oltHost, slot, port, ontId, ips, manageIp: mgmt?.ip || ips[0].ip }
}

// Raisecom per-ONU WAN octet counters (VLAN 40 = service internet) — mirrors F's
// device_ajax/raisecom_rcmg_bandwith.php. Cumulative octets (32-bit, may wrap):
//   rx (download) = 1.3.6.1.4.1.8886.18.3.7.2.4.1.5.{oltId}.40
//   tx (upload)   = 1.3.6.1.4.1.8886.18.3.7.2.4.1.3.{oltId}.40
//   oltId = slot*10000000 + port*100000 + ontid   (same encoding as the WAN-IP table)
// Raisecom (tech-2) ONLY — TP-Link/tech-3 has no live WAN-rate counter in F.
// The CALLER samples twice and computes rate = ((val - last) / sec) * 8 bits/s
// (with counter-wrap handling), exactly like F's chart.
const RAISECOM_WAN_OCTETS_BASE = '1.3.6.1.4.1.8886.18.3.7.2.4.1'
const RC_WAN_VLAN = 40

async function getOnuBandwidth (serial, opts = {}) {
  let oltHost, community, oltName, slot, port, ontId, type

  if (serial) {
    const onu = getOnuBySerial(serial)
    if (onu) {
      const olt = olts.get(onu.oltHost)
      if (olt) {
        type = olt.type; oltHost = olt.host; community = olt.community; oltName = olt.name
        const parts = (onu.port || '').split('/') // Raisecom cache stores port as "0/slot/port"
        if (parts.length === 3) { slot = parseInt(parts[1]); port = parseInt(parts[2]) }
        ontId = onu.onuIdx
      }
    }
  }
  // Explicit params (from ERPNext Service Equipment when ONU not in cache)
  if (opts.oltIp) oltHost = opts.oltIp
  if (opts.community) community = opts.community
  if (opts.slot != null) slot = parseInt(opts.slot)
  if (opts.port != null) port = parseInt(opts.port)
  if (opts.ontId != null) ontId = parseInt(opts.ontId)
  if (opts.type) type = (opts.type + '').toLowerCase()
  if (!community) community = 'targosnmp'
  if (!oltName) oltName = oltHost

  // tech-3 (TP-Link) has no live WAN octet counter in F → report unavailable, don't poll
  if (type && type !== 'raisecom') return { available: false, reason: 'unsupported_olt_type', type }
  if (!oltHost || slot == null || port == null || ontId == null || isNaN(slot) || isNaN(port) || isNaN(ontId)) {
    return { available: false, reason: 'onu_not_resolved' }
  }

  const oltId = slot * 10000000 + port * 100000 + ontId
  const session = createSession(oltHost, community)
  let rxOctets = null, txOctets = null
  try {
    // Les 2 gets en parallèle (même session) → ~2× plus rapide à répondre
    const [rxVb, txVb] = await Promise.all([
      snmpGet(session, `${RAISECOM_WAN_OCTETS_BASE}.5.${oltId}.${RC_WAN_VLAN}`),
      snmpGet(session, `${RAISECOM_WAN_OCTETS_BASE}.3.${oltId}.${RC_WAN_VLAN}`),
    ])
    // These counters are SNMP Counter64 → net-snmp hands them back as an 8-byte
    // big-endian Buffer (NOT an Integer). Parse to a JS number (octet totals stay
    // well under 2^53 between resets, so precision is fine).
    const toNum = (vb) => {
      if (!vb) return null
      const v = vb.value
      if (typeof v === 'number') return v // Counter32/Gauge/Integer
      if (typeof v === 'bigint') return Number(v)
      if (Buffer.isBuffer(v)) {
        try { return Number(v.readBigUInt64BE(0)) }
        catch (e) { const n = Number('0x' + v.toString('hex')); return isNaN(n) ? null : n }
      }
      const n = parseInt(v, 10); return isNaN(n) ? null : n
    }
    rxOctets = toNum(rxVb); txOctets = toNum(txVb)
  } finally { try { session.close() } catch {} }

  if (rxOctets == null && txOctets == null) return { available: false, reason: 'no_counter', oltName, oltId }
  return { available: true, serial: serial || null, oltName, slot, port, ontId, oltId, rxOctets, txOctets, ts: Date.now() }
}

function extractVal (vb) {
  if (vb.type === snmp.ObjectType.OctetString) return vb.value.toString()
  if (vb.type === snmp.ObjectType.Integer) return vb.value
  return vb.value?.toString?.() || ''
}

function parseTplinkTable (varbinds) {
  const byKey = new Map()
  const baseLen = TPLINK.BASE.split('.').length

  for (const vb of varbinds) {
    const suffix = vb.oid.toString().split('.').slice(baseLen)
    if (suffix.length < 3) continue
    const col = parseInt(suffix[0])
    const key = suffix.slice(1).join('.')

    if (!byKey.has(key)) byKey.set(key, {})
    const entry = byKey.get(key)
    const val = extractVal(vb)

    if (col === TPLINK.COL_STATUS) {
      entry.status = (val === 1 || val === '1') ? 'online' : 'offline'
    } else if (TPLINK.COL_MAP[col]) {
      entry[TPLINK.COL_MAP[col]] = val
    }
  }

  const onus = []
  for (const [key, entry] of byKey) {
    if (!entry.serial) continue
    entry._key = key
    onus.push(entry)
  }
  return onus
}

function parseRaisecomTable (tableVarbinds, signalVarbinds) {
  const byId = new Map()
  const tableBaseLen = RAISECOM.BASE_TABLE.split('.').length

  for (const vb of tableVarbinds) {
    const suffix = vb.oid.toString().split('.').slice(tableBaseLen)
    if (suffix.length < 2) continue
    const col = parseInt(suffix[0])
    const onuId = parseInt(suffix[1])
    if (isNaN(onuId)) continue

    if (!byId.has(onuId)) byId.set(onuId, { _onuId: onuId })
    const entry = byId.get(onuId)
    const val = extractVal(vb)

    if (col === RAISECOM.COL_STATUS) {
      entry.status = (val === 1 || val === '1') ? 'online' : 'offline'
    } else if (RAISECOM.COL_MAP[col]) {
      entry[RAISECOM.COL_MAP[col]] = val
    }
  }

  // Parse signal table for OLT-side rx power
  const sigBaseLen = RAISECOM.BASE_SIGNAL.split('.').length
  for (const vb of signalVarbinds) {
    const suffix = vb.oid.toString().split('.').slice(sigBaseLen)
    if (suffix.length < 1) continue
    const onuId = parseInt(suffix[0])
    if (isNaN(onuId) || !byId.has(onuId)) continue
    const val = extractVal(vb)
    // Raisecom reports rx_power in dBm * 10
    byId.get(onuId).rxPowerOlt = typeof val === 'number' ? val / 10 : val
  }

  const onus = []
  for (const [onuId, entry] of byId) {
    if (!entry.serial) continue
    const { slot, port, ontid } = decodeRaisecomOnuId(onuId)
    entry.port = `0/${slot}/${port}`
    entry.onuIdx = ontid
    entry._key = `${slot}.${port}.${ontid}`
    onus.push(entry)
  }
  return onus
}

// backward compat alias
const parseOnuTable = parseTplinkTable

async function pollTplink (session) {
  const allVarbinds = []
  for (const col of TPLINK.FETCH_COLS) {
    const vbs = await snmpWalk(session, `${TPLINK.BASE}.${col}`)
    allVarbinds.push(...vbs)
  }
  return parseTplinkTable(allVarbinds)
}

async function pollRaisecom (olt) {
  // Raisecom needs a fresh session per walk (session gets closed after subtree timeout)
  const tableVarbinds = []
  for (const col of RAISECOM.FETCH_COLS) {
    const s = createSession(olt.host, olt.community)
    const vbs = await snmpWalk(s, `${RAISECOM.BASE_TABLE}.${col}`)
    try { s.close() } catch {}
    tableVarbinds.push(...vbs)
  }
  const sigSession = createSession(olt.host, olt.community)
  const signalVarbinds = await snmpWalk(sigSession, RAISECOM.BASE_SIGNAL)
  try { sigSession.close() } catch {}
  return parseRaisecomTable(tableVarbinds, signalVarbinds)
}

async function pollOlt (olt) {
  const startMs = Date.now()
  let session
  try {
    session = createSession(olt.host, olt.community)

    let onus
    if (olt.type === 'raisecom') {
      session.close(); session = null
      onus = await pollRaisecom(olt)
    } else {
      onus = await pollTplink(session)
      session.close(); session = null
    }
    const prevOnus = new Map(olt.onus)

    olt.onus.clear()
    let onlineCount = 0
    for (const onu of onus) {
      olt.onus.set(onu.serial, onu)
      if (onu.status === 'online') onlineCount++

      const prev = prevOnus.get(onu.serial)
      if (prev && prev.status !== onu.status) {
        const event = onu.status === 'offline' ? 'olt_offline' : 'olt_online'
        logOnuEvent(olt, onu, event, onu.status === 'offline' ? (onu.lastOfflineCause || null) : null)

        // Feed outage monitor for proactive detection
        try {
          const monitor = require('./outage-monitor')
          if (onu.status === 'offline') monitor.onOnuOffline(olt, onu)
          else monitor.onOnuOnline(olt, onu)
        } catch (_) {}
      }
    }

    const wasDown = !!olt.lastError // was previously in error state
    olt.onuCount = onus.length
    olt.onlineCount = onlineCount
    olt.lastPoll = new Date().toISOString()
    olt.lastError = null
    olt.lastDuration = Date.now() - startMs

    log(`OLT ${olt.name}: ${onus.length} ONUs (${onlineCount} online) in ${olt.lastDuration}ms`)

    // If OLT was down and is now back, resolve the OLT-level incident
    if (wasDown) {
      try {
        const monitor = require('./outage-monitor')
        monitor.onOltUp(olt.name, 'snmp')
      } catch (_) {}
    }
  } catch (e) {
    const wasOk = !olt.lastError // first failure after success
    olt.lastError = { message: e.message, ts: new Date().toISOString() }
    olt.lastDuration = Date.now() - startMs
    log(`OLT ${olt.name} SNMP FAILED (${olt.lastDuration}ms): ${e.message} — resuming next`)

    // Trigger OLT-level outage synthesis (only on first failure, not repeated)
    if (wasOk) {
      try {
        const monitor = require('./outage-monitor')
        monitor.onOltDown(olt.name, 'snmp', { customerCount: olt.onuCount, error: e.message })
      } catch (_) {}
    }
  } finally {
    if (session) try { session.close() } catch {}
  }
}

async function logOnuEvent (olt, onu, event, reason) {
  log(`ONU ${onu.serial} on ${olt.name}: ${event}${reason ? ' (' + reason + ')' : ''}`)
  try {
    const { logDeviceEvent } = require('./devices')
    if (logDeviceEvent) {
      await logDeviceEvent(onu.serial, event, reason, {
        olt: olt.name, port: onu.port, mac: onu.mac,
        rxPowerOlt: onu.rxPowerOlt, rxPowerOnu: onu.rxPowerOnu,
        distance: onu.distance, lastOfflineCause: onu.lastOfflineCause,
      })
    }
  } catch (e) {
    log(`Failed to log ONU event: ${e.message}`)
  }
}

let _oltSkipCount = 0
async function pollAllOlts () {
  if (!olts.size) return
  // Pause gate — admin toggle via /admin/pollers. Skip silently-ish when
  // paused (log every 12th tick = ~1h) so the log doesn't fill with "skipped".
  if (require('./poller-control').isPaused('olt')) {
    if ((_oltSkipCount++ % 12) === 0) log('OLT poll: skipped (paused via /admin/pollers)')
    return
  }
  const startMs = Date.now()
  let ok = 0, failed = 0
  for (const [, olt] of olts) {
    await pollOlt(olt)
    olt.lastError ? failed++ : ok++
  }
  if (failed > 0) log(`OLT poll complete: ${ok} OK, ${failed} FAILED in ${Date.now() - startMs}ms`)
}

function getOnuBySerial (serial) {
  if (!serial) return null
  const normalized = serial.replace(/^(TPLG)([A-F0-9])/i, '$1-$2').toUpperCase()
  for (const [, olt] of olts) {
    const enrich = (o) => ({ ...o, oltName: olt.name, oltHost: olt.host })
    let onu = olt.onus.get(serial) || olt.onus.get(normalized)
    if (onu) return enrich(onu)
    for (const [s, o] of olt.onus) {
      if (s.includes(serial) || serial.includes(s.replace(/-/g, ''))) return enrich(o)
    }
  }
  return null
}

// Get all ONUs on the same OLT port as a given serial
function getPortNeighbors (serial) {
  const target = getOnuBySerial(serial)
  if (!target || !target.port) return null

  const neighbors = []
  for (const [, olt] of olts) {
    if (olt.name !== target.oltName) continue
    for (const [, onu] of olt.onus) {
      if (onu.port === target.port && onu.serial !== target.serial) {
        neighbors.push({ ...onu, oltName: olt.name })
      }
    }
    break
  }
  return { target, port: target.port, oltName: target.oltName, neighbors }
}

// Get aggregated port health stats
function getPortHealth (oltName, port) {
  for (const [, olt] of olts) {
    if (olt.name !== oltName) continue
    const portOnus = []
    for (const [, onu] of olt.onus) {
      if (onu.port === port) portOnus.push({ ...onu, oltName: olt.name })
    }
    const total = portOnus.length
    const online = portOnus.filter(o => o.status === 'online').length
    const offline = portOnus.filter(o => o.status === 'offline')

    // Classify offline causes
    const causes = {}
    for (const o of offline) {
      const cause = classifyOfflineCause(o.lastOfflineCause)
      causes[cause] = (causes[cause] || 0) + 1
    }

    return { oltName, port, total, online, offline: total - online, causes, onus: portOnus }
  }
  return null
}

function classifyOfflineCause (raw) {
  if (!raw) return 'unknown'
  const lc = (raw + '').toLowerCase()
  if (lc.includes('dying') || lc.includes('gasp')) return 'dying_gasp'
  if (lc.includes('branch') || lc.includes('fiber cut') || lc.includes('fibre')) return 'branch_fiber_cut'
  if (lc.includes('losi') || lc.includes('los')) return 'loss_of_signal'
  if (lc.includes('lobi') || lc.includes('lob')) return 'loss_of_burst'
  return 'other'
}

const getOltStats = () => [...olts.entries()].map(([host, olt]) => ({
  host, name: olt.name, type: olt.type, onuCount: olt.onuCount, onlineCount: olt.onlineCount,
  lastPoll: olt.lastPoll, lastDuration: olt.lastDuration, lastError: olt.lastError,
}))

const getAllOnus = () => {
  const all = []
  for (const [, olt] of olts) {
    for (const [, onu] of olt.onus) all.push({ ...onu, oltName: olt.name })
  }
  return all
}

function startOltPoller () {
  for (const o of (cfg.OLT_LIST || [])) registerOlt(o)
  if (cfg.OLT_HOST && !olts.has(cfg.OLT_HOST)) {
    registerOlt({ host: cfg.OLT_HOST, community: cfg.OLT_COMMUNITY || 'public', name: cfg.OLT_NAME || cfg.OLT_HOST, type: cfg.OLT_TYPE })
  }

  if (!olts.size) return log('OLT SNMP poller: no OLTs configured')

  log(`OLT SNMP poller: ${olts.size} OLT(s), polling every ${POLL_INTERVAL / 1000}s`)
  setTimeout(() => {
    pollAllOlts().catch(e => log('OLT poll error:', e.message))
    pollTimer = setInterval(() => {
      pollAllOlts().catch(e => log('OLT poll error:', e.message))
    }, POLL_INTERVAL)
  }, 75000) // 1er poll OLT DIFFÉRÉ à 75s (était 10s) + DÉCALÉ après le poll appareils (45s) → pas de rafale coïncidente au boot
}

function stopOltPoller () {
  if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
}

module.exports = {
  registerOlt, startOltPoller, stopOltPoller,
  getOnuBySerial, getPortNeighbors, getPortHealth, classifyOfflineCause,
  getOltStats, getAllOnus, pollAllOlts, getManageIp, getOnuBandwidth,
}
