"""Search helpers — deduplication, filtering, and relevance ranking.

Stateless functions used by :class:`~axm_bib.pipeline.BibPipeline`
to post-process papers returned by multiple source clients.
"""

from __future__ import annotations

import logging
import re
from difflib import SequenceMatcher

from axm_bib.models import Paper

logger = logging.getLogger(__name__)

__all__ = [
    "dedup_papers",
    "filter_papers",
    "rank_by_relevance",
]

RELEVANCE_THRESHOLD = 0.4
"""Minimum title similarity ratio to keep a paper in search results."""


# ── Deduplication ─────────────────────────────────────────────────────────


def _paper_key(paper: Paper) -> str:
    """Compute dedup key for a paper: DOI > arXiv ID > normalized title."""
    if paper.doi:
        return f"doi:{paper.doi.lower()}"
    if paper.arxiv_id:
        return f"arxiv:{paper.arxiv_id.lower()}"
    return f"title:{paper.title.lower().strip()}"


def _field_count(paper: Paper) -> int:
    """Count non-None fields on a Paper for richness comparison."""
    fields = (
        paper.doi,
        paper.arxiv_id,
        paper.abstract,
        paper.venue,
        paper.year,
        paper.citation_count,
        paper.open_access_pdf,
        paper.dblp_key,
        paper.openreview_id,
        paper.openalex_id,
        paper.hal_id,
    )
    return sum(1 for f in fields if f is not None)


def dedup_papers(papers: list[Paper]) -> list[Paper]:
    """Deduplicate papers by DOI / arXiv ID / title, keeping richest.

    Args:
        papers: Raw aggregated papers from multiple sources.

    Returns:
        Deduplicated list preserving first-seen order.
    """
    seen: dict[str, Paper] = {}
    for paper in papers:
        key = _paper_key(paper)
        existing = seen.get(key)
        if existing is None or _field_count(paper) > _field_count(existing):
            seen[key] = paper
    return list(seen.values())


# ── Filtering ─────────────────────────────────────────────────────────────


def _normalize_text(text: str) -> str:
    """Normalize text for accent-safe comparison."""
    import unicodedata

    nfkd = unicodedata.normalize("NFKD", text)
    return "".join(c for c in nfkd if not unicodedata.combining(c)).lower()


def filter_papers(
    papers: list[Paper],
    *,
    venue: str | None = None,
    author: str | None = None,
    year: int | None = None,
) -> list[Paper]:
    """Apply post-filters on papers.

    Args:
        papers: Deduplicated papers.
        venue: Case-insensitive substring match on paper.venue.
        author: Case-insensitive substring match on any author (accent-safe).
        year: Exact match on paper.year.

    Returns:
        Filtered list.
    """
    result = papers
    if venue:
        venue_lower = venue.lower()
        result = [p for p in result if p.venue and venue_lower in p.venue.lower()]
    if author:
        author_norm = _normalize_text(author)
        result = [
            p
            for p in result
            if any(author_norm in _normalize_text(a) for a in p.authors)
        ]
    if year is not None:
        result = [p for p in result if p.year == year]
    return result


# ── Relevance ranking ─────────────────────────────────────────────────────


def _normalize_for_similarity(text: str) -> str:
    """Normalize text for similarity comparison: lowercase, strip punctuation."""
    return re.sub(r"[^\w\s]", "", text.lower().strip())


def rank_by_relevance(
    query: str,
    papers: list[Paper],
    *,
    threshold: float = RELEVANCE_THRESHOLD,
) -> list[Paper]:
    """Filter and rank papers by title similarity to query.

    Uses ``SequenceMatcher`` on normalized titles (lowercase, no
    punctuation) to compute similarity.  Papers below *threshold*
    are dropped; remaining papers are sorted by descending ratio.

    If *query* is empty, returns *papers* unchanged (no filtering).

    Args:
        query: User query string.
        papers: Pre-filtered papers.
        threshold: Minimum similarity ratio (default 0.4).

    Returns:
        Ranked and filtered list of papers.
    """
    if not query:
        return papers

    norm_query = _normalize_for_similarity(query)
    scored: list[tuple[float, Paper]] = []
    for paper in papers:
        norm_title = _normalize_for_similarity(paper.title)
        ratio = SequenceMatcher(None, norm_query, norm_title).ratio()
        if ratio >= threshold:
            scored.append((ratio, paper))
    # Sort by similarity descending; at equal ratio, prefer published papers
    # (those with a venue) over preprints.
    scored.sort(key=lambda t: (t[0], bool(t[1].venue)), reverse=True)
    return [paper for _, paper in scored]
