"""Text-embedding factory shared across modules.

Two embedders are supported:
  * OpenAI ``text-embedding-3-small`` when ``settings.OPENAI_API_KEY`` is set.
  * A deterministic local fallback (HashingVectorizer projected to 1536 dims)
    so dev/CI environments don't need a key.

Formerly this factory lived in the REP LMS
(``apps.rep.personalisation.embeddings``). It moved here, decoupled from any
course models, when the REP LMS was retired — Alumni semantic search still
depends on it.
"""
from __future__ import annotations

import logging
import math
from typing import Callable, List

import numpy as np
from django.conf import settings

logger = logging.getLogger(__name__)

# Output dimensionality (matches OpenAI text-embedding-3-small and the
# pgvector columns that store these vectors, e.g. AlumniProfileEmbedding).
EMBEDDING_DIMS = 1536

Embedder = Callable[[str], List[float]]


def _local_embedder() -> Embedder:
    """Deterministic fallback. HashingVectorizer feature space is much smaller
    than 1536, so we tile/truncate to fit EMBEDDING_DIMS exactly. This yields
    stable vectors for tests without an API key."""
    from sklearn.feature_extraction.text import HashingVectorizer

    base_dims = 512
    vec = HashingVectorizer(
        n_features=base_dims,
        alternate_sign=False,
        norm="l2",
        analyzer="word",
        lowercase=True,
    )

    def embed(text: str) -> List[float]:
        if not text or not text.strip():
            return [0.0] * EMBEDDING_DIMS
        sparse = vec.transform([text])
        dense = sparse.toarray()[0]
        # Tile up to the required dimensionality, then truncate to exact size.
        repeats = math.ceil(EMBEDDING_DIMS / base_dims)
        tiled = np.tile(dense, repeats)[:EMBEDDING_DIMS]
        norm = np.linalg.norm(tiled)
        if norm > 0:
            tiled = tiled / norm
        return tiled.astype(float).tolist()

    return embed


def _openai_embedder() -> Embedder:
    from openai import OpenAI

    client = OpenAI(api_key=settings.OPENAI_API_KEY)

    def embed(text: str) -> List[float]:
        if not text or not text.strip():
            return [0.0] * EMBEDDING_DIMS
        resp = client.embeddings.create(
            model="text-embedding-3-small",
            input=text,
        )
        return list(resp.data[0].embedding)

    return embed


def get_embedder() -> Embedder:
    """Return the embedder configured for this environment."""
    if getattr(settings, "OPENAI_API_KEY", "").strip():
        try:
            return _openai_embedder()
        except Exception as exc:  # pragma: no cover - import / config error
            logger.warning("core.ai.openai_embedder_unavailable: %s", exc)
    return _local_embedder()
