'use strict'
/**
 * Oktopus TR-369/USP Integration
 *
 * Handles device pre-authorization and service provisioning via the
 * Oktopus controller API at oss.gigafibre.ca.
 *
 * Device identity in Oktopus = MAC address (no colons, uppercase).
 * ERPNext Service Equipment stores mac_address (with colons) and serial_number.
 *
 * Flow:
 *  1. On equipment scan (field app) or bulk sync, create device auth in Oktopus
 *  2. Attach a "registration" service instance so the CPE auto-provisions on BOOTSTRAP
 *  3. Optionally push OLT/fibre config variables for ZTP
 */
const { log, httpRequest, erpFetch } = require('./helpers')
const cfg = require('./config')

// Internal Docker network URL (bypasses Authentik SSO proxy)
// Internal Docker network URL (bypasses Authentik SSO proxy)
const OKTOPUS_URL = cfg.OKTOPUS_URL || 'http://oktopus-controller-1:8000'
const OKTOPUS_USER = cfg.OKTOPUS_USER || ''
const OKTOPUS_PASS = cfg.OKTOPUS_PASS || ''
const OKTOPUS_MONGO_URL = cfg.OKTOPUS_MONGO_URL || 'mongodb://oktopus-mongo-1:27017'

let authToken = null
let tokenExpiry = 0
let mongoClient = null
let mongoDb = null

/* ── Auth ─────────────────────────────────────────────────────────── */

async function authenticate () {
  if (authToken && Date.now() < tokenExpiry) return authToken
  try {
    const res = await httpRequest(OKTOPUS_URL, '/api/auth/login', {
      method: 'PUT',
      body: { email: OKTOPUS_USER, password: OKTOPUS_PASS },
    })
    // Oktopus returns JWT as a plain JSON string (with quotes)
    const token = typeof res.data === 'string' ? res.data.replace(/^"|"$/g, '') : res.data?.token
    if (res.status === 200 && token) {
      authToken = token
      // Refresh 5 min before expiry (assume 1h token)
      tokenExpiry = Date.now() + 55 * 60 * 1000
      log('[oktopus] Authenticated')
      return authToken
    }
    log('[oktopus] Auth failed:', res.status, JSON.stringify(res.data))
    return null
  } catch (e) {
    log('[oktopus] Auth error:', e.message)
    return null
  }
}

async function oktopusRequest (method, path, body = null) {
  const token = await authenticate()
  if (!token) throw new Error('Oktopus authentication failed')
  return httpRequest(OKTOPUS_URL, path, {
    method,
    body,
    // Oktopus expects raw JWT token (no "Bearer " prefix)
    headers: { Authorization: token },
    timeout: 15000,
  })
}

/* ── MongoDB (Oktopus adapter DB — device records) ────────────────── */

async function getMongoDb () {
  if (mongoDb) return mongoDb
  try {
    const { MongoClient } = require('mongodb')
    mongoClient = new MongoClient(OKTOPUS_MONGO_URL)
    await mongoClient.connect()
    mongoDb = mongoClient.db('adapter')
    log('[oktopus] Connected to Oktopus MongoDB')
    return mongoDb
  } catch (e) {
    log('[oktopus] MongoDB connect error:', e.message)
    return null
  }
}

/**
 * Ensure a device record exists in Oktopus MongoDB (adapter.devices).
 * The adapter requires the device to exist before it can process status updates.
 */
async function ensureDeviceRecord (endpointId, opts = {}) {
  const db = await getMongoDb()
  if (!db) return { ok: false, error: 'MongoDB unavailable' }

  try {
    const col = db.collection('devices')
    const existing = await col.findOne({ sn: endpointId })
    if (existing) {
      // Update if we have new info
      if (opts.vendor || opts.model || opts.customer || opts.alias) {
        const update = {}
        if (opts.vendor) update.vendor = opts.vendor
        if (opts.model) update.model = opts.model
        if (opts.customer) update.customer = opts.customer
        if (opts.alias) update.alias = opts.alias
        await col.updateOne({ sn: endpointId }, { $set: update })
      }
      return { ok: true, existing: true }
    }

    await col.insertOne({
      sn: endpointId,
      model: opts.model || '',
      customer: opts.customer || '',
      vendor: opts.vendor || '',
      version: '',
      product_class: opts.product_class || 'ONT',
      alias: opts.alias || '',
      status: 0,
      mqtt: true,
      stomp: false,
      websockets: false,
      cwmp: false,
    })
    log(`[oktopus] Device record created: ${endpointId}`)
    return { ok: true, created: true }
  } catch (e) {
    log(`[oktopus] MongoDB error for ${endpointId}:`, e.message)
    return { ok: false, error: e.message }
  }
}

