#!/usr/bin/env node

import { execSync } from "node:child_process";
import { readFileSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import process from "node:process";
import url from "node:url";

const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "..");
const SRC_ROOT = path.join(ROOT, "src");
const GLOBALS_PATH = path.join(SRC_ROOT, "styles", "globals.css");
const TOKENS_PATH = path.join(SRC_ROOT, "styles", "workspace-color-tokens.ts");
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
const ARBITRARY_COLOR_RE =
  /\b(?:bg|text|border|ring|from|to|via|fill|stroke)-\[[^\]]+\]/g;
const NAMED_COLOR_RE =
  /\b(?:bg|text|border|ring|from|to|via|fill|stroke)-(?:white|black)(?:\/\d+)?\b/g;
const EXCLUDED_HEX_FILES = new Set([GLOBALS_PATH, TOKENS_PATH]);
const MODE = process.argv.includes("--mode=guard") ? "guard" : "audit";

function walkFiles(dir) {
  const result = [];
  const queue = [dir];
  while (queue.length > 0) {
    const current = queue.pop();
    if (!current) continue;
    for (const entry of readdirSync(current)) {
      const fullPath = path.join(current, entry);
      const stats = statSync(fullPath);
      if (stats.isDirectory()) {
        queue.push(fullPath);
      } else if (stats.isFile()) {
        result.push(fullPath);
      }
    }
  }
  return result;
}

function collectMatchesInContent(content, regex, includeLine) {
  const findings = [];
  const lines = content.split(/\r?\n/);
  lines.forEach((line, index) => {
    if (!includeLine(index + 1, line)) return;
    regex.lastIndex = 0;
    for (const match of line.matchAll(regex)) {
      findings.push({ line: index + 1, match: match[0] });
    }
  });
  return findings;
}

function scanFullSource() {
  const files = walkFiles(SRC_ROOT);
  const report = {
    hex: [],
    arbitrary: [],
    named: [],
  };

  for (const file of files) {
    if (!/\.(cjs|mjs|js|jsx|ts|tsx|css|scss|sass|less|mdx?)$/.test(file)) {
      continue;
    }
    const content = readFileSync(file, "utf8");
    if (!EXCLUDED_HEX_FILES.has(file)) {
      const hexFindings = collectMatchesInContent(content, HEX_RE, () => true);
      for (const finding of hexFindings) {
        report.hex.push({ file, ...finding });
      }
    }
    const arbitraryFindings = collectMatchesInContent(
      content,
      ARBITRARY_COLOR_RE,
      () => true,
    );
    for (const finding of arbitraryFindings) {
      report.arbitrary.push({ file, ...finding });
    }
    const namedFindings = collectMatchesInContent(content, NAMED_COLOR_RE, () => true);
    for (const finding of namedFindings) {
      report.named.push({ file, ...finding });
    }
  }

  return report;
}

function parseDiffAddedLines() {
  const addedLines = new Map();

  const addLine = (file, lineNo, content) => {
    if (!addedLines.has(file)) addedLines.set(file, []);
    addedLines.get(file).push({ line: lineNo, content });
  };

  let diffText = "";
  try {
    diffText = execSync("git diff --no-color --unified=0 -- frontend/src", {
      cwd: ROOT,
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"],
    });
  } catch {
    diffText = "";
  }

  let currentFile = null;
  let newLineNo = 0;

  for (const line of diffText.split(/\r?\n/)) {
    if (line.startsWith("+++ b/")) {
      currentFile = path.join(ROOT, line.slice(6));
      continue;
    }
    if (line.startsWith("@@")) {
      const match = line.match(/\+(\d+)(?:,(\d+))?/);
      if (!match) continue;
      newLineNo = Number(match[1]);
      continue;
    }
    if (!currentFile) continue;
    if (line.startsWith("+") && !line.startsWith("+++")) {
      addLine(currentFile, newLineNo, line.slice(1));
      newLineNo += 1;
      continue;
    }
    if (!line.startsWith("-")) {
      newLineNo += 1;
    }
  }

  let untracked = "";
  try {
    untracked = execSync("git ls-files --others --exclude-standard frontend/src", {
      cwd: ROOT,
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"],
    });
  } catch {
    untracked = "";
  }

  for (const relativeFile of untracked.split(/\r?\n/).filter(Boolean)) {
    const fullFile = path.join(ROOT, relativeFile);
    if (!/\.(cjs|mjs|js|jsx|ts|tsx|css|scss|sass|less|mdx?)$/.test(fullFile)) {
      continue;
    }
    const lines = readFileSync(fullFile, "utf8").split(/\r?\n/);
    lines.forEach((content, idx) => addLine(fullFile, idx + 1, content));
  }

  return addedLines;
}

function scanAddedViolations() {
  const addedLines = parseDiffAddedLines();
  const report = {
    hex: [],
    arbitrary: [],
    named: [],
  };

  for (const [file, lines] of addedLines.entries()) {
    for (const { line, content } of lines) {
      if (!EXCLUDED_HEX_FILES.has(file)) {
        HEX_RE.lastIndex = 0;
        for (const match of content.matchAll(HEX_RE)) {
          report.hex.push({ file, line, match: match[0] });
        }
      }
      ARBITRARY_COLOR_RE.lastIndex = 0;
      for (const match of content.matchAll(ARBITRARY_COLOR_RE)) {
        report.arbitrary.push({ file, line, match: match[0] });
      }
      NAMED_COLOR_RE.lastIndex = 0;
      for (const match of content.matchAll(NAMED_COLOR_RE)) {
        report.named.push({ file, line, match: match[0] });
      }
    }
  }

  return report;
}

