/**
 * Headless milestone context PDD check.
 *
 * Purpose: enforce ADR-0000's purpose gate at the `sf headless new-milestone`
 * entry point. ADR-0000 names the eight PDD fields (Purpose, Consumer, Contract,
 * Failure boundary, Evidence, Non-goals, Invariants, Assumptions) as the
 * upstream filter — SF cannot know it is solving the right problem without them.
 * A trivially-thin context document silently undermines every downstream gate.
 *
 * Consumer: src/headless.ts before it bootstraps `.sf/` and spawns the RPC
 * child for a new-milestone run. The operator may opt out per-call with
 * `--skip-pdd-check` during the migration period (existing milestones predate
 * this gate); the bypass is logged so the abandonment is auditable.
 *
 * Contract: given the raw context string the operator passed via
 * `--context <file>` or `--context-text <text>`, return a structured report
 * that distinguishes (a) PDD fields that are completely absent, (b) PDD fields
 * present but trivially-thin, and (c) whether the minimum spine — Purpose,
 * Consumer, Contract, plus either Evidence or a Falsifier — is satisfied.
 *
 * Failure boundary: the check is heuristic (markdown headings or labelled
 * lines). It never throws on malformed input; it returns `ok: false` with a
 * description so the headless entry point can refuse the create with a
 * pointed, actionable message. Pre-existing milestones predate the gate and
 * are bypassed via `--skip-pdd-check`.
 *
 * Evidence: tests/headless-pdd-check.test.mjs exercises the missing-Purpose,
 * missing-Consumer, all-fields-present, and minimum-spine cases.
 *
 * Non-goals: this module does not enforce structural ceremony depth (that
 * belongs to milestone-quality.js once a milestone has been planned). It only
 * enforces that the seed vision document has the eight PDD slots filled in
 * with at least a sentence of intent before SF starts work.
 *
 * Invariants: the order and spelling of the eight PDD field names must track
 * `docs/adr/0000-purpose-to-software-compiler.md`. The minimum spine
 * (Purpose + Consumer + Contract + Evidence-or-Falsifier) must remain a
 * subset of the full eight so the report stays internally consistent.
 *
 * Assumptions: the operator supplies the context as plain markdown or text.
 * The check tolerates variant heading styles (`## Purpose`, `**Purpose:**`,
 * `Purpose:` on a line of its own) because seed docs come from many tools.
 */

/**
 * Canonical PDD fields per ADR-0000. Order is doctrine; do not reorder.
 * Each entry pairs the field's display name with the alternate spellings
 * SF accepts when scanning prose vision documents.
 */
const PDD_FIELDS = [
	{ name: "Purpose", aliases: ["Purpose"] },
	{ name: "Consumer", aliases: ["Consumer", "Consumers"] },
	{ name: "Contract", aliases: ["Contract", "Contracts"] },
	{
		name: "Failure boundary",
		aliases: [
			"Failure boundary",
			"Failure Boundary",
			"Failure-boundary",
			"FailureBoundary",
		],
	},
	{ name: "Evidence", aliases: ["Evidence"] },
	{
		name: "Non-goals",
		aliases: ["Non-goals", "Non-Goals", "Non goals", "Nongoals"],
	},
	{ name: "Invariants", aliases: ["Invariants", "Invariant"] },
	{ name: "Assumptions", aliases: ["Assumptions", "Assumption"] },
];

/**
 * A "Falsifier" stanza is accepted as a substitute for "Evidence" when the
 * operator framed the success criterion as the test that would refute it.
 * Either is sufficient for the minimum spine.
 */
const FALSIFIER_ALIASES = ["Falsifier", "Falsifiers"];

const MIN_FIELD_BODY_CHARS = 12;

function escapeForRegex(text) {
	return text.replace(/[\\.*+?^${}()|[\]]/g, "\\$&");
}

/**
 * Find the body associated with a field heading. Accepts three common forms:
 *
 *   ## Purpose
 *   <body until next heading>
 *
 *   **Purpose:** body on the same line (and optional continuation lines).
 *
 *   Purpose: body on the same line.
 *
 * Returns the trimmed body string, or `null` if no occurrence is found.
 */
