"""
Hybrid search for Lattice.

This module implements hybrid search combining:
- BM25 full-text search (FTS5)
- Vector similarity search (sqlite-vec)
- Reciprocal Rank Fusion (RRF) for result combination

All functions are Shell layer — they perform I/O and return Result[T, E].

Reference: RFC-002 §5.3 Schema, §4.5 Search
"""

from __future__ import annotations

import sqlite3
from typing import TYPE_CHECKING

from returns.result import Failure, Result, Success

from lattice.core.search import fuse_results
from lattice.core.types.enums import Role
from lattice.core.types.search import FtsResult, SearchResult, VecResult

if TYPE_CHECKING:
    pass


# @shell_complexity: FTS5 + optional vector search + RRF fusion + content retrieval
def hybrid_search(
    conn: sqlite3.Connection,
    query: str,
    query_embedding: list[float] | None = None,
    limit: int = 20,
    rrf_k: int = 60,
) -> Result[list[SearchResult], str]:
    """Execute search: FTS5-only (default) or hybrid (BM25 + Vector + RRF).

    When query_embedding is None (default, no embedding configured), uses FTS5 BM25.
    When query_embedding is provided (embedding configured), upgrades to hybrid RRF.

    Args:
        conn: SQLite connection to store.db.
        query: FTS5 search query.
        query_embedding: Query embedding vector, or None for FTS5-only.
        limit: Maximum number of results.
        rrf_k: RRF parameter (default 60).

    Returns:
        Success(list[SearchResult]) sorted by relevance.
        Failure(error_message) on error.

    Example:
        >>> import tempfile
        >>> from pathlib import Path
        >>> from lattice.shell.schema import create_store
        >>> with tempfile.TemporaryDirectory() as tmpdir:
        ...     result = create_store(Path(tmpdir) / "store.db")
        ...     conn = result.unwrap()
        ...     search_result = hybrid_search(conn, "hello")
        ...     isinstance(search_result, Success)
        True
    """
    try:
        # Execute FTS5 search
        fts_cursor = conn.execute(
            """
            SELECT rowid, bm25(logs_fts) as rank
            FROM logs_fts
            WHERE logs_fts MATCH ?
            ORDER BY rank ASC
            LIMIT ?
            """,
            (query, limit * 2),
        )
        bm25_results: list[FtsResult] = [
            FtsResult(rowid=row["rowid"], rank=row["rank"])
            for row in fts_cursor.fetchall()
        ]

        # Vector search only if embedding provided
        vec_results: list[VecResult] = []
        if query_embedding is not None:
            import struct

            query_bytes = struct.pack(f"{len(query_embedding)}f", *query_embedding)
            vec_cursor = conn.execute(
                """
                SELECT rowid, distance
                FROM logs_vec
                WHERE embedding MATCH ?
                ORDER BY distance ASC
                LIMIT ?
                """,
                (query_bytes, limit * 2),
            )
            vec_results = [
                VecResult(rowid=row["rowid"], distance=row["distance"])
                for row in vec_cursor.fetchall()
            ]

        # Fuse with RRF (works with empty vec_results for FTS5-only)
        fused = fuse_results(bm25_results, vec_results, k=rrf_k)

        if not fused:
            return Success([])

        # Retrieve full log content for top results
        rowids = [item.rowid for item in fused[:limit]]
        placeholders = ",".join("?" * len(rowids))
        logs_cursor = conn.execute(
            f"""
            SELECT id, external_id, session_id, timestamp, role, content, metadata
            FROM logs
            WHERE id IN ({placeholders})
            """,
            rowids,
        )

        # Build map from rowid to log
        log_map: dict[int, dict] = {}
        for row in logs_cursor.fetchall():
            log_map[row["id"]] = dict(row)

        # Build SearchResult list in RRF order
        results: list[SearchResult] = []
        for item in fused[:limit]:
            log = log_map.get(item.rowid)
            if log:
                results.append(
                    SearchResult(
                        rowid=log["id"],
                        external_id=log["external_id"],
                        session_id=log["session_id"],
                        timestamp=log["timestamp"],
                        role=Role(log["role"])
                        if log["role"] in [r.value for r in Role]
                        else Role.USER,
                        content=log["content"],
                        rrf_score=item.rrf_score,
                        metadata={},
                    )
                )

        return Success(results)

    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")
    except Exception as e:
        return Failure(f"Hybrid search error: {e}")


