import { BaseAgent } from "./base.js";
/**
 * Validates Settler output by comparing old and new truth files via LLM.
 * Catches contradictions, missing state changes, and temporal inconsistencies.
 *
 * Uses a minimal verdict protocol instead of requiring structured JSON:
 *   Line 1: PASS or FAIL
 *   Remaining lines: free-form warnings (one per line, optional category prefix)
 */
export class StateValidatorAgent extends BaseAgent {
    get name() {
        return "state-validator";
    }
    async validate(chapterContent, chapterNumber, oldState, newState, oldHooks, newHooks, language = "zh", authorityContext) {
        const stateDiff = this.computeDiff(oldState, newState, "State Card");
        const hooksDiff = this.computeDiff(oldHooks, newHooks, "Hooks Pool");
        // Skip validation if nothing changed
        if (!stateDiff && !hooksDiff) {
            return { warnings: [], passed: true };
        }
        const langInstruction = language === "en"
            ? "Respond in English."
            : "用中文回答。";
        const systemPrompt = `You are a continuity validator for a novel writing system. ${langInstruction}

Given the chapter text and the CHANGES made to truth files (state card + hooks pool), check for contradictions:

1. State change without narrative support — truth file says something changed but the chapter text doesn't describe it
2. Missing state change — chapter text describes something happening but the truth file didn't capture it
3. Temporal impossibility — character moves locations without transition, injury heals without time passing
4. Hook anomaly — a hook disappeared without being marked resolved, or a new hook has no basis in the chapter
5. Retroactive edit — truth file change implies something happened in a PREVIOUS chapter, not the current one
6. Cross-truth key-setting conflict — numbered rules, named laws, ranks, identities, locations, or relationship labels in the new truth files contradict the chapter text or the authority context

Output format (simple, NOT JSON):
- First line: exactly PASS or FAIL (nothing else on this line)
- Following lines: one warning per line, optionally prefixed with [category]
- If no issues at all, just output: PASS

Example:
PASS
[unsupported_change] State card says character moved to the forest, but text only shows intent
[minor] Hook H03 advanced but text mention is brief

Or if there are hard contradictions:
FAIL
[contradiction] State says character is dead but chapter text shows them speaking
[unsupported_change] New location not mentioned anywhere in chapter text

IMPORTANT: Output FAIL ONLY for hard contradictions — facts that directly conflict with the chapter text. Do NOT fail for:
- Slightly ahead-of-text inferences
- Missing details that the state card didn't capture
- Reasonable extrapolations from text
- Hook management differences that don't contradict text
These should be warnings with PASS, not FAIL.`;
        const authorityBlock = this.buildAuthorityContextBlock(authorityContext);
        const userPrompt = `Chapter ${chapterNumber} validation:

${authorityBlock}

## State Card Changes
${stateDiff || "(no changes)"}

## Hooks Pool Changes
${hooksDiff || "(no changes)"}

## Chapter Text (for reference)
${chapterContent.slice(0, 6000)}`;
        try {
            const response = await this.chat([
                { role: "system", content: systemPrompt },
                { role: "user", content: userPrompt },
            ], { temperature: 0.1 });
            return this.parseResult(response.content);
        }
        catch (error) {
            this.log?.warn(`State validation failed: ${error}`);
            throw error;
        }
    }
    computeDiff(oldText, newText, label) {
        if (oldText === newText)
            return null;
        const oldLines = oldText.split("\n").filter((l) => l.trim());
        const newLines = newText.split("\n").filter((l) => l.trim());
        const added = newLines.filter((l) => !oldLines.includes(l));
        const removed = oldLines.filter((l) => !newLines.includes(l));
        if (added.length === 0 && removed.length === 0)
            return null;
        const parts = [`### ${label}`];
        if (removed.length > 0)
            parts.push("Removed:\n" + removed.map((l) => `- ${l}`).join("\n"));
        if (added.length > 0)
            parts.push("Added:\n" + added.map((l) => `+ ${l}`).join("\n"));
        return parts.join("\n");
    }
    buildAuthorityContextBlock(authorityContext) {
        if (!authorityContext)
            return "## Authority / Cross-Truth Context\n(no authority context provided)";
        const storyFrame = this.truncateHead(authorityContext.storyFrame ?? "", 3500);
        const bookRules = this.truncateHead(authorityContext.bookRules ?? "", 2000);
        const chapterSummaries = this.truncateTail(authorityContext.chapterSummaries ?? "", 3500);
        return [
            "## Authority / Cross-Truth Context",
            "Authority priority: current chapter text > runtime truth files/current summaries > story_frame/book_rules > legacy story_bible intro or marketing-style prose. If the current chapter establishes a numbered/name mapping, new truth files must follow that mapping instead of preserving an older intro-only version.",
            "",
            "### story_frame / legacy story_bible excerpt",
            storyFrame || "(empty)",
            "",
            "### book_rules excerpt",
            bookRules || "(empty)",
            "",
            "### recent chapter_summaries excerpt",
            chapterSummaries || "(empty)",
        ].join("\n");
    }
    truncateHead(text, maxChars) {
        const trimmed = text.trim();
        if (trimmed.length <= maxChars)
            return trimmed;
        return `${trimmed.slice(0, maxChars).trimEnd()}\n\n[...truncated...]`;
    }
    truncateTail(text, maxChars) {
        const trimmed = text.trim();
        if (trimmed.length <= maxChars)
            return trimmed;
        return `[...truncated...]\n\n${trimmed.slice(-maxChars).trimStart()}`;
    }
    parseResult(content) {
        const trimmed = content.trim();
        if (!trimmed) {
            throw new Error("LLM returned empty response");
        }
        const jsonResult = this.tryParseJsonResult(trimmed);
        if (jsonResult) {
            return jsonResult;
        }
        const lines = trimmed.split("\n").map((line) => line.trim()).filter(Boolean);
        if (lines.length === 0) {
            throw new Error("LLM returned empty response");
        }
        const verdictLine = lines[0];
        if (!/^(PASS|FAIL)$/i.test(verdictLine)) {
            throw new Error("State validator returned invalid response");
        }
        const passed = /^PASS$/i.test(verdictLine);
        const warnings = [];
        for (let i = 1; i < lines.length; i++) {
            const line = lines[i];
            if (/^(PASS|FAIL)$/i.test(line))
                continue;
            const categoryMatch = line.match(/^\[([^\]]+)\]\s*(.+)$/);
            if (categoryMatch) {
                warnings.push({
                    category: categoryMatch[1].trim(),
                    description: categoryMatch[2].trim(),
                });
            }
            else if (line.startsWith("- ") || line.startsWith("* ")) {
                warnings.push({
                    category: "general",
                    description: line.slice(2).trim(),
                });
            }
            else if (line.length > 5) {
                warnings.push({
                    category: "general",
                    description: line,
                });
            }
        }
        return { warnings, passed };
    }
    tryParseJsonResult(text) {
        const direct = this.tryParseExactJsonResult(text);
        if (direct) {
            return direct;
        }
        const candidate = extractBalancedJsonObject(text);
        if (!candidate) {
            return null;
        }
        return this.tryParseExactJsonResult(candidate);
    }
    tryParseExactJsonResult(text) {
        try {
            const parsed = JSON.parse(text);
            if (typeof parsed.passed !== "boolean")
                return null;
            return {
                warnings: (parsed.warnings ?? []).map((w) => ({
                    category: w.category ?? "unknown",
                    description: w.description ?? "",
                })),
                passed: parsed.passed,
            };
        }
        catch {
            return null;
        }
    }
}
function extractBalancedJsonObject(text) {
    const start = text.indexOf("{");
    if (start < 0) {
        return null;
    }
    let depth = 0;
    let inString = false;
    let escaped = false;
    let endIndex = -1;
    for (let index = start; index < text.length; index += 1) {
        const char = text[index];
        if (inString) {
            if (escaped) {
                escaped = false;
                continue;
            }
            if (char === "\\") {
                escaped = true;
                continue;
            }
            if (char === "\"") {
                inString = false;
            }
            continue;
        }
        if (char === "\"") {
            inString = true;
            continue;
        }
        if (char === "{") {
            depth += 1;
            continue;
        }
        if (char === "}") {
            depth -= 1;
            if (depth === 0) {
                endIndex = index;
                break;
            }
            if (depth < 0) {
                return null;
            }
        }
    }
    if (endIndex < 0)
        return null;
    // Only accept the candidate if what follows the closing brace is
    // nothing, whitespace, or a structural JSON terminator.
    // This rejects trailing content like "{...} more text here"
    const followingChar = text[endIndex + 1];
    if (followingChar !== undefined &&
        followingChar !== "\n" &&
        followingChar !== "\r" &&
        followingChar !== "\t" &&
        followingChar !== " " &&
        followingChar !== "," &&
        followingChar !== "]" &&
        followingChar !== "}") {
        return null;
    }
    return text.slice(start, endIndex + 1);
}
//# sourceMappingURL=state-validator.js.map