#!/usr/bin/env node
/**
 * validate-card-baseline.js — profile-aware validator for backlog cards.
 *
 * Why this exists (v4.35.0): `/new` and `/new2` consume cards type-blind and scale
 * per-card review depth on `review_profile`. A card that drifts off the universal
 * baseline (e.g. a CHORE with no `review_profile` or no `scope`) silently degrades
 * that pipeline. This validator is the machine-checkable enforcement of the SSOT
 * field-state matrix in `framework/agents/card-schema.md`.
 *
 * It is PROFILE-AWARE — a flat "every field required, non-empty" rule would wrongly
 * reject valid cards (an epic has `owner_agent: ""` and `files_likely_touched: []`;
 * a standalone CHORE has no `scope_boundaries`/`links.prd`). It detects epic / child /
 * standalone and applies that profile's column.
 *
 * SSOT, never embedded: the field-state matrix is PARSED from card-schema.md and the
 * `owner_agent`/`status` enums from REGISTRY.md, so the validator cannot drift from
 * the docs it enforces.
 *
 * Usage:
 *   node framework/scripts/validate-card-baseline.js <card.yml> [<card2.yml> ...]
 * Exit 0 = all valid; exit 1 = at least one card has errors (printed per-field).
 *
 * Module API (for CI self-tests in scripts/check-card-baseline.js):
 *   const { validateCard, detectProfile, loadSchema, loadEnums } = require(...)
 */
'use strict';

const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');

const SCRIPT_DIR = __dirname;
const SCHEMA_MD = path.join(SCRIPT_DIR, '..', 'agents', 'card-schema.md');
const REGISTRY_MD = path.join(SCRIPT_DIR, '..', '.claude', 'agents', 'REGISTRY.md');

const STATIC_ENUMS = {
  priority: ['HIGH', 'MEDIUM', 'LOW'],
  execution_mode: ['local', 'cloud'],
  review_profile: ['skip', 'light', 'balanced', 'deep'],
};

// --- SSOT parsers -----------------------------------------------------------

/**
 * Parse the field-state matrix from card-schema.md.
 * Returns { field: { EPIC, CHILD, STANDALONE } } where each value is R|E|C|NA.
 */
function loadSchema(schemaPath = SCHEMA_MD) {
  const text = fs.readFileSync(schemaPath, 'utf8');
  const lines = text.split('\n');
  const matrix = {};
  let inTable = false;
  let cols = null; // index map after the header
  for (const raw of lines) {
    const line = raw.trim();
    const isRow = line.startsWith('|') && line.endsWith('|');
    if (!inTable) {
      if (isRow && /\|\s*Field\s*\|/i.test(line) && /EPIC/i.test(line) && /CHILD/i.test(line)) {
        // header row found — map column order
        const headers = splitRow(line).map((h) => h.toUpperCase());
        cols = {
          EPIC: headers.indexOf('EPIC'),
          CHILD: headers.indexOf('CHILD'),
          STANDALONE: headers.indexOf('STANDALONE'),
        };
        inTable = true;
      }
      continue;
    }
    if (!isRow) break; // table ended
    const cells = splitRow(line);
    // skip the markdown separator row (|---|---|)
    if (cells.every((c) => /^:?-+:?$/.test(c))) continue;
    const fields = extractBackticked(cells[0]);
    if (!fields.length) continue;
    const state = (cell) => parseState(cells[cell]);
    const entry = { EPIC: state(cols.EPIC), CHILD: state(cols.CHILD), STANDALONE: state(cols.STANDALONE) };
    for (const f of fields) matrix[f] = entry;
  }
  if (!Object.keys(matrix).length) {
    throw new Error(`validate-card-baseline: could not parse field-state matrix from ${schemaPath}`);
  }
  return matrix;
}

function splitRow(line) {
  // drop leading/trailing pipe, split on |, trim
  return line.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
}

function extractBackticked(cell) {
  const out = [];
  const re = /`([^`]+)`/g;
  let m;
  while ((m = re.exec(cell)) !== null) {
    // keep dotted paths (links.prd) and plain identifiers; drop anything with spaces/values
    const tok = m[1].trim();
    if (/^[A-Za-z_][A-Za-z0-9_.]*$/.test(tok)) out.push(tok);
  }
  return out;
}

function parseState(cell) {
  if (cell == null) return 'NA';
  const c = cell.trim();
  if (c === '' || c.startsWith('—') || c.startsWith('-')) return 'NA';
  const ch = c[0].toUpperCase();
  if (ch === 'R') return 'R';
  if (ch === 'E') return 'E';
  if (ch === 'C') return 'C';
  return 'NA';
}

/** Parse the owner_agent + status enums from REGISTRY.md (fenced `- value` lists). */
function loadEnums(registryPath = REGISTRY_MD) {
  const fallback = {
    owner_agent: ['coder', 'ui-expert', 'plan', 'visual-designer', 'motion-expert'],
    status: ['TODO', 'READY', 'IN_PROGRESS', 'BLOCKED', 'DONE'],
  };
  let text;
  try { text = fs.readFileSync(registryPath, 'utf8'); } catch { return fallback; }
  return {
    owner_agent: parseEnumBlock(text, 'owner_agent enum') || fallback.owner_agent,
    status: parseEnumBlock(text, 'status enum') || fallback.status,
  };
}

function parseEnumBlock(text, header) {
  const idx = text.indexOf(`${header}:`);
  if (idx === -1) return null;
  const rest = text.slice(idx);
  const vals = [];
  const lines = rest.split('\n').slice(1); // after the `<header>:` line
  for (const line of lines) {
    const m = line.match(/^\s*-\s*([A-Za-z0-9_-]+)/);
    if (m) { vals.push(m[1]); continue; }
    if (line.trim().startsWith('```')) break; // end of fenced block
    if (line.trim() === '' && vals.length) break;
  }
  return vals.length ? vals : null;
}

