"""Preference pairing service for jokester-p agent.

Creates preference pairs from rated jokes for DPO training.
Uses core/training infrastructure for pairing algorithms.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from sqlalchemy import text

from llm_gent.core.training import (
    StarFilter,
    StarPairingResult,
    StarRatedItem,
    pair_by_margin,
    pair_by_threshold,
)


if TYPE_CHECKING:
    from appinfra.db.pg import PG
    from appinfra.log import Logger


# Re-export types for backward compatibility
__all__ = ["PairingService", "StarFilter"]


class PairingService:
    """Service for creating preference pairs from rated jokes.

    Fetches rated jokes from the database and uses core pairing algorithms
    to create preference pairs for DPO training.
    """

    def __init__(self, lg: Logger, pg: PG, context_key: str) -> None:
        self._lg = lg
        self._pg = pg
        self._context_key = context_key

    def get_rated_jokes(
        self, max_chars: int | None = None, model: str | None = None
    ) -> list[StarRatedItem]:
        """Get all rated jokes sorted by stars (desc)."""
        joins_sql, filters_sql, params = self._build_rated_jokes_filters(max_chars, model)
        sql = text(f"""
            SELECT DISTINCT ON (af.id)
                af.id, af.content, (afd.context->>'stars')::int as stars
            FROM atomic_facts af
            JOIN atomic_feedback_details afd ON af.id = afd.fact_id
            {joins_sql}
            WHERE af.context_key = :context_key
              AND afd.context->>'stars' IS NOT NULL {filters_sql}
            ORDER BY af.id, afd.id DESC
        """)
        with self._pg.connect() as conn:
            rows = conn.execute(sql, params).fetchall()
        rated = [StarRatedItem(id=r[0], content=r[1], score=r[2]) for r in rows]
        rated.sort(key=lambda item: (-item.score, item.id))
        return rated

    def _build_rated_jokes_filters(
        self, max_chars: int | None, model: str | None
    ) -> tuple[str, str, dict[str, object]]:
        """Build SQL fragments and params for rated jokes query."""
        joins: list[str] = []
        filters: list[str] = []
        params: dict[str, object] = {"context_key": self._context_key}

        if max_chars:
            joins.append("JOIN atomic_solution_details asd ON asd.fact_id = af.id")
            filters.append("AND length(asd.answer_text) < :max_chars")
            params["max_chars"] = max_chars

        if model:
            joins.append("JOIN agent_jokester_model_usage u ON u.fact_id = af.id")
            filters.append("AND u.model_name LIKE :model_pattern")
            params["model_pattern"] = f"%{model}%"

        return "\n            ".join(joins), " ".join(filters), params

    def _validate_strategy(self, strategy: str) -> None:
        """Validate pairing strategy."""
        if strategy not in ("relative", "threshold"):
            raise ValueError(
                f"Unknown pairing strategy: {strategy!r}. Expected 'relative' or 'threshold'."
            )

    def create_pairs(
        self,
        strategy: str = "relative",
        margin: int = 1,
        high_threshold: int = 4,
        low_threshold: int = 2,
        min_pairs: int | None = None,
        max_pairs: int | None = None,
        max_chars: int | None = None,
        no_reuse: bool = False,
        model: str | None = None,
        chosen_stars: StarFilter | None = None,
        rejected_stars: StarFilter | None = None,
    ) -> StarPairingResult:
        """Create preference pairs from rated jokes.

        Args:
            strategy: "relative" or "threshold"
            margin: Minimum star difference for relative pairing
            high_threshold: Minimum stars for chosen (threshold strategy)
            low_threshold: Maximum stars for rejected (threshold strategy)
            min_pairs: Minimum pairs to generate (reuses chosen jokes if needed)
            max_pairs: Maximum pairs to generate (caps output)
            max_chars: Only include jokes under this character length
            no_reuse: If True, each chosen joke is used at most once (1:1 pairing)
            model: Only include jokes generated by this model (substring match)
            chosen_stars: Filter for chosen jokes (e.g., StarFilter(3, ">=") for 3+)
            rejected_stars: Filter for rejected jokes (e.g., StarFilter(2, "==") for 2)

        Returns:
            PairingResult with pairs and metadata.
        """
        self._validate_strategy(strategy)
        self._lg.debug(
            "fetching rated jokes...",
            extra={"max_chars": max_chars, "model": model},
        )
        rated = self.get_rated_jokes(max_chars=max_chars, model=model)
        self._lg.debug("fetched rated jokes", extra={"count": len(rated)})

        self._lg.debug("creating pairs...", extra={"strategy": strategy, "margin": margin})

        if strategy == "relative":
            result = pair_by_margin(
                rated,
                margin=margin,
                min_pairs=min_pairs,
                max_pairs=max_pairs,
                no_reuse=no_reuse,
                chosen_filter=chosen_stars,
                rejected_filter=rejected_stars,
            )
        else:
            result = pair_by_threshold(
                rated,
                high_threshold=high_threshold,
                low_threshold=low_threshold,
                max_pairs=max_pairs,
            )

        self._lg.debug("created pairs", extra={"count": len(result.pairs)})
        return result
