"""
Unified MCP tools for Cortex Agent + Analyst (Phase 3C consolidation).

Consolidates 26 individual tools into 6 action-dispatched tools:

Agent (12 → 3):
- cortex_config(action, ...) — 2 actions: configure, status
- cortex_function(function_type, ...) — 5 types: complete, summarize, sentiment, translate, extract_answer
- cortex_reason(action, ...) — 5 actions: reason, analyze_data, clean_data, get_console_log, get_conversation

Analyst (14 → 3):
- semantic_model(action, ...) — 5 actions: create, add_table, deploy, list, validate
- analyst_query(action, ...) — 3 actions: ask, ask_and_run, conversation
- generate_semantic_model(source_type, ...) — 6 types: from_hierarchy, from_schema, from_faux, bootstrap, list_templates, from_template

Original tool names are preserved as deprecation wrappers in mcp_tools.py and analyst_tools.py.
"""

import json
import logging
import re
import uuid
from pathlib import Path
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

# ============================================================================
# Lazy singletons — Agent
# ============================================================================

_cortex_client = None
_console = None
_reasoning_loop = None


def _get_query_func(settings):
    """Get the query function from connections API."""
    try:
        try:
            from src.connections_api import get_client
        except ImportError:
            from connections_api import get_client
        client = get_client(settings)

        def query_func(connection_id: str, query: str) -> List[Dict]:
            return client.execute_query(connection_id, query)
        return query_func
    except Exception as e:
        logger.warning(f"Failed to get connections API client: {e}")

        def mock_query(connection_id: str, query: str) -> List[Dict]:
            logger.info(f"Mock query on {connection_id}: {query[:100]}...")
            return [{"RESULT": '{"message": "Mock response - configure real connection"}'}]
        return mock_query


def _ensure_agent_initialized(settings) -> bool:
    """Ensure the cortex agent is initialized."""
    global _cortex_client, _console, _reasoning_loop
    from .context import get_context
    from .cortex_client import CortexClient
    from .console import CommunicationConsole, CLIOutput, FileOutput
    from .reasoning_loop import CortexReasoningLoop

    context = get_context()
    config = context.get_config()
    if not config:
        return False

    if _cortex_client is None:
        query_func = _get_query_func(settings)
        _cortex_client = CortexClient(
            connection_id=config.connection_id,
            query_func=query_func,
            default_model=config.cortex_model,
            temperature=config.temperature,
        )

    if _console is None:
        outputs = []
        if "cli" in config.console_outputs:
            outputs.append(CLIOutput(verbose=True))
        if "file" in config.console_outputs:
            log_path = Path("data/cortex_agent/console.jsonl")
            outputs.append(FileOutput(log_path))
        _console = CommunicationConsole(outputs=outputs if outputs else None)

    if _reasoning_loop is None:
        _reasoning_loop = CortexReasoningLoop(
            cortex=_cortex_client,
            console=_console,
            context=context,
            config=config,
        )

    return True


# ============================================================================
# Lazy singletons — Analyst
# ============================================================================

_analyst_client = None
_model_manager = None


def _ensure_model_manager(settings):
    global _model_manager
    if _model_manager is None:
        from .semantic_model import SemanticModelManager
        data_dir = Path(settings.data_dir) / "cortex_agent"
        _model_manager = SemanticModelManager(
            data_dir=str(data_dir),
            query_func=_get_query_func(settings),
        )
    return _model_manager


def _get_hierarchy_service():
    try:
        try:
            from src.hierarchy.service import HierarchyService
        except ImportError:
            from hierarchy.service import HierarchyService
        return HierarchyService()
    except Exception:
        return None


# ============================================================================
# cortex_config action handlers
# ============================================================================

def _config_configure(settings, **kwargs) -> Dict[str, Any]:
    global _cortex_client, _console, _reasoning_loop
    from .context import get_context
    from .types import CortexAgentConfig

    connection_id = kwargs.get("connection_id")
    if not connection_id:
        return {"error": "connection_id is required for 'configure' action"}

    _cortex_client = None
    _console = None
    _reasoning_loop = None

    outputs = [o.strip() for o in (kwargs.get("console_outputs", "cli,file") or "cli,file").split(",") if o.strip()]
    config = CortexAgentConfig(
        connection_id=connection_id,
        cortex_model=kwargs.get("cortex_model", "mistral-large"),
        max_reasoning_steps=kwargs.get("max_reasoning_steps", 10),
        temperature=kwargs.get("temperature", 0.3),
        enable_console=kwargs.get("enable_console", True),
        console_outputs=outputs,
    )
    context = get_context()
    context.configure(config)

    if _ensure_agent_initialized(settings):
        test_result = _cortex_client.test_connection()
        return {
            "status": "configured",
            "connection_id": connection_id,
            "cortex_model": config.cortex_model,
            "max_reasoning_steps": config.max_reasoning_steps,
            "temperature": config.temperature,
            "console_outputs": outputs,
            "connection_test": test_result,
        }
    return {"status": "configured", "connection_id": connection_id, "warning": "Components not fully initialized"}


def _config_status(settings, **kwargs) -> Dict[str, Any]:
    from .context import get_context

    context = get_context()
    config = context.get_config()
    status = {
        "is_configured": context.is_configured(),
        "config": config.model_dump() if config else None,
        "context_stats": context.get_stats(),
    }
    if _console:
        status["console_status"] = _console.get_status()
    if _cortex_client:
        status["cortex_calls_made"] = _cortex_client.get_call_count()
    return status


# ============================================================================
# cortex_function type handlers
# ============================================================================

def _func_complete(settings, **kwargs) -> Dict[str, Any]:
    if not _ensure_agent_initialized(settings):
        return {"error": "Cortex Agent not configured. Call cortex_config(action='configure') first."}
    prompt = kwargs.get("prompt")
    if not prompt:
        return {"error": "prompt is required for 'complete' function"}
    result = _cortex_client.complete(
        prompt=prompt, model=kwargs.get("model"),
        temperature=kwargs.get("temperature"), max_tokens=kwargs.get("max_tokens"),
    )
    return {"success": result.success, "result": result.result, "model": kwargs.get("model") or _cortex_client.default_model, "duration_ms": result.duration_ms, "error": result.error}


