/**
 * `olam flywheel k5-validate` — port of `shared/engineering/scripts/k5-rubric-helper.py`
 * validate mode. Schema-checks a plan's frontmatter `k5_scores` / `k5_boost` /
 * `k5_composite` / `k5_scored_at` block.
 *
 * Exit codes mirror the Python script:
 *   0 = valid (or all k5 fields absent — absence is acceptable for legacy plans)
 *   1 = malformed (one or more present k5 fields fail validation)
 *   2 = unexpected error (cannot stat / read / oversize file)
 *
 * Plan reference: docs/plans/olam-flywheel-cli-namespace/phase-a-tasks.md § A4
 */
import { readFileSync, statSync } from 'node:fs';
import { parse as parseYAML } from 'yaml';
const K5_DIMS = ['direction', 'approach', 'open_questions', 'constraints', 'reuse'];
const MAX_PLAN_BYTES = 1_048_576;
function extractFrontmatter(text) {
    // BOM strip
    const cleaned = text.replace(/^﻿/, '');
    if (!cleaned.startsWith('---'))
        return null;
    const closeIdx = cleaned.indexOf('\n---', 3);
    if (closeIdx === -1)
        return null;
    const fmRaw = cleaned.slice(4, closeIdx);
    try {
        const parsed = parseYAML(fmRaw);
        return parsed ?? {};
    }
    catch {
        return null;
    }
}
function validateK5Scores(scores) {
    if (typeof scores !== 'object' || scores === null) {
        return `k5_scores must be a mapping with keys ${K5_DIMS.join(', ')}`;
    }
    const obj = scores;
    for (const dim of K5_DIMS) {
        if (!(dim in obj))
            return `k5_scores missing key '${dim}' (required: ${K5_DIMS.join(', ')})`;
        const val = obj[dim];
        const n = typeof val === 'number' ? val : Number.parseInt(String(val), 10);
        if (!Number.isFinite(n) || !Number.isInteger(n)) {
            return `k5_scores.${dim} must be an integer 0-100, got: ${JSON.stringify(val)}`;
        }
        if (n < 0 || n > 100) {
            return `k5_scores.${dim} out of range 0-100: ${n}`;
        }
    }
    return null;
}
function validateK5Boost(boost) {
    const n = typeof boost === 'number' ? boost : Number.parseFloat(String(boost));
    if (!Number.isFinite(n))
        return `k5_boost must be a float 0.0-2.0, got: ${JSON.stringify(boost)}`;
    if (n < 0.0 || n > 2.0)
        return `k5_boost out of range 0.0-2.0: ${n}`;
    return null;
}
function validateK5Composite(composite) {
    const n = typeof composite === 'number' ? composite : Number.parseInt(String(composite), 10);
    if (!Number.isFinite(n) || !Number.isInteger(n)) {
        return `k5_composite must be an integer 0-100, got: ${JSON.stringify(composite)}`;
    }
    if (n < 0 || n > 100)
        return `k5_composite out of range 0-100: ${n}`;
    return null;
}
function validateK5ScoredAt(scoredAt) {
    if (typeof scoredAt !== 'string') {
        return `k5_scored_at must be a string date YYYY-MM-DD, got: ${JSON.stringify(scoredAt)}`;
    }
    if (!/^\d{4}-\d{2}-\d{2}$/.test(scoredAt.trim())) {
        return `k5_scored_at must be ISO date YYYY-MM-DD, got: ${JSON.stringify(scoredAt)}`;
    }
    return null;
}
export function validatePlan(path) {
    const emptyJson = (status, errors) => ({
        path,
        status,
        errors,
        k5_scores: null,
        k5_boost: null,
        k5_composite: null,
        k5_scored_at: null,
        min_score: null,
    });
    let stat;
    try {
        stat = statSync(path);
    }
    catch (err) {
        const m = `FAIL cannot stat ${path}: ${err instanceof Error ? err.message : 'unknown'}`;
        return { ok: false, message: m, json: emptyJson('error', [m]) };
    }
    if (stat.size > MAX_PLAN_BYTES) {
        const m = `FAIL ${path}: plan file exceeds 1MB (${stat.size} bytes); refusing to parse`;
        return { ok: false, message: m, json: emptyJson('error', [m]) };
    }
    let text;
    try {
        text = readFileSync(path, 'utf8');
    }
    catch (err) {
        const m = `FAIL cannot read ${path}: ${err instanceof Error ? err.message : 'unknown'}`;
        return { ok: false, message: m, json: emptyJson('error', [m]) };
    }
    if (!text.replace(/^﻿/, '').startsWith('---')) {
        const m = `FAIL ${path}: no YAML frontmatter (missing opening --- marker)`;
        return { ok: false, message: m, json: emptyJson('error', [m]) };
    }
    if (!text.includes('\n---', 3)) {
        const m = `FAIL ${path}: frontmatter block never closed (missing closing --- marker)`;
        return { ok: false, message: m, json: emptyJson('error', [m]) };
    }
    const fm = extractFrontmatter(text);
    if (fm === null) {
        const m = `FAIL ${path}: frontmatter could not be parsed as YAML`;
        return { ok: false, message: m, json: emptyJson('error', [m]) };
    }
    const hasScores = 'k5_scores' in fm;
    const hasBoost = 'k5_boost' in fm;
    const hasComposite = 'k5_composite' in fm;
    const hasScoredAt = 'k5_scored_at' in fm;
    if (!hasScores && !hasBoost && !hasComposite && !hasScoredAt) {
        return {
            ok: true,
            message: `PASS ${path}: k5_scores absent (acceptable — legacy plan)`,
            json: emptyJson('absent', []),
        };
    }
    const errors = [];
    if (hasScores) {
        const err = validateK5Scores(fm.k5_scores);
        if (err !== null)
            errors.push(err);
    }
    if (hasBoost) {
        const err = validateK5Boost(fm.k5_boost);
        if (err !== null)
            errors.push(err);
    }
    if (hasComposite) {
        const err = validateK5Composite(fm.k5_composite);
        if (err !== null)
            errors.push(err);
    }
    if (hasScoredAt) {
        const err = validateK5ScoredAt(fm.k5_scored_at);
        if (err !== null)
            errors.push(err);
    }
    if (errors.length > 0) {
        return {
            ok: false,
            message: `FAIL ${path}: ${errors.join('; ')}`,
            json: emptyJson('fail', errors),
        };
    }
    // Build both the human-readable lines and the structured json payload from
    // the same parsed values — single source of truth, no drift between surfaces.
    const json = emptyJson('pass', []);
    const lines = [`PASS ${path}: k5_scores valid`];
    if (hasScores && typeof fm.k5_scores === 'object' && fm.k5_scores !== null) {
        const scoresObj = fm.k5_scores;
        const parsed = {};
        const vals = [];
        for (const dim of K5_DIMS) {
            const raw = scoresObj[dim];
            const n = typeof raw === 'number' ? raw : Number.parseInt(String(raw), 10);
            parsed[dim] = n;
            vals.push(n);
            lines.push(`  ${dim}: ${n}`);
        }
        json.k5_scores = parsed;
        json.min_score = Math.min(...vals);
        lines.push(`  min(k5_scores): ${json.min_score}`);
    }
    if (hasBoost) {
        const n = typeof fm.k5_boost === 'number' ? fm.k5_boost : Number.parseFloat(String(fm.k5_boost));
        json.k5_boost = n;
        lines.push(`  k5_boost: ${n.toFixed(2)}`);
    }
    if (hasComposite) {
        const n = typeof fm.k5_composite === 'number' ? fm.k5_composite : Number.parseInt(String(fm.k5_composite), 10);
        json.k5_composite = n;
        lines.push(`  k5_composite: ${String(fm.k5_composite)}`);
    }
    if (hasScoredAt) {
        json.k5_scored_at = String(fm.k5_scored_at);
        lines.push(`  k5_scored_at: ${String(fm.k5_scored_at)}`);
    }
    if ('confidence' in fm)
        lines.push(`  confidence (existing): ${String(fm.confidence)}`);
    return { ok: true, message: lines.join('\n'), json };
}
export function registerFlywheelK5Validate(parent) {
    parent
        .command('k5-validate')
        .description("Schema-check a plan's frontmatter k5_scores / k5_boost / k5_composite / k5_scored_at")
        .argument('<plan-path>', 'path to the plan markdown file')
        .option('--json', 'emit the structured validation result as JSON on stdout instead of prose')
        .action((planPath, opts) => {
        // Exit-code contract is identical across both surfaces:
        //   0 = pass / absent, 1 = fail (schema violation), 2 = error (read/parse).
        // --json mode writes the JSON payload to stdout regardless of outcome so a
        // chain consumer can capture + branch on `.status`; prose mode keeps the
        // pass→stdout / fail→stderr split for human-shell ergonomics.
        try {
            const result = validatePlan(planPath);
            if (opts.json) {
                process.stdout.write(JSON.stringify(result.json) + '\n');
            }
            else if (result.ok) {
                process.stdout.write(result.message + '\n');
            }
            else {
                process.stderr.write(result.message + '\n');
            }
            const code = result.json.status === 'error' ? 2 : result.ok ? 0 : 1;
            process.exit(code);
        }
        catch (err) {
            const msg = `FAIL ${planPath}: unexpected error: ${err instanceof Error ? err.message : 'unknown'}`;
            if (opts.json) {
                process.stdout.write(JSON.stringify({
                    path: planPath,
                    status: 'error',
                    errors: [msg],
                    k5_scores: null,
                    k5_boost: null,
                    k5_composite: null,
                    k5_scored_at: null,
                    min_score: null,
                }) + '\n');
            }
            else {
                process.stderr.write(msg + '\n');
            }
            process.exit(2);
        }
    });
}
//# sourceMappingURL=k5-validate.js.map