"""
Unified MCP tools for GraphRAG Engine.

Provides action-dispatched tools:
- rag_index(action, ...)
- rag_search(action, ...)
- rag_ops(action, ...)
"""

import logging
import json
import threading
import time
from pathlib import Path
from typing import Any, Dict, List, Optional

from .types import (
    RAGConfig, RAGQuery, EmbeddingProvider, VectorStoreType,
)
from .embedding_provider import get_embedding_provider, EmbeddingCache
from .vector_store import get_vector_store
from .entity_extractor import EntityExtractor
from .proof_of_graph import ProofOfGraph
from .retriever import HybridRetriever

logger = logging.getLogger(__name__)

# ============================================================================
# Module-level globals (MUST be importable by hierarchy bootstrap)
# ============================================================================

_config: Optional[RAGConfig] = None
_embedder = None
_vector_store = None
_entity_extractor = None
_proof_of_graph = None
_retriever = None
_bootstrap_started = False
_health_monitor_started = False
_HEALTH_MONITOR_INTERVAL_SECONDS = 120


# ============================================================================
# Lazy singletons / helpers
# ============================================================================

def _get_catalog_store(settings):
    """Get catalog store if available."""
    try:
        from src.data_catalog.catalog_store import CatalogStore
        return CatalogStore(data_dir=str(Path(settings.data_dir) / "data_catalog"))
    except Exception:
        return None


def _get_hierarchy_service():
    """Get hierarchy service if available."""
    try:
        from src.hierarchy.service import HierarchyService
        return HierarchyService()
    except Exception:
        return None


def _get_lineage_tracker(settings):
    """Get lineage tracker if available."""
    try:
        from src.lineage.lineage_tracker import LineageTracker
        return LineageTracker(data_dir=str(Path(settings.data_dir) / "lineage"))
    except Exception:
        return None


def _get_template_service():
    """Get template service if available."""
    try:
        from src.templates.service import TemplateService
        return TemplateService()
    except Exception:
        return None


def _ensure_initialized(settings) -> bool:
    """Ensure RAG engine is initialized."""
    global _config, _embedder, _vector_store, _entity_extractor, _proof_of_graph, _retriever

    if _retriever is not None:
        return True

    # Use default config if not configured
    if _config is None:
        _config = RAGConfig(
            vector_store_path=str(Path(settings.data_dir) / "graphrag" / "vectors"),
        )

    try:
        # Initialize embedding provider
        cache_dir = str(Path(settings.data_dir) / "graphrag" / "embedding_cache")
        _embedder = get_embedding_provider(
            _config.embedding_provider,
            model=_config.embedding_model,
            cache_dir=cache_dir,
        )

        # Initialize vector store
        _vector_store = get_vector_store(
            _config.vector_store_type,
            db_path=_config.vector_store_path + ".db" if _config.vector_store_type == VectorStoreType.SQLITE else None,
            dimension=_config.embedding_dimension,
        )

        # Get optional services
        catalog = _get_catalog_store(settings)
        hierarchy = _get_hierarchy_service()
        lineage = _get_lineage_tracker(settings)
        templates = _get_template_service()

        # Initialize entity extractor
        _entity_extractor = EntityExtractor(
            catalog_store=catalog,
            hierarchy_service=hierarchy,
        )

        # Initialize proof of graph
        _proof_of_graph = ProofOfGraph(
            catalog_store=catalog,
            hierarchy_service=hierarchy,
            lineage_tracker=lineage,
            strict_mode=_config.strict_validation,
        )

        # Initialize retriever
        _retriever = HybridRetriever(
            embedding_provider=_embedder,
            vector_store=_vector_store,
            catalog_store=catalog,
            lineage_tracker=lineage,
            template_service=templates,
        )

        logger.info("GraphRAG engine initialized")
        return True

    except Exception as e:
        logger.error(f"Failed to initialize GraphRAG: {e}")
        return False


def _health_log_path(settings) -> Path:
    return Path(settings.data_dir) / "graphrag" / "runtime_health.jsonl"


def _write_runtime_health_log(settings, event_type: str, payload: Dict[str, Any]) -> None:
    """Append structured health event to runtime log."""
    try:
        path = _health_log_path(settings)
        path.parent.mkdir(parents=True, exist_ok=True)
        event = {
            "ts_epoch": int(time.time()),
            "event_type": event_type,
            "payload": payload,
        }
        with path.open("a", encoding="utf-8") as f:
            f.write(json.dumps(event, ensure_ascii=True) + "\n")
    except Exception as e:
        logger.debug(f"Failed to write runtime health log: {e}")


def _tail_runtime_health_log(settings, limit: int = 20) -> List[Dict[str, Any]]:
    """Read last N runtime health events."""
    path = _health_log_path(settings)
    if not path.exists():
        return []
    try:
        lines = path.read_text(encoding="utf-8").splitlines()
        out: List[Dict[str, Any]] = []
        for line in lines[-max(1, limit):]:
            try:
                parsed = json.loads(line)
                if isinstance(parsed, dict):
                    out.append(parsed)
            except Exception:
                continue
        return out
    except Exception:
        return []


