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

export const BOUNDARY_CHECKS = [
  ["prompt:snapshots:check", "pnpm", ["prompt:snapshots:check"]],
  ["plugin-extension-boundary", "pnpm", ["run", "lint:plugins:no-extension-imports"]],
  ["lint:tmp:no-random-messaging", "pnpm", ["run", "lint:tmp:no-random-messaging"]],
  ["lint:tmp:channel-agnostic-boundaries", "pnpm", ["run", "lint:tmp:channel-agnostic-boundaries"]],
  ["lint:tmp:tsgo-core-boundary", "pnpm", ["run", "lint:tmp:tsgo-core-boundary"]],
  ["lint:tmp:no-raw-channel-fetch", "pnpm", ["run", "lint:tmp:no-raw-channel-fetch"]],
  ["lint:tmp:no-raw-http2-imports", "pnpm", ["run", "lint:tmp:no-raw-http2-imports"]],
  ["lint:agent:ingress-owner", "pnpm", ["run", "lint:agent:ingress-owner"]],
  [
    "lint:plugins:no-register-http-handler",
    "pnpm",
    ["run", "lint:plugins:no-register-http-handler"],
  ],
  [
    "lint:plugins:no-monolithic-plugin-sdk-entry-imports",
    "pnpm",
    ["run", "lint:plugins:no-monolithic-plugin-sdk-entry-imports"],
  ],
  [
    "lint:plugins:no-extension-src-imports",
    "pnpm",
    ["run", "lint:plugins:no-extension-src-imports"],
  ],
  [
    "lint:plugins:no-extension-test-core-imports",
    "pnpm",
    ["run", "lint:plugins:no-extension-test-core-imports"],
  ],
  [
    "lint:plugins:plugin-sdk-subpaths-exported",
    "pnpm",
    ["run", "lint:plugins:plugin-sdk-subpaths-exported"],
  ],
  ["deps:root-ownership:check", "pnpm", ["deps:root-ownership:check"]],
  ["web-search-provider-boundary", "pnpm", ["run", "lint:web-search-provider-boundaries"]],
  ["web-fetch-provider-boundary", "pnpm", ["run", "lint:web-fetch-provider-boundaries"]],
  [
    "extension-src-outside-plugin-sdk-boundary",
    "pnpm",
    ["run", "lint:extensions:no-src-outside-plugin-sdk"],
  ],
  [
    "extension-plugin-sdk-internal-boundary",
    "pnpm",
    ["run", "lint:extensions:no-plugin-sdk-internal"],
  ],
  [
    "extension-relative-outside-package-boundary",
    "pnpm",
    ["run", "lint:extensions:no-relative-outside-package"],
  ],
  [
    "lint:extensions:telegram-grammy-types",
    "pnpm",
    ["run", "lint:extensions:telegram-grammy-types"],
  ],
  ["lint:ui:no-raw-window-open", "pnpm", ["lint:ui:no-raw-window-open"]],
].map(([label, command, args]) => ({ label, command, args }));

export function resolveConcurrency(value, fallback = 4) {
  const parsed = Number.parseInt(String(value ?? ""), 10);
  if (!Number.isFinite(parsed) || parsed < 1) {
    return fallback;
  }
  return parsed;
}

export function parseShardSpec(value) {
  if (!value) {
    return null;
  }
  const match = String(value).match(/^(\d+)\/(\d+)$/u);
  if (!match) {
    throw new Error(`Invalid shard spec '${value}' (expected N/TOTAL)`);
  }
  const index = Number.parseInt(match[1], 10);
  const count = Number.parseInt(match[2], 10);
  if (
    !Number.isInteger(index) ||
    !Number.isInteger(count) ||
    index < 1 ||
    count < 1 ||
    index > count
  ) {
    throw new Error(`Invalid shard spec '${value}' (expected 1 <= N <= TOTAL)`);
  }
  return { count, index: index - 1, label: `${index}/${count}` };
}

export function selectChecksForShard(checks, shardSpec) {
  const shard = typeof shardSpec === "string" ? parseShardSpec(shardSpec) : shardSpec;
  if (!shard) {
    return checks;
  }
  return checks.filter((_check, index) => index % shard.count === shard.index);
}

export function formatCommand({ command, args }) {
  return [command, ...args].join(" ");
}

