"""Read-only audit of the materialized RU CPT corpus and training checkpoints.

This never rewrites the corpus. It verifies Arrow shape, block lengths/token
ranges, clean-source provenance, hashes, and the latest Trainer checkpoint.
Output is JSON so the result can be archived and compared across runs.
"""
from __future__ import annotations

import argparse
import hashlib
import json
import random
from pathlib import Path

from datasets import load_from_disk
from transformers import AutoTokenizer

from corpus_common import CLEAN, PACKED, has_mixed_script, cyrillic_ratio

MODEL = "Qwen/Qwen3-1.7B-Base"
BLOCK = 2048


def sha256(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()


def clean_stats() -> dict:
    result = {}
    for path in sorted(CLEAN.glob("*.jsonl")):
        docs = chars = mixed = 0
        cyr_sum = 0.0
        with path.open(encoding="utf-8") as f:
            for line in f:
                if not line.strip():
                    continue
                text = json.loads(line)["text"]
                docs += 1
                chars += len(text)
                mixed += int(has_mixed_script(text))
                cyr_sum += cyrillic_ratio(text)
        result[path.stem] = {
            "docs": docs,
            "chars": chars,
            "mean_doc_chars": round(chars / docs, 2) if docs else 0,
            "mean_cyrillic_ratio": round(cyr_sum / docs, 6) if docs else 0,
            "mixed_script_docs": mixed,
            "bytes": path.stat().st_size,
            "sha256": sha256(path),
        }
    return result


def checkpoint_stats(root: Path) -> dict:
    states = []
    for state_path in root.glob("checkpoint-*/trainer_state.json"):
        state = json.loads(state_path.read_text(encoding="utf-8"))
        states.append((int(state["global_step"]), state_path, state))
    if not states:
        return {"present": False}
    step, state_path, state = max(states)
    log = state.get("log_history", [])
    last = next((x for x in reversed(log) if "loss" in x), {})
    return {
        "present": True,
        "latest_checkpoint": str(state_path.parent),
        "global_step": step,
        "max_steps": state.get("max_steps"),
        "progress_pct": round(100 * step / state["max_steps"], 3),
        "epoch": state.get("epoch"),
        "last_loss": last.get("loss"),
        "last_grad_norm": last.get("grad_norm"),
        "last_learning_rate": last.get("learning_rate"),
        "full_weight_model_bytes": (state_path.parent / "model.safetensors").stat().st_size,
    }


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--samples", type=int, default=64)
    ap.add_argument("--seed", type=int, default=20260718)
    ap.add_argument("--output")
    ap.add_argument("--checkpoint-root",
                    default=str(Path(__file__).resolve().parent / "Qwen3-1.7B-ru-cpt"))
    args = ap.parse_args()

    ds = load_from_disk(str(PACKED))
    if not len(ds):
        raise SystemExit("packed dataset is empty")
    rng = random.Random(args.seed)
    indices = sorted(rng.sample(range(len(ds)), min(args.samples, len(ds))))
    tokenizer = AutoTokenizer.from_pretrained(MODEL, local_files_only=True)
    vocab = len(tokenizer)
    bad_lengths = bad_ids = decoded_mixed = 0
    samples = []
    for idx in indices:
        ids = ds[idx]["input_ids"]
        bad_lengths += int(len(ids) != BLOCK)
        bad_ids += sum(1 for token in ids if token < 0 or token >= vocab)
        text = tokenizer.decode(ids, skip_special_tokens=True)
        decoded_mixed += int(has_mixed_script(text))
        samples.append({
            "index": idx,
            "tokens": len(ids),
            "cyrillic_ratio": round(cyrillic_ratio(text), 6),
            "mixed_script": has_mixed_script(text),
            "preview": text[:240].replace("\n", " "),
        })

    arrows = sorted(PACKED.glob("*.arrow"))
    clean = clean_stats()
    total_chars = sum(x["chars"] for x in clean.values())
    report = {
        "schema_version": 1,
        "read_only": True,
        "model": MODEL,
        "packed": {
            "path": str(PACKED),
            "rows": len(ds),
            "block_tokens": BLOCK,
            "total_tokens": len(ds) * BLOCK,
            "arrow_bytes": sum(p.stat().st_size for p in arrows),
            "arrow_sha256": {p.name: sha256(p) for p in arrows},
            "sampled_rows": len(indices),
            "sample_bad_lengths": bad_lengths,
            "sample_out_of_vocab_ids": bad_ids,
            "sample_mixed_script_rows": decoded_mixed,
        },
        "clean_sources": clean,
        "clean_char_share_pct": {
            key: round(100 * value["chars"] / total_chars, 3)
            for key, value in clean.items()
        },
        "provenance_warning": (
            "Packed Arrow has no per-block source/document metadata; source shares can "
            "only be reconstructed from the clean inputs, not proven from Arrow alone."
        ),
        "checkpoint": checkpoint_stats(Path(args.checkpoint_root)),
        "samples": samples,
    }
    rendered = json.dumps(report, ensure_ascii=False, indent=2) + "\n"
    if args.output:
        Path(args.output).write_text(rendered, encoding="utf-8")
    print(rendered, end="")


if __name__ == "__main__":
    main()
