#!/usr/bin/env node

import { execFileSync } from "node:child_process";
import process from "node:process";
import { pathToFileURL } from "node:url";

export function renderUsage() {
  return `Usage:
  node scripts/worktree-hygiene.mjs [--scope <path>]... [--allow-outside-scope] [--fail-outside-scope] [--fail-on-dirty] [--json]

Examples:
  node scripts/worktree-hygiene.mjs --scope docs/ --scope scripts/
  node scripts/worktree-hygiene.mjs --scope apps/web/src/components/timeline/ --scope apps/web/e2e/timeline.spec.ts
  node scripts/worktree-hygiene.mjs --scope docs/ --scope scripts/ --fail-outside-scope
  node scripts/worktree-hygiene.mjs --fail-on-dirty
  node scripts/worktree-hygiene.mjs --scope packages/api/src/router/ --allow-outside-scope
`;
}

export function normalizeScope(scope) {
  return scope.replace(/\\/gu, "/").replace(/^\.\/+/u, "");
}

export function parseArgs(argv) {
  const scopes = [];
  let allowOutsideScope = false;
  let failOutsideScope = false;
  let failOnDirty = false;
  let json = false;
  let help = false;

  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];
    if (arg === "--") {
      continue;
    }
    if (arg === "--scope") {
      const value = argv[index + 1];
      if (!value) {
        throw new Error("--scope requires a path value.");
      }
      scopes.push(normalizeScope(value));
      index += 1;
      continue;
    }
    if (arg === "--allow-outside-scope") {
      allowOutsideScope = true;
      continue;
    }
    if (arg === "--fail-outside-scope") {
      failOutsideScope = true;
      continue;
    }
    if (arg === "--fail-on-dirty") {
      failOnDirty = true;
      continue;
    }
    if (arg === "--json") {
      json = true;
      continue;
    }
    if (arg === "--help" || arg === "-h") {
      help = true;
      continue;
    }
    throw new Error(`Unknown argument: ${arg}`);
  }

  return { scopes, allowOutsideScope, failOutsideScope, failOnDirty, json, help };
}

export function runGit(args) {
  return execFileSync("git", args, {
    encoding: "utf8",
    stdio: ["ignore", "pipe", "pipe"],
  });
}

export function parsePorcelain(output) {
  return output
    .split("\n")
    .filter(Boolean)
    .map((line) => {
      const xy = line.slice(0, 2);
      const rawPath = line.slice(3);
      const renamed = xy.includes("R") || xy.includes("C");
      const path = renamed ? rawPath.split(" -> ").at(-1) ?? rawPath : rawPath;
      return {
        xy,
        path: path.replace(/\\/gu, "/"),
      };
    });
}

export function matchesScope(path, scopes) {
  return scopes.some((scope) => path === scope || path.startsWith(scope.endsWith("/") ? scope : `${scope}/`));
}

export function summarize(entries) {
  const summary = {
    staged: 0,
    unstaged: 0,
    untracked: 0,
  };

  for (const entry of entries) {
    if (entry.xy === "??") {
      summary.untracked += 1;
      continue;
    }
    if (entry.xy[0] && entry.xy[0] !== " ") {
      summary.staged += 1;
    }
    if (entry.xy[1] && entry.xy[1] !== " ") {
      summary.unstaged += 1;
    }
  }

  return summary;
}

export function formatEntries(title, entries) {
  if (entries.length === 0) {
    return `${title}: none`;
  }
  return `${title}:\n${entries.map((entry) => `- ${entry.xy} ${entry.path}`).join("\n")}`;
}

function renderReport({ root, branch, scopes, allowOutsideScope, failOutsideScope, failOnDirty, json, entries }) {
  const inScope = scopes.length === 0 ? entries : entries.filter((entry) => matchesScope(entry.path, scopes));
  const outOfScope = scopes.length === 0 ? [] : entries.filter((entry) => !matchesScope(entry.path, scopes));
  const status = {
    root,
    branch,
    scopes,
    totals: summarize(entries),
    inScope: inScope.length,
    outOfScope: outOfScope.length,
    allowOutsideScope,
    failOutsideScope,
    failOnDirty,
  };

  const lines = [];
  if (json) {
    lines.push(JSON.stringify({ ...status, entries: { inScope, outOfScope } }, null, 2));
  } else {
    lines.push(`Repository: ${root}`);
    lines.push(`Branch: ${branch}`);
    lines.push(`Dirty entries: ${entries.length}`);
    lines.push(
      `Summary: staged=${status.totals.staged}, unstaged=${status.totals.unstaged}, untracked=${status.totals.untracked}`,
    );
    if (scopes.length > 0) {
      lines.push(`Owned scope: ${scopes.join(", ")}`);
      lines.push(formatEntries("In scope", inScope));
      lines.push(formatEntries("Outside scope", outOfScope));
      if (outOfScope.length > 0 && !allowOutsideScope) {
        lines.push("", "Result: outside-scope changes detected.");
      } else if (outOfScope.length > 0) {
        lines.push("", "Result: outside-scope changes detected but tolerated by flag.");
      } else {
        lines.push("", "Result: all dirty files are inside the declared scope.");
      }
    } else {
      lines.push(formatEntries("Dirty files", entries));
    }
  }

  let exitCode = 0;
  if (failOnDirty && entries.length > 0) {
    exitCode = 1;
  }

  if (scopes.length > 0 && outOfScope.length > 0 && (failOutsideScope || !allowOutsideScope)) {
    exitCode = 2;
  }

  return {
    exitCode,
    output: lines.join("\n"),
    status,
    entries: {
      inScope,
      outOfScope,
    },
  };
}

export function inspectWorktreeHygiene(argv, git = runGit) {
  const { scopes, allowOutsideScope, failOutsideScope, failOnDirty, json, help } = parseArgs(argv);
  if (help) {
    return {
      exitCode: 0,
      output: renderUsage(),
      status: null,
      entries: {
        inScope: [],
        outOfScope: [],
      },
    };
  }

  const root = git(["rev-parse", "--show-toplevel"]).trim();
  const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]).trim();
  const entries = parsePorcelain(git(["status", "--short"]));

  return renderReport({
    root,
    branch,
    scopes,
    allowOutsideScope,
    failOutsideScope,
    failOnDirty,
    json,
    entries,
  });
}

function main() {
  const result = inspectWorktreeHygiene(process.argv.slice(2));
  console.log(result.output);
  if (result.exitCode !== 0) {
    process.exit(result.exitCode);
  }
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  try {
    main();
  } catch (error) {
    console.error(error instanceof Error ? error.message : String(error));
    console.error(renderUsage());
    process.exit(1);
  }
}
