/**
 * modem-bridge.js — Client for the modem-bridge Playwright service
 *
 * DEPENDENCY: modem-bridge container (port 3301 on Docker proxy network)
 *
 * Provides targo-hub with access to TP-Link ONU web GUI data
 * by proxying requests to the modem-bridge headless Chromium service.
 *
 * ENDPOINTS EXPOSED (on targo-hub, port 3300):
 *   GET  /modem/status?ip=X&user=Y&pass=Z      — Full status (auto-login)
 *   GET  /modem/dm/:oid?ip=X&user=Y&pass=Z      — Raw DM read
 *   GET  /modem/diagnostic?ip=X&user=Y&pass=Z    — Full WiFi/mesh/WAN diagnostic
 *   GET  /modem/screenshot?ip=X&user=Y&pass=Z   — PNG screenshot
 *   GET  /modem/sessions                         — List active sessions
 *   POST /modem/close                            — Close a session
 */

const http = require('http');
const { log, json: jsonResp, parseBody, erpFetch } = require('./helpers');

const BRIDGE_HOST = process.env.BRIDGE_HOST || 'modem-bridge';
const BRIDGE_PORT = parseInt(process.env.BRIDGE_PORT || '3301');
const BRIDGE_TOKEN = process.env.BRIDGE_TOKEN || '';
const DEFAULT_MODEM_USER = 'superadmin';
const DEFAULT_MODEM_PASS = process.env.DEFAULT_MODEM_PASS || '';

// Concurrency limiter for Playwright sessions (512MB container limit)
const MAX_CONCURRENT = 3;
let activeCount = 0;
const waitQueue = [];

function withConcurrencyLimit(fn) {
  return new Promise((resolve, reject) => {
    function run() {
      activeCount++;
      fn().then(resolve).catch(reject).finally(() => {
        activeCount--;
        if (waitQueue.length > 0) waitQueue.shift()();
      });
    }
    if (activeCount >= MAX_CONCURRENT) {
      waitQueue.push(run);
    } else {
      run();
    }
  });
}

// Diagnostic result cache (in-memory, TTL-based)
const diagCache = new Map();
const DIAG_CACHE_TTL = 5 * 60 * 1000; // 5 minutes

/**
 * Lookup equipment credentials from ERPNext by serial number
 */
async function getEquipmentCredentials(serial) {
  // Find equipment by serial
  const filters = encodeURIComponent(JSON.stringify([['serial_number', '=', serial]]));
  const fields = encodeURIComponent(JSON.stringify(['name', 'ip_address', 'login_user', 'brand', 'model', 'serial_number']));
  const res = await erpFetch(`/api/resource/Service Equipment?filters=${filters}&fields=${fields}&limit_page_length=1`);

  if (res.status !== 200 || !res.data?.data?.length) return null;
  const eq = res.data.data[0];

  // Get password via Frappe's password API
  let pass = DEFAULT_MODEM_PASS;
  try {
    const passRes = await erpFetch(`/api/method/frappe.client.get_password?doctype=Service%20Equipment&name=${encodeURIComponent(eq.name)}&fieldname=login_password`);
    if (passRes.status === 200 && passRes.data?.message) {
      pass = passRes.data.message;
    }
  } catch (e) {
    log('getEquipmentCredentials: password fetch failed for', eq.name, e.message);
  }

  return {
    ip: (typeof eq.ip_address === 'string' && eq.ip_address.trim()) ? eq.ip_address.trim() : null,
    user: eq.login_user || DEFAULT_MODEM_USER,
    pass,
    brand: eq.brand,
    model: eq.model,
    serial: eq.serial_number,
    equipmentName: eq.name,
  };
}

/**
 * Make HTTP request to modem-bridge service
 */
function bridgeRequest(method, path, body = null) {
  return new Promise((resolve, reject) => {
    const opts = {
      hostname: BRIDGE_HOST,
      port: BRIDGE_PORT,
      path,
      method,
      headers: {
        'Content-Type': 'application/json',
      }
    };
    if (BRIDGE_TOKEN) {
      opts.headers['Authorization'] = `Bearer ${BRIDGE_TOKEN}`;
    }

    const bodyStr = body ? JSON.stringify(body) : '';
    if (body) opts.headers['Content-Length'] = Buffer.byteLength(bodyStr);

    const req = http.request(opts, res => {
      let data = '';
      res.on('data', c => data += c);
      res.on('end', () => {
        try {
          // Screenshots return binary
          if (res.headers['content-type']?.includes('image/png')) {
            resolve({ status: res.statusCode, body: Buffer.from(data, 'binary'), isPng: true });
          } else {
            resolve({ status: res.statusCode, body: JSON.parse(data) });
          }
        } catch(e) {
          resolve({ status: res.statusCode, body: data });
        }
      });
    });
    req.on('error', reject);
    if (body) req.write(bodyStr);
    req.end();
  });
}

