#!/usr/bin/env node

import fs from "node:fs";
import path from "node:path";
import { isDirectRunUrl } from "./lib/direct-run.mjs";

const SHARED_LOCATION_PREFIX = "../shared/OpenClawKit/Sources/";
const SHARED_SOURCE_ROOT = "apps/shared/OpenClawKit/Sources";
const BARE_PERIPHERY_IGNORE_COMMENT = /\/\/\/?\s*periphery:ignore(?![:\w])/;

function requireValue(args, index, option) {
  const value = args[index + 1];
  if (!value || value.startsWith("--")) {
    throw new Error(`${option} requires a value`);
  }
  return value;
}

export function parseArgs(args) {
  const options = {};
  for (let index = 0; index < args.length; index += 1) {
    const option = args[index];
    switch (option) {
      case "--ios-results":
        options.iosResults = requireValue(args, index, option);
        index += 1;
        break;
      case "--ios-status":
        options.iosStatus = requireValue(args, index, option);
        index += 1;
        break;
      case "--macos-results":
        options.macosResults = requireValue(args, index, option);
        index += 1;
        break;
      case "--macos-status":
        options.macosStatus = requireValue(args, index, option);
        index += 1;
        break;
      case "--output":
        options.output = requireValue(args, index, option);
        index += 1;
        break;
      default:
        throw new Error(`unknown option: ${option}`);
    }
  }

  for (const key of ["iosResults", "iosStatus", "macosResults", "macosStatus", "output"]) {
    if (!options[key]) {
      throw new Error(`missing required option: ${key}`);
    }
  }
  return options;
}

export function validateFindings(value, label) {
  if (!Array.isArray(value)) {
    throw new Error(`${label} results must be a JSON array`);
  }

  return value.map((finding, index) => {
    if (!finding || typeof finding !== "object" || Array.isArray(finding)) {
      throw new Error(`${label} finding ${index} must be an object`);
    }
    if (
      !Array.isArray(finding.ids) ||
      finding.ids.length === 0 ||
      finding.ids.some((id) => typeof id !== "string" || id.length === 0)
    ) {
      throw new Error(`${label} finding ${index} has no usable Swift USR`);
    }
    if (
      typeof finding.location !== "string" ||
      !finding.location.startsWith(SHARED_LOCATION_PREFIX)
    ) {
      throw new Error(`${label} finding ${index} is outside shared OpenClawKit sources`);
    }
    if (typeof finding.kind !== "string" || typeof finding.name !== "string") {
      throw new Error(`${label} finding ${index} is missing its kind or name`);
    }
    return finding;
  });
}

export function intersectFindings(iosFindings, macosFindings) {
  const ios = validateFindings(iosFindings, "iOS");
  const macos = validateFindings(macosFindings, "macOS");
  const macosIds = new Set(macos.flatMap((finding) => finding.ids));

  return ios
    .filter((finding) => finding.ids.some((id) => macosIds.has(id)))
    .toSorted((left, right) =>
      [left.location, left.kind, left.name]
        .join("\0")
        .localeCompare([right.location, right.kind, right.name].join("\0")),
    );
}

export function parseRepoLocation(location) {
  const match = /^(.*):(\d+):(\d+)$/.exec(location);
  if (!match || !match[1].startsWith("../shared/")) {
    throw new Error(`invalid shared Periphery location: ${location}`);
  }
  return {
    column: match[3],
    file: `apps/shared/${match[1].slice("../shared/".length)}`,
    line: match[2],
  };
}

