'use strict'
/**
 * Proactive Outage Monitor
 *
 * Detects mass outages by analyzing ONU status changes from the SNMP poller.
 * When multiple ONUs go offline on the same port within a short window:
 *   1. Runs AI outage analysis (or deterministic for simple cases)
 *   2. Creates a mass outage ticket in ERPNext
 *   3. Proactively notifies affected customers via SMS
 *   4. Broadcasts to ops dashboard via SSE
 *
 * Also provides serial → customer reverse lookup for linking SNMP data to CRM.
 *
 * Philosophy: Customers should hear from US before they need to call.
 */

const cfg = require('./config')
const { log, erpFetch } = require('./helpers')

// ── Outage tracking state ───────────────────────────────────────────────────

const portIncidents = new Map()    // "oltName:port" → { onus: [], ts, ticketId, notified }
const INCIDENT_WINDOW_MS = 600000  // 10 min — group offline events within this window
const MASS_OUTAGE_THRESHOLD = 3    // 3+ offline on same port = mass outage (SMS-worthy)
const INTERNAL_ALERT_THRESHOLD = 2 // 2+ = internal ops alert (SSE only, no SMS)
const ISOLATED_NOTIFY_DELAY = 1800000 // 30 min — notify single customer only after prolonged outage
const COOLDOWN_MS = 3600000        // 1 hour — don't re-notify for same port

// ── Telemetry logging (CrateDB when available, in-memory buffer for now) ───
const _eventLog = []               // ring buffer of recent events for /ai/outage-log
const EVENT_LOG_MAX = 5000

// ── Serial → Customer reverse lookup ────────────────────────────────────────

const _customerCache = new Map()   // serial → { customer, customer_name, phone, location }
const CACHE_TTL = 300000           // 5 min cache

async function getCustomerBySerial (serial) {
  const cached = _customerCache.get(serial)
  if (cached && (Date.now() - cached._ts) < CACHE_TTL) return cached

  try {
    // Query Service Equipment by serial_number → get customer
    const fields = JSON.stringify(['name', 'serial_number', 'customer', 'service_location'])
    const filters = JSON.stringify({ serial_number: serial })
    const res = await erpFetch(`/api/resource/Service Equipment?filters=${encodeURIComponent(filters)}&fields=${encodeURIComponent(fields)}&limit_page_length=1`)

    if (res.status !== 200 || !res.data?.data?.length) {
      // Try with normalized serial (TPLG-XXXX vs TPLGXXXX)
      const alt = serial.includes('-') ? serial.replace(/-/g, '') : serial.replace(/^(TPLG)([A-F0-9])/i, '$1-$2')
      const res2 = await erpFetch(`/api/resource/Service Equipment?filters=${encodeURIComponent(JSON.stringify({ serial_number: alt }))}&fields=${encodeURIComponent(fields)}&limit_page_length=1`)
      if (res2.status !== 200 || !res2.data?.data?.length) return null
      return _enrichAndCache(serial, res2.data.data[0])
    }
    return _enrichAndCache(serial, res.data.data[0])
  } catch (e) {
    log(`Customer lookup error for ${serial}: ${e.message}`)
    return null
  }
}

async function _enrichAndCache (serial, equip) {
  if (!equip.customer) return null

  // Fetch customer phone numbers
  const custFields = JSON.stringify(['name', 'customer_name', 'cell_phone', 'tel_home', 'tel_office', 'email_billing', 'language'])
  const custRes = await erpFetch(`/api/resource/Customer/${encodeURIComponent(equip.customer)}?fields=${encodeURIComponent(custFields)}`)
  const cust = custRes.status === 200 ? custRes.data?.data : null

  const result = {
    customer: equip.customer,
    customer_name: cust?.customer_name || equip.customer,
    phone: cust?.cell_phone || cust?.tel_home || cust?.tel_office || null,
    email: cust?.email_billing || null,
    language: cust?.language || 'fr',
    service_location: equip.service_location,
    _ts: Date.now(),
  }
  _customerCache.set(serial, result)
  return result
}

