"""
Search endpoints for SLTK API.

Provides efficient cross-dataset gloss and transcription search.
"""

from __future__ import annotations

import asyncio
import logging
import re
import time
from typing import Any

from fastapi import APIRouter, Query, Request

from sltk.api.dependencies import get_cached_dataset, rate_limit

router = APIRouter(prefix="/api/search", tags=["search"])
logger = logging.getLogger(__name__)

# Pre-compiled patterns for text normalization
_WORD_SPLIT_PATTERN = re.compile(r"[\s\-_/]+")
_NON_ALPHA_PATTERN = re.compile(r"[^a-z0-9]")


def _normalize_text(text: str) -> str:
    """Normalize text for comparison."""
    return _NON_ALPHA_PATTERN.sub("", text.lower())


def _levenshtein_distance(s1: str, s2: str) -> int:
    """
    Compute Levenshtein distance between two strings.

    Simple implementation optimized for short strings (glosses).
    """
    if len(s1) < len(s2):
        s1, s2 = s2, s1

    if len(s2) == 0:
        return len(s1)

    previous_row = list(range(len(s2) + 1))

    for i, c1 in enumerate(s1):
        current_row = [i + 1]
        for j, c2 in enumerate(s2):
            insertions = previous_row[j + 1] + 1
            deletions = current_row[j] + 1
            substitutions = previous_row[j] + (c1 != c2)
            current_row.append(min(insertions, deletions, substitutions))
        previous_row = current_row

    return previous_row[-1]


def _similarity_score(s1: str, s2: str) -> float:
    """
    Compute similarity score between two strings (0-1).

    Uses normalized Levenshtein distance.
    """
    s1_norm = _normalize_text(s1)
    s2_norm = _normalize_text(s2)

    if not s1_norm or not s2_norm:
        return 0.0

    if s1_norm == s2_norm:
        return 1.0

    # Check for prefix/suffix match (common in glosses)
    if s1_norm.startswith(s2_norm) or s2_norm.startswith(s1_norm):
        return 0.9

    max_len = max(len(s1_norm), len(s2_norm))
    distance = _levenshtein_distance(s1_norm, s2_norm)
    return 1.0 - (distance / max_len)


def _find_matches_in_text(
    query: str,
    text: str,
    min_similarity: float = 0.7,
) -> list[tuple[str, float]]:
    """
    Find fuzzy matches for query in text.

    Returns list of (matched_word, similarity_score) tuples.
    """
    if not text:
        return []

    matches = []
    query_norm = _normalize_text(query)

    if not query_norm:
        return []

    # Split text into words
    words = _WORD_SPLIT_PATTERN.split(text)

    for word in words:
        if not word:
            continue

        score = _similarity_score(query, word)
        if score >= min_similarity:
            matches.append((word, score))

    return matches


# ============================================================================
# Search Result Models
# ============================================================================

from pydantic import BaseModel, Field


class SearchResult(BaseModel):
    """A single search result."""

    dataset: str
    sample_id: str
    match_type: str  # 'exact_gloss', 'fuzzy_gloss', 'transcription'
    matched_text: str
    similarity: float
    start: float
    end: float
    context: list[str] = Field(default_factory=list)
    video_path: str | None = None
    signer_id: str | None = None
    tier: str | None = None


class SearchResponse(BaseModel):
    """Response for gloss search."""

    query: str
    total_results: int
    datasets_searched: int
    results: list[SearchResult]
    search_time_ms: float


# ============================================================================
# Search Endpoints
# ============================================================================