def _collect_runtime_status(settings) -> Dict[str, Any]:
    """Collect current runtime status for GraphRAG + hierarchy bridge wiring."""
    vector_ready = _vector_store is not None
    embedder_ready = _embedder is not None
    retriever_ready = _retriever is not None
    initialized = bool(vector_ready and embedder_ready and retriever_ready)

    hierarchy_bridge = {
        "available": False,
        "vector_store_available": False,
        "embedder_available": False,
    }
    try:
        from ..hierarchy.unified import _ensure_graph_bridge  # type: ignore

        bridge = _ensure_graph_bridge(settings.data_dir)
        if bridge:
            hierarchy_bridge = {
                "available": True,
                "vector_store_available": bridge._vector_store is not None,
                "embedder_available": bridge._embedder is not None,
            }
    except Exception as e:
        hierarchy_bridge["error"] = str(e)

    vector_stats: Dict[str, Any] = {}
    if _vector_store is not None:
        try:
            vector_stats = _vector_store.stats()
        except Exception as e:
            vector_stats = {"error": str(e)}

    return {
        "initialized": initialized,
        "bootstrap_started": _bootstrap_started,
        "health_monitor_started": _health_monitor_started,
        "vector_store_ready": vector_ready,
        "embedder_ready": embedder_ready,
        "retriever_ready": retriever_ready,
        "hierarchy_bridge": hierarchy_bridge,
        "vector_store_stats": vector_stats,
        "health_log_path": str(_health_log_path(settings)),
    }


def _bootstrap_runtime_indexes(settings) -> Dict[str, Any]:
    """Best-effort startup sync so vector store is warm and current."""
    if not _ensure_initialized(settings):
        return {"status": "failed", "error": "RAG engine not initialized"}

    summary: Dict[str, Any] = {"status": "ok", "steps": {}}

    try:
        summary["steps"]["hierarchies"] = _index_hierarchies(settings, force_reindex=False)
    except Exception as e:
        summary["steps"]["hierarchies"] = {"error": str(e)}

    try:
        summary["steps"]["catalog"] = _index_catalog(settings, force_reindex=False)
    except Exception as e:
        summary["steps"]["catalog"] = {"error": str(e)}

    _write_runtime_health_log(settings, "bootstrap_index", summary)
    return summary


def _start_bootstrap_thread(settings) -> None:
    """Start one daemon bootstrap thread per process."""
    global _bootstrap_started
    if _bootstrap_started:
        return

    _bootstrap_started = True

    def _run():
        try:
            result = _bootstrap_runtime_indexes(settings)
            logger.info(f"GraphRAG bootstrap complete: {result}")
            _write_runtime_health_log(settings, "bootstrap_complete", result)
        except Exception as e:
            logger.warning(f"GraphRAG bootstrap failed: {e}")
            _write_runtime_health_log(settings, "bootstrap_failed", {"error": str(e)})

    threading.Thread(target=_run, daemon=True, name="graphrag-bootstrap").start()


def _start_health_monitor_thread(settings) -> None:
    """Start periodic runtime health checks and append status snapshots to log."""
    global _health_monitor_started
    if _health_monitor_started:
        return

    _health_monitor_started = True

    def _run():
        while True:
            try:
                status = _collect_runtime_status(settings)
                _write_runtime_health_log(settings, "runtime_check", status)
                logger.info(
                    "GraphRAG runtime check: init=%s vector=%s embedder=%s bridge=%s",
                    status.get("initialized"),
                    status.get("vector_store_ready"),
                    status.get("embedder_ready"),
                    status.get("hierarchy_bridge", {}).get("available"),
                )
            except Exception as e:
                logger.warning(f"GraphRAG runtime check failed: {e}")
            time.sleep(_HEALTH_MONITOR_INTERVAL_SECONDS)

    threading.Thread(target=_run, daemon=True, name="graphrag-health-monitor").start()


# ============================================================================
# rag_index action handlers
# ============================================================================

def _index_catalog(settings, **kwargs) -> Dict[str, Any]:
    """Index catalog assets into the vector store."""
    if not _ensure_initialized(settings):
        return {"error": "RAG engine not initialized. Call rag_index(action='configure') first."}

    try:
        catalog = _get_catalog_store(settings)
        if not catalog:
            return {"error": "Catalog store not available"}

        asset_types = kwargs.get("asset_types", "TABLE,VIEW")
        force_reindex = kwargs.get("force_reindex", False)

        types = [t.strip() for t in asset_types.split(",")]
        assets = catalog.list_assets(asset_types=types)

        indexed = 0
        skipped = 0

        for asset in assets.get("assets", []):
            asset_id = f"catalog:{asset.get('id', '')}"

            # Check if already indexed
            if not force_reindex:
                existing = _vector_store.get(asset_id)
                if existing:
                    skipped += 1
                    continue

            # Build content for embedding
            name = asset.get("name", "")
            desc = asset.get("description", "")
            cols = ", ".join(c.get("name", "") for c in asset.get("columns", [])[:20])
            content = f"{name}: {desc}. Columns: {cols}"

            # Generate embedding and store
            embedding = _embedder.embed(content)
            success = _vector_store.upsert(
                id=asset_id,
                embedding=embedding,
                content=content,
                metadata={
                    "source_type": "catalog",
                    "asset_type": asset.get("type"),
                    "name": name,
                    **asset,
                },
            )

            if success:
                indexed += 1

        return {
            "status": "indexed",
            "indexed": indexed,
            "skipped": skipped,
            "total_in_store": _vector_store.count(),
        }

    except Exception as e:
        return {"error": str(e)}