// Batch lookup: get customers for multiple serials efficiently
async function getCustomersForSerials (serials) {
  const results = new Map()
  const uncached = []

  for (const s of serials) {
    const cached = _customerCache.get(s)
    if (cached && (Date.now() - cached._ts) < CACHE_TTL) {
      results.set(s, cached)
    } else {
      uncached.push(s)
    }
  }

  // Fetch uncached in parallel (max 10 concurrent)
  const chunks = []
  for (let i = 0; i < uncached.length; i += 10) chunks.push(uncached.slice(i, i + 10))
  for (const chunk of chunks) {
    const lookups = await Promise.allSettled(chunk.map(s => getCustomerBySerial(s)))
    lookups.forEach((r, idx) => {
      if (r.status === 'fulfilled' && r.value) results.set(chunk[idx], r.value)
    })
  }
  return results
}

// ── Telemetry logging ──────────────────────────────────────────────────────

function logEvent (type, data) {
  const event = { type, ts: new Date().toISOString(), ...data }
  _eventLog.push(event)
  if (_eventLog.length > EVENT_LOG_MAX) _eventLog.splice(0, _eventLog.length - EVENT_LOG_MAX)
  // TODO: When CrateDB is available, INSERT INTO outage_events here
  return event
}

function getEventLog (since, limit = 200) {
  let events = _eventLog
  if (since) events = events.filter(e => e.ts > since)
  return events.slice(-limit)
}

// ── Outage event handler (called from olt-snmp.js on status change) ─────────

async function onOnuOffline (olt, onu) {
  const portKey = `${olt.name}:${onu.port}`
  const now = Date.now()

  // Always log to telemetry — every event, no matter how small
  logEvent('onu_offline', {
    olt: olt.name, port: onu.port, serial: onu.serial,
    cause: onu.lastOfflineCause || null,
  })

  // Get or create incident for this port
  let incident = portIncidents.get(portKey)
  if (!incident || (now - incident.ts) > INCIDENT_WINDOW_MS) {
    incident = { onus: [], ts: now, ticketId: null, notified: false, analyzed: false, internalAlerted: false }
    portIncidents.set(portKey, incident)
  }

  // Add this ONU to the incident (avoid duplicates)
  if (!incident.onus.find(o => o.serial === onu.serial)) {
    incident.onus.push({
      serial: onu.serial, port: onu.port, cause: onu.lastOfflineCause || null,
      ts: new Date().toISOString(),
    })
  }

  // Tier 1: Internal ops alert (2+ ONUs) — SSE only, no SMS, no ticket
  if (incident.onus.length >= INTERNAL_ALERT_THRESHOLD && !incident.internalAlerted) {
    incident.internalAlerted = true
    log(`⚡ Port alert: ${incident.onus.length} ONUs offline on ${portKey} — internal only`)
    logEvent('internal_alert', { olt: olt.name, port: onu.port, count: incident.onus.length })
    try {
      require('./sse').broadcastAll('port-alert', {
        oltName: olt.name, port: onu.port,
        affected_count: incident.onus.length,
        serials: incident.onus.map(o => o.serial),
        severity: 'warning',
        ts: new Date().toISOString(),
      })
    } catch (_) {}
  }

  // Tier 2: Mass outage (3+ ONUs) — AI analysis + ERPNext ticket + SMS customers
  if (incident.onus.length >= MASS_OUTAGE_THRESHOLD && !incident.analyzed) {
    incident.analyzed = true
    log(`⚠ MASS OUTAGE DETECTED: ${incident.onus.length} ONUs offline on ${portKey}`)
    logEvent('mass_outage', { olt: olt.name, port: onu.port, count: incident.onus.length })

    // Don't re-process if we recently handled this port
    if (incident.ticketId) return
    const lastNotify = portIncidents.get(portKey + ':lastNotify')
    if (lastNotify && (now - lastNotify) < COOLDOWN_MS) {
      log(`  Skipping notification — cooldown active for ${portKey}`)
      return
    }

    // Run analysis + notification in background
    handleMassOutage(olt.name, onu.port, incident).catch(e => {
      log(`Mass outage handler error: ${e.message}`)
    })
  }

  // Tier 3: Isolated prolonged outage (single ONU, 30 min+) — delayed check
  if (incident.onus.length === 1 && !incident._isolatedTimer) {
    incident._isolatedTimer = setTimeout(async () => {
      // Re-check if still offline and still alone
      const current = portIncidents.get(portKey)
      if (!current || current.onus.length !== 1) return
      const still = current.onus.find(o => o.serial === onu.serial)
      if (!still) return

      log(`⏰ Isolated outage 30min+: ${onu.serial} on ${portKey}`)
      logEvent('isolated_prolonged', { olt: olt.name, port: onu.port, serial: onu.serial })

      // Notify just this one customer
      try {
        const cust = await getCustomerBySerial(onu.serial)
        if (cust?.phone) {
          const { sendSmsInternal } = require('./twilio')
          const msg = cust.language === 'en'
            ? `Hi ${cust.customer_name}, we detected your internet service may be interrupted. Our team is aware. If the issue persists, call us. Ref: ${portKey}`
            : `Bonjour ${cust.customer_name}, nous avons détecté une possible interruption de votre service internet. Notre équipe est au courant. Si le problème persiste, appelez-nous. Réf: ${portKey}`
          await sendSmsInternal(cust.phone, msg, cust.customer)
          log(`Isolated outage SMS sent to ${cust.customer_name}`)
        }
      } catch (e) {
        log(`Isolated outage SMS failed: ${e.message}`)
      }
    }, ISOLATED_NOTIFY_DELAY)
  }
}

