#!/usr/bin/env bun
import os from "node:os";
import { spawn } from "node:child_process";

import { createAuditEvent, createEvidenceRecord, operationContext } from "../../internal/sim/l2-runtime.mjs";
import { createGatewayState, createHealth } from "../../internal/sim/model.mjs";
import { createJsonServer, fetchJson, listen, parsePort } from "../../internal/sim/http.mjs";
import { ENVIRONMENT_DEV, ERROR_CODES, JSON_RPC_VERSION, createRpcErrorBody, isProtocolEnvironment } from "../../internal/protocol/index.mjs";

const gatewayId = process.env.HWLAB_GATEWAY_ID ?? "gateway_dev_stub";
const gatewaySessionId = process.env.HWLAB_GATEWAY_SESSION_ID ?? `gws_${gatewayId}`;
const port = parsePort(process.env.PORT, 7001);
const cloudUrl = trimUrl(process.env.HWLAB_GATEWAY_CLOUD_URL);
const pollIntervalMs = parsePositiveInteger(process.env.HWLAB_GATEWAY_POLL_INTERVAL_MS, 500);
const commandTimeoutMs = parsePositiveInteger(process.env.HWLAB_GATEWAY_CMD_TIMEOUT_MS, 120000);
const outputLimitBytes = parsePositiveInteger(process.env.HWLAB_GATEWAY_CMD_OUTPUT_LIMIT_BYTES, 65536);
const maxInflightRequests = parsePositiveInteger(process.env.HWLAB_GATEWAY_MAX_INFLIGHT, 3);
const gatewayEnvironment = normalizeEnvironment(process.env.HWLAB_ENVIRONMENT || process.env.HWLAB_GITOPS_PROFILE || ENVIRONMENT_DEV);
const resourceId = process.env.HWLAB_GATEWAY_RESOURCE_ID ?? "res_windows_host";
const boxId = process.env.HWLAB_GATEWAY_BOX_ID ?? "box_windows_host";
const commandExecutionEnabled = truthy(process.env.HWLAB_GATEWAY_CMD_EXEC_ENABLED) || truthy(process.env.HWLAB_GATEWAY_DEMO_OPEN);
const inflightRequests = new Map();
const state = createGatewayState({
  gatewayId,
  serviceId: "hwlab-gateway",
  endpoint: process.env.HWLAB_GATEWAY_ENDPOINT ?? `http://127.0.0.1:${port}`,
  boxes: [resourceId]
});
state.session.gatewaySessionId = gatewaySessionId;
state.session.status = "connected";
state.registry = {
  gatewayId,
  gatewaySessionId,
  resourceId,
  boxId,
  capabilities: shellCapabilities()
};
state.outbound = {
  enabled: Boolean(cloudUrl),
  cloudUrl,
  pollIntervalMs,
  commandExecutionEnabled,
  maxInflightRequests,
  inflightCount: 0,
  inflightRequests: [],
  lastPollAt: null,
  lastPollError: null,
  lastResultAt: null,
  lastResultError: null,
  lastBusyAt: null
};

const routes = new Map();

routes.set("GET /health/live", () => ({
  ...createHealth({ serviceId: "hwlab-gateway", name: gatewayId }),
  gatewayId,
  gatewaySessionId,
  outboundConnected: Boolean(state.outbound.lastPollAt && !state.outbound.lastPollError)
}));
routes.set("GET /status", () => ({
  ...state,
  note: "Outbound poll demo gateway; cloud dispatches through a gateway-initiated connection."
}));
routes.set("GET /capabilities", () => ({
  serviceId: "hwlab-gateway",
  gatewayId,
  gatewaySessionId,
  resourceId,
  boxId,
  capabilities: shellCapabilities()
}));

listen(createJsonServer({ routes }), port);

if (cloudUrl) {
  ensureNoProxyForLocalCloud(cloudUrl);
  schedulePoll(0);
}

function schedulePoll(delayMs = pollIntervalMs) {
  setTimeout(async () => {
    await pollCloudOnce();
    schedulePoll();
  }, delayMs).unref?.();
}

async function pollCloudOnce() {
  try {
    const response = await fetchJson(new URL("/v1/gateway/poll", ensureTrailingSlash(cloudUrl)), {
      method: "POST",
      body: registrationPayload()
    });
    if (!response.ok) {
      throw new Error(`cloud poll failed: HTTP ${response.status}`);
    }
    state.outbound.lastPollAt = new Date().toISOString();
    state.outbound.lastPollError = null;
    state.session.lastSeenAt = state.outbound.lastPollAt;
    if (response.body?.request) {
      startCloudRequest(response.body.request);
    }
  } catch (error) {
    state.outbound.lastPollError = error instanceof Error ? error.message : String(error);
  }
}