# @shell_complexity: FTS5-only search for global evidence
def search_evidence_fts(
    conn: sqlite3.Connection,
    query: str,
    limit: int = 20,
) -> Result[list[dict], str]:
    """Execute BM25 full-text search on evidence_fts in global.db.

    Args:
        conn: SQLite connection to global.db.
        query: FTS5 search query.
        limit: Maximum number of results.

    Returns:
        Success(list[dict]) with evidence records sorted by BM25 rank.
        Failure(error_message) on database error.
    """
    try:
        cursor = conn.execute(
            """
            SELECT e.id, e.source_project, e.source_session_id, e.pattern,
                   e.summary, e.original_snippet, e.extracted_at, e.confidence,
                   bm25(evidence_fts) as rank
            FROM evidence e
            JOIN evidence_fts fts ON e.id = fts.rowid
            WHERE evidence_fts MATCH ?
            ORDER BY rank ASC
            LIMIT ?
            """,
            (query, limit),
        )

        results: list[dict] = []
        for row in cursor.fetchall():
            results.append(dict(row))

        return Success(results)

    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")


# @shell_complexity: Global hybrid search (evidence table)
def hybrid_search_global(
    conn: sqlite3.Connection,
    query: str,
    query_embedding: list[float] | None = None,
    limit: int = 20,
    rrf_k: int = 60,
) -> Result[list[dict], str]:
    """Execute search on global.db evidence table.

    FTS5-only by default. When query_embedding is provided, upgrades to
    hybrid (FTS5 + vector + RRF).

    Args:
        conn: SQLite connection to global.db.
        query: FTS5 search query.
        query_embedding: Query embedding vector, or None for FTS5-only.
        limit: Maximum number of results.
        rrf_k: RRF parameter (default 60).

    Returns:
        Success(list[dict]) with evidence records sorted by relevance.
        Failure(error_message) on error.
    """
    try:
        # Execute FTS5 search on evidence
        fts_cursor = conn.execute(
            """
            SELECT e.id, bm25(evidence_fts) as rank
            FROM evidence e
            JOIN evidence_fts fts ON e.id = fts.rowid
            WHERE evidence_fts MATCH ?
            ORDER BY rank ASC
            LIMIT ?
            """,
            (query, limit * 2),
        )
        bm25_results: list[FtsResult] = [
            FtsResult(rowid=row["id"], rank=row["rank"])
            for row in fts_cursor.fetchall()
        ]

        # Vector search only if embedding provided
        vec_results: list[VecResult] = []
        if query_embedding is not None:
            import struct

            query_bytes = struct.pack(f"{len(query_embedding)}f", *query_embedding)
            vec_cursor = conn.execute(
                """
                SELECT rowid, distance
                FROM evidence_vec
                WHERE embedding MATCH ?
                ORDER BY distance ASC
                LIMIT ?
                """,
                (query_bytes, limit * 2),
            )
            vec_results = [
                VecResult(rowid=row["rowid"], distance=row["distance"])
                for row in vec_cursor.fetchall()
            ]

        # Fuse with RRF (works with empty vec_results for FTS5-only)
        fused = fuse_results(bm25_results, vec_results, k=rrf_k)

        if not fused:
            return Success([])

        # Retrieve full evidence content for top results
        rowids = [item.rowid for item in fused[:limit]]
        placeholders = ",".join("?" * len(rowids))
        evidence_cursor = conn.execute(
            f"""
            SELECT id, source_project, source_session_id, source_rule_file,
                   pattern, summary, original_snippet, extracted_at,
                   compiler_trace, confidence
            FROM evidence
            WHERE id IN ({placeholders})
            """,
            rowids,
        )

        # Build map from id to evidence
        evidence_map: dict[int, dict] = {}
        for row in evidence_cursor.fetchall():
            evidence_map[row["id"]] = dict(row)

        # Build results in RRF order
        results: list[dict] = []
        for item in fused[:limit]:
            evidence = evidence_map.get(item.rowid)
            if evidence:
                evidence["rrf_score"] = item.rrf_score
                results.append(evidence)

        return Success(results)

    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")
    except Exception as e:
        return Failure(f"Hybrid search error: {e}")


__all__ = [
    "hybrid_search",
    "search_evidence_fts",
    "hybrid_search_global",
]