async function handleMassOutage (oltName, port, incident) {
  const { analyzeOutage } = require('./ai')
  const { sendSmsInternal } = require('./twilio')
  const { broadcastAll } = require('./sse')

  // Run AI outage analysis on first affected ONU
  const firstSerial = incident.onus[0]?.serial
  let analysis
  try {
    analysis = await analyzeOutage(firstSerial)
  } catch (e) {
    log(`Outage analysis failed: ${e.message}`)
    analysis = {
      severity: 'high', outage_type: 'unknown',
      diagnosis_fr: `${incident.onus.length} appareils hors ligne sur le port ${port} (${oltName})`,
      action_required: 'escalate_noc', auto_notify_customers: true,
      notification_msg_fr: `Nous avons détecté une interruption de service dans votre secteur. Nos équipes sont mobilisées pour rétablir le service le plus rapidement possible.`,
    }
  }

  log(`Outage analysis for ${oltName}:${port}: type=${analysis.outage_type}, severity=${analysis.severity}, action=${analysis.action_required}`)

  // Broadcast to ops dashboard
  try {
    broadcastAll('outage-alert', {
      oltName, port, analysis,
      affected_count: incident.onus.length,
      serials: incident.onus.map(o => o.serial),
      ts: new Date().toISOString(),
    })
  } catch (_) {}

  // Create ticket in ERPNext
  let ticketId = null
  try {
    const subject = `[Panne] ${incident.onus.length} clients - ${oltName} port ${port} - ${analysis.outage_type}`
    const description = [
      `## Panne détectée automatiquement`,
      `**OLT:** ${oltName}  **Port:** ${port}`,
      `**Clients affectés:** ${incident.onus.length}`,
      `**Type:** ${analysis.outage_type}`,
      `**Sévérité:** ${analysis.severity}`,
      `**Cause probable:** ${analysis.cause_fr || 'En analyse'}`,
      `**Action requise:** ${analysis.action_required}`,
      `**Technicien nécessaire:** ${analysis.tech_skill_needed || 'À déterminer'}`,
      '',
      `### Diagnostic`,
      analysis.diagnosis_fr || '',
      '',
      `### Preuves`,
      ...(analysis.evidence || []).map(e => `- ${e}`),
      '',
      `### ONUs affectés`,
      ...incident.onus.map(o => `- ${o.serial} (cause: ${o.cause || 'inconnue'})`),
    ].join('\n')

    const issueRes = await erpFetch('/api/resource/Issue', {
      method: 'POST',
      body: JSON.stringify({
        subject,
        description,
        issue_type: 'Outage',
        priority: analysis.severity === 'critical' ? 'Urgent' : analysis.severity === 'high' ? 'High' : 'Medium',
        status: 'Open',
      }),
    })
    if (issueRes.status >= 200 && issueRes.status < 300) {
      ticketId = issueRes.data?.data?.name
      incident.ticketId = ticketId
      log(`Outage ticket created: ${ticketId}`)
    }
  } catch (e) {
    log(`Failed to create outage ticket: ${e.message}`)
  }

  // Notify affected customers via SMS
  if (analysis.auto_notify_customers && analysis.notification_msg_fr) {
    const serials = incident.onus.map(o => o.serial)
    const customers = await getCustomersForSerials(serials)
    const notified = new Set()
    let smsCount = 0

    for (const [serial, cust] of customers) {
      if (!cust.phone || notified.has(cust.customer)) continue
      notified.add(cust.customer)

      try {
        const msg = analysis.notification_msg_fr +
          (ticketId ? `\nRéférence: ${ticketId}` : '') +
          `\nInfos: ${cfg.CLIENT_PUBLIC_URL || 'https://portal.gigafibre.ca'}`

        await sendSmsInternal(cust.phone, msg, cust.customer)
        smsCount++
        log(`Outage SMS sent to ${cust.customer_name} (${cust.phone})`)
      } catch (e) {
        log(`Outage SMS failed for ${cust.customer}: ${e.message}`)
      }
    }

    incident.notified = true
    portIncidents.set(`${oltName}:${port}:lastNotify`, Date.now())
    log(`Outage notification complete: ${smsCount} SMS sent for ${incident.onus.length} affected ONUs`)
  }

  // If dispatch needed, suggest it
  if (analysis.action_required === 'fiber_splicer' || analysis.action_required === 'simple_tech') {
    log(`⚡ Dispatch recommended: ${analysis.action_required} for ${oltName}:${port}`)
    // Broadcast dispatch suggestion to ops UI
    try {
      const { broadcastAll: bc } = require('./sse')
      bc('dispatch-suggestion', {
        type: 'outage',
        oltName, port, ticketId,
        tech_skill_needed: analysis.tech_skill_needed,
        priority: analysis.priority || 'high',
        subject: `Réparation fibre - ${oltName} port ${port}`,
        notes: analysis.diagnosis_fr,
      })
    } catch (_) {}
  }
}