function findFieldBody(content, aliases) {
	const lines = content.split(/\r?\n/);
	for (let i = 0; i < lines.length; i++) {
		const line = lines[i];
		for (const alias of aliases) {
			const esc = escapeForRegex(alias);
			// `## Purpose` / `### Purpose` style heading
			const headingMatch = new RegExp(`^#{1,6}\\s+${esc}\\s*:?\\s*$`, "i").exec(
				line.trim(),
			);
			if (headingMatch) {
				const bodyLines = [];
				for (let j = i + 1; j < lines.length; j++) {
					if (/^#{1,6}\s+/.test(lines[j].trim())) break;
					bodyLines.push(lines[j]);
				}
				return bodyLines.join("\n").trim();
			}
			// `**Purpose:**` or `Purpose:` inline label, optionally bullet-prefixed
			const inlineRegex = new RegExp(
				`^\\s*(?:[-*]\\s+)?(?:\\*\\*)?${esc}(?:\\*\\*)?\\s*:\\s*(.*)$`,
				"i",
			);
			const inlineMatch = inlineRegex.exec(line);
			if (inlineMatch) {
				const tail = [inlineMatch[1]];
				for (let j = i + 1; j < lines.length; j++) {
					const next = lines[j];
					if (!next.trim()) break;
					if (
						/^\s*(?:[-*]\s+)?(?:\*\*)?[A-Z][A-Za-z -]+(?:\*\*)?\s*:/.test(next)
					) {
						// Next labelled field starts here.
						break;
					}
					if (/^#{1,6}\s+/.test(next.trim())) break;
					tail.push(next);
				}
				return tail.join("\n").trim();
			}
		}
	}
	return null;
}

/**
 * Inspect a milestone seed context for PDD coverage.
 *
 * @param {string} content - Raw context the operator passed in.
 * @returns {{
 *   ok: boolean,
 *   missing: string[],   // field names absent entirely
 *   sparse: string[],    // field names present but body shorter than MIN_FIELD_BODY_CHARS
 *   spineSatisfied: boolean,
 *   spineMissing: string[],
 *   summary: string,
 * }}
 */
export function checkPddFields(content) {
	const text = typeof content === "string" ? content : "";
	const missing = [];
	const sparse = [];
	const presentBodies = new Map();

	for (const field of PDD_FIELDS) {
		const body = findFieldBody(text, field.aliases);
		if (body === null) {
			missing.push(field.name);
			continue;
		}
		presentBodies.set(field.name, body);
		if (body.length < MIN_FIELD_BODY_CHARS) {
			sparse.push(field.name);
		}
	}

	const falsifierBody = findFieldBody(text, FALSIFIER_ALIASES);
	const hasEvidenceOrFalsifier =
		(presentBodies.has("Evidence") &&
			presentBodies.get("Evidence").length >= MIN_FIELD_BODY_CHARS) ||
		(falsifierBody !== null && falsifierBody.length >= MIN_FIELD_BODY_CHARS);

	const spineMissing = [];
	for (const required of ["Purpose", "Consumer", "Contract"]) {
		const body = presentBodies.get(required);
		if (!body || body.length < MIN_FIELD_BODY_CHARS) {
			spineMissing.push(required);
		}
	}
	if (!hasEvidenceOrFalsifier) {
		spineMissing.push("Evidence (or Falsifier)");
	}
	const spineSatisfied = spineMissing.length === 0;

	const ok = missing.length === 0 && sparse.length === 0;

	const parts = [];
	if (missing.length > 0) {
		parts.push(`missing: ${missing.join(", ")}`);
	}
	if (sparse.length > 0) {
		parts.push(`sparse (<${MIN_FIELD_BODY_CHARS} chars): ${sparse.join(", ")}`);
	}
	if (!spineSatisfied) {
		parts.push(`spine gap: ${spineMissing.join(", ")}`);
	}
	const summary =
		parts.length === 0 ? "all 8 PDD fields present" : parts.join("; ");

	return {
		ok,
		missing,
		sparse,
		spineSatisfied,
		spineMissing,
		summary,
	};
}

/**
 * Build the operator-facing refusal message when checkPddFields rejects a
 * context. The message names exactly which fields are missing or sparse and
 * points the operator at the escape hatch for migration cases.
 *
 * @param {ReturnType<typeof checkPddFields>} report
 * @returns {string}
 */
export function formatPddRefusal(report) {
	const lines = [
		"[headless] Refused: milestone context fails the ADR-0000 purpose gate.",
		"",
		"ADR-0000 requires the eight PDD fields before SF starts work:",
		"  Purpose, Consumer, Contract, Failure boundary, Evidence,",
		"  Non-goals, Invariants, Assumptions.",
		"",
	];
	if (report.missing.length > 0) {
		lines.push(`Missing entirely: ${report.missing.join(", ")}`);
	}
	if (report.sparse.length > 0) {
		lines.push(
			`Present but too thin (<${MIN_FIELD_BODY_CHARS} chars): ${report.sparse.join(", ")}`,
		);
	}
	if (!report.spineSatisfied) {
		lines.push(`Minimum spine still unmet: ${report.spineMissing.join(", ")}`);
	}
	lines.push("");
	lines.push(
		"Fix the seed context (add the missing fields with at least a sentence of intent each) and retry.",
	);
	lines.push(
		"To bypass for a milestone that pre-dates this gate, re-run with --skip-pdd-check (you will see a warning).",
	);
	return lines.join("\n");
}

export const PDD_FIELD_NAMES = PDD_FIELDS.map((f) => f.name);
export const PDD_MIN_FIELD_BODY_CHARS = MIN_FIELD_BODY_CHARS;
