// modem-session.js — Playwright-based multi-vendor modem session manager
// Supports: TP-Link XX230v (GDPR encrypt), Raisecom BOA, Raisecom PHP

const { chromium } = require('playwright');

const sessions = new Map();
const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
const LOGIN_TIMEOUT_MS = 15 * 1000;
const NAV_TIMEOUT_MS = 20 * 1000;

let browser = null;

async function getBrowser() {
  if (!browser || !browser.isConnected()) {
    browser = await chromium.launch({
      headless: true,
      args: [
        '--no-sandbox', '--disable-setuid-sandbox',
        '--disable-dev-shm-usage', '--disable-gpu',
        '--ignore-certificate-errors', '--disable-web-security',
      ]
    });
    console.log('[modem-bridge] Chromium launched');
  }
  return browser;
}

function resetIdleTimer(ip) {
  const s = sessions.get(ip); if (!s) return;
  clearTimeout(s.idleTimer);
  s.idleTimer = setTimeout(() => {
    console.log(`[modem-bridge] Session expired: ${ip}`);
    closeSession(ip);
  }, IDLE_TIMEOUT_MS);
}

function getSession(ip) {
  const s = sessions.get(ip);
  if (!s || !s.loggedIn) throw new Error(`No active session for ${ip}`);
  s.lastActivity = Date.now();
  resetIdleTimer(ip);
  return s;
}

// --- Modem identification ---
// Probes the device via HTTP/HTTPS to detect vendor and firmware type
const MODEM_TYPES = {
  TPLINK_XX230V: 'tplink_xx230v',
  RAISECOM_BOA: 'raisecom_boa',       // Type A: /admin/login.asp, BOA form, subcustom=raisecom
  RAISECOM_PHP: 'raisecom_php',       // Type B: /, PHP form, encrypt() JS
  UNKNOWN: 'unknown',
};

async function identify(ip) {
  const b = await getBrowser();
  const context = await b.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1280, height: 720 } });
  const page = await context.newPage();

  const result = { ip, type: MODEM_TYPES.UNKNOWN, title: '', protocol: 'https', loginPath: '/' };

  // Try HTTPS first (TP-Link uses HTTPS)
  for (const proto of ['https', 'http']) {
    for (const path of ['/', '/superadmin', '/admin/login.asp']) {
      try {
        const resp = await page.goto(`${proto}://${ip}${path}`, { waitUntil: 'domcontentloaded', timeout: 8000 });
        if (!resp || resp.status() >= 400) continue;

        const html = await page.content();
        const title = await page.title();
        result.title = title;
        result.protocol = proto;
        result.loginPath = path;

        // TP-Link XX230v: has $.tpLang or pc-login-password
        if (html.includes('pc-login-password') || html.includes('tpLang') || html.includes('INCLUDE_LOGIN')) {
          result.type = MODEM_TYPES.TPLINK_XX230V;
          result.loginPath = path;
          await context.close();
          return result;
        }

        // Raisecom BOA: title "Home Gateway", subcustom="raisecom", formLogin action
        if (html.includes('subcustom') && html.includes('raisecom') && html.includes('formLogin')) {
          result.type = MODEM_TYPES.RAISECOM_BOA;
          await context.close();
          return result;
        }

        // Raisecom PHP: title "Web user login", encrypt() function, user_name field
        if ((title.includes('Web user login') || html.includes('Web user login')) && html.includes('user_name') && html.includes('encrypt')) {
          result.type = MODEM_TYPES.RAISECOM_PHP;
          await context.close();
          return result;
        }
      } catch { continue; }
    }
  }

  await context.close();
  return result;
}

// --- Login handlers per modem type ---