// ── Called when ONU comes back online ───────────────────────────────────────

function onOnuOnline (olt, onu) {
  const portKey = `${olt.name}:${onu.port}`
  const incident = portIncidents.get(portKey)

  // Always log recovery
  logEvent('onu_online', { olt: olt.name, port: onu.port, serial: onu.serial })

  if (!incident) return

  // Cancel isolated outage timer if exists
  if (incident._isolatedTimer) {
    clearTimeout(incident._isolatedTimer)
    incident._isolatedTimer = null
  }

  // Remove this ONU from the incident
  incident.onus = incident.onus.filter(o => o.serial !== onu.serial)

  // If all ONUs back online, resolve the incident
  if (incident.onus.length === 0) {
    logEvent('incident_resolved', { olt: olt.name, port: onu.port, ticketId: incident.ticketId })

    if (incident.ticketId) {
      log(`✓ Outage resolved on ${portKey} — all ONUs back online`)

      // Update ticket status in ERPNext (sticky data — resolution matters)
      erpFetch(`/api/resource/Issue/${incident.ticketId}`, {
        method: 'PUT',
        body: JSON.stringify({ status: 'Closed', resolution_details: 'Service rétabli automatiquement.' }),
      }).catch(e => log(`Failed to close outage ticket: ${e.message}`))
    }

    portIncidents.delete(portKey)

    // Broadcast resolution to ops
    try {
      require('./sse').broadcastAll('outage-resolved', {
        oltName: olt.name, port: onu.port,
        ticketId: incident.ticketId,
        ts: new Date().toISOString(),
      })
    } catch (_) {}
  }
}

