"""Benchmark run logger — append-only JSONL database for reproducible results.

Every benchmark run gets a single JSONL line with:
- run_id: unique identifier (benchmark-timestamp-hash)
- timestamp: ISO 8601 with timezone
- version: Cortex version from pyproject.toml
- benchmark: which benchmark (locomo, eval_suite, latency, etc.)
- models: extraction_model, embedding_model, judge_model, generation_model
- metrics: structured scores
- environment: relevant config snapshot
- notes: human-readable context

Usage:
    from cortex.eval.run_logger import RunLogger
    
    logger = RunLogger()
    logger.log_run(
        benchmark="locomo",
        metrics={"f1_excl_adversarial": 0.186, ...},
        extraction_model="gemini-3-flash-preview",
        embedding_model="gemini-embedding-2-preview",
        notes="Post JSON-repair run",
    )
"""

import json
import hashlib
import platform
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional, Any


RUNS_FILE = Path(__file__).parent.parent.parent / "benchmarks" / "results" / "runs.jsonl"


def _get_version() -> str:
    """Read version from pyproject.toml."""
    pyproject = Path(__file__).parent.parent.parent / "pyproject.toml"
    if pyproject.exists():
        for line in pyproject.read_text().splitlines():
            if line.strip().startswith("version"):
                # version = "1.2.4"
                return line.split("=", 1)[1].strip().strip('"').strip("'")
    return "unknown"


def _make_run_id(benchmark: str, timestamp: str) -> str:
    """Generate a unique but human-readable run ID."""
    h = hashlib.sha256(f"{benchmark}-{timestamp}-{platform.node()}".encode()).hexdigest()[:8]
    date_part = timestamp[:10].replace("-", "")
    return f"{benchmark}-{date_part}-{h}"


class RunLogger:
    """Append-only JSONL logger for benchmark runs."""

    def __init__(self, runs_file: Optional[Path] = None):
        self.runs_file = runs_file or RUNS_FILE
        self.runs_file.parent.mkdir(parents=True, exist_ok=True)

    def log_run(
        self,
        benchmark: str,
        metrics: dict[str, Any],
        *,
        conversation: Optional[str] = None,
        qa_pairs: Optional[int] = None,
        extraction_model: Optional[str] = None,
        embedding_model: Optional[str] = None,
        embedding_dim: Optional[int] = None,
        generation_model: Optional[str] = None,
        judge_model: Optional[str] = None,
        retrieval_mode: str = "retrieval-only",
        claims_extracted: Optional[int] = None,
        sessions_total: Optional[int] = None,
        sessions_failed: Optional[int] = None,
        version: Optional[str] = None,
        notes: str = "",
        extra: Optional[dict[str, Any]] = None,
    ) -> dict[str, Any]:
        """Log a benchmark run. Returns the logged record."""
        now = datetime.now(timezone.utc).astimezone()
        ts = now.isoformat()
        ver = version or _get_version()
        run_id = _make_run_id(benchmark, ts)

        record = {
            "run_id": run_id,
            "timestamp": ts,
            "version": ver,
            "benchmark": benchmark,
            "retrieval_mode": retrieval_mode,
        }

        if conversation:
            record["conversation"] = conversation
        if qa_pairs is not None:
            record["qa_pairs"] = qa_pairs
        if extraction_model:
            record["extraction_model"] = extraction_model
        if embedding_model:
            record["embedding_model"] = embedding_model
        if embedding_dim is not None:
            record["embedding_dim"] = embedding_dim
        if generation_model:
            record["generation_model"] = generation_model
        if judge_model:
            record["judge_model"] = judge_model
        if claims_extracted is not None:
            record["claims_extracted"] = claims_extracted
        if sessions_total is not None:
            record["sessions_total"] = sessions_total
        if sessions_failed is not None:
            record["sessions_failed"] = sessions_failed

        record["metrics"] = metrics

        if notes:
            record["notes"] = notes
        if extra:
            record.update(extra)

        # Append to JSONL
        with open(self.runs_file, "a") as f:
            f.write(json.dumps(record, default=str) + "\n")

        return record

    def list_runs(
        self,
        benchmark: Optional[str] = None,
        limit: int = 20,
    ) -> list[dict[str, Any]]:
        """Read recent runs, optionally filtered by benchmark."""
        if not self.runs_file.exists():
            return []

        runs = []
        for line in self.runs_file.read_text().splitlines():
            line = line.strip()
            if not line:
                continue
            try:
                run = json.loads(line)
                if benchmark and run.get("benchmark") != benchmark:
                    continue
                runs.append(run)
            except json.JSONDecodeError:
                continue

        return runs[-limit:]

    def latest_run(self, benchmark: Optional[str] = None) -> Optional[dict[str, Any]]:
        """Get the most recent run."""
        runs = self.list_runs(benchmark=benchmark, limit=1)
        return runs[0] if runs else None

    def compare(
        self,
        run_ids: Optional[list[str]] = None,
        benchmark: Optional[str] = None,
        metric_key: str = "f1_excl_adversarial",
    ) -> str:
        """Generate a comparison table for runs."""
        runs = self.list_runs(benchmark=benchmark, limit=50)
        if run_ids:
            runs = [r for r in runs if r["run_id"] in run_ids]

        if not runs:
            return "No runs found."

        lines = [
            f"{'Run ID':<30} {'Version':<8} {'Extraction':<25} {'Embedding':<30} {'Claims':>6} {metric_key:>12}",
            "-" * 120,
        ]
        for r in runs:
            score = r.get("metrics", {}).get(metric_key, "n/a")
            if isinstance(score, float):
                score = f"{score:.3f}"
            lines.append(
                f"{r.get('run_id', '?'):<30} "
                f"{r.get('version', '?'):<8} "
                f"{r.get('extraction_model', '?'):<25} "
                f"{r.get('embedding_model', '?'):<30} "
                f"{r.get('claims_extracted', '?'):>6} "
                f"{score:>12}"
            )

        return "\n".join(lines)
