import { exec } from "node:child_process";
import { promisify } from "node:util";
import { platform } from "node:os";
import { networkInterfaces } from "node:os";
import { createConnection } from "node:net";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { existsSync } from "node:fs";
import { getLatestNetworkScan, setLatestNetworkScan } from "../tools/network-tools.js";
import { createRequire } from "node:module";
import { scanNetwork } from "../lib/network-scan.js";
import { TerminalSurface } from "../surfaces/terminal.js";
const require = createRequire(import.meta.url);
const { version: PKG_VERSION } = require("../../package.json");
const __dirname = dirname(fileURLToPath(import.meta.url));
const execAsync = promisify(exec);
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** TCP-connect probe to check if a single port is open. */
function probePort(ip, port, timeoutMs = 2000) {
    return new Promise((resolve) => {
        const socket = createConnection({ host: ip, port, timeout: timeoutMs });
        socket.once("connect", () => { socket.destroy(); resolve(true); });
        socket.once("timeout", () => { socket.destroy(); resolve(false); });
        socket.once("error", () => { socket.destroy(); resolve(false); });
    });
}
export function shellQuote(value) {
    return `'${value.replace(/'/g, `'\"'\"'`)}'`;
}
export function buildSshAuthArgs(authMethod, password) {
    if (authMethod === "password" && password) {
        return [
            "-o", "BatchMode=no",
            "-o", "PreferredAuthentications=password,keyboard-interactive",
            "-o", "PubkeyAuthentication=no",
            "-o", "NumberOfPasswordPrompts=1",
        ];
    }
    return ["-o", "BatchMode=yes"];
}
export function buildInteractiveDeployCommand(ip, username, version = PKG_VERSION) {
    const tsEntry = resolve(__dirname, "../index.ts");
    const jsEntry = resolve(__dirname, "../index.js");
    const entry = existsSync(tsEntry) ? tsEntry : jsEntry;
    return [
        "bash <<'JAIT_DEPLOY'",
        "set -euo pipefail",
        `IP=${shellQuote(ip)}`,
        `USERNAME=${shellQuote(username)}`,
        `VERSION=${shellQuote(version)}`,
        `ENTRY=${shellQuote(entry)}`,
        "",
        "echo \"Connecting to ${USERNAME}@${IP}...\"",
        "raw=\"$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 \"${USERNAME}@${IP}\" \"uname -m\" | tr -d '\\r' | tail -n 1)\"",
        "case \"$raw\" in",
        "  x86_64|amd64) arch='x64' ;;",
        "  aarch64|arm64) arch='arm64' ;;",
        "  *) echo \"Unsupported architecture: $raw\"; exit 1 ;;",
        "esac",
        "echo \"Detected linux-${arch}\"",
        "",
        "echo '[1/3] Compiling gateway binary...'",
        "cacheDir=\"${TMPDIR:-/tmp}/jait-deploy\"",
        "mkdir -p \"$cacheDir\"",
        "outFile=\"$cacheDir/jait-gateway-${VERSION}-linux-${arch}\"",
        "if [ -f \"$outFile\" ]; then",
        "  echo \"Using cached binary (v${VERSION})\"",
        "else",
        "  target=\"bun-linux-${arch}\"",
        "  echo \"bun build --compile --target=${target} --minify\"",
        "  bun build --compile \"--target=${target}\" --minify \"$ENTRY\" --outfile \"$outFile\"",
        "  echo 'Binary compiled'",
        "fi",
        "",
        "size_mb=\"$(du -m \"$outFile\" | cut -f1)\"",
        "echo \"[2/3] Transferring binary (${size_mb} MB)...\"",
        "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 \"${USERNAME}@${IP}\" 'mkdir -p ~/.jait'",
        "scp -o StrictHostKeyChecking=no -o ConnectTimeout=10 \"$outFile\" \"${USERNAME}@${IP}:~/.jait/jait-gateway\"",
        "echo 'Binary transferred'",
        "",
        "echo '[3/3] Configuring and starting...'",
        `ssh -tt -o StrictHostKeyChecking=no -o ConnectTimeout=10 "${username}@${ip}" 'bash -s' <<'REMOTE_SETUP'`,
        "set -e",
        "chmod +x ~/.jait/jait-gateway",
        "SUDO=''",
        "if [ \"$(id -u)\" -ne 0 ] && command -v sudo >/dev/null 2>&1; then",
        "  SUDO='sudo'",
        "fi",
        "",
        "[ -f ~/.jait/.env ] || cat > ~/.jait/.env <<'ENVEOF'",
        "PORT=8000",
        "HOST=0.0.0.0",
        "LOG_LEVEL=info",
        "CORS_ORIGIN=*",
        "ENVEOF",
        "",
        "${SUDO:+$SUDO }tee /etc/systemd/system/jait-gateway.service > /dev/null <<'SVCEOF'",
        "[Unit]",
        "Description=Jait Gateway",
        "After=network.target",
        "",
        "[Service]",
        `User=${username}`,
        "WorkingDirectory=%h/.jait",
        "ExecStart=%h/.jait/jait-gateway",
        "Restart=on-failure",
        "",
        "[Install]",
        "WantedBy=multi-user.target",
        "SVCEOF",
        "",
        "${SUDO:+$SUDO }systemctl daemon-reload",
        "${SUDO:+$SUDO }systemctl enable --now jait-gateway",
        "echo 'Jait Gateway deployed successfully!'",
        "REMOTE_SETUP",
        "",
        "echo \"Gateway v${VERSION} deployed to ${IP}\"",
        "JAIT_DEPLOY",
    ].join("\n");
}
// ---------------------------------------------------------------------------
// DB helpers — persist and read scanned hosts
// ---------------------------------------------------------------------------
function persistHostsToDb(sqlite, hosts, scannedAt) {
    const upsert = sqlite.prepare(`
    INSERT INTO network_hosts (ip, mac, hostname, os_version, open_ports, ssh_reachable, agent_status, providers, first_seen_at, last_seen_at, scanned_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    ON CONFLICT(ip) DO UPDATE SET
      mac = excluded.mac,
      hostname = excluded.hostname,
      os_version = COALESCE(excluded.os_version, network_hosts.os_version),
      open_ports = excluded.open_ports,
      ssh_reachable = excluded.ssh_reachable,
      agent_status = excluded.agent_status,
      providers = excluded.providers,
      last_seen_at = excluded.last_seen_at,
      scanned_at = excluded.scanned_at
  `);
    for (const h of hosts) {
        upsert.run(h.ip, h.mac ?? null, h.hostname ?? null, h.osVersion ?? null, JSON.stringify(h.openPorts), h.sshReachable ? 1 : 0, h.agentStatus, h.providers?.length ? JSON.stringify(h.providers) : null, scannedAt, h.lastSeen, scannedAt);
    }
}
function readHostsFromDb(sqlite) {
    const rows = sqlite.prepare("SELECT * FROM network_hosts ORDER BY last_seen_at DESC").all();
    let scannedAt = null;
    const hosts = rows.map((r) => {
        if (!scannedAt || r.scanned_at > scannedAt)
            scannedAt = r.scanned_at;
        return {
            ip: r.ip,
            mac: r.mac,
            hostname: r.hostname,
            vendor: null,
            isRouter: false,
            alive: true,
            openPorts: JSON.parse(r.open_ports),
            sshReachable: r.ssh_reachable === 1,
            agentStatus: r.agent_status,
            osVersion: r.os_version,
            providers: r.providers ? JSON.parse(r.providers) : undefined,
            lastSeen: r.last_seen_at,
        };
    });
    return { hosts, scannedAt };
}
// ---------------------------------------------------------------------------
// In-memory node registry (gateway mesh)
// ---------------------------------------------------------------------------
const knownNodes = new Map();
let cachedGateway = null;
const GATEWAY_CACHE_TTL = 5 * 60_000; // 5 minutes
async function buildGatewayNode(providerRegistry) {
    const ifaces = networkInterfaces();
    let gatewayIp = "127.0.0.1";
    for (const entries of Object.values(ifaces)) {
        if (!entries)
            continue;
        for (const entry of entries) {
            if (entry.family === "IPv4" && !entry.internal && !entry.address.startsWith("169.254.")) {
                gatewayIp = entry.address;
                break;
            }
        }
        if (gatewayIp !== "127.0.0.1")
            break;
    }
    let osVersion = null;
    try {
        if (platform() === "win32") {
            const { stdout } = await execAsync("cmd /c ver", { timeout: 3000 });
            osVersion = stdout.trim().replace(/^\s*\n+/, "") || null;
        }
        else if (platform() === "darwin") {
            const { stdout } = await execAsync("sw_vers -productVersion", { timeout: 3000 });
            osVersion = `macOS ${stdout.trim()}`;
        }
        else {
            const { stdout } = await execAsync("cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '\"'", { timeout: 3000 });
            osVersion = stdout.trim() || null;
        }
    }
    catch { }
    const gatewayProviders = [];
    if (providerRegistry) {
        for (const p of providerRegistry.list()) {
            try {
                await p.checkAvailability();
            }
            catch { }
            if (p.info.available)
                gatewayProviders.push(p.id);
        }
    }
    const name = platform() === "win32"
        ? process.env.COMPUTERNAME ?? "Gateway"
        : await execAsync("hostname").then(r => r.stdout.trim()).catch(() => "Gateway");
    return {
        id: "gateway",
        type: "gateway",
        name,
        platform: platform() === "win32" ? "windows" : platform() === "darwin" ? "macos" : "linux",
        ip: gatewayIp,
        version: PKG_VERSION,
        osVersion,
        providers: gatewayProviders,
        online: true,
    };
}
async function getGatewayNode(providerRegistry) {
    const now = Date.now();
    if (cachedGateway && now - cachedGateway.builtAt < GATEWAY_CACHE_TTL) {
        return cachedGateway.node;
    }
    const node = await buildGatewayNode(providerRegistry);
    cachedGateway = { node, builtAt: now };
    return node;
}
export function registerNetworkRoutes(app, ws, sqlite, providerRegistry, surfaceRegistry) {
    // ---- GET /api/network/interfaces — local NIC info ----
    app.get("/api/network/interfaces", async () => {
        const ifaces = networkInterfaces();
        const result = [];
        for (const [name, entries] of Object.entries(ifaces)) {
            if (!entries)
                continue;
            for (const entry of entries) {
                if (entry.family === "IPv4") {
                    result.push({ name, ip: entry.address, mac: entry.mac, netmask: entry.netmask, internal: entry.internal });
                }
            }
        }
        return { interfaces: result };
    });
    // ---- GET /api/network/scan/latest — return cached scan or DB data ----
    app.get("/api/network/scan/latest", async () => {
        const cached = getLatestNetworkScan();
        if (cached)
            return cached;
        // Fall back to DB if no in-memory cache yet
        if (sqlite) {
            const { hosts, scannedAt } = readHostsFromDb(sqlite);
            if (hosts.length > 0) {
                return { subnet: "", hosts, routerIp: null, scannedAt: scannedAt ?? new Date().toISOString(), durationMs: 0 };
            }
        }
        return { ok: false, message: "No scan results yet. Trigger a scan or wait for the scheduled job." };
    });
    // ---- POST /api/network/scan — ARP scan + port probe ----
    app.post("/api/network/scan", async (request) => {
        const body = (request.body ?? {});
        const targetSubnet = body["subnet"];
        const deep = body["deep"] === undefined ? true : Boolean(body["deep"]);
        const includeIps = Array.isArray(body["includeIps"])
            ? body["includeIps"].filter((value) => typeof value === "string")
            : [];
        try {
            const result = await scanNetwork({ subnet: targetSubnet, deep, includeIps });
            setLatestNetworkScan(result);
            if (sqlite) {
                try {
                    persistHostsToDb(sqlite, result.hosts, result.scannedAt);
                }
                catch (err) {
                    console.error("Failed to persist network hosts to DB:", err);
                }
            }
            return result;
        }
        catch (err) {
            return { error: err instanceof Error ? err.message : "Network scan failed" };
        }
    });
    // ---- POST /api/network/ssh/test — test SSH connectivity ----
    app.post("/api/network/ssh/test", async (request) => {
        const body = (request.body ?? {});
        const ip = String(body["ip"] ?? "").trim();
        const port = Number(body["port"] ?? 22);
        if (!ip)
            return { error: "IP address required" };
        const reachable = await probePort(ip, port, 3000);
        const result = {
            ip,
            reachable,
            authMethods: reachable ? ["password", "publickey"] : [],
        };
        if (reachable) {
            // Try to get platform info via SSH banner
            try {
                const banner = await new Promise((resolve) => {
                    const socket = createConnection({ host: ip, port, timeout: 3000 });
                    let data = "";
                    socket.on("data", (chunk) => { data += chunk.toString(); socket.destroy(); resolve(data); });
                    socket.on("timeout", () => { socket.destroy(); resolve(""); });
                    socket.on("error", () => { socket.destroy(); resolve(""); });
                });
                if (banner)
                    result.platform = banner.trim().slice(0, 100);
            }
            catch {
                // ignore
            }
        }
        return result;
    });
    // ---- POST /api/network/ssh/enable — guide/enable SSH on a target ----
    app.post("/api/network/ssh/enable", async (request) => {
        const body = (request.body ?? {});
        const targetPlatform = String(body["platform"] ?? "").trim();
        // Return platform-specific instructions for enabling SSH
        const instructions = {
            windows: {
                command: "Add-WindowsCapability -Online -Name OpenSSH.Server; Start-Service sshd; Set-Service -Name sshd -StartupType Automatic",
                steps: [
                    "Open PowerShell as Administrator on the target machine",
                    "Run: Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0",
                    "Run: Start-Service sshd",
                    "Run: Set-Service -Name sshd -StartupType Automatic",
                    'Run: New-NetFirewallRule -Name "OpenSSH-Server" -DisplayName "OpenSSH Server" -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22',
                ],
            },
            linux: {
                command: "sudo apt-get install -y openssh-server && sudo systemctl enable --now sshd",
                steps: [
                    "Run: sudo apt-get install -y openssh-server",
                    "Run: sudo systemctl enable --now sshd",
                    "Run: sudo ufw allow 22/tcp (if UFW is active)",
                ],
            },
            macos: {
                command: "sudo systemsetup -setremotelogin on",
                steps: [
                    "Open System Preferences → Sharing → enable Remote Login",
                    "Or run: sudo systemsetup -setremotelogin on",
                ],
            },
        };
        const key = targetPlatform.toLowerCase();
        const info = instructions[key] ?? instructions["linux"];
        return { platform: key || "linux", ...info };
    });
    // ---- POST /api/network/deploy — deploy gateway binary to a remote host ----
    app.post("/api/network/deploy", async (request, reply) => {
        const body = (request.body ?? {});
        const ip = String(body["ip"] ?? "").trim();
        const username = String(body["username"] ?? "root").trim();
        const terminalId = String(body["terminalId"] ?? "").trim();
        if (!ip) {
            return reply.status(400).send({ error: "IP address is required" });
        }
        if (!terminalId) {
            return reply.status(400).send({ error: "Terminal ID is required" });
        }
        if (!surfaceRegistry) {
            return reply.status(503).send({ error: "Terminal support is unavailable" });
        }
        const surface = surfaceRegistry.getSurface(terminalId);
        if (!surface || surface.type !== "terminal") {
            return reply.status(404).send({ error: "Terminal not found" });
        }
        const terminal = surface;
        try {
            await terminal.waitForPrompt();
            terminal.write(buildInteractiveDeployCommand(ip, username) + "\r");
            return {
                ok: true,
                terminalId,
                message: `Deploy started in terminal ${terminalId}`,
            };
        }
        catch (err) {
            return reply.status(500).send({ error: err instanceof Error ? err.message : "Deployment failed" });
        }
    });
    // ---- GET /api/network/nodes — list known gateway mesh nodes ----
    app.get("/api/network/nodes", async () => {
        return { nodes: [...knownNodes.values()] };
    });
    // ---- POST /api/network/nodes/register — a remote gateway node announces itself ----
    app.post("/api/network/nodes/register", async (request) => {
        const body = (request.body ?? {});
        const node = {
            id: String(body["id"] ?? ""),
            ip: String(body["ip"] ?? ""),
            hostname: body["hostname"] ?? null,
            platform: String(body["platform"] ?? "unknown"),
            version: String(body["version"] ?? "unknown"),
            status: "online",
            lastSeen: new Date().toISOString(),
            capabilities: Array.isArray(body["capabilities"]) ? body["capabilities"].map(String) : [],
        };
        if (!node.id || !node.ip)
            return { error: "id and ip are required" };
        knownNodes.set(node.id, node);
        return { ok: true, node };
    });
    // ---- GET /api/network/devices — registered companion devices ----
    app.get("/api/network/devices", async () => {
        // Proxy to existing mobile device registry
        // This unifies the view of all connected devices in the Network panel
        return { devices: [] }; // Will be wired via deps if needed
    });
    // ---- GET /api/network/topology — unified graph data for the force-graph visualization ----
    app.get("/api/network/topology", async () => {
        // Gateway node — cached, refreshed every 5 min
        const gatewayNode = await getGatewayNode(providerRegistry);
        // Connected FsNode devices
        const connectedDevices = (ws?.getFsNodes() ?? []).filter(n => !n.isGateway).map(n => ({
            id: `device-${n.id}`,
            type: "device",
            name: n.name,
            platform: n.platform,
            providers: n.providers ?? [],
            online: true,
            registeredAt: n.registeredAt,
        }));
        // Scanned network hosts — prefer in-memory cache, fall back to DB
        const scan = getLatestNetworkScan();
        let hostSource = scan?.hosts ?? [];
        let scannedAt = scan?.scannedAt ?? null;
        if (hostSource.length === 0 && sqlite) {
            const dbData = readHostsFromDb(sqlite);
            hostSource = dbData.hosts;
            scannedAt = dbData.scannedAt;
        }
        const routerIp = scan?.routerIp ?? null;
        const scannedHosts = hostSource.map(h => ({
            id: `host-${h.ip}`,
            type: "host",
            name: h.isRouter || h.ip === routerIp ? (h.hostname ?? "Router") : (h.hostname ?? h.ip),
            ip: h.ip,
            mac: h.mac,
            isRouter: Boolean(h.isRouter || h.ip === routerIp),
            openPorts: h.openPorts,
            sshReachable: h.sshReachable,
            agentStatus: h.agentStatus,
            osVersion: h.osVersion ?? null,
            providers: h.providers ?? [],
            online: h.alive ?? true,
        }));
        // Known gateway mesh nodes
        const meshNodes = [...knownNodes.values()].map(n => ({
            id: `mesh-${n.id}`,
            type: "mesh",
            name: n.hostname ?? n.ip,
            ip: n.ip,
            platform: n.platform,
            version: n.version,
            status: n.status,
            online: n.status === "online",
        }));
        return {
            gateway: gatewayNode,
            devices: connectedDevices,
            hosts: scannedHosts,
            meshNodes,
            routerIp,
            scannedAt,
        };
    });
}
//# sourceMappingURL=network.js.map