from __future__ import annotations

import csv
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from .compare import ERROR_CATEGORIES, AggregateScore, FieldDiff, RecordScore

_CSV_FIELDS = [
    "record_id",
    "record_type",
    "log_date",
    "page",
    "table",
    "predicted_row",
    "truth_row",
    "field",
    "ground_truth_value",
    "predicted_value",
    "predicted_raw_text",
    "predicted_normalized_text",
    "predicted_confidence",
    "category",
    "note",
]


def print_console_summary(
    scores: list[RecordScore],
    aggregate: dict[str, AggregateScore],
    date_checks: list[FieldDiff],
    run_errors: list[tuple[str, str]],
) -> None:
    print(f"\n{'=' * 70}")
    print("Ground-truth OCR evaluation")
    print(f"{'=' * 70}")

    for score in scores:
        accuracy = f"{score.accuracy:.1%}" if score.accuracy is not None else "n/a"
        print(f"\n{score.record_id} ({score.record_type})  accuracy={accuracy}  fields={score.comparable_fields}")
        for category in ERROR_CATEGORIES:
            count = score.category_counts.get(category, 0)
            if count:
                print(f"    {category:<28} {count}")

    if run_errors:
        print(f"\n{len(run_errors)} record(s) failed to re-run OCR:")
        for record_id, message in run_errors:
            print(f"    {record_id}: {message}")

    mismatched_dates = [d for d in date_checks if "does not match" in d.note]
    if mismatched_dates:
        print(f"\n{len(mismatched_dates)} record(s) have a predicted date differing from log_date (informational):")
        for d in mismatched_dates:
            print(f"    predicted={d.predicted_value!r} log_date={d.ground_truth_value!r}")

    if aggregate:
        print(f"\n{'-' * 70}")
        print("Aggregate by record type")
        print(f"{'-' * 70}")
        for record_type, agg in aggregate.items():
            accuracy = f"{agg.accuracy:.1%}" if agg.accuracy is not None else "n/a"
            print(f"\n{record_type}: {agg.records_scored} record(s), accuracy={accuracy}, fields={agg.comparable_fields}")
            for category in ERROR_CATEGORIES:
                count = agg.category_counts.get(category, 0)
                if count:
                    print(f"    {category:<28} {count}")
    print()


def write_csv(path: str | Path, scores: list[RecordScore], log_dates: dict[str, str | None]) -> None:
    path = Path(path)
    with path.open("w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=_CSV_FIELDS)
        writer.writeheader()
        for score in scores:
            for diff in score.diffs:
                writer.writerow(
                    {
                        "record_id": score.record_id,
                        "record_type": score.record_type,
                        "log_date": log_dates.get(score.record_id),
                        "page": diff.page,
                        "table": diff.table,
                        "predicted_row": diff.predicted_row,
                        "truth_row": diff.truth_row,
                        "field": diff.field,
                        "ground_truth_value": diff.ground_truth_value,
                        "predicted_value": diff.predicted_value,
                        "predicted_raw_text": diff.predicted_raw_text,
                        "predicted_normalized_text": diff.predicted_normalized_text,
                        "predicted_confidence": diff.predicted_confidence,
                        "category": diff.category,
                        "note": diff.note,
                    }
                )


def _diff_to_dict(diff: FieldDiff) -> dict[str, Any]:
    return {
        "page": diff.page,
        "table": diff.table,
        "predicted_row": diff.predicted_row,
        "truth_row": diff.truth_row,
        "field": diff.field,
        "ground_truth_value": diff.ground_truth_value,
        "predicted_value": diff.predicted_value,
        "predicted_raw_text": diff.predicted_raw_text,
        "predicted_normalized_text": diff.predicted_normalized_text,
        "predicted_confidence": diff.predicted_confidence,
        "category": diff.category,
        "note": diff.note,
    }


def _aggregate_to_dict(agg: AggregateScore) -> dict[str, Any]:
    return {
        "records_scored": agg.records_scored,
        "accuracy": agg.accuracy,
        "comparable_fields": agg.comparable_fields,
        "matched_fields": agg.matched_fields,
        "category_counts": dict(agg.category_counts),
    }


def write_json(path: str | Path, scores: list[RecordScore], aggregate: dict[str, AggregateScore]) -> None:
    path = Path(path)
    data = {
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "records": [
            {
                "record_id": s.record_id,
                "record_type": s.record_type,
                "accuracy": s.accuracy,
                "comparable_fields": s.comparable_fields,
                "matched_fields": s.matched_fields,
                "category_counts": dict(s.category_counts),
                # only the diffs a reviewer needs to look at -- matches are implicit
                "diffs": [_diff_to_dict(d) for d in s.diffs if d.category != "match"],
            }
            for s in scores
        ],
        "aggregate": {record_type: _aggregate_to_dict(agg) for record_type, agg in aggregate.items()},
    }
    path.write_text(json.dumps(data, indent=2, default=str) + "\n")


def load_baseline(path: str | Path) -> dict[str, Any] | None:
    path = Path(path)
    if not path.is_file():
        return None
    return json.loads(path.read_text())


def save_baseline(path: str | Path, aggregate: dict[str, AggregateScore]) -> None:
    path = Path(path)
    # Each CLI invocation is scoped to the --record-type(s) it was run with, so a
    # baseline for e.g. coagulation must not erase a previously recorded RO baseline
    # sitting in the same file -- merge into whatever's already there instead of
    # overwriting it wholesale.
    existing = load_baseline(path) or {}
    record_types = dict(existing.get("record_types", {}))
    record_types.update({record_type: _aggregate_to_dict(agg) for record_type, agg in aggregate.items()})

    data = {
        "recorded_at": datetime.now(timezone.utc).isoformat(),
        "record_types": record_types,
    }
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, indent=2) + "\n")


def print_baseline_delta(baseline: dict[str, Any] | None, aggregate: dict[str, AggregateScore]) -> None:
    if baseline is None:
        print("\nNo prior baseline found -- pass --save-baseline to record one.")
        return

    print(f"\n{'-' * 70}")
    print(f"Comparison to baseline recorded {baseline.get('recorded_at')}")
    print(f"{'-' * 70}")
    for record_type, agg in aggregate.items():
        prior = baseline.get("record_types", {}).get(record_type)
        if not prior or prior.get("accuracy") is None or agg.accuracy is None:
            continue
        delta = agg.accuracy - prior["accuracy"]
        sign = "+" if delta >= 0 else ""
        print(f"{record_type}: {prior['accuracy']:.1%} -> {agg.accuracy:.1%} ({sign}{delta:.1%})")