function startCloudRequest(request) {
  const requestId = String(request?.id ?? `req_gateway_${Date.now()}`);
  if (inflightRequests.size >= maxInflightRequests) {
    state.outbound.lastBusyAt = new Date().toISOString();
    postGatewayBusyResult({
      ...request,
      id: requestId
    }).catch((error) => {
      state.outbound.lastResultError = error instanceof Error ? error.message : String(error);
    });
    return;
  }

  const startedAt = new Date().toISOString();
  inflightRequests.set(requestId, {
    requestId,
    method: request?.method ?? null,
    operationId: request?.params?.operationId ?? null,
    traceId: request?.meta?.traceId ?? null,
    startedAt
  });
  refreshInflightState();

  Promise.resolve()
    .then(() => handleCloudRequest({
      ...request,
      id: requestId
    }))
    .catch((error) => {
      state.outbound.lastResultError = error instanceof Error ? error.message : String(error);
    })
    .finally(() => {
      inflightRequests.delete(requestId);
      refreshInflightState();
    });
}

async function handleCloudRequest(request) {
  const environment = requestEnvironment(request?.meta);
  const context = operationContext({
    operationId: request.params?.operationId,
    traceId: request.meta?.traceId,
    requestId: request.id,
    actorId: request.meta?.actorId ?? "svc_hwlab-cloud-api"
  }, {
    serviceId: "hwlab-gateway",
    actorId: "svc_hwlab-gateway"
  });

  let response;
  try {
    if (request.method !== "hardware.invoke.shell") {
      throw rpcError(ERROR_CODES.methodNotFound, `Gateway method ${request.method ?? "unknown"} is not implemented`, {
        method: request.method ?? null
      });
    }
    const result = await invokeShell(request.params ?? {}, context, environment);
    response = {
      jsonrpc: JSON_RPC_VERSION,
      id: request.id,
      result,
      meta: responseMeta(context.traceId, environment)
    };
  } catch (error) {
    const body = error?.body ?? createRpcErrorBody({
      code: error?.code ?? ERROR_CODES.hwlabUnknown,
      message: error instanceof Error ? error.message : String(error),
      data: error?.data ?? {}
    });
    response = {
      jsonrpc: JSON_RPC_VERSION,
      id: request.id,
      error: body.error,
      meta: responseMeta(context.traceId, environment)
    };
  }

  await postGatewayResult(response);
}

async function postGatewayBusyResult(request) {
  const traceId = request?.meta?.traceId ?? `trc_gateway_busy_${Date.now()}`;
  const environment = requestEnvironment(request?.meta);
  const body = createRpcErrorBody({
    code: ERROR_CODES.operationRejected,
    message: "Gateway is busy; retry after an in-flight command finishes or increase HWLAB_GATEWAY_MAX_INFLIGHT.",
    data: {
      reason: "gateway_busy",
      operationId: request?.params?.operationId ?? null,
      inflightCount: inflightRequests.size,
      maxInflightRequests
    }
  });
  await postGatewayResult({
    jsonrpc: JSON_RPC_VERSION,
    id: request?.id ?? null,
    error: body.error,
    meta: responseMeta(traceId, environment)
  });
}

async function postGatewayResult(response) {
  const result = await fetchJson(new URL("/v1/gateway/result", ensureTrailingSlash(cloudUrl)), {
    method: "POST",
    body: {
      gatewayId,
      gatewaySessionId,
      response
    }
  });
  if (!result.ok) {
    throw new Error(`cloud result failed: HTTP ${result.status}`);
  }
  state.outbound.lastResultAt = new Date().toISOString();
  state.outbound.lastResultError = null;
}