@router.get("/glosses", response_model=SearchResponse)
@rate_limit("30/minute")
async def search_glosses(
    request: Request,
    q: str = Query(..., min_length=1, max_length=100, description="Search query"),
    datasets: str | None = Query(None, description="Comma-separated dataset names (None = all)"),
    include_transcriptions: bool = Query(
        True, description="Search transcriptions for similar words"
    ),
    min_similarity: float = Query(
        0.7, ge=0.5, le=1.0, description="Minimum similarity for fuzzy matching"
    ),
    limit: int = Query(50, ge=1, le=200, description="Maximum results to return"),
    max_samples_per_dataset: int = Query(
        200, ge=1, le=1000, description="Max samples to search per dataset"
    ),
):
    """
    Search for glosses across all connected datasets.

    Features:
    - Exact gloss matching (highest priority)
    - Fuzzy gloss matching (for typos/variants)
    - Transcription search (finds similar English words)
    - Returns results sorted by relevance

    The search is optimized for quick response times by:
    - Limiting samples searched per dataset
    - Using efficient string matching algorithms
    - Running dataset searches in parallel
    """
    start_time = time.time()
    query = q.strip()
    query_lower = query.lower()

    # Determine which datasets to search
    from pathlib import Path

    from sltk.config import DATASET_PATHS
    from sltk.data.datasets import DATASETS

    if datasets:
        dataset_names = [d.strip() for d in datasets.split(",") if d.strip()]
    else:
        # Get all accessible datasets (quick check - only verify root exists)
        dataset_names = []
        for name in DATASETS.keys():
            dataset_info = DATASET_PATHS.get(name, {})
            root_path = dataset_info.get("root")
            if root_path:
                try:
                    path = Path(root_path)
                    # Quick existence check without listing contents
                    if path.exists() and path.is_dir():
                        dataset_names.append(name)
                except (OSError, PermissionError):
                    pass

    if not dataset_names:
        return SearchResponse(
            query=query,
            total_results=0,
            datasets_searched=0,
            results=[],
            search_time_ms=0,
        )

    # Search function for a single dataset
    def _search_dataset(name: str) -> list[SearchResult]:
        """Search a single dataset for matches."""
        results: list[SearchResult] = []
        max_results_per_dataset = limit  # Stop searching once we have enough

        try:
            dataset = get_cached_dataset(name, split="all")
        except Exception as e:
            logger.debug(f"Could not load dataset {name}: {e}")
            return []

        samples_searched = 0

        for sample_id in dataset._index[:max_samples_per_dataset]:
            # Early termination if we have enough results
            if len(results) >= max_results_per_dataset:
                break

            samples_searched += 1

            try:
                sample = dataset._load_sample(sample_id)
            except Exception:
                continue

            video_path = str(sample.video_path) if sample.video_path else None
            signer_id = sample.signer_id

            # Search in glosses (exact and fuzzy)
            if sample.glosses:
                for i, gloss in enumerate(sample.glosses):
                    if not gloss:
                        continue

                    gloss_lower = gloss.lower()

                    # Exact match
                    if gloss_lower == query_lower:
                        context_before = sample.glosses[max(0, i - 2) : i]
                        context_after = sample.glosses[i + 1 : i + 3]

                        results.append(
                            SearchResult(
                                dataset=name,
                                sample_id=sample.id,
                                match_type="exact_gloss",
                                matched_text=gloss,
                                similarity=1.0,
                                start=0.0,  # Gloss-level doesn't have timing
                                end=0.0,
                                context=context_before + context_after,
                                video_path=video_path,
                                signer_id=signer_id,
                            )
                        )
                    # Fuzzy match
                    elif min_similarity < 1.0:
                        score = _similarity_score(query, gloss)
                        if score >= min_similarity and score < 1.0:
                            context_before = sample.glosses[max(0, i - 2) : i]
                            context_after = sample.glosses[i + 1 : i + 3]

                            results.append(
                                SearchResult(
                                    dataset=name,
                                    sample_id=sample.id,
                                    match_type="fuzzy_gloss",
                                    matched_text=gloss,
                                    similarity=score,
                                    start=0.0,
                                    end=0.0,
                                    context=context_before + context_after,
                                    video_path=video_path,
                                    signer_id=signer_id,
                                )
                            )

            # Search in segments (exact and fuzzy)
            if sample.segments:
                for i, seg in enumerate(sample.segments):
                    label = seg.label
                    if not label:
                        continue

                    label_lower = label.lower()

                    # Exact match
                    if label_lower == query_lower:
                        context = []
                        for j in range(max(0, i - 2), min(len(sample.segments), i + 3)):
                            if j != i and sample.segments[j].label:
                                context.append(sample.segments[j].label)

                        results.append(
                            SearchResult(
                                dataset=name,
                                sample_id=sample.id,
                                match_type="exact_gloss",
                                matched_text=label,
                                similarity=1.0,
                                start=seg.start,
                                end=seg.end,
                                context=context[:4],
                                video_path=video_path,
                                signer_id=signer_id,
                                tier=seg.tier,
                            )
                        )
                    # Fuzzy match
                    elif min_similarity < 1.0:
                        score = _similarity_score(query, label)
                        if score >= min_similarity and score < 1.0:
                            context = []
                            for j in range(max(0, i - 2), min(len(sample.segments), i + 3)):
                                if j != i and sample.segments[j].label:
                                    context.append(sample.segments[j].label)

                            results.append(
                                SearchResult(
                                    dataset=name,
                                    sample_id=sample.id,
                                    match_type="fuzzy_gloss",
                                    matched_text=label,
                                    similarity=score,
                                    start=seg.start,
                                    end=seg.end,
                                    context=context[:4],
                                    video_path=video_path,
                                    signer_id=signer_id,
                                    tier=seg.tier,
                                )
                            )

            # Search in transcriptions (text field)
            if include_transcriptions and hasattr(sample, "text") and sample.text:
                matches = _find_matches_in_text(query, sample.text, min_similarity)
                for matched_word, score in matches:
                    results.append(
                        SearchResult(
                            dataset=name,
                            sample_id=sample.id,
                            match_type="transcription",
                            matched_text=matched_word,
                            similarity=score,
                            start=0.0,
                            end=0.0,
                            context=[
                                sample.text[:100] + "..." if len(sample.text) > 100 else sample.text
                            ],
                            video_path=video_path,
                            signer_id=signer_id,
                        )
                    )

            # Also search in metadata if it contains transcription-like fields
            if include_transcriptions and sample.metadata:
                for key in ["translation", "transcription", "subtitle", "caption", "english"]:
                    if key in sample.metadata and isinstance(sample.metadata[key], str):
                        text = sample.metadata[key]
                        matches = _find_matches_in_text(query, text, min_similarity)
                        for matched_word, score in matches:
                            results.append(
                                SearchResult(
                                    dataset=name,
                                    sample_id=sample.id,
                                    match_type="transcription",
                                    matched_text=matched_word,
                                    similarity=score,
                                    start=0.0,
                                    end=0.0,
                                    context=[text[:100] + "..." if len(text) > 100 else text],
                                    video_path=video_path,
                                    signer_id=signer_id,
                                )
                            )

        return results

    # Run searches in parallel
    tasks = [asyncio.to_thread(_search_dataset, name) for name in dataset_names]

    all_results_nested = await asyncio.gather(*tasks, return_exceptions=True)

    # Flatten and filter results
    all_results: list[SearchResult] = []
    datasets_searched = 0

    for result in all_results_nested:
        if isinstance(result, BaseException):
            logger.debug(f"Search error: {result}")
            continue
        datasets_searched += 1
        all_results.extend(result)

    # Sort by similarity (descending) and match type priority
    type_priority = {"exact_gloss": 0, "fuzzy_gloss": 1, "transcription": 2}
    all_results.sort(key=lambda r: (type_priority.get(r.match_type, 3), -r.similarity))

    # Deduplicate (same dataset + sample + matched_text)
    seen = set()
    unique_results = []
    for r in all_results:
        key = (r.dataset, r.sample_id, r.matched_text.lower())
        if key not in seen:
            seen.add(key)
            unique_results.append(r)

    # Limit results
    final_results = unique_results[:limit]

    search_time_ms = (time.time() - start_time) * 1000

    return SearchResponse(
        query=query,
        total_results=len(unique_results),
        datasets_searched=datasets_searched,
        results=final_results,
        search_time_ms=round(search_time_ms, 2),
    )


