"""Router HTTP FastAPI per amministrazione RAG a runtime.

Montato sullo stesso FastAPI che serve il websocket /v1/realtime (vedi
:func:`speech_to_speech.api.openai_realtime.websocket_router.create_app`).

Tutti gli endpoint sono JSON e si appoggiano all'istanza globale
``get_global_rag()``; se RAG non è attivo rispondono 503.
"""
from __future__ import annotations

import logging
from typing import Any, Optional

from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field

from speech_to_speech.RAG.retriever import get_global_rag

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/v1/rag", tags=["RAG"])


# ── Pydantic schemi request / response ────────────────────────────────────────


class AddDocumentRequest(BaseModel):
    text: str = Field(..., description="Testo completo documento da aggiungere.")
    source: str = Field(
        ...,
        description="Nome univoco sorgente (es. 'crm/cliente_123_nota_20260826').",
        min_length=1,
    )
    metadata: Optional[dict[str, Any]] = Field(
        default=None,
        description="Metadata arbitrari (non usati dal retrieval, ma salvati).",
    )
    persist: bool = Field(
        default=True,
        description="Se True, salva in kb/_dynamic.jsonl per il prossimo avvio.",
    )
    dynamic: bool = Field(
        default=True,
        description="Se True, marca come chunk dinamico (rimovibile via API).",
    )


class AddChunksRequest(BaseModel):
    items: list[dict[str, Any]] = Field(
        ...,
        description="Lista di chunk: ognuno con chiavi {text, source?, chunk_index?, metadata?}.",
        min_length=1,
    )
    persist: bool = True
    dynamic: bool = True


class RemoveRequest(BaseModel):
    source_prefix: str = Field(
        ...,
        description="Sorgente o prefisso da rimuovere (vedi `exact`).",
        min_length=1,
    )
    exact: bool = Field(
        default=False,
        description="False = prefisso; True = match esatto su source.",
    )
    persist: bool = True


class SearchRequest(BaseModel):
    query: str = Field(..., description="Testo da cercare.", min_length=1)
    top_k: Optional[int] = Field(default=None, ge=1, le=50)
    threshold: Optional[float] = Field(default=None, ge=0.0, le=1.0)


class ReloadRequest(BaseModel):
    force_rebuild: bool = Field(
        default=True,
        description="Ricostruzione totale da file md/txt/jsonl in kb/.",
    )


class ListSourcesResponse(BaseModel):
    count: int
    items: list[dict[str, Any]]


class ListChunksRequest(BaseModel):
    source_prefix: Optional[str] = Field(default=None, description="Filtro prefisso sorgente.")
    source_exact: Optional[str] = Field(default=None, description="Filtro esatto sorgente (vince se entrambi sono settati).")
    query: Optional[str] = Field(default=None, description="Se fornito, ordina per rilevanza embedding coseno.")
    min_score: Optional[float] = Field(default=None, ge=0.0, le=1.0, description="Solo con `query`: score minimo.")
    offset: int = Field(default=0, ge=0)
    limit: int = Field(default=100, ge=1, le=1000)
    include_text: bool = Field(default=True)
    include_embedding: bool = Field(default=False, description="Attenzione: output grosso.")


class UpdateChunkRequest(BaseModel):
    """Identificazione + modifiche. Uno e un solo dei gruppi deve essere valorizzato:

    - ``index``
    - ``source`` + ``chunk_index`` (opzionale)
    """
    index: Optional[int] = Field(default=None, ge=0, description="Posizione nell'array dei chunk (da list_chunks).")
    source: Optional[str] = Field(default=None, description="Sorgente. Se unico chunk basta questo.")
    chunk_index: Optional[int] = Field(default=None, description="Indice chunk interno alla source.")

    new_text: Optional[str] = Field(default=None, description="Se non vuoto/None → re-embedding del testo.")
    new_metadata: Optional[dict[str, Any]] = Field(default=None, description="Sostituisce TUTTI i metadata (flag _dynamic viene preservato).")
    new_source: Optional[str] = Field(default=None, description="Cambia nome sorgente.")
    persist: bool = True


class UpsertDocumentRequest(BaseModel):
    """Update atomico di un intero documento identificato da `source`."""
    text: str = Field(..., description="Nuovo testo completo. Stringa vuota = DELETE atomico per questa source.")
    source: str = Field(..., min_length=1)
    metadata: Optional[dict[str, Any]] = None
    persist: bool = True
    dynamic: bool = True


# ── Helpers ──────────────────────────────────────────────────────────────────


def _require_rag():
    rag = get_global_rag()
    if rag is None:
        raise HTTPException(
            status_code=503,
            detail={
                "code": "RAG_NOT_ENABLED",
                "message": "RAG non attivo: avvia con --rag_enabled o verifica start_pipeline_rag.sh",
            },
        )
    return rag


# ── Endpoints ─────────────────────────────────────────────────────────────────


@router.get("/status")
def rag_status() -> dict[str, Any]:
    """Stato generale dell'istanza RAG (chunk, embeddings, sorgenti, device)."""
    return _require_rag().status()


@router.get("/sources")
def rag_list_sources() -> dict[str, Any]:
    """Lista sorgenti uniche nell'indice con il conteggio chunk per ognuna."""
    items = _require_rag().list_sources()
    return {"count": len(items), "items": items}


