"""
Core memory operations: embed, upsert, and search for memory items and docs.
"""
from __future__ import annotations

from empirica.core.qdrant.collections import (
    _docs_collection,
    _eidetic_collection,
    _episodic_collection,
    _memory_collection,
)
from empirica.core.qdrant.connection import (
    _check_qdrant_available,
    _get_embedding_safe,
    _get_embeddings_batch,
    _get_qdrant_client,
    _get_qdrant_imports,
    _get_vector_size,
    _rest_search,
    logger,
)


def embed_single_memory_item(
    project_id: str,
    item_id: str,
    text: str,
    item_type: str,
    session_id: str = None,
    goal_id: str = None,
    subtask_id: str = None,
    subject: str = None,
    impact: float = None,
    is_resolved: bool = None,
    resolved_by: str = None,
    timestamp: str = None
) -> bool:
    """
    Embed a single memory item (finding, unknown, mistake, dead_end) to Qdrant.
    Called automatically when logging epistemic breadcrumbs.

    Returns True if successful, False if Qdrant not available or embedding failed.
    This is a non-blocking operation - core Empirica works without it.
    """
    # Check if Qdrant is available (graceful degradation)
    if not _check_qdrant_available():
        return False

    try:
        _, Distance, VectorParams, PointStruct = _get_qdrant_imports()
        client = _get_qdrant_client()
        if client is None:
            return False
        coll = _memory_collection(project_id)

        # Ensure collection exists
        if not client.collection_exists(coll):
            vector_size = _get_vector_size()
            client.create_collection(coll, vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE))

        vector = _get_embedding_safe(text)
        if vector is None:
            return False

        payload = {
            "type": item_type,
            "text": text[:500] if text else None,
            "text_full": text if len(text) <= 500 else None,
            "session_id": session_id,
            "goal_id": goal_id,
            "subtask_id": subtask_id,
            "subject": subject,
            "impact": impact,
            "is_resolved": is_resolved,
            "resolved_by": resolved_by,
            "timestamp": timestamp,
        }

        # Use hash of item_id for numeric Qdrant point ID
        import hashlib
        point_id = int(hashlib.md5(item_id.encode()).hexdigest()[:15], 16)

        point = PointStruct(id=point_id, vector=vector, payload=payload)
        client.upsert(collection_name=coll, points=[point])
        return True
    except Exception as e:
        # Log but don't fail - embedding is enhancement, not critical path
        import logging
        logging.getLogger(__name__).warning(f"Failed to embed memory item: {e}")
        return False


def upsert_docs(project_id: str, docs: list[dict]) -> int:
    """
    Upsert documentation embeddings.
    docs: List of {id, text, metadata:{doc_path, tags, concepts, questions, use_cases}}
    Returns number of docs upserted, or 0 if Qdrant not available.
    """
    if not _check_qdrant_available():
        return 0

    try:
        _, Distance, VectorParams, PointStruct = _get_qdrant_imports()
        client = _get_qdrant_client()
        if client is None:
            return 0
        coll = _docs_collection(project_id)

        if not client.collection_exists(coll):
            vector_size = _get_vector_size()
            client.create_collection(
                coll,
                vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE),
            )

        points = []
        for d in docs:
            vector = _get_embedding_safe(d.get("text", ""))
            if vector is None:
                continue
            payload = {
                "doc_path": d.get("metadata", {}).get("doc_path"),
                "tags": d.get("metadata", {}).get("tags", []),
                "concepts": d.get("metadata", {}).get("concepts", []),
                "questions": d.get("metadata", {}).get("questions", []),
                "use_cases": d.get("metadata", {}).get("use_cases", []),
            }
            points.append(PointStruct(id=d["id"], vector=vector, payload=payload))
        if points:
            client.upsert(collection_name=coll, points=points)
        return len(points)
    except Exception as e:
        logger.warning(f"Failed to upsert docs: {e}")
        return 0