def _func_summarize(settings, **kwargs) -> Dict[str, Any]:
    if not _ensure_agent_initialized(settings):
        return {"error": "Cortex Agent not configured. Call cortex_config(action='configure') first."}
    text = kwargs.get("text")
    if not text:
        return {"error": "text is required for 'summarize' function"}
    result = _cortex_client.summarize(text)
    return {"success": result.success, "summary": result.result, "duration_ms": result.duration_ms, "error": result.error}


def _func_sentiment(settings, **kwargs) -> Dict[str, Any]:
    if not _ensure_agent_initialized(settings):
        return {"error": "Cortex Agent not configured. Call cortex_config(action='configure') first."}
    text = kwargs.get("text")
    if not text:
        return {"error": "text is required for 'sentiment' function"}
    result = _cortex_client.sentiment(text)
    sentiment_value = result.result if result.success else None
    interpretation = "neutral"
    if sentiment_value is not None:
        try:
            score = float(sentiment_value)
            if score > 0.3:
                interpretation = "positive"
            elif score < -0.3:
                interpretation = "negative"
        except (ValueError, TypeError):
            pass
    return {"success": result.success, "sentiment": sentiment_value, "interpretation": interpretation, "duration_ms": result.duration_ms, "error": result.error}


def _func_translate(settings, **kwargs) -> Dict[str, Any]:
    if not _ensure_agent_initialized(settings):
        return {"error": "Cortex Agent not configured. Call cortex_config(action='configure') first."}
    text = kwargs.get("text")
    from_lang = kwargs.get("from_lang")
    to_lang = kwargs.get("to_lang")
    if not all([text, from_lang, to_lang]):
        return {"error": "text, from_lang, to_lang are required for 'translate' function"}
    result = _cortex_client.translate(text, from_lang, to_lang)
    return {"success": result.success, "translation": result.result, "from_lang": from_lang, "to_lang": to_lang, "duration_ms": result.duration_ms, "error": result.error}


def _func_extract_answer(settings, **kwargs) -> Dict[str, Any]:
    if not _ensure_agent_initialized(settings):
        return {"error": "Cortex Agent not configured. Call cortex_config(action='configure') first."}
    context_text = kwargs.get("context")
    question = kwargs.get("question")
    if not context_text or not question:
        return {"error": "context and question are required for 'extract_answer' function"}
    result = _cortex_client.extract_answer(context_text, question)
    return {"success": result.success, "answer": result.result, "question": question, "duration_ms": result.duration_ms, "error": result.error}


# ============================================================================
# cortex_reason action handlers
# ============================================================================

def _reason_reason(settings, **kwargs) -> Dict[str, Any]:
    if not _ensure_agent_initialized(settings):
        return {"error": "Cortex Agent not configured. Call cortex_config(action='configure') first."}
    goal = kwargs.get("goal")
    if not goal:
        return {"error": "goal is required for 'reason' action"}
    context_dict = None
    ctx = kwargs.get("context")
    if ctx:
        try:
            context_dict = json.loads(ctx)
        except json.JSONDecodeError:
            context_dict = {"raw_context": ctx}
    response = _reasoning_loop.run_sync(goal, context_dict)
    return {
        "success": response.success, "conversation_id": response.conversation_id,
        "goal": response.goal, "result": response.result,
        "thinking_steps": [s.to_dict() for s in response.thinking_steps],
        "total_cortex_calls": response.total_cortex_calls,
        "total_duration_ms": response.total_duration_ms,
        "plan": response.plan.to_dict() if response.plan else None,
        "error": response.error,
    }


def _reason_analyze_data(settings, **kwargs) -> Dict[str, Any]:
    if not _ensure_agent_initialized(settings):
        return {"error": "Cortex Agent not configured. Call cortex_config(action='configure') first."}
    table_name = kwargs.get("table_name")
    if not table_name:
        return {"error": "table_name is required for 'analyze_data' action"}
    analysis_type = kwargs.get("analysis_type", "quality")
    context = {"table": table_name, "analysis_type": analysis_type, "sample_size": kwargs.get("sample_size", 100)}
    focus = kwargs.get("focus_columns")
    if focus:
        context["focus_columns"] = [c.strip() for c in focus.split(",")]
    goal = f"Analyze {analysis_type} of data in {table_name}"
    if focus:
        goal += f", focusing on columns: {focus}"
    response = _reasoning_loop.run_sync(goal, context)
    return {"success": response.success, "conversation_id": response.conversation_id, "table": table_name, "analysis_type": analysis_type, "insights": response.result, "thinking_steps": len(response.thinking_steps), "duration_ms": response.total_duration_ms, "error": response.error}


def _reason_clean_data(settings, **kwargs) -> Dict[str, Any]:
    if not _ensure_agent_initialized(settings):
        return {"error": "Cortex Agent not configured. Call cortex_config(action='configure') first."}
    table_name = kwargs.get("table_name")
    column_name = kwargs.get("column_name")
    cleaning_goal = kwargs.get("cleaning_goal")
    if not all([table_name, column_name, cleaning_goal]):
        return {"error": "table_name, column_name, cleaning_goal are required for 'clean_data' action"}
    preview_only = kwargs.get("preview_only", True)
    context = {"table": table_name, "column": column_name, "cleaning_goal": cleaning_goal, "preview_only": preview_only, "limit": kwargs.get("limit", 10)}
    goal = f"Clean {column_name} in {table_name}: {cleaning_goal}"
    if preview_only:
        goal += " (preview only, do not apply changes)"
    response = _reasoning_loop.run_sync(goal, context)
    return {"success": response.success, "conversation_id": response.conversation_id, "table": table_name, "column": column_name, "cleaning_goal": cleaning_goal, "preview_only": preview_only, "proposed_changes": response.result, "thinking_steps": len(response.thinking_steps), "duration_ms": response.total_duration_ms, "error": response.error}


def _reason_get_console_log(settings, **kwargs) -> Dict[str, Any]:
    from .types import MessageType
    if not _console:
        return {"entries": [], "message": "Console not initialized. Configure agent first."}
    msg_type = None
    mt = kwargs.get("message_type")
    if mt:
        try:
            msg_type = MessageType(mt.lower())
        except ValueError:
            pass
    messages = _console.get_recent(limit=kwargs.get("limit", 50), message_type=msg_type)
    conv_id = kwargs.get("conversation_id")
    if conv_id:
        messages = [m for m in messages if m.conversation_id == conv_id]
    return {"entries": [m.to_dict() for m in messages], "count": len(messages), "console_status": _console.get_status()}


