#!/usr/bin/env python3
"""Validate the deterministic constraints in the advanced creative-writing episode.

The validator intentionally does not pretend to score literary quality. Human review of
voice, imagery, subtext, causality, and revision precision is reported separately.
"""

from __future__ import annotations

import argparse
import json
import re
from pathlib import Path


HEADINGS = ("SCENE ONE", "SCENE TWO", "SCENE THREE")
FINAL_SENTENCE = "Nia left the brass key in the lock and kept tomorrow's ticket."
MAP_SENTENCE = "The map remembers what we erase."
HEADING_PATTERN = r"(?m)^SCENE (?:ONE|TWO|THREE)[ \t]*$"


def split_scenes(text: str) -> tuple[list[str], dict[str, str]]:
    found = re.findall(HEADING_PATTERN, text)
    parts = re.split(HEADING_PATTERN, text)
    scenes = {
        heading: parts[index + 1].strip() if index + 1 < len(parts) else ""
        for index, heading in enumerate(found)
    }
    return found, scenes


def first_index(text: str, pattern: str) -> int:
    match = re.search(pattern, text, flags=re.I | re.S)
    return match.start() if match else -1


def spoken_turns(text: str) -> int:
    return len(re.findall(r"[\"“][^\"”\n]+[\"”]", text))


def validate_text(text: str) -> dict[str, object]:
    text = text.strip()
    found, scenes = split_scenes(text)
    one = scenes.get("SCENE ONE", "")
    two = scenes.get("SCENE TWO", "")
    three = scenes.get("SCENE THREE", "")
    words = re.findall(r"\b[\w’'-]+\b", text, flags=re.UNICODE)

    red_lamp_index = first_index(three, r"red\s+(?:signal\s+)?lamp")
    green_index = first_index(three, r"\bgreen\b")
    open_index = first_index(
        three,
        r"\b(?:Nia|Tomas|she|he)\s+(?:opened|opens|unsealed|unseals|tore\s+open|tears\s+open)\s+(?:the\s+)?(?:sealed\s+)?(?:blue\s+)?envelope",
    )

    scene_one_envelope_sealed = (
        bool(re.search(r"sealed\s+blue\s+envelope", one, flags=re.I))
        and not re.search(
            r"\b(?:Nia|Tomas|she|he)\s+(?:opened|opens|unsealed|unseals|tore\s+open|tears\s+open)\s+(?:the\s+)?(?:sealed\s+)?(?:blue\s+)?envelope",
            one,
            flags=re.I,
        )
    )
    ticket_match = re.search(r"(?:exactly\s+|only\s+|a\s+single\s+)?one\s+train\s+ticket|a\s+(?:single\s+)?train\s+ticket", three, flags=re.I)
    tomorrow_match = re.search(r"ticket.{0,100}(?:dated|for)\s+tomorrow|tomorrow(?:'s)?\s+(?:train\s+)?ticket", three, flags=re.I | re.S)

    checks = {
        "exact_headings_in_order": found == list(HEADINGS),
        "word_count_1200_to_1600": 1200 <= len(words) <= 1600,
        "scene_one_time_and_place": bool(re.search(r"05:40", one) and re.search(r"ticket hall", one, flags=re.I)),
        "scene_one_bandaged_left_hand": bool(re.search(r"(?:left\s+hand.{0,40}bandag|bandag.{0,40}left\s+hand)", one, flags=re.I | re.S)),
        "scene_one_sealed_blue_envelope": scene_one_envelope_sealed,
        "scene_two_time_place_and_power": bool(
            re.search(r"06:10", two)
            and re.search(r"platform", two, flags=re.I)
            and re.search(
                r"power\s+(?:failure|outage)|power\s+(?:failed|went\s+out|is\s+gone|was\s+gone)|lights?\s+(?:failed|died|went\s+out)|generators?.{0,40}(?:failed|fail|died)",
                two,
                flags=re.I | re.S,
            )
        ),
        "scene_two_brass_key_transfer": bool(
            re.search(r"brass\s+key", two, flags=re.I)
            and re.search(
                r"(?:gave|gives|handed|hands|passed|passes|offered|offers).{0,100}(?:Nia|her).{0,100}brass\s+key|brass\s+key.{0,100}(?:into|to)\s+(?:Nia|her)|Nia\s+(?:took|takes|accepted|accepts|received|receives)\s+(?:the\s+)?key",
                two,
                flags=re.I | re.S,
            )
        ),
        "scene_two_cracked_watch_0517": bool(
            re.search(r"watch", two, flags=re.I)
            and re.search(r"crack", two, flags=re.I)
            and re.search(r"05:17", two)
        ),
        "scene_three_time_and_place": bool(re.search(r"06:25", three) and re.search(r"signal room", three, flags=re.I)),
        "lamp_green_before_envelope_open": red_lamp_index >= 0 and green_index > red_lamp_index and open_index > green_index,
        "one_ticket_dated_tomorrow": bool(ticket_match and tomorrow_match),
        "map_sentence_exactly_once": text.count(MAP_SENTENCE) == 1,
        "at_least_six_spoken_turns": spoken_turns(text) >= 6,
        "exact_final_sentence": text.endswith(FINAL_SENTENCE),
    }
    passed = sum(checks.values())
    return {
        "passed": passed,
        "total": len(checks),
        "percent": round(100 * passed / len(checks), 1),
        "wordCount": len(words),
        "headingsFound": found,
        "spokenTurnCount": spoken_turns(text),
        "checks": checks,
        "scopeNote": "These checks score explicit continuity and instruction control, not literary quality.",
    }


def response_text(session: Path) -> str:
    payload = json.loads((session / "ollama-response.json").read_text(encoding="utf-8"))
    return payload.get("response", "")


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("session", type=Path)
    args = parser.parse_args()
    result = validate_text(response_text(args.session))
    result["session"] = str(args.session)
    print(json.dumps(result, indent=2, ensure_ascii=False))
    return 0 if result["passed"] == result["total"] else 1


if __name__ == "__main__":
    raise SystemExit(main())