async function loginTplink(ip, user, pass, path) {
  const b = await getBrowser();
  const context = await b.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1280, height: 720 }, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' });
  const page = await context.newPage();

  await page.goto(`https://${ip}${path}`, { waitUntil: 'networkidle', timeout: NAV_TIMEOUT_MS });
  await page.waitForSelector('#pc-login-password', { timeout: LOGIN_TIMEOUT_MS });

  const hasUsername = await page.evaluate(() => {
    const el = document.getElementById('pc-login-user-div');
    return el && !el.classList.contains('nd');
  });
  if (hasUsername) await page.fill('#pc-login-user', user);
  await page.fill('#pc-login-password', pass);
  await page.waitForTimeout(300);

  const navP = page.waitForNavigation({ waitUntil: 'networkidle', timeout: LOGIN_TIMEOUT_MS }).catch(() => null);
  await page.click('#pc-login-btn');
  await navP;
  await page.waitForTimeout(2000);

  // Force-logout dialog
  const forceBtn = await page.$('#confirm-yes');
  if (forceBtn && await forceBtn.isVisible()) {
    const navP2 = page.waitForNavigation({ waitUntil: 'networkidle', timeout: LOGIN_TIMEOUT_MS }).catch(() => null);
    await forceBtn.click();
    await navP2;
    await page.waitForTimeout(3000);
  }

  const loginResult = await page.evaluate(() => {
    const loginDiv = document.getElementById('pc-login');
    if (loginDiv && !loginDiv.classList.contains('nd')) {
      const errEl = document.querySelector('.errDiv .errText, .errDivP .errTextP');
      return { success: false, error: errEl ? errEl.textContent.trim() : 'Login failed' };
    }
    return { success: true, hasDm: typeof $ !== 'undefined' && $ && $.dm && typeof $.dm.get === 'function' };
  });

  if (!loginResult.success) { await context.close(); throw new Error(`Login failed for ${ip}: ${loginResult.error}`); }
  return { context, page, hasDm: loginResult.hasDm, modemType: MODEM_TYPES.TPLINK_XX230V };
}

async function loginRaisecomBoa(ip, user, pass, path) {
  const b = await getBrowser();
  const context = await b.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1280, height: 720 } });
  const page = await context.newPage();

  await page.goto(`http://${ip}${path}`, { waitUntil: 'domcontentloaded', timeout: NAV_TIMEOUT_MS });
  await page.waitForSelector('input[name="username"]', { timeout: LOGIN_TIMEOUT_MS });

  await page.fill('input[name="username"]', user);
  await page.fill('input[name="psd"]', pass);
  await page.waitForTimeout(200);

  // BOA form submits to /boaform/admin/formLogin — triggers page reload
  const navP = page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: LOGIN_TIMEOUT_MS }).catch(() => null);
  await page.evaluate(() => document.forms[0].submit());
  await navP;
  await page.waitForTimeout(2000);

  // Check if we're past login
  const url = page.url();
  const html = await page.content();
  const stillOnLogin = html.includes('formLogin') || html.includes('User Login');
  if (stillOnLogin) { await context.close(); throw new Error(`Raisecom BOA login failed for ${ip}`); }

  return { context, page, hasDm: false, modemType: MODEM_TYPES.RAISECOM_BOA };
}

async function loginRaisecomPhp(ip, user, pass, protocol = 'https') {
  const b = await getBrowser();
  const context = await b.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1280, height: 720 } });
  const page = await context.newPage();

  // WS2 models use HTTPS, WS1 PHP variants use HTTP — auto-detected by identify()
  await page.goto(`${protocol}://${ip}/`, { waitUntil: 'networkidle', timeout: NAV_TIMEOUT_MS });
  await page.waitForSelector('input[name="user_name"]', { timeout: LOGIN_TIMEOUT_MS });

  await page.fill('input[name="user_name"]', user);
  await page.fill('input[name="password"]', pass);
  await page.waitForTimeout(300);

  // Click the submit button to trigger JS encrypt() chain (mySubmit -> encrypt -> CryptoJS)
  const navP = page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: LOGIN_TIMEOUT_MS }).catch(() => null);
  await page.click('input[name="login"]').catch(() => {
    // Fallback: evaluate mySubmit or native submit
    return page.evaluate(() => {
      if (typeof mySubmit === 'function') mySubmit(document.forms[0]);
      else document.forms[0].submit();
    });
  });
  await navP;
  await page.waitForTimeout(3000);

  const html = await page.content();
  const stillOnLogin = html.includes('Web user login') && html.includes('user_name');
  if (stillOnLogin) { await context.close(); throw new Error(`Raisecom PHP login failed for ${ip}`); }

  return { context, page, hasDm: false, modemType: MODEM_TYPES.RAISECOM_PHP };
}

