#!/usr/bin/env python3
"""Backfill pipeline trace ledger rows from existing acquisition runs.

Dry-run is the default. This script never fabricates historical LLM traces:
old runs only get structural events plus an explicit backfill note.
"""
from __future__ import annotations

import argparse
import json
import sys
from dataclasses import asdict, is_dataclass
from pathlib import Path
from uuid import UUID

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

from core.config import CreationDbConfig  # noqa: E402
from core.db_session import transaction  # noqa: E402
from pipeline.postgres import PostgresPipelineRepository  # noqa: E402
from pipeline.tracing import TraceContext  # noqa: E402


def _model_dump(value):
    if hasattr(value, "model_dump"):
        return value.model_dump(mode="json")
    if is_dataclass(value):
        return asdict(value)
    if isinstance(value, dict):
        return value
    return dict(value)


def _acquisition_runs(conn, *, limit: int | None = None) -> list[dict]:
    sql = """
        SELECT ar.*,
               COUNT(DISTINCT aj.id)::int AS job_count,
               COUNT(DISTINCT ci.id)::int AS candidate_count
        FROM acquisition_runs ar
        LEFT JOIN acquisition_jobs aj ON aj.run_id = ar.id
        LEFT JOIN candidate_items ci ON ci.job_id = aj.id
        GROUP BY ar.id
        ORDER BY ar.created_at
    """
    if limit is not None:
        sql += " LIMIT %s"
        params = (limit,)
    else:
        params = ()
    return PostgresPipelineRepository(conn)._all(sql, params)


def _candidate_hits(conn, acquisition_run_id: UUID) -> list[dict]:
    return PostgresPipelineRepository(conn)._all(
        """
        SELECT
            aj.id AS acquisition_job_id,
            aj.query_id,
            aj.platform,
            ci.id AS item_id,
            ci.unique_key,
            ci.platform_item_id,
            ci.metadata,
            ci.source_payload
        FROM acquisition_jobs aj
        JOIN candidate_items ci ON ci.job_id = aj.id
        WHERE aj.run_id = %s
        ORDER BY aj.platform, ci.created_at
        """,
        (acquisition_run_id,),
    )


def backfill(*, env_file: str, apply: bool, limit: int | None = None) -> dict:
    db_config = CreationDbConfig.from_env(env_file)
    with transaction(db_config) as conn:
        repo = PostgresPipelineRepository(conn)
        runs = _acquisition_runs(conn, limit=limit)
        if not apply:
            return {
                "dry_run": True,
                "acquisition_run_count": len(runs),
                "candidate_hit_count": sum(int(row.get("candidate_count") or 0) for row in runs),
                "runs": [
                    {
                        "acquisition_run_id": str(row["id"]),
                        "run_key": row.get("run_key"),
                        "status": row.get("status"),
                        "job_count": row.get("job_count"),
                        "candidate_count": row.get("candidate_count"),
                    }
                    for row in runs
                ],
            }

        pipeline_count = 0
        hit_count = 0
        for row in runs:
            acquisition_run_id = row["id"]
            run = repo.create_pipeline_run(
                run_key=f"backfill:{acquisition_run_id}",
                batch_id=row.get("batch_id"),
                status=row.get("status") or "done",
                current_stage="ingest",
                metadata={
                    "source": "backfill_pipeline_traces",
                    "acquisition_run_id": str(acquisition_run_id),
                    "original_run_key": row.get("run_key"),
                },
            )
            pipeline_count += 1
            context = TraceContext(
                pipeline_run_id=run.id,
                acquisition_run_id=acquisition_run_id,
                stage="search",
            )
            repo.append_event(
                context=context,
                stage="query",
                event_type="backfilled_without_llm_trace",
                status="done",
                target_table="acquisition_runs",
                target_id=acquisition_run_id,
                payload={
                    "reason": "historical raw LLM requests and responses were not stored",
                    "job_count": row.get("job_count"),
                    "candidate_count": row.get("candidate_count"),
                },
            )
            for hit in _candidate_hits(conn, acquisition_run_id):
                metadata = hit.get("metadata") or {}
                repo.record_candidate_hit(
                    context=context.child(
                        acquisition_job_id=hit.get("acquisition_job_id"),
                        query_id=hit.get("query_id"),
                        item_id=hit.get("item_id"),
                        platform=hit.get("platform"),
                    ),
                    item_id=hit.get("item_id"),
                    platform=hit.get("platform"),
                    unique_key=hit.get("unique_key"),
                    platform_item_id=hit.get("platform_item_id"),
                    search_provider=metadata.get("search_provider"),
                    detail_provider=metadata.get("detail_provider"),
                    attempt_index=metadata.get("retry_attempt_index"),
                    page_index=metadata.get("page_index"),
                    page_rank=metadata.get("page_rank"),
                    candidate_rank=metadata.get("candidate_rank"),
                    source_cursor=metadata.get("source_cursor"),
                    is_duplicate_hit=metadata.get("acquisition_match_status") == "existing",
                    raw_candidate=metadata.get("matched_candidate") or {},
                    metadata={"source": "backfill_pipeline_traces"},
                )
                hit_count += 1
        return {
            "dry_run": False,
            "pipeline_run_count": pipeline_count,
            "candidate_hit_count": hit_count,
        }


def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--env-file", default=".env")
    parser.add_argument("--apply", action="store_true", help="Actually write trace rows. Default is dry-run.")
    parser.add_argument("--limit", type=int, default=None)
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
    args = parse_args(argv)
    print(json.dumps(
        backfill(env_file=args.env_file, apply=args.apply, limit=args.limit),
        ensure_ascii=False,
        default=str,
        indent=2,
    ))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
