#!/usr/bin/env python3
"""Exhaustively audit every public BuzzGoing quiz outcome path.

This is a deterministic first-party study. It uses the same paced quiz data,
score bands and typed-result tie rule as the public site, enumerates every
possible answer combination, and writes the complete per-quiz results as CSV
and JSON. It does not use visitor data or simulated probabilities.
"""
import csv
import itertools
import json
import math
import shutil
import sys
from collections import Counter
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
import build  # noqa: E402


OUT = ROOT / "static" / "research"
CHART = ROOT / "artwork" / "source" / "posts" / "how-balanced-are-buzzgoing-quiz-results.svg"


def typed_result(quiz, choices):
    """Match quiz.js: preserve first-seen key when tallies tie."""
    tally = {}
    for question, option_index in zip(quiz["questions"], choices):
        key = question["options"][option_index]["type"]
        tally[key] = tally.get(key, 0) + 1
    best = None
    for key in tally:
        if best is None or tally[key] > tally[best]:
            best = key
    return best


def scored_result(quiz, choices):
    score = sum(
        int(question["options"][option_index]["type"])
        for question, option_index in zip(quiz["questions"], choices)
    )
    for result, (lo, hi) in zip(quiz["results"], build.score_bands(quiz)):
        if lo <= score <= hi:
            return result["key"]
    raise RuntimeError("score outside result bands")


def audit_quiz(quiz):
    keys = [result["key"] for result in quiz["results"]]
    titles = {result["key"]: result["title"] for result in quiz["results"]}
    counts = Counter({key: 0 for key in keys})
    option_ranges = [range(len(question["options"])) for question in quiz["questions"]]
    resolver = scored_result if quiz["type"] == "scored" else typed_result
    for choices in itertools.product(*option_ranges):
        counts[resolver(quiz, choices)] += 1
    total = sum(counts.values())
    shares = {key: counts[key] / total for key in keys}
    entropy = -sum(share * math.log(share, 2) for share in shares.values() if share)
    normalized_entropy = entropy / math.log(len(keys), 2)
    return {
        "slug": quiz["slug"],
        "title": quiz["title"],
        "category": quiz["category"],
        "type": quiz["type"],
        "question_count": len(quiz["questions"]),
        "result_count": len(keys),
        "answer_paths": total,
        "reachable_results": sum(1 for key in keys if counts[key]),
        "min_share_pct": round(min(shares.values()) * 100, 4),
        "max_share_pct": round(max(shares.values()) * 100, 4),
        "spread_pp": round((max(shares.values()) - min(shares.values())) * 100, 4),
        "normalized_entropy": round(normalized_entropy, 6),
        "outcomes": [
            {
                "key": key,
                "title": titles[key],
                "paths": counts[key],
                "share_pct": round(shares[key] * 100, 4),
            }
            for key in keys
        ],
    }


def percentile(values, fraction):
    values = sorted(values)
    position = (len(values) - 1) * fraction
    lower = int(math.floor(position))
    upper = int(math.ceil(position))
    if lower == upper:
        return values[lower]
    return values[lower] + (values[upper] - values[lower]) * (position - lower)