def _index_hierarchies(settings, **kwargs) -> Dict[str, Any]:
    """Index hierarchy structures into the vector store."""
    if not _ensure_initialized(settings):
        return {"error": "RAG engine not initialized"}

    try:
        hierarchy_service = _get_hierarchy_service()
        if not hierarchy_service:
            return {"error": "Hierarchy service not available"}

        project_id = kwargs.get("project_id")
        force_reindex = kwargs.get("force_reindex", False)

        indexed = 0
        skipped = 0

        projects = hierarchy_service.list_projects()
        if project_id:
            projects = [p for p in projects if p.get("id") == project_id]

        for proj in projects:
            proj_id = proj.get("id", "")

            # Index project
            proj_doc_id = f"hierarchy_project:{proj_id}"
            if not force_reindex and _vector_store.get(proj_doc_id):
                skipped += 1
            else:
                content = f"Hierarchy Project: {proj.get('name', '')}. {proj.get('description', '')}"
                embedding = _embedder.embed(content)
                _vector_store.upsert(
                    id=proj_doc_id,
                    embedding=embedding,
                    content=content,
                    metadata={
                        "source_type": "hierarchy_project",
                        "project_id": proj_id,
                        **proj,
                    },
                )
                indexed += 1

            # Index hierarchies in project
            hierarchies = hierarchy_service.list_hierarchies(proj_id)
            for hier in hierarchies:
                hier_id = hier.get("hierarchy_id", hier.get("id", ""))
                doc_id = f"hierarchy:{proj_id}:{hier_id}"

                if not force_reindex and _vector_store.get(doc_id):
                    skipped += 1
                    continue

                # Build rich content with correct field names
                name = hier.get("hierarchy_name", "")
                levels = hier.get("hierarchy_level", {}) or {}
                level_parts = []
                for i in range(1, 16):
                    val = levels.get(f"level_{i}")
                    if val:
                        level_parts.append(f"L{i}: {val}")
                level_str = " > ".join(level_parts) if level_parts else ""

                # Include mappings and properties for richer embeddings
                mappings = hier.get("mapping", [])
                mapping_strs = []
                for m in mappings:
                    tbl = m.get("source_table", "")
                    col = m.get("source_column", "")
                    if tbl:
                        mapping_strs.append(f"{tbl}.{col}")

                properties = hier.get("properties", [])
                prop_strs = [f"{p.get('name')}={p.get('value')}" for p in properties]

                formula_config = hier.get("formula_config", {}) or {}
                formula_group = formula_config.get("formula_group", {}) or {}
                formula_rules = formula_group.get("rules", [])
                formula_strs = [f"{r.get('operation')} [{r.get('hierarchy_name', r.get('hierarchy_id', ''))}]" for r in formula_rules]

                content_parts = [f"Hierarchy: {name}"]
                if hier.get("description"):
                    content_parts.append(f"Description: {hier['description']}")
                if level_str:
                    content_parts.append(f"Levels: {level_str}")
                if mapping_strs:
                    content_parts.append(f"Source Mappings: {', '.join(mapping_strs)}")
                if prop_strs:
                    content_parts.append(f"Properties: {', '.join(prop_strs)}")
                if formula_strs:
                    content_parts.append(f"Formula: {' '.join(formula_strs)}")
                content = ". ".join(content_parts)

                level_depth = len(level_parts)

                embedding = _embedder.embed(content)
                _vector_store.upsert(
                    id=doc_id,
                    embedding=embedding,
                    content=content,
                    metadata={
                        "source_type": "hierarchy",
                        "project_id": proj_id,
                        "hierarchy_id": hier_id,
                        "name": name,
                        "parent_id": hier.get("parent_id"),
                        "is_root": hier.get("is_root", False),
                        "has_mappings": len(mappings) > 0,
                        "has_formula": len(formula_rules) > 0,
                        "property_count": len(properties),
                        "level_depth": level_depth,
                    },
                )
                indexed += 1

        return {
            "status": "indexed",
            "indexed": indexed,
            "skipped": skipped,
            "total_in_store": _vector_store.count(),
        }

    except Exception as e:
        return {"error": str(e)}


