"""
Poma ↔ Qdrant helper utils.

Pure helpers to:
- convert POMA chunk_data (.poma or dict) into Qdrant PointStruct using models.Document
- convert Qdrant results into POMA cheatsheets
"""

from __future__ import annotations

import os
import uuid
import warnings
from typing import Any

from qdrant_client.http import models as qmodels

from poma.client import extract_chunks_and_chunksets_from_poma_archive
from poma.retrieval import generate_cheatsheets

__all__ = [
    "chunk_uuid_string",
    "prepare_points_from_chunk_data",
    "points_from_chunk_data",
    "results_to_cheatsheet_inputs",
    "cheatsheets_from_results",
    "ensure_collection",
]


def chunk_uuid_string(file_id: str, chunkset_index: int) -> str:
    """
    Deterministic RFC4122 UUID (v5) from (file_id, chunkset_index).
    Same inputs -> same UUID. Different index -> different UUID.
    """
    if chunkset_index < 0:
        raise ValueError("chunkset_index must be non-negative")
    name = f"{file_id}#{chunkset_index}"
    return str(uuid.uuid5(uuid.NAMESPACE_URL, name))


def _chunk_data_from_input(
    chunk_data: dict | str | os.PathLike[str],
) -> dict:
    """Accept chunk_data dict or path to a .poma file; return chunk_data dict."""
    if isinstance(chunk_data, (str, os.PathLike)):
        path = os.fspath(chunk_data)
        if not path.lower().endswith(".poma"):
            raise ValueError(
                "Path must point to a .poma file; got {!r}".format(path)
            )
        return extract_chunks_and_chunksets_from_poma_archive(
            poma_archive_path=chunk_data
        )
    return chunk_data


def _get_file_id_fallback(chunksets: list[dict], chunks: list[dict]) -> str:
    file_id = None
    if chunksets:
        file_id = chunksets[0].get("file_id")
    if not file_id and chunks:
        file_id = chunks[0].get("file_id") or chunks[0].get("tag")
    return file_id or "unknown"


def prepare_points_from_chunk_data(
    chunk_data: dict | str | os.PathLike[str],
    *,
    store_chunk_details: bool = True,
) -> tuple[list[str], list[str], list[dict]]:
    """
    Returns: (ids, documents, payloads)
    - ids: deterministic UUID strings (file_id + chunkset_index)
    - documents: chunkset.contents (this is what you embed)
    - payloads: metadata including "text"; "chunk_details" only if store_chunk_details=True
    """
    chunk_data = _chunk_data_from_input(chunk_data)
    chunksets: list[dict] = chunk_data.get("chunksets", [])
    chunks: list[dict] = chunk_data.get("chunks", [])
    if not chunksets:
        return [], [], []

    file_id = _get_file_id_fallback(chunksets, chunks)
    chunks_by_index = {c.get("chunk_index"): c for c in chunks}

    ids: list[str] = []
    documents: list[str] = []
    payloads: list[dict] = []

    for idx, chunkset in enumerate(chunksets):
        contents = chunkset.get("contents", "")
        if not contents:
            continue

        chunkset_index = chunkset.get("chunkset_index")
        if chunkset_index is None:
            chunkset_index = idx

        chunkset_file_id = chunkset.get("file_id") or file_id
        point_id = chunk_uuid_string(chunkset_file_id, int(chunkset_index))

        raw_chunks = chunkset.get("chunks", [])
        chunk_indices = list(raw_chunks) if isinstance(raw_chunks, (list, tuple)) else []

        payload: dict = {
            "chunkset_index": int(chunkset_index),
            "chunks": chunk_indices,
            "file_id": chunkset_file_id,
            "text": contents,
        }
        if store_chunk_details:
            relevant_chunks = [
                chunks_by_index.get(ci)
                for ci in chunk_indices
                if ci in chunks_by_index
            ]
            payload["chunk_details"] = relevant_chunks

        ids.append(point_id)
        documents.append(contents)
        payloads.append(payload)

    return ids, documents, payloads


def _validate_model_name(model: str | None, label: str) -> str:
    if not isinstance(model, str) or not model.strip():
        raise ValueError(f"{label} must be a non-empty string")
    return model


def points_from_chunk_data(
    chunk_data: dict | str | os.PathLike[str],
    *,
    dense_model: str,
    sparse_model: str | None = None,
    dense_name: str = "dense",
    sparse_name: str = "sparse",
    dense_options: dict | None = None,
    sparse_options: dict | None = None,
    store_chunk_details: bool = True,
) -> list[qmodels.PointStruct]:
    """
    Build Qdrant PointStruct list using models.Document for dense/sparse vectors.
    """
    dense_model = _validate_model_name(dense_model, "dense_model")
    if sparse_model is not None:
        sparse_model = _validate_model_name(sparse_model, "sparse_model")

    ids, documents, payloads = prepare_points_from_chunk_data(
        chunk_data,
        store_chunk_details=store_chunk_details,
    )
    if not documents:
        return []

    points: list[qmodels.PointStruct] = []
    for pid, doc, payload in zip(ids, documents, payloads, strict=True):
        vector: dict[str, qmodels.Document] = {
            dense_name: qmodels.Document(
                text=doc,
                model=dense_model,
                options=dense_options,
            )
        }
        if sparse_model is not None:
            vector[sparse_name] = qmodels.Document(
                text=doc,
                model=sparse_model,
                options=sparse_options,
            )
        points.append(qmodels.PointStruct(id=pid, vector=vector, payload=payload))
    return points