@router.get("/chunks")
def rag_list_chunks_get(
    source_prefix: Optional[str] = Query(default=None),
    source_exact: Optional[str] = Query(default=None),
    query: Optional[str] = Query(default=None),
    min_score: Optional[float] = Query(default=None, ge=0.0, le=1.0),
    offset: int = Query(default=0, ge=0),
    limit: int = Query(default=100, ge=1, le=1000),
    include_text: bool = Query(default=True),
    include_embedding: bool = Query(default=False),
) -> dict[str, Any]:
    """GET friendly (query params): lista chunk con filtri + paginazione."""
    rag = _require_rag()
    return rag.list_chunks(
        source_prefix=source_prefix,
        source_exact=source_exact,
        query=query,
        min_score=min_score,
        offset=offset,
        limit=limit,
        include_text=include_text,
        include_embedding=include_embedding,
    )


@router.post("/chunks/list")
def rag_list_chunks_post(req: ListChunksRequest) -> dict[str, Any]:
    """Stesso list_chunks ma con body JSON (consigliato per query complesse)."""
    rag = _require_rag()
    return rag.list_chunks(**req.model_dump())


@router.post("/chunks/update")
def rag_update_chunk(req: UpdateChunkRequest) -> dict[str, Any]:
    """Aggiorna un singolo chunk (testo / metadata / sorgente)."""
    if (
        req.index is None
        and req.source is None
    ):
        raise HTTPException(
            status_code=400,
            detail={
                "code": "IDENTIFIER_REQUIRED",
                "message": "Devi specificare almeno `index`, oppure `source` (con chunk_index opzionale).",
            },
        )
    rag = _require_rag()
    res = rag.update_chunk(
        index=req.index,
        source=req.source,
        chunk_index=req.chunk_index,
        new_text=req.new_text,
        new_metadata=req.new_metadata,
        new_source=req.new_source,
        persist=req.persist,
    )
    if res.get("updated", 0) == 0:
        err = res.get("error", "CHUNK_NOT_FOUND")
        msg = res.get("message", "Nessun chunk trovato")
        if err == "CHUNK_NOT_FOUND":
            status = 404
        elif err == "AMBIGUOUS_SOURCE":
            status = 409
        else:
            status = 400
        raise HTTPException(status_code=status, detail={"code": err, "message": msg, **{k: v for k, v in res.items() if k not in {"updated", "error", "message"}}})
    return res


@router.post("/upsert/document")
def rag_upsert_document(req: UpsertDocumentRequest) -> dict[str, Any]:
    """Upsert atomico di un intero documento (delete source exact + split nuovo testo).

    Consigliato per l'update "normale" di un documento (non devi preoccuparti
    del chunking). Se mandi ``text: ""`` → DELETE atomico della sorgente.
    """
    rag = _require_rag()
    return rag.upsert_document(
        req.text,
        source=req.source,
        metadata=req.metadata,
        persist=req.persist,
        dynamic=req.dynamic,
    )


@router.post("/search")
def rag_search(req: SearchRequest) -> dict[str, Any]:
    """Cerca chunk rilevanti — stesso algoritmo usato per l'iniezione nel prompt."""
    rag = _require_rag()
    results = rag.search(req.query, top_k=req.top_k, threshold=req.threshold)
    return {
        "query": req.query,
        "count": len(results),
        "results": [
            {
                "text": r.chunk.text,
                "source": r.chunk.source,
                "chunk_index": r.chunk.chunk_index,
                "score": r.score,
                "metadata": r.chunk.metadata,
            }
            for r in results
        ],
    }


@router.post("/add/document")
def rag_add_document(req: AddDocumentRequest) -> dict[str, Any]:
    """Splitta un documento in chunk (stesso chunker di build_index) e li aggiunge.

    Consigliato quando il tuo payload è un testo libero lungo (es. una nota cliente,
    un articolo, una mail).
    """
    rag = _require_rag()
    return rag.add_document(
        req.text,
        source=req.source,
        metadata=req.metadata,
        persist=req.persist,
        dynamic=req.dynamic,
    )


@router.post("/add/chunks")
def rag_add_chunks(req: AddChunksRequest) -> dict[str, Any]:
    """Aggiungi chunk pre-costruiti senza chunking automatico.

    Consigliato quando hai già splittato i dati lato sorgente (es. records DB,
    risultati query strutturati).
    """
    rag = _require_rag()
    return rag.add_chunks(
        req.items,
        persist=req.persist,
        dynamic=req.dynamic,
    )


@router.post("/remove")
def rag_remove(req: RemoveRequest) -> dict[str, Any]:
    """Rimuove chunk.

    - ``exact=false`` (default): tutti i chunk la cui source INIZIA con ``source_prefix``
    - ``exact=true``: solo i chunk la cui source è ESATTAMENTE uguale
    """
    rag = _require_rag()
    return rag.remove_by_source(
        req.source_prefix,
        exact=req.exact,
        persist=req.persist,
    )


@router.post("/reload")
def rag_reload(req: Optional[ReloadRequest] = None) -> dict[str, Any]:
    """Ricostruisce l'indice da zero leggendo i file in kb/.

    Equivalente a riavviare con ``--rag_force_rebuild``, ma a caldo.
    """
    rag = _require_rag()
    fr = req.force_rebuild if req is not None else True
    return rag.reload_from_disk(force_rebuild=fr)