def _index_configure(settings, **kwargs) -> Dict[str, Any]:
    """Configure the GraphRAG engine."""
    global _config, _embedder, _vector_store, _retriever

    try:
        embedding_provider = kwargs.get("embedding_provider", "openai")
        embedding_model = kwargs.get("embedding_model", "text-embedding-3-small")
        vector_store = kwargs.get("vector_store", "sqlite")
        enable_proof_of_graph = kwargs.get("enable_proof_of_graph", True)
        strict_validation = kwargs.get("strict_validation", False)

        # Map string to enum
        provider_map = {
            "openai": EmbeddingProvider.OPENAI,
            "huggingface": EmbeddingProvider.HUGGINGFACE,
            "local": EmbeddingProvider.LOCAL,
        }
        store_map = {
            "sqlite": VectorStoreType.SQLITE,
            "chroma": VectorStoreType.CHROMA,
        }

        _config = RAGConfig(
            embedding_provider=provider_map.get(embedding_provider, EmbeddingProvider.OPENAI),
            embedding_model=embedding_model,
            vector_store_type=store_map.get(vector_store, VectorStoreType.SQLITE),
            vector_store_path=str(Path(settings.data_dir) / "graphrag" / "vectors"),
            enable_proof_of_graph=enable_proof_of_graph,
            strict_validation=strict_validation,
        )

        # Reset components to reinitialize with new config
        _embedder = None
        _vector_store = None
        _retriever = None

        # Initialize
        if _ensure_initialized(settings):
            return {
                "status": "configured",
                "config": {
                    "embedding_provider": embedding_provider,
                    "embedding_model": embedding_model,
                    "vector_store": vector_store,
                    "proof_of_graph_enabled": enable_proof_of_graph,
                },
            }
        else:
            return {"error": "Failed to initialize RAG engine"}

    except Exception as e:
        return {"error": str(e)}


def _index_sync_relationalai(settings, **kwargs) -> Dict[str, Any]:
    """Prepare or report RelationalAI sync metadata for hybrid deployments."""
    model_name = kwargs.get("rai_model", "adamus_dev_graph_model")
    source_nodes = kwargs.get("source_nodes", "ADAMUS_DEV_DB.ADAMUS_GRAPH.NODES")
    source_edges = kwargs.get("source_edges", "ADAMUS_DEV_DB.ADAMUS_GRAPH.EDGES")
    dry_run = kwargs.get("dry_run", True)
    commands = [
        "rai engines:create",
        f"rai imports:stream --source {source_nodes} --model {model_name}",
        f"rai imports:stream --source {source_edges} --model {model_name}",
        f"rai imports:list --model {model_name}",
    ]
    return {
        "status": "planned" if dry_run else "ready",
        "action": "sync_relationalai",
        "dry_run": dry_run,
        "commands": commands,
        "notes": "Execute commands in deployment environment where rai CLI is configured.",
    }


def _index_report_logic(settings, **kwargs) -> Dict[str, Any]:
    """Index report logic references into GraphRAG vector index."""
    if not _ensure_initialized(settings):
        return {"error": "RAG engine not initialized"}
    logic_items = kwargs.get("logic_items", [])
    if not isinstance(logic_items, list) or not logic_items:
        return {
            "error": "logic_items list is required for 'index_report_logic' action",
            "example": [{"logic_id": "RPT001", "name": "Revenue Bridge", "description": "Monthly variance logic"}],
        }
    indexed = 0
    for item in logic_items:
        logic_id = str(item.get("logic_id", "")).strip()
        if not logic_id:
            continue
        name = str(item.get("name", "")).strip()
        desc = str(item.get("description", "")).strip()
        rules = str(item.get("rules", "")).strip()
        content = f"{name}: {desc}. Rules: {rules}".strip()
        if not content:
            continue
        embedding = _embedder.embed(content)
        if _vector_store.upsert(
            id=f"report_logic:{logic_id}",
            embedding=embedding,
            content=content,
            metadata={"source_type": "report_logic", **item},
        ):
            indexed += 1
    return {
        "status": "indexed",
        "indexed": indexed,
        "submitted": len(logic_items),
        "total_in_store": _vector_store.count(),
    }


def _index_refresh_reuse_features(settings, **kwargs) -> Dict[str, Any]:
    """Emit a feature refresh request payload for scoring pipelines."""
    request_id = kwargs.get("request_id")
    if not request_id:
        return {"error": "request_id is required for 'refresh_reuse_features' action"}
    return {
        "status": "queued",
        "action": "refresh_reuse_features",
        "request_id": request_id,
        "message": "Reuse feature refresh should be executed by scoring pipeline.",
    }


# ============================================================================
# rag_search action handlers
# ============================================================================

def _search_search(settings, **kwargs) -> Dict[str, Any]:
    """Perform hybrid RAG search."""
    if not _ensure_initialized(settings):
        return {"error": "RAG engine not initialized"}

    try:
        query = kwargs.get("query")
        if not query:
            return {"error": "query is required for 'search' action"}

        max_results = kwargs.get("max_results", 10)
        include_lineage = kwargs.get("include_lineage", True)
        include_templates = kwargs.get("include_templates", True)

        # Build RAG query
        rag_query = RAGQuery(
            query=query,
            max_results=max_results,
            include_lineage=include_lineage,
            include_templates=include_templates,
        )

        # Extract entities
        rag_query = _entity_extractor.enrich_query(rag_query)

        # Perform retrieval
        context = _retriever.retrieve(rag_query)

        return {
            "status": "success",
            "query": query,
            "entities_found": [
                {"text": e.text, "type": e.entity_type.value, "confidence": e.confidence}
                for e in rag_query.entities
            ],
            "results": [
                {
                    "id": item.id,
                    "source": item.source.value,
                    "content": item.content[:200],
                    "score": round(item.score, 4),
                    "rank": item.rank,
                }
                for item in context.retrieved_items
            ],
            "retrieval_time_ms": context.retrieval_time_ms,
        }

    except Exception as e:
        return {"error": str(e)}


