#!/usr/bin/env node

"use strict";

/**
 * Build script: recomputes all checksums and counts in global-catalog.yaml.
 *
 * Modes:
 *   node scripts/build-catalog-checksums.js           # Update mode (default)
 *   node scripts/build-catalog-checksums.js --verify   # Verify mode (CI gate)
 *
 * Update mode rewrites stale checksums, updates generated_at, and corrects
 * _count fields, the # Total Components header, and the summary table.
 * Verify mode exits non-zero if any checksums are stale.
 *
 * Run via: npm run build:catalog
 */

const fs = require("fs");
const path = require("path");
const crypto = require("crypto");

// Match bin/install.js:27
const CHECKSUM_DISPLAY_LENGTH = 12;

const DEFAULT_CATALOG_PATH = path.resolve(
  __dirname,
  "..",
  "scaffold",
  "global",
  "global-catalog.yaml",
);
const DEFAULT_TEMPLATES_DIR = path.resolve(__dirname, "..", "scaffold");
const PACKAGE_JSON_PATH = path.resolve(__dirname, "..", "package.json");

// ─── Core Functions ─────────────────────────────────────────────────────────

/**
 * Compute truncated SHA-256 checksum of a file.
 *
 * Two normalizations are applied before hashing:
 * 1. CRLF (0x0D 0x0A) → LF (0x0A) so checksums are platform-stable across
 *    Windows checkouts with core.autocrlf=true.
 * 2. `generated_at: "..."` lines are replaced with a fixed value so catalog
 *    checksums are content-stable across rebuild dates.
 */
function computeChecksum(filePath) {
  const raw = fs.readFileSync(filePath);
  const buf = Buffer.alloc(raw.length);
  let j = 0;
  for (let i = 0; i < raw.length; i++) {
    if (raw[i] === 0x0d && raw[i + 1] === 0x0a) continue;
    buf[j++] = raw[i];
  }
  let input = buf.subarray(0, j);
  const GENERATED_AT_RE = /generated_at: "[^"]*"/;
  const str = input.toString("utf8");
  if (GENERATED_AT_RE.test(str)) {
    input = str.replace(/generated_at: "[^"]*"/g, 'generated_at: "DATE"');
  }
  return crypto
    .createHash("sha256")
    .update(input)
    .digest("hex")
    .slice(0, CHECKSUM_DISPLAY_LENGTH);
}

/**
 * Return today's date as YYYY-MM-DD.
 */
function todayISO() {
  return new Date().toISOString().slice(0, 10);
}

/**
 * Extract file/checksum pairs from catalog YAML text using line-by-line scan.
 *
 * Tracks the most recent `file:` value and pairs it with the next `checksum:`
 * line encountered. Resets on `- name:` boundaries (new list item).
 * Naturally skips the skill_metadata section (no file:/checksum: fields).
 *
 * Returns array of { filePath, checksumLineIndex, oldChecksum }.
 * checksumLineIndex is 0-based.
 */
function extractEntries(text) {
  const lines = text.split("\n");
  const entries = [];
  let currentFile = null;

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    const trimmed = line.trim();

    // New list item resets tracked file
    if (trimmed.startsWith("- name:")) {
      currentFile = null;
      continue;
    }

    // Track file: value
    const fileMatch = trimmed.match(/^file:\s*"(.+)"/);
    if (fileMatch) {
      currentFile = fileMatch[1];
      continue;
    }

    // Pair checksum: with tracked file
    const checksumMatch = trimmed.match(/^checksum:\s*"([a-f0-9]+)"/);
    if (checksumMatch && currentFile) {
      entries.push({
        filePath: currentFile,
        checksumLineIndex: i,
        oldChecksum: checksumMatch[1],
      });
      currentFile = null;
    }
  }

  return entries;
}

/**
 * Count `- name:` entries per top-level category section.
 *
 * Walks the lines looking for unindented category keys (e.g. `root_files:`,
 * `agents:`). Within each category, counts `- name:` entries until the next
 * top-level key or EOF. Skips non-component sections (catalog_version,
 * generated_at, standards, skill_metadata, etc.).
 *
 * Returns a Map<string, { count: number, countLineIndex: number|null }>.
 */