/* ── MAC helpers ──────────────────────────────────────────────────── */

/** Normalize MAC to uppercase hex without separators: E4FAC4160688 */
function normalizeMac (mac) {
  if (!mac) return null
  return mac.replace(/[:\-. ]/g, '').toUpperCase()
}

/** Format as USP Agent Endpoint ID: ops::OUI-ProductClass-SerialNumber (TR-369 ops authority) */
function generateEndpointId (mac, serial, productClass = 'Device2') {
  const cleanMac = normalizeMac(mac)
  if (!cleanMac) return null
  const oui = cleanMac.substring(0, 6)
  if (serial) {
    return `ops::${oui}-${productClass}-${serial}`
  }
  // Fallback if serial is missing
  return `USP::${cleanMac}`
}

/** Generate a random password (alphanumeric, 16 chars) */
function generatePassword (len = 16) {
  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'
  let result = ''
  for (let i = 0; i < len; i++) result += chars[Math.floor(Math.random() * chars.length)]
  return result
}

/* ── Device Auth (pre-authorization) ──────────────────────────────── */

/**
 * Create device credentials in Oktopus so the CPE can authenticate via MQTT.
 * @param {string} mac - MAC address (any format)
 * @returns {{ ok: boolean, deviceId: string, password?: string, error?: string }}
 */
async function preAuthorizeDevice (mac) {
  const deviceId = normalizeMac(mac)
  if (!deviceId) return { ok: false, error: 'Invalid MAC address' }

  const password = generatePassword()
  try {
    const res = await oktopusRequest('POST', '/api/device/auth', {
      id: deviceId,
      password,
    })
    if (res.status === 200 || res.status === 201) {
      log(`[oktopus] Device pre-authorized: ${deviceId}`)
      return { ok: true, deviceId, password }
    }
    if (res.status === 409) {
      // Already exists — that's fine
      log(`[oktopus] Device already registered: ${deviceId}`)
      return { ok: true, deviceId, password: null, existing: true }
    }
    log(`[oktopus] Pre-auth failed: ${res.status}`, JSON.stringify(res.data))
    return { ok: false, deviceId, error: `HTTP ${res.status}: ${JSON.stringify(res.data)}` }
  } catch (e) {
    log(`[oktopus] Pre-auth error for ${deviceId}:`, e.message)
    return { ok: false, deviceId, error: e.message }
  }
}

/* ── Full provisioning: pre-auth + fibre context ─────────────────── */

/**
 * Pre-authorize a device and enrich with fibre/OLT context from ERPNext.
 * Called during field scan (on-scan) or bulk sync.
 *
 * @param {object} opts
 * @param {string} opts.mac - MAC address
 * @param {string} opts.serial - ERPNext serial number
 * @param {string} [opts.service_location] - ERPNext Service Location name
 * @param {string} [opts.equipment_name] - ERPNext Service Equipment name
 * @param {string} [opts.customer] - ERPNext Customer name
 * @returns {object} Result with pre-auth status and fibre context
 */
