#!/usr/bin/env python3
import hashlib
import json
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
RUN = ROOT / "work/story/run-002"
REQUIRED_AXES = [
    "pov_identity_power",
    "external_goal_deadline",
    "pressure_system",
    "omitted_group_or_relationship",
    "consent_agency_mechanism",
    "protagonist_active_mistake",
    "real_loss",
    "reveal_provenance",
    "climax_forum_mechanism",
    "resolution_model",
    "social_professional_payoff",
    "romantic_payoff_ending_image",
]
RUBRIC_MAX = {
    "teaser_pressure_retention": 15,
    "distinctive_recallable_premise": 15,
    "protagonist_agency": 10,
    "multi-reversal_story_engine": 15,
    "relationship_attraction_resistance_asymmetry": 10,
    "escalation_mini_payoff_irreversible_consequence": 10,
    "fair_seeded_evidence_twist": 10,
    "choice_driven_costly_climax": 10,
    "accessible_setting_profession": 3,
    "originality": 2,
}


def sha256(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()


def fail(message):
    raise RuntimeError(message)


def main():
    manifest = json.loads((ROOT / "script/project-manifest.json").read_text())
    family_path = RUN / "family-lock.json"
    corpus_path = RUN / "corpus-inventory.json"
    names_path = RUN / "name-candidate-check.json"
    findings_path = RUN / "corpus-fingerprint-findings.json"
    repaired_fingerprint_path = RUN / "fingerprint-index-11-muoi-hai-giay-bong-toi.json"
    options_path = RUN / "creative-options.json"
    if manifest.get("active_run") != "run-002" or manifest.get("status") != "run-002_options_ready_for_validation":
        fail("manifest not ready for Creative Options validation")
    family = json.loads(family_path.read_text())
    corpus = json.loads(corpus_path.read_text())
    names = json.loads(names_path.read_text())
    findings = json.loads(findings_path.read_text())
    repaired_fingerprint = json.loads(repaired_fingerprint_path.read_text())
    options = json.loads(options_path.read_text())
    bindings = options.get("bindings", {})
    expected = {
        "family_lock_sha256": sha256(family_path),
        "corpus_inventory_sha256": sha256(corpus_path),
        "name_candidate_check_sha256": sha256(names_path),
        "corpus_fingerprint_findings_sha256": sha256(findings_path),
        "fingerprint_index_11_sha256": sha256(repaired_fingerprint_path),
    }
    if bindings != expected:
        fail("Creative Options input binding drift")
    if family.get("status") != "locked_before_premise" or family.get("story_family_primary") != "Cưới trước yêu sau":
        fail("family-first lock failed")
    if corpus.get("covered_prior_project_count") != 17 or corpus.get("identity_coverage_complete") is not True:
        fail("corpus/name coverage incomplete")
    if names.get("status") != "passed":
        fail("name candidate check failed")
    if findings.get("verified") is not True or findings.get("actual_unique_fingerprinted_project_count") != 17:
        fail("semantic fingerprint coverage is not terminal 17/17")
    if repaired_fingerprint.get("verified") is not True or repaired_fingerprint.get("inventory_index") != 11:
        fail("repaired index-11 fingerprint is not terminal")
    rows = options.get("options", [])
    if len(rows) != 3 or len({row.get("option_id") for row in rows}) != 3:
        fail("exactly three distinct options required")
    eligible_name_pairs = {
        tuple(item["pair"])
        for item in names.get("candidate_pairs", [])
        if item.get("eligible") is True
    }
    for row in rows:
        if row.get("story_family_primary") != "Cưới trước yêu sau" or row.get("story_family_secondary") != "không có":
            fail(f"family drift in {row.get('option_id')}")
        if tuple(row.get("central_names", [])) not in eligible_name_pairs:
            fail(f"central names are not eligible in {row.get('option_id')}")
        fp = row.get("fingerprint", {})
        if list(fp) != REQUIRED_AXES:
            fail(f"12-axis fingerprint schema/order failed in {row.get('option_id')}")
        scores = row.get("rubric_scores", {})
        if set(scores) != set(RUBRIC_MAX):
            fail(f"rubric schema failed in {row.get('option_id')}")
        if any(not isinstance(scores[key], int) or scores[key] < 0 or scores[key] > maximum for key, maximum in RUBRIC_MAX.items()):
            fail(f"rubric bounds failed in {row.get('option_id')}")
        total = sum(scores.values())
        if row.get("rubric_total") != total:
            fail(f"rubric total mismatch in {row.get('option_id')}")
        answers = row.get("gate2_answers", [])
        if len(answers) != 10 or sum(bool(item.get("answered")) for item in answers) < 8:
            fail(f"Gate 2 answer threshold failed in {row.get('option_id')}")
        hard_gate = row.get("hard_gate", {})
        required_false = {
            "solvable_by_one_honest_conversation",
            "one_sided_self_erasure_rewarded",
            "decide_for_spouse_as_protection",
            "climax_depends_on_public_or_family_approval",
            "procedural_evidence_engine",
            "system_reform_resolution",
            "proposal_invitation_permission_or_adjacent_seat_ending",
        }
        if set(hard_gate.get("prohibited_patterns", {})) != required_false:
            fail(f"hard-gate pattern schema failed in {row.get('option_id')}")
        if any(hard_gate["prohibited_patterns"].values()):
            fail(f"prohibited pattern present in {row.get('option_id')}")
        if len(hard_gate.get("power_shifts", [])) < 3:
            fail(f"three power shifts required in {row.get('option_id')}")
        if not hard_gate.get("real_loss_not_repaired_by_apology"):
            fail(f"real loss missing in {row.get('option_id')}")
        if len(hard_gate.get("independent_private_climax_choices", [])) != 2:
            fail(f"two independent climax choices required in {row.get('option_id')}")
        comparisons = row.get("corpus_comparisons", [])
        if len(comparisons) != 17 or {x.get("project") for x in comparisons} != {x["project"] for x in corpus["entries"]}:
            fail(f"17-project comparison coverage failed in {row.get('option_id')}")
        if any(x.get("verdict") not in {"PASS", "REDESIGN"} for x in comparisons):
            fail(f"comparison verdict schema failed in {row.get('option_id')}")
        for comparison in comparisons:
            if not isinstance(comparison.get("shared_axes"), list):
                fail(f"shared_axes missing in {row.get('option_id')} against {comparison.get('project')}")
            difference = comparison.get("decisive_difference")
            if not isinstance(difference, str) or not difference.strip():
                fail(f"decisive difference missing in {row.get('option_id')} against {comparison.get('project')}")
            if comparison.get("causal_chain_same") is not False:
                comparison["verdict"] = "REDESIGN"
            if len(comparison["shared_axes"]) >= 3 and comparison.get("three_axis_chain_adjudication") != "different_causal_chain":
                comparison["verdict"] = "REDESIGN"
        row["eligible"] = total >= 85 and all(x.get("verdict") == "PASS" for x in comparisons)
    eligible = [row for row in rows if row["eligible"]]
    if not eligible:
        fail("no Creative Option eligible for Brief Lock")
    selected = options.get("selected_option_id")
    if selected not in {row["option_id"] for row in eligible}:
        fail("selected option is not eligible")
    print(json.dumps({
        "status": "PASS",
        "verified": True,
        "options_sha256": sha256(options_path),
        "eligible_options": [row["option_id"] for row in eligible],
        "selected_option_id": selected,
        "corpus_project_count": 17,
    }, ensure_ascii=False))


if __name__ == "__main__":
    main()