def _reason_get_conversation(settings, **kwargs) -> Dict[str, Any]:
    from .context import get_context
    conversation_id = kwargs.get("conversation_id")
    if not conversation_id:
        return {"error": "conversation_id is required for 'get_conversation' action"}
    context = get_context()
    conversation = context.get_conversation(conversation_id)
    if not conversation:
        return {"error": f"Conversation not found: {conversation_id}", "available_conversations": context.list_conversations(limit=5)}
    return {"conversation": conversation.to_dict(), "scratchpad": context.get_scratchpad_context(conversation_id)}


def _reason_recommend_report_reuse(settings, **kwargs) -> Dict[str, Any]:
    """Generate a reuse recommendation by delegating to GraphRAG hybrid search."""
    request_text = kwargs.get("request_text") or kwargs.get("goal") or kwargs.get("query")
    if not request_text:
        return {"error": "request_text is required for 'recommend_report_reuse' action"}
    top_n = int(kwargs.get("top_n", 5))

    try:
        from src.graphrag.unified import dispatch_rag_search
    except Exception as exc:
        return {"error": f"GraphRAG integration unavailable: {exc}"}

    candidates_result = dispatch_rag_search(
        settings,
        "reuse_candidates",
        query=request_text,
        top_n=top_n,
    )
    if candidates_result.get("error"):
        return candidates_result

    candidates = candidates_result.get("candidates", [])
    top = candidates[0] if candidates else None
    return {
        "status": "success",
        "request_text": request_text,
        "top_recommendation": top,
        "recommendations": candidates,
        "decision": top.get("reuse_probability") >= 0.75 if top else False,
    }


# ============================================================================
# semantic_model action handlers
# ============================================================================

def _sm_create(settings, **kwargs) -> Dict[str, Any]:
    name = kwargs.get("name")
    description = kwargs.get("description")
    database = kwargs.get("database")
    schema_name = kwargs.get("schema_name")
    if not all([name, description, database, schema_name]):
        return {"error": "name, description, database, schema_name are required for 'create' action"}
    try:
        manager = _ensure_model_manager(settings)
        model = manager.create_model(name=name, description=description, database=database, schema_name=schema_name)
        return {"status": "created", "model": {"name": model.name, "description": model.description, "database": model.database, "schema": model.schema_name}, "next_steps": ["Use semantic_model(action='add_table', ...) to add tables", "Use semantic_model(action='deploy', ...) to deploy"]}
    except (ValueError, Exception) as e:
        return {"error": str(e)}


def _sm_add_table(settings, **kwargs) -> Dict[str, Any]:
    model_name = kwargs.get("model_name")
    table_name = kwargs.get("table_name")
    base_table = kwargs.get("base_table")
    if not all([model_name, table_name, base_table]):
        return {"error": "model_name, table_name, base_table are required for 'add_table' action"}
    try:
        parts = base_table.split(".")
        if len(parts) != 3:
            return {"error": "base_table must be DATABASE.SCHEMA.TABLE format"}
        manager = _ensure_model_manager(settings)
        dim_list = json.loads(kwargs["dimensions"]) if kwargs.get("dimensions") else None
        time_dim_list = json.loads(kwargs["time_dimensions"]) if kwargs.get("time_dimensions") else None
        metric_list = json.loads(kwargs["metrics"]) if kwargs.get("metrics") else None
        fact_list = json.loads(kwargs["facts"]) if kwargs.get("facts") else None
        table = manager.add_table(
            model_name=model_name, table_name=table_name, description=kwargs.get("description", ""),
            base_database=parts[0], base_schema=parts[1], base_table=parts[2],
            dimensions=dim_list, time_dimensions=time_dim_list, metrics=metric_list, facts=fact_list,
        )
        return {"status": "added", "table": {"name": table.name, "base_table": table.base_table.fully_qualified(), "dimensions": len(table.dimensions), "time_dimensions": len(table.time_dimensions), "metrics": len(table.metrics), "facts": len(table.facts)}}
    except json.JSONDecodeError as e:
        return {"error": f"Invalid JSON: {e}"}
    except (ValueError, Exception) as e:
        return {"error": str(e)}


def _sm_deploy(settings, **kwargs) -> Dict[str, Any]:
    model_name = kwargs.get("model_name")
    stage_path = kwargs.get("stage_path")
    if not model_name or not stage_path:
        return {"error": "model_name and stage_path are required for 'deploy' action"}
    try:
        manager = _ensure_model_manager(settings)
        yaml_content = manager.generate_yaml(model_name)
        result = manager.deploy_to_stage(model_name=model_name, stage_path=stage_path, connection_id=kwargs.get("connection_id"))
        result["yaml_preview"] = yaml_content[:500] + "..." if len(yaml_content) > 500 else yaml_content
        return result
    except (ValueError, Exception) as e:
        return {"error": str(e)}


def _sm_list(settings, **kwargs) -> Dict[str, Any]:
    try:
        manager = _ensure_model_manager(settings)
        models = manager.list_models()
        return {"count": len(models), "models": models}
    except Exception as e:
        return {"error": str(e)}


def _sm_validate(settings, **kwargs) -> Dict[str, Any]:
    model_name = kwargs.get("model_name")
    if not model_name:
        return {"error": "model_name is required for 'validate' action"}
    try:
        manager = _ensure_model_manager(settings)
        result = manager.validate_model(model_name=model_name, connection_id=kwargs.get("connection_id"))
        if result.get("valid"):
            try:
                yaml_content = manager.generate_yaml(model_name)
                result["yaml_preview"] = yaml_content[:300] + "..." if len(yaml_content) > 300 else yaml_content
            except Exception:
                pass
        return result
    except (ValueError, Exception) as e:
        return {"error": str(e)}


# ============================================================================
# analyst_query action handlers
# ============================================================================