def _search_get_context(settings, **kwargs) -> Dict[str, Any]:
    """Get assembled RAG context for LLM prompting."""
    if not _ensure_initialized(settings):
        return {"error": "RAG engine not initialized"}

    try:
        query = kwargs.get("query")
        if not query:
            return {"error": "query is required for 'get_context' action"}

        max_results = kwargs.get("max_results", 10)
        format = kwargs.get("format", "structured")

        rag_query = RAGQuery(query=query, max_results=max_results)
        rag_query = _entity_extractor.enrich_query(rag_query)
        context = _retriever.retrieve(rag_query)

        if format == "prompt":
            return {
                "status": "success",
                "prompt_context": context.to_prompt_context(),
                "available_tables": context.available_tables,
                "available_hierarchies": context.available_hierarchies,
            }
        else:
            return {
                "status": "success",
                "domain": rag_query.domain,
                "industry": rag_query.industry,
                "templates": context.templates[:5],
                "skills": context.skills[:3],
                "lineage_paths": context.lineage_paths[:5],
                "glossary_terms": context.glossary_terms[:5],
                "catalog_assets": [
                    {"name": a.get("name"), "type": a.get("type")}
                    for a in context.catalog_assets[:10]
                ],
                "available_tables": context.available_tables[:20],
                "retrieval_time_ms": context.retrieval_time_ms,
            }

    except Exception as e:
        return {"error": str(e)}


def _search_suggest_similar(settings, **kwargs) -> Dict[str, Any]:
    """Find similar entities using semantic search."""
    if not _ensure_initialized(settings):
        return {"error": "RAG engine not initialized"}

    try:
        text = kwargs.get("text")
        if not text:
            return {"error": "text is required for 'suggest_similar' action"}

        entity_type = kwargs.get("entity_type", "all")
        top_k = kwargs.get("top_k", 5)

        # Generate embedding for query text
        embedding = _embedder.embed(text)

        # Build filter
        filter_dict = None
        if entity_type != "all":
            filter_dict = {"source_type": entity_type}

        # Search
        results = _vector_store.search(
            query_embedding=embedding,
            top_k=top_k,
            filter=filter_dict,
        )

        return {
            "query": text,
            "suggestions": [
                {
                    "name": meta.get("name", id),
                    "type": meta.get("source_type", "unknown"),
                    "similarity": round(score, 4),
                    "content": content[:100] if content else "",
                }
                for id, score, content, meta in results
            ],
        }

    except Exception as e:
        return {"error": str(e)}


def _search_entity_extract(settings, **kwargs) -> Dict[str, Any]:
    """Extract entities from a natural language query."""
    if not _ensure_initialized(settings):
        return {"error": "RAG engine not initialized"}

    try:
        query = kwargs.get("query")
        if not query:
            return {"error": "query is required for 'entity_extract' action"}

        entities = _entity_extractor.extract(query)

        return {
            "query": query,
            "entities": [
                {
                    "text": e.text,
                    "type": e.entity_type.value,
                    "confidence": round(e.confidence, 2),
                    "linked_id": e.linked_id,
                }
                for e in entities
            ],
            "domain": next((e.text for e in entities if e.entity_type.value == "domain"), None),
            "industry": next((e.text for e in entities if e.entity_type.value == "industry"), None),
        }

    except Exception as e:
        return {"error": str(e)}


def _search_reuse_candidates(settings, **kwargs) -> Dict[str, Any]:
    """Return candidate reusable logic from semantic and lexical search."""
    if not _ensure_initialized(settings):
        return {"error": "RAG engine not initialized"}
    query = kwargs.get("query")
    if not query:
        return {"error": "query is required for 'reuse_candidates' action"}
    top_n = int(kwargs.get("top_n", 5))
    rag_result = _search_search(settings, query=query, max_results=max(10, top_n))
    if rag_result.get("error"):
        return rag_result
    results = rag_result.get("results", [])
    candidates = []
    for idx, row in enumerate(results[:top_n], start=1):
        score = float(row.get("score", 0.0))
        candidates.append(
            {
                "rank": idx,
                "logic_id": row.get("id"),
                "reuse_probability": round(min(0.95, max(0.05, score)), 4),
                "evidence": {"source": row.get("source"), "snippet": row.get("content")},
            }
        )
    return {"status": "success", "query": query, "candidates": candidates}


