#!/usr/bin/env bun
import { spawn } from "node:child_process";
import { createServer } from "node:net";
import { existsSync } from "node:fs";
import http from "node:http";
import https from "node:https";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import assert from "node:assert/strict";

const cwd = new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/u, "$1");
const useEdgeProxy = process.argv.includes("--edge");
const cloudPort = await freePort();
const gatewayPort = await freePort();
const edgePort = useEdgeProxy ? await freePort() : null;
const noProxy = mergeNoProxy(process.env.NO_PROXY, process.env.no_proxy, [
  "localhost",
  "127.0.0.1",
  "::1"
]);
const commonEnv = {
  ...process.env,
  NO_PROXY: noProxy,
  no_proxy: noProxy,
  HWLAB_CLOUD_RUNTIME_ADAPTER: "memory",
  HWLAB_CLOUD_RUNTIME_DURABLE: "false"
};
const children = [];
const bunCommand = resolveBunCommand();
const nodeCommand = process.env.HWLAB_NODE_COMMAND || "node";

try {
  const cloud = startNode("cmd/hwlab-cloud-api/main.ts", {
    ...commonEnv,
    HWLAB_CLOUD_API_HOST: "127.0.0.1",
    HWLAB_CLOUD_API_PORT: String(cloudPort),
    HWLAB_GATEWAY_DISPATCH_TIMEOUT_MS: "10000"
  });
  children.push(cloud);
  await waitForJson(`http://127.0.0.1:${cloudPort}/health/live`);

  if (useEdgeProxy) {
    const edge = startNode("cmd/hwlab-edge-proxy/main.mjs", {
      ...commonEnv,
      HWLAB_EDGE_HOST: "127.0.0.1",
      HWLAB_EDGE_PORT: String(edgePort),
      HWLAB_EDGE_UPSTREAM: `http://127.0.0.1:${cloudPort}`
    });
    children.push(edge);
    await waitForJson(`http://127.0.0.1:${edgePort}/health`);
  }

  const apiBaseUrl = useEdgeProxy ? `http://127.0.0.1:${edgePort}` : `http://127.0.0.1:${cloudPort}`;
  const gateway = startNode("cmd/hwlab-gateway/main.ts", {
    ...commonEnv,
    PORT: String(gatewayPort),
    HWLAB_GATEWAY_CLOUD_URL: apiBaseUrl,
    HWLAB_GATEWAY_ID: "gtw_local_demo",
    HWLAB_GATEWAY_SESSION_ID: "gws_gtw_local_demo",
    HWLAB_GATEWAY_CMD_EXEC_ENABLED: "1",
    HWLAB_GATEWAY_DEMO_OPEN: "1",
    HWLAB_GATEWAY_POLL_INTERVAL_MS: "100",
    HWLAB_GATEWAY_CMD_TIMEOUT_MS: "5000"
  });
  children.push(gateway);
  await waitForGatewaySession(apiBaseUrl, "gws_gtw_local_demo");

  const rpc = await postJson(`${apiBaseUrl}/json-rpc`, {
    jsonrpc: "2.0",
    id: "req_gateway_outbound_demo_shell",
    method: "hardware.invoke.shell",
    params: {
      projectId: "prj_mvp_topology",
      gatewaySessionId: "gws_gtw_local_demo",
      resourceId: "res_windows_host",
      capabilityId: "cap_windows_cmd_exec",
      input: {
        command: "echo hwlab-demo",
        cwd,
        timeoutMs: 5000
      }
    },
    meta: {
      traceId: "trc_gateway_outbound_demo_shell",
      actorId: "usr_gateway_outbound_demo",
      serviceId: "hwlab-cloud-api",
      environment: "dev"
    }
  });

  assert.equal(rpc.error, undefined, JSON.stringify(rpc.error ?? {}, null, 2));
  assert.equal(rpc.result?.dispatch?.shellExecuted, true);
  assert.equal(rpc.result?.dispatch?.exitCode, 0);
  assert.match(rpc.result?.dispatch?.stdout ?? "", /hwlab-demo/u);

  const slow = postJson(`${apiBaseUrl}/json-rpc`, shellRpc({
    id: "req_gateway_outbound_demo_slow",
    traceId: "trc_gateway_outbound_demo_slow",
    command: process.platform === "win32" ? "ping -n 4 127.0.0.1 >NUL" : "sleep 3",
    cwd,
    timeoutMs: 5000
  }));
  await delay(250);
  const quickStartedAt = Date.now();
  const quick = await postJson(`${apiBaseUrl}/json-rpc`, shellRpc({
    id: "req_gateway_outbound_demo_quick",
    traceId: "trc_gateway_outbound_demo_quick",
    command: "echo hwlab-quick",
    cwd,
    timeoutMs: 5000
  }));
  const quickDurationMs = Date.now() - quickStartedAt;
  assert.equal(quick.result?.dispatch?.shellExecuted, true);
  assert.equal(quick.result?.dispatch?.exitCode, 0);
  assert.match(quick.result?.dispatch?.stdout ?? "", /hwlab-quick/u);
  assert.ok(quickDurationMs < 2500, `quick request was blocked by slow request for ${quickDurationMs}ms`);

  const sessionDuringSlow = await waitForGatewaySession(apiBaseUrl, "gws_gtw_local_demo");
  assert.equal(Number.isInteger(sessionDuringSlow.inflightCount), true);
  const slowResult = await slow;
  assert.equal(slowResult.result?.dispatch?.shellExecuted, true);
  assert.equal(slowResult.result?.dispatch?.exitCode, 0);

  console.log(JSON.stringify({
    status: "pass",
    mode: useEdgeProxy ? "edge-proxy" : "direct-cloud-api",
    cloudUrl: `http://127.0.0.1:${cloudPort}`,
    apiBaseUrl,
    gatewaySessionId: "gws_gtw_local_demo",
    stdout: rpc.result.dispatch.stdout.trim(),
    nonBlockingQuickMs: quickDurationMs,
    inflightCountObserved: sessionDuringSlow.inflightCount
  }, null, 2));
} finally {
  for (const child of children.reverse()) {
    stopChild(child);
  }
}