def _aq_ask(settings, **kwargs) -> Dict[str, Any]:
    question = kwargs.get("question")
    semantic_model_file = kwargs.get("semantic_model_file")
    if not question or not semantic_model_file:
        return {"error": "question and semantic_model_file are required for 'ask' action"}
    return {
        "success": True, "question": question, "semantic_model": semantic_model_file,
        "sql": f"-- SQL generated for: {question}\nSELECT * FROM table LIMIT 10",
        "explanation": f"This query answers your question about: {question}",
        "suggestions": ["Show me the trend over time", "Break down by product category", "Compare to previous period"],
        "note": "Configure Cortex Agent with Snowflake connection for live queries",
    }


def _aq_ask_and_run(settings, **kwargs) -> Dict[str, Any]:
    question = kwargs.get("question")
    semantic_model_file = kwargs.get("semantic_model_file")
    connection_id = kwargs.get("connection_id")
    if not all([question, semantic_model_file, connection_id]):
        return {"error": "question, semantic_model_file, connection_id are required for 'ask_and_run' action"}
    limit = kwargs.get("limit", 100)

    analyst_response = _aq_ask(settings, question=question, semantic_model_file=semantic_model_file)
    if not analyst_response.get("success"):
        return analyst_response

    sql = analyst_response.get("sql", "")
    if not sql or sql.startswith("--"):
        return {"success": False, "question": question, "error": "No executable SQL generated. Configure Cortex Agent with Snowflake connection."}

    try:
        query_func = _get_query_func(settings)
        if query_func:
            if limit and "LIMIT" not in sql.upper():
                sql = f"{sql.rstrip(';')} LIMIT {limit}"
            rows = query_func(connection_id, sql)
            columns = list(rows[0].keys()) if rows else []
            return {"success": True, "question": question, "sql": sql, "explanation": analyst_response.get("explanation"), "results": {"columns": columns, "rows": rows[:limit], "row_count": len(rows), "truncated": len(rows) > limit}, "suggestions": analyst_response.get("suggestions", [])}
        return {"success": False, "error": "Query function not available. Check connection configuration."}
    except Exception as e:
        return {"success": False, "question": question, "sql": sql, "error": f"Query execution failed: {e}"}


def _aq_conversation(settings, **kwargs) -> Dict[str, Any]:
    question = kwargs.get("question")
    semantic_model_file = kwargs.get("semantic_model_file")
    if not question or not semantic_model_file:
        return {"error": "question and semantic_model_file are required for 'conversation' action"}
    conv_id = kwargs.get("conversation_id") or str(uuid.uuid4())
    result = _aq_ask(settings, question=question, semantic_model_file=semantic_model_file)
    result["conversation_id"] = conv_id
    result["is_followup"] = kwargs.get("conversation_id") is not None
    return result


# ============================================================================
# generate_semantic_model type handlers
# ============================================================================

def _slugify(text: str) -> str:
    slug = re.sub(r"[^a-zA-Z0-9]+", "_", text.lower())
    return re.sub(r"^_+|_+$", "", slug)[:50]


def _humanize(col_name: str) -> str:
    name = col_name.lower()
    for prefix in ("dim_", "fact_", "fk_", "pk_", "src_"):
        if name.startswith(prefix):
            name = name[len(prefix):]
    for suffix in ("_id", "_key", "_code", "_num", "_dt", "_ts"):
        if name.endswith(suffix):
            name = name[:-len(suffix)]
    return name.replace("_", " ").title()


def _generate_business_synonyms(col_name: str) -> List[str]:
    synonyms = []
    clean = col_name.lower()
    for prefix in ("dim_", "fact_", "fk_", "pk_", "src_"):
        if clean.startswith(prefix):
            clean = clean[len(prefix):]
    for suffix in ("_id", "_key", "_code", "_num", "_dt", "_ts", "_amt", "_qty"):
        if clean.endswith(suffix):
            synonyms.append(clean[:-len(suffix)].replace("_", " "))
    if "_" in clean:
        synonyms.append(clean.replace("_", " "))
    mappings = {"amt": "amount", "qty": "quantity", "dt": "date", "ts": "timestamp", "num": "number", "cnt": "count", "pct": "percentage", "desc": "description", "nm": "name", "cd": "code"}
    for abbrev, full in mappings.items():
        if abbrev in clean:
            synonyms.append(clean.replace(abbrev, full).replace("_", " "))
    return list(set(synonyms))[:4]


def _gen_from_hierarchy(settings, **kwargs) -> Dict[str, Any]:
    project_id = kwargs.get("project_id")
    if not project_id:
        return {"error": "project_id is required for 'from_hierarchy'"}
    try:
        manager = _ensure_model_manager(settings)
        hierarchy_service = _get_hierarchy_service()
        if not hierarchy_service:
            return {"error": "Hierarchy service not available", "suggestion": "Ensure hierarchy module is loaded"}
        model = manager.from_hierarchy_project(project_id=project_id, hierarchy_service=hierarchy_service, model_name=kwargs.get("model_name"))
        result = {"status": "generated", "model": {"name": model.name, "description": model.description, "tables": len(model.tables), "relationships": len(model.relationships)}, "source_project": project_id}
        if kwargs.get("deploy_to_stage"):
            result["deployment"] = manager.deploy_to_stage(model_name=model.name, stage_path=kwargs["deploy_to_stage"])
        return result
    except (ValueError, Exception) as e:
        return {"error": str(e)}


def _gen_from_schema(settings, **kwargs) -> Dict[str, Any]:
    connection_id = kwargs.get("connection_id")
    database = kwargs.get("database")
    schema_name = kwargs.get("schema_name")
    if not all([connection_id, database, schema_name]):
        return {"error": "connection_id, database, schema_name are required for 'from_schema'"}
    try:
        manager = _ensure_model_manager(settings)
        table_list = [t.strip() for t in kwargs["tables"].split(",")] if kwargs.get("tables") else None
        model = manager.from_snowflake_schema(
            connection_id=connection_id, database=database, schema_name=schema_name,
            tables=table_list, model_name=kwargs.get("model_name"),
            include_sample_values=kwargs.get("include_sample_values", False),
        )
        result = {"status": "generated", "model": {"name": model.name, "description": model.description, "tables": len(model.tables), "relationships": len(model.relationships)}, "source": {"database": database, "schema": schema_name, "tables_scanned": table_list or "all"}, "summary": {"total_dimensions": sum(len(t.dimensions) for t in model.tables), "total_time_dimensions": sum(len(t.time_dimensions) for t in model.tables), "total_facts": sum(len(t.facts) for t in model.tables), "total_metrics": sum(len(t.metrics) for t in model.tables)}}
        if kwargs.get("deploy_to_stage"):
            result["deployment"] = manager.deploy_to_stage(model_name=model.name, stage_path=kwargs["deploy_to_stage"], connection_id=connection_id)
        return result
    except (ValueError, Exception) as e:
        return {"error": str(e)}