def chart_svg(rows, summary):
    ranked = sorted(rows, key=lambda row: row["spread_pp"])
    width, height = 1440, 810
    left, top, plot_w, row_h = 500, 112, 820, 15
    max_spread = max(row["spread_pp"] for row in ranked)
    bars = []
    for index, row in enumerate(ranked):
        y = top + index * row_h
        bar_w = max(2, row["spread_pp"] / max_spread * plot_w)
        colour = "#4DDBD2" if row["spread_pp"] <= 10 else ("#D4E94C" if row["spread_pp"] <= 20 else "#F4502E")
        title = row["title"].replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
        bars.append(
            '<text x="470" y="{0}" text-anchor="end" class="label">{1}</text>'
            '<rect x="{2}" y="{3}" width="{4:.2f}" height="10" rx="5" fill="{5}"/>'
            '<text x="{6:.2f}" y="{0}" class="value">{7:.1f} pp</text>'.format(
                y + 9, title, left, y, bar_w, colour, left + bar_w + 9, row["spread_pp"]
            )
        )
    return """<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">
<rect width="100%" height="100%" fill="#FAF7F0"/>
<style>.title{{font:900 38px Arial,sans-serif;fill:#111110}}.sub{{font:18px Arial,sans-serif;fill:#5D5A52}}.label{{font:12px Arial,sans-serif;fill:#111110}}.value{{font:11px Arial,sans-serif;fill:#5D5A52}}</style>
<text x="70" y="58" class="title">Every possible path through 38 BuzzGoing quizzes</text>
<text x="70" y="88" class="sub">Outcome spread: largest result share minus smallest · {paths:,} answer paths checked · 24 Sep 2026</text>
<line x1="{left}" y1="98" x2="{left}" y2="690" stroke="#111110" stroke-width="2"/>
{bars}
<text x="70" y="760" class="sub">Median spread {median:.1f} percentage points · all {results} configured outcomes reachable</text>
</svg>""".format(
        width=width,
        height=height,
        paths=summary["answer_paths_checked"],
        left=left,
        bars="\n".join(bars),
        median=summary["median_spread_pp"],
        results=summary["configured_results"],
    )


def main():
    rows = [audit_quiz(quiz) for quiz in build.QUIZZES]
    spreads = [row["spread_pp"] for row in rows]
    typed = [row for row in rows if row["type"] == "typed"]
    scored = [row for row in rows if row["type"] == "scored"]
    summary = {
        "study_date": "2026-09-24",
        "method": "Exhaustive enumeration of every possible answer combination using the public scoring rules.",
        "quiz_count": len(rows),
        "question_count": sum(row["question_count"] for row in rows),
        "configured_results": sum(row["result_count"] for row in rows),
        "reachable_results": sum(row["reachable_results"] for row in rows),
        "answer_paths_checked": sum(row["answer_paths"] for row in rows),
        "median_spread_pp": round(percentile(spreads, 0.5), 4),
        "p25_spread_pp": round(percentile(spreads, 0.25), 4),
        "p75_spread_pp": round(percentile(spreads, 0.75), 4),
        "quizzes_with_spread_le_10pp": sum(spread <= 10 for spread in spreads),
        "quizzes_with_spread_gt_20pp": sum(spread > 20 for spread in spreads),
        "lowest_spread_quiz": min(rows, key=lambda row: row["spread_pp"])["slug"],
        "highest_spread_quiz": max(rows, key=lambda row: row["spread_pp"])["slug"],
        "typed_quizzes": len(typed),
        "typed_answer_paths": sum(row["answer_paths"] for row in typed),
        "typed_median_spread_pp": round(percentile([row["spread_pp"] for row in typed], 0.5), 4),
        "scored_quizzes": len(scored),
        "scored_answer_paths": sum(row["answer_paths"] for row in scored),
        "scored_median_spread_pp": round(percentile([row["spread_pp"] for row in scored], 0.5), 4),
        "limitations": [
            "Every option is weighted equally; this is a structural audit, not a prediction of visitor behaviour.",
            "Typed-result ties follow the site's first-seen tie rule.",
            "No visitor answers, identities, cookies or analytics were collected or used.",
        ],
    }
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "quiz-outcome-distribution.json").write_text(
        json.dumps({"summary": summary, "quizzes": rows}, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    fields = [
        "slug", "title", "category", "type", "question_count", "result_count",
        "answer_paths", "reachable_results", "min_share_pct", "max_share_pct",
        "spread_pp", "normalized_entropy",
    ]
    with (OUT / "quiz-outcome-distribution.csv").open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=fields)
        writer.writeheader()
        for row in rows:
            writer.writerow({field: row[field] for field in fields})
    shutil.copy2(__file__, OUT / "audit_outcome_balance.py")
    CHART.parent.mkdir(parents=True, exist_ok=True)
    CHART.write_text(chart_svg(rows, summary), encoding="utf-8")
    print(json.dumps(summary, indent=2))


if __name__ == "__main__":
    main()