async function validateTokenRegistry() {
  const moduleUrl = url.pathToFileURL(TOKENS_PATH).href;
  const tokenModule = await import(moduleUrl);
  const tokens = tokenModule.WORKSPACE_COLOR_TOKENS ?? {};
  const entries = Object.entries(tokens);
  const errors = [];

  const lightSeen = new Map();
  const darkSeen = new Map();

  for (const [name, value] of entries) {
    if (!/^ws-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
      errors.push(`invalid token name "${name}"`);
    }
    const light = String(value.light ?? "").toLowerCase();
    const dark = String(value.dark ?? "").toLowerCase();
    if (!/^#[0-9a-f]{6,8}$/.test(light)) {
      errors.push(`invalid light color for ${name}: ${value.light}`);
    }
    if (!/^#[0-9a-f]{6,8}$/.test(dark)) {
      errors.push(`invalid dark color for ${name}: ${value.dark}`);
    }
    if (lightSeen.has(light)) {
      errors.push(
        `duplicate light color mapping: ${light} used by ${lightSeen.get(light)} and ${name}`,
      );
    } else {
      lightSeen.set(light, name);
    }
    if (darkSeen.has(dark)) {
      errors.push(
        `duplicate dark color mapping: ${dark} used by ${darkSeen.get(dark)} and ${name}`,
      );
    } else {
      darkSeen.set(dark, name);
    }
  }

  return {
    entries,
    errors,
  };
}

function collectWsVarsFromBlocks(css, selectorPattern) {
  const vars = new Set();
  const blockRegex = /([^{}]+)\{([^{}]*)\}/g;
  for (const block of css.matchAll(blockRegex)) {
    const selector = block[1]?.trim() ?? "";
    const body = block[2] ?? "";
    if (!selectorPattern.test(selector)) continue;
    for (const match of body.matchAll(/--ws-color-([0-9a-z-]+)\s*:/g)) {
      vars.add(`ws-${match[1]}`);
    }
  }
  return vars;
}

function validateGlobalsCoverage(tokenEntries) {
  const css = readFileSync(GLOBALS_PATH, "utf8");
  const rootVars = collectWsVarsFromBlocks(css, /(^|,)\s*:root(\s|,|$)/);
  const darkVars = collectWsVarsFromBlocks(css, /(^|,)\s*\.dark(\s|,|$)/);
  const inlineVars = new Set(
    [...css.matchAll(/--color-ws-([0-9a-z-]+)\s*:/g)].map((match) => `ws-${match[1]}`),
  );
  const tokenNames = new Set(tokenEntries.map(([name]) => name));

  const errors = [];
  for (const tokenName of tokenNames) {
    if (!rootVars.has(tokenName)) {
      errors.push(`missing :root ws variable for ${tokenName}`);
    }
    if (!darkVars.has(tokenName)) {
      errors.push(`missing .dark ws variable for ${tokenName}`);
    }
    if (!inlineVars.has(tokenName)) {
      errors.push(`missing @theme inline mapping for ${tokenName}`);
    }
  }

  return {
    rootCount: rootVars.size,
    darkCount: darkVars.size,
    inlineCount: inlineVars.size,
    errors,
  };
}

function printFindings(label, findings) {
  if (findings.length === 0) return;
  console.log(label);
  for (const finding of findings) {
    const relativePath = path.relative(ROOT, finding.file);
    console.log(`  - ${relativePath}:${finding.line} ${finding.match}`);
  }
}

async function main() {
  const fullScan = scanFullSource();
  const addedViolations = scanAddedViolations();
  const tokenValidation = await validateTokenRegistry();
  const globalsValidation = validateGlobalsCoverage(tokenValidation.entries);

  console.log(`[color-guard] mode=${MODE}`);
  console.log(
    `[summary] full-scan hex=${fullScan.hex.length} arbitrary=${fullScan.arbitrary.length} named=${fullScan.named.length}`,
  );
  console.log(
    `[summary] added-violations hex=${addedViolations.hex.length} arbitrary=${addedViolations.arbitrary.length} named=${addedViolations.named.length}`,
  );
  console.log(
    `[summary] ws-vars root=${globalsValidation.rootCount} dark=${globalsValidation.darkCount} inline=${globalsValidation.inlineCount}`,
  );

  printFindings("[added] hex violations", addedViolations.hex);
  printFindings("[added] arbitrary color violations", addedViolations.arbitrary);
  printFindings("[added] named color violations", addedViolations.named);

  const semanticErrors = [...tokenValidation.errors, ...globalsValidation.errors];
  if (semanticErrors.length > 0) {
    console.log("[semantic] token/globals errors");
    for (const error of semanticErrors) {
      console.log(`  - ${error}`);
    }
  }

  const hasViolations =
    addedViolations.hex.length > 0 ||
    addedViolations.arbitrary.length > 0 ||
    addedViolations.named.length > 0 ||
    semanticErrors.length > 0;

  if (MODE === "guard" && hasViolations) {
    console.error("[color-guard] guard failed");
    process.exit(1);
  }

  console.log("[color-guard] done");
}

await main();