def _gen_from_faux(settings, **kwargs) -> Dict[str, Any]:
    faux_project_id = kwargs.get("faux_project_id")
    if not faux_project_id:
        return {"error": "faux_project_id is required for 'from_faux'"}
    try:
        try:
            from src.faux_objects.service import FauxObjectsService
        except ImportError:
            from faux_objects.service import FauxObjectsService
        faux_service = FauxObjectsService()
    except ImportError:
        return {"error": "Faux Objects module not available"}

    project = faux_service.get_project(faux_project_id)
    if not project:
        return {"error": f"Faux project '{faux_project_id}' not found"}
    try:
        manager = _ensure_model_manager(settings)
        model_name = kwargs.get("model_name") or f"faux_{project.name}"
        model = manager.create_model(name=model_name, description=f"Generated from Faux project: {project.name}", database=project.target_database or "ANALYTICS", schema_name=project.target_schema or "PUBLIC")
        if project.semantic_view_definition:
            manager.add_table(model_name=model_name, table_name="semantic_view", description=project.description or "", base_database=project.target_database or "ANALYTICS", base_schema=project.target_schema or "PUBLIC", base_table=project.semantic_view_name or "SEMANTIC_VIEW")
        return {"status": "generated", "model": {"name": model.name, "tables": len(model.tables)}, "source_faux_project": faux_project_id}
    except Exception as e:
        return {"error": str(e)}