export function filterIgnoredFindings(findings, repoRoot = process.cwd()) {
  const sourceRoot = path.resolve(repoRoot, SHARED_SOURCE_ROOT);
  const sourceLines = new Map();

  return findings.filter((finding) => {
    const location = parseRepoLocation(finding.location);
    const sourceFile = path.resolve(repoRoot, location.file);
    const relativeSource = path.relative(sourceRoot, sourceFile);
    if (
      !relativeSource ||
      path.isAbsolute(relativeSource) ||
      relativeSource === ".." ||
      relativeSource.startsWith(`..${path.sep}`)
    ) {
      throw new Error(`invalid shared Periphery source path: ${location.file}`);
    }
    let lines = sourceLines.get(sourceFile);
    if (!lines) {
      lines = fs.readFileSync(sourceFile, "utf8").split(/\r?\n/);
      sourceLines.set(sourceFile, lines);
    }

    const declarationIndex = Number(location.line) - 1;
    if (declarationIndex < 0 || declarationIndex >= lines.length) {
      throw new Error(`invalid shared Periphery source line: ${finding.location}`);
    }

    return ![lines[declarationIndex - 1], lines[declarationIndex]].some(
      (line) => typeof line === "string" && BARE_PERIPHERY_IGNORE_COMMENT.test(line),
    );
  });
}

export function escapeCommandData(value) {
  return String(value ?? "")
    .replaceAll("%", "%25")
    .replaceAll("\r", "%0D")
    .replaceAll("\n", "%0A");
}

export function escapeCommandProperty(value) {
  return escapeCommandData(value).replaceAll(":", "%3A").replaceAll(",", "%2C");
}

export function formatAnnotation(finding) {
  const location = parseRepoLocation(finding.location);
  const title = `${finding.kind || "Unused code"} ${finding.name}`.trim();
  return `::error file=${escapeCommandProperty(location.file)},line=${location.line},col=${location.column},title=Dead shared Swift code::${escapeCommandData(title)}`;
}

export function buildSummary(findings) {
  if (findings.length === 0) {
    return [
      "### Shared OpenClawKit Periphery",
      "",
      "No declarations were reported dead by both the iOS and macOS consumer scans.",
    ].join("\n");
  }
  return [
    "### Shared OpenClawKit Periphery",
    "",
    `Found ${findings.length} shared Swift ${findings.length === 1 ? "declaration" : "declarations"} reported dead by both consumer scans.`,
    "",
    "The gate matches Periphery's Swift USRs, not declaration names.",
  ].join("\n");
}

function readStatus(file, label) {
  const raw = fs.readFileSync(file, "utf8").trim();
  if (!/^\d+$/.test(raw)) {
    throw new Error(`${label} Periphery status is invalid`);
  }
  const status = Number(raw);
  if (status !== 0) {
    throw new Error(`${label} Periphery scan exited with status ${status}`);
  }
}

function readFindings(file, label) {
  let parsed;
  try {
    parsed = JSON.parse(fs.readFileSync(file, "utf8"));
  } catch (error) {
    throw new Error(`${label} Periphery results are not valid JSON`, { cause: error });
  }
  return validateFindings(parsed, label);
}

export function run(args, env = process.env) {
  const options = parseArgs(args);
  readStatus(options.iosStatus, "iOS");
  readStatus(options.macosStatus, "macOS");
  const findings = filterIgnoredFindings(
    intersectFindings(
      readFindings(options.iosResults, "iOS"),
      readFindings(options.macosResults, "macOS"),
    ),
  );

  fs.mkdirSync(path.dirname(options.output), { recursive: true });
  fs.writeFileSync(options.output, `${JSON.stringify(findings, null, 2)}\n`);
  for (const finding of findings) {
    console.log(formatAnnotation(finding));
  }

  const summary = buildSummary(findings);
  if (env.GITHUB_STEP_SUMMARY) {
    fs.appendFileSync(env.GITHUB_STEP_SUMMARY, `${summary}\n`);
  } else {
    console.log(summary);
  }
  return findings.length === 0 ? 0 : 1;
}

if (isDirectRunUrl(process.argv[1], import.meta.url)) {
  try {
    process.exitCode = run(process.argv.slice(2));
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    console.error(
      `::error title=Shared Periphery intersection failed::${escapeCommandData(message)}`,
    );
    process.exitCode = 2;
  }
}