function runSingleCheck(check, { cwd, env }) {
  return new Promise((resolve) => {
    const startedAt = performance.now();
    const child = spawn(check.command, check.args, {
      cwd,
      env,
      shell: false,
      stdio: ["ignore", "pipe", "pipe"],
    });
    const chunks = [];

    child.stdout.setEncoding("utf8");
    child.stderr.setEncoding("utf8");
    child.stdout.on("data", (chunk) => chunks.push(chunk));
    child.stderr.on("data", (chunk) => chunks.push(chunk));
    child.on("error", (error) => {
      chunks.push(`${error.stack ?? error.message}\n`);
      resolve({
        check,
        code: 1,
        durationMs: Math.round(performance.now() - startedAt),
        signal: null,
        output: chunks.join(""),
      });
    });
    child.on("close", (code, signal) => {
      resolve({
        check,
        code: code ?? 1,
        durationMs: Math.round(performance.now() - startedAt),
        signal,
        output: chunks.join(""),
      });
    });
  });
}

function formatDuration(ms) {
  if (!Number.isFinite(ms)) {
    return "";
  }
  if (ms < 1000) {
    return `${ms}ms`;
  }
  return `${(ms / 1000).toFixed(1)}s`;
}

function writeGroupedResult(result, output) {
  const success = result.code === 0;
  output.write(`::group::${result.check.label}\n`);
  output.write(`$ ${formatCommand(result.check)}\n`);
  if (result.output) {
    output.write(result.output.endsWith("\n") ? result.output : `${result.output}\n`);
  }
  if (success) {
    output.write(`[ok] ${result.check.label} in ${formatDuration(result.durationMs)}\n`);
  } else {
    const suffix = result.signal ? ` (signal ${result.signal})` : ` (exit ${result.code})`;
    output.write(
      `::error title=${result.check.label} failed::${result.check.label} failed${suffix} after ${formatDuration(result.durationMs)}\n`,
    );
  }
  output.write("::endgroup::\n");
}

function writeTimingSummary(results, output) {
  output.write("Additional boundary check timings:\n");
  for (const result of [...results].toSorted((left, right) => right.durationMs - left.durationMs)) {
    output.write(
      `${result.check.label.padEnd(48)} ${formatDuration(result.durationMs).padStart(8)}\n`,
    );
  }
}

export async function runChecks(
  checks = BOUNDARY_CHECKS,
  { concurrency = 4, cwd = process.cwd(), env = process.env, output = process.stdout } = {},
) {
  const results = Array.from({ length: checks.length });
  let nextIndex = 0;
  let active = 0;

  await new Promise((resolve) => {
    const launch = () => {
      if (nextIndex >= checks.length && active === 0) {
        resolve();
        return;
      }

      while (active < concurrency && nextIndex < checks.length) {
        const index = nextIndex;
        const check = checks[nextIndex++];
        active += 1;
        void runSingleCheck(check, { cwd, env })
          .then((result) => {
            results[index] = result;
          })
          .finally(() => {
            active -= 1;
            launch();
          });
      }
    };

    launch();
  });

  let failures = 0;
  for (const result of results) {
    writeGroupedResult(result, output);
    if (result.code !== 0) {
      failures += 1;
    }
  }
  writeTimingSummary(results, output);
  return failures;
}

function resolveCliShardSpec(args, env) {
  const shardIndex = args.indexOf("--shard");
  if (shardIndex !== -1) {
    return args[shardIndex + 1] ?? "";
  }
  const inlineShard = args.find((arg) => arg.startsWith("--shard="));
  if (inlineShard) {
    return inlineShard.slice("--shard=".length);
  }
  return env.OPENCLAW_ADDITIONAL_BOUNDARY_SHARD ?? "";
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const concurrency = resolveConcurrency(
    process.env.OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY ??
      process.env.OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY,
  );
  const shard = parseShardSpec(resolveCliShardSpec(process.argv.slice(2), process.env));
  const checks = selectChecksForShard(BOUNDARY_CHECKS, shard);
  if (shard) {
    process.stdout.write(
      `Running ${checks.length}/${BOUNDARY_CHECKS.length} additional boundary checks (shard ${shard.label})\n`,
    );
  }
  const failures = await runChecks(checks, { concurrency });
  process.exitCode = failures === 0 ? 0 : 1;
}