// --- Unified login ---

async function login(ip, user, pass, path = '/superadmin') {
  if (sessions.has(ip)) await closeSession(ip);

  let result;

  // Always identify modem type first — don't assume from path alone
  const id = await identify(ip);
  console.log(`[modem-bridge] Identified ${ip} as ${id.type} (title: "${id.title}", path: ${id.loginPath})`);

  if (id.type === MODEM_TYPES.TPLINK_XX230V) {
    result = await loginTplink(ip, user, pass, id.loginPath || path);
  } else if (id.type === MODEM_TYPES.RAISECOM_BOA) {
    result = await loginRaisecomBoa(ip, user, pass, id.loginPath || '/');
  } else if (id.type === MODEM_TYPES.RAISECOM_PHP) {
    result = await loginRaisecomPhp(ip, user, pass, id.protocol || 'https');
  } else {
    throw new Error(`Unknown modem type at ${ip} (title: "${id.title}")`);
  }

  const session = {
    ip, context: result.context, page: result.page,
    loggedIn: true, loginTime: Date.now(), lastActivity: Date.now(),
    hasDm: result.hasDm, modemType: result.modemType,
  };
  sessions.set(ip, session);
  resetIdleTimer(ip);

  console.log(`[modem-bridge] Logged in to ${ip} (type: ${result.modemType}, hasDm: ${result.hasDm})`);
  return { ip, loggedIn: true, loginTime: session.loginTime, hasDm: result.hasDm, modemType: result.modemType };
}

// --- Data access (TP-Link specific: $.dm) ---

async function dmGet(ip, oid, opts = {}) {
  const s = getSession(ip);
  if (!s.hasDm) throw new Error(`Data manager not available on ${ip} (type: ${s.modemType})`);
  const stack = opts.stack || '0,0,0,0,0,0';
  const attrs = opts.attrs || null;

  return await s.page.evaluate(({ oid, stack, attrs }) => {
    return new Promise((resolve, reject) => {
      if (!$ || !$.dm || !$.dm.get) { reject(new Error('Data manager not available')); return; }
      const req = { oid, data: { stack }, callback: {
        success: (data, others) => resolve({ success: true, data, others }),
        fail: (errorcode, others, data) => resolve({ success: false, errorcode, data }),
        error: (status) => reject(new Error('Request error: ' + status))
      }};
      if (attrs) req.data.attrs = attrs;
      try { $.dm.get(req); } catch(e) { reject(e); }
      setTimeout(() => reject(new Error('DM request timeout')), 8000);
    });
  }, { oid, stack, attrs });
}

async function cgiRequest(ip, oidPath) {
  const s = getSession(ip);
  if (!s.hasDm) throw new Error(`CGI not available on ${ip} (type: ${s.modemType})`);
  return await s.page.evaluate((oidPath) => {
    return new Promise((resolve, reject) => {
      if (!$ || !$.dm || !$.dm.cgi) { reject(new Error('CGI not available')); return; }
      $.dm.cgi({ oid: oidPath, callback: {
        success: (data) => resolve({ success: true, data }),
        fail: (errorcode) => resolve({ success: false, errorcode })
      }});
      setTimeout(() => reject(new Error('CGI request timeout')), 8000);
    });
  }, oidPath);
}

// --- TP-Link WiFi diagnostic ($.dm based) ---

