#!/usr/bin/env node
/**
 * emit-retention-policy.mjs — Collection retention policy emitter.
 *
 * Walks packages/*\/src/integrations/cms/collections/*.ts, validates that each
 * collection declares custom.retention.purgeSchedule, and emits a deterministic
 * YAML retention schedule at compliance/retention-policy.yml.
 *
 * Usage:
 *   node scripts/compliance/emit-retention-policy.mjs              # write compliance/retention-policy.yml
 *   node scripts/compliance/emit-retention-policy.mjs --print      # write to stdout
 *   node scripts/compliance/emit-retention-policy.mjs --check      # diff vs committed file; exit 1 on mismatch
 */

import fs from "node:fs";
import path from "node:path";

import {
  findCollectionFiles,
  parseCollectionFile,
  unifiedDiff,
  REPO_ROOT,
} from "./emit-data-map.mjs";

export { findCollectionFiles, parseCollectionFile, unifiedDiff, REPO_ROOT };

export const OUTPUT_PATH = "compliance/retention-policy.yml";

// ---- Validation ----

/**
 * Validate that every collection has custom.retention.purgeSchedule.
 * Returns an array of error messages (empty array = valid).
 */
export function validateRetentionPolicy(rawCollections) {
  const errors = [];
  for (const coll of rawCollections) {
    if (!coll || typeof coll.slug !== "string") continue;
    const retention = coll.custom && coll.custom.retention;
    if (!retention || typeof retention.purgeSchedule !== "string") {
      errors.push(
        `Collection "${coll.slug}" is missing custom.retention.purgeSchedule.\n` +
          `  Hint: add to your collection config:\n` +
          `    custom: { retention: { purgeSchedule: "daily" | "weekly" | "monthly" } }`,
      );
    }
  }
  return errors;
}

// ---- Retention policy builder ----

/**
 * Build the retention policy map from parsed collections.
 * Returns Record<slug, { slug, purgeSchedule, activeRetention?, postDeletion?, coldArchive? }>
 */
export function buildRetentionPolicy(rawCollections) {
  const map = {};
  for (const coll of rawCollections) {
    if (!coll || typeof coll.slug !== "string") continue;
    const retention = (coll.custom && coll.custom.retention) || {};
    const entry = { slug: coll.slug, purgeSchedule: retention.purgeSchedule };

    if (retention.activeRetention) {
      entry.activeRetention = { ...retention.activeRetention };
    }
    if (retention.coldArchive) {
      entry.coldArchive = { ...retention.coldArchive };
    }
    if (retention.postDeletion) {
      entry.postDeletion = { ...retention.postDeletion };
    }

    map[coll.slug] = entry;
  }
  return map;
}

// ---- YAML serialization ----

const YAML_HEADER = [
  "# compliance/retention-policy.yml — Collection retention schedules",
  "# Generated by scripts/compliance/emit-retention-policy.mjs — do not edit manually.",
  "# Run `pnpm compliance:retention-policy` to regenerate.",
].join("\n");

/** Quote a YAML string scalar only when necessary. */
function yamlStr(s) {
  if (typeof s !== "string") return String(s);
  if (
    s === "" ||
    ["true", "false", "null", "yes", "no", "on", "off"].includes(s) ||
    /[[{},:#&*!|>'"%@`\]]/u.test(s) ||
    /^\s|\s$/.test(s)
  ) {
    return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
  }
  return s;
}

/**
 * Render an optional retention sub-object (activeRetention, postDeletion, coldArchive).
 * Keys are sorted alphabetically for determinism.
 */
function renderRetentionBlock(label, obj, indent) {
  let out = `${indent}${label}:\n`;
  for (const key of Object.keys(obj).sort()) {
    out += `${indent}  ${key}: ${yamlStr(obj[key])}\n`;
  }
  return out;
}

/**
 * Render the retention policy as deterministic YAML.
 * Collections sorted by slug; optional sub-objects rendered alphabetically by key.
 */
export function renderRetentionPolicyYaml(policyMap) {
  let yaml = YAML_HEADER + "\n";
  yaml += "collections:\n";

  for (const slug of Object.keys(policyMap).sort()) {
    const entry = policyMap[slug];
    yaml += `  ${yamlStr(slug)}:\n`;

    if (entry.activeRetention) {
      yaml += renderRetentionBlock(
        "activeRetention",
        entry.activeRetention,
        "    ",
      );
    }
    if (entry.coldArchive) {
      yaml += renderRetentionBlock("coldArchive", entry.coldArchive, "    ");
    }
    if (entry.postDeletion) {
      yaml += renderRetentionBlock("postDeletion", entry.postDeletion, "    ");
    }

    yaml += `    purgeSchedule: ${yamlStr(entry.purgeSchedule)}\n`;
    yaml += `    slug: ${yamlStr(slug)}\n`;
  }

  return yaml;
}

// ---- CLI ----

function parseArgs(argv) {
  const out = { mode: "write" }; // 'write' | 'print' | 'check'
  for (let i = 2; i < argv.length; i++) {
    if (argv[i] === "--print") out.mode = "print";
    else if (argv[i] === "--check") out.mode = "check";
    else if (argv[i] === "--help" || argv[i] === "-h") {
      console.log(
        [
          "Usage: node scripts/compliance/emit-retention-policy.mjs [--print | --check]",
          "  (default): write compliance/retention-policy.yml",
          "  --print:   write YAML to stdout",
          "  --check:   diff vs committed file; exit 1 on mismatch",
        ].join("\n"),
      );
      process.exit(0);
    }
  }
  return out;
}

function main() {
  const args = parseArgs(process.argv);
  const repoRoot = process.cwd();

  const files = findCollectionFiles(repoRoot);
  const rawCollections = files
    .map((f) => parseCollectionFile(f))
    .filter(Boolean);

  const errors = validateRetentionPolicy(rawCollections);
  if (errors.length > 0) {
    for (const err of errors) {
      process.stderr.write(`[compliance:retention-policy] ${err}\n`);
    }
    process.exit(1);
  }

  const policyMap = buildRetentionPolicy(rawCollections);
  const yaml = renderRetentionPolicyYaml(policyMap);

  const outPath = path.resolve(repoRoot, OUTPUT_PATH);

  if (args.mode === "print") {
    process.stdout.write(yaml);
    return;
  }

  if (args.mode === "check") {
    if (!fs.existsSync(outPath)) {
      process.stderr.write(
        `[compliance:retention-policy] --check: no committed file at ${OUTPUT_PATH}\n` +
          `Run \`pnpm compliance:retention-policy\` to generate it first.\n`,
      );
      process.exit(1);
    }
    const committed = fs.readFileSync(outPath, "utf8");
    const diff = unifiedDiff(committed, yaml, OUTPUT_PATH);
    if (diff === null) {
      console.log(
        `✓ compliance:retention-policy — ${OUTPUT_PATH} is up to date`,
      );
      process.exit(0);
    }
    process.stderr.write(
      `✗ compliance:retention-policy — ${OUTPUT_PATH} is out of date:\n${diff}\n`,
    );
    process.exit(1);
  }

  // Default: write to file
  const dir = path.dirname(outPath);
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
  fs.writeFileSync(outPath, yaml, "utf8");
  console.log(`✓ compliance:retention-policy — wrote ${OUTPUT_PATH}`);
}

if (import.meta.url === `file://${process.argv[1]}`) {
  main();
}