function countCategoryItems(lines) {
  const NON_COMPONENT_KEYS = new Set([
    "catalog_version",
    "claude_code_standards_version",
    "template_version",
    "generated_at",
    "standards",
  ]);
  const categories = new Map();
  let currentCategory = null;

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];

    // Top-level key: no leading whitespace, ends with colon
    const topMatch = line.match(/^([a-z_]+):\s*$/);
    if (topMatch) {
      const key = topMatch[1];
      if (!NON_COMPONENT_KEYS.has(key)) {
        currentCategory = key;
        categories.set(key, { count: 0, countLineIndex: null });
      } else {
        currentCategory = null;
      }
      continue;
    }

    if (!currentCategory) continue;

    // Track _count: line index for later replacement
    const countMatch = line.match(/^ {2}_count:\s*(\d+)/);
    if (countMatch) {
      categories.get(currentCategory).countLineIndex = i;
      continue;
    }

    // Count items by `- name:` entries
    if (line.trim().startsWith("- name:")) {
      categories.get(currentCategory).count++;
    }
  }

  return categories;
}

/**
 * Update _count fields, # Total Components header, and summary table.
 *
 * Mutates the lines array in-place. Returns the number of count corrections.
 */
function updateCounts(lines, categories) {
  let corrections = 0;
  let grandTotal = 0;

  // Update _count: fields per category
  for (const [, info] of categories) {
    grandTotal += info.count;
    if (info.countLineIndex !== null) {
      const oldLine = lines[info.countLineIndex];
      const oldCount = oldLine.match(/_count:\s*(\d+)/);
      if (oldCount && parseInt(oldCount[1], 10) !== info.count) {
        lines[info.countLineIndex] = oldLine.replace(
          /_count:\s*\d+/,
          `_count: ${info.count}`,
        );
        corrections++;
      }
    }
  }

  // Update # Total Components header
  for (let i = 0; i < lines.length; i++) {
    const totalMatch = lines[i].match(/^# Total Components:\s*(\d+)/);
    if (totalMatch && parseInt(totalMatch[1], 10) !== grandTotal) {
      lines[i] = `# Total Components: ${grandTotal}`;
      corrections++;
    }
  }

  // Update summary table rows: `# category_name  |  NNN |`
  for (let i = 0; i < lines.length; i++) {
    const rowMatch = lines[i].match(/^# (\w+)\s+\|\s+(\d+)\s+\|/);
    if (rowMatch) {
      const cat = rowMatch[1];
      const oldCount = parseInt(rowMatch[2], 10);
      if (cat === "TOTAL") {
        if (oldCount !== grandTotal) {
          lines[i] = lines[i].replace(
            /\|\s+\d+\s+\|/,
            `|   ${String(grandTotal).padStart(3)} |`,
          );
          corrections++;
        }
      } else if (categories.has(cat)) {
        const newCount = categories.get(cat).count;
        if (oldCount !== newCount) {
          lines[i] = lines[i].replace(
            /\|\s+\d+\s+\|/,
            `|   ${String(newCount).padStart(3)} |`,
          );
          corrections++;
        }
      }
    }
  }

  return corrections;
}

/**
 * Main processing logic.
 *
 * @param {object} opts
 * @param {boolean} opts.verify - If true, verify only (exit non-zero on mismatch)
 * @param {string}  opts.catalogPath - Path to global-catalog.yaml
 * @param {string}  opts.templatesDir - Path to templates/ directory
 * @param {string}  opts.packageJsonPath - Path to package.json (for version sync)
 * @returns {{ changed: number, total: number, mismatches: string[], countCorrections: number, versionSynced: boolean }}
 */
function processChecksums({
  verify = false,
  catalogPath,
  templatesDir,
  packageJsonPath,
} = {}) {
  catalogPath = catalogPath || DEFAULT_CATALOG_PATH;
  templatesDir = templatesDir || DEFAULT_TEMPLATES_DIR;
  packageJsonPath = packageJsonPath || PACKAGE_JSON_PATH;

  const text = fs.readFileSync(catalogPath, "utf8");
  const entries = extractEntries(text);
  const lines = text.split("\n");

  const mismatches = [];
  let changed = 0;

  for (const entry of entries) {
    const fullPath = path.join(templatesDir, entry.filePath);

    if (!fs.existsSync(fullPath)) {
      console.error(`  [WARN] File not found: ${entry.filePath}`);
      continue;
    }

    const actual = computeChecksum(fullPath);

    if (actual !== entry.oldChecksum) {
      mismatches.push(entry.filePath);
      changed++;

      if (!verify) {
        // Replace checksum in-place
        lines[entry.checksumLineIndex] = lines[entry.checksumLineIndex].replace(
          `"${entry.oldChecksum}"`,
          `"${actual}"`,
        );
      }
    }
  }

  // Update counts (_count fields, header, summary table)
  const categories = countCategoryItems(lines);
  const countCorrections = verify ? 0 : updateCounts(lines, categories);

  // Sync catalog_version and template_version from package.json
  let versionSynced = false;
  const pkgVersion = JSON.parse(
    fs.readFileSync(packageJsonPath, "utf8"),
  ).version;

  for (let i = 0; i < lines.length; i++) {
    // YAML data fields
    const catalogVersionMatch = lines[i].match(/^catalog_version:\s*"([^"]+)"/);
    if (catalogVersionMatch && catalogVersionMatch[1] !== pkgVersion) {
      if (verify) {
        versionSynced = true; // flag mismatch for verify mode
      } else {
        lines[i] = `catalog_version: "${pkgVersion}"`;
        versionSynced = true;
      }
    }
    const templateVersionMatch = lines[i].match(
      /^template_version:\s*"([^"]+)"/,
    );
    if (templateVersionMatch && templateVersionMatch[1] !== pkgVersion) {
      if (verify) {
        versionSynced = true;
      } else {
        lines[i] = `template_version: "${pkgVersion}"`;
        versionSynced = true;
      }
    }
    // Comment header fields
    if (lines[i].startsWith("# Version:")) {
      const headerVersion = lines[i].replace("# Version:", "").trim();
      if (headerVersion !== pkgVersion) {
        if (!verify) {
          lines[i] = `# Version: ${pkgVersion}`;
        }
        versionSynced = true;
      }
    }
    if (lines[i].startsWith("# Template Version:")) {
      const headerVersion = lines[i].replace("# Template Version:", "").trim();
      if (headerVersion !== pkgVersion) {
        if (!verify) {
          lines[i] = `# Template Version: ${pkgVersion}`;
        }
        versionSynced = true;
      }
    }
  }

  // Write file if there were any changes (update mode only)
  if (!verify && (changed > 0 || countCorrections > 0 || versionSynced)) {
    const today = todayISO();
    for (let i = 0; i < lines.length; i++) {
      if (lines[i].startsWith("# Generated:")) {
        lines[i] = `# Generated: ${today}`;
      }
      if (lines[i].startsWith("generated_at:")) {
        lines[i] = `generated_at: "${today}"`;
      }
    }

    fs.writeFileSync(catalogPath, lines.join("\n"), "utf8");
  }

  return {
    changed,
    total: entries.length,
    mismatches,
    countCorrections,
    versionSynced,
  };
}