/**
 * Ensure a session exists for the given modem, login if needed
 */
async function ensureSession(ip, user, pass, path) {
  // Check if session already exists
  const listRes = await bridgeRequest('GET', '/session/list');
  if (listRes.status === 200 && Array.isArray(listRes.body)) {
    const existing = listRes.body.find(s => s.ip === ip && s.loggedIn);
    if (existing) return existing;
  }

  // Login
  const loginRes = await bridgeRequest('POST', '/session/login', {
    ip, user, pass, path: path || '/superadmin'
  });

  if (loginRes.status !== 200) {
    throw new Error(`Modem login failed: ${loginRes.body?.error || 'Unknown error'}`);
  }

  return loginRes.body;
}

/**
 * Route handler for targo-hub
 */
async function handleModemRequest(req, res, path) {
  try {
    // GET /modem/sessions — list active sessions
    if (path === '/modem/sessions' && req.method === 'GET') {
      const result = await bridgeRequest('GET', '/session/list');
      return jsonResp(res, result.status, result.body);
    }

    // POST /modem/close — close a session
    if (path === '/modem/close' && req.method === 'POST') {
      const body = await parseBody(req);
      if (!body.ip) return jsonResp(res, 400, { error: 'Missing ip' });
      const result = await bridgeRequest('DELETE', `/session/${body.ip}`);
      return jsonResp(res, result.status, result.body);
    }

    // GET /modem/health — bridge health (no auth needed)
    if (path === '/modem/health' && req.method === 'GET') {
      const result = await bridgeRequest('GET', '/health');
      return jsonResp(res, result.status, result.body);
    }

    // GET /modem/identify?ip=X — auto-detect modem type (no login needed)
    if (path === '/modem/identify' && req.method === 'GET') {
      const url = new URL(req.url, `http://${req.headers.host}`);
      const ip = url.searchParams.get('ip');
      if (!ip) return jsonResp(res, 400, { error: 'Missing ip param' });
      const result = await bridgeRequest('GET', `/identify/${ip}`);
      return jsonResp(res, result.status, result.body);
    }

    // GET /modem/manage-ip?serial=X — get management IP from OLT via SNMP
    // Optionally pass olt_ip, slot, port, ontid if not in SNMP cache
    if (path === '/modem/manage-ip' && req.method === 'GET') {
      const url = new URL(req.url, `http://${req.headers.host}`);
      const serial = url.searchParams.get('serial');
      const opts = {};
      if (url.searchParams.get('olt_ip')) opts.oltIp = url.searchParams.get('olt_ip');
      if (url.searchParams.get('slot')) opts.slot = parseInt(url.searchParams.get('slot'));
      if (url.searchParams.get('port')) opts.port = parseInt(url.searchParams.get('port'));
      if (url.searchParams.get('ontid')) opts.ontId = parseInt(url.searchParams.get('ontid'));
      try {
        const { getManageIp } = require('./olt-snmp');
        const result = await getManageIp(serial, opts);
        if (!result) return jsonResp(res, 404, { error: 'No management IP found' });
        return jsonResp(res, 200, result);
      } catch(e) {
        return jsonResp(res, 502, { error: e.message });
      }
    }

    // GET /modem/diagnostic/auto?serial=X — auto-fetch credentials from ERPNext, run diagnostic
    if (path === '/modem/diagnostic/auto' && req.method === 'GET') {
      const url = new URL(req.url, `http://${req.headers.host}`);
      const serial = url.searchParams.get('serial');
      if (!serial) return jsonResp(res, 400, { error: 'Missing serial param' });

      // Check cache first
      const cached = diagCache.get(serial);
      if (cached && (Date.now() - cached.ts) < DIAG_CACHE_TTL) {
        return jsonResp(res, 200, { ...cached.data, cached: true });
      }

      // Lookup credentials from ERPNext
      const creds = await getEquipmentCredentials(serial);
      if (!creds) return jsonResp(res, 404, { error: 'Equipment not found in ERPNext', serial });

      // Resolve management IP from OLT SNMP if not stored on equipment
      let ip = creds.ip;
      if (!ip) {
        try {
          const { getManageIp } = require('./olt-snmp');
          const mgmt = await getManageIp(serial);
          if (mgmt) ip = mgmt.manageIp || (typeof mgmt === 'string' ? mgmt : null);
        } catch (e) {
          log('diagnostic/auto: OLT IP resolve failed for', serial, e.message);
        }
      }
      if (!ip || typeof ip !== 'string') return jsonResp(res, 404, { error: 'No management IP found', serial });

      if (!creds.pass) return jsonResp(res, 400, { error: 'No password configured for equipment', serial, equipment: creds.equipmentName });

      log('diagnostic/auto: running for', serial, 'ip:', ip, 'user:', creds.user);

      // Stateless oneshot: login → scrape → logout → close (no session reuse)
      const result = await withConcurrencyLimit(async () => {
        const diag = await bridgeRequest('POST', '/diagnostic/oneshot', {
          ip: String(ip), user: creds.user, pass: creds.pass,
        });
        if (diag.status !== 200) throw new Error(diag.body?.error || `Bridge returned ${diag.status}`);
        return diag.body;
      });

      // Cache result
      diagCache.set(serial, { data: result, ts: Date.now() });
      return jsonResp(res, 200, result);
    }

    // All other /modem/* routes need ip, user, pass
    const url = new URL(req.url, `http://${req.headers.host}`);
    const ip = url.searchParams.get('ip');
    const user = url.searchParams.get('user') || 'superadmin';
    const pass = url.searchParams.get('pass');
    const loginPath = url.searchParams.get('path') || '/superadmin';

    if (!ip || !pass) {
      return jsonResp(res, 400, { error: 'Missing required params: ip, pass' });
    }
    // Anti-SSRF : le bridge se connecte à cette IP → exiger une adresse de GESTION
    // PRIVÉE (RFC1918 / CGNAT 100.64/10), jamais un hôte public/arbitraire/metadata.
    if (!/^(?:10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.)\d{1,3}\.\d{1,3}$/.test(String(ip))) {
      return jsonResp(res, 400, { error: 'IP de gestion privée requise (RFC1918/CGNAT)' });
    }

    // Ensure session exists
    await ensureSession(ip, user, pass, loginPath);

    // GET /modem/status — full status
    if (path === '/modem/status' && req.method === 'GET') {
      const result = await bridgeRequest('GET', `/modem/${ip}/status`);
      return jsonResp(res, result.status, result.body);
    }

    // GET /modem/dm/:oid — raw data manager read
    const dmMatch = path.match(/^\/modem\/dm\/(.+)$/);
    if (dmMatch && req.method === 'GET') {
      const oid = dmMatch[1];
      let queryStr = `?`;
      if (url.searchParams.get('stack')) queryStr += `stack=${url.searchParams.get('stack')}&`;
      if (url.searchParams.get('attrs')) queryStr += `attrs=${url.searchParams.get('attrs')}&`;
      const result = await bridgeRequest('GET', `/modem/${ip}/dm/${oid}${queryStr}`);
      return jsonResp(res, result.status, result.body);
    }

    // GET /modem/diagnostic — unified diagnostic via stateless oneshot
    if (path === '/modem/diagnostic' && req.method === 'GET') {
      const result = await bridgeRequest('POST', '/diagnostic/oneshot', { ip, user, pass });
      return jsonResp(res, result.status, result.body);
    }

    // GET /modem/screenshot — PNG
    if (path === '/modem/screenshot' && req.method === 'GET') {
      const result = await bridgeRequest('GET', `/modem/${ip}/screenshot`);
      if (result.isPng) {
        res.writeHead(200, { 'Content-Type': 'image/png', 'Content-Length': result.body.length });
        return res.end(result.body);
      }
      return jsonResp(res, result.status, result.body);
    }

    return jsonResp(res, 404, { error: 'Unknown modem endpoint' });

  } catch(e) {
    log('modem-bridge error:', e.message);
    return jsonResp(res, 502, { error: e.message });
  }
}

module.exports = { handleModemRequest };
