"""M3 RAG Embeddings — strict two-tier (no hash fallback).

Per T04: the hash-based "fallback" embedding was returning
confidently-wrong results (non-semantic vectors). When all neural
backends fail, the RAG system now fails LOUDLY instead of returning
gibberish.

Tier 1: Ollama bge-m3 (1024d, our own, zero cost) — best quality when reachable
Tier 2: OpenRouter NVIDIA Nemotron (2048d, free tier) — if API key set

Design (per DESIGN.md M3):
- Single async API: `await get_embedding(text) -> list[float]`
- BGE-M3 preferred when available
- Embedding dim is reported via EMBEDDING_DIM; collections use same dim
- All embeddings are float lists (JSON-serializable) for Redis storage
- FAIL LOUD: if both backends fail, raise RAGUnavailableError
- Monitor via Prometheus counter rag_empty_returns_total
"""
from __future__ import annotations

import logging
import os
from typing import Final

import httpx
import numpy as np

log = logging.getLogger(__name__)

EMBEDDING_DIM: Final = 1024

# Backends
_BACKEND = os.getenv("RAG_EMBED_BACKEND", "auto").lower()
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://host.docker.internal:11434").rstrip("/")
OLLAMA_MODEL = os.getenv("OLLAMA_EMBED_MODEL", "bge-m3")
OPENROUTER_URL = "https://openrouter.ai/api/v1/embeddings"
OPENROUTER_MODEL = "nvidia/llama-nemotron-embed-vl-1b-v2:free"


class RAGUnavailableError(Exception):
    """Raised when no embedding backend is reachable. The RAG endpoint
    translates this to HTTP 503 + an explicit empty-result body.
    See T04 in MINIMAX_M3_TASKS.md."""


def _ollama_reachable() -> bool:
    """Quick probe: is Ollama listening? Cached per process via env."""
    if os.getenv("_OLLAMA_PROBED"):
        return os.getenv("_OLLAMA_OK") == "1"
    try:
        with httpx.Client(timeout=2.0) as c:
            r = c.get(f"{OLLAMA_URL}/api/tags")
            ok = r.status_code == 200
    except Exception:
        ok = False
    os.environ["_OLLAMA_PROBED"] = "1"
    os.environ["_OLLAMA_OK"] = "1" if ok else "0"
    if not ok:
        log.info("rag_ollama_unreachable url=%s", OLLAMA_URL)
    return ok


def _resize(vec: list[float], dim: int) -> list[float]:
    """Pad or truncate a vector to target dim, then L2 normalize."""
    arr = np.asarray(vec, dtype=np.float32)
    arr = arr[:dim] if arr.shape[0] >= dim else np.concatenate([arr, np.zeros(dim - arr.shape[0], dtype=np.float32)])
    n = float(np.linalg.norm(arr))
    if n > 0:
        arr = arr / n
    return arr.tolist()


def _ollama_embed(text: str, dim: int = EMBEDDING_DIM) -> list[float] | None:
    """Try Ollama bge-m3. Returns None on any failure."""
    try:
        with httpx.Client(timeout=10.0) as c:
            r = c.post(
                f"{OLLAMA_URL}/api/embeddings",
                json={"model": OLLAMA_MODEL, "prompt": text[:8000]},
            )
            if r.status_code != 200:
                return None
            vec = r.json().get("embedding")
            if not vec or not isinstance(vec, list):
                return None
            return _resize(vec, dim)
    except Exception as e:
        log.debug("ollama_embed_failed: %s", e)
        return None


def _openrouter_embed(text: str, dim: int = EMBEDDING_DIM) -> list[float] | None:
    """Try OpenRouter Nemotron. Returns None on any failure."""
    key = os.getenv("OPENROUTER_API_KEY")
    if not key:
        return None
    try:
        with httpx.Client(timeout=15.0) as c:
            r = c.post(
                OPENROUTER_URL,
                headers={"Authorization": f"Bearer {key}"},
                json={"model": OPENROUTER_MODEL, "input": text[:8000]},
            )
            if r.status_code != 200:
                return None
            data = r.json().get("data", [])
            if not data:
                return None
            vec = data[0].get("embedding")
            if not vec:
                return None
            return _resize(vec, dim)
    except Exception as e:
        log.debug("openrouter_embed_failed: %s", e)
        return None


async def get_embedding(text: str) -> list[float]:
    """Async embedding API. Returns 1024d float list.

    Per T04: NO hash fallback. If both neural backends fail,
    raises RAGUnavailableError. The RAG endpoint translates this to
    HTTP 503 + {"status": "empty", "chunks": [], "message": ...}.
    """
    if not text or not text.strip():
        raise RAGUnavailableError("empty text input")

    # Explicit backend override
    if _BACKEND == "ollama":
        v = _ollama_embed(text)
        if v is not None:
            return v
        raise RAGUnavailableError("ollama backend failed and hash fallback removed per T04")
    if _BACKEND == "openrouter":
        v = _openrouter_embed(text)
        if v is not None:
            return v
        raise RAGUnavailableError("openrouter backend failed and hash fallback removed per T04")

    # auto: try ollama -> openrouter, fail loud if both fail
    if _ollama_reachable():
        v = _ollama_embed(text)
        if v is not None:
            return v
    v = _openrouter_embed(text)
    if v is not None:
        return v
    raise RAGUnavailableError(
        "no embedding backend available (ollama unreachable, openrouter failed). "
        "Per T04, hash fallback removed to prevent false-confidence in RAG results."
    )


async def get_embeddings(texts: list[str]) -> list[list[float]]:
    """Batch embedding. Sequential calls — Ollama and OpenRouter handle
    larger requests poorly and we'd rather keep memory bounded."""
    return [await get_embedding(t) for t in texts]


def current_backend() -> str:
    """Which tier is currently active? For observability."""
    if _BACKEND != "auto":
        return _BACKEND
    if _ollama_reachable():
        return "ollama"
    if os.getenv("OPENROUTER_API_KEY"):
        return "openrouter"
    return "unavailable"