def _gen_bootstrap(settings, **kwargs) -> Dict[str, Any]:
    connection_id = kwargs.get("connection_id")
    database = kwargs.get("database")
    schema_name = kwargs.get("schema_name")
    if not all([connection_id, database, schema_name]):
        return {"error": "connection_id, database, schema_name are required for 'bootstrap'"}
    try:
        manager = _ensure_model_manager(settings)
        query_func = _get_query_func(settings)
        if not query_func:
            return {"error": "Query function not available. Check connection configuration."}

        model_name = kwargs.get("model_name") or f"{database}_{schema_name}".lower()
        profile_data = kwargs.get("profile_data", True)
        use_cortex_ai = kwargs.get("use_cortex_ai", True)
        table_list = [t.strip() for t in kwargs["tables"].split(",")] if kwargs.get("tables") else None

        if not table_list:
            tables_query = f"SELECT TABLE_NAME FROM {database}.INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '{schema_name}' AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME"
            try:
                result = query_func(connection_id, tables_query)
                table_list = [row.get("TABLE_NAME", row.get("table_name", "")) for row in result]
            except Exception as e:
                return {"error": f"Failed to list tables: {e}"}

        if not table_list:
            return {"error": f"No tables found in {database}.{schema_name}"}

        try:
            model = manager.create_model(name=model_name, description=f"AI-bootstrapped from {database}.{schema_name}", database=database, schema_name=schema_name)
        except ValueError:
            manager.delete_model(model_name)
            model = manager.create_model(name=model_name, description=f"AI-bootstrapped from {database}.{schema_name}", database=database, schema_name=schema_name)

        profiling_results = {}
        for tbl in table_list:
            columns_query = f"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE FROM {database}.INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '{schema_name}' AND TABLE_NAME = '{tbl}' ORDER BY ORDINAL_POSITION"
            try:
                columns = query_func(connection_id, columns_query)
            except Exception:
                continue

            dimensions, time_dimensions, metrics, facts = [], [], [], []
            primary_key = None
            table_profile = {}

            for col in columns:
                col_name = col.get("COLUMN_NAME", col.get("column_name", ""))
                data_type = col.get("DATA_TYPE", col.get("data_type", "")).upper()
                if col_name.startswith("_") or col_name.upper() in ("METADATA$", "ROW_NUMBER"):
                    continue

                col_profile = {}
                if profile_data:
                    try:
                        pq = f"SELECT COUNT(*) as total_rows, COUNT(DISTINCT {col_name}) as distinct_count, COUNT(*) - COUNT({col_name}) as null_count FROM {database}.{schema_name}.{tbl}"
                        pr = query_func(connection_id, pq)
                        if pr:
                            p = pr[0]
                            total = p.get("TOTAL_ROWS", p.get("total_rows", 0))
                            distinct = p.get("DISTINCT_COUNT", p.get("distinct_count", 0))
                            nulls = p.get("NULL_COUNT", p.get("null_count", 0))
                            col_profile = {"total_rows": total, "distinct_count": distinct, "null_count": nulls, "cardinality_ratio": round(distinct / total, 4) if total > 0 else 0, "null_percentage": round(nulls / total * 100, 2) if total > 0 else 0}
                            table_profile[col_name] = col_profile
                    except Exception:
                        pass

                synonyms = _generate_business_synonyms(col_name)

                if data_type in ("DATE", "DATETIME", "TIMESTAMP", "TIMESTAMP_NTZ", "TIMESTAMP_LTZ", "TIMESTAMP_TZ"):
                    time_dimensions.append({"name": _slugify(col_name), "synonyms": synonyms, "description": f"Date/time: {_humanize(col_name)}", "expr": col_name, "data_type": "DATE" if data_type == "DATE" else "TIMESTAMP"})
                elif data_type in ("VARCHAR", "TEXT", "STRING", "CHAR", "NVARCHAR"):
                    is_high_card = col_profile.get("cardinality_ratio", 0) > 0.9
                    sample_values = []
                    if profile_data and not is_high_card:
                        try:
                            samples = query_func(connection_id, f"SELECT DISTINCT {col_name} FROM {database}.{schema_name}.{tbl} WHERE {col_name} IS NOT NULL LIMIT 5")
                            sample_values = [str(s.get(col_name, s.get(col_name.lower(), ""))) for s in samples]
                        except Exception:
                            pass
                    dimensions.append({"name": _slugify(col_name), "synonyms": synonyms, "description": f"Dimension: {_humanize(col_name)}", "expr": col_name, "data_type": "VARCHAR", "unique": is_high_card, "sample_values": sample_values[:5] if sample_values else []})
                elif data_type in ("NUMBER", "NUMERIC", "DECIMAL", "FLOAT", "DOUBLE", "REAL", "INTEGER", "INT", "BIGINT", "SMALLINT"):
                    if col_name.upper().endswith("_ID") or col_name.upper() == "ID" or col_name.upper().endswith("_KEY"):
                        if col_name.upper() == "ID" and not primary_key:
                            primary_key = col_name
                        dimensions.append({"name": _slugify(col_name), "description": f"ID/Key: {_humanize(col_name)}", "expr": col_name, "data_type": "NUMBER", "unique": col_name.upper() == "ID"})
                    else:
                        facts.append({"name": _slugify(col_name), "synonyms": synonyms, "description": f"Measure: {_humanize(col_name)}", "expr": col_name, "data_type": "NUMBER"})
                        base_name = _slugify(col_name)
                        metrics.append({"name": f"total_{base_name}", "synonyms": [f"sum of {_humanize(col_name)}", f"total {_humanize(col_name)}"], "description": f"Sum of {_humanize(col_name)}", "expr": f"SUM({col_name})", "data_type": "NUMBER"})
                        if not col_name.upper().endswith("_COUNT") and not col_name.upper().startswith("NUM_"):
                            metrics.append({"name": f"avg_{base_name}", "synonyms": [f"average {_humanize(col_name)}"], "description": f"Average {_humanize(col_name)}", "expr": f"AVG({col_name})", "data_type": "NUMBER"})
                elif data_type in ("BOOLEAN", "BOOL"):
                    dimensions.append({"name": _slugify(col_name), "description": f"Flag: {_humanize(col_name)}", "expr": col_name, "data_type": "BOOLEAN"})

            if table_profile:
                profiling_results[tbl] = table_profile
            if dimensions or time_dimensions or facts or metrics:
                try:
                    manager.add_table(model_name=model_name, table_name=_slugify(tbl), description=f"Table: {tbl}", base_database=database, base_schema=schema_name, base_table=tbl, dimensions=dimensions, time_dimensions=time_dimensions, metrics=metrics, facts=facts, primary_key=primary_key)
                except ValueError:
                    pass

        # FK detection
        try:
            fk_query = f"SELECT FK.TABLE_NAME as FK_TABLE, FK.COLUMN_NAME as FK_COLUMN, PK.TABLE_NAME as PK_TABLE, PK.COLUMN_NAME as PK_COLUMN FROM {database}.INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC JOIN {database}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE FK ON RC.CONSTRAINT_NAME = FK.CONSTRAINT_NAME AND RC.CONSTRAINT_SCHEMA = FK.CONSTRAINT_SCHEMA JOIN {database}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE PK ON RC.UNIQUE_CONSTRAINT_NAME = PK.CONSTRAINT_NAME AND RC.UNIQUE_CONSTRAINT_SCHEMA = PK.CONSTRAINT_SCHEMA WHERE FK.TABLE_SCHEMA = '{schema_name}'"
            fk_results = query_func(connection_id, fk_query)
            for fk in fk_results:
                fk_table = _slugify(fk.get("FK_TABLE", fk.get("fk_table", "")))
                pk_table = _slugify(fk.get("PK_TABLE", fk.get("pk_table", "")))
                fk_col = fk.get("FK_COLUMN", fk.get("fk_column", ""))
                pk_col = fk.get("PK_COLUMN", fk.get("pk_column", ""))
                if fk_table and pk_table:
                    try:
                        manager.add_relationship(model_name=model_name, left_table=fk_table, right_table=pk_table, columns=[{"left_column": fk_col, "right_column": pk_col}], join_type="left_outer", relationship_type="many_to_one")
                    except Exception:
                        pass
        except Exception:
            pass

        final_model = manager.get_model(model_name)
        result = {"status": "bootstrapped", "ai_enhanced": use_cortex_ai, "data_profiled": profile_data, "model": {"name": final_model.name, "description": final_model.description, "tables": len(final_model.tables), "relationships": len(final_model.relationships)}, "summary": {"total_dimensions": sum(len(t.dimensions) for t in final_model.tables), "total_time_dimensions": sum(len(t.time_dimensions) for t in final_model.tables), "total_facts": sum(len(t.facts) for t in final_model.tables), "total_metrics": sum(len(t.metrics) for t in final_model.tables)}, "profiling_insights": {"tables_profiled": len(profiling_results), "sample": dict(list(profiling_results.items())[:2]) if profiling_results else {}}, "next_steps": ["Use semantic_model(action='validate', ...) to check the model", "Use semantic_model(action='deploy', ...) to deploy to a Snowflake stage", "Use analyst_query(action='ask', ...) to test natural language queries"]}
        if kwargs.get("deploy_to_stage"):
            result["deployment"] = manager.deploy_to_stage(model_name=model_name, stage_path=kwargs["deploy_to_stage"], connection_id=connection_id)
        return result
    except Exception as e:
        return {"error": f"Bootstrap failed: {e}"}


def _gen_list_templates(settings, **kwargs) -> Dict[str, Any]:
    try:
        templates_dir = Path("semantic_templates")
        index_file = templates_dir / "index.json"
        if not index_file.exists():
            return {"templates": [], "message": "No templates directory found"}
        with open(index_file, "r") as f:
            index = json.load(f)
        templates = index.get("templates", [])
        domain = kwargs.get("domain")
        industry = kwargs.get("industry")
        if domain:
            templates = [t for t in templates if t.get("domain") == domain]
        if industry:
            templates = [t for t in templates if t.get("industry") == industry]
        return {"count": len(templates), "templates": templates, "filters_applied": {"domain": domain, "industry": industry}}
    except Exception as e:
        return {"error": str(e)}


