"""Postgres full-text search services (PRD REPO-SP3)."""

from __future__ import annotations

from typing import Any

from django.contrib.postgres.search import (
    SearchHeadline,
    SearchQuery,
    SearchRank,
)
from django.db.models import Count, F, OuterRef, Q, QuerySet, Subquery


def search_documents(
    *,
    query: str = "",
    filters: dict[str, Any] | None = None,
    sort: str = "relevance",
    user=None,
) -> QuerySet:
    from apps.repository.documents.models import Document

    qs = Document.objects.for_user(user).select_related("collection")

    filters = filters or {}
    if dt := filters.get("document_type"):
        qs = qs.filter(document_type=dt)
    if col := filters.get("collection"):
        qs = qs.filter(collection__slug=col)
    if author := filters.get("author"):
        # FRREP-SR001/004 — explicit author facet. Case-insensitive substring
        # across first/last name (mirrors advanced_search semantics) so the
        # facet works even when the FTS vector is stale on the imported corpus.
        qs = qs.filter(
            Q(authors__first_name__icontains=author)
            | Q(authors__last_name__icontains=author)
        ).distinct()
    if year_from := filters.get("year_from"):
        try:
            qs = qs.filter(year__gte=int(year_from))
        except (TypeError, ValueError):
            pass
    if year_to := filters.get("year_to"):
        try:
            qs = qs.filter(year__lte=int(year_to))
        except (TypeError, ValueError):
            pass
    if language := filters.get("language"):
        qs = qs.filter(language=language)
    if license := filters.get("license"):
        qs = qs.filter(license_type=license)
    if grant := filters.get("grant_reference"):
        # FRREP-INT002 — forgiving substring match on the RIMS grant reference.
        qs = qs.filter(grant_reference__icontains=grant)
    if institution := filters.get("institution"):
        qs = qs.filter(authors__institution_id=institution).distinct()
    if country := filters.get("country"):
        qs = qs.filter(country=country)
    if region := filters.get("region"):
        qs = qs.filter(regions__slug=region).distinct()
    if subject := filters.get("subject"):
        # Browse-by-subject uses an exact keyword slug; the Search rail's free-text
        # subject box matches the keyword label loosely — accept either so both
        # entry points (SR004) work through one filter.
        qs = qs.filter(
            Q(keywords__slug=subject) | Q(keywords__label__icontains=subject)
        ).distinct()
    if publisher := filters.get("publisher"):
        qs = qs.filter(publisher=publisher)
    if conference := filters.get("conference"):
        qs = qs.filter(conference_name=conference)

    q = (query or "").strip()
    if q:
        sq = SearchQuery(q, search_type="websearch")
        qs = qs.filter(search_vector=sq).annotate(
            rank=SearchRank(F("search_vector"), sq),
            snippet_html=SearchHeadline(
                "abstract",
                sq,
                start_sel='<mark class="snippet">',
                stop_sel="</mark>",
                max_words=40,
                min_words=20,
                short_word=3,
            ),
        )
    if sort == "recent" or (not q and sort == "relevance"):
        qs = qs.order_by("-published_at", "-created_at")
    elif sort == "oldest":
        qs = qs.order_by("published_at", "created_at")
    elif sort == "title":
        qs = qs.order_by("title")
    elif sort == "title_desc":
        qs = qs.order_by("-title")
    elif sort == "author":
        # FRREP-SR003 — sort by the primary/first-listed author's last name.
        # DocumentAuthor.Meta.ordering is ["order", "pk"], so the first row per
        # document (order=0 for the primary author) is the one we want.
        from apps.repository.documents.models import DocumentAuthor

        primary_author_last_name = Subquery(
            DocumentAuthor.objects.filter(document=OuterRef("pk"))
            .order_by("order", "pk")
            .values("author__last_name")[:1]
        )
        qs = qs.annotate(sort_author=primary_author_last_name).order_by("sort_author", "title")
    elif sort == "downloads":
        # FRREP-SR003 — real most-downloaded sort (distinct=True guards against
        # join fan-out from the M2M filters applied above).
        qs = qs.annotate(download_count=Count("download_events", distinct=True)).order_by(
            "-download_count", "-published_at"
        )
    else:
        qs = qs.order_by("-rank", "-published_at")
    return qs