async function wifiDiagnostic(ip) {
  const s = getSession(ip);
  if (s.modemType !== MODEM_TYPES.TPLINK_XX230V) throw new Error(`wifiDiagnostic not supported on ${s.modemType}`);

  return await s.page.evaluate(() => {
    const start = Date.now();
    function dmQuery(method, oid, extraData) {
      return new Promise((resolve) => {
        if (!$ || !$.dm || !$.dm[method]) { resolve({ oid, error: `${method} not available` }); return; }
        const req = { oid, callback: { success: (data) => resolve({ oid, data }), fail: (code) => resolve({ oid, error: code }) }};
        if (extraData) req.data = extraData;
        $.dm[method](req);
        setTimeout(() => resolve({ oid, error: 'timeout' }), 8000);
      });
    }

    return Promise.allSettled([
      dmQuery('getSubList', 'DEV2_WIFI_RADIO'),
      dmQuery('getSubList', 'DEV2_WIFI_APDEV'),
      dmQuery('getSubList', 'DEV2_WIFI_APDEV_RADIO'),
      dmQuery('getSubList', 'DEV2_WIFI_APDEV_ASSOCDEV'),
      dmQuery('getSubList', 'DEV2_WIFI_DE_STA'),
      dmQuery('getSubList', 'DEV2_WIFI_APDEV_STATS'),
      dmQuery('getSubList', 'DEV2_IP_INTF_V4ADDR'),
      dmQuery('get', 'DEV2_ONLINESTATUS', { stack: '0,0,0,0,0,0' }),
    ]).then(results => {
      const keys = ['radios', 'meshNodes', 'nodeRadios', 'clients', 'clientStats', 'packetStats', 'wanIPs', 'onlineStatus'];
      const out = { fetchedAt: new Date().toISOString(), durationMs: Date.now() - start, modemType: 'tplink_xx230v' };
      results.forEach((r, i) => {
        const val = r.status === 'fulfilled' ? r.value : { error: r.reason };
        out[keys[i]] = val.error ? { error: val.error } : val.data;
      });
      return out;
    });
  });
}

// --- Raisecom diagnostic (targeted JS extraction) ---

// Raisecom BOA pages embed data as JS arrays: var arr=[]; arr.push(new it_nr("idx", new it("key", val), ...))
// We fetch each page's raw HTML and return it for server-side parsing by diagnostic-normalizer.js

const RAISECOM_BOA_PAGES = [
  { path: '/status_ethernet_info.asp', key: 'ethernet' },   // Ethernet ports + DHCP clients
  { path: '/status_device_basic_info.asp', key: 'deviceInfo' }, // Model, serial, firmware, CPU, uptime
  { path: '/status_wlan_info_11n.asp', key: 'wlan' },       // WiFi clients with RSSI, rates
  { path: '/status_net_connet_info.asp', key: 'wan' },       // WAN interfaces, IPs, gateways
  { path: '/status_gpon.asp', key: 'gpon' },                 // GPON optical power, link state
];

// WS2 (PHP) uses /sys_manager/list_status.php?parts=X and /interface/ pages
const RAISECOM_PHP_PAGES = [
  { path: '/sys_manager/list_status.php?parts=hostinfo', key: 'deviceInfo' },
  { path: '/sys_manager/list_status.php?parts=waninfo', key: 'wan' },
  { path: '/sys_manager/list_status.php?parts=laninfo', key: 'ethernet' },
  { path: '/sys_manager/list_status.php?parts=wlaninfo', key: 'wlan' },
  { path: '/sys_manager/list_status.php?parts=poninfo', key: 'gpon' },
  { path: '/sys_manager/list_session.php', key: 'sessions' },
];

