"""PostgreSQL repository and best-effort writer for pipeline tracing."""
from __future__ import annotations

import logging
from typing import Any
from uuid import UUID

import psycopg2.extras

from core.config import CreationDbConfig
from core.db_session import transaction
from pipeline.models import PipelineJob, PipelineRun, PipelineRunEvent, PipelineStage
from pipeline.tracing import TraceConfig, TraceContext, sanitize_payload

Json = psycopg2.extras.Json
psycopg2.extras.register_uuid()
logger = logging.getLogger(__name__)


class PostgresPipelineRepository:
    """Repository for orchestration state and trace ledger rows.

    This class owns no transaction boundary; API dependencies pass a pooled
    connection, while the best-effort trace writer opens short transactions.
    """

    def __init__(self, conn: Any):
        self.conn = conn

    def _one(self, sql: str, params: tuple[Any, ...]) -> dict[str, Any]:
        with self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
            cur.execute(sql, params)
            row = cur.fetchone()
            if row is None:
                raise RuntimeError("expected one row, got none")
            return dict(row)

    def _one_or_none(self, sql: str, params: tuple[Any, ...]) -> dict[str, Any] | None:
        with self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
            cur.execute(sql, params)
            row = cur.fetchone()
            return dict(row) if row else None

    def _all(self, sql: str, params: tuple[Any, ...]) -> list[dict[str, Any]]:
        with self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
            cur.execute(sql, params)
            return [dict(row) for row in cur.fetchall()]

    def create_pipeline_run(
        self,
        *,
        run_key: str | None = None,
        batch_id: UUID | None = None,
        status: str = "pending",
        current_stage: str | None = None,
        config: dict[str, Any] | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> PipelineRun:
        row = self._one(
            """
            INSERT INTO pipeline_runs(
                run_key, batch_id, status, current_stage, config, metadata, started_at
            )
            VALUES (%s, %s, %s, %s, %s, %s, CASE WHEN %s = 'running' THEN now() ELSE NULL END)
            ON CONFLICT (run_key) DO UPDATE SET
                batch_id = COALESCE(EXCLUDED.batch_id, pipeline_runs.batch_id),
                status = EXCLUDED.status,
                current_stage = COALESCE(EXCLUDED.current_stage, pipeline_runs.current_stage),
                config = pipeline_runs.config || EXCLUDED.config,
                metadata = pipeline_runs.metadata || EXCLUDED.metadata,
                started_at = COALESCE(pipeline_runs.started_at, EXCLUDED.started_at)
            RETURNING *
            """,
            (
                run_key,
                batch_id,
                status,
                current_stage,
                Json(config or {}),
                Json(metadata or {}),
                status,
            ),
        )
        return _pipeline_run(row)

    def mark_pipeline_run_status(
        self,
        run_id: UUID,
        *,
        status: str,
        current_stage: str | None = None,
        error_message: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> PipelineRun:
        row = self._one(
            """
            UPDATE pipeline_runs SET
                status = %s,
                current_stage = COALESCE(%s, current_stage),
                error_message = %s,
                metadata = CASE WHEN %s THEN metadata || %s ELSE metadata END,
                finished_at = CASE WHEN %s IN ('done', 'partial', 'failed') THEN now() ELSE finished_at END
            WHERE id = %s
            RETURNING *
            """,
            (
                status,
                current_stage,
                error_message,
                metadata is not None,
                Json(metadata or {}),
                status,
                run_id,
            ),
        )
        return _pipeline_run(row)

    def get_pipeline_run(self, run_id: UUID) -> PipelineRun:
        return _pipeline_run(self._one("SELECT * FROM pipeline_runs WHERE id = %s", (run_id,)))

    def get_pipeline_run_by_acquisition_run(self, acquisition_run_id: UUID) -> PipelineRun:
        row = self._one(
            """
            SELECT pr.* FROM pipeline_runs pr
            JOIN pipeline_run_events pre ON pre.pipeline_run_id = pr.id
            WHERE pre.acquisition_run_id = %s
            ORDER BY pre.created_at DESC
            LIMIT 1
            """,
            (acquisition_run_id,),
        )
        return _pipeline_run(row)

    def save_pipeline_job(
        self,
        *,
        run_id: UUID,
        stage: PipelineStage | str,
        target_id: UUID | None = None,
        target_table: str | None = None,
        status: str = "pending",
        metadata: dict[str, Any] | None = None,
    ) -> PipelineJob:
        row = self._one(
            """
            INSERT INTO pipeline_jobs(
                pipeline_run_id, stage, target_table, target_id, status, metadata, started_at
            )
            VALUES (%s, %s, %s, %s, %s, %s, CASE WHEN %s = 'running' THEN now() ELSE NULL END)
            RETURNING *
            """,
            (run_id, stage, target_table, target_id, status, Json(metadata or {}), status),
        )
        return _pipeline_job(row)

    def mark_job_status(
        self,
        job_id: UUID,
        *,
        status: str,
        error_message: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> PipelineJob:
        row = self._one(
            """
            UPDATE pipeline_jobs SET
                status = %s,
                error_message = %s,
                metadata = CASE WHEN %s THEN metadata || %s ELSE metadata END,
                finished_at = CASE WHEN %s IN ('done', 'partial', 'failed', 'skipped') THEN now() ELSE finished_at END
            WHERE id = %s
            RETURNING *
            """,
            (
                status,
                error_message,
                metadata is not None,
                Json(metadata or {}),
                status,
                job_id,
            ),
        )
        return _pipeline_job(row)

    def append_event(
        self,
        *,
        context: TraceContext,
        stage: str,
        event_type: str,
        status: str | None = None,
        severity: str = "info",
        target_table: str | None = None,
        target_id: UUID | None = None,
        message: str | None = None,
        payload: dict[str, Any] | None = None,
        error_message: str | None = None,
        duration_ms: int | None = None,
        attempt_index: int | None = None,
    ) -> PipelineRunEvent:
        row = self._one(
            """
            INSERT INTO pipeline_run_events(
                pipeline_run_id, pipeline_job_id, stage, event_type, status, severity,
                target_table, target_id, acquisition_run_id, acquisition_job_id,
                query_id, item_id, decode_job_id, decode_result_id, payload_draft_id,
                ingest_record_id, platform, attempt_index, message, payload,
                error_message, duration_ms
            )
            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
            RETURNING *
            """,
            (
                context.pipeline_run_id,
                context.pipeline_job_id,
                stage,
                event_type,
                status,
                severity,
                target_table,
                target_id,
                context.acquisition_run_id,
                context.acquisition_job_id,
                context.query_id,
                context.item_id,
                context.decode_job_id,
                context.decode_result_id,
                context.payload_draft_id,
                context.ingest_record_id,
                context.platform,
                attempt_index,
                message,
                Json(payload or {}),
                error_message,
                duration_ms,
            ),
        )
        return PipelineRunEvent.model_validate(_event_row(row))

    def record_candidate_hit(
        self,
        *,
        context: TraceContext,
        item_id: UUID | None,
        platform: str,
        unique_key: str | None = None,
        platform_item_id: str | None = None,
        search_provider: str | None = None,
        detail_provider: str | None = None,
        attempt_index: int | None = None,
        page_index: int | None = None,
        page_rank: int | None = None,
        candidate_rank: int | None = None,
        source_cursor: str | None = None,
        is_duplicate_hit: bool = False,
        raw_candidate: dict[str, Any] | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        return self._one(
            """
            INSERT INTO candidate_item_hits(
                pipeline_run_id, acquisition_run_id, acquisition_job_id,
                query_id, item_id, platform, unique_key, platform_item_id,
                search_provider, detail_provider, attempt_index, page_index,
                page_rank, candidate_rank, source_cursor, is_duplicate_hit,
                raw_candidate, metadata
            )
            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
            RETURNING *
            """,
            (
                context.pipeline_run_id,
                context.acquisition_run_id,
                context.acquisition_job_id,
                context.query_id,
                item_id,
                platform,
                unique_key,
                platform_item_id,
                search_provider,
                detail_provider,
                attempt_index,
                page_index,
                page_rank,
                candidate_rank,
                source_cursor,
                is_duplicate_hit,
                Json(raw_candidate or {}),
                Json(metadata or {}),
            ),
        )

    def save_llm_call_trace(
        self,
        *,
        context: TraceContext,
        stage: str,
        substage: str | None = None,
        provider: str | None = None,
        model_name: str | None = None,
        endpoint: str | None = None,
        prompt_name: str | None = None,
        prompt_hash: str | None = None,
        request_payload: dict[str, Any] | None = None,
        response_payload: dict[str, Any] | None = None,
        parsed_payload: dict[str, Any] | None = None,
        status: str = "pending",
        error_message: str | None = None,
        latency_ms: int | None = None,
        attempt_index: int | None = None,
    ) -> dict[str, Any]:
        return self._one(
            """
            INSERT INTO llm_call_traces(
                pipeline_run_id, pipeline_job_id, stage, substage, provider,
                model_name, endpoint, prompt_name, prompt_hash, trace_context,
                request_payload, response_payload, parsed_payload, status,
                error_message, latency_ms, attempt_index
            )
            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
            RETURNING *
            """,
            (
                context.pipeline_run_id,
                context.pipeline_job_id,
                stage,
                substage,
                provider,
                model_name,
                endpoint,
                prompt_name,
                prompt_hash,
                Json(context.as_payload()),
                Json(request_payload or {}),
                Json(response_payload or {}),
                Json(parsed_payload or {}),
                status,
                error_message,
                latency_ms,
                attempt_index,
            ),
        )

    def list_timeline(self, run_id: UUID) -> list[dict[str, Any]]:
        return self._all(
            """
            SELECT * FROM pipeline_run_events
            WHERE pipeline_run_id = %s
            ORDER BY created_at, id
            """,
            (run_id,),
        )

    def list_jobs(self, run_id: UUID) -> list[dict[str, Any]]:
        return self._all(
            """
            SELECT * FROM pipeline_jobs
            WHERE pipeline_run_id = %s
            ORDER BY created_at, id
            """,
            (run_id,),
        )

    def list_candidate_hits(self, run_id: UUID) -> list[dict[str, Any]]:
        return self._all(
            """
            SELECT * FROM candidate_item_hits
            WHERE pipeline_run_id = %s
            ORDER BY created_at, page_index NULLS LAST, page_rank NULLS LAST, id
            """,
            (run_id,),
        )

    def list_llm_call_traces(self, run_id: UUID, *, limit: int = 500) -> list[dict[str, Any]]:
        return self._all(
            """
            SELECT * FROM llm_call_traces
            WHERE pipeline_run_id = %s
            ORDER BY created_at, id
            LIMIT %s
            """,
            (run_id, limit),
        )

    def get_timeline_bundle(self, run_id: UUID) -> dict[str, Any]:
        return {
            "events": self.list_timeline(run_id),
            "jobs": self.list_jobs(run_id),
            "candidate_hits": self.list_candidate_hits(run_id),
            "llm_call_traces": self.list_llm_call_traces(run_id),
        }

    def get_resume_cursor(self, run_id: UUID):
        return None


class PostgresTraceWriter:
    """Best-effort writer that never lets tracing break the business flow."""

    def __init__(self, *, db_config: CreationDbConfig, config: TraceConfig):
        self.db_config = db_config
        self.config = config

    def event(self, *, context: TraceContext, stage: str, event_type: str, **kwargs: Any) -> None:
        self._best_effort(
            lambda repo: repo.append_event(
                context=context,
                stage=stage,
                event_type=event_type,
                payload=sanitize_payload(kwargs.pop("payload", {}) or {}, max_chars=self.config.max_payload_chars),
                **kwargs,
            )
        )

    def candidate_hit(self, *, context: TraceContext, **kwargs: Any) -> None:
        self._best_effort(
            lambda repo: repo.record_candidate_hit(
                context=context,
                raw_candidate=sanitize_payload(kwargs.pop("raw_candidate", {}) or {}, max_chars=self.config.max_payload_chars),
                metadata=sanitize_payload(kwargs.pop("metadata", {}) or {}, max_chars=self.config.max_payload_chars),
                **kwargs,
            )
        )

    def llm_call(self, *, context: TraceContext, stage: str, substage: str | None = None, **kwargs: Any) -> None:
        request_payload = kwargs.pop("request_payload", {}) if self.config.capture_request else {}
        response_payload = kwargs.pop("response_payload", {}) if self.config.capture_response else {}
        parsed_payload = kwargs.pop("parsed_payload", {})
        self._best_effort(
            lambda repo: repo.save_llm_call_trace(
                context=context,
                stage=stage,
                substage=substage,
                request_payload=sanitize_payload(request_payload or {}, max_chars=self.config.max_payload_chars),
                response_payload=sanitize_payload(response_payload or {}, max_chars=self.config.max_payload_chars),
                parsed_payload=sanitize_payload(parsed_payload or {}, max_chars=self.config.max_payload_chars),
                **kwargs,
            )
        )

    def _best_effort(self, fn) -> None:
        if not self.config.enabled:
            return
        try:
            with transaction(self.db_config) as conn:
                fn(PostgresPipelineRepository(conn))
        except Exception as exc:  # pragma: no cover - defensive isolation
            logger.warning("pipeline trace write skipped: %s", exc)


def _pipeline_run(row: dict[str, Any]) -> PipelineRun:
    return PipelineRun(
        id=row.get("id"),
        run_key=row.get("run_key"),
        batch_id=row.get("batch_id"),
        status=row.get("status") or "pending",
        current_stage=row.get("current_stage"),
        metadata={
            **(row.get("config") or {}),
            **(row.get("metadata") or {}),
        },
        error_message=row.get("error_message"),
        started_at=row.get("started_at"),
        finished_at=row.get("finished_at"),
    )


def _pipeline_job(row: dict[str, Any]) -> PipelineJob:
    return PipelineJob(
        id=row.get("id"),
        run_id=row.get("pipeline_run_id"),
        stage=row.get("stage"),
        status=row.get("status") or "pending",
        target_table=row.get("target_table"),
        target_id=row.get("target_id"),
        attempt_count=row.get("attempt_count") or 0,
        metadata=row.get("metadata") or {},
        error_message=row.get("error_message"),
        started_at=row.get("started_at"),
        finished_at=row.get("finished_at"),
    )


def _event_row(row: dict[str, Any]) -> dict[str, Any]:
    out = dict(row)
    out["pipeline_run_id"] = out.pop("pipeline_run_id", None)
    return out