def results_to_cheatsheet_inputs(
    results: Any,
) -> tuple[list[dict], list[dict]]:
    """
    Extract relevant_chunksets and all_chunks from Qdrant results.

    Accepts:
    - list of ScoredPoint
    - QueryResponse with .points
    - list of dicts with payload/metadata keys
    """
    if results is None:
        return [], []
    hits = results
    if not isinstance(results, list):
        hits = getattr(results, "points", results)
    if hits is None:
        return [], []

    relevant_chunksets: list[dict] = []
    all_chunks: list[dict] = []
    seen_chunks: set[int] = set()

    for h in hits:
        payload: dict | None
        if isinstance(h, dict):
            payload = h.get("payload") or h.get("metadata")
        else:
            payload = getattr(h, "payload", None)
        if not isinstance(payload, dict):
            payload = {}

        relevant_chunksets.append(
            {
                "chunkset_index": payload.get("chunkset_index"),
                "chunks": payload.get("chunks", []),
                "file_id": payload.get("file_id", "unknown"),
            }
        )

        chunk_details = payload.get("chunk_details", []) or []
        for ch in chunk_details:
            if not isinstance(ch, dict):
                continue
            cidx = ch.get("chunk_index")
            if isinstance(cidx, int) and cidx not in seen_chunks:
                seen_chunks.add(cidx)
                all_chunks.append(ch)

    return relevant_chunksets, all_chunks


def cheatsheets_from_results(
    results: Any,
    *,
    chunk_data: dict | str | os.PathLike[str] | None = None,
) -> list[dict]:
    """
    Convert Qdrant results into POMA cheatsheets.

    If chunk_data is provided, its chunks are used instead of payload chunk_details.
    """
    relevant_chunksets, all_chunks = results_to_cheatsheet_inputs(results)
    if chunk_data is not None:
        chunk_data = _chunk_data_from_input(chunk_data)
        all_chunks = chunk_data.get("chunks", []) or []
    return generate_cheatsheets(relevant_chunksets, all_chunks) or []


def ensure_collection(
    client,
    name: str,
    *,
    dense: tuple[str, int] | dict | None = None,
    sparse: str | None = None,
    distance: qmodels.Distance = qmodels.Distance.COSINE,
) -> None:
    """
    Idempotent collection creation helper for Qdrant.
    Uses QdrantClient collection_exists() when available, otherwise checks get_collections().
    """
    if hasattr(client, "collection_exists"):
        exists = client.collection_exists(name)
    else:
        exists = name in {c.name for c in client.get_collections().collections}
    if exists:
        try:
            info = client.get_collection(name)
            params = getattr(getattr(info, "config", None), "params", None)
            vectors = getattr(params, "vectors", None) if params is not None else None
            sparse_vectors = getattr(params, "sparse_vectors", None) if params is not None else None
            vectors_desc = _format_vectors_config(vectors)
            sparse_desc = _format_sparse_vectors_config(sparse_vectors)
            warnings.warn(
                f"Collection {name!r} already exists; using existing config. "
                f"vectors={vectors_desc}; sparse_vectors={sparse_desc}",
                RuntimeWarning,
                stacklevel=2,
            )
        except Exception:
            warnings.warn(
                f"Collection {name!r} already exists; using existing config.",
                RuntimeWarning,
                stacklevel=2,
            )
        return

    dense_name = None
    dense_size = None
    if dense is not None:
        if isinstance(dense, dict):
            dense_name = dense.get("name")
            dense_size = dense.get("size")
        else:
            dense_name, dense_size = dense

    vectors_config = {}
    if dense_name is not None and dense_size is not None:
        vectors_config = {
            dense_name: qmodels.VectorParams(
                size=int(dense_size),
                distance=distance,
            )
        }

    kwargs = {
        "collection_name": name,
        "vectors_config": vectors_config,
    }
    if sparse is not None:
        kwargs["sparse_vectors_config"] = {sparse: qmodels.SparseVectorParams()}

    client.create_collection(**kwargs)


def _format_vectors_config(vectors: object) -> str:
    if vectors is None:
        return "None"
    if isinstance(vectors, dict):
        parts = []
        for name, params in vectors.items():
            size = getattr(params, "size", None)
            distance = getattr(params, "distance", None)
            parts.append(f"{name}(size={size}, distance={distance})")
        return ", ".join(parts) if parts else "None"
    size = getattr(vectors, "size", None)
    distance = getattr(vectors, "distance", None)
    return f"default(size={size}, distance={distance})"


def _format_sparse_vectors_config(sparse_vectors: object) -> str:
    if not sparse_vectors:
        return "None"
    if isinstance(sparse_vectors, dict):
        return ", ".join(sorted(str(k) for k in sparse_vectors.keys()))
    return str(sparse_vectors)