async function raisecomDiagnostic(ip) {
  const s = getSession(ip);
  if (!s.modemType.startsWith('raisecom')) throw new Error(`raisecomDiagnostic not supported on ${s.modemType}`);

  const start = Date.now();
  const pages = s.modemType === MODEM_TYPES.RAISECOM_BOA ? RAISECOM_BOA_PAGES : RAISECOM_PHP_PAGES;
  const proto = s.modemType === MODEM_TYPES.RAISECOM_PHP ? 'https' : 'http';
  const baseUrl = `${proto}://${ip}`;
  const rawPages = {};

  for (const pg of pages) {
    try {
      // Fetch page HTML directly (faster than navigating + waiting for render)
      const html = await s.page.evaluate((url) => fetch(url).then(r => r.text()), `${baseUrl}${pg.path}`);
      rawPages[pg.key] = html;
    } catch (e) {
      rawPages[pg.key] = null;
    }
  }

  return {
    fetchedAt: new Date().toISOString(),
    durationMs: Date.now() - start,
    modemType: s.modemType,
    rawPages,
  };
}

// --- Unified diagnostic (routes to correct handler, returns normalized data) ---

async function unifiedDiagnostic(ip) {
  const s = getSession(ip);
  const { normalizeRaisecomBoa, normalizeRaisecomPhp, normalizeTplink } = require('./diagnostic-normalizer');

  if (s.modemType === MODEM_TYPES.TPLINK_XX230V) {
    const raw = await wifiDiagnostic(ip);
    const result = normalizeTplink(raw);
    result.fetchedAt = raw.fetchedAt;
    result.durationMs = raw.durationMs;
    return result;
  }

  if (s.modemType.startsWith('raisecom')) {
    const raw = await raisecomDiagnostic(ip);
    const normalize = s.modemType === MODEM_TYPES.RAISECOM_PHP ? normalizeRaisecomPhp : normalizeRaisecomBoa;
    const result = normalize(raw.rawPages);
    result.fetchedAt = raw.fetchedAt;
    result.durationMs = raw.durationMs;

    // WS2 (PHP) only allows 1 session — auto-logout to free it for the user
    if (s.modemType === MODEM_TYPES.RAISECOM_PHP) {
      const proto = 'https';
      s.page.evaluate((url) => fetch(url).catch(() => {}), `${proto}://${ip}/top_operation.php?top_parts=login_out_time`).catch(() => {});
      closeSession(ip).catch(() => {});
      console.log(`[modem-bridge] Auto-logout WS2 ${ip} after diagnostic`);
    }

    return result;
  }

  throw new Error(`Unsupported modem type: ${s.modemType}`);
}

// --- Stateless one-shot diagnostic (no session reuse) ---
// Single browser context: open page → detect type → login → scrape → logout → close
// No separate identify() call — avoids WS2 session-slot consumption