async function provisionDevice (opts) {
  const { mac, serial, service_location, equipment_name, customer } = opts
  const result = {
    serial,
    mac,
    deviceId: normalizeMac(mac),
    endpointId: generateEndpointId(mac, serial),
    actions: [],
  }

  // Step 1: Pre-authorize in Oktopus
  const authResult = await preAuthorizeDevice(mac)
  result.oktopus_auth = authResult
  if (authResult.ok) {
    result.actions.push({
      action: authResult.existing ? 'oktopus_already_registered' : 'oktopus_pre_authorized',
      deviceId: authResult.deviceId,
    })
  } else {
    result.actions.push({ action: 'oktopus_auth_failed', error: authResult.error })
  }

  // Step 1b: Ensure device record exists in Oktopus MongoDB
  if (result.endpointId) {
    const mongoResult = await ensureDeviceRecord(result.endpointId, {
      vendor: opts.vendor || 'TP-Link',
      model: opts.model || '',
      customer: customer || '',
      alias: opts.alias || serial || '',
      product_class: opts.equipment_type || 'ONT',
    })
    if (mongoResult.ok) {
      result.actions.push({
        action: mongoResult.created ? 'device_record_created' : 'device_record_exists',
        endpointId: result.endpointId,
      })
    } else {
      result.actions.push({ action: 'device_record_failed', error: mongoResult.error })
    }
  }

  // Step 2: Store Oktopus credentials back in ERPNext (if new)
  if (authResult.ok && authResult.password && equipment_name) {
    try {
      await erpFetch(`/api/resource/Service Equipment/${encodeURIComponent(equipment_name)}`, {
        method: 'PUT',
        body: JSON.stringify({
          oktopus_device_id: authResult.deviceId,
          oktopus_password: authResult.password,
        }),
      })
      result.actions.push({ action: 'erp_credentials_stored', equipment: equipment_name })
    } catch (e) {
      result.actions.push({ action: 'erp_update_failed', error: e.message })
    }
  }

  // Step 3: Fetch fibre/OLT context from Service Location
  if (service_location) {
    try {
      const locRes = await erpFetch(
        `/api/resource/Service Location/${encodeURIComponent(service_location)}` +
        '?fields=["name","address_line1","city","olt_ip","olt_port","olt_name","ont_id","ont_serial",' +
        '"vlan_internet","vlan_manage","vlan_telephone","vlan_tv","connection_type","network_id"]'
      )
      const loc = locRes.data?.data || locRes.data || {}
      result.fibre_context = {
        location: loc.name,
        address: `${loc.address_line1 || ''}, ${loc.city || ''}`.trim().replace(/^,\s*/, ''),
        olt_ip: loc.olt_ip,
        olt_port: loc.olt_port,
        olt_name: loc.olt_name,
        ont_id: loc.ont_id,
        ont_serial: loc.ont_serial,
        vlans: {
          internet: loc.vlan_internet,
          manage: loc.vlan_manage,
          telephone: loc.vlan_telephone,
          tv: loc.vlan_tv,
        },
        connection_type: loc.connection_type,
        network_id: loc.network_id,
      }
      result.actions.push({ action: 'fibre_context_loaded', location: loc.name })
    } catch (e) {
      result.actions.push({ action: 'fibre_context_failed', error: e.message })
    }
  }

  // Step 4: Generate TR-369 MQTT config summary for the CPE
  if (result.deviceId) {
    result.tr369_config = {
      connection_type: 'MQTT',
      mqtt_server: 'oss.gigafibre.ca',
      mqtt_port: 1883,
      agent_endpoint_id: result.endpointId,
      agent_mqtt_topic: `oktopus/usp/v1/agent/${result.deviceId}`,
      controller_endpoint_id: 'oktopusController',
      controller_mqtt_topic: `oktopus/usp/v1/controller/${result.deviceId}`,
    }
  }

  log(`[oktopus] Provision: ${serial} (${result.deviceId}) → ${result.actions.length} actions`)
  return result
}

/* ── Bulk sync: ERPNext → Oktopus ─────────────────────────────────── */

/**
 * Sync all active ONT equipment from ERPNext into Oktopus.
 * Fetches Service Equipment where equipment_type=ONT and mac_address is set,
 * then pre-authorizes each in Oktopus.
 *
 * @param {object} [opts]
 * @param {string} [opts.status] - Filter by status (default: 'Actif')
 * @param {number} [opts.limit] - Max devices to sync (default: all)
 * @returns {{ total: number, authorized: number, existing: number, failed: number, errors: string[] }}
 */