def upsert_memory(project_id: str, items: list[dict]) -> int:
    """
    Upsert memory embeddings (findings, unknowns, mistakes, dead_ends).
    items: List of {id, text, type, goal_id, subtask_id, session_id, timestamp, ...}
    Returns number of items upserted, or 0 if Qdrant not available.
    """
    if not _check_qdrant_available():
        return 0

    try:
        _, _, _, PointStruct = _get_qdrant_imports()
        client = _get_qdrant_client()
        if client is None:
            return 0
        coll = _memory_collection(project_id)

        # Batch embed texts (chunked to avoid API payload limits)
        import hashlib
        texts = [it.get("text", "") for it in items]
        embed_batch_size = 50
        vectors = []
        for i in range(0, len(texts), embed_batch_size):
            batch_texts = texts[i:i + embed_batch_size]
            batch_vectors = _get_embeddings_batch(batch_texts)
            vectors.extend(batch_vectors)

        points = []
        for it, vector in zip(items, vectors):
            if vector is None:
                continue
            text = it.get("text", "")
            # Extract source file refs for provenance in search results
            source_files = None
            try:
                from empirica.utils.finding_refs import parse_file_references
                file_refs = parse_file_references(text)
                if file_refs:
                    source_files = [r["file"] for r in file_refs]
            except Exception:
                pass

            payload = {
                "type": it.get("type", "unknown"),
                "text": text[:500] if text else None,
                "text_full": text if len(text) <= 500 else None,
                "goal_id": it.get("goal_id"),
                "subtask_id": it.get("subtask_id"),
                "session_id": it.get("session_id"),
                "timestamp": it.get("timestamp"),
                "subject": it.get("subject"),
                "impact": it.get("impact"),
                "is_resolved": it.get("is_resolved"),
                "resolved_by": it.get("resolved_by"),
                "source_files": source_files,
            }
            raw_id = it["id"]
            if isinstance(raw_id, str):
                point_id = int(hashlib.md5(raw_id.encode()).hexdigest()[:15], 16)
            else:
                point_id = raw_id
            points.append(PointStruct(id=point_id, vector=vector, payload=payload))
        if points:
            # Batch upserts to stay under Qdrant's payload size limit (32MB)
            batch_size = 200
            for i in range(0, len(points), batch_size):
                batch = points[i:i + batch_size]
                client.upsert(collection_name=coll, points=batch)
        return len(points)
    except Exception as e:
        logger.warning(f"Failed to upsert memory: {e}")
        return 0


def search(project_id: str, query_text: str, kind: str = "focused", limit: int = 5) -> dict[str, list[dict]]:
    """
    Semantic search over project knowledge.

    Args:
        project_id: Project UUID
        query_text: Search query
        kind: "focused" (default: docs + eidetic + episodic), "all", "docs", "memory", "eidetic", "episodic"
        limit: Max results per collection

    Returns empty results if Qdrant not available.
    """
    # Focused = docs + eidetic + episodic so project-embed content is searchable
    # without forcing callers to discover the hidden --type docs / --type all modes.
    if kind == "focused":
        search_kinds = ["docs", "eidetic", "episodic"]
    elif kind == "all":
        search_kinds = ["docs", "memory", "eidetic", "episodic"]
    else:
        search_kinds = [kind]
    empty_result = {k: [] for k in search_kinds}

    if not _check_qdrant_available():
        return empty_result

    qvec = _get_embedding_safe(query_text)
    if qvec is None:
        return empty_result

    # Collection config: (name, collection_fn, payload_fields)
    _SEARCH_COLLECTIONS = {
        "docs": (_docs_collection, ["doc_path", "tags", "concepts"]),
        "memory": (_memory_collection, ["type", "text", "session_id", "goal_id", "timestamp", "impact"]),
        "eidetic": (_eidetic_collection, ["type", "content", "confidence", "domain"]),
        "episodic": (_episodic_collection, ["type", "narrative", "session_id", "outcome"]),
    }

    results: dict[str, list[dict]] = {}
    client = _get_qdrant_client()
    if client is None:
        return empty_result

    # Query each collection via client
    for kind_name in search_kinds:
        if kind_name not in _SEARCH_COLLECTIONS:
            continue
        coll_fn, fields = _SEARCH_COLLECTIONS[kind_name]
        try:
            coll_name = coll_fn(project_id)
            if client.collection_exists(coll_name):
                resp = client.query_points(
                    collection_name=coll_name, query=qvec, limit=limit, with_payload=True)
                results[kind_name] = [
                    {"score": getattr(r, 'score', 0.0) or 0.0,
                     **{f: (r.payload or {}).get(f) for f in fields}}
                    for r in resp.points
                ]
            else:
                results[kind_name] = []
        except Exception as e:
            logger.debug(f"{kind_name} query failed: {e}")
            results[kind_name] = []

    if results:
        return results

    # REST fallback
    logger.debug("Trying REST fallback for search")
    try:
        for kind_name in search_kinds:
            if kind_name not in _SEARCH_COLLECTIONS:
                continue
            coll_fn, fields = _SEARCH_COLLECTIONS[kind_name]
            raw = _rest_search(coll_fn(project_id), qvec, limit)
            results[kind_name] = [
                {"score": d.get('score', 0.0),
                 **{f: (d.get('payload') or {}).get(f) for f in fields}}
                for d in raw
            ]
        return results
    except Exception as e:
        logger.debug(f"REST search also failed: {e}")
        return empty_result

