"""Data Access Object for highlight candidate audit trail.

Supports comprehensive audit trail for clip generation A/B testing by persisting
ALL highlight candidates (accepted AND rejected) with full scoring data.
"""

import json
from datetime import datetime
from typing import List, Optional

from common.database import DBConfig, Db
from common.logging import get_logger, span
from common.models.highlight_candidate import (
    HighlightCandidate,
    HighlightCandidateCreate,
    CandidateDecision,
)

logger = get_logger(__name__)

SELECT_CANDIDATE_QUERY = """
    SELECT id,
           stream_id,
           user_id,
           peak_timestamp,
           start_timestamp,
           end_timestamp,
           created_at,
           confidence,
           combined_score,
           dimension_scores,
           prompt_version,
           game_config_name,
           decision,
           rejection_reason,
           description,
           trigger_type,
           source_segment_id,
           clip_id,
           deleted_at
    FROM highlight_candidates
"""


class HighlightCandidateDAO:
    """Data Access Object for highlight candidates in PostgreSQL."""

    def __init__(self, db: Optional[Db] = None):
        """Initialize with an existing DB connection or create a new one."""
        self.db = db if db else Db(DBConfig())
        logger.debug(
            "HighlightCandidateDAO initialized",
            extra={"component": "HighlightCandidateDAO"},
        )

    async def create_candidate(
        self, candidate: HighlightCandidateCreate
    ) -> Optional[HighlightCandidate]:
        """Create a new highlight candidate record.

        Args:
            candidate: The candidate data to create

        Returns:
            The created candidate with ID, or None on failure
        """
        with span(
            logger,
            "create_highlight_candidate",
            {
                "stream_id": candidate.stream_id,
                "decision": candidate.decision.value,
            },
            log_entry=False,
            log_exit=False,
        ):
            columns = [
                "stream_id",
                "user_id",
                "peak_timestamp",
                "start_timestamp",
                "end_timestamp",
                "confidence",
                "combined_score",
                "dimension_scores",
                "prompt_version",
                "game_config_name",
                "decision",
                "rejection_reason",
                "description",
                "trigger_type",
                "source_segment_id",
                "clip_id",
            ]

            dimension_scores_json = json.dumps(candidate.dimension_scores)
            rejection_reason_value = (
                candidate.rejection_reason.value if candidate.rejection_reason else None
            )

            values = (
                candidate.stream_id,
                candidate.user_id,
                candidate.peak_timestamp,
                candidate.start_timestamp,
                candidate.end_timestamp,
                candidate.confidence,
                candidate.combined_score,
                dimension_scores_json,
                candidate.prompt_version,
                candidate.game_config_name,
                candidate.decision.value,
                rejection_reason_value,
                candidate.description,
                candidate.trigger_type,
                candidate.source_segment_id,
                candidate.clip_id,
            )

            try:
                result = await self.db.insert("highlight_candidates", columns, values)
                if not result:
                    logger.error(
                        "Failed to create highlight candidate",
                        extra={
                            "stream_id": candidate.stream_id,
                            "decision": candidate.decision.value,
                        },
                    )
                    return None

                return await self.get_by_id(result)
            except Exception as e:
                logger.error(
                    "Error creating highlight candidate",
                    extra={
                        "stream_id": candidate.stream_id,
                        "error": str(e),
                    },
                    exc_info=e,
                )
                return None

    async def batch_create_candidates(
        self, candidates: List[HighlightCandidateCreate]
    ) -> int:
        """Create multiple highlight candidate records efficiently.

        This is the primary method for persisting candidates during stream processing.

        Args:
            candidates: List of candidates to create

        Returns:
            Number of candidates successfully inserted
        """
        if not candidates:
            return 0

        with span(
            logger,
            "batch_create_highlight_candidates",
            {"count": len(candidates)},
            log_entry=False,
            log_exit=False,
        ):
            columns = [
                "stream_id",
                "user_id",
                "peak_timestamp",
                "start_timestamp",
                "end_timestamp",
                "confidence",
                "combined_score",
                "dimension_scores",
                "prompt_version",
                "game_config_name",
                "decision",
                "rejection_reason",
                "description",
                "trigger_type",
                "source_segment_id",
                "clip_id",
            ]

            values_list = []
            for c in candidates:
                dimension_scores_json = json.dumps(c.dimension_scores)
                rejection_reason_value = (
                    c.rejection_reason.value if c.rejection_reason else None
                )
                values_list.append(
                    (
                        c.stream_id,
                        c.user_id,
                        c.peak_timestamp,
                        c.start_timestamp,
                        c.end_timestamp,
                        c.confidence,
                        c.combined_score,
                        dimension_scores_json,
                        c.prompt_version,
                        c.game_config_name,
                        c.decision.value,
                        rejection_reason_value,
                        c.description,
                        c.trigger_type,
                        c.source_segment_id,
                        c.clip_id,
                    )
                )

            # Build batch insert query
            placeholders = ", ".join(["%s"] * len(columns))
            values_placeholders = ", ".join(
                [f"({placeholders})" for _ in range(len(values_list))]
            )
            columns_str = ", ".join(columns)

            query = f"""
                INSERT INTO highlight_candidates ({columns_str})
                VALUES {values_placeholders}
            """

            # Flatten values for execution
            flat_values = [v for row in values_list for v in row]

            try:
                await self.db.execute_commit(query, tuple(flat_values))
                logger.debug(
                    "Batch created highlight candidates",
                    extra={"count": len(candidates)},
                )
                return len(candidates)
            except Exception as e:
                logger.error(
                    "Error batch creating highlight candidates",
                    extra={"count": len(candidates), "error": str(e)},
                    exc_info=e,
                )
                return 0

    async def get_by_id(self, candidate_id: int) -> Optional[HighlightCandidate]:
        """Get a highlight candidate by ID.

        Args:
            candidate_id: The ID of the candidate

        Returns:
            The candidate if found, None otherwise
        """
        with span(
            logger,
            "get_highlight_candidate_by_id",
            {"candidate_id": candidate_id},
            log_entry=False,
            log_exit=False,
        ):
            query = SELECT_CANDIDATE_QUERY + " WHERE id = %s AND deleted_at IS NULL"
            row = await self.db.fetch_one(query, (candidate_id,))
            return self._row_to_candidate(row) if row else None

    async def link_to_clip(self, candidate_id: int, clip_id: int) -> bool:
        """Link a candidate to its created clip.

        Args:
            candidate_id: The ID of the candidate
            clip_id: The ID of the created clip

        Returns:
            True if updated successfully
        """
        with span(
            logger,
            "link_candidate_to_clip",
            {"candidate_id": candidate_id, "clip_id": clip_id},
            log_entry=False,
            log_exit=False,
        ):
            try:
                await self.db.execute_commit(
                    """
                    UPDATE highlight_candidates
                    SET clip_id = %s
                    WHERE id = %s AND deleted_at IS NULL
                    """,
                    (clip_id, candidate_id),
                )
                return True
            except Exception as e:
                logger.error(
                    "Error linking candidate to clip",
                    extra={
                        "candidate_id": candidate_id,
                        "clip_id": clip_id,
                        "error": str(e),
                    },
                    exc_info=e,
                )
                return False

    async def get_rejected_above_score(
        self, min_score: float, limit: int = 100
    ) -> List[HighlightCandidate]:
        """Find high-scoring rejected candidates (potential false negatives).

        Useful for identifying good moments that weren't clipped due to thresholds.

        Args:
            min_score: Minimum combined score threshold
            limit: Maximum number of results

        Returns:
            List of rejected candidates with high scores
        """
        with span(
            logger,
            "get_rejected_above_score",
            {"min_score": min_score, "limit": limit},
            log_entry=False,
            log_exit=False,
        ):
            query = (
                SELECT_CANDIDATE_QUERY
                + """
                WHERE decision = %s
                  AND combined_score > %s
                  AND deleted_at IS NULL
                ORDER BY combined_score DESC
                LIMIT %s
            """
            )
            rows = await self.db.fetch_all(
                query, (CandidateDecision.REJECTED.value, min_score, limit)
            )
            return [self._row_to_candidate(row) for row in (rows or [])]

    async def get_by_prompt_version(
        self, prompt_version: str, limit: int = 1000
    ) -> List[HighlightCandidate]:
        """Get candidates by prompt version for A/B testing analysis.

        Args:
            prompt_version: The prompt version to filter by
            limit: Maximum number of results

        Returns:
            List of candidates with the specified prompt version
        """
        with span(
            logger,
            "get_candidates_by_prompt_version",
            {"prompt_version": prompt_version, "limit": limit},
            log_entry=False,
            log_exit=False,
        ):
            query = (
                SELECT_CANDIDATE_QUERY
                + """
                WHERE prompt_version = %s
                  AND deleted_at IS NULL
                ORDER BY created_at DESC
                LIMIT %s
            """
            )
            rows = await self.db.fetch_all(query, (prompt_version, limit))
            return [self._row_to_candidate(row) for row in (rows or [])]

    async def get_by_stream_id(self, stream_id: int) -> List[HighlightCandidate]:
        """Get all candidates for a specific stream.

        Args:
            stream_id: The stream ID

        Returns:
            List of candidates for the stream
        """
        with span(
            logger,
            "get_candidates_by_stream_id",
            {"stream_id": stream_id},
            log_entry=False,
            log_exit=False,
        ):
            query = (
                SELECT_CANDIDATE_QUERY
                + """
                WHERE stream_id = %s
                  AND deleted_at IS NULL
                ORDER BY peak_timestamp ASC
            """
            )
            rows = await self.db.fetch_all(query, (stream_id,))
            return [self._row_to_candidate(row) for row in (rows or [])]

    async def cleanup_expired(self, cutoff_date: datetime) -> int:
        """Soft-delete candidates older than the cutoff date.

        Used by retention policy to clean up old records.

        Args:
            cutoff_date: Candidates created before this date will be soft-deleted

        Returns:
            Number of candidates soft-deleted
        """
        with span(
            logger,
            "cleanup_expired_candidates",
            {"cutoff_date": cutoff_date.isoformat()},
            log_entry=False,
            log_exit=False,
        ):
            try:
                result = await self.db.execute_commit(
                    """
                    UPDATE highlight_candidates
                    SET deleted_at = NOW()
                    WHERE created_at < %s
                      AND deleted_at IS NULL
                    """,
                    (cutoff_date,),
                )
                count = result if result else 0
                if count > 0:
                    logger.info(
                        "Cleaned up expired highlight candidates",
                        extra={"count": count, "cutoff_date": cutoff_date.isoformat()},
                    )
                return count
            except Exception as e:
                logger.error(
                    "Error cleaning up expired candidates",
                    extra={"cutoff_date": cutoff_date.isoformat(), "error": str(e)},
                    exc_info=e,
                )
                return 0

    async def get_acceptance_rate_by_version(self) -> List[dict]:
        """Get acceptance rate statistics grouped by prompt version.

        Useful for A/B testing comparison.

        Returns:
            List of dicts with prompt_version, total, accepted, acceptance_rate
        """
        with span(
            logger,
            "get_acceptance_rate_by_version",
            log_entry=False,
            log_exit=False,
        ):
            query = """
                SELECT prompt_version,
                       COUNT(*) as total,
                       SUM(CASE WHEN decision = 'accepted' THEN 1 ELSE 0 END) as accepted,
                       ROUND(
                           100.0 * SUM(CASE WHEN decision = 'accepted' THEN 1 ELSE 0 END) / COUNT(*),
                           2
                       ) as acceptance_rate
                FROM highlight_candidates
                WHERE deleted_at IS NULL
                GROUP BY prompt_version
                ORDER BY prompt_version
            """
            rows = await self.db.fetch_all(query, ())
            return [
                {
                    "prompt_version": row[0],
                    "total": row[1],
                    "accepted": row[2],
                    "acceptance_rate": float(row[3]) if row[3] else 0.0,
                }
                for row in (rows or [])
            ]

    async def get_rejection_distribution(
        self, prompt_version: Optional[str] = None
    ) -> List[dict]:
        """Get rejection reason distribution, optionally filtered by prompt version.

        Args:
            prompt_version: Optional prompt version to filter by

        Returns:
            List of dicts with prompt_version, rejection_reason, count
        """
        with span(
            logger,
            "get_rejection_distribution",
            {"prompt_version": prompt_version},
            log_entry=False,
            log_exit=False,
        ):
            if prompt_version:
                query = """
                    SELECT prompt_version, rejection_reason, COUNT(*) as count
                    FROM highlight_candidates
                    WHERE decision = 'rejected'
                      AND prompt_version = %s
                      AND deleted_at IS NULL
                    GROUP BY prompt_version, rejection_reason
                    ORDER BY count DESC
                """
                rows = await self.db.fetch_all(query, (prompt_version,))
            else:
                query = """
                    SELECT prompt_version, rejection_reason, COUNT(*) as count
                    FROM highlight_candidates
                    WHERE decision = 'rejected'
                      AND deleted_at IS NULL
                    GROUP BY prompt_version, rejection_reason
                    ORDER BY prompt_version, count DESC
                """
                rows = await self.db.fetch_all(query, ())

            return [
                {
                    "prompt_version": row[0],
                    "rejection_reason": row[1],
                    "count": row[2],
                }
                for row in (rows or [])
            ]

    def _row_to_candidate(self, row) -> HighlightCandidate:
        """Convert a database row to a HighlightCandidate model.

        Column order matches SELECT_CANDIDATE_QUERY:
        0: id, 1: stream_id, 2: user_id, 3: peak_timestamp, 4: start_timestamp,
        5: end_timestamp, 6: created_at, 7: confidence, 8: combined_score,
        9: dimension_scores, 10: prompt_version, 11: game_config_name,
        12: decision, 13: rejection_reason, 14: description, 15: trigger_type,
        16: source_segment_id, 17: clip_id, 18: deleted_at
        """
        from common.models.highlight_candidate import RejectionReason

        # Parse dimension_scores from JSONB
        dimension_scores = row[9]
        if isinstance(dimension_scores, str):
            dimension_scores = json.loads(dimension_scores)

        # Parse enums
        decision = CandidateDecision(row[12])
        rejection_reason = RejectionReason(row[13]) if row[13] else None

        return HighlightCandidate(
            id=row[0],
            stream_id=row[1],
            user_id=row[2],
            peak_timestamp=row[3],
            start_timestamp=row[4],
            end_timestamp=row[5],
            created_at=row[6],
            confidence=row[7],
            combined_score=row[8],
            dimension_scores=dimension_scores,
            prompt_version=row[10],
            game_config_name=row[11],
            decision=decision,
            rejection_reason=rejection_reason,
            description=row[14],
            trigger_type=row[15],
            source_segment_id=row[16],
            clip_id=row[17],
            deleted_at=row[18],
        )
