#!/usr/bin/env node
import http from "node:http";
import https from "node:https";
import { randomUUID } from "node:crypto";
import { readFileSync } from "node:fs";

const DEFAULT_API_BASE_URL = "http://127.0.0.1:6667";
const DEFAULT_PROJECT_ID = "prj_mvp_topology";
const DEFAULT_GATEWAY_SESSION_ID = "gws_DESKTOP-1MHOD9I";
const DEFAULT_RESOURCE_ID = "res_windows_host";
const DEFAULT_CAPABILITY_ID = "cap_windows_cmd_exec";
const DEFAULT_GATEWAY_SHELL_TIMEOUT_MS = 120000;
const MAX_GATEWAY_SHELL_TIMEOUT_MS = 600000;
const REQUEST_TIMEOUT_GRACE_MS = 10000;
const POWERSHELL_PROLOGUE = [
  "$ErrorActionPreference = 'Stop';",
  "$utf8NoBom = [System.Text.UTF8Encoding]::new($false);",
  "[Console]::InputEncoding = $utf8NoBom;",
  "[Console]::OutputEncoding = $utf8NoBom;",
  "$OutputEncoding = $utf8NoBom;",
  "$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8';",
  "$PSDefaultParameterValues['Set-Content:Encoding'] = 'utf8';",
  "function Read-HwlabText { param([Parameter(Mandatory=$true)][string]$LiteralPath) [System.IO.File]::ReadAllText($LiteralPath, $utf8NoBom) }",
  "function Select-HwlabText { param([Parameter(Mandatory=$true)][string]$LiteralPath,[Parameter(Mandatory=$true)][string]$Pattern,[int]$First=20) Select-String -LiteralPath $LiteralPath -Pattern $Pattern -Encoding UTF8 | Select-Object -First $First @{Name='path';Expression={$_.Path}}, @{Name='lineNumber';Expression={$_.LineNumber}}, @{Name='line';Expression={$_.Line}} }",
  "function ConvertTo-HwlabJson { param([Parameter(ValueFromPipeline=$true)][object]$InputObject,[int]$Depth=8) begin { $items = @() } process { $items += $InputObject } end { if ($items.Count -eq 1) { $items[0] | ConvertTo-Json -Compress -Depth $Depth } else { $items | ConvertTo-Json -Compress -Depth $Depth } } }"
].join("\n");

async function main() {
  const args = parseArgs(process.argv.slice(2));
  const command = await resolveCommand(args);
  if (args.help || !command) {
    printHelp();
    process.exit(args.help ? 0 : 2);
  }

  const apiBaseUrl = stripTrailingSlash(args.apiBaseUrl || process.env.HWLAB_GATEWAY_SHELL_API_BASE_URL || DEFAULT_API_BASE_URL);
  const traceId = args.traceId || `trc_gateway_shell_cli_${Date.now()}`;
  const requestId = args.requestId || `req_gateway_shell_cli_${randomUUID()}`;
  const operationId = args.operationId || `op_gateway_shell_cli_${randomUUID()}`;
  const timeoutMs = boundedPositiveInteger(args.timeoutMs ?? process.env.HWLAB_GATEWAY_SHELL_TIMEOUT_MS, DEFAULT_GATEWAY_SHELL_TIMEOUT_MS, {
    min: 1000,
    max: MAX_GATEWAY_SHELL_TIMEOUT_MS
  });
  const requestTimeoutMs = boundedPositiveInteger(args.requestTimeoutMs, timeoutMs + REQUEST_TIMEOUT_GRACE_MS, {
    min: timeoutMs,
    max: timeoutMs + Math.max(REQUEST_TIMEOUT_GRACE_MS, Math.floor(timeoutMs / 2))
  });
  const response = await requestJson(`${apiBaseUrl}/v1/rpc/hardware.invoke.shell`, {
    method: "POST",
    timeoutMs: requestTimeoutMs,
    headers: {
      "content-type": "application/json",
      "x-trace-id": traceId,
      "x-request-id": requestId,
      "x-actor-id": args.actorId || "usr_code_agent",
      "x-source-service-id": "hwlab-cloud-api"
    },
    body: {
      projectId: args.projectId || DEFAULT_PROJECT_ID,
      operationId,
      gatewaySessionId: args.gatewaySessionId || DEFAULT_GATEWAY_SESSION_ID,
      resourceId: args.resourceId || DEFAULT_RESOURCE_ID,
      capabilityId: args.capabilityId || DEFAULT_CAPABILITY_ID,
      input: {
        command,
        cwd: args.cwd || undefined,
        timeoutMs
      }
    }
  });

  if (args.json) {
    process.stdout.write(`${JSON.stringify(response.body, null, 2)}\n`);
  } else {
    process.stdout.write(formatResponse(response.body, { httpStatus: response.statusCode, traceId, requestId }));
  }

  const result = response.body?.result ?? {};
  const dispatch = result.dispatch ?? {};
  const exitCode = Number.isInteger(dispatch.exitCode) ? dispatch.exitCode : Number.isInteger(result.exitCode) ? result.exitCode : null;
  const failed = response.statusCode < 200 || response.statusCode >= 300 || response.body?.error || result.status === "failed" || result.status === "rejected" || (exitCode !== null && exitCode !== 0);
  process.exit(failed ? 1 : 0);
}

function parseArgs(argv) {
  const args = {};
  const booleanFlags = new Set(["json", "help", "h", "powershellStdin", "psStdin"]);
  for (let index = 0; index < argv.length; index += 1) {
    const item = argv[index];
    if ((item === "--help") || (item === "-h")) {
      args.help = true;
    } else if (item.startsWith("--") && item.includes("=")) {
      const [key, ...rest] = item.slice(2).split("=");
      args[toCamel(key)] = rest.join("=");
    } else if (item.startsWith("--")) {
      const key = toCamel(item.slice(2));
      if (booleanFlags.has(key)) {
        args[key] = true;
      } else {
        args[key] = argv[index + 1] ?? "";
        index += 1;
      }
    }
  }
  return args;
}

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