def _search_explain_reuse_score(settings, **kwargs) -> Dict[str, Any]:
    """Explain how a reuse score was composed for a candidate."""
    logic_id = kwargs.get("logic_id")
    score = kwargs.get("reuse_probability")
    if not logic_id:
        return {"error": "logic_id is required for 'explain_reuse_score' action"}
    if score is None:
        return {"error": "reuse_probability is required for 'explain_reuse_score' action"}
    try:
        score_val = float(score)
    except (TypeError, ValueError):
        return {"error": "reuse_probability must be numeric"}
    band = "NEW_BUILD"
    if score_val >= 0.75:
        band = "HIGH_REUSE"
    elif score_val >= 0.45:
        band = "REVIEW_REUSE"
    return {
        "logic_id": logic_id,
        "reuse_probability": round(score_val, 4),
        "decision_band": band,
        "feature_contributions": {
            "semantic_similarity": round(score_val * 0.45, 4),
            "graph_overlap": round(score_val * 0.30, 4),
            "rule_compatibility": round(score_val * 0.15, 4),
            "freshness_and_ownership": round(score_val * 0.10, 4),
        },
    }


# ============================================================================
# rag_ops action handlers
# ============================================================================

def _ops_validate_output(settings, **kwargs) -> Dict[str, Any]:
    """Validate AI-generated content using Proof of Graph."""
    if not _ensure_initialized(settings):
        return {"error": "RAG engine not initialized"}

    try:
        content = kwargs.get("content")
        if not content:
            return {"error": "content is required for 'validate_output' action"}

        content_type = kwargs.get("content_type", "sql")
        query = kwargs.get("query")

        # Get context if query provided
        context = None
        if query:
            rag_query = RAGQuery(query=query, max_results=5)
            context = _retriever.retrieve(rag_query)

        # Validate
        result = _proof_of_graph.validate(
            content=content,
            content_type=content_type,
            context=context,
        )

        return {
            "valid": result.valid,
            "errors": result.errors,
            "warnings": result.warnings,
            "referenced_entities": result.referenced_entities,
            "verified_entities": result.verified_entities,
            "missing_entities": result.missing_entities,
            "suggestions": result.suggested_fixes,
        }

    except Exception as e:
        return {"error": str(e)}


def _ops_explain_lineage(settings, **kwargs) -> Dict[str, Any]:
    """Generate natural language explanation of data lineage."""
    if not _ensure_initialized(settings):
        return {"error": "RAG engine not initialized"}

    try:
        entity_name = kwargs.get("entity_name")
        if not entity_name:
            return {"error": "entity_name is required for 'explain_lineage' action"}

        direction = kwargs.get("direction", "both")
        max_depth = kwargs.get("max_depth", 3)

        lineage = _get_lineage_tracker(settings)
        if not lineage:
            return {"error": "Lineage tracker not available"}

        explanations = []

        # Search for matching nodes across all graphs
        for graph_name in lineage.list_graphs():
            graph = lineage.get_graph(graph_name)
            if not graph:
                continue

            for node in graph.nodes:
                if entity_name.lower() in node.name.lower():
                    # Get lineage
                    if direction in ("upstream", "both"):
                        upstream = lineage.get_all_upstream(graph_name, node.id)
                        if upstream:
                            path = " -> ".join(n.name for n in upstream[:max_depth])
                            explanations.append(f"Upstream: {path} -> {node.name}")

                    if direction in ("downstream", "both"):
                        downstream = lineage.get_all_downstream(graph_name, node.id)
                        if downstream:
                            path = " -> ".join(n.name for n in downstream[:max_depth])
                            explanations.append(f"Downstream: {node.name} -> {path}")

        if not explanations:
            return {
                "entity": entity_name,
                "found": False,
                "explanation": f"No lineage found for '{entity_name}'",
            }

        return {
            "entity": entity_name,
            "found": True,
            "direction": direction,
            "explanations": explanations,
        }

    except Exception as e:
        return {"error": str(e)}


def _ops_stats(settings, **kwargs) -> Dict[str, Any]:
    """Get GraphRAG engine statistics."""
    if not _ensure_initialized(settings):
        return {"error": "RAG engine not initialized"}

    try:
        # Vector store stats
        vs_stats = _vector_store.stats()

        # Embedding cache stats
        if hasattr(_embedder, 'cache'):
            cache_stats = _embedder.cache.stats()
        else:
            cache_stats = {}

        return {
            "status": "healthy",
            "config": {
                "embedding_provider": _config.embedding_provider.value if _config else "unknown",
                "embedding_model": _config.embedding_model if _config else "unknown",
                "vector_store": _config.vector_store_type.value if _config else "unknown",
                "proof_of_graph_enabled": _config.enable_proof_of_graph if _config else False,
            },
            "index": {
                "total_documents": vs_stats.total_documents,
                "by_source": vs_stats.by_source,
                "last_indexed": vs_stats.last_indexed.isoformat() if vs_stats.last_indexed else None,
            },
            "cache": cache_stats,
        }

    except Exception as e:
        return {"error": str(e)}


def _ops_graph_health(settings, **kwargs) -> Dict[str, Any]:
    """Return lightweight graph health indicators."""
    if not _ensure_initialized(settings):
        return {"error": "RAG engine not initialized"}
    try:
        vs_stats = _vector_store.stats()
        total_docs = int(vs_stats.total_documents or 0)
        source_buckets = vs_stats.by_source or {}
        orphan_estimate = 0 if total_docs == 0 else max(0, int(total_docs * 0.05))
        return {
            "status": "healthy",
            "graph_health": {
                "total_documents": total_docs,
                "source_buckets": source_buckets,
                "orphan_estimate": orphan_estimate,
                "stale_ratio_estimate": 0.0,
            },
        }
    except Exception as e:
        return {"error": str(e)}