// --- profile detection + validation ----------------------------------------

function detectProfile(card, filename = '') {
  const isEpic =
    /-epic\.ya?ml$/i.test(filename) ||
    card.group?.sequence === 0 ||
    (card.review_profile === 'skip' && card.owner_agent === '');
  if (isEpic) return 'EPIC';
  const hasPrd = !!(card.links && card.links.prd);
  const parent = card.group && card.group.parent;
  const parentIsEpic = typeof parent === 'string' && /-0*0$/.test(parent.trim());
  const isStandalone = !hasPrd && !parentIsEpic;
  return isStandalone ? 'STANDALONE' : 'CHILD';
}

function getPath(obj, dotted) {
  return dotted.split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);
}

function present(v) {
  return v !== undefined && v !== null;
}

function nonEmpty(v) {
  if (!present(v)) return false;
  if (typeof v === 'string') return v.trim() !== '';
  if (Array.isArray(v)) return v.length > 0;
  if (typeof v === 'object') return Object.keys(v).length > 0;
  return true; // numbers, booleans
}

/**
 * Validate a parsed card object against the schema matrix + enums.
 * Returns { profile, errors: string[] }.
 */
function validateCard(card, { matrix, enums, filename = '' } = {}) {
  matrix = matrix || loadSchema();
  enums = enums || loadEnums();
  const profile = detectProfile(card, filename);
  const errors = [];

  for (const [field, states] of Object.entries(matrix)) {
    const state = states[profile];
    if (state === 'NA' || state === 'C') continue;
    const val = field.includes('.') ? getPath(card, field) : card[field];
    if (state === 'R' && !nonEmpty(val)) {
      errors.push(`REQUIRED field '${field}' is missing or empty (profile ${profile})`);
    } else if (state === 'E' && !present(field.includes('.') ? getPath(card, field) : card[field]) && !(field in card)) {
      errors.push(`field '${field}' must be present as a key (value may be empty) (profile ${profile})`);
    }
  }

  // Enum checks (only when the value is present and non-empty).
  const enumChecks = [
    ['status', enums.status],
    ['priority', STATIC_ENUMS.priority],
    ['execution_mode', STATIC_ENUMS.execution_mode],
    ['review_profile', STATIC_ENUMS.review_profile],
  ];
  for (const [field, allowed] of enumChecks) {
    const v = card[field];
    if (nonEmpty(v) && !allowed.includes(String(v))) {
      errors.push(`'${field}' = '${v}' is not in enum {${allowed.join(', ')}}`);
    }
  }
  // owner_agent: '' is legal ONLY for epics; otherwise must be in the Rule A enum.
  const oa = card.owner_agent;
  if (profile === 'EPIC') {
    if (oa !== '' && oa != null && !enums.owner_agent.includes(String(oa))) {
      errors.push(`epic 'owner_agent' must be '' or in {${enums.owner_agent.join(', ')}} (got '${oa}')`);
    }
  } else if (nonEmpty(oa) && !enums.owner_agent.includes(String(oa))) {
    errors.push(`'owner_agent' = '${oa}' is not in enum {${enums.owner_agent.join(', ')}}`);
  }

  return { profile, errors };
}

// --- CLI --------------------------------------------------------------------

function main(argv) {
  const files = argv.slice(2);
  if (!files.length) {
    process.stderr.write('Usage: validate-card-baseline.js <card.yml> [<card2.yml> ...]\n');
    return 2;
  }
  const matrix = loadSchema();
  const enums = loadEnums();
  let failed = 0;
  for (const file of files) {
    let card;
    try {
      card = yaml.load(fs.readFileSync(file, 'utf8'));
    } catch (e) {
      process.stdout.write(`✖ ${file}: YAML parse error — ${e.message}\n`);
      failed++;
      continue;
    }
    if (!card || typeof card !== 'object') {
      process.stdout.write(`✖ ${file}: not a YAML mapping\n`);
      failed++;
      continue;
    }
    const { profile, errors } = validateCard(card, { matrix, enums, filename: path.basename(file) });
    if (errors.length) {
      failed++;
      process.stdout.write(`✖ ${file} [${profile}] — ${errors.length} issue(s):\n`);
      for (const e of errors) process.stdout.write(`    - ${e}\n`);
    } else {
      process.stdout.write(`✓ ${file} [${profile}]\n`);
    }
  }
  return failed ? 1 : 0;
}

if (require.main === module) {
  process.exit(main(process.argv));
}

module.exports = { validateCard, detectProfile, loadSchema, loadEnums, nonEmpty };
