import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
import { basename, join } from "node:path";
const VALID_OUTCOMES = new Set([
    "fix-in-scope",
    "spawn-plan",
    "accept-finding",
]);
/**
 * Reject multiline content at the library boundary. The log's on-disk
 * format is markdown sections with bold-labeled single-line fields; any
 * line-separator character in hypothesis / note / reason silently
 * truncates during parse (the regex uses /m mode, where $ matches before
 * LF, CR, LS, and PS). Throwing here forces callers to sanitize — either
 * collapse to a single line or split across multiple entries.
 *
 * Line separators rejected: LF (\n), CR (\r), LS (U+2028), PS (U+2029).
 */
const LINE_SEPARATOR_RE = /[\n\r\u2028\u2029]/;
function assertSingleLine(field, value) {
    if (LINE_SEPARATOR_RE.test(value)) {
        throw new Error(`Falsification log ${field} must be single-line (got a value containing a line separator). Either collapse to one line (replace '\\n' with '; '), or split the content across multiple entries. The log's parser is line-oriented; any line-separator (LF, CR, LS, PS) would silently truncate.`);
    }
}
function logPath(planRoot) {
    return join(planRoot, "falsification.md");
}
function headerFor(planRoot) {
    return `# Falsification Log — ${basename(planRoot)}\n\nAppend-only record of the /falsify bounty hunt for this plan. Never edit in place; entries are appended via \`appendHypothesis\` and \`markTerminated\` from \`apps/indusk-mcp/src/lib/falsification/log.ts\`.\n\n`;
}
/**
 * Append a confirmed-hypothesis entry to the plan's falsification log.
 * Creates the log file with a header if it doesn't yet exist. Throws if
 * the log is already terminated (a new hypothesis after a terminator is a
 * sign the ritual was restarted incorrectly — see `isFalsificationComplete`
 * and start a new plan or explicitly un-terminate first).
 */
export function appendHypothesis(planRoot, entry) {
    assertSingleLine("hypothesis", entry.hypothesis);
    if (entry.note !== undefined)
        assertSingleLine("note", entry.note);
    const path = logPath(planRoot);
    const existing = existsSync(path) ? readFalsificationLog(planRoot) : [];
    if (existing.length > 0 && existing[existing.length - 1].kind === "terminator") {
        throw new Error(`Falsification log at ${path} is already terminated. Start a new plan or remove the terminator before appending.`);
    }
    if (!existsSync(path)) {
        writeFileSync(path, headerFor(planRoot), "utf-8");
    }
    const stored = {
        kind: "hypothesis",
        hypothesis: entry.hypothesis,
        testPath: entry.testPath,
        outcome: entry.outcome,
        note: entry.note,
        timestamp: new Date().toISOString(),
    };
    appendFileSync(path, renderHypothesis(stored), "utf-8");
    return stored;
}
/**
 * Append a terminator entry marking the falsification ritual complete for
 * this plan. No further hypotheses can be appended after this. The reason
 * is the user-confirmed rationale for termination (e.g., "investigated
 * concurrency, race conditions, partial-write paths, and type-narrowing
 * gaps; no in-scope failure remained").
 */
export function markTerminated(planRoot, reason) {
    if (!reason.trim()) {
        throw new Error("markTerminated requires a non-empty reason.");
    }
    assertSingleLine("reason", reason);
    const path = logPath(planRoot);
    const existing = existsSync(path) ? readFalsificationLog(planRoot) : [];
    if (existing.length > 0 && existing[existing.length - 1].kind === "terminator") {
        throw new Error(`Falsification log at ${path} is already terminated.`);
    }
    if (!existsSync(path)) {
        writeFileSync(path, headerFor(planRoot), "utf-8");
    }
    const stored = {
        kind: "terminator",
        reason: reason.trim(),
        timestamp: new Date().toISOString(),
    };
    appendFileSync(path, renderTerminator(stored), "utf-8");
    return stored;
}
/**
 * Read the falsification log for a plan. Returns an empty array if the log
 * file does not exist. Malformed entries are skipped (not thrown) and
 * surfaced via the optional `onMalformed` callback, matching the semantic
 * graph event log's resilience pattern.
 */