async function invokeShell(params, context, environment = gatewayEnvironment) {
  if (!commandExecutionEnabled) {
    throw rpcError(ERROR_CODES.operationRejected, "Gateway command execution is disabled", {
      reason: "cmd_exec_disabled",
      requiredEnv: "HWLAB_GATEWAY_CMD_EXEC_ENABLED=1"
    });
  }

  const shellInput = normalizeShellInput(params);
  const startedAt = Date.now();
  const execution = await executeCommand(shellInput, {
    timeoutMs: shellInput.timeoutMs ?? commandTimeoutMs,
    outputLimitBytes
  });
  const durationMs = Date.now() - startedAt;
  const now = new Date().toISOString();
  const status = execution.timedOut ? "timed_out" : execution.exitCode === 0 ? "succeeded" : "failed";
  const audit = createAuditEvent({
    serviceId: "hwlab-gateway",
    action: "hardware.invoke.shell",
    targetType: "box_resource",
    targetId: params.resourceId ?? resourceId,
    projectId: params.projectId,
    gatewaySessionId,
    operationId: context.operationId,
    traceId: context.traceId,
    actorType: "service",
    actorId: "svc_hwlab-gateway",
    outcome: status === "succeeded" ? "succeeded" : "failed",
    environment,
    metadata: {
      gatewayId,
      resourceId: params.resourceId ?? resourceId,
      capabilityId: params.capabilityId ?? "cap_windows_cmd_exec",
      command: redactCommand(shellInput.command),
      cwd: execution.cwd,
      exitCode: execution.exitCode,
      timedOut: execution.timedOut,
      durationMs
    },
    occurredAt: now
  });
  const evidence = createEvidenceRecord({
    serviceId: "hwlab-gateway",
    projectId: params.projectId ?? "prj_mvp_topology",
    operationId: context.operationId,
    traceId: context.traceId,
    kind: "trace",
    environment,
    payload: {
      gatewayId,
      gatewaySessionId,
      resourceId: params.resourceId ?? resourceId,
      capabilityId: params.capabilityId ?? "cap_windows_cmd_exec",
      shell: execution
    },
    metadata: {
      gatewaySessionId,
      producerServiceId: "hwlab-gateway"
    },
    createdAt: now
  });

  state.lastDispatch = {
    operationId: context.operationId,
    traceId: context.traceId,
    resourceId: params.resourceId ?? resourceId,
    observedAt: now,
    accepted: true,
    status
  };
  state.updatedAt = now;

  return {
    accepted: true,
    status,
    serviceId: "hwlab-gateway",
    environment,
    gatewayId,
    gatewaySessionId,
    operationId: context.operationId,
    traceId: context.traceId,
    shellExecuted: true,
    dispatchStatus: status,
    ...execution,
    durationMs,
    auditId: audit.auditId,
    evidenceId: evidence.evidenceId,
    audit,
    evidence,
    gateway: {
      gatewayId,
      gatewaySessionId
    }
  };
}

function executeCommand(input, { timeoutMs, outputLimitBytes: limitBytes }) {
  return new Promise((resolve) => {
    const startedAt = new Date().toISOString();
    const shell = shellCommand(input.command);
    const child = spawn(shell.command, shell.args, {
      cwd: input.cwd,
      windowsHide: true,
      env: {
        ...process.env,
        PYTHONUTF8: process.env.PYTHONUTF8 ?? "1",
        PYTHONIOENCODING: process.env.PYTHONIOENCODING ?? "utf-8",
        NO_PROXY: process.env.NO_PROXY,
        no_proxy: process.env.no_proxy
      },
      stdio: ["ignore", "pipe", "pipe"]
    });
    const stdout = createLimitedCollector(limitBytes);
    const stderr = createLimitedCollector(limitBytes);
    let timedOut = false;
    let settled = false;
    const timer = setTimeout(() => {
      timedOut = true;
      killProcessTree(child);
    }, timeoutMs);

    child.stdout.on("data", (chunk) => stdout.push(chunk));
    child.stderr.on("data", (chunk) => stderr.push(chunk));
    child.on("error", (error) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      resolve({
        shell: shell.command,
        command: input.command,
        cwd: input.cwd ?? process.cwd(),
        startedAt,
        completedAt: new Date().toISOString(),
        exitCode: null,
        signal: null,
        timedOut,
        stdout: stdout.text(),
        stderr: stderr.text() || error.message,
        stdoutTruncated: stdout.truncated,
        stderrTruncated: stderr.truncated
      });
    });
    child.on("close", (exitCode, signal) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      resolve({
        shell: shell.command,
        command: input.command,
        cwd: input.cwd ?? process.cwd(),
        startedAt,
        completedAt: new Date().toISOString(),
        exitCode,
        signal,
        timedOut,
        stdout: stdout.text(),
        stderr: stderr.text(),
        stdoutTruncated: stdout.truncated,
        stderrTruncated: stderr.truncated
      });
    });
  });
}

function shellCommand(command) {
  if (process.platform === "win32") {
    return {
      command: process.env.ComSpec ?? "cmd.exe",
      args: ["/d", "/s", "/c", String(command)]
    };
  }
  return {
    command: "sh",
    args: ["-lc", String(command)]
  };
}

function killProcessTree(child) {
  if (!child.pid) return;
  if (process.platform === "win32") {
    spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
      windowsHide: true,
      stdio: "ignore"
    });
    return;
  }
  child.kill("SIGTERM");
}