async function oneshotDiagnostic(ip, user, pass) {
  const start = Date.now();
  const { normalizeRaisecomBoa, normalizeRaisecomPhp, normalizeTplink } = require('./diagnostic-normalizer');

  // Kill any existing session for this IP first
  if (sessions.has(ip)) {
    await closeSession(ip).catch(() => {});
  }

  const b = await getBrowser();
  const context = await b.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1280, height: 720 }, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' });
  const page = await context.newPage();

  try {
    // --- Phase 1: detect modem type from the same page/context we'll login with ---
    let modemType = MODEM_TYPES.UNKNOWN;
    let proto = 'https';

    for (const p of ['https', 'http']) {
      try {
        await page.goto(`${p}://${ip}/`, { waitUntil: 'domcontentloaded', timeout: 10000 });
        const html = await page.content();
        const title = await page.title();

        if (html.includes('pc-login-password') || html.includes('tpLang') || html.includes('INCLUDE_LOGIN')) {
          modemType = MODEM_TYPES.TPLINK_XX230V; proto = p; break;
        }
        if (html.includes('subcustom') && html.includes('raisecom') && html.includes('formLogin')) {
          modemType = MODEM_TYPES.RAISECOM_BOA; proto = p; break;
        }
        if ((title.includes('Web user login') || html.includes('Web user login')) && html.includes('user_name') && html.includes('encrypt')) {
          modemType = MODEM_TYPES.RAISECOM_PHP; proto = p; break;
        }
      } catch { continue; }
    }

    console.log(`[modem-bridge] oneshot ${ip}: type=${modemType} proto=${proto}`);
    if (modemType === MODEM_TYPES.UNKNOWN) throw new Error(`Unknown modem type at ${ip}`);

    // --- Phase 2: login on the SAME page (no new context) ---
    let hasDm = false;

    if (modemType === MODEM_TYPES.TPLINK_XX230V) {
      // TP-Link: navigate to superadmin, fill password
      await page.goto(`${proto}://${ip}/superadmin`, { waitUntil: 'networkidle', timeout: NAV_TIMEOUT_MS });
      await page.waitForSelector('#pc-login-password', { timeout: LOGIN_TIMEOUT_MS });
      const hasUser = await page.evaluate(() => { const el = document.getElementById('pc-login-user-div'); return el && !el.classList.contains('nd'); });
      if (hasUser) await page.fill('#pc-login-user', user);
      await page.fill('#pc-login-password', pass);
      await page.waitForTimeout(300);
      const navP = page.waitForNavigation({ waitUntil: 'networkidle', timeout: LOGIN_TIMEOUT_MS }).catch(() => null);
      await page.click('#pc-login-btn');
      await navP;
      await page.waitForTimeout(2000);
      const forceBtn = await page.$('#confirm-yes');
      if (forceBtn && await forceBtn.isVisible()) {
        const navP2 = page.waitForNavigation({ waitUntil: 'networkidle', timeout: LOGIN_TIMEOUT_MS }).catch(() => null);
        await forceBtn.click();
        await navP2;
        await page.waitForTimeout(3000);
      }
      hasDm = await page.evaluate(() => typeof $ !== 'undefined' && $ && $.dm && typeof $.dm.get === 'function');

    } else if (modemType === MODEM_TYPES.RAISECOM_BOA) {
      // BOA: already on login page from detection, fill form
      await page.waitForSelector('input[name="username"]', { timeout: LOGIN_TIMEOUT_MS });
      await page.fill('input[name="username"]', user);
      await page.fill('input[name="psd"]', pass);
      await page.waitForTimeout(200);
      const navP = page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: LOGIN_TIMEOUT_MS }).catch(() => null);
      await page.evaluate(() => document.forms[0].submit());
      await navP;
      await page.waitForTimeout(2000);
      const html = await page.content();
      if (html.includes('formLogin') || html.includes('User Login')) throw new Error(`Raisecom BOA login failed for ${ip}`);

    } else if (modemType === MODEM_TYPES.RAISECOM_PHP) {
      // WS2: already on login page from detection, fill and submit
      await page.waitForSelector('input[name="user_name"]', { timeout: LOGIN_TIMEOUT_MS });
      await page.fill('input[name="user_name"]', user);
      await page.fill('input[name="password"]', pass);
      await page.waitForTimeout(300);
      const navP = page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: LOGIN_TIMEOUT_MS }).catch(() => null);
      await page.click('input[name="login"]').catch(() => {
        return page.evaluate(() => { if (typeof mySubmit === 'function') mySubmit(document.forms[0]); else document.forms[0].submit(); });
      });
      await navP;
      await page.waitForTimeout(3000);
      const html = await page.content();
      if (html.includes('Web user login') && html.includes('user_name')) throw new Error(`Raisecom PHP login failed for ${ip}`);
    }

    console.log(`[modem-bridge] oneshot ${ip}: logged in (${modemType})`);

    // --- Phase 3: register temp session, run diagnostic ---
    const session = {
      ip, context, page, loggedIn: true, loginTime: Date.now(),
      lastActivity: Date.now(), hasDm, modemType,
    };
    sessions.set(ip, session);

    let result;
    if (modemType === MODEM_TYPES.TPLINK_XX230V) {
      const raw = await wifiDiagnostic(ip);
      result = normalizeTplink(raw);
      result.fetchedAt = raw.fetchedAt;
    } else {
      const raw = await raisecomDiagnostic(ip);
      const normalize = modemType === MODEM_TYPES.RAISECOM_PHP ? normalizeRaisecomPhp : normalizeRaisecomBoa;
      result = normalize(raw.rawPages);
      result.fetchedAt = new Date().toISOString();
    }
    result.durationMs = Date.now() - start;

    // --- Phase 4: logout + close ---
    if (modemType === MODEM_TYPES.RAISECOM_PHP) {
      try {
        // Navigate to logout page (not fetch — ensures modem processes it fully)
        await page.goto(`${proto}://${ip}/top_operation.php?top_parts=login_out_time`, { waitUntil: 'domcontentloaded', timeout: 8000 }).catch(() => {});
        // Give the modem time to release the session slot
        await page.waitForTimeout(2000);
      } catch {}
      console.log(`[modem-bridge] oneshot ${ip}: WS2 logged out`);
    }

    return result;

  } finally {
    sessions.delete(ip);
    await context.close().catch(() => {});
    console.log(`[modem-bridge] oneshot ${ip}: done (${Date.now() - start}ms)`);
  }
}