function printHelp() {
  process.stdout.write([
    "Usage: node tools/hwlab-gateway-shell.mjs --command \"cmd /c ...\" [--json]",
    "       node tools/hwlab-gateway-shell.mjs --powershell-command \"Get-ChildItem ...\" [--json]",
    "       node tools/hwlab-gateway-shell.mjs --powershell-stdin [--json] < script.ps1",
    "",
    "Options:",
    "  --api-base-url URL         Cloud API/edge base URL, default http://127.0.0.1:6667",
    "  --command CMD              Windows cmd command to execute through the registered PC gateway",
    "  --powershell-command PS1   Encode PS1 as UTF-16LE and run powershell.exe -EncodedCommand",
    "  --powershell-file PATH     Read a local PS1 file, encode it, and run through the gateway",
    "  --powershell-stdin         Read PS1 from stdin, encode it, and run through the gateway",
    "  --powershell-exe NAME      Default powershell.exe",
    "  --cwd WINDOWS_PATH         Gateway-side working directory; prefer this over shell-level cd &&",
    "  --timeout-ms N             Gateway command timeout, default 120000ms; can also use HWLAB_GATEWAY_SHELL_TIMEOUT_MS",
    "  --request-timeout-ms N     HTTP wait timeout; defaults to command timeout plus a small response grace",
    "  --operation-id ID          Stable operationId to preserve correlation if dispatch times out",
    "  --project-id ID            Default prj_mvp_topology",
    "  --gateway-session-id ID    Default gws_DESKTOP-1MHOD9I",
    "  --resource-id ID           Default res_windows_host",
    "  --capability-id ID         Default cap_windows_cmd_exec",
    "  --json                     Print full JSON-RPC response"
  ].join("\n") + "\n");
}

async function resolveCommand(args) {
  if (args.powershellStdin || args.psStdin) {
    return powershellEncodedCommand(await readStdin(), args);
  }
  if (args.powershellFile || args.psFile) {
    return powershellEncodedCommand(readFileSync(args.powershellFile || args.psFile, "utf8"), args);
  }
  if (args.powershellCommand || args.psCommand || args.powershell) {
    return powershellEncodedCommand(args.powershellCommand || args.psCommand || args.powershell, args);
  }
  return args.command || "";
}

function powershellEncodedCommand(script, args = {}) {
  const source = String(script || "").trim();
  if (!source) return "";
  const shell = args.powershellExe || "powershell.exe";
  const prologue = args.noPowershellPrologue ? "" : `${POWERSHELL_PROLOGUE}\n`;
  const encoded = Buffer.from(`${prologue}${source}`, "utf16le").toString("base64");
  return `${shell} -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`;
}

function readStdin() {
  return new Promise((resolve, reject) => {
    let text = "";
    process.stdin.setEncoding("utf8");
    process.stdin.on("data", (chunk) => {
      text += chunk;
    });
    process.stdin.on("end", () => resolve(text));
    process.stdin.on("error", reject);
    process.stdin.resume();
  });
}

function requestJson(url, { method, headers, body, timeoutMs }) {
  return new Promise((resolve, reject) => {
    const target = new URL(url);
    const client = target.protocol === "https:" ? https : http;
    const payload = JSON.stringify(body ?? {});
    const request = client.request(target, {
      method,
      headers: {
        ...headers,
        "content-length": Buffer.byteLength(payload)
      },
      timeout: timeoutMs
    }, (response) => {
      let text = "";
      response.setEncoding("utf8");
      response.on("data", (chunk) => {
        text += chunk;
      });
      response.on("end", () => {
        try {
          resolve({
            statusCode: response.statusCode ?? 0,
            body: text ? JSON.parse(text) : null
          });
        } catch (error) {
          reject(new Error(`Cloud API returned non-JSON response: ${error.message}`));
        }
      });
    });
    request.on("timeout", () => {
      request.destroy(new Error(`Gateway shell request timed out after ${timeoutMs}ms`));
    });
    request.on("error", reject);
    request.end(payload);
  });
}

function formatResponse(body, { httpStatus, traceId, requestId }) {
  if (body?.error) {
    return [
      `http=${httpStatus} trace=${traceId} request=${requestId}`,
      `error=${body.error.message ?? body.error.code}`,
      body.error.data?.reason ? `reason=${body.error.data.reason}` : null
    ].filter(Boolean).join("\n") + "\n";
  }
  const result = body?.result ?? {};
  const dispatch = result.dispatch ?? {};
  const lines = [
    `http=${httpStatus} trace=${traceId} request=${requestId}`,
    `status=${result.status ?? "unknown"} operation=${result.operationId ?? "null"} dispatch=${dispatch.dispatchStatus ?? "unknown"} exit=${dispatch.exitCode ?? "null"}`
  ];
  if (dispatch.stdout) lines.push(String(dispatch.stdout).trimEnd());
  if (dispatch.stderr) lines.push(String(dispatch.stderr).trimEnd());
  return `${lines.join("\n")}\n`;
}

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

function boundedPositiveInteger(value, fallback, { min, max } = {}) {
  const parsed = positiveInteger(value, fallback);
  const lower = Number.isInteger(min) ? min : 1;
  const upper = Number.isInteger(max) ? max : parsed;
  return Math.min(Math.max(parsed, lower), upper);
}

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

main().catch((error) => {
  process.stderr.write(`${error.message}\n`);
  process.exit(1);
});
