#!/usr/bin/env node
import assert from "node:assert/strict";
import http from "node:http";
import https from "node:https";
import { setTimeout as delay } from "node:timers/promises";

const args = parseArgs(process.argv.slice(2));
const apiBaseUrl = stripTrailingSlash(args.apiBaseUrl || process.env.HWLAB_GATEWAY_PROBE_API_BASE_URL || "http://127.0.0.1:6667");
const gatewaySessionId = args.gatewaySessionId || process.env.HWLAB_GATEWAY_PROBE_SESSION_ID || "gws_gtw_local_demo";
const cwd = args.cwd || process.cwd();
const slowCommand = args.slowCommand || (process.platform === "win32" ? "ping -n 4 127.0.0.1 >NUL" : "sleep 3");
const quickCommand = args.quickCommand || "echo hwlab-nonblocking-quick";
const timeoutMs = positiveInteger(args.timeoutMs, 10000);
const quickMaxMs = positiveInteger(args.quickMaxMs, 2500);

await waitForGatewaySession();

const slow = postRpc({
  id: "req_gateway_nonblocking_slow",
  traceId: "trc_gateway_nonblocking_slow",
  command: slowCommand
});
await delay(250);

const quickStartedAt = Date.now();
const quick = await postRpc({
  id: "req_gateway_nonblocking_quick",
  traceId: "trc_gateway_nonblocking_quick",
  command: quickCommand
});
const quickDurationMs = Date.now() - quickStartedAt;
const slowResult = await slow;

assert.equal(quick.result?.dispatch?.shellExecuted, true, JSON.stringify(quick, null, 2));
assert.equal(quick.result?.dispatch?.exitCode, 0, JSON.stringify(quick, null, 2));
assert.equal(slowResult.result?.dispatch?.shellExecuted, true, JSON.stringify(slowResult, null, 2));
assert.equal(slowResult.result?.dispatch?.exitCode, 0, JSON.stringify(slowResult, null, 2));
assert.ok(quickDurationMs < quickMaxMs, `quick request was blocked by slow request for ${quickDurationMs}ms`);

console.log(JSON.stringify({
  status: "pass",
  apiBaseUrl,
  gatewaySessionId,
  quickDurationMs,
  quickOperationId: quick.result?.operationId ?? null,
  slowOperationId: slowResult.result?.operationId ?? null,
  quickStdout: String(quick.result?.dispatch?.stdout ?? "").trim()
}, null, 2));

async function waitForGatewaySession() {
  const deadline = Date.now() + 8000;
  let last;
  while (Date.now() < deadline) {
    try {
      const body = await getJson("/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 getJson(path) {
  return requestJson(`${apiBaseUrl}${path}`, {
    method: "GET",
    headers: { accept: "application/json" }
  });
}

async function postRpc({ id, traceId, command }) {
  return requestJson(`${apiBaseUrl}/json-rpc`, {
    method: "POST",
    headers: {
      accept: "application/json",
      "content-type": "application/json"
    },
    body: {
      jsonrpc: "2.0",
      id,
      method: "hardware.invoke.shell",
      params: {
        projectId: "prj_mvp_topology",
        gatewaySessionId,
        resourceId: "res_windows_host",
        capabilityId: "cap_windows_cmd_exec",
        input: {
          command,
          cwd,
          timeoutMs
        }
      },
      meta: {
        traceId,
        actorId: "usr_gateway_nonblocking_probe",
        serviceId: "hwlab-cloud-api",
        environment: "dev"
      }
    }
  });
}

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(`${method} ${target.pathname} HTTP ${response.statusCode}: ${text}`));
          return;
        }
        try {
          resolve(text ? JSON.parse(text) : null);
        } catch (error) {
          reject(new Error(`${method} ${target.pathname} returned non-JSON: ${error.message}`));
        }
      });
    });
    request.on("error", reject);
    if (payload) request.write(payload);
    request.end();
  });
}

function parseArgs(argv) {
  const result = {};
  for (let index = 0; index < argv.length; index += 1) {
    const item = argv[index];
    if (item.startsWith("--") && item.includes("=")) {
      const [key, ...rest] = item.slice(2).split("=");
      result[toCamel(key)] = rest.join("=");
    } else if (item.startsWith("--")) {
      result[toCamel(item.slice(2))] = argv[index + 1] ?? "";
      index += 1;
    }
  }
  return result;
}

function toCamel(value) {
  return String(value).replace(/-([a-z])/gu, (_, char) => char.toUpperCase());
}

function positiveInteger(value, fallback) {
  const parsed = Number.parseInt(value ?? "", 10);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}

function stripTrailingSlash(value) {
  return String(value).replace(/\/+$/u, "");
}

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