// ─── CLI ────────────────────────────────────────────────────────────────────

if (require.main === module) {
  const verify = process.argv.includes("--verify");

  console.log(
    `\n  Catalog checksum ${verify ? "verification" : "update"}...\n`,
  );

  const result = processChecksums({ verify });

  if (verify) {
    const failures = [];
    if (result.changed > 0) {
      failures.push(`${result.changed} stale checksum(s)`);
      for (const f of result.mismatches) {
        console.error(`    - ${f}`);
      }
    }
    if (result.versionSynced) {
      failures.push(
        "catalog_version/template_version out of sync with package.json",
      );
    }
    if (failures.length > 0) {
      console.error(`  [FAIL] ${failures.join("; ")}`);
      console.error("\n  Run: node scripts/build-catalog-checksums.js\n");
      process.exit(1);
    } else {
      console.log(
        `  [OK] All ${result.total} checksums verified, versions aligned.\n`,
      );
    }
  } else {
    if (result.changed > 0) {
      console.log(
        `  Updated ${result.changed} of ${result.total} checksum(s).`,
      );
      for (const f of result.mismatches) {
        console.log(`    - ${f}`);
      }
      console.log("");
    } else {
      console.log(`  All ${result.total} checksums are current.`);
    }
    if (result.countCorrections > 0) {
      console.log(
        `  Updated ${result.countCorrections} stale count(s) in catalog summary.`,
      );
    }
    if (result.versionSynced) {
      console.log(
        "  Synced catalog_version and template_version to package.json.",
      );
    }
    console.log("");
  }
}

module.exports = {
  computeChecksum,
  extractEntries,
  countCategoryItems,
  updateCounts,
  processChecksums,
  todayISO,
  CHECKSUM_DISPLAY_LENGTH,
};