def _ops_calibrate_reuse_model(settings, **kwargs) -> Dict[str, Any]:
    """Emit calibration metadata for reuse score governance."""
    sample_size = int(kwargs.get("sample_size", 50))
    if sample_size <= 0:
        return {"error": "sample_size must be > 0"}
    return {
        "status": "calibration_planned",
        "sample_size": sample_size,
        "steps": [
            "Collect accepted/rejected reuse recommendations",
            "Compute calibration error by decision band",
            "Adjust feature weights and thresholds",
            "Publish new model_version after validation",
        ],
    }


def _ops_runtime_status(settings, **kwargs) -> Dict[str, Any]:
    """Return current runtime status and recent runtime health logs."""
    log_limit = int(kwargs.get("log_limit", 20))
    if log_limit < 1:
        log_limit = 1
    if log_limit > 200:
        log_limit = 200

    status = _collect_runtime_status(settings)
    events = _tail_runtime_health_log(settings, limit=log_limit)
    result = {
        "status": "ok",
        "runtime": status,
        "recent_events": events,
    }
    _write_runtime_health_log(settings, "status_requested", {"log_limit": log_limit})
    return result


# ============================================================================
# Action dispatch dictionaries
# ============================================================================

_INDEX_ACTIONS = {
    "catalog": _index_catalog,
    "hierarchies": _index_hierarchies,
    "configure": _index_configure,
    "sync_relationalai": _index_sync_relationalai,
    "index_report_logic": _index_report_logic,
    "refresh_reuse_features": _index_refresh_reuse_features,
}

_SEARCH_ACTIONS = {
    "search": _search_search,
    "get_context": _search_get_context,
    "suggest_similar": _search_suggest_similar,
    "entity_extract": _search_entity_extract,
    "reuse_candidates": _search_reuse_candidates,
    "explain_reuse_score": _search_explain_reuse_score,
}

_OPS_ACTIONS = {
    "validate_output": _ops_validate_output,
    "explain_lineage": _ops_explain_lineage,
    "stats": _ops_stats,
    "graph_health": _ops_graph_health,
    "calibrate_reuse_model": _ops_calibrate_reuse_model,
    "runtime_status": _ops_runtime_status,
}


# ============================================================================
# Unified dispatch functions
# ============================================================================

def dispatch_rag_index(settings, action: str, **kwargs) -> Dict[str, Any]:
    """Dispatch a rag_index action."""
    handler = _INDEX_ACTIONS.get(action)
    if not handler:
        return {
            "error": f"Unknown action: '{action}'",
            "valid_actions": sorted(_INDEX_ACTIONS.keys()),
        }
    try:
        return handler(settings, **kwargs)
    except Exception as e:
        logger.error(f"rag_index({action}) failed: {e}")
        return {"error": f"rag_index({action}) failed: {e}"}


def dispatch_rag_search(settings, action: str, **kwargs) -> Dict[str, Any]:
    """Dispatch a rag_search action."""
    handler = _SEARCH_ACTIONS.get(action)
    if not handler:
        return {
            "error": f"Unknown action: '{action}'",
            "valid_actions": sorted(_SEARCH_ACTIONS.keys()),
        }
    try:
        return handler(settings, **kwargs)
    except Exception as e:
        logger.error(f"rag_search({action}) failed: {e}")
        return {"error": f"rag_search({action}) failed: {e}"}


def dispatch_rag_ops(settings, action: str, **kwargs) -> Dict[str, Any]:
    """Dispatch a rag_ops action."""
    handler = _OPS_ACTIONS.get(action)
    if not handler:
        return {
            "error": f"Unknown action: '{action}'",
            "valid_actions": sorted(_OPS_ACTIONS.keys()),
        }
    try:
        return handler(settings, **kwargs)
    except Exception as e:
        logger.error(f"rag_ops({action}) failed: {e}")
        return {"error": f"rag_ops({action}) failed: {e}"}


# ============================================================================
# Registration
# ============================================================================

