"""Vector embeddings + semantic search for alumni profiles.

PRD §4.3.5 calls for "semantic searches that understand the intent behind
queries rather than just exact phrases". The Postgres FTS in
:mod:`apps.alumni.profiles.search` covers boolean + keyword. This module
adds an opt-in vector path that ranks by cosine similarity using pgvector
and the shared OpenAI / local-fallback embedder in
:mod:`apps.core.ai.embeddings`.

Designed to be a graceful no-op when pgvector isn't installed — callers
fall back to FTS via the ``mode`` parameter on :func:`search_alumni`.
"""
from __future__ import annotations

import hashlib
import logging
from typing import Any

from django.conf import settings

from apps.alumni.profiles.models import AlumniProfile

logger = logging.getLogger(__name__)


PROFILE_EMBEDDING_DIMS = 1536


def _compose_profile_text(profile: AlumniProfile) -> str:
    """Concatenate the searchable surface of a profile in a stable order.

    Stable ordering matters because the SHA256 hash decides whether to
    skip an embedding refresh.
    """
    bits: list[str] = []
    if profile.headline:
        bits.append(profile.headline)
    if profile.current_position:
        bits.append(profile.current_position)
    if profile.current_employer:
        bits.append(profile.current_employer)
    tags = list(profile.expertise_tags or [])
    if tags:
        bits.append("Expertise: " + ", ".join(sorted(tags)))
    for degree in profile.degrees.order_by("graduated_on", "pk"):
        bits.append(f"{degree.get_degree_type_display()} — {degree.programme_name}")
        if degree.thesis_title:
            bits.append(f"Thesis: {degree.thesis_title}")
    for emp in profile.employments.order_by("started_on", "pk"):
        line = f"{emp.position} @ {emp.organisation}"
        if emp.sector:
            line += f" ({emp.sector})"
        bits.append(line)
        if emp.description:
            bits.append(emp.description)
    for pub in profile.publications.order_by("year", "pk")[:50]:
        bits.append(pub.title)
    return "\n".join(b for b in bits if b).strip()


def _hash_text(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()


def compute_profile_embedding(profile: AlumniProfile, *, force: bool = False):
    """Recompute and persist the embedding for ``profile``.

    Skips the rewrite when the source-text hash matches the prior run,
    unless ``force=True``. Returns the model instance or ``None`` when
    pgvector isn't installed.
    """
    try:
        from apps.alumni.profiles.models import AlumniProfileEmbedding
    except ImportError:
        return None

    text = _compose_profile_text(profile)
    if not text:
        return None
    text_hash = _hash_text(text)
    existing = AlumniProfileEmbedding.objects.filter(profile=profile).first()
    if existing and existing.source_text_hash == text_hash and not force:
        return existing

    from apps.core.ai.embeddings import get_embedder

    try:
        embedder = get_embedder()
        vector = embedder(text)
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.embedding compute failed profile=%s", profile.pk)
        return existing

    obj, _ = AlumniProfileEmbedding.objects.update_or_create(
        profile=profile,
        defaults={
            "embedding": vector,
            "source_text_hash": text_hash,
        },
    )
    return obj


def semantic_search_alumni(
    query: str,
    *,
    limit: int = 24,
    country: str = "",
    sector: str = "",
    discipline: str = "",
):
    """Rank opted-in alumni by cosine similarity to ``query``.

    Returns the same shape as :func:`apps.alumni.profiles.search.search_alumni`
    so callers can swap modes transparently.
    """
    if not query or not query.strip():
        return AlumniProfile.objects.none()

    try:
        from pgvector.django import CosineDistance

        from apps.alumni.profiles.models import AlumniProfileEmbedding
    except ImportError:
        return AlumniProfile.objects.none()

    from apps.core.ai.embeddings import get_embedder

    try:
        q_vec = get_embedder()(query.strip())
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.embedding query failed")
        return AlumniProfile.objects.none()

    embedded_profile_ids = AlumniProfileEmbedding.objects.annotate(
        _distance=CosineDistance("embedding", q_vec)
    ).order_by("_distance").values_list("profile_id", "_distance")

    ranked = list(embedded_profile_ids[: limit * 4])
    if not ranked:
        return AlumniProfile.objects.none()

    qs = AlumniProfile.objects.filter(
        visibility_consent=True,
        pk__in=[pid for pid, _d in ranked],
    ).select_related("user", "current_institution")
    if country:
        qs = qs.filter(user__profile__country__iexact=country)
    if sector:
        qs = qs.filter(employments__sector__icontains=sector).distinct()
    if discipline:
        qs = qs.filter(expertise_tags__contains=[discipline.lower()])

    distance_by_id = {pid: dist for pid, dist in ranked}
    profiles_in_order = sorted(
        list(qs),
        key=lambda p: distance_by_id.get(p.pk, 9.9),
    )[:limit]
    return profiles_in_order