// ── OLT-level failure synthesis ────────────────────────────────────────────
//
// When an entire OLT goes down (SNMP unreachable or Kuma alert), we don't
// create one alert per customer. Instead: ONE consolidated alert with root
// cause and customer impact count.

const oltIncidents = new Map() // oltName → { ts, ticketId, notified, onuCount, resolved }
const OLT_INCIDENT_COOLDOWN = 3600000 // 1h

async function onOltDown (oltName, source, meta = {}) {
  const now = Date.now()
  const existing = oltIncidents.get(oltName)
  if (existing && !existing.resolved && (now - existing.ts) < OLT_INCIDENT_COOLDOWN) {
    log(`OLT ${oltName} already tracked as down (${source})`)
    return existing
  }

  const oltSnmp = require('./olt-snmp')
  const stats = oltSnmp.getOltStats()
  const oltStat = stats.find(s => s.name === oltName)
  const customerCount = oltStat?.onuCount || meta.customerCount || 0

  log(`🔴 OLT DOWN: ${oltName} (source: ${source}) — ${customerCount} customers potentially affected`)
  logEvent('olt_down', { olt: oltName, source, customerCount, meta })

  const incident = {
    ts: now, oltName, source, customerCount,
    ticketId: null, notified: false, resolved: false,
  }
  oltIncidents.set(oltName, incident)

  // Broadcast consolidated alert to ops
  try {
    require('./sse').broadcastAll('olt-down', {
      oltName, source, customerCount,
      severity: customerCount > 50 ? 'critical' : 'high',
      ts: new Date().toISOString(),
      ...meta,
    })
  } catch (_) {}

  // Create ONE consolidated ticket in ERPNext
  try {
    const subject = `[OLT] ${oltName} hors service — ~${customerCount} clients affectés (${source})`
    const description = [
      `## OLT hors service`,
      `**OLT:** ${oltName}`,
      `**Source détection:** ${source === 'kuma' ? 'Uptime-Kuma monitoring' : source === 'snmp' ? 'Échec SNMP poll' : source}`,
      `**Clients potentiellement affectés:** ~${customerCount}`,
      `**Heure détection:** ${new Date().toLocaleString('fr-CA', { timeZone: 'America/Toronto' })}`,
      '',
      `### Impact`,
      `Tous les clients sur cet OLT sont potentiellement sans service.`,
      `Ce ticket couvre l'ensemble des clients — pas besoin de créer des tickets individuels.`,
      '',
      meta.message ? `### Message Kuma\n${meta.message}` : '',
    ].filter(Boolean).join('\n')

    const issueRes = await erpFetch('/api/resource/Issue', {
      method: 'POST',
      body: JSON.stringify({
        subject, description,
        issue_type: 'Outage',
        priority: customerCount > 50 ? 'Urgent' : 'High',
        status: 'Open',
        outage_type: 'Panne OLT',
        olt_name: oltName,
        affected_count: customerCount,
      }),
    })
    if (issueRes.status >= 200 && issueRes.status < 300) {
      incident.ticketId = issueRes.data?.data?.name
      log(`OLT outage ticket created: ${incident.ticketId}`)
    }
  } catch (e) {
    log(`Failed to create OLT outage ticket: ${e.message}`)
  }

  // DO NOT SMS every customer — just top-level alert + ERPNext ticket
  // SMS should be sent ONLY after NOC confirms it's real and has an ETA
  // The ticket + dashboard alert is enough for the team to act

  return incident
}