def _gen_from_template(settings, **kwargs) -> Dict[str, Any]:
    template_id = kwargs.get("template_id")
    model_name = kwargs.get("model_name")
    database = kwargs.get("database")
    schema_name = kwargs.get("schema_name")
    if not all([template_id, model_name, database, schema_name]):
        return {"error": "template_id, model_name, database, schema_name are required for 'from_template'"}
    try:
        import yaml
        templates_dir = Path("semantic_templates")
        index_file = templates_dir / "index.json"
        if not index_file.exists():
            return {"error": "Templates directory not found"}
        with open(index_file, "r") as f:
            index = json.load(f)
        template_info = None
        for t in index.get("templates", []):
            if t.get("id") == template_id:
                template_info = t
                break
        if not template_info:
            return {"error": f"Template '{template_id}' not found", "available": [t["id"] for t in index.get("templates", [])]}
        template_file = templates_dir / template_info["file"]
        if not template_file.exists():
            return {"error": f"Template file not found: {template_info['file']}"}
        with open(template_file, "r") as f:
            template_content = f.read()
        template_content = template_content.replace("{{DATABASE}}", database).replace("{{SCHEMA}}", schema_name)
        template_data = yaml.safe_load(template_content)
        manager = _ensure_model_manager(settings)
        model = manager.create_model(name=model_name, description=template_data.get("description", template_info.get("description", "")), database=database, schema_name=schema_name)
        for table_data in template_data.get("tables", []):
            base_table = table_data.get("base_table", {})
            manager.add_table(model_name=model_name, table_name=table_data["name"], description=table_data.get("description", ""), base_database=base_table.get("database", database), base_schema=base_table.get("schema", schema_name), base_table=base_table.get("table", table_data["name"].upper()), dimensions=table_data.get("dimensions", []), time_dimensions=table_data.get("time_dimensions", []), metrics=table_data.get("metrics", []), facts=table_data.get("facts", []))
        for rel_data in template_data.get("relationships", []):
            try:
                manager.add_relationship(model_name=model_name, left_table=rel_data["left_table"], right_table=rel_data["right_table"], columns=rel_data.get("columns", []), join_type=rel_data.get("join_type", "left_outer"), relationship_type=rel_data.get("relationship_type", "many_to_one"))
            except ValueError:
                pass
        result = {"status": "created", "template_id": template_id, "model": {"name": model_name, "tables": len(model.tables), "relationships": len(model.relationships)}, "customization": {"database": database, "schema": schema_name}}
        if kwargs.get("deploy_to_stage"):
            result["deployment"] = manager.deploy_to_stage(model_name=model_name, stage_path=kwargs["deploy_to_stage"])
        return result
    except Exception as e:
        return {"error": str(e)}


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

_CONFIG_ACTIONS = {
    "configure": _config_configure,
    "status": _config_status,
}

_FUNCTION_TYPES = {
    "complete": _func_complete,
    "summarize": _func_summarize,
    "sentiment": _func_sentiment,
    "translate": _func_translate,
    "extract_answer": _func_extract_answer,
}

_REASON_ACTIONS = {
    "reason": _reason_reason,
    "analyze_data": _reason_analyze_data,
    "clean_data": _reason_clean_data,
    "get_console_log": _reason_get_console_log,
    "get_conversation": _reason_get_conversation,
    "recommend_report_reuse": _reason_recommend_report_reuse,
}

_SEMANTIC_MODEL_ACTIONS = {
    "create": _sm_create,
    "add_table": _sm_add_table,
    "deploy": _sm_deploy,
    "list": _sm_list,
    "validate": _sm_validate,
}

_ANALYST_QUERY_ACTIONS = {
    "ask": _aq_ask,
    "ask_and_run": _aq_ask_and_run,
    "conversation": _aq_conversation,
}

_SEMANTIC_GEN_TYPES = {
    "from_hierarchy": _gen_from_hierarchy,
    "from_schema": _gen_from_schema,
    "from_faux": _gen_from_faux,
    "bootstrap": _gen_bootstrap,
    "list_templates": _gen_list_templates,
    "from_template": _gen_from_template,
}


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

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


def dispatch_cortex_function(settings, function_type: str, **kwargs) -> Dict[str, Any]:
    handler = _FUNCTION_TYPES.get(function_type)
    if not handler:
        return {"error": f"Unknown function_type: '{function_type}'", "valid_types": sorted(_FUNCTION_TYPES.keys())}
    try:
        return handler(settings, **kwargs)
    except Exception as e:
        logger.error(f"cortex_function({function_type}) failed: {e}")
        return {"error": f"cortex_function({function_type}) failed: {e}"}


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


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


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


def dispatch_generate_semantic_model(settings, source_type: str, **kwargs) -> Dict[str, Any]:
    handler = _SEMANTIC_GEN_TYPES.get(source_type)
    if not handler:
        return {"error": f"Unknown source_type: '{source_type}'", "valid_types": sorted(_SEMANTIC_GEN_TYPES.keys())}
    try:
        return handler(settings, **kwargs)
    except Exception as e:
        logger.error(f"generate_semantic_model({source_type}) failed: {e}")
        return {"error": f"generate_semantic_model({source_type}) failed: {e}"}


# ============================================================================
# Registration — Agent (3 unified tools)
# ============================================================================