@router.get("/suggestions")
async def get_search_suggestions(
    q: str = Query(..., min_length=1, max_length=50, description="Partial query"),
    limit: int = Query(10, ge=1, le=20, description="Max suggestions"),
):
    """
    Get autocomplete suggestions based on vocabulary from cached datasets.

    Uses vocabulary cache if available for fast response.
    """
    from sltk.api.dependencies import get_vocabulary_cache

    query_lower = q.lower().strip()
    suggestions: set[tuple[str, Any]] = set()

    _vocabulary_cache, _vocabulary_cache_lock, _ = get_vocabulary_cache()

    with _vocabulary_cache_lock:
        for cache_key, (cached_data, _) in _vocabulary_cache.items():
            if len(suggestions) >= limit * 2:  # Get extras for filtering
                break

            frequencies = cached_data.get("frequencies", {})
            for gloss, freq in frequencies.items():
                if gloss.lower().startswith(query_lower):
                    suggestions.add((gloss, freq))
                elif query_lower in gloss.lower():
                    suggestions.add((gloss, freq))

    # Sort by frequency and take top results
    sorted_suggestions = sorted(suggestions, key=lambda x: -x[1])[:limit]

    return {
        "query": q,
        "suggestions": [s[0] for s in sorted_suggestions],
    }