async function onOltUp (oltName, source) {
  const incident = oltIncidents.get(oltName)
  if (!incident || incident.resolved) return

  incident.resolved = true
  log(`✅ OLT RESTORED: ${oltName} (source: ${source})`)
  logEvent('olt_up', { olt: oltName, source })

  // Close ticket
  if (incident.ticketId) {
    erpFetch(`/api/resource/Issue/${incident.ticketId}`, {
      method: 'PUT',
      body: JSON.stringify({ status: 'Closed', resolution_details: `OLT rétabli (détecté par ${source}).` }),
    }).catch(e => log(`Failed to close OLT ticket: ${e.message}`))
  }

  // Broadcast resolution
  try {
    require('./sse').broadcastAll('olt-up', {
      oltName, source, ticketId: incident.ticketId,
      ts: new Date().toISOString(),
      downtime_min: Math.round((Date.now() - incident.ts) / 60000),
    })
  } catch (_) {}
}

// ── Kuma webhook handler ──────────────────────────────────────────────────
//
// Uptime-Kuma can send webhook notifications when monitors go up/down.
// We accept these and trigger the OLT-level synthesis.
// Webhook URL to configure in Kuma: https://msg.gigafibre.ca/webhook/kuma
//
// Kuma webhook payload:
//   { heartbeat: { status: 0|1, msg: "...", time: "..." }, monitor: { name: "OLT-xxx", url: "..." }, msg: "..." }

async function handleKumaWebhook (body) {
  const monitorName = body.monitor?.name || 'unknown'
  const status = body.heartbeat?.status // 0 = down, 1 = up
  const message = body.msg || body.heartbeat?.msg || ''

  logEvent('kuma_webhook', { monitor: monitorName, status, message })

  // Try to match monitor name to an OLT
  const oltSnmp = require('./olt-snmp')
  const stats = oltSnmp.getOltStats()
  // Match by name or IP in the monitor name
  const oltMatch = stats.find(s =>
    monitorName.toLowerCase().includes(s.name.toLowerCase()) ||
    monitorName.includes(s.host)
  )

  if (!oltMatch) {
    // Not an OLT monitor — could be a generic service (ERPNext, GenieACS, etc.)
    log(`Kuma alert for "${monitorName}" (status: ${status}) — not an OLT, logging only`)
    // Broadcast generic Kuma alert to ops
    try {
      require('./sse').broadcastAll('kuma-alert', {
        monitor: monitorName, status, message,
        ts: new Date().toISOString(),
      })
    } catch (_) {}
    return { handled: true, type: 'generic', monitor: monitorName }
  }

  // It's an OLT monitor
  if (status === 0) {
    // DOWN
    const incident = await onOltDown(oltMatch.name, 'kuma', {
      customerCount: oltMatch.onuCount,
      kumaMonitor: monitorName,
      message,
    })
    return { handled: true, type: 'olt_down', oltName: oltMatch.name, customerCount: oltMatch.onuCount, ticketId: incident?.ticketId }
  } else if (status === 1) {
    // UP
    await onOltUp(oltMatch.name, 'kuma')
    return { handled: true, type: 'olt_up', oltName: oltMatch.name }
  }

  return { handled: true, type: 'unknown_status' }
}

// ── Get active incidents (for ops dashboard) ────────────────────────────────

function getActiveIncidents () {
  const active = []

  // Port-level incidents
  for (const [key, incident] of portIncidents) {
    if (key.includes(':lastNotify') || incident.onus.length === 0) continue
    const [oltName, ...portParts] = key.split(':')
    active.push({
      type: 'port',
      oltName, port: portParts.join(':'),
      affected_count: incident.onus.length,
      ticketId: incident.ticketId,
      notified: incident.notified,
      since: new Date(incident.ts).toISOString(),
    })
  }

  // OLT-level incidents
  for (const [oltName, incident] of oltIncidents) {
    if (incident.resolved) continue
    active.push({
      type: 'olt',
      oltName,
      source: incident.source,
      affected_count: incident.customerCount,
      ticketId: incident.ticketId,
      notified: incident.notified,
      since: new Date(incident.ts).toISOString(),
    })
  }

  return active
}

module.exports = {
  onOnuOffline,
  onOnuOnline,
  onOltDown,
  onOltUp,
  handleKumaWebhook,
  getActiveIncidents,
  getCustomerBySerial,
  getCustomersForSerials,
  getEventLog,
  logEvent,
}