def register_unified_cortex_agent_tools(mcp, settings):
    """Register 3 unified Cortex Agent MCP tools."""

    @mcp.tool()
    def cortex_config(
        action: str,
        connection_id: Optional[str] = None,
        cortex_model: Optional[str] = "mistral-large",
        max_reasoning_steps: Optional[int] = 10,
        temperature: Optional[float] = 0.3,
        enable_console: Optional[bool] = True,
        console_outputs: Optional[str] = "cli,file",
    ) -> Dict[str, Any]:
        """
        Unified Cortex Agent configuration tool. Replaces 2 individual tools.

        Actions:
        - configure: Configure connection and model settings (requires connection_id)
        - status: Get agent status and connection info
        """
        return dispatch_cortex_config(settings, action, **{
            k: v for k, v in {"connection_id": connection_id, "cortex_model": cortex_model, "max_reasoning_steps": max_reasoning_steps, "temperature": temperature, "enable_console": enable_console, "console_outputs": console_outputs}.items() if v is not None
        })

    @mcp.tool()
    def cortex_function(
        function_type: str,
        prompt: Optional[str] = None,
        text: Optional[str] = None,
        from_lang: Optional[str] = None,
        to_lang: Optional[str] = None,
        context: Optional[str] = None,
        question: Optional[str] = None,
        model: Optional[str] = None,
        temperature: Optional[float] = None,
        max_tokens: Optional[int] = None,
    ) -> Dict[str, Any]:
        """
        Unified Cortex function tool. Replaces 5 individual tools.

        Types:
        - complete: Text generation (requires prompt)
        - summarize: Text summarization (requires text)
        - sentiment: Sentiment analysis (requires text)
        - translate: Translation (requires text, from_lang, to_lang)
        - extract_answer: QA extraction (requires context, question)
        """
        return dispatch_cortex_function(settings, function_type, **{
            k: v for k, v in {"prompt": prompt, "text": text, "from_lang": from_lang, "to_lang": to_lang, "context": context, "question": question, "model": model, "temperature": temperature, "max_tokens": max_tokens}.items() if v is not None
        })

    @mcp.tool()
    def cortex_reason(
        action: str,
        goal: Optional[str] = None,
        context: Optional[str] = None,
        query: Optional[str] = None,
        request_text: Optional[str] = None,
        table_name: Optional[str] = None,
        column_name: Optional[str] = None,
        cleaning_goal: Optional[str] = None,
        analysis_type: Optional[str] = "quality",
        sample_size: Optional[int] = 100,
        focus_columns: Optional[str] = None,
        preview_only: Optional[bool] = True,
        limit: Optional[int] = None,
        top_n: Optional[int] = 5,
        conversation_id: Optional[str] = None,
        message_type: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Unified Cortex reasoning tool. Replaces 5 individual tools.

        Actions:
        - reason: Run reasoning loop (requires goal)
        - analyze_data: AI data analysis (requires table_name)
        - clean_data: AI data cleaning (requires table_name, column_name, cleaning_goal)
        - get_console_log: Get console entries (optional limit, conversation_id, message_type)
        - get_conversation: Get full conversation (requires conversation_id)
        - recommend_report_reuse: Rank reusable report logic (requires request_text or goal/query)
        """
        return dispatch_cortex_reason(settings, action, **{
            k: v for k, v in {"goal": goal, "context": context, "query": query, "request_text": request_text, "table_name": table_name, "column_name": column_name, "cleaning_goal": cleaning_goal, "analysis_type": analysis_type, "sample_size": sample_size, "focus_columns": focus_columns, "preview_only": preview_only, "limit": limit, "top_n": top_n, "conversation_id": conversation_id, "message_type": message_type}.items() if v is not None
        })

    logger.info("Registered 3 unified Cortex Agent tools: cortex_config, cortex_function, cortex_reason")


# ============================================================================
# Registration — Analyst (3 unified tools)
# ============================================================================

def register_unified_cortex_analyst_tools(mcp, settings):
    """Register 3 unified Cortex Analyst MCP tools."""

    @mcp.tool()
    def semantic_model(
        action: str,
        name: Optional[str] = None,
        description: Optional[str] = None,
        database: Optional[str] = None,
        schema_name: Optional[str] = None,
        model_name: Optional[str] = None,
        table_name: Optional[str] = None,
        base_table: Optional[str] = None,
        dimensions: Optional[str] = None,
        time_dimensions: Optional[str] = None,
        metrics: Optional[str] = None,
        facts: Optional[str] = None,
        stage_path: Optional[str] = None,
        connection_id: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Unified semantic model management tool. Replaces 5 individual tools.

        Actions:
        - create: Create model (requires name, description, database, schema_name)
        - add_table: Add table (requires model_name, table_name, base_table)
        - deploy: Deploy to stage (requires model_name, stage_path)
        - list: List all models
        - validate: Validate model (requires model_name)
        """
        return dispatch_semantic_model(settings, action, **{
            k: v for k, v in {"name": name, "description": description, "database": database, "schema_name": schema_name, "model_name": model_name, "table_name": table_name, "base_table": base_table, "dimensions": dimensions, "time_dimensions": time_dimensions, "metrics": metrics, "facts": facts, "stage_path": stage_path, "connection_id": connection_id}.items() if v is not None
        })

    @mcp.tool()
    def analyst_query(
        action: str,
        question: Optional[str] = None,
        semantic_model_file: Optional[str] = None,
        connection_id: Optional[str] = None,
        conversation_id: Optional[str] = None,
        limit: Optional[int] = 100,
    ) -> Dict[str, Any]:
        """
        Unified analyst query tool. Replaces 3 individual tools.

        Actions:
        - ask: Ask question, get SQL (requires question, semantic_model_file)
        - ask_and_run: Ask and execute (requires question, semantic_model_file, connection_id)
        - conversation: Multi-turn chat (requires question, semantic_model_file)
        """
        return dispatch_analyst_query(settings, action, **{
            k: v for k, v in {"question": question, "semantic_model_file": semantic_model_file, "connection_id": connection_id, "conversation_id": conversation_id, "limit": limit}.items() if v is not None
        })

    @mcp.tool()
    def generate_semantic_model(
        source_type: str,
        project_id: Optional[str] = None,
        faux_project_id: Optional[str] = None,
        connection_id: Optional[str] = None,
        database: Optional[str] = None,
        schema_name: Optional[str] = None,
        tables: Optional[str] = None,
        model_name: Optional[str] = None,
        deploy_to_stage: Optional[str] = None,
        include_sample_values: Optional[bool] = False,
        profile_data: Optional[bool] = True,
        use_cortex_ai: Optional[bool] = True,
        template_id: Optional[str] = None,
        domain: Optional[str] = None,
        industry: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Unified semantic model generation tool. Replaces 6 individual tools.

        Source types:
        - from_hierarchy: Generate from hierarchy project (requires project_id)
        - from_schema: Generate from Snowflake schema (requires connection_id, database, schema_name)
        - from_faux: Generate from Faux Objects (requires faux_project_id)
        - bootstrap: AI-powered bootstrapping (requires connection_id, database, schema_name)
        - list_templates: List available templates (optional domain, industry)
        - from_template: Create from template (requires template_id, model_name, database, schema_name)
        """
        return dispatch_generate_semantic_model(settings, source_type, **{
            k: v for k, v in {"project_id": project_id, "faux_project_id": faux_project_id, "connection_id": connection_id, "database": database, "schema_name": schema_name, "tables": tables, "model_name": model_name, "deploy_to_stage": deploy_to_stage, "include_sample_values": include_sample_values, "profile_data": profile_data, "use_cortex_ai": use_cortex_ai, "template_id": template_id, "domain": domain, "industry": industry}.items() if v is not None
        })

    logger.info("Registered 3 unified Cortex Analyst tools: semantic_model, analyst_query, generate_semantic_model")