def incremental_search(prefix: str, user=None, limit: int = 8) -> list[dict]:
    """FRREP-SR002 — real-time suggestions drawn from indexed titles, author
    names, and subject keywords. Document hits carry a ``uid`` so the dropdown
    can jump straight to the record; author/keyword hits carry a ``query`` used
    to run a full search.
    """
    from apps.repository.documents.models import Author, Document, Keyword

    p = (prefix or "").strip()
    if not p:
        return []

    # Substring match on the title — portable (no pg_trgm dependency) and ideal
    # for as-you-type prefixes. Shorter titles rank first as a light relevance
    # proxy.
    from django.db.models.functions import Length

    doc_qs = (
        Document.objects.for_user(user)
        .filter(title__icontains=p)
        .annotate(title_len=Length("title"))
        .order_by("title_len", "title")[:limit]
    )
    suggestions: list[dict] = [
        {
            "type": "document",
            "uid": str(d.document_uid),
            "title": d.title,
            "public_id": d.public_document_id,
            "collection": d.collection.name,
        }
        for d in doc_qs
    ]

    # Author-name suggestions (distinct, published-document authors only).
    author_qs = (
        Author.objects.filter(
            Q(first_name__icontains=p) | Q(last_name__icontains=p),
            documents__in=Document.objects.for_user(user),
        )
        .distinct()
        .order_by("last_name", "first_name")[:4]
    )
    for a in author_qs:
        name = f"{a.first_name} {a.last_name}".strip()
        if name:
            suggestions.append({"type": "author", "label": name, "query": name})

    # Controlled-vocabulary keyword suggestions.
    kw_qs = (
        Keyword.objects.filter(status=Keyword.Status.APPROVED, label__icontains=p)
        .order_by("label")[:4]
    )
    for kw in kw_qs:
        suggestions.append({"type": "keyword", "label": kw.label, "query": kw.label})

    return suggestions


def advanced_search(
    *,
    title: str = "",
    abstract: str = "",
    author: str = "",
    author_orcid: str = "",
    doi: str = "",
    license: str = "",
    year_from: int | None = None,
    year_to: int | None = None,
    document_type: str = "",
    collection: str = "",
    subject: str = "",
    grant: str = "",
    user=None,
) -> QuerySet:
    from apps.repository.documents.models import Document

    qs = Document.objects.for_user(user)
    if title:
        qs = qs.filter(title__icontains=title)
    if abstract:
        qs = qs.filter(abstract__icontains=abstract)
    if author:
        qs = qs.filter(
            Q(authors__first_name__icontains=author) | Q(authors__last_name__icontains=author)
        )
    if author_orcid:
        qs = qs.filter(authors__orcid=author_orcid)
    if doi:
        # FRREP-SR005 — DOIs vary by prefix/case; substring match is forgiving.
        qs = qs.filter(doi__icontains=doi)
    if license:
        qs = qs.filter(license_type=license)
    if year_from:
        qs = qs.filter(year__gte=year_from)
    if year_to:
        qs = qs.filter(year__lte=year_to)
    # FRREP-SR005 — document type / collection / subject-keyword narrowing,
    # matching what the plain search filter rail already exposes.
    if document_type:
        qs = qs.filter(document_type=document_type)
    if collection:
        qs = qs.filter(collection__slug=collection)
    if subject:
        qs = qs.filter(keywords__slug=subject)
    if grant:
        # FRREP-INT002 — RIMS grant references vary by prefix/case; substring
        # match keeps the cross-system lookup forgiving.
        qs = qs.filter(grant_reference__icontains=grant)
    return qs.distinct().order_by("-published_at")


def track_search_event(*, query: str, filters: dict, result_count: int, user, request) -> int | None:
    """Record a search event and return its pk (FRREP-AR011).

    The pk lets the calling view thread ``?se=<pk>`` onto result links so a
    later document-detail visit can be attributed back to this search as a
    click-through (see ``analytics.services.record_search_click``).
    """
    try:
        from apps.repository.analytics.services import record_search_event

        return record_search_event(
            query=query,
            filters=filters,
            result_count=result_count,
            clicked_doc=None,
            user=user,
            request=request,
        )
    except Exception:  # pragma: no cover
        return None