def register_unified_graphrag_tools(mcp, settings):
    """Register the 3 unified GraphRAG MCP tools."""
    _start_bootstrap_thread(settings)
    _start_health_monitor_thread(settings)

    @mcp.tool()
    def rag_index(
        action: str,
        asset_types: Optional[str] = "TABLE,VIEW",
        force_reindex: bool = False,
        project_id: Optional[str] = None,
        embedding_provider: Optional[str] = "openai",
        embedding_model: Optional[str] = "text-embedding-3-small",
        vector_store: Optional[str] = "sqlite",
        enable_proof_of_graph: Optional[bool] = True,
        strict_validation: Optional[bool] = False,
        request_id: Optional[str] = None,
        rai_model: Optional[str] = None,
        source_nodes: Optional[str] = None,
        source_edges: Optional[str] = None,
        dry_run: Optional[bool] = True,
        logic_items: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Unified GraphRAG indexing and configuration tool. Replaces 3 individual tools.

        Actions:
        - catalog: Index catalog assets into the vector store (optional asset_types, force_reindex)
        - hierarchies: Index hierarchy structures (optional project_id, force_reindex)
        - configure: Configure RAG engine settings (optional embedding_provider, embedding_model, vector_store)

        Args:
            action: The action to perform (catalog, hierarchies, configure)
            asset_types: Comma-separated asset types to index (for catalog)
            force_reindex: If True, reindex existing items (for catalog/hierarchies)
            project_id: Optional specific project to index (for hierarchies)
            embedding_provider: Provider for embeddings (for configure: openai, huggingface, local)
            embedding_model: Model name for embeddings (for configure)
            vector_store: Vector database type (for configure: sqlite, chroma)
            enable_proof_of_graph: Enable anti-hallucination validation (for configure)
            strict_validation: Treat warnings as errors (for configure)

        Returns:
            Action-specific result dict
        """
        parsed_logic_items = None
        if logic_items:
            try:
                import json
                parsed_logic_items = json.loads(logic_items)
            except Exception:
                parsed_logic_items = None
        return dispatch_rag_index(settings, action, **{
            k: v for k, v in {
                "asset_types": asset_types, "force_reindex": force_reindex,
                "project_id": project_id, "embedding_provider": embedding_provider,
                "embedding_model": embedding_model, "vector_store": vector_store,
                "enable_proof_of_graph": enable_proof_of_graph,
                "strict_validation": strict_validation,
                "request_id": request_id, "rai_model": rai_model,
                "source_nodes": source_nodes, "source_edges": source_edges,
                "dry_run": dry_run, "logic_items": parsed_logic_items,
            }.items() if v is not None
        })

    @mcp.tool()
    def rag_search(
        action: str,
        query: Optional[str] = None,
        text: Optional[str] = None,
        max_results: int = 10,
        include_lineage: bool = True,
        include_templates: bool = True,
        format: str = "structured",
        entity_type: str = "all",
        top_k: int = 5,
        top_n: int = 5,
        logic_id: Optional[str] = None,
        reuse_probability: Optional[float] = None,
    ) -> Dict[str, Any]:
        """
        Unified GraphRAG search tool. Replaces 4 individual tools.

        Actions:
        - search: Hybrid RAG search with vector similarity, graph traversal, and keyword search (requires query)
        - get_context: Get assembled RAG context for LLM prompting (requires query)
        - suggest_similar: Find similar entities using semantic search (requires text)
        - entity_extract: Extract entities from natural language query (requires query)

        Args:
            action: The action to perform (search, get_context, suggest_similar, entity_extract)
            query: Natural language query (for search/get_context/entity_extract)
            text: Text to find similar entities for (for suggest_similar)
            max_results: Maximum results to return (for search/get_context)
            include_lineage: Include lineage graph search (for search)
            include_templates: Include template matching (for search)
            format: Output format for get_context (structured, prompt)
            entity_type: Filter by type for suggest_similar (table, hierarchy, all)
            top_k: Number of suggestions for suggest_similar

        Returns:
            Action-specific result dict
        """
        return dispatch_rag_search(settings, action, **{
            k: v for k, v in {
                "query": query, "text": text, "max_results": max_results,
                "include_lineage": include_lineage, "include_templates": include_templates,
                "format": format, "entity_type": entity_type, "top_k": top_k,
                "top_n": top_n, "logic_id": logic_id,
                "reuse_probability": reuse_probability,
            }.items() if v is not None
        })

    @mcp.tool()
    def rag_ops(
        action: str,
        content: Optional[str] = None,
        content_type: str = "sql",
        query: Optional[str] = None,
        entity_name: Optional[str] = None,
        direction: str = "both",
        max_depth: int = 3,
        sample_size: int = 50,
        log_limit: int = 20,
    ) -> Dict[str, Any]:
        """
        Unified GraphRAG operations tool. Replaces 3 individual tools.

        Actions:
        - validate_output: Validate AI-generated content using Proof of Graph (requires content)
        - explain_lineage: Generate natural language lineage explanation (requires entity_name)
        - stats: Get RAG engine statistics and health
        - runtime_status: Get startup/bootstrap/bridge status and recent runtime health log entries

        Args:
            action: The action to perform (validate_output, explain_lineage, stats)
            content: Generated content to validate (for validate_output: SQL, YAML, etc.)
            content_type: Type of content (for validate_output: sql, hierarchy, dbt, yaml)
            query: Optional original query for context (for validate_output)
            entity_name: Name of table, hierarchy, or column (for explain_lineage)
            direction: Lineage direction (for explain_lineage: upstream, downstream, both)
            max_depth: Maximum traversal depth (for explain_lineage)
            log_limit: Number of recent runtime health events to return (for runtime_status)

        Returns:
            Action-specific result dict
        """
        return dispatch_rag_ops(settings, action, **{
            k: v for k, v in {
                "content": content, "content_type": content_type, "query": query,
                "entity_name": entity_name, "direction": direction, "max_depth": max_depth,
                "sample_size": sample_size,
                "log_limit": log_limit,
            }.items() if v is not None
        })

    logger.info("Registered 3 unified GraphRAG tools: rag_index, rag_search, rag_ops")
    return ["rag_index", "rag_search", "rag_ops"]
