"""Hybrid search for similar fixes.

Combines vector similarity search with keyword (BM25) search
using Reciprocal Rank Fusion for optimal results.
"""

from __future__ import annotations

import logging
from dataclasses import asdict, dataclass
from typing import TYPE_CHECKING, Any, Dict, List, Optional

if TYPE_CHECKING:
    from autodebug.learning.embeddings import EmbeddingService

logger = logging.getLogger(__name__)


@dataclass
class SimilarFix:
    """A similar fix found via search."""

    fix_id: str
    similarity_score: float
    vector_score: float
    keyword_score: float
    exception_type: str
    root_cause: Optional[str]
    patch: str
    feedback: Optional[str]
    success: bool

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)


class FixSearchService:
    """Hybrid search for similar fixes using pgvector."""

    def __init__(
        self,
        db_pool,
        embedding_service: EmbeddingService,
        vector_weight: float = 0.6,
        keyword_weight: float = 0.4,
    ):
        """Initialize search service.

        Args:
            db_pool: Database connection pool.
            embedding_service: Service for generating embeddings.
            vector_weight: Weight for vector similarity (0-1).
            keyword_weight: Weight for keyword similarity (0-1).
        """
        self.db = db_pool
        self.embeddings = embedding_service
        self.vector_weight = vector_weight
        self.keyword_weight = keyword_weight

    async def find_similar_fixes(
        self,
        failure_context: Dict[str, Any],
        org_id: Optional[str] = None,
        top_k: int = 5,
        min_score: float = 0.3,
        only_accepted: bool = True,
    ) -> List[SimilarFix]:
        """Find similar past fixes using hybrid search.

        Combines vector similarity (semantic) with keyword search (exact match)
        using Reciprocal Rank Fusion for better results.

        Args:
            failure_context: Current failure information.
            org_id: Filter by organization (optional).
            top_k: Maximum results to return.
            min_score: Minimum combined score threshold.
            only_accepted: Only return accepted fixes.

        Returns:
            List of similar fixes sorted by relevance.
        """
        # Generate embedding for current failure
        embedding = await self.embeddings.embed_fix_record(failure_context)

        # Build keyword query from failure context
        keyword_parts = [
            failure_context.get("exception_type", ""),
            failure_context.get("exception_message", ""),
            failure_context.get("failing_file", ""),
        ]
        keyword_query = " ".join(p for p in keyword_parts if p)

        # Hybrid search query using pgvector and full-text search
        # Uses Reciprocal Rank Fusion to combine results
        query = """
            WITH vector_search AS (
                SELECT
                    id,
                    1 - (embedding <=> $1::vector) as vector_score,
                    ROW_NUMBER() OVER (ORDER BY embedding <=> $1::vector) as vector_rank
                FROM fix_records
                WHERE embedding IS NOT NULL
                AND ($2::uuid IS NULL OR org_id = $2)
                AND ($3::boolean = false OR feedback = 'accepted')
                ORDER BY embedding <=> $1::vector
                LIMIT $4 * 3
            ),
            keyword_search AS (
                SELECT
                    id,
                    ts_rank_cd(
                        to_tsvector('english',
                            coalesce(exception_type, '') || ' ' ||
                            coalesce(exception_message, '') || ' ' ||
                            coalesce(root_cause, '') || ' ' ||
                            coalesce(failing_file, '')
                        ),
                        plainto_tsquery('english', $5),
                        32
                    ) as keyword_score,
                    ROW_NUMBER() OVER (
                        ORDER BY ts_rank_cd(
                            to_tsvector('english',
                                coalesce(exception_type, '') || ' ' ||
                                coalesce(exception_message, '') || ' ' ||
                                coalesce(root_cause, '') || ' ' ||
                                coalesce(failing_file, '')
                            ),
                            plainto_tsquery('english', $5),
                            32
                        ) DESC
                    ) as keyword_rank
                FROM fix_records
                WHERE ($2::uuid IS NULL OR org_id = $2)
                AND ($3::boolean = false OR feedback = 'accepted')
                AND length($5) > 2
                AND to_tsvector('english',
                    coalesce(exception_type, '') || ' ' ||
                    coalesce(exception_message, '') || ' ' ||
                    coalesce(root_cause, '') || ' ' ||
                    coalesce(failing_file, '')
                ) @@ plainto_tsquery('english', $5)
                LIMIT $4 * 3
            )
            SELECT
                f.id,
                f.exception_type,
                f.exception_message,
                f.root_cause,
                f.patch,
                f.feedback,
                f.confidence,
                COALESCE(v.vector_score, 0) as vector_score,
                COALESCE(k.keyword_score, 0) as keyword_score,
                -- Reciprocal Rank Fusion
                (
                    $6 * COALESCE(1.0 / (60 + v.vector_rank), 0) +
                    $7 * COALESCE(1.0 / (60 + k.keyword_rank), 0)
                ) as rrf_score,
                -- Weighted linear combination
                (
                    $6 * COALESCE(v.vector_score, 0) +
                    $7 * COALESCE(k.keyword_score / NULLIF((SELECT MAX(keyword_score) FROM keyword_search), 0), 0)
                ) as combined_score
            FROM fix_records f
            LEFT JOIN vector_search v ON f.id = v.id
            LEFT JOIN keyword_search k ON f.id = k.id
            WHERE v.id IS NOT NULL OR k.id IS NOT NULL
            ORDER BY rrf_score DESC, combined_score DESC
            LIMIT $4
        """

        try:
            results = await self.db.fetch(
                query,
                embedding,  # $1
                org_id,  # $2
                only_accepted,  # $3
                top_k,  # $4
                keyword_query,  # $5
                self.vector_weight,  # $6
                self.keyword_weight,  # $7
            )

            similar_fixes = []
            for r in results:
                combined = r["combined_score"] or 0
                if combined >= min_score or r["rrf_score"] > 0:
                    similar_fixes.append(SimilarFix(
                        fix_id=str(r["id"]),
                        similarity_score=max(combined, r["rrf_score"] * 100),
                        vector_score=r["vector_score"] or 0,
                        keyword_score=r["keyword_score"] or 0,
                        exception_type=r["exception_type"],
                        root_cause=r["root_cause"],
                        patch=r["patch"],
                        feedback=r["feedback"],
                        success=r["feedback"] == "accepted",
                    ))

            return similar_fixes

        except Exception as e:
            logger.error(f"Search error: {e}")
            # Fall back to simple exception type match
            return await self._fallback_search(failure_context, org_id, top_k, only_accepted)

    async def _fallback_search(
        self,
        failure_context: Dict[str, Any],
        org_id: Optional[str],
        top_k: int,
        only_accepted: bool,
    ) -> List[SimilarFix]:
        """Fallback search using exact exception type match."""
        exception_type = failure_context.get("exception_type", "")

        query = """
            SELECT
                id, exception_type, exception_message, root_cause,
                patch, feedback, confidence
            FROM fix_records
            WHERE exception_type = $1
            AND ($2::uuid IS NULL OR org_id = $2)
            AND ($3::boolean = false OR feedback = 'accepted')
            ORDER BY
                CASE WHEN feedback = 'accepted' THEN 0 ELSE 1 END,
                confidence DESC NULLS LAST,
                created_at DESC
            LIMIT $4
        """

        try:
            results = await self.db.fetch(query, exception_type, org_id, only_accepted, top_k)

            return [
                SimilarFix(
                    fix_id=str(r["id"]),
                    similarity_score=0.7 if r["feedback"] == "accepted" else 0.5,
                    vector_score=0.0,
                    keyword_score=1.0,
                    exception_type=r["exception_type"],
                    root_cause=r["root_cause"],
                    patch=r["patch"],
                    feedback=r["feedback"],
                    success=r["feedback"] == "accepted",
                )
                for r in results
            ]
        except Exception as e:
            logger.error(f"Fallback search error: {e}")
            return []

    async def index_fix(self, fix_record: Dict[str, Any]) -> bool:
        """Index a fix record for future search.

        Args:
            fix_record: Fix record to index.

        Returns:
            True if indexed successfully.
        """
        try:
            embedding = await self.embeddings.embed_fix_record(fix_record)

            await self.db.execute(
                """
                UPDATE fix_records
                SET embedding = $1
                WHERE id = $2
                """,
                embedding,
                fix_record["id"],
            )
            return True

        except Exception as e:
            logger.error(f"Failed to index fix: {e}")
            return False