async function bulkSync (opts = {}) {
  const status = opts.status || 'Actif'
  const limit = opts.limit || 0
  const PAGE = 500
  let offset = 0
  let allEquip = []

  log(`[oktopus] Bulk sync starting (status=${status}, limit=${limit || 'all'})`)

  // Fetch all ONT equipment with MAC addresses from ERPNext
  while (true) {
    const filters = JSON.stringify([
      ['equipment_type', '=', 'ONT'],
      ['mac_address', 'is', 'set'],
      ['status', '=', status],
    ])
    const fields = JSON.stringify(['name', 'serial_number', 'mac_address', 'service_location', 'customer'])
    const path = `/api/resource/Service Equipment?filters=${encodeURIComponent(filters)}&fields=${encodeURIComponent(fields)}&limit_page_length=${PAGE}&limit_start=${offset}&order_by=name asc`

    const res = await erpFetch(path)
    const rows = res.data?.data || []
    allEquip.push(...rows)

    if (rows.length < PAGE) break
    offset += PAGE
    if (limit && allEquip.length >= limit) break
  }

  if (limit) allEquip = allEquip.slice(0, limit)
  log(`[oktopus] Found ${allEquip.length} ONT devices to sync`)

  const result = { total: allEquip.length, authorized: 0, existing: 0, failed: 0, errors: [] }

  for (const equip of allEquip) {
    const authRes = await preAuthorizeDevice(equip.mac_address)
    if (authRes.ok) {
      // Ensure MongoDB record exists
      const endpointId = generateEndpointId(equip.mac_address, equip.serial_number)
      await ensureDeviceRecord(endpointId, {
        vendor: equip.brand || '',
        model: equip.model || '',
        customer: equip.customer || '',
        alias: equip.serial_number || '',
      })

      if (authRes.existing) {
        result.existing++
      } else {
        result.authorized++
        // Store credentials in ERPNext
        try {
          await erpFetch(`/api/resource/Service Equipment/${encodeURIComponent(equip.name)}`, {
            method: 'PUT',
            body: JSON.stringify({
              oktopus_device_id: authRes.deviceId,
              oktopus_password: authRes.password,
            }),
          })
        } catch (e) {
          log(`[oktopus] Failed to store creds for ${equip.name}:`, e.message)
        }
      }
    } else {
      result.failed++
      result.errors.push(`${equip.serial_number}: ${authRes.error}`)
    }

    // Small delay to avoid hammering Oktopus API
    if (result.total > 50) await new Promise(r => setTimeout(r, 50))
  }

  log(`[oktopus] Bulk sync done: ${result.authorized} new, ${result.existing} existing, ${result.failed} failed`)
  return result
}

/* ── HTTP handler for /oktopus/* routes ───────────────────────────── */