function createLimitedCollector(limitBytes) {
  const chunks = [];
  let size = 0;
  let truncated = false;
  return {
    get truncated() {
      return truncated;
    },
    push(chunk) {
      if (size >= limitBytes) {
        truncated = true;
        return;
      }
      const buffer = Buffer.from(chunk);
      const available = limitBytes - size;
      if (buffer.byteLength > available) {
        chunks.push(buffer.subarray(0, available));
        size += available;
        truncated = true;
        return;
      }
      chunks.push(buffer);
      size += buffer.byteLength;
    },
    text() {
      return Buffer.concat(chunks).toString("utf8");
    }
  };
}

function registrationPayload() {
  refreshInflightState();
  return {
    serviceId: "hwlab-gateway",
    projectId: process.env.HWLAB_PROJECT_ID ?? "prj_mvp_topology",
    gatewayId,
    gatewaySessionId,
    endpoint: state.session.endpoint,
    resourceId,
    boxId,
    capabilities: shellCapabilities(),
    outbound: {
      pollIntervalMs,
      commandExecutionEnabled,
      maxInflightRequests,
      inflightCount: inflightRequests.size,
      inflightRequests: snapshotInflightRequests()
    },
    system: {
      platform: process.platform,
      arch: process.arch,
      hostname: os.hostname(),
      release: os.release()
    }
  };
}

function shellCapabilities() {
  return [
    {
      capabilityId: process.env.HWLAB_GATEWAY_CMD_CAPABILITY_ID ?? "cap_windows_cmd_exec",
      resourceId,
      projectId: process.env.HWLAB_PROJECT_ID ?? "prj_mvp_topology",
      name: "shell.exec",
      description: "Execute a local shell command through an outbound HWLAB gateway demo connection.",
      direction: "bidirectional",
      valueType: "object",
      constraints: {
        gatewayOutboundOnly: true,
        demoOpen: truthy(process.env.HWLAB_GATEWAY_DEMO_OPEN),
        maxInflightRequests
      },
      mutatesState: true
    }
  ];
}

function refreshInflightState() {
  state.outbound.inflightCount = inflightRequests.size;
  state.outbound.inflightRequests = snapshotInflightRequests();
}

function snapshotInflightRequests() {
  const now = Date.now();
  return [...inflightRequests.values()].map((request) => ({
    ...request,
    durationMs: durationSince(request.startedAt, now)
  }));
}

function durationSince(value, now = Date.now()) {
  const started = Date.parse(value ?? "");
  return Number.isFinite(started) ? Math.max(0, now - started) : 0;
}

function normalizeShellInput(params = {}) {
  const input = params.input && typeof params.input === "object" && !Array.isArray(params.input) ? params.input : {};
  const command = params.command ?? input.command ?? input.shell?.command;
  if (!command) {
    throw rpcError(ERROR_CODES.invalidParams, "hardware.invoke.shell requires input.command", {
      required: ["input.command"]
    });
  }
  return {
    command: Array.isArray(command) ? command.map(String).join(" ") : String(command),
    cwd: input.cwd ? String(input.cwd) : undefined,
    timeoutMs: parsePositiveInteger(input.timeoutMs, commandTimeoutMs)
  };
}

function responseMeta(traceId, environment = gatewayEnvironment) {
  return {
    traceId,
    serviceId: "hwlab-gateway",
    environment: normalizeEnvironment(environment)
  };
}

function requestEnvironment(meta = {}) {
  return normalizeEnvironment(meta?.environment || gatewayEnvironment);
}

function normalizeEnvironment(environment) {
  return isProtocolEnvironment(environment) ? environment : ENVIRONMENT_DEV;
}

function rpcError(code, message, data = {}) {
  const error = new Error(message);
  error.code = code;
  error.data = data;
  error.body = createRpcErrorBody({ code, message, data });
  return error;
}

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

function truthy(value) {
  return ["1", "true", "yes", "on", "enabled"].includes(String(value ?? "").trim().toLowerCase());
}

function trimUrl(value) {
  const text = String(value ?? "").trim();
  return text ? text : null;
}

function ensureTrailingSlash(url) {
  return url.endsWith("/") ? url : `${url}/`;
}

function redactCommand(command) {
  const text = String(command ?? "");
  return text.length <= 200 ? text : `${text.slice(0, 200)}...`;
}

function ensureNoProxyForLocalCloud(url) {
  let host = "";
  try {
    host = new URL(url).hostname;
  } catch {
    return;
  }
  const additions = new Set(["localhost", "127.0.0.1", "::1", host].filter(Boolean));
  for (const key of ["NO_PROXY", "no_proxy"]) {
    const current = String(process.env[key] ?? "");
    const values = new Set(current.split(",").map((item) => item.trim()).filter(Boolean));
    for (const addition of additions) values.add(addition);
    process.env[key] = [...values].join(",");
  }
}