export function readFalsificationLog(planRoot, opts) {
    const path = logPath(planRoot);
    if (!existsSync(path))
        return [];
    const content = readFileSync(path, "utf-8");
    const entries = [];
    const sectionRegex = /^##\s+(Hypothesis|Terminated)\s+(.+?)\s*$/gm;
    const matches = [...content.matchAll(sectionRegex)];
    for (let i = 0; i < matches.length; i++) {
        const match = matches[i];
        const [, kind, timestamp] = match;
        const start = (match.index ?? 0) + match[0].length;
        const end = i + 1 < matches.length ? (matches[i + 1].index ?? content.length) : content.length;
        const body = content.slice(start, end).trim();
        if (kind === "Hypothesis") {
            const entry = parseHypothesisBody(body, timestamp);
            if ("lineNumber" in entry) {
                opts?.onMalformed?.(entry);
            }
            else {
                entries.push(entry);
            }
        }
        else if (kind === "Terminated") {
            const entry = parseTerminatorBody(body, timestamp);
            if ("lineNumber" in entry) {
                opts?.onMalformed?.(entry);
            }
            else {
                entries.push(entry);
            }
        }
    }
    return entries;
}
/**
 * True iff the plan's falsification log exists AND its last entry is a
 * terminator. False for a missing log, a log with only hypotheses (ritual
 * started but not terminated), or an empty log file.
 */
export function isFalsificationComplete(planRoot) {
    const entries = readFalsificationLog(planRoot);
    if (entries.length === 0)
        return false;
    return entries[entries.length - 1].kind === "terminator";
}
// ---------------------------------------------------------------
// Rendering (writing entries to markdown)
// ---------------------------------------------------------------
function renderHypothesis(entry) {
    const lines = [
        `## Hypothesis ${entry.timestamp}`,
        "",
        `**Hypothesis:** ${entry.hypothesis}`,
        `**Test:** ${entry.testPath ?? "(not written)"}`,
        `**Outcome:** ${entry.outcome}`,
    ];
    if (entry.note) {
        lines.push(`**Note:** ${entry.note}`);
    }
    lines.push("", "");
    return lines.join("\n");
}
function renderTerminator(entry) {
    return [`## Terminated ${entry.timestamp}`, "", `**Reason:** ${entry.reason}`, "", ""].join("\n");
}
// ---------------------------------------------------------------
// Parsing (reading entries from markdown)
// ---------------------------------------------------------------
function parseHypothesisBody(body, timestamp) {
    const hypothesisMatch = body.match(/^\*\*Hypothesis:\*\*\s+(.+)$/m);
    const testMatch = body.match(/^\*\*Test:\*\*\s+(.+)$/m);
    const outcomeMatch = body.match(/^\*\*Outcome:\*\*\s+([a-z-]+)$/m);
    const noteMatch = body.match(/^\*\*Note:\*\*\s+(.+)$/m);
    if (!hypothesisMatch || !outcomeMatch) {
        return {
            lineNumber: 0,
            content: body,
            reason: "Hypothesis entry missing required fields (hypothesis or outcome)",
        };
    }
    const outcome = outcomeMatch[1];
    if (!VALID_OUTCOMES.has(outcome)) {
        return {
            lineNumber: 0,
            content: body,
            reason: `Invalid outcome "${outcome}"; must be one of ${[...VALID_OUTCOMES].join(", ")}`,
        };
    }
    const testRaw = testMatch?.[1]?.trim() ?? "(not written)";
    return {
        kind: "hypothesis",
        hypothesis: hypothesisMatch[1].trim(),
        testPath: testRaw === "(not written)" ? null : testRaw,
        outcome,
        note: noteMatch?.[1]?.trim(),
        timestamp,
    };
}
function parseTerminatorBody(body, timestamp) {
    const reasonMatch = body.match(/^\*\*Reason:\*\*\s+(.+)$/m);
    if (!reasonMatch) {
        return {
            lineNumber: 0,
            content: body,
            reason: "Terminator entry missing required Reason field",
        };
    }
    return {
        kind: "terminator",
        reason: reasonMatch[1].trim(),
        timestamp,
    };
}