async function handle (req, res, method, path) {
  const { json: sendJson, parseBody: parse } = require('./helpers')

  try {
    const action = path.replace('/oktopus/', '').split('/').filter(Boolean)[0]

    // POST /oktopus/provision — provision single device
    if (action === 'provision' && method === 'POST') {
      const body = await parse(req)
      if (!body.mac) return sendJson(res, 400, { error: 'Missing mac address' })
      const result = await provisionDevice(body)
      return sendJson(res, 200, result)
    }

    // POST /oktopus/pre-authorize — just create device auth
    if (action === 'pre-authorize' && method === 'POST') {
      const body = await parse(req)
      if (!body.mac) return sendJson(res, 400, { error: 'Missing mac address' })
      const result = await preAuthorizeDevice(body.mac)
      return sendJson(res, 200, result)
    }

    // POST /oktopus/bulk-sync — sync all ONT devices from ERPNext
    if (action === 'bulk-sync' && method === 'POST') {
      const body = await parse(req)
      const result = await bulkSync(body)
      return sendJson(res, 200, result)
    }

    // GET /oktopus/status — check Oktopus connectivity
    if (action === 'status' && method === 'GET') {
      try {
        const token = await authenticate()
        return sendJson(res, 200, {
          ok: !!token,
          url: OKTOPUS_URL,
          authenticated: !!token,
        })
      } catch (e) {
        return sendJson(res, 200, { ok: false, url: OKTOPUS_URL, error: e.message })
      }
    }

    // PUT /oktopus/walk — USP GetSupportedParams (like snmpwalk)
    if (action === 'walk' && method === 'PUT') {
      const body = await parse(req)
      const sn = body.sn || body.serial
      const rootPath = body.path || 'Device.'
      if (!sn) return sendJson(res, 400, { error: 'Missing sn (e.g. USP::E4FAC4160688)' })
      try {
        const result = await oktopusRequest('PUT', `/api/device/${encodeURIComponent(sn)}/mqtt/parameters`, {
          paths: [rootPath],
        })
        return sendJson(res, result.status, result.data)
      } catch (e) {
        return sendJson(res, 504, { error: e.message })
      }
    }

    // PUT /oktopus/get — USP Get (read parameter values)
    if (action === 'get' && method === 'PUT') {
      const body = await parse(req)
      const sn = body.sn || body.serial
      const paths = body.paths || [body.path || 'Device.']
      if (!sn) return sendJson(res, 400, { error: 'Missing sn' })
      try {
        const result = await oktopusRequest('PUT', `/api/device/${encodeURIComponent(sn)}/mqtt/get`, { paths })
        return sendJson(res, result.status, result.data)
      } catch (e) {
        return sendJson(res, 504, { error: e.message })
      }
    }

    // PUT /oktopus/set — USP Set (write parameter values)
    if (action === 'set' && method === 'PUT') {
      const body = await parse(req)
      const sn = body.sn || body.serial
      if (!sn || !body.params) return sendJson(res, 400, { error: 'Missing sn and params' })
      try {
        const result = await oktopusRequest('PUT', `/api/device/${encodeURIComponent(sn)}/mqtt/set`, body.params)
        return sendJson(res, result.status, result.data)
      } catch (e) {
        return sendJson(res, 504, { error: e.message })
      }
    }

    // GET /oktopus/mqtt-status — MQTT monitor status
    if (action === 'mqtt-status' && method === 'GET') {
      try {
        const mqttMon = require('./oktopus-mqtt')
        return sendJson(res, 200, mqttMon.getStatus())
      } catch (e) {
        return sendJson(res, 200, { ok: false, error: 'MQTT monitor not loaded' })
      }
    }

    // POST /oktopus/mark-online — manually mark device online
    if (action === 'mark-online' && method === 'POST') {
      const body = await parse(req)
      const sn = body.sn || body.endpointId
      if (!sn) return sendJson(res, 400, { error: 'Missing sn (e.g. USP::E4FAC4160688)' })
      try {
        const mqttMon = require('./oktopus-mqtt')
        const result = await mqttMon.markOnline(sn)
        return sendJson(res, 200, result)
      } catch (e) {
        return sendJson(res, 500, { error: e.message })
      }
    }

    // POST /oktopus/probe — probe device to check if online
    if (action === 'probe' && method === 'POST') {
      const body = await parse(req)
      const sn = body.sn || body.endpointId
      if (!sn) return sendJson(res, 400, { error: 'Missing sn' })
      try {
        const mqttMon = require('./oktopus-mqtt')
        const result = await mqttMon.probeDevice(sn)
        return sendJson(res, 200, result)
      } catch (e) {
        return sendJson(res, 504, { error: e.message })
      }
    }

    // GET /oktopus/devices — list all devices from Oktopus
    if (action === 'devices' && method === 'GET') {
      try {
        const result = await oktopusRequest('GET', '/api/device')
        return sendJson(res, result.status, result.data)
      } catch (e) {
        return sendJson(res, 502, { error: e.message })
      }
    }

    return sendJson(res, 404, { error: 'Unknown oktopus endpoint: ' + action })
  } catch (e) {
    log('[oktopus] Handler error:', e.message)
    return require('./helpers').json(res, 500, { error: 'Oktopus error: ' + e.message })
  }
}

module.exports = {
  handle,
  preAuthorizeDevice,
  provisionDevice,
  bulkSync,
  normalizeMac,
  generateEndpointId,
}