function startNode(entrypoint, env) {
  const command = entrypoint.endsWith(".ts") ? bunCommand : nodeCommand;
  const child = spawn(command, [entrypoint], {
    cwd,
    env,
    stdio: ["ignore", "pipe", "pipe"]
  });
  child.output = "";
  child.stderrOutput = "";
  child.stdout.on("data", (chunk) => {
    child.output += chunk.toString("utf8");
  });
  child.stderr.on("data", (chunk) => {
    child.stderrOutput += chunk.toString("utf8");
  });
  child.on("exit", (code, signal) => {
    child.exitInfo = { code, signal };
  });
  return child;
}

function resolveBunCommand() {
  const candidates = [
    process.env.HWLAB_BUN_COMMAND,
    process.env.HOME ? path.join(process.env.HOME, ".bun/bin/bun") : null,
    "bun"
  ].filter(Boolean);
  return candidates.find((candidate) => !candidate.includes("/") || existsSync(candidate)) || "bun";
}

async function waitForGatewaySession(apiBaseUrl, gatewaySessionId) {
  const deadline = Date.now() + 8000;
  let last;
  while (Date.now() < deadline) {
    try {
      const body = await getJson(`${apiBaseUrl}/v1/gateway/sessions`);
      last = body;
      const session = body.sessions?.find((item) => item.gatewaySessionId === gatewaySessionId);
      if (session?.status === "online") return session;
    } catch (error) {
      last = error;
    }
    await delay(100);
  }
  throw new Error(`gateway session ${gatewaySessionId} did not become online: ${stringifyLast(last)}`);
}

async function waitForJson(url) {
  const deadline = Date.now() + 8000;
  let last;
  while (Date.now() < deadline) {
    try {
      return await getJson(url);
    } catch (error) {
      last = error;
      await delay(100);
    }
  }
  throw new Error(`endpoint did not become ready: ${url}; last=${stringifyLast(last)}`);
}

async function getJson(url) {
  return requestJson(url, {
    method: "GET",
    headers: { accept: "application/json" }
  });
}

async function postJson(url, body) {
  return requestJson(url, {
    method: "POST",
    headers: {
      accept: "application/json",
      "content-type": "application/json"
    },
    body
  });
}

function requestJson(url, { method, headers = {}, body }) {
  return new Promise((resolve, reject) => {
    const target = new URL(url);
    const client = target.protocol === "https:" ? https : http;
    const payload = body === undefined ? "" : JSON.stringify(body);
    const request = client.request(target, {
      method,
      headers: {
        ...headers,
        ...(payload ? { "content-length": Buffer.byteLength(payload) } : {})
      }
    }, (response) => {
      let text = "";
      response.setEncoding("utf8");
      response.on("data", (chunk) => {
        text += chunk;
      });
      response.on("end", () => {
        if ((response.statusCode ?? 0) < 200 || (response.statusCode ?? 0) >= 300) {
          reject(new Error(`HTTP ${response.statusCode}: ${text}`));
          return;
        }
        try {
          resolve(text ? JSON.parse(text) : null);
        } catch (error) {
          reject(new Error(`non-JSON response: ${error.message}`));
        }
      });
    });
    request.on("error", reject);
    if (payload) request.write(payload);
    request.end();
  });
}

function shellRpc({ id, traceId, command, cwd, timeoutMs }) {
  return {
    jsonrpc: "2.0",
    id,
    method: "hardware.invoke.shell",
    params: {
      projectId: "prj_mvp_topology",
      gatewaySessionId: "gws_gtw_local_demo",
      resourceId: "res_windows_host",
      capabilityId: "cap_windows_cmd_exec",
      input: {
        command,
        cwd,
        timeoutMs
      }
    },
    meta: {
      traceId,
      actorId: "usr_gateway_outbound_demo",
      serviceId: "hwlab-cloud-api",
      environment: "dev"
    }
  };
}

function freePort() {
  return new Promise((resolve, reject) => {
    const server = createServer();
    server.once("error", reject);
    server.listen(0, "127.0.0.1", () => {
      const address = server.address();
      const port = typeof address === "object" && address ? address.port : null;
      server.close(() => resolve(port));
    });
  });
}

function mergeNoProxy(...parts) {
  const values = new Set();
  for (const part of parts.flat()) {
    for (const item of String(part ?? "").split(",")) {
      const trimmed = item.trim();
      if (trimmed) values.add(trimmed);
    }
  }
  return [...values].join(",");
}

function stopChild(child) {
  if (!child.killed && child.exitCode === null) {
    child.kill();
  }
}

function stringifyLast(value) {
  if (value instanceof Error) return value.message;
  return JSON.stringify(value);
}