// --- Generic status (works for all types) ---

async function getStatus(ip) {
  const s = getSession(ip);
  if (s.modemType === MODEM_TYPES.TPLINK_XX230V) {
    return await s.page.evaluate(() => {
      const status = {};
      try { if (typeof deviceInfo !== 'undefined') status.deviceInfo = deviceInfo; } catch(e) {}
      try { if ($ && $.status) status.cached = $.status; } catch(e) {}
      try {
        status.dom = {};
        document.querySelectorAll('[id*="status"], [id*="info"], [class*="status"]').forEach(el => {
          if (el.textContent.trim().length < 200) status.dom[el.id || el.className] = el.textContent.trim();
        });
      } catch(e) {}
      return status;
    });
  }
  // Raisecom: return page scrape
  return raisecomDiagnostic(ip);
}

async function screenshot(ip) {
  const s = getSession(ip);
  return await s.page.screenshot({ type: 'png', fullPage: false });
}

async function evaluate(ip, code) {
  const s = getSession(ip);
  return await s.page.evaluate(new Function('return (' + code + ')'));
}

async function getPageContent(ip) {
  const s = getSession(ip);
  return await s.page.content();
}

async function navigate(ip, path) {
  const s = getSession(ip);
  const proto = s.modemType.startsWith('raisecom') ? 'http' : 'https';
  await s.page.goto(`${proto}://${ip}${path}`, { waitUntil: 'domcontentloaded', timeout: NAV_TIMEOUT_MS });
}

async function closeSession(ip) {
  const s = sessions.get(ip);
  if (!s) return;
  clearTimeout(s.idleTimer);
  try { await s.context.close(); }
  catch(e) { console.warn(`[modem-bridge] Error closing ${ip}:`, e.message); }
  sessions.delete(ip);
}

async function shutdown() {
  for (const [ip] of sessions) await closeSession(ip);
  if (browser) { await browser.close(); browser = null; }
}

function listSessions() {
  return [...sessions.entries()].map(([ip, s]) => ({
    ip, loggedIn: s.loggedIn, loginTime: s.loginTime,
    lastActivity: s.lastActivity, idleMs: Date.now() - s.lastActivity,
    hasDm: s.hasDm, modemType: s.modemType,
  }));
}

module.exports = {
  identify, login, dmGet, cgiRequest, getStatus,
  wifiDiagnostic, raisecomDiagnostic, unifiedDiagnostic, oneshotDiagnostic,
  evaluate, screenshot, getPageContent, navigate,
  closeSession, shutdown, listSessions,
  MODEM_TYPES,
};
