#!/usr/bin/env node
/**
 * Sencho tier-reconciliation: canonical catalog validator
 * Verifies docs/feature-catalog.yaml against the canonical schema and
 * the required cross-field invariant (tier: internal iff availability: internal).
 * Exits 0 on valid, exits 1 with diagnostic lines on invalid.
 */
import yaml from 'js-yaml';
import fs from 'fs';
import path from 'path';

import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CATALOG_FILE = path.resolve(__dirname, '../../docs/feature-catalog.yaml');

const VALID_TIERS = new Set(['community', 'admiral', 'internal']);
const VALID_AVAILABILITY = new Set(['shipped', 'planned', 'internal']);
const VALID_CATEGORIES = new Set([
  'compose-deploy', 'fleet-orchestration', 'security-foundation',
  'automation-operations', 'recovery', 'identity-access',
  'governance', 'assurance', 'internal',
]);

const errors = [];

function error(msg) { errors.push('ERROR: ' + msg); }

function readCatalog() {
  const text = fs.readFileSync(CATALOG_FILE, 'utf8');
  const doc = yaml.load(text, { schema: yaml.CORE_SCHEMA });
  if (!doc || typeof doc !== 'object') {
    throw new Error('catalog is not a YAML mapping');
  }
  return doc;
}

function main() {
  const doc = readCatalog();
  if (!Array.isArray(doc.entries)) {
    console.error('FAIL: catalog has no entries array.');
    process.exit(1);
  }
  const entries = doc.entries;
  const ids = new Set();

  for (const [i, entry] of entries.entries()) {
    const prefix = `entry[${i}].id=${entry?.id ?? '(missing)'}`;

    if (!entry || typeof entry !== 'object') {
      error(`${prefix}: entry is not an object`);
      continue;
    }

    if (!entry.id) error(`${prefix}: missing id`);
    else if (ids.has(entry.id)) error(`${prefix}: duplicate id "${entry.id}"`);
    else ids.add(entry.id);

    if (!entry.name) error(`${prefix}: missing name`);
    if (!VALID_TIERS.has(entry.tier))
      error(`${prefix}: invalid tier "${entry.tier}"; must be one of community/admiral/internal`);
    if (!VALID_AVAILABILITY.has(entry.availability))
      error(`${prefix}: invalid availability "${entry.availability}"; must be one of shipped/planned/internal`);

    // Cross-field invariant: tier: internal iff availability: internal
    if (entry.tier === 'internal' && entry.availability !== 'internal')
      error(`${prefix}: tier: internal requires availability: internal`);
    if (entry.tier !== 'internal' && entry.availability === 'internal')
      error(`${prefix}: non-internal tier requires non-internal availability`);

    if (entry.availability === 'planned') {
      if (!entry.publicRoadmapKey) error(`${prefix}: planned entry must have publicRoadmapKey`);
    }

    if (!VALID_CATEGORIES.has(entry.category))
      error(`${prefix}: unknown category "${entry.category}"`);

    // Internal-only fields must not leak into committed catalog.
    // The canonical file IS public, so we enforce: no linear, no evidence with internal identifiers,
    // no internalNote. Public-name fields only.
    if (entry.linear) {
      // Reject any internal Linear identifier in committed file.
      if (/SEN-[0-9]/.test(String(entry.linear)))
        error(`${prefix}: committed catalog contains internal Linear identifier in linear field ("${entry.linear}"); use publicRoadmapKey instead`);
    }
    // No evidence field allowed in canonical committed file (evidence stays internal).
    if (entry.evidence)
      error(`${prefix}: evidence field must not appear in committed canonical catalog (use Linear/non-public record for evidence); got: ${entry.evidence}`);
    if (entry.internalNote)
      error(`${prefix}: internalNote field must not appear in committed canonical catalog`);
  }

  if (errors.length === 0) {
    console.log(`VALID: catalog has ${entries.length} entries; all invariants pass.`);
    process.exit(0);
  } else {
    for (const msg of errors) console.error(msg);
    console.error(`FAIL: ${errors.length} error(s) found.`);
    process.exit(1);
  }
}

try {
  main();
} catch (e) {
  console.error('FAIL: ' + (e.message || e));
  process.exit(1);
}
