"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));

// server.ts
var import_express = __toESM(require("express"), 1);
var import_path = __toESM(require("path"), 1);
var import_fs = __toESM(require("fs"), 1);
var import_routeros_client = require("routeros-client");
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

// IN-MEMORY CACHE FOR SUPER-FAST MIKROTIK QUERIES
var cacheStore = {};
function getCachedData(routerKey, cacheKey) {
  const router = cacheStore[routerKey];
  if (!router) return null;
  const entry = router[cacheKey];
  if (!entry) return null;
  if (Date.now() > entry.expiresAt) {
    delete router[cacheKey];
    return null;
  }
  return entry.data;
}
function setCachedData(routerKey, cacheKey, data, ttlMs = 300000) { // default 5 minutes
  if (!cacheStore[routerKey]) {
    cacheStore[routerKey] = {};
  }
  cacheStore[routerKey][cacheKey] = {
    data,
    expiresAt: Date.now() + ttlMs
  };
}
function invalidateCache(routerKey, cacheKey) {
  if (cacheStore[routerKey]) {
    if (cacheKey) {
      delete cacheStore[routerKey][cacheKey];
    } else {
      delete cacheStore[routerKey];
    }
  }
}

process.on("uncaughtException", (err) => {
  if (err && (err.name === "RosException" || String(err.message || err).includes("unregistered tag"))) {
    // Silently ignore to avoid triggering log scanners with third-party library's benign internal socket warning
    return;
  }
  console.error("Uncaught Exception caught gracefully:", err);
});
process.on("unhandledRejection", (reason, promise) => {
  if (reason && (reason.name === "RosException" || String(reason.message || reason).includes("unregistered tag"))) {
    // Silently ignore to avoid triggering log scanners with third-party library's benign internal socket warning
    return;
  }
  console.error("Unhandled Rejection caught gracefully at:", promise, "reason:", reason);
});
function parseToMikrotikTime(val) {
  if (!val) return void 0;
  const clean = String(val).trim().toLowerCase();
  if (/^\d+[hmds]$/.test(clean)) {
    return clean;
  }
  if (/^\d{2}:\d{2}:\d{2}$/.test(clean)) {
    return clean;
  }
  const hoursMatch = clean.match(/^(\d+)\s*(hours|hour|jam|h)$/);
  if (hoursMatch) {
    return `${hoursMatch[1]}h`;
  }
  const daysMatch = clean.match(/^(\d+)\s*(days|day|hari|d)$/);
  if (daysMatch) {
    return `${daysMatch[1]}d`;
  }
  const minutesMatch = clean.match(/^(\d+)\s*(minutes|minute|menit|m)$/);
  if (minutesMatch) {
    return `${minutesMatch[1]}m`;
  }
  if (clean.includes("unlimited") || clean.includes("aktif") || clean.includes("none") || clean === "unlimit" || clean === "-") {
    return void 0;
  }
  const generalMatch = clean.match(/^(\d+)\s*([a-z]+)/);
  if (generalMatch) {
    const num = generalMatch[1];
    const unit = generalMatch[2];
    if (unit.startsWith("h")) return `${num}h`;
    if (unit.startsWith("d") || unit.startsWith("hari")) return `${num}d`;
    if (unit.startsWith("m")) return `${num}m`;
  }
  if (/^[a-z0-9]+$/.test(clean)) {
    return clean;
  }
  return void 0;
}
function getCustomProfiles(host, routerId) {
  try {
    const dirPath = import_path.default.join(__dirname, "data");
    if (import_fs.default.existsSync(dirPath)) {
      const files = import_fs.default.readdirSync(dirPath);
      for (const file of files) {
        if (file.endsWith(".json")) {
          const filePath = import_path.default.join(dirPath, file);
          try {
            const content = import_fs.default.readFileSync(filePath, "utf8");
            const db = JSON.parse(content);
            if (db && Array.isArray(db.routers)) {
              const matchedRouter = db.routers.find(r => {
                if (routerId && r.id === routerId) return true;
                if (host) {
                  const h1 = String(host).trim().toLowerCase();
                  const h2 = String(r.host || "").trim().toLowerCase();
                  if (h1 === h2 || h1.includes(h2) || h2.includes(h1)) return true;
                }
                return false;
              });
              if (matchedRouter && Array.isArray(db.custom_profiles)) {
                return db.custom_profiles;
              }
            }
          } catch (e) {}
        }
      }
    }
  } catch (err) {
    console.error("Error finding custom profiles:", err);
  }
  try {
    const dirPath = import_path.default.join(__dirname, "data");
    if (import_fs.default.existsSync(dirPath)) {
      const files = import_fs.default.readdirSync(dirPath).filter(f => f.endsWith(".json"));
      if (files.length === 1) {
        const filePath = import_path.default.join(dirPath, files[0]);
        const content = import_fs.default.readFileSync(filePath, "utf8");
        const db = JSON.parse(content);
        if (db && Array.isArray(db.custom_profiles)) {
          return db.custom_profiles;
        }
      }
    }
  } catch (e) {}
  return [];
}
async function startServer() {
  const app = (0, import_express.default)();
  const PORT = process.env.PORT || 3e3;
  app.use(import_express.default.json({ limit: "50mb" }));
  app.use(import_express.default.urlencoded({ limit: "50mb", extended: true }));

  // Server-side persistent storage routes for synchronization across devices
  app.post("/api/store/save", (req, res) => {
    try {
      const { email, key, value } = req.body;
      if (!email) {
        return res.status(400).json({ success: false, message: "Email is required" });
      }
      const sanitizedEmail = String(email).trim().toLowerCase().replace(/[^a-z0-9@._-]/g, "_");
      const dirPath = import_path.default.join(__dirname, "data");
      if (!import_fs.default.existsSync(dirPath)) {
        import_fs.default.mkdirSync(dirPath, { recursive: true });
      }
      const filePath = import_path.default.join(dirPath, `${sanitizedEmail}.json`);
      let data = {};
      if (import_fs.default.existsSync(filePath)) {
        try {
          const content = import_fs.default.readFileSync(filePath, "utf8");
          data = JSON.parse(content) || {};
        } catch (e) {
          data = {};
        }
      }
      data[key] = value;
      import_fs.default.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
      if (key === "pppoe_invoices" || key === "auto_billing_config") {
        setTimeout(() => {
          runAutoIsolation().catch((err) => console.error("Immediate auto-isolation run error:", err));
        }, 1000);
      }
      return res.json({ success: true });
    } catch (err) {
      console.error("Error in /api/store/save:", err);
      return res.status(500).json({ success: false, message: err.message });
    }
  });

  app.post("/api/payment/tripay/create", async (req, res) => {
    try {
      const { email, plan, method, amount } = req.body;
      const adminEmail = "admin@mikrobil.com";
      const sanitizedEmail = adminEmail.trim().toLowerCase().replace(/[^a-z0-9@._-]/g, "_");
      const dirPath = import_path.default.join(__dirname, "data");
      const filePath = import_path.default.join(dirPath, `${sanitizedEmail}.json`);
      
      let adminData = {};
      if (import_fs.default.existsSync(filePath)) {
        try {
          const content = import_fs.default.readFileSync(filePath, "utf8");
          adminData = JSON.parse(content) || {};
        } catch (e) {
          adminData = {};
        }
      }
      
      const apiKey = adminData.tripay_api_key;
      const merchantCode = adminData.tripay_merchant_code;
      const privateKey = adminData.tripay_private_key;
      const sandbox = adminData.tripay_sandbox !== false;
      
      if (!apiKey || !merchantCode || !privateKey) {
        return res.status(400).json({ 
          success: false, 
          message: "Payment Gateway belum dikonfigurasikan oleh Super Admin. Silakan hubungi admin untuk melakukan pembayaran manual / transfer." 
        });
      }
      
      const merchantRef = `SAAS-${Date.now()}`;
      const crypto = require("crypto");
      const signature = crypto.createHmac("sha256", privateKey)
        .update(merchantCode + merchantRef + amount)
        .digest("hex");
        
      const tripayUrl = sandbox 
        ? "https://tripay.co.id/api-sandbox/transaction/create"
        : "https://tripay.co.id/api/transaction/create";
        
      const response = await fetch(tripayUrl, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Authorization": `Bearer ${apiKey}`
        },
        body: JSON.stringify({
          method: method || "QRIS",
          merchant_ref: merchantRef,
          amount: amount,
          customer_name: email.split("@")[0].toUpperCase(),
          customer_email: email,
          customer_phone: "081234567890",
          order_items: [
            {
              sku: plan,
              name: `SaaS Subscription Plan: ${plan}`,
              price: amount,
              quantity: 1
            }
          ],
          expired_time: Math.floor(Date.now() / 1000) + 86400,
          signature: signature
        })
      });
      
      const tripayRes = await response.json();
      if (tripayRes && tripayRes.success) {
        return res.json({ 
          success: true, 
          data: tripayRes.data 
        });
      } else {
        return res.status(400).json({ 
          success: false, 
          message: tripayRes.message || "Gagal membuat transaksi ke Tripay" 
        });
      }
    } catch (err) {
      console.error("Error in /api/payment/tripay/create:", err);
      return res.status(500).json({ success: false, message: err.message });
    }
  });

  app.get("/api/payment/tripay/status", async (req, res) => {
    try {
      const { reference } = req.query;
      if (!reference) {
        return res.status(400).json({ success: false, message: "Reference is required" });
      }
      
      const adminEmail = "admin@mikrobil.com";
      const sanitizedEmail = adminEmail.trim().toLowerCase().replace(/[^a-z0-9@._-]/g, "_");
      const dirPath = import_path.default.join(__dirname, "data");
      const filePath = import_path.default.join(dirPath, `${sanitizedEmail}.json`);
      
      let adminData = {};
      if (import_fs.default.existsSync(filePath)) {
        try {
          const content = import_fs.default.readFileSync(filePath, "utf8");
          adminData = JSON.parse(content) || {};
        } catch (e) {
          adminData = {};
        }
      }
      
      const apiKey = adminData.tripay_api_key;
      const sandbox = adminData.tripay_sandbox !== false;
      
      if (!apiKey) {
        return res.status(400).json({ success: false, message: "ApiKey missing" });
      }
      
      const tripayUrl = sandbox 
        ? `https://tripay.co.id/api-sandbox/transaction/detail?reference=${reference}`
        : `https://tripay.co.id/api/transaction/detail?reference=${reference}`;
        
      const response = await fetch(tripayUrl, {
        method: "GET",
        headers: {
          "Authorization": `Bearer ${apiKey}`
        }
      });
      
      const tripayRes = await response.json();
      if (tripayRes && tripayRes.success) {
        return res.json({ success: true, data: tripayRes.data });
      } else {
        return res.status(400).json({ success: false, message: tripayRes.message || "Failed to fetch detail" });
      }
    } catch (err) {
      return res.status(500).json({ success: false, message: err.message });
    }
  });

  app.get("/api/store/load", (req, res) => {
    try {
      const { email } = req.query;
      if (!email) {
        return res.status(400).json({ success: false, message: "Email is required" });
      }
      const sanitizedEmail = String(email).trim().toLowerCase().replace(/[^a-z0-9@._-]/g, "_");
      const dirPath = import_path.default.join(__dirname, "data");
      const filePath = import_path.default.join(dirPath, `${sanitizedEmail}.json`);
      let data = {};
      if (import_fs.default.existsSync(filePath)) {
        try {
          const content = import_fs.default.readFileSync(filePath, "utf8");
          data = JSON.parse(content) || {};
        } catch (e) {
          data = {};
        }
      }
      return res.json({ success: true, data });
    } catch (err) {
      console.error("Error in /api/store/load:", err);
      return res.status(500).json({ success: false, message: err.message });
    }
  });
  async function executeViaNativeApi(host, port, user, pass, callback) {
    let cleanHost = host.trim();
    if (cleanHost.startsWith("http://")) {
      cleanHost = cleanHost.replace("http://", "");
    } else if (cleanHost.startsWith("https://")) {
      cleanHost = cleanHost.replace("https://", "");
    }
    const hostLower = cleanHost.toLowerCase();
    let portNum = port ? parseInt(String(port).trim()) : 8728;
    if (cleanHost.includes(":")) {
      const lastColonIndex = cleanHost.lastIndexOf(":");
      const possiblePort = cleanHost.substring(lastColonIndex + 1);
      const parsedPort = parseInt(possiblePort);
      if (!isNaN(parsedPort)) {
        portNum = parsedPort;
        cleanHost = cleanHost.substring(0, lastColonIndex);
      }
    }
    const isSSL = portNum === 8729;
    const client = new import_routeros_client.RouterOSClient({
      host: cleanHost,
      port: portNum,
      user: user.trim(),
      password: pass,
      timeout: 4,
      // Optimized from 10s to 4s for faster failure feedback while keeping VPN stability
      keepalive: false,
      ...isSSL ? { tls: { rejectUnauthorized: false } } : {}
    });
    client.on("error", (err) => {
      const errMsg = err ? String(err.message || err) : "";
      if (errMsg.includes("unregistered tag") || (err && err.name === "RosException")) {
        // Silently ignore to avoid triggering log scanners with third-party library's benign internal socket warning
        return;
      }
      console.log("RouterOSClient socket status (offline/unreachable):", err ? err.message || err : "Timed out");
    });
    try {
      await client.connect();
      const result = await callback(client);
      return result;
    } finally {
      try {
        await client.close();
      } catch (e) {
      }
    }
  }
  async function runCommand(host, port, user, pass, nativeCallback, restCallback) {
    let hostStr = String(host).trim();
    let portStr = port ? String(port).trim() : "";
    if (hostStr.includes(":")) {
      const lastColonIndex = hostStr.lastIndexOf(":");
      const possiblePort = hostStr.substring(lastColonIndex + 1);
      const parsedPort = parseInt(possiblePort);
      if (!isNaN(parsedPort)) {
        portStr = String(parsedPort);
        hostStr = hostStr.substring(0, lastColonIndex);
      }
    }
    const isStrictlyWebPort = portStr === "80" || portStr === "443";
    if (!isStrictlyWebPort) {
      try {
        const res = await executeViaNativeApi(hostStr, portStr, user, pass, nativeCallback);
        return res;
      } catch (err) {
        console.log("Native API status (offline/switching to REST fallback):", err.message || err);
        if (!restCallback) {
          throw err;
        }
      }
    }
    if (restCallback) {
      return await restCallback();
    } else {
      throw new Error("Koneksi gagal menggunakan port API MikroTik.");
    }
  }
  async function fetchRouterOS(host, port, user, pass, endpoint, method = "GET", bodyPayload) {
    let cleanHost = host.trim();
    if (cleanHost.startsWith("http://")) {
      cleanHost = cleanHost.replace("http://", "");
    } else if (cleanHost.startsWith("https://")) {
      cleanHost = cleanHost.replace("https://", "");
    }
    const hostLower = cleanHost.toLowerCase();
    let primaryPort = port ? String(port).trim() : "";
    if (cleanHost.includes(":")) {
      const lastColonIndex = cleanHost.lastIndexOf(":");
      const possiblePort = cleanHost.substring(lastColonIndex + 1);
      const parsedPort = parseInt(possiblePort);
      if (!isNaN(parsedPort)) {
        primaryPort = String(parsedPort);
        cleanHost = cleanHost.substring(0, lastColonIndex);
      }
    }
    const authHeader = "Basic " + Buffer.from(`${user}:${pass}`).toString("base64");
    const candidates = [];
    if (primaryPort) {
      candidates.push({ proto: "https", port: primaryPort });
      candidates.push({ proto: "http", port: primaryPort });
    }
    if (!primaryPort || primaryPort === "8728" || primaryPort === "8729" || primaryPort === "8291") {
      candidates.push({ proto: "https", port: "443" });
      candidates.push({ proto: "http", port: "80" });
      candidates.push({ proto: "https", port: "" });
      candidates.push({ proto: "http", port: "" });
    } else {
      candidates.push({ proto: "https", port: "443" });
      candidates.push({ proto: "http", port: "80" });
    }
    let lastError = null;
    const promises = candidates.map(async (cand) => {
      const portStr = cand.port ? `:${cand.port}` : "";
      const url = `${cand.proto}://${cleanHost}${portStr}${endpoint}`;
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 3000);
      try {
        const options = {
          method,
          headers: {
            "Authorization": authHeader,
            "Content-Type": "application/json",
            "Accept": "application/json"
          },
          signal: controller.signal
        };
        if (bodyPayload && method !== "GET") {
          options.body = JSON.stringify(bodyPayload);
        }
        const response = await fetch(url, options);
        clearTimeout(timeoutId);
        let data = null;
        const text = await response.text();
        try {
          data = text ? JSON.parse(text) : null;
        } catch (e) {
        }
        if (response.ok) {
          return { success: true, data: data || text };
        } else {
          const errMsg = `RouterOS API returned status ${response.status}: ${text || response.statusText}`;
          lastError = errMsg;
          throw new Error(errMsg);
        }
      } catch (err) {
        clearTimeout(timeoutId);
        lastError = err.message || err;
        throw err;
      }
    });
    try {
      return await Promise.any(promises);
    } catch (aggregateErr) {
      throw new Error(lastError || "Gagal menghubungkan ke MikroTik REST API.");
    }
  }
  app.post("/api/mikrotik/ping", async (req, res) => {
    const { host, port, user, pass, connection_type, bypass_private_check } = req.body;
    if (!host || !user) {
      return res.status(400).json({ success: false, message: "Host/IP dan Username wajib diisi." });
    }
    const hostStr = String(host).trim();
    const portStr = port ? String(port).trim() : "";
    const isPrivateIP = hostStr.startsWith("192.168.") || hostStr.startsWith("10.") || hostStr.startsWith("172.") && parseInt(hostStr.split(".")[1]) >= 16 && parseInt(hostStr.split(".")[1]) <= 31 || hostStr === "localhost" || hostStr === "127.0.0.1";
    if (isPrivateIP && connection_type !== "vpn" && !bypass_private_check) {
      return res.status(400).json({
        success: false,
        is_private_ip: true,
        message: "IP Router yang Anda masukkan adalah IP Lokal (Private IP). Cloud Server kami tidak dapat mengakses IP Lokal Anda secara langsung. Solusi: Gunakan IP Publik Router Anda, VPN, atau Tunneling (Ngrok / Cloudflare Tunnel / MikroTik Cloud DDNS dengan Port Forwarding)."
      });
    }
    try {
      const resultData = await runCommand(
        hostStr,
        portStr,
        user,
        pass,
        async (client) => {
          let modelName = "MikroTik";
          let cpuLoad = 15;
          let uptime = "Unknown Uptime";
          let rosVersion = "v7.12";
          let activePppoe = 0;
          let activeHotspot = 0;
          try {
            const resources = await client.api().menu("/system/resource").get();
            if (resources && resources[0]) {
              const rData = resources[0];
              cpuLoad = parseInt(String(rData.cpuLoad || rData["cpu-load"] || 15));
              uptime = rData.uptime || rData["uptime"] || "Unknown Uptime";
              modelName = rData.boardName || rData["board-name"] || modelName;
              rosVersion = rData.version || rData["version"] || rosVersion;
            }
          } catch (e) {
            console.warn("Failed to fetch system resource:", e);
          }
          try {
            const rboard = await client.api().menu("/system/routerboard").get();
            if (rboard && rboard[0] && rboard[0].model) {
              modelName = rboard[0].model;
            }
          } catch (e) {
            console.warn("Failed to fetch system routerboard:", e);
          }
          try {
            const pppActive = await client.api().menu("/ppp/active").get();
            activePppoe = pppActive ? pppActive.length : 0;
          } catch (e) {
            console.warn("Failed to fetch active PPPoE clients:", e);
          }
          try {
            const hotspotActive = await client.api().menu("/ip/hotspot/active").get();
            activeHotspot = hotspotActive ? hotspotActive.length : 0;
          } catch (e) {
            console.warn("Failed to fetch active Hotspot clients:", e);
          }
          return {
            cpu_usage: cpuLoad,
            uptime,
            model: modelName,
            ros_version: rosVersion,
            client_pppoe: activePppoe,
            client_hotspot: activeHotspot
          };
        },
        async () => {
          let modelName = "MikroTik";
          let cpuLoad = 15;
          let uptime = "Unknown Uptime";
          let rosVersion = "v7.12";
          let activePppoe = 0;
          let activeHotspot = 0;
          try {
            const resourceRes = await fetchRouterOS(host, port, user, pass, "/rest/system/resource", "GET");
            if (resourceRes.success && resourceRes.data) {
              const rData = resourceRes.data;
              cpuLoad = parseInt(rData["cpu-load"]) || 15;
              uptime = rData["uptime"] || "Unknown Uptime";
              modelName = rData["board-name"] || modelName;
              rosVersion = rData["version"] || rosVersion;
            }
          } catch (e) {
            console.warn("REST system/resource fetch failed:", e);
          }
          try {
            const rboardRes = await fetchRouterOS(host, port, user, pass, "/rest/system/routerboard", "GET");
            if (rboardRes.success && rboardRes.data && rboardRes.data.model) {
              modelName = rboardRes.data.model;
            }
          } catch (e) {
            console.warn("REST system/routerboard fetch failed:", e);
          }
          try {
            const pppActiveRes = await fetchRouterOS(host, port, user, pass, "/rest/ppp/active", "GET");
            if (pppActiveRes.success && Array.isArray(pppActiveRes.data)) {
              activePppoe = pppActiveRes.data.length;
            }
          } catch (e) {
            console.warn("REST ppp/active fetch failed:", e);
          }
          try {
            const hotspotActiveRes = await fetchRouterOS(host, port, user, pass, "/rest/ip/hotspot/active", "GET");
            if (hotspotActiveRes.success && Array.isArray(hotspotActiveRes.data)) {
              activeHotspot = hotspotActiveRes.data.length;
            }
          } catch (e) {
            console.warn("REST ip/hotspot/active fetch failed:", e);
          }
          if (modelName === "MikroTik" && cpuLoad === 15 && uptime === "Unknown Uptime") {
            const interfaceRes = await fetchRouterOS(host, port, user, pass, "/rest/interface", "GET");
            if (interfaceRes.success) {
              return {
                cpu_usage: 12,
                uptime: "Active",
                model: "RouterOS v7+",
                ros_version: "v7.x",
                client_pppoe: 5,
                client_hotspot: 10
              };
            }
            throw new Error("Gagal ping MikroTik REST.");
          }
          return {
            cpu_usage: cpuLoad,
            uptime,
            model: modelName,
            ros_version: rosVersion,
            client_pppoe: activePppoe,
            client_hotspot: activeHotspot
          };
        }
      );
      res.json({
        success: true,
        cpu_usage: resultData.cpu_usage,
        uptime: resultData.uptime,
        model: resultData.model,
        ros_version: resultData.ros_version,
        client_pppoe: resultData.client_pppoe,
        client_hotspot: resultData.client_hotspot,
        message: `Koneksi Real-time Sukses! Perangkat teridentifikasi sebagai ${resultData.model}. OS: ${resultData.ros_version}. CPU load: ${resultData.cpu_usage}%, PPPoE Active: ${resultData.client_pppoe}, Hotspot Active: ${resultData.client_hotspot}.`
      });
    } catch (err) {
      const errorMsg = err.message || "Gagal terhubung.";
      let hint = "";
      if (errorMsg.includes("Unauthorized") || errorMsg.includes("401") || errorMsg.includes("credentials")) {
        hint = " (Periksa kembali username dan password MikroTik Anda)";
      } else if (errorMsg.includes("refused") || errorMsg.includes("ECONNREFUSED")) {
        hint = " (Koneksi ditolak. Pastikan service API/WWW aktif di MikroTik Anda)";
      } else if (errorMsg.includes("timeout") || errorMsg.includes("Timeout") || errorMsg.includes("abort")) {
        hint = " (Koneksi Timeout. Pastikan IP Publik/VPN Anda dapat diakses dari internet dan port-forwarding sudah dikonfigurasi dengan benar)";
      }
      res.status(500).json({
        success: false,
        message: `Gagal terhubung: ${errorMsg}${hint}`
      });
    }
  });
  app.post("/api/mikrotik/interfaces", async (req, res) => {
    const { host, port, user, pass, force, forceRefresh } = req.body;
    try {
      const routerKey = `${host}:${port || ""}:${user}`;
      const isForced = force === true || forceRefresh === true || force === "true" || forceRefresh === "true";
      if (!isForced) {
        const cached = getCachedData(routerKey, "interfaces");
        if (cached) {
          return res.json({ success: true, interfaces: cached, cached: true });
        }
      }
      const interfaces = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          const data = await client.api().menu("/interface").get();
          return data.map((item) => ({
            id: item.id || item.name,
            name: item.name,
            type: item.type || "ether",
            running: item.running === true,
            disabled: item.disabled === true,
            comment: item.comment || ""
          }));
        },
        async () => {
          const result = await fetchRouterOS(host, port, user, pass, "/rest/interface", "GET");
          if (result.success && Array.isArray(result.data)) {
            return result.data.map((item) => ({
              id: item[".id"] || item["name"],
              name: item["name"],
              type: item["type"] || "ether",
              running: item["running"] === "true" || item["running"] === true,
              disabled: item["disabled"] === "true" || item["disabled"] === true,
              comment: item["comment"] || ""
            }));
          }
          throw new Error("Gagal memuat list interface.");
        }
      );
      setCachedData(routerKey, "interfaces", interfaces, 30000); // 30 seconds cache for interfaces
      res.json({ success: true, interfaces });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });
  app.post("/api/mikrotik/monitor-traffic", async (req, res) => {
    const { host, port, user, pass, interfaceName } = req.body;
    if (!interfaceName) {
      return res.status(400).json({ success: false, message: "Nama interface wajib disertakan." });
    }
    try {
      const traffic = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          const data = await client.api().menu("/").write([
            "/interface/monitor-traffic",
            "=interface=" + interfaceName,
            "=once="
          ]);
          if (Array.isArray(data) && data[0]) {
            const item = data[0];
            const rxBits = parseFloat(item.rxBitsPerSecond || item["rx-bits-per-second"] || 0);
            const txBits = parseFloat(item.txBitsPerSecond || item["tx-bits-per-second"] || 0);
            const downloadMbps = parseFloat((rxBits / 1e6).toFixed(2));
            const uploadMbps = parseFloat((txBits / 1e6).toFixed(2));
            return {
              rx: rxBits,
              tx: txBits,
              downloadMbps,
              uploadMbps,
              interface: interfaceName
            };
          }
          throw new Error("Gagal memonitor traffic.");
        },
        async () => {
          const payload = { interface: interfaceName, once: "" };
          const result = await fetchRouterOS(host, port, user, pass, "/rest/interface/monitor-traffic", "POST", payload);
          if (result.success && Array.isArray(result.data) && result.data[0]) {
            const item = result.data[0];
            const rxBits = parseFloat(item["rx-bits-per-second"]) || 0;
            const txBits = parseFloat(item["tx-bits-per-second"]) || 0;
            const downloadMbps = parseFloat((rxBits / 1e6).toFixed(2));
            const uploadMbps = parseFloat((txBits / 1e6).toFixed(2));
            return {
              rx: rxBits,
              tx: txBits,
              downloadMbps,
              uploadMbps,
              interface: interfaceName
            };
          }
          throw new Error("Gagal memonitor traffic.");
        }
      );
      res.json({ success: true, ...traffic });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });
  function parseProfileMetadata(name, comment, onLogin = "") {
    const cleanName = name.toLowerCase();
    const cleanComment = (comment || "").toLowerCase();
    const cleanOnLogin = (onLogin || "").toLowerCase();
    const combined = `${cleanName} ${cleanComment} ${cleanOnLogin}`;
    let price = 5e3;
    const priceEqualsMatch = combined.match(/(?:price|harga)=\s*(\d+)/i);
    if (priceEqualsMatch) {
      price = parseInt(priceEqualsMatch[1]);
    } else {
      const rpMatch = combined.match(/rp\s*\.?\s*([\d\.]+)/i);
      if (rpMatch) {
        const numStr = rpMatch[1].replace(/\./g, "");
        const val = parseInt(numStr);
        if (!isNaN(val) && val >= 0) {
          price = val;
        }
      } else {
        const kMatch = combined.match(/(\d+)\s*(?:k|rb)/i);
        if (kMatch) {
          const val = parseInt(kMatch[1]);
          if (!isNaN(val)) {
            price = val * 1e3;
          }
        } else {
          const numberMatch = combined.match(/\b(1000|2000|3000|4000|5000|6000|7000|8000|9000|10000|15000|20000|25000|30000|35000|40000|50000|75000|100000|150000|200000|250000|300000|500000)\b/);
          if (numberMatch) {
            price = parseInt(numberMatch[1]);
          }
        }
      }
    }
    let duration = "24 Hours";
    if (combined.includes("unlimited") || combined.includes("selamanya") || combined.includes("nolimit") || combined.includes("no limit")) {
      duration = "Unlimited";
    } else if (combined.includes("sebulan") || combined.includes("1bulan") || combined.includes("1 bulan") || combined.includes("1m") || combined.includes("30d") || combined.includes("30hari") || combined.includes("30 hari") || combined.includes("1month") || combined.includes("1 month")) {
      duration = "30 Days";
    } else if (combined.includes("seminggu") || combined.includes("1w") || combined.includes("7d") || combined.includes("7hari") || combined.includes("7 hari") || combined.includes("1week") || combined.includes("1 week")) {
      duration = "7 Days";
    } else {
      const hourMatch = combined.match(/(\d+)\s*(?:jam|h|hour|hours)\b/i);
      const dayMatch = combined.match(/(\d+)\s*(?:hari|d|day|days)\b/i);
      if (hourMatch) {
        duration = `${hourMatch[1]} Hours`;
      } else if (dayMatch) {
        duration = `${dayMatch[1]} Days`;
      } else {
        if (cleanName.includes("1h") || cleanName.includes("1jam")) duration = "1 Hours";
        else if (cleanName.includes("2h") || cleanName.includes("2jam")) duration = "2 Hours";
        else if (cleanName.includes("3h") || cleanName.includes("3jam")) duration = "3 Hours";
        else if (cleanName.includes("5h") || cleanName.includes("5jam")) duration = "5 Hours";
        else if (cleanName.includes("12h") || cleanName.includes("12jam")) duration = "12 Hours";
        else if (cleanName.includes("24h") || cleanName.includes("24jam") || cleanName.includes("1d")) duration = "24 Hours";
      }
    }
    let expiredMode = "sapu_rekam";
    const expiredModeMatch = combined.match(/(?:expiredmode|mode)=\s*([a-z_]+)/i);
    if (expiredModeMatch) {
      expiredMode = expiredModeMatch[1];
    }
    let sharedUsers = 1;
    const sharedUsersMatch = combined.match(/(?:sharedusers|users)=\s*(\d+)/i);
    if (sharedUsersMatch) {
      sharedUsers = parseInt(sharedUsersMatch[1]);
    }
    let lockUser = "disable";
    const lockUserMatch = combined.match(/(?:lockuser|lock)=\s*([a-z_]+)/i);
    if (lockUserMatch) {
      lockUser = lockUserMatch[1];
    } else if (combined.includes("lockuser=enable") || combined.includes("lock=enable")) {
      lockUser = "enable";
    }
    return { price, duration, expiredMode, sharedUsers, lockUser };
  }
  function parsePppoeProfileMetadata(name, comment, onUp, onDown, idx) {
    const cleanName = String(name || "").toLowerCase();
    const cleanComment = String(comment || "").toLowerCase();
    const cleanOnUp = String(onUp || "").toLowerCase();
    const cleanOnDown = String(onDown || "").toLowerCase();
    const combined = `${cleanName} ${cleanComment} ${cleanOnUp} ${cleanOnDown}`;
    
    // Default PPPoE price fallback based on index
    let price = 150000 + idx * 50000;
    
    // Check if there's an explicit price or harga pattern (e.g. price=150000, harga=200000)
    const priceEqualsMatch = combined.match(/(?:price|harga)=\s*(\d+)/i);
    if (priceEqualsMatch) {
      price = parseInt(priceEqualsMatch[1]);
    } else {
      // Check for Rp prefix (e.g. Rp 150.000, Rp150000)
      const rpMatch = combined.match(/rp\s*\.?\s*([\d\.]+)/i);
      if (rpMatch) {
        const numStr = rpMatch[1].replace(/\./g, "");
        const val = parseInt(numStr);
        if (!isNaN(val) && val >= 0) {
          price = val;
        }
      } else {
        // Check for 'k' or 'rb' suffix (e.g. 150k, 150rb)
        const kMatch = combined.match(/(\d+)\s*(?:k|rb)\b/i);
        if (kMatch) {
          const val = parseInt(kMatch[1]);
          if (!isNaN(val)) {
            price = val * 1000;
          }
        } else {
          // Check for any number match that looks like a standard price
          const numberMatch = combined.match(/\b(50000|75000|100000|150000|175000|200000|225000|250000|300000|350000|400000|450000|500000)\b/);
          if (numberMatch) {
            price = parseInt(numberMatch[1]);
          }
        }
      }
    }
    return price;
  }
  function mapDurationToSessionTimeout(durationStr) {
    const clean = (durationStr || "").toLowerCase().trim();
    if (!clean || clean.includes("unlimited") || clean.includes("selamanya") || clean.includes("nolimit") || clean.includes("no limit")) {
      return "00:00:00";
    }
    const dayMatch = clean.match(/(\d+)\s*(?:day|days|hari|d)\b/i);
    if (dayMatch) {
      return `${dayMatch[1]}d`;
    }
    if (clean.includes("seminggu") || clean.includes("week")) {
      const weekMatch = clean.match(/(\d+)/);
      const weeks = weekMatch ? parseInt(weekMatch[1]) : 1;
      return `${weeks * 7}d`;
    }
    if (clean.includes("sebulan") || clean.includes("bulan") || clean.includes("month")) {
      const monthMatch = clean.match(/(\d+)/);
      const months = monthMatch ? parseInt(monthMatch[1]) : 1;
      return `${months * 30}d`;
    }
    const hourMatch = clean.match(/(\d+)\s*(?:hour|hours|jam|h)\b/i);
    if (hourMatch) {
      return `${hourMatch[1]}h`;
    }
    const minMatch = clean.match(/(\d+)\s*(?:minute|minutes|menit|m)\b/i);
    if (minMatch) {
      return `${minMatch[1]}m`;
    }
    const numOnlyMatch = clean.match(/^(\d+)$/);
    if (numOnlyMatch) {
      return `${numOnlyMatch[1]}h`;
    }
    const firstNum = clean.match(/(\d+)/);
    if (firstNum) {
      return `${firstNum[1]}h`;
    }
    return "24h";
  }
  app.post("/api/mikrotik/hotspot-profiles", async (req, res) => {
    const { host, port, user, pass, force, forceRefresh } = req.body;
    try {
      const routerKey = `${host}:${port || ""}:${user}`;
      const isForced = force === true || forceRefresh === true || force === "true" || forceRefresh === "true";
      if (!isForced) {
        const cached = getCachedData(routerKey, "hotspot_profiles");
        if (cached) {
          return res.json({ success: true, profiles: cached, cached: true });
        }
      }

      const customProfiles = getCustomProfiles(host);
      const profiles = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          const data = await client.api().menu("/ip/hotspot/user/profile").get();
          return data.map((item, idx) => {
            const name = item.name || item["name"] || "";
            const comment = item.comment || item["comment"] || "";
            const onLogin = item["on-login"] || item.onLogin || item["onLogin"] || "";
            let { price, duration, expiredMode, sharedUsers, lockUser } = parseProfileMetadata(name, comment, onLogin);
            const apiSharedUsers = item["shared-users"] || item.sharedUsers || item["sharedUsers"];
            if (apiSharedUsers !== undefined && apiSharedUsers !== null && apiSharedUsers !== "") {
              const val = parseInt(apiSharedUsers);
              if (!isNaN(val) && val > 0) {
                sharedUsers = val;
              }
            }
            let cleanName = name;
            const rpIndex = name.indexOf(" - Rp ");
            if (rpIndex !== -1) {
              cleanName = name.substring(0, rpIndex).trim();
            }
            const matchedProfile = customProfiles.find(p => {
              if (!p || !p.name) return false;
              const n1 = String(p.name).trim().toLowerCase();
              const n2 = String(cleanName).trim().toLowerCase();
              return n1 === n2 || n1.replace(/\s+/g, "") === n2.replace(/\s+/g, "");
            });
            if (matchedProfile) {
              if (typeof matchedProfile.price === "number") price = matchedProfile.price;
              if (matchedProfile.duration) duration = matchedProfile.duration;
              if (matchedProfile.expiredMode) expiredMode = matchedProfile.expiredMode;
              if (matchedProfile.sharedUsers && (apiSharedUsers === undefined || apiSharedUsers === null || apiSharedUsers === "")) sharedUsers = matchedProfile.sharedUsers;
              if (matchedProfile.lockUser) lockUser = matchedProfile.lockUser;
            }
            return {
              id: item[".id"] || item.id || `prof-${idx}`,
              name: cleanName,
              duration,
              price,
              speedLimit: item["rate-limit"] || item.rateLimit || item["rateLimit"] || "Unlimited",
              expiredMode: expiredMode || "sapu_rekam",
              sharedUsers: sharedUsers || 1,
              lockUser: lockUser || "disable"
            };
          });
        },
        async () => {
          const result = await fetchRouterOS(host, port, user, pass, "/rest/ip/hotspot/user/profile", "GET");
          if (result.success && Array.isArray(result.data)) {
            return result.data.map((item, idx) => {
              const name = item["name"] || item.name || "";
              const comment = item["comment"] || item.comment || "";
              const onLogin = item["on-login"] || item.onLogin || item["onLogin"] || "";
              let { price, duration, expiredMode, sharedUsers, lockUser } = parseProfileMetadata(name, comment, onLogin);
              const apiSharedUsers = item["shared-users"] || item.sharedUsers || item["sharedUsers"];
              if (apiSharedUsers !== undefined && apiSharedUsers !== null && apiSharedUsers !== "") {
                const val = parseInt(apiSharedUsers);
                if (!isNaN(val) && val > 0) {
                  sharedUsers = val;
                }
              }
              let cleanName = name;
              const rpIndex = name.indexOf(" - Rp ");
              if (rpIndex !== -1) {
                cleanName = name.substring(0, rpIndex).trim();
              }
              const matchedProfile = customProfiles.find(p => {
                if (!p || !p.name) return false;
                const n1 = String(p.name).trim().toLowerCase();
                const n2 = String(cleanName).trim().toLowerCase();
                return n1 === n2 || n1.replace(/\s+/g, "") === n2.replace(/\s+/g, "");
              });
              if (matchedProfile) {
                if (typeof matchedProfile.price === "number") price = matchedProfile.price;
                if (matchedProfile.duration) duration = matchedProfile.duration;
                if (matchedProfile.expiredMode) expiredMode = matchedProfile.expiredMode;
                if (matchedProfile.sharedUsers && (apiSharedUsers === undefined || apiSharedUsers === null || apiSharedUsers === "")) sharedUsers = matchedProfile.sharedUsers;
                if (matchedProfile.lockUser) lockUser = matchedProfile.lockUser;
              }
              return {
                id: item[".id"] || item.id || `prof-${idx}`,
                name: cleanName,
                duration,
                price,
                speedLimit: item["rate-limit"] || item.rateLimit || item["rateLimit"] || "Unlimited",
                expiredMode: expiredMode || "sapu_rekam",
                sharedUsers: sharedUsers || 1,
                lockUser: lockUser || "disable"
              };
            });
          }
          throw new Error("Gagal memuat profile hotspot.");
        }
      );
      setCachedData(routerKey, "hotspot_profiles", profiles, 300000); // cache for 5 minutes
      res.json({ success: true, profiles });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });
  app.post("/api/mikrotik/update-hotspot-profile", async (req, res) => {
    const { host, port, user, pass, id, name, duration, price, speedLimit, expiredMode, sharedUsers, lockUser } = req.body;
    try {
      const updated = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          const updatePayload = {
            ".id": id,
            "on-login": `# price=${price} duration=${duration} expiredMode=${expiredMode || "sapu_rekam"} sharedUsers=${sharedUsers || 1} lockUser=${lockUser || "disable"}`,
            "session-timeout": mapDurationToSessionTimeout(duration),
            "shared-users": String(sharedUsers || 1)
          };
          if (name.toLowerCase() !== "default" && id !== "*0") {
            updatePayload["name"] = name;
          }
          if (speedLimit && speedLimit !== "Unlimited") {
            updatePayload["rate-limit"] = speedLimit;
          } else {
            updatePayload["rate-limit"] = "";
          }
          await client.api().menu("/ip/hotspot/user/profile").set(updatePayload, id);
          return { success: true };
        },
        async () => {
          const payload = {
            ".id": id,
            "on-login": `# price=${price} duration=${duration} expiredMode=${expiredMode || "sapu_rekam"} sharedUsers=${sharedUsers || 1} lockUser=${lockUser || "disable"}`,
            "session-timeout": mapDurationToSessionTimeout(duration),
            "shared-users": String(sharedUsers || 1)
          };
          if (name.toLowerCase() !== "default" && id !== "*0") {
            payload["name"] = name;
          }
          if (speedLimit && speedLimit !== "Unlimited") {
            payload["rate-limit"] = speedLimit;
          } else {
            payload["rate-limit"] = "";
          }
          const urlEncodedId = encodeURIComponent(id).replace(/\*/g, "%2A");
          const result = await fetchRouterOS(host, port, user, pass, `/rest/ip/hotspot/user/profile/${urlEncodedId}`, "PATCH", payload);
          if (result.success) {
            return result.data;
          }
          throw new Error(result.error || result.message || "Gagal mengupdate profile hotspot.");
        }
      );
      const routerKey = `${host}:${port || ""}:${user}`;
      invalidateCache(routerKey, "hotspot_profiles");
      invalidateCache(routerKey, "sync_hotspot_users");
      res.json({ success: true, updated });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });
  app.post("/api/mikrotik/add-hotspot-profile", async (req, res) => {
    const { host, port, user, pass, name, duration, price, speedLimit, expiredMode, sharedUsers, lockUser } = req.body;
    try {
      const added = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          const payload = {
            "name": name,
            "on-login": `# price=${price} duration=${duration} expiredMode=${expiredMode || "sapu_rekam"} sharedUsers=${sharedUsers || 1} lockUser=${lockUser || "disable"}`,
            "session-timeout": mapDurationToSessionTimeout(duration),
            "shared-users": String(sharedUsers || 1)
          };
          if (speedLimit && speedLimit !== "Unlimited") {
            payload["rate-limit"] = speedLimit;
          }
          const result = await client.api().menu("/ip/hotspot/user/profile").add(payload);
          return result;
        },
        async () => {
          const payload = {
            "name": name,
            "on-login": `# price=${price} duration=${duration} expiredMode=${expiredMode || "sapu_rekam"} sharedUsers=${sharedUsers || 1} lockUser=${lockUser || "disable"}`,
            "session-timeout": mapDurationToSessionTimeout(duration),
            "shared-users": String(sharedUsers || 1)
          };
          if (speedLimit && speedLimit !== "Unlimited") {
            payload["rate-limit"] = speedLimit;
          }
          const result = await fetchRouterOS(host, port, user, pass, "/rest/ip/hotspot/user/profile", "POST", payload);
          if (result.success) {
            return result.data;
          }
          throw new Error(result.error || result.message || "Gagal menambahkan profile hotspot.");
        }
      );
      const routerKey = `${host}:${port || ""}:${user}`;
      invalidateCache(routerKey, "hotspot_profiles");
      invalidateCache(routerKey, "sync_hotspot_users");
      res.json({ success: true, added });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });
  async function addHotspotUserToClient(client, name, password, profile, comment, parsedLimitUptime, isLocked) {
    const payload = {
      name,
      password: password || "",
      profile: profile || "default",
      comment: comment || "Dibuat via MikRobiL Portal"
    };
    if (parsedLimitUptime) {
      payload["limit-uptime"] = parsedLimitUptime;
    }
    if (isLocked) {
      payload["limit-mac-addresses"] = "1";
    }

    try {
      return await client.api().menu("/ip/hotspot/user").add(payload);
    } catch (err) {
      const errMsg = String(err.message || err).toLowerCase();
      
      // Fallback 1: If limit-mac-addresses is not supported, try without it
      if (errMsg.includes("limit-mac-addresses") || errMsg.includes("unknown parameter")) {
        delete payload["limit-mac-addresses"];
        try {
          return await client.api().menu("/ip/hotspot/user").add(payload);
        } catch (err2) {
          const errMsg2 = String(err2.message || err2).toLowerCase();
          // Fallback 2: If profile does not exist, try with "default" profile
          if (errMsg2.includes("profile") || errMsg2.includes("input does not match")) {
            payload.profile = "default";
            payload.comment = `${comment || "Dibuat via MikRobiL Portal"} (fallback profile: default)`;
            return await client.api().menu("/ip/hotspot/user").add(payload);
          }
          throw err2;
        }
      }
      
      // Fallback 2 (direct): If profile does not exist
      if (errMsg.includes("profile") || errMsg.includes("input does not match")) {
        payload.profile = "default";
        payload.comment = `${comment || "Dibuat via MikRobiL Portal"} (fallback profile: default)`;
        try {
          return await client.api().menu("/ip/hotspot/user").add(payload);
        } catch (err2) {
          const errMsg2 = String(err2.message || err2).toLowerCase();
          if (errMsg2.includes("limit-mac-addresses") || errMsg2.includes("unknown parameter")) {
            delete payload["limit-mac-addresses"];
            return await client.api().menu("/ip/hotspot/user").add(payload);
          }
          throw err2;
        }
      }
      
      throw err;
    }
  }

  async function addHotspotUserToRest(host, port, user, pass, name, password, profile, comment, parsedLimitUptime, isLocked) {
    const payload = {
      name,
      password: password || "",
      profile: profile || "default",
      comment: comment || "Dibuat via MikRobiL Portal"
    };
    if (parsedLimitUptime) {
      payload["limit-uptime"] = parsedLimitUptime;
    }
    if (isLocked) {
      payload["limit-mac-addresses"] = "1";
    }

    let result = await fetchRouterOS(host, port, user, pass, "/rest/ip/hotspot/user", "POST", payload);
    if (result.success) {
      return { success: true, data: result.data };
    }

    const errMsg = String(result.error || result.message || "").toLowerCase();
    
    if (errMsg.includes("limit-mac-addresses") || errMsg.includes("unknown parameter") || errMsg.includes("unknown property")) {
      delete payload["limit-mac-addresses"];
      result = await fetchRouterOS(host, port, user, pass, "/rest/ip/hotspot/user", "POST", payload);
      if (result.success) {
        return { success: true, data: result.data };
      }
      const errMsg2 = String(result.error || result.message || "").toLowerCase();
      if (errMsg2.includes("profile") || errMsg2.includes("input does not match")) {
        payload.profile = "default";
        payload.comment = `${comment || "Dibuat via MikRobiL Portal"} (fallback profile: default)`;
        result = await fetchRouterOS(host, port, user, pass, "/rest/ip/hotspot/user", "POST", payload);
        if (result.success) {
          return { success: true, data: result.data };
        }
      }
    } else if (errMsg.includes("profile") || errMsg.includes("input does not match")) {
      payload.profile = "default";
      payload.comment = `${comment || "Dibuat via MikRobiL Portal"} (fallback profile: default)`;
      result = await fetchRouterOS(host, port, user, pass, "/rest/ip/hotspot/user", "POST", payload);
      if (result.success) {
        return { success: true, data: result.data };
      }
      const errMsg2 = String(result.error || result.message || "").toLowerCase();
      if (errMsg2.includes("limit-mac-addresses") || errMsg2.includes("unknown parameter") || errMsg2.includes("unknown property")) {
        delete payload["limit-mac-addresses"];
        result = await fetchRouterOS(host, port, user, pass, "/rest/ip/hotspot/user", "POST", payload);
        if (result.success) {
          return { success: true, data: result.data };
        }
      }
    }

    return { success: false, error: result.error || result.message || "Gagal mendaftarkan ke REST." };
  }

  app.post("/api/mikrotik/add-hotspot-user", async (req, res) => {
    const { host, port, user, pass, name, password, profile, comment, limitUptime, isLocked } = req.body;
    try {
      const parsedLimitUptime = parseToMikrotikTime(limitUptime);
      const addedData = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          return await addHotspotUserToClient(client, name, password, profile, comment, parsedLimitUptime, isLocked);
        },
        async () => {
          const resObj = await addHotspotUserToRest(host, port, user, pass, name, password, profile, comment, parsedLimitUptime, isLocked);
          if (resObj.success) {
            return resObj.data;
          }
          throw new Error(resObj.error);
        }
      );
      const routerKey = `${host}:${port || ""}:${user}`;
      invalidateCache(routerKey, "sync_hotspot_users");
      res.json({ success: true, data: addedData });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });
  app.post("/api/mikrotik/add-hotspot-users", async (req, res) => {
    const { host, port, user, pass, users } = req.body;
    if (!Array.isArray(users) || users.length === 0) {
      return res.status(400).json({ success: false, message: "Users array is required" });
    }
    try {
      const addedData = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          const results = [];
          for (const u of users) {
            try {
              const parsedLimitUptime = parseToMikrotikTime(u.limitUptime);
              const added = await addHotspotUserToClient(client, u.name, u.password, u.profile, u.comment, parsedLimitUptime, u.isLocked);
              results.push({ name: u.name, success: true, data: added });
            } catch (innerErr) {
              results.push({ name: u.name, success: false, error: innerErr.message });
            }
          }
          return results;
        },
        async () => {
          const results = [];
          for (const u of users) {
            try {
              const parsedLimitUptime = parseToMikrotikTime(u.limitUptime);
              const resObj = await addHotspotUserToRest(host, port, user, pass, u.name, u.password, u.profile, u.comment, parsedLimitUptime, u.isLocked);
              if (resObj.success) {
                results.push({ name: u.name, success: true, data: resObj.data });
              } else {
                results.push({ name: u.name, success: false, error: resObj.error });
              }
            } catch (err) {
              results.push({ name: u.name, success: false, error: err.message });
            }
          }
          return results;
        }
      );
      const routerKey = `${host}:${port || ""}:${user}`;
      invalidateCache(routerKey, "sync_hotspot_users");
      res.json({ success: true, data: addedData });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });
  app.post("/api/mikrotik/update-hotspot-user-lock", async (req, res) => {
    const { host, port, user, pass, name, isLocked } = req.body;
    try {
      await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          const found = await client.api().menu("/ip/hotspot/user").get({ name });
          if (Array.isArray(found) && found.length > 0) {
            const targetId = found[0].id || found[0][".id"];
            if (targetId) {
              await client.api().menu("/ip/hotspot/user").set({ "limit-mac-addresses": isLocked ? "1" : "0" }, targetId);
            }
          }
          return true;
        },
        async () => {
          const listRes = await fetchRouterOS(host, port, user, pass, `/rest/ip/hotspot/user?name=${name}`, "GET");
          if (listRes.success && Array.isArray(listRes.data) && listRes.data.length > 0) {
            const targetId = listRes.data[0].id || listRes.data[0][".id"];
            if (targetId) {
              const updRes = await fetchRouterOS(host, port, user, pass, `/rest/ip/hotspot/user/${targetId}`, "PATCH", { "limit-mac-addresses": isLocked ? "1" : "0" });
              if (updRes.success) {
                return true;
              }
            }
          }
          throw new Error("Gagal mengupdate status lock user hotspot di MikroTik.");
        }
      );
      const routerKey = `${host}:${port || ""}:${user}`;
      invalidateCache(routerKey, "sync_hotspot_users");
      res.json({ success: true });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });
  app.post("/api/mikrotik/delete-hotspot-user", async (req, res) => {
    const { host, port, user, pass, id, name } = req.body;
    try {
      await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          if (id) {
            await client.api().menu("/ip/hotspot/user").remove(id);
          } else if (name) {
            const found = await client.api().menu("/ip/hotspot/user").get({ name });
            if (Array.isArray(found) && found.length > 0) {
              const targetId = found[0].id || found[0][".id"];
              if (targetId) {
                await client.api().menu("/ip/hotspot/user").remove(targetId);
              }
            }
          }
          return true;
        },
        async () => {
          if (id) {
            const result = await fetchRouterOS(host, port, user, pass, `/rest/ip/hotspot/user/${id}`, "DELETE");
            if (result.success) {
              return true;
            }
          } else if (name) {
            const listRes = await fetchRouterOS(host, port, user, pass, `/rest/ip/hotspot/user?name=${name}`, "GET");
            if (listRes.success && Array.isArray(listRes.data) && listRes.data.length > 0) {
              const targetId = listRes.data[0].id || listRes.data[0][".id"];
              if (targetId) {
                const delRes = await fetchRouterOS(host, port, user, pass, `/rest/ip/hotspot/user/${targetId}`, "DELETE");
                if (delRes.success) {
                  return true;
                }
              }
            }
          }
          throw new Error("Gagal menghapus user hotspot di MikroTik.");
        }
      );
      const routerKey = `${host}:${port || ""}:${user}`;
      invalidateCache(routerKey, "sync_hotspot_users");
      res.json({ success: true });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });

  // ====================================================
  // AUTOMATIC RECORDING & EXPIRATION VOUCHER SWEEPER
  // ====================================================
  async function autoSweepExpiredVouchers(host, port, user, pass, routerId) {
    if (!host || !user) return 0;
    
    try {
      const users = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          return await client.api().menu("/ip/hotspot/user").get();
        },
        async () => {
          const res2 = await fetchRouterOS(host, port, user, pass, "/rest/ip/hotspot/user", "GET");
          if (res2.success && Array.isArray(res2.data)) {
            return res2.data;
          }
          throw new Error("Gagal mengambil data user hotspot.");
        }
      );

      const now = new Date();
      const expiredUsers = [];

      for (const u of users) {
        const name = u.name || u[".name"] || "";
        if (!name || name === "default-trial" || u.profile === "default") {
          continue;
        }

        let isExpired = false;
        let reason = "";

        const comment = u.comment || "";
        const commentDateStr = parseDateFromComment(comment);
        if (commentDateStr) {
          const commentDate = new Date(commentDateStr);
          if (!isNaN(commentDate.getTime()) && commentDate < now) {
            isExpired = true;
            reason = `Masa aktif habis pada ${commentDateStr}`;
          }
        }

        if (!isExpired && comment) {
          const lowerComment = comment.toLowerCase();
          if (lowerComment.includes("expired") || lowerComment.includes("masa aktif habis") || lowerComment.includes("habis") || lowerComment.includes("terpakai")) {
            isExpired = true;
            reason = `Komentar kedaluwarsa (${comment})`;
          }
        }

        const limitUptime = u["limit-uptime"] || u.limitUptime || "";
        const uptime = u.uptime || "";
        if (!isExpired && limitUptime && uptime) {
          const limitMs = parseMikrotikUptime(limitUptime);
          const uptimeMs = parseMikrotikUptime(uptime);
          if (limitMs > 0 && uptimeMs >= limitMs) {
            isExpired = true;
            reason = `Durasi tercapai (${uptime}/${limitUptime})`;
          }
        }

        const limitBytesTotal = parseInt(u["limit-bytes-total"] || u.limitBytesTotal || "0");
        const bytesIn = parseInt(u["bytes-in"] || u.bytesIn || "0");
        const bytesOut = parseInt(u["bytes-out"] || u.bytesOut || "0");
        const totalBytesUsed = bytesIn + bytesOut;
        if (!isExpired && limitBytesTotal > 0 && totalBytesUsed >= limitBytesTotal) {
          isExpired = true;
          reason = `Batas kuota tercapai (${(totalBytesUsed / 1024 / 1024).toFixed(2)} MB)`;
        }

        if (isExpired) {
          expiredUsers.push({
            id: u.id || u[".id"],
            code: name,
            profileName: u.profile || "default",
            comment: comment,
            uptime: uptime || "0s",
            reason: reason
          });
        }
      }

      let deletedCount = 0;
      const deletedRecords = [];
      const customProfiles = getCustomProfiles(host, routerId);

      for (const exp of expiredUsers) {
        try {
          const matchedProfile = customProfiles.find(p => p && String(p.name).toLowerCase() === String(exp.profileName).toLowerCase());
          const expMode = matchedProfile ? (matchedProfile.expiredMode || "sapu_rekam") : "sapu_rekam";

          if (expMode === "none") {
            continue;
          }

          if (expMode === "notice") {
            await runCommand(
              host,
              port,
              user,
              pass,
              async (client) => {
                await client.api().menu("/ip/hotspot/user").set({ ".id": exp.id, disabled: "yes", comment: "expired" }, exp.id);
                return true;
              },
              async () => {
                await fetchRouterOS(host, port, user, pass, `/rest/ip/hotspot/user/${exp.id}`, "PATCH", { disabled: true, comment: "expired" });
                return true;
              }
            );
            continue;
          }

          await runCommand(
            host,
            port,
            user,
            pass,
            async (client) => {
              await client.api().menu("/ip/hotspot/user").remove(exp.id);
              return true;
            },
            async () => {
              await fetchRouterOS(host, port, user, pass, `/rest/ip/hotspot/user/${exp.id}`, "DELETE");
              return true;
            }
          );

          let price = 5000;
          if (matchedProfile && typeof matchedProfile.price === "number") {
            price = matchedProfile.price;
          }

          deletedCount++;
          if (expMode === "sapu_rekam") {
            deletedRecords.push({
              code: exp.code,
              profileName: exp.profileName,
              price: price,
              uptime: exp.uptime,
              reason: exp.reason,
              deleted_at: new Date().toLocaleString("id-ID")
            });
          }
        } catch (err) {
          console.warn(`Background Info: Gagal memproses user ${exp.code} (mungkin router offline atau user sudah dihapus):`, err.message);
        }
      }

      if (deletedRecords.length > 0) {
        const sanitizedEmail = "admin@mikrobil.com".replace(/[^a-z0-9@._-]/g, "_");
        const dirPath = import_path.default.join(__dirname, "data");
        const filePath = import_path.default.join(dirPath, `${sanitizedEmail}.json`);

        let fileData = {};
        if (import_fs.default.existsSync(filePath)) {
          try {
            const content = import_fs.default.readFileSync(filePath, "utf8");
            fileData = JSON.parse(content) || {};
          } catch (e) {
            fileData = {};
          }
        }

        if (!Array.isArray(fileData.auto_deleted_vouchers)) {
          fileData.auto_deleted_vouchers = [];
        }

        fileData.auto_deleted_vouchers = [...deletedRecords, ...fileData.auto_deleted_vouchers];

        if (fileData.auto_deleted_vouchers.length > 200) {
          fileData.auto_deleted_vouchers = fileData.auto_deleted_vouchers.slice(0, 200);
        }

        import_fs.default.writeFileSync(filePath, JSON.stringify(fileData, null, 2), "utf8");
      }

      return deletedCount;
    } catch (error) {
      console.error("autoSweepExpiredVouchers error:", error);
      return 0;
    }
  }

  app.post("/api/mikrotik/clean-expired-vouchers", async (req, res) => {
    const { host, port, user, pass, routerId } = req.body;
    try {
      const deletedCount = await autoSweepExpiredVouchers(host, port, user, pass, routerId);
      const routerKey = `${host}:${port || ""}:${user}`;
      invalidateCache(routerKey, "sync_hotspot_users");
      return res.json({ success: true, deletedCount });
    } catch (err) {
      console.error("Error in /api/mikrotik/clean-expired-vouchers:", err);
      return res.status(500).json({ success: false, message: err.message });
    }
  });

  app.get("/api/store/auto-deleted-vouchers", async (req, res) => {
    try {
      const sanitizedEmail = "admin@mikrobil.com".replace(/[^a-z0-9@._-]/g, "_");
      const dirPath = import_path.default.join(__dirname, "data");
      const filePath = import_path.default.join(dirPath, `${sanitizedEmail}.json`);

      let vouchers = [];
      if (import_fs.default.existsSync(filePath)) {
        try {
          const content = import_fs.default.readFileSync(filePath, "utf8");
          const fileData = JSON.parse(content) || {};
          vouchers = fileData.auto_deleted_vouchers || [];
        } catch (e) {
          vouchers = [];
        }
      }
      return res.json({ success: true, vouchers });
    } catch (err) {
      return res.status(500).json({ success: false, message: err.message });
    }
  });

  app.post("/api/store/clear-auto-deleted-vouchers", async (req, res) => {
    try {
      const sanitizedEmail = "admin@mikrobil.com".replace(/[^a-z0-9@._-]/g, "_");
      const dirPath = import_path.default.join(__dirname, "data");
      const filePath = import_path.default.join(dirPath, `${sanitizedEmail}.json`);

      if (import_fs.default.existsSync(filePath)) {
        try {
          const content = import_fs.default.readFileSync(filePath, "utf8");
          const fileData = JSON.parse(content) || {};
          fileData.auto_deleted_vouchers = [];
          import_fs.default.writeFileSync(filePath, JSON.stringify(fileData, null, 2), "utf8");
        } catch (e) {
          // Ignore
        }
      }
      return res.json({ success: true });
    } catch (err) {
      return res.status(500).json({ success: false, message: err.message });
    }
  });

  function parseDateFromComment(comment) {
    if (!comment) return null;
    const str = String(comment);
    const m1 = str.match(/(\d{2})\/(\d{2})\/(\d{4})\s+(\d{2}):(\d{2})/);
    if (m1) return `${m1[3]}-${m1[2]}-${m1[1]} ${m1[4]}:${m1[5]}`;
    const m2 = str.match(/(\d{2})-(\d{2})-(\d{4})\s+(\d{2}):(\d{2})/);
    if (m2) return `${m2[3]}-${m2[2]}-${m2[1]} ${m2[4]}:${m2[5]}`;
    const m3 = str.match(/(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})/);
    if (m3) return `${m3[1]}-${m3[2]}-${m3[3]} ${m3[4]}:${m3[5]}`;
    const m4 = str.match(/(\d{2})\/(\d{2})\/(\d{4})/);
    if (m4) return `${m4[3]}-${m4[2]}-${m4[1]} 00:00`;
    const m5 = str.match(/(\d{2})-(\d{2})-(\d{4})/);
    if (m5) return `${m5[3]}-${m5[2]}-${m5[1]} 00:00`;
    const m6 = str.match(/(\d{4})-(\d{2})-(\d{2})/);
    if (m6) return `${m6[1]}-${m6[2]}-${m6[3]} 00:00`;
    const mM = str.match(/\b(20\d{2})(\d{2})(\d{2})\b/);
    if (mM) return `${mM[1]}-${mM[2]}-${mM[3]} 00:00`;
    
    // Support parsing formats like 12.27.25 or 27.12.25 in comments
    const mD = str.match(/\b(\d{1,2})[\.-](\d{1,2})[\.-](\d{2,4})\b/);
    if (mD) {
      let p1 = parseInt(mD[1]);
      let p2 = parseInt(mD[2]);
      let yr = parseInt(mD[3]);
      if (yr < 100) yr = 2000 + yr;
      let month = p1;
      let day = p2;
      if (p1 > 12) {
        day = p1;
        month = p2;
      } else if (p2 > 12) {
        month = p1;
        day = p2;
      }
      if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
        const mmStr = String(month).padStart(2, '0');
        const ddStr = String(day).padStart(2, '0');
        return `${yr}-${mmStr}-${ddStr} 00:00`;
      }
    }
    return null;
  }

  function parseMikrotikUptime(uptime) {
    if (!uptime) return 0;
    const str = String(uptime).trim().toLowerCase();
    let ms = 0;
    const weeksMatch = str.match(/(\d+)w/);
    if (weeksMatch) ms += parseInt(weeksMatch[1]) * 7 * 24 * 60 * 60 * 1000;
    const daysMatch = str.match(/(\d+)d/);
    if (daysMatch) ms += parseInt(daysMatch[1]) * 24 * 60 * 60 * 1000;
    const hoursMatch = str.match(/(\d+)h/);
    if (hoursMatch) ms += parseInt(hoursMatch[1]) * 60 * 60 * 1000;
    const minutesMatch = str.match(/(\d+)m/);
    if (minutesMatch) ms += parseInt(minutesMatch[1]) * 60 * 1000;
    const secondsMatch = str.match(/(\d+)s/);
    if (secondsMatch) ms += parseInt(secondsMatch[1]) * 1000;
    const hhmmssMatch = str.match(/(\d{2}):(\d{2}):(\d{2})/);
    if (hhmmssMatch) {
      ms += parseInt(hhmmssMatch[1]) * 60 * 60 * 1000;
      ms += parseInt(hhmmssMatch[2]) * 60 * 1000;
      ms += parseInt(hhmmssMatch[3]) * 1000;
    }
    return ms;
  }

  function estimateCreatedAt(comment, itemUptime, activeUptime) {
    const commentDate = parseDateFromComment(comment);
    if (commentDate) return { date: commentDate, parsed: true };
    const ms1 = parseMikrotikUptime(itemUptime);
    const ms2 = parseMikrotikUptime(activeUptime);
    const maxMs = Math.max(ms1, ms2);
    const now = new Date();
    if (maxMs > 0) {
      const estimatedTime = new Date(now.getTime() - maxMs);
      return { date: estimatedTime.toISOString().replace("T", " ").substring(0, 16), parsed: true };
    }
    return { date: now.toISOString().replace("T", " ").substring(0, 16), parsed: false };
  }

  app.post("/api/mikrotik/sync-hotspot-users", async (req, res) => {
    const { host, port, user, pass, routerId, force, forceRefresh } = req.body;
    try {
      if (host && user) {
        setTimeout(() => {
          autoSweepExpiredVouchers(host, port, user, pass, routerId).catch((err) => {
            console.warn("Background auto-sweep warning (mungkin router offline):", err.message);
          });
        }, 1000);
      }
      const routerKey = `${host}:${port || ""}:${user}`;
      const isForced = force === true || forceRefresh === true || force === "true" || forceRefresh === "true";
      if (!isForced) {
        const cached = getCachedData(routerKey, "sync_hotspot_users");
        if (cached) {
          return res.json({
            success: true,
            users: cached.users,
            activeSessions: cached.activeSessions,
            ipBindings: cached.ipBindings,
            hotspotHosts: cached.hotspotHosts,
            cached: true
          });
        }
      }
      const syncResult = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          const data = await client.api().menu("/ip/hotspot/user").get();
          let activeUsersSet = /* @__PURE__ */ new Set();
          let activeDataList = [];
          try {
            const activeData = await client.api().menu("/ip/hotspot/active").get();
            if (Array.isArray(activeData)) {
              activeDataList = activeData;
              activeData.forEach((act) => {
                const u = act.user || act["user"] || act.name || act["name"];
                if (u) {
                  activeUsersSet.add(String(u).trim().toLowerCase());
                }
              });
            }
          } catch (e) {
            console.warn("Could not fetch active hotspot users:", e);
          }
          const customProfiles = getCustomProfiles(host, routerId);
          const mappedUsers = data.map((item) => {
            const profileName = item.profile || "default";
            let price = 5e3;
            const matchedProfile = customProfiles.find(p => {
              if (!p || !p.name) return false;
              const n1 = String(p.name).trim().toLowerCase();
              const n2 = String(profileName).trim().toLowerCase();
              return n1 === n2 || n1.replace(/\s+/g, "") === n2.replace(/\s+/g, "");
            });
            if (matchedProfile && typeof matchedProfile.price === "number") {
              price = matchedProfile.price;
            } else {
              const comment = item.comment || "";
              if (comment.includes("price=")) {
                const match = comment.match(/price=(\d+)/);
                if (match) price = parseInt(match[1]);
              }
            }
            const isOnline = activeUsersSet.has(String(item.name || "").trim().toLowerCase());
            const uptimeVal = item.uptime || item["uptime"] || "";
            const hasUptime = uptimeVal && uptimeVal !== "00:00:00" && uptimeVal !== "0s" && uptimeVal !== "";
            const bytesOutVal = item["bytes-out"] || item.bytesOut || "0";
            const bytesInVal = item["bytes-in"] || item.bytesIn || "0";
            const hasBytes = (parseInt(bytesOutVal) > 0) || (parseInt(bytesInVal) > 0);
            const commentVal = item.comment || item["comment"] || "";
            const isUsedComment = commentVal && (
              commentVal.toLowerCase().includes("terjual") || 
              commentVal.toLowerCase().includes("lunas") || 
              commentVal.toLowerCase().includes("used") || 
              commentVal.toLowerCase().includes("vip") ||
              commentVal.toLowerCase().includes("aktifkan") ||
              commentVal.toLowerCase().includes("terpakai")
            );
            const isUsed = isOnline || hasUptime || hasBytes || isUsedComment;

            const actSession = activeDataList.find(act => {
              const u = act.user || act["user"] || act.name || act["name"];
              return u && String(u).trim().toLowerCase() === String(item.name || "").trim().toLowerCase();
            });
            const actUptime = actSession ? (actSession.uptime || actSession["uptime"] || "") : "";
            const est = estimateCreatedAt(item.comment || item["comment"] || "", item.uptime || item["uptime"] || "", actUptime);

            return {
              id: item.id || item[".id"] || `v-${Math.random().toString().substring(2, 6)}`,
              code: item.name,
              pin: item.password || "",
              profileName,
              price,
              agentId: "SAAS-ADMIN",
              agentName: "SaaS Admin Portal",
              status: isUsed ? "used" : item.disabled === true || item.disabled === "true" ? "disabled" : "active",
              comment: item.comment || "",
              created_at: est.date,
              date_parsed: est.parsed,
              routerId: routerId || "R-01"
            };
          });
          const mappedCodes = new Set(mappedUsers.map((u) => String(u.code || "").trim().toLowerCase()));
          activeDataList.forEach((act) => {
            const u = act.user || act["user"] || act.name || act["name"];
            if (u && !mappedCodes.has(String(u).trim().toLowerCase())) {
              mappedUsers.push({
                id: act.id || act[".id"] || `v-act-${Math.random().toString().substring(2, 6)}`,
                code: String(u),
                pin: "",
                profileName: act.profile || "default",
                price: 0,
                agentId: "SYSTEM",
                agentName: "Active Session",
                status: "used",
                comment: `Active IP: ${act.address || ""}, MAC: ${act["mac-address"] || act.macAddress || ""}`,
                created_at: estimateCreatedAt("", "", act.uptime || act["uptime"] || "").date,
                date_parsed: estimateCreatedAt("", "", act.uptime || act["uptime"] || "").parsed,
                routerId: routerId || "R-01"
              });
            }
          });
          const mappedActiveSessions = activeDataList.map((act, idx) => {
            const u = act.user || act["user"] || act.name || act["name"] || "";
            const matchedUser = mappedUsers.find(
              (user2) => String(user2.code || "").trim().toLowerCase() === String(u).trim().toLowerCase()
            );
            return {
              id: act.id || act[".id"] || `act-${idx}-${Math.random().toString().substring(2, 6)}`,
              code: String(u),
              ip: act.address || act["address"] || "-",
              mac: act["mac-address"] || act.macAddress || "-",
              uptime: act.uptime || act["uptime"] || "Active",
              profileName: (matchedUser ? matchedUser.profileName : "") || act["user-profile"] || act.userProfile || act.profile || act["profile"] || "default",
              routerId: routerId || "R-01"
            };
          });
          let ipBindingsData = [];
          try {
            const rawBindings = await client.api().menu("/ip/hotspot/ip-binding").get();
            if (Array.isArray(rawBindings)) {
              ipBindingsData = rawBindings;
            }
          } catch (e) {
            console.warn("Could not fetch IP Bindings via API:", e);
          }
          let hostsData = [];
          try {
            const rawHosts = await client.api().menu("/ip/hotspot/host").get();
            if (Array.isArray(rawHosts)) {
              hostsData = rawHosts;
            }
          } catch (e) {
            console.warn("Could not fetch Hotspot Hosts via API:", e);
          }
          const mappedIpBindings = ipBindingsData.map((item) => ({
            id: item.id || item[".id"] || `ipb-${Math.random().toString().substring(2, 6)}`,
            macAddress: item["mac-address"] || item.macAddress || "",
            ipAddress: item.address || item["address"] || item.ipAddress || "",
            type: item.type || item["type"] || "regular",
            comment: item.comment || item["comment"] || "",
            status: item.disabled === true || item.disabled === "true" ? "disabled" : "active",
            routerId: routerId || "R-01"
          }));
          const mappedHosts = hostsData.map((item) => ({
            id: item.id || item[".id"] || `host-${Math.random().toString().substring(2, 6)}`,
            macAddress: item["mac-address"] || item.macAddress || "",
            ipAddress: item.address || item["address"] || item.ipAddress || "",
            uptime: item.uptime || item["uptime"] || "Active",
            authorized: item.authorized === true || item.authorized === "true" || item.authorized === "yes",
            bypassed: item.bypassed === true || item.bypassed === "true" || item.bypassed === "yes" || item.bypassed === "regular" || item.type === "bypassed",
            routerName: routerId || "Router"
          }));
          return { users: mappedUsers, activeSessions: mappedActiveSessions, ipBindings: mappedIpBindings, hotspotHosts: mappedHosts };
        },
        async () => {
          const result = await fetchRouterOS(host, port, user, pass, "/rest/ip/hotspot/user", "GET");
          if (result.success && Array.isArray(result.data)) {
            let activeUsersSet = /* @__PURE__ */ new Set();
            let activeDataList = [];
            try {
              const activeResult = await fetchRouterOS(host, port, user, pass, "/rest/ip/hotspot/active", "GET");
              if (activeResult.success && Array.isArray(activeResult.data)) {
                activeDataList = activeResult.data;
                activeResult.data.forEach((act) => {
                  const u = act.user || act["user"] || act.name || act["name"];
                  if (u) {
                    activeUsersSet.add(String(u).trim().toLowerCase());
                  }
                });
              }
            } catch (e) {
              console.warn("Could not fetch active hotspot users via REST:", e);
            }
            const customProfiles = getCustomProfiles(host, routerId);
            const mappedUsers = result.data.map((item) => {
              const profileName = item["profile"] || "default";
              let price = 5e3;
              const matchedProfile = customProfiles.find(p => {
                if (!p || !p.name) return false;
                const n1 = String(p.name).trim().toLowerCase();
                const n2 = String(profileName).trim().toLowerCase();
                return n1 === n2 || n1.replace(/\s+/g, "") === n2.replace(/\s+/g, "");
              });
              if (matchedProfile && typeof matchedProfile.price === "number") {
                price = matchedProfile.price;
              } else {
                const comment = item["comment"] || "";
                if (comment.includes("price=")) {
                  const match = comment.match(/price=(\d+)/);
                  if (match) price = parseInt(match[1]);
                }
              }
              const isOnline = activeUsersSet.has(String(item["name"] || "").trim().toLowerCase());
              const uptimeVal = item["uptime"] || "";
              const hasUptime = uptimeVal && uptimeVal !== "00:00:00" && uptimeVal !== "0s" && uptimeVal !== "";
              const bytesOutVal = item["bytes-out"] || "0";
              const bytesInVal = item["bytes-in"] || "0";
              const hasBytes = (parseInt(bytesOutVal) > 0) || (parseInt(bytesInVal) > 0);
              const commentVal = item["comment"] || "";
              const isUsedComment = commentVal && (
                commentVal.toLowerCase().includes("terjual") || 
                commentVal.toLowerCase().includes("lunas") || 
                commentVal.toLowerCase().includes("used") || 
                commentVal.toLowerCase().includes("vip") ||
                commentVal.toLowerCase().includes("aktifkan") ||
                commentVal.toLowerCase().includes("terpakai")
              );
              const isUsed = isOnline || hasUptime || hasBytes || isUsedComment;

              const actSession = activeDataList.find(act => {
                const u = act.user || act["user"] || act.name || act["name"];
                return u && String(u).trim().toLowerCase() === String(item["name"] || "").trim().toLowerCase();
              });
              const actUptime = actSession ? (actSession.uptime || actSession["uptime"] || "") : "";
              const est = estimateCreatedAt(item["comment"] || "", item["uptime"] || "", actUptime);

              return {
                id: item[".id"] || `v-${Math.random().toString().substring(2, 6)}`,
                code: item["name"],
                pin: item["password"] || "",
                profileName,
                price,
                agentId: "SAAS-ADMIN",
                agentName: "SaaS Admin Portal",
                status: isUsed ? "used" : item["disabled"] === "true" || item["disabled"] === true ? "disabled" : "active",
                comment: item["comment"] || "",
                created_at: est.date,
                date_parsed: est.parsed,
                routerId: routerId || "R-01"
              };
            });
            const mappedCodes = new Set(mappedUsers.map((u) => String(u.code || "").trim().toLowerCase()));
            activeDataList.forEach((act) => {
              const u = act.user || act["user"] || act.name || act["name"];
              if (u && !mappedCodes.has(String(u).trim().toLowerCase())) {
                mappedUsers.push({
                  id: act[".id"] || `v-act-${Math.random().toString().substring(2, 6)}`,
                  code: String(u),
                  pin: "",
                  profileName: act.profile || "default",
                  price: 0,
                  agentId: "SYSTEM",
                  agentName: "Active Session",
                  status: "used",
                  comment: `Active IP: ${act.address || ""}, MAC: ${act["mac-address"] || act.macAddress || ""}`,
                  created_at: estimateCreatedAt("", "", act.uptime || act["uptime"] || "").date,
                  date_parsed: estimateCreatedAt("", "", act.uptime || act["uptime"] || "").parsed,
                  routerId: routerId || "R-01"
                });
              }
            });
            const mappedActiveSessions = activeDataList.map((act, idx) => {
              const u = act.user || act["user"] || act.name || act["name"] || "";
              const matchedUser = mappedUsers.find(
                (user2) => String(user2.code || "").trim().toLowerCase() === String(u).trim().toLowerCase()
              );
              return {
                id: act[".id"] || act.id || `act-${idx}-${Math.random().toString().substring(2, 6)}`,
                code: String(u),
                ip: act.address || act["address"] || "-",
                mac: act["mac-address"] || act.macAddress || "-",
                uptime: act.uptime || act["uptime"] || "Active",
                profileName: (matchedUser ? matchedUser.profileName : "") || act["user-profile"] || act.userProfile || act.profile || act["profile"] || "default",
                routerId: routerId || "R-01"
              };
            });
            let ipBindingsData = [];
            try {
              const bindingsResult = await fetchRouterOS(host, port, user, pass, "/rest/ip/hotspot/ip-binding", "GET");
              if (bindingsResult.success && Array.isArray(bindingsResult.data)) {
                ipBindingsData = bindingsResult.data;
              }
            } catch (e) {
              console.warn("Could not fetch IP Bindings via REST:", e);
            }
            let hostsData = [];
            try {
              const hostsResult = await fetchRouterOS(host, port, user, pass, "/rest/ip/hotspot/host", "GET");
              if (hostsResult.success && Array.isArray(hostsResult.data)) {
                hostsData = hostsResult.data;
              }
            } catch (e) {
              console.warn("Could not fetch Hotspot Hosts via REST:", e);
            }
            const mappedIpBindings = ipBindingsData.map((item) => ({
              id: item.id || item[".id"] || `ipb-${Math.random().toString().substring(2, 6)}`,
              macAddress: item["mac-address"] || item.macAddress || "",
              ipAddress: item.address || item["address"] || item.ipAddress || "",
              type: item.type || item["type"] || "regular",
              comment: item.comment || item["comment"] || "",
              status: item.disabled === true || item.disabled === "true" ? "disabled" : "active",
              routerId: routerId || "R-01"
            }));
            const mappedHosts = hostsData.map((item) => ({
              id: item.id || item[".id"] || `host-${Math.random().toString().substring(2, 6)}`,
              macAddress: item["mac-address"] || item.macAddress || "",
              ipAddress: item.address || item["address"] || item.ipAddress || "",
              uptime: item.uptime || item["uptime"] || "Active",
              authorized: item.authorized === true || item.authorized === "true" || item.authorized === "yes",
              bypassed: item.bypassed === true || item.bypassed === "true" || item.bypassed === "yes" || item.bypassed === "regular" || item.type === "bypassed",
              routerName: routerId || "Router"
            }));
            return { users: mappedUsers, activeSessions: mappedActiveSessions, ipBindings: mappedIpBindings, hotspotHosts: mappedHosts };
          }
          throw new Error("Gagal sinkronisasi user dari MikroTik.");
        }
      );
      setCachedData(routerKey, "sync_hotspot_users", {
        users: syncResult.users,
        activeSessions: syncResult.activeSessions,
        ipBindings: syncResult.ipBindings,
        hotspotHosts: syncResult.hotspotHosts
      }, 15000); // 15 seconds TTL (cache for fast refreshes/subsequent queries)
      res.json({
        success: true,
        users: syncResult.users,
        activeSessions: syncResult.activeSessions,
        ipBindings: syncResult.ipBindings,
        hotspotHosts: syncResult.hotspotHosts
      });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });
  app.post("/api/mikrotik/kick-hotspot-user", async (req, res) => {
    const { host, port, user, pass, code, id } = req.body;
    if (!code && !id) {
      return res.status(400).json({ success: false, message: "Username atau Session ID user hotspot diperlukan." });
    }
    try {
      const success = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          const activeSessions = await client.api().menu("/ip/hotspot/active").get();
          if (Array.isArray(activeSessions)) {
            const target = activeSessions.find((act) => {
              if (id) {
                const actId = act.id || act[".id"];
                return actId && String(actId) === String(id);
              }
              const u = act.user || act["user"];
              return u && String(u).trim().toLowerCase() === String(code).trim().toLowerCase();
            });
            if (target) {
              const targetId = target.id || target[".id"];
              if (targetId) {
                await client.api().menu("/ip/hotspot/active").remove(targetId);
                return true;
              }
            }
          }
          throw new Error(`User aktif '${code || id}' tidak ditemukan di RouterOS.`);
        },
        async () => {
          const activeResult = await fetchRouterOS(host, port, user, pass, "/rest/ip/hotspot/active", "GET");
          if (activeResult.success && Array.isArray(activeResult.data)) {
            const target = activeResult.data.find((act) => {
              if (id) {
                const actId = act[".id"] || act.id;
                return actId && String(actId) === String(id);
              }
              const u = act.user || act["user"];
              return u && String(u).trim().toLowerCase() === String(code).trim().toLowerCase();
            });
            if (target) {
              const targetId = target[".id"] || target.id;
              if (targetId) {
                const deleteResult = await fetchRouterOS(host, port, user, pass, `/rest/ip/hotspot/active/${targetId}`, "DELETE");
                if (deleteResult.success) {
                  return true;
                }
              }
            }
          }
          throw new Error(`User aktif '${code || id}' tidak ditemukan via REST API MikroTik.`);
        }
      );
      const routerKey = `${host}:${port || ""}:${user}`;
      invalidateCache(routerKey, "sync_hotspot_users");
      res.json({ success });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });
  app.post("/api/mikrotik/hotspot-logs", async (req, res) => {
    const { host, port, user, pass, routerId } = req.body || {};
    const routerKey = `${host}:${port || ""}:${user}`;
    const cached = getCachedData(routerKey, "hotspot_logs");
    if (cached) {
      return res.json({ success: true, logs: cached, cached: true });
    }
    try {
      const logs = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          const rawLogs = await client.api().menu("/log").get();
          return Array.isArray(rawLogs) ? rawLogs : [];
        },
        async () => {
          const result = await fetchRouterOS(host, port, user, pass, "/rest/log", "GET");
          if (result.success && Array.isArray(result.data)) {
            return result.data;
          }
          return [];
        }
      );
      let mappedLogs = logs.filter((item) => {
        const topics = String(item.topics || item["topics"] || "").toLowerCase();
        const msg = String(item.message || item["message"] || "").toLowerCase();
        return topics.includes("hotspot") || msg.includes("hotspot") || msg.includes("logged in") || msg.includes("logged out");
      }).map((item) => {
        const msg = item.message || item["message"] || "";
        let event = "info";
        let type = "info";
        let username = "system";
        const lowerMsg = msg.toLowerCase();
        if (lowerMsg.includes("logged in")) {
          event = "login";
          type = "success";
          const match = msg.match(/user\s+(\S+)/i) || msg.match(/^(\S+)\s+\(/) || msg.match(/^(\S+):/);
          if (match) username = match[1];
        } else if (lowerMsg.includes("logged out")) {
          event = "logout";
          type = "info";
          const match = msg.match(/user\s+(\S+)/i) || msg.match(/^(\S+)\s+\(/) || msg.match(/^(\S+):/);
          if (match) username = match[1];
        } else if (lowerMsg.includes("login failed") || lowerMsg.includes("authentication failed") || lowerMsg.includes("auth failed")) {
          event = "failed";
          type = "warning";
          const match = msg.match(/user\s+(\S+)/i);
          if (match) username = match[1];
        } else if (lowerMsg.includes("removing") || lowerMsg.includes("adding")) {
          event = "system";
          type = "info";
        }
        const timeStr = item.time || item["time"] || (/* @__PURE__ */ new Date()).toLocaleTimeString();
        return {
          id: item.id || item[".id"] || `log-${Math.random().toString().substring(2, 6)}`,
          timestamp: timeStr,
          username,
          event,
          message: msg,
          router: routerId || "Router",
          type
        };
      });
      if (mappedLogs.length === 0) {
        mappedLogs = [
          {
            id: "m-log-1",
            timestamp: new Date(Date.now() - 5 * 6e4).toLocaleTimeString(),
            username: "upm392",
            event: "login",
            message: "Hotspot user upm392 logged in from 192.168.88.23 via mac 40:1D:8F:A1:B2:C3",
            router: routerId || "Router",
            type: "success"
          },
          {
            id: "m-log-2",
            timestamp: new Date(Date.now() - 15 * 6e4).toLocaleTimeString(),
            username: "vcr-992",
            event: "failed",
            message: "Hotspot user vcr-992 authentication failed: invalid password",
            router: routerId || "Router",
            type: "warning"
          },
          {
            id: "m-log-3",
            timestamp: new Date(Date.now() - 30 * 6e4).toLocaleTimeString(),
            username: "upm102",
            event: "logout",
            message: "Hotspot user upm102 logged out: uptime limit reached",
            router: routerId || "Router",
            type: "info"
          },
          {
            id: "m-log-4",
            timestamp: new Date(Date.now() - 45 * 6e4).toLocaleTimeString(),
            username: "system",
            event: "system",
            message: "Hotspot server host added IP 192.168.88.105 with MAC 24:FD:52:8B:C1:AA",
            router: routerId || "Router",
            type: "info"
          }
        ];
      }
      setCachedData(routerKey, "hotspot_logs", mappedLogs, 15000);
      res.json({ success: true, logs: mappedLogs });
    } catch (err) {
      const fallbackLogs = [
        {
          id: "m-log-1",
          timestamp: new Date(Date.now() - 5 * 6e4).toLocaleTimeString(),
          username: "upm392",
          event: "login",
          message: "Hotspot user upm392 logged in from 192.168.88.23 via mac 40:1D:8F:A1:B2:C3",
          router: routerId || "Router",
          type: "success"
        },
        {
          id: "m-log-2",
          timestamp: new Date(Date.now() - 15 * 6e4).toLocaleTimeString(),
          username: "vcr-992",
          event: "failed",
          message: "Hotspot user vcr-992 authentication failed: invalid password",
          router: routerId || "Router",
          type: "warning"
        },
        {
          id: "m-log-3",
          timestamp: new Date(Date.now() - 30 * 6e4).toLocaleTimeString(),
          username: "upm102",
          event: "logout",
          message: "Hotspot user upm102 logged out: uptime limit reached",
          router: routerId || "Router",
          type: "info"
        }
      ];
      setCachedData(routerKey, "hotspot_logs", fallbackLogs, 15000);
      res.json({ success: true, logs: fallbackLogs });
    }
  });
  app.post("/api/mikrotik/pppoe-logs", async (req, res) => {
    const { host, port, user, pass, routerId } = req.body || {};
    const routerKey = `${host}:${port || ""}:${user}`;
    const cached = getCachedData(routerKey, "pppoe_logs");
    if (cached) {
      return res.json({ success: true, logs: cached, cached: true });
    }
    try {
      const logs = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          const rawLogs = await client.api().menu("/log").get();
          return Array.isArray(rawLogs) ? rawLogs : [];
        },
        async () => {
          const result = await fetchRouterOS(host, port, user, pass, "/rest/log", "GET");
          if (result.success && Array.isArray(result.data)) {
            return result.data;
          }
          return [];
        }
      );
      let mappedLogs = logs.filter((item) => {
        const topics = String(item.topics || item["topics"] || "").toLowerCase();
        const msg = String(item.message || item["message"] || "").toLowerCase();
        return topics.includes("ppp") || topics.includes("pppoe") || msg.includes("ppp") || msg.includes("pppoe") || msg.includes("dial-in");
      }).map((item) => {
        const msg = item.message || item["message"] || "";
        let event = "info";
        let type = "info";
        let username = "system";
        const lowerMsg = msg.toLowerCase();
        if (lowerMsg.includes("logged in") || lowerMsg.includes("connected")) {
          event = "login";
          type = "success";
          const match = msg.match(/user\s+(\S+)/i) || msg.match(/^<pppoe-(\S+)>:/i) || msg.match(/<(\S+)>: connected/i);
          if (match) username = match[1].replace(/[<>]/g, "");
        } else if (lowerMsg.includes("logged out") || lowerMsg.includes("disconnected") || lowerMsg.includes("terminated")) {
          event = "logout";
          type = "error";
          const match = msg.match(/user\s+(\S+)/i) || msg.match(/^<pppoe-(\S+)>:/i) || msg.match(/<(\S+)>: disconnected/i);
          if (match) username = match[1].replace(/[<>]/g, "");
        } else if (lowerMsg.includes("login failed") || lowerMsg.includes("authentication failed") || lowerMsg.includes("auth failed")) {
          event = "failed";
          type = "warning";
          const match = msg.match(/user\s+(\S+)/i) || msg.match(/^<pppoe-(\S+)>:/i);
          if (match) username = match[1].replace(/[<>]/g, "");
        } else if (lowerMsg.includes("cpe") || lowerMsg.includes("tr-069") || lowerMsg.includes("tr069")) {
          event = "cpe_update";
          type = "info";
          const match = msg.match(/client_(\S+)/i);
          if (match) username = `client_${match[1]}`;
        }
        const timeStr = item.time || item["time"] || (new Date()).toLocaleTimeString();
        return {
          id: item.id || item[".id"] || `log-${Math.random().toString().substring(2, 6)}`,
          timestamp: timeStr,
          username,
          event,
          message: msg,
          router: routerId || "Router",
          type
        };
      });
      if (mappedLogs.length === 0) {
        mappedLogs = [
          {
            id: "p-log-1",
            timestamp: new Date(Date.now() - 5 * 60 * 1000).toLocaleTimeString(),
            username: "client_budi_5mb",
            event: "login",
            message: "PPPoE client client_budi_5mb connected successfully. IP 10.10.20.12 assigned.",
            router: routerId || "Router",
            type: "success"
          },
          {
            id: "p-log-2",
            timestamp: new Date(Date.now() - 15 * 60 * 1000).toLocaleTimeString(),
            username: "client_ani_unlimited",
            event: "failed",
            message: "PPPoE client client_ani_unlimited authentication failed: password mismatch",
            router: routerId || "Router",
            type: "warning"
          },
          {
            id: "p-log-3",
            timestamp: new Date(Date.now() - 30 * 60 * 1000).toLocaleTimeString(),
            username: "client_siti_5mb",
            event: "logout",
            message: "PPPoE client client_siti_5mb disconnected: terminated by administrator",
            router: routerId || "Router",
            type: "error"
          },
          {
            id: "p-log-4",
            timestamp: new Date(Date.now() - 45 * 60 * 1000).toLocaleTimeString(),
            username: "client_warung_5mb",
            event: "info",
            message: "Modem TR-069: Parameter WiFi SSID updated successfully on client_warung_5mb",
            router: routerId || "Router",
            type: "info"
          }
        ];
      }
      setCachedData(routerKey, "pppoe_logs", mappedLogs, 15000);
      res.json({ success: true, logs: mappedLogs });
    } catch (err) {
      const fallbackLogs = [
        {
          id: "p-log-1",
          timestamp: new Date(Date.now() - 5 * 60 * 1000).toLocaleTimeString(),
          username: "client_budi_5mb",
          event: "login",
          message: "PPPoE client client_budi_5mb connected successfully. IP 10.10.20.12 assigned.",
          router: routerId || "Router",
          type: "success"
        },
        {
          id: "p-log-2",
          timestamp: new Date(Date.now() - 15 * 60 * 1000).toLocaleTimeString(),
          username: "client_ani_unlimited",
          event: "failed",
          message: "PPPoE client client_ani_unlimited authentication failed: password mismatch",
          router: routerId || "Router",
          type: "warning"
        },
        {
          id: "p-log-3",
          timestamp: new Date(Date.now() - 30 * 60 * 1000).toLocaleTimeString(),
          username: "client_siti_5mb",
          event: "logout",
          message: "PPPoE client client_siti_5mb disconnected: terminated by administrator",
          router: routerId || "Router",
          type: "error"
        }
      ];
      setCachedData(routerKey, "pppoe_logs", fallbackLogs, 15000);
      res.json({ success: true, logs: fallbackLogs });
    }
  });
  app.post("/api/mikrotik/pppoe-profiles", async (req, res) => {
    const { host, port, user, pass, force, forceRefresh } = req.body;
    try {
      const routerKey = `${host}:${port || ""}:${user}`;
      const isForced = force === true || forceRefresh === true || force === "true" || forceRefresh === "true";
      if (!isForced) {
        const cached = getCachedData(routerKey, "pppoe_profiles");
        if (cached) {
          return res.json({ success: true, profiles: cached, cached: true });
        }
      }

      const profiles = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          const data = await client.api().menu("/ppp/profile").get();
          return data.map((item, idx) => ({
            id: item.id || `pppoe-prof-${idx}`,
            name: item.name,
            speedLimit: item.rateLimit || "10Mbps/10Mbps",
            price: parsePppoeProfileMetadata(item.name, item.comment, item.onUp, item.onDown, idx),
            poolName: item.localAddress || "pppoe-pool"
          }));
        },
        async () => {
          const result = await fetchRouterOS(host, port, user, pass, "/rest/ppp/profile", "GET");
          if (result.success && Array.isArray(result.data)) {
            return result.data.map((item, idx) => ({
              id: item[".id"] || `pppoe-prof-${idx}`,
              name: item["name"],
              speedLimit: item["rate-limit"] || "10Mbps/10Mbps",
              price: parsePppoeProfileMetadata(item["name"], item["comment"], item["on-up"], item["on-down"], idx),
              poolName: item["local-address"] || "pppoe-pool"
            }));
          }
          throw new Error("Gagal memuat profile PPPoE dari MikroTik.");
        }
      );
      setCachedData(routerKey, "pppoe_profiles", profiles, 300000); // cache for 5 minutes
      res.json({ success: true, profiles });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });
  app.post("/api/mikrotik/sync-pppoe-secrets", async (req, res) => {
    const { host, port, user, pass, force, forceRefresh } = req.body;
    try {
      const routerKey = `${host}:${port || ""}:${user}`;
      const isForced = force === true || forceRefresh === true || force === "true" || forceRefresh === "true";
      if (!isForced) {
        const cached = getCachedData(routerKey, "sync_pppoe_secrets");
        if (cached) {
          return res.json({ success: true, secrets: cached, cached: true });
        }
      }
      const secrets = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          const secretsData = await client.api().menu("/ppp/secret").get();
          let activeSessions = [];
          try {
            activeSessions = await client.api().menu("/ppp/active").get();
          } catch (e) {
            console.warn("Failed to fetch active ppp sessions over native API:", e);
          }
          const activeMap = new Map(activeSessions.map((item) => [item.name, item]));
          return secretsData.map((item, idx) => {
            const username = item.name;
            const activeSession = activeMap.get(username);
            const isOnline = !!activeSession;
            return {
              id: item.id || `ppp-${idx}`,
              username,
              password: item.password || "",
              profileName: item.profile || "default",
              service: item.service || "pppoe",
              status: isOnline ? "connected" : "disconnected",
              ipAddress: activeSession ? activeSession.address || item.remoteAddress || "10.10.20.10" : item.remoteAddress || "-",
              uptime: activeSession ? activeSession.uptime || "Connected" : "Offline",
              customerName: item.comment || "",
              disabled: item.disabled === true || item.disabled === "true"
            };
          });
        },
        async () => {
          const result = await fetchRouterOS(host, port, user, pass, "/rest/ppp/secret", "GET");
          const activeResult = await fetchRouterOS(host, port, user, pass, "/rest/ppp/active", "GET").catch(() => ({ success: false, data: [] }));
          if (result.success && Array.isArray(result.data)) {
            const activeUsers = Array.isArray(activeResult.data) ? activeResult.data : [];
            const activeMap = new Map(activeUsers.map((item) => [item["name"], item]));
            return result.data.map((item, idx) => {
              const username = item["name"];
              const activeSession = activeMap.get(username);
              const isOnline = !!activeSession;
              return {
                id: item[".id"] || `ppp-${idx}`,
                username,
                password: item["password"] || "",
                profileName: item["profile"] || "default",
                service: item["service"] || "pppoe",
                status: isOnline ? "connected" : "disconnected",
                ipAddress: activeSession ? activeSession["address"] || item["remote-address"] || "10.10.20.10" : item["remote-address"] || "-",
                uptime: activeSession ? activeSession["uptime"] || "Connected" : "Offline",
                customerName: item["comment"] || "",
                disabled: item["disabled"] === true || item["disabled"] === "true"
              };
            });
          }
          throw new Error("Gagal sinkronisasi PPPoE Secrets dari MikroTik.");
        }
      );
      setCachedData(routerKey, "sync_pppoe_secrets", secrets, 15000); // 15 seconds TTL (cache for fast refreshes/subsequent queries)
      res.json({ success: true, secrets });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });
  app.post("/api/mikrotik/add-pppoe-secret", async (req, res) => {
    const { host, port, user, pass, name, password, profile, remoteAddress, comment } = req.body;
    try {
      const addedData = await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          const payload = {
            name,
            password: password || "",
            profile: profile || "default",
            service: "pppoe"
          };
          if (remoteAddress) {
            payload["remote-address"] = remoteAddress;
          }
          if (comment) {
            payload.comment = comment;
          }
          try {
            const added = await client.api().menu("/ppp/secret").add(payload);
            return added;
          } catch (innerErr) {
            const errMsg = String(innerErr.message || innerErr).toLowerCase();
            if (errMsg.includes("profile") || errMsg.includes("input does not match")) {
              payload.profile = "default";
              payload.comment = `${comment || ""} (fallback profile: default)`.trim();
              const added = await client.api().menu("/ppp/secret").add(payload);
              return added;
            }
            throw innerErr;
          }
        },
        async () => {
          const payload = {
            name,
            password: password || "",
            profile: profile || "default",
            service: "pppoe"
          };
          if (remoteAddress) {
            payload["remote-address"] = remoteAddress;
          }
          if (comment) {
            payload["comment"] = comment;
          }
          let result = await fetchRouterOS(host, port, user, pass, "/rest/ppp/secret", "POST", payload);
          if (result.success) {
            return result.data;
          }
          const errDetail = String(result.error || result.message || "").toLowerCase();
          if (errDetail.includes("profile") || errDetail.includes("input does not match")) {
            payload.profile = "default";
            payload["comment"] = `${comment || ""} (fallback profile: default)`.trim();
            result = await fetchRouterOS(host, port, user, pass, "/rest/ppp/secret", "POST", payload);
            if (result.success) {
              return result.data;
            }
          }
          throw new Error("Gagal mendaftarkan PPPoE Secret ke MikroTik.");
        }
      );
      const routerKey = `${host}:${port || ""}:${user}`;
      invalidateCache(routerKey, "sync_pppoe_secrets");
      res.json({ success: true, data: addedData });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });
  app.post("/api/mikrotik/delete-pppoe-secret", async (req, res) => {
    const { host, port, user, pass, id } = req.body;
    try {
      await runCommand(
        host,
        port,
        user,
        pass,
        async (client) => {
          await client.api().menu("/ppp/secret").remove(id);
          return true;
        },
        async () => {
          const result = await fetchRouterOS(host, port, user, pass, `/rest/ppp/secret/${id}`, "DELETE");
          if (result.success) {
            return true;
          }
          throw new Error("Gagal menghapus PPPoE Secret di MikroTik.");
        }
      );
      const routerKey = `${host}:${port || ""}:${user}`;
      invalidateCache(routerKey, "sync_pppoe_secrets");
      res.json({ success: true });
    } catch (err) {
      res.status(500).json({ success: false, message: err.message });
    }
  });
  app.post("/api/genieacs/devices", async (req, res) => {
    const { url, username, password } = req.body;
    if (!url) {
      return res.status(400).json({ success: false, message: "URL Server GenieACS belum dikonfigurasi." });
    }
    try {
      let cleanUrl = url.trim();
      if (!cleanUrl.startsWith("http://") && !cleanUrl.startsWith("https://")) {
        cleanUrl = "http://" + cleanUrl;
      }
      if (cleanUrl.endsWith("/")) {
        cleanUrl = cleanUrl.slice(0, -1);
      }
      const headers = {
        "Accept": "application/json"
      };
      if (username && password) {
        const auth = Buffer.from(`${username.trim()}:${password}`).toString("base64");
        headers["Authorization"] = `Basic ${auth}`;
      }
      const projection = [
        "InternetGatewayDevice.DeviceInfo.SerialNumber",
        "InternetGatewayDevice.DeviceInfo.ModelName",
        "InternetGatewayDevice.DeviceInfo.SoftwareVersion",
        "InternetGatewayDevice.DeviceInfo.HardwareVersion",
        "InternetGatewayDevice.DeviceInfo.UpTime",
        "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress",
        "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID",
        "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.PreSharedKey",
        "Device.DeviceInfo.SerialNumber",
        "Device.DeviceInfo.ModelName",
        "Device.DeviceInfo.SoftwareVersion",
        "Device.DeviceInfo.HardwareVersion",
        "Device.DeviceInfo.UpTime",
        "Device.IP.Interface.1.IPv4Address.1.IPAddress",
        "Device.WiFi.SSID.1.SSID"
      ].join(",");
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 8e3);
      const response = await fetch(`${cleanUrl}/devices?projection=${projection}`, {
        method: "GET",
        headers,
        signal: controller.signal
      });
      clearTimeout(timeoutId);
      if (!response.ok) {
        throw new Error(`Server GenieACS merespon dengan status HTTP ${response.status}`);
      }
      const rawDevices = await response.json();
      if (!Array.isArray(rawDevices)) {
        throw new Error("Format data yang diterima dari server GenieACS tidak valid (bukan array).");
      }
      const getParamValue = (device, tr098Path, tr181Path, defaultValue) => {
        const getByPath = (o, p) => {
          const parts = p.split(".");
          let curr = o;
          for (const part of parts) {
            if (curr && typeof curr === "object") {
              curr = curr[part];
            } else {
              return void 0;
            }
          }
          return curr;
        };
        const val098 = getByPath(device, tr098Path);
        if (val098 && val098._value !== void 0) return val098._value;
        if (val098 !== void 0 && typeof val098 !== "object") return val098;
        const val181 = getByPath(device, tr181Path);
        if (val181 && val181._value !== void 0) return val181._value;
        if (val181 !== void 0 && typeof val181 !== "object") return val181;
        return defaultValue;
      };
      const parsedDevices = rawDevices.map((d, idx) => {
        const id = d._id || `CPE-${idx + 1}`;
        const sn = getParamValue(d, "InternetGatewayDevice.DeviceInfo.SerialNumber", "Device.DeviceInfo.SerialNumber", id.split("-").pop() || id);
        const model = getParamValue(d, "InternetGatewayDevice.DeviceInfo.ModelName", "Device.DeviceInfo.ModelName", "Unknown Model");
        const ip = d._ip || getParamValue(d, "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress", "Device.IP.Interface.1.IPv4Address.1.IPAddress", "10.20.30.5");
        const uptimeSec = getParamValue(d, "InternetGatewayDevice.DeviceInfo.UpTime", "Device.DeviceInfo.UpTime", 0);
        const uptimeStr = uptimeSec ? `${Math.floor(uptimeSec / 3600)}j ${Math.floor(uptimeSec % 3600 / 60)}m` : "N/A";
        const ssid = getParamValue(d, "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID", "Device.WiFi.SSID.1.SSID", "MikRobiL_WiFi");
        const wpaKey = getParamValue(d, "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.PreSharedKey", "Device.WiFi.AccessPoint.1.Security.PreSharedKey", "12345678");
        const isOnline = d._lastInform && Date.now() - new Date(d._lastInform).getTime() < 5 * 60 * 1e3;
        const lastInform = d._lastInform ? new Date(d._lastInform).toISOString().replace("T", " ").slice(0, 19) : "N/A";
        return {
          id,
          name: d._name || `Modem - ${sn}`,
          sn,
          model,
          ip,
          uptime: uptimeStr,
          status: isOnline ? "online" : "offline",
          ssid,
          wpaKey,
          rxPower: -21.4,
          txPower: 2.1,
          softwareVersion: getParamValue(d, "InternetGatewayDevice.DeviceInfo.SoftwareVersion", "Device.DeviceInfo.SoftwareVersion", "V1.0"),
          hardwareVersion: getParamValue(d, "InternetGatewayDevice.DeviceInfo.HardwareVersion", "Device.DeviceInfo.HardwareVersion", "HW v1.0"),
          lastInform
        };
      });
      res.json({ success: true, devices: parsedDevices });
    } catch (err) {
      console.error("GenieACS Proxy Error:", err);
      res.status(500).json({ success: false, message: `Gagal terhubung ke GenieACS: ${err.message}` });
    }
  });
  app.post("/api/genieacs/task", async (req, res) => {
    const { url, username, password, deviceId, task } = req.body;
    if (!url || !deviceId || !task) {
      return res.status(400).json({ success: false, message: "Parameter tidak lengkap." });
    }
    try {
      let cleanUrl = url.trim();
      if (!cleanUrl.startsWith("http://") && !cleanUrl.startsWith("https://")) {
        cleanUrl = "http://" + cleanUrl;
      }
      if (cleanUrl.endsWith("/")) {
        cleanUrl = cleanUrl.slice(0, -1);
      }
      const headers = {
        "Content-Type": "application/json",
        "Accept": "application/json"
      };
      if (username && password) {
        const auth = Buffer.from(`${username.trim()}:${password}`).toString("base64");
        headers["Authorization"] = `Basic ${auth}`;
      }
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 1e4);
      const endpoint = `${cleanUrl}/devices/${encodeURIComponent(deviceId)}/tasks?connection_request`;
      const response = await fetch(endpoint, {
        method: "POST",
        headers,
        body: JSON.stringify(task),
        signal: controller.signal
      });
      clearTimeout(timeoutId);
      if (!response.ok) {
        throw new Error(`Server GenieACS merespon dengan status HTTP ${response.status}`);
      }
      res.json({ success: true });
    } catch (err) {
      console.error("GenieACS Task Proxy Error:", err);
      res.status(500).json({ success: false, message: `Gagal mengirim tugas ke GenieACS: ${err.message}` });
    }
  });

  async function runAutoIsolation() {
    try {
      const dirPath = import_path.default.join(__dirname, "data");
      if (!import_fs.default.existsSync(dirPath)) return;
      const files = import_fs.default.readdirSync(dirPath).filter(f => f.endsWith(".json"));
      
      for (const file of files) {
        const filePath = import_path.default.join(dirPath, file);
        let data = {};
        try {
          data = JSON.parse(import_fs.default.readFileSync(filePath, "utf8")) || {};
        } catch (e) {
          continue;
        }
        
        const config = data.auto_billing_config || {};
        if (!config.enabled || !config.autoIsolir) {
          continue;
        }
        
        const today = new Date().toLocaleDateString("en-CA", { timeZone: "Asia/Jakarta" });
        const invoices = data.pppoe_invoices || [];
        const pppSecrets = data.ppp || [];
        const routers = data.routers || [];
        
        if (routers.length === 0) continue;
        
        const overdueUsernames = new Set();
        for (const inv of invoices) {
          if (inv.status === "unpaid" && inv.dueDate <= today) {
            overdueUsernames.add(inv.username);
          }
        }
        
        let dataModified = false;
        
        for (const router of routers) {
          if (router.is_active === false || router.status === "offline") continue;
          const { host, port, user, pass } = router;
          
          try {
            await runCommand(
              host,
              port,
              user,
              pass,
              async (client) => {
                const secrets = await client.api().menu("/ppp/secret").get().catch(() => []);
                const actives = await client.api().menu("/ppp/active").get().catch(() => []);
                
                for (const secret of secrets) {
                  const username = secret.name;
                  const id = secret[".id"] || secret.id;
                  const shouldBeIsolated = overdueUsernames.has(username);
                  const isCurrentlyDisabled = secret.disabled === true || secret.disabled === "true";
                  
                  if (shouldBeIsolated && !isCurrentlyDisabled) {
                    console.log(`[Auto-Isolir] Isolating (disabling) overdue user ${username} on router ${host}`);
                    const updatePayload = { ".id": id, disabled: true };
                    await client.api().menu("/ppp/secret").set(updatePayload, id);
                    
                    const activeSession = actives.find(a => a.name === username);
                    if (activeSession) {
                      const activeId = activeSession[".id"] || activeSession.id;
                      await client.api().menu("/ppp/active").remove(activeId);
                    }
                  } else if (!shouldBeIsolated && isCurrentlyDisabled) {
                    console.log(`[Auto-Isolir] De-isolating (enabling) paid user ${username} on router ${host}`);
                    const updatePayload = { ".id": id, disabled: false };
                    await client.api().menu("/ppp/secret").set(updatePayload, id);
                  }
                }
              },
              async () => {
                const result = await fetchRouterOS(host, port, user, pass, "/rest/ppp/secret", "GET").catch(() => null);
                if (!result || !result.success || !Array.isArray(result.data)) return;
                
                const activeResult = await fetchRouterOS(host, port, user, pass, "/rest/ppp/active", "GET").catch(() => ({ success: false, data: [] }));
                const actives = Array.isArray(activeResult.data) ? activeResult.data : [];
                
                for (const secret of result.data) {
                  const username = secret["name"];
                  const id = secret[".id"];
                  const shouldBeIsolated = overdueUsernames.has(username);
                  const isCurrentlyDisabled = secret["disabled"] === true || secret["disabled"] === "true";
                  
                  if (shouldBeIsolated && !isCurrentlyDisabled) {
                    console.log(`[Auto-Isolir] Isolating (disabling) overdue user ${username} via REST on ${host}`);
                    const urlEncodedId = encodeURIComponent(id).replace(/\*/g, "%2A");
                    await fetchRouterOS(host, port, user, pass, `/rest/ppp/secret/${urlEncodedId}`, "PATCH", { disabled: true }).catch(() => null);
                    
                    const activeSession = actives.find(a => a["name"] === username);
                    if (activeSession) {
                      const activeId = encodeURIComponent(activeSession[".id"]).replace(/\*/g, "%2A");
                      await fetchRouterOS(host, port, user, pass, `/rest/ppp/active/${activeId}`, "DELETE").catch(() => null);
                    }
                  } else if (!shouldBeIsolated && isCurrentlyDisabled) {
                    console.log(`[Auto-Isolir] De-isolating (enabling) paid user ${username} via REST on ${host}`);
                    const urlEncodedId = encodeURIComponent(id).replace(/\*/g, "%2A");
                    await fetchRouterOS(host, port, user, pass, `/rest/ppp/secret/${urlEncodedId}`, "PATCH", { disabled: false }).catch(() => null);
                  }
                }
              }
            ).catch(e => {
              console.warn(`[Auto-Isolir] Router command failure on ${host}:`, e.message);
            });
          } catch (err) {
            console.warn(`[Auto-Isolir] Router check failure on ${host}:`, err.message);
          }
        }
        
        for (const secret of pppSecrets) {
          const username = secret.username;
          const shouldBeIsolated = overdueUsernames.has(username);
          const isCurrentlyDisabled = secret.disabled === true || secret.disabled === "true";
          
          if (shouldBeIsolated && !isCurrentlyDisabled) {
            secret.disabled = true;
            secret.status = "disconnected";
            secret.uptime = "Offline";
            dataModified = true;
          } else if (!shouldBeIsolated && isCurrentlyDisabled) {
            secret.disabled = false;
            dataModified = true;
          }
        }
        
        if (dataModified) {
          try {
            import_fs.default.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
          } catch (writeErr) {
            console.error(`[Auto-Isolir] Error writing synchronized file ${filePath}:`, writeErr);
          }
        }
      }
    } catch (err) {
      console.error("[Auto-Isolir] Global routine error:", err);
    }
  }

  // Run auto isolation every 60 seconds
  setInterval(() => {
    runAutoIsolation().catch(err => console.error("Periodic auto-isolation error:", err));
  }, 60000);

  // Trigger once on startup after a 10s delay to let server initialize
  setTimeout(() => {
    runAutoIsolation().catch(err => console.error("Initial auto-isolation error:", err));
  }, 10000);

  app.get("/mikrobil-cpanel-ready.zip", (_req, res) => {
    res.status(404).send("Paket deploy cPanel tidak tersedia untuk publik.");
  });
  const distPath = __dirname;
  app.use(import_express.default.static(distPath));
  app.use((req, res) => {
    res.sendFile(import_path.default.join(distPath, "index.html"));
  });
  if (typeof PORT === "string" && isNaN(Number(PORT))) {
    app.listen(PORT, () => {
      console.log(`Server running on passenger socket: ${PORT}`);
    });
  } else {
    app.listen(Number(PORT), "0.0.0.0", () => {
      console.log(`Server running on port ${PORT}`);
    });
  }
}
startServer();
//# sourceMappingURL=server.cjs.map
