from __future__ import annotations

from dataclasses import asdict, dataclass, field
import json
from typing import Any

from backend.config import TOOLS_DIR, _BUILTIN_TOOL_NAMES, _PROMOTED_CUSTOM_BUILTIN_NAMES, _normalize_tool_enabled_map, _normalize_tool_name


_READ_ONLY_TOOL_NAMES = {
    "web_search", "fetch_url", "extract_source", "read_file", "list_files", "read_workspace_file",
    "search_workspace", "preview_web_app", "describe_image", "outlook_get_inbox", "outlook_read_calendar", "outlook_search_emails",
    "outlook_get_thread", "outlook_get_email", "outlook_get_tasks", "gmail_get_inbox", "gmail_search_emails", "gmail_get_thread", "gmail_get_email",
    "google_read_calendar", "google_drive_list",
    "google_drive_search", "google_drive_read", "onedrive_list", "onedrive_search", "onedrive_read",
    "browser_get_text", "browser_screenshot", "browser_eval", "mcp_list_tools",
    "find_free_slots", "gmail_get_attachments", "outlook_get_attachments",
}

_SIDE_EFFECT_TOOL_NAMES = {
    "run_python", "write_file", "write_web_files", "package_web_app", "run_shell", "http_request", "create_tool", "update_tool", "schedule_goal",
    "outlook_send_email", "outlook_update_email", "outlook_delete_email",
    "outlook_reply_email", "outlook_save_draft_email",
    "outlook_create_calendar_event", "outlook_update_calendar_event", "outlook_delete_calendar_event",
    "gmail_send_email", "gmail_update_email", "gmail_delete_email",
    "gmail_reply_email", "gmail_save_draft_email",
    "google_create_calendar_event", "google_update_calendar_event", "google_delete_calendar_event",
    "slack_post_message", "whatsapp_send_message", "google_drive_upload", "onedrive_upload",
    "browser_navigate", "browser_click", "browser_type", "mcp_call",
    "gmail_manage_labels", "outlook_move_email",
    "gmail_bulk_action", "outlook_bulk_action",
}


@dataclass(slots=True)
class ToolCapability:
    name: str
    source: str
    category: str
    domains: list[str] = field(default_factory=list)
    enabled: bool = True
    available: bool = True
    side_effect: bool = False
    requires_approval: bool = False
    description: str = ""
    input_schema: dict[str, Any] = field(default_factory=dict)
    output_schema: dict[str, Any] = field(default_factory=dict)
    planner_hints: dict[str, Any] = field(default_factory=dict)
    policy: dict[str, Any] = field(default_factory=dict)
    metadata: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)


def build_runtime_tool_catalog(
    settings: dict | None = None,
    *,
    dynamic_tools: dict | None = None,
    mcp_cache: dict[str, dict] | None = None,
    use_outlook: bool = False,
    gmail_available: bool = False,
) -> dict[str, ToolCapability]:
    run_settings = dict(settings or {})
    tool_enabled_map = _normalize_tool_enabled_map(run_settings.get("tool_enabled_map"))
    approval_tools = {_normalize_tool_name(str(name).strip()) for name in (run_settings.get("policy_require_approval_tools") or []) if str(name).strip()}
    builtin_descriptions = _load_builtin_descriptions()
    catalog: dict[str, ToolCapability] = {}

    builtin_names = set(_BUILTIN_TOOL_NAMES)
    builtin_names.add("mcp_list_tools")

    for raw_name in sorted(builtin_names):
        name = _normalize_tool_name(raw_name)
        category = _infer_category(name)
        domains = _infer_domains(name)
        side_effect = _infer_side_effect(name)
        requires_approval = name in approval_tools
        catalog[name] = ToolCapability(
            name=name,
            source="builtin",
            category=category,
            domains=domains,
            enabled=bool(tool_enabled_map.get(name, True)),
            available=_infer_availability(name, run_settings, use_outlook=use_outlook, gmail_available=gmail_available),
            side_effect=side_effect,
            requires_approval=requires_approval,
            description=builtin_descriptions.get(name, ""),
            input_schema=_infer_input_schema(name),
            output_schema=_infer_output_schema(name, category=category, domains=domains, side_effect=side_effect),
            planner_hints=_infer_planner_hints(name, category=category, domains=domains, side_effect=side_effect),
            policy=_infer_policy(name, category=category, domains=domains, side_effect=side_effect, requires_approval=requires_approval),
            metadata={"promoted_custom": name in _PROMOTED_CUSTOM_BUILTIN_NAMES},
        )

    for raw_name, tool in (dynamic_tools or {}).items():
        name = _normalize_tool_name(raw_name)
        source = "builtin" if name in _PROMOTED_CUSTOM_BUILTIN_NAMES else "custom"
        existing = catalog.get(name)
        metadata = dict(existing.metadata) if existing else {}
        metadata["dynamic"] = True
        category = existing.category if existing else _infer_category(name)
        domains = list(existing.domains) if existing else _infer_domains(name)
        side_effect = existing.side_effect if existing else _infer_side_effect(name)
        requires_approval = existing.requires_approval if existing else name in approval_tools
        input_schema = dict(existing.input_schema) if existing else _dynamic_input_schema(tool)
        output_schema = dict(existing.output_schema) if existing else _infer_output_schema(name, category=category, domains=domains, side_effect=side_effect)
        planner_hints = dict(existing.planner_hints) if existing else _infer_planner_hints(name, category=category, domains=domains, side_effect=side_effect)
        policy = dict(existing.policy) if existing else _infer_policy(name, category=category, domains=domains, side_effect=side_effect, requires_approval=requires_approval)
        catalog[name] = ToolCapability(
            name=name,
            source=source,
            category=category,
            domains=domains,
            enabled=bool(tool_enabled_map.get(name, True)),
            available=True,
            side_effect=side_effect,
            requires_approval=requires_approval,
            description=str((tool or {}).get("description") or (existing.description if existing else "")).strip(),
            input_schema=input_schema,
            output_schema=output_schema,
            planner_hints=planner_hints,
            policy=policy,
            metadata=metadata,
        )

    servers = dict(run_settings.get("mcp_servers") or {})
    for server_name, server_cfg in servers.items():
        server_key = f"mcp_server::{server_name}"
        server_ok = bool((mcp_cache or {}).get(server_name, {}).get("ok"))
        catalog[server_key] = ToolCapability(
            name=server_key,
            source="mcp_server",
            category="mcp",
            domains=["mcp"],
            enabled=True,
            available=server_ok,
            side_effect=False,
            requires_approval=False,
            description=f"Configured MCP server '{server_name}'.",
            input_schema={"type": "object", "properties": {}},
            output_schema=_infer_output_schema(server_key, category="mcp", domains=["mcp"], side_effect=False),
            planner_hints={
                "use_when": [f"Need to inspect or select tools from MCP server '{server_name}'."],
                "avoid_when": ["A direct builtin or custom skill already covers the task."],
                "requires_previous_tool": [],
                "produces": ["mcp_server_connection"],
                "consumes": [],
                "preferred_followups": ["mcp_call"],
                "conflicts_with": [],
                "max_calls_per_plan": 1,
            },
            policy=_infer_policy(server_key, category="mcp", domains=["mcp"], side_effect=False, requires_approval=False),
            metadata={"server": server_name, "config": dict(server_cfg or {})},
        )
        for tool_meta in ((mcp_cache or {}).get(server_name, {}).get("tools") or []):
            tool_name = str((tool_meta or {}).get("name") or "").strip()
            if not tool_name:
                continue
            pseudo_name = f"mcp::{server_name}::{tool_name}"
            catalog[pseudo_name] = ToolCapability(
                name=pseudo_name,
                source="mcp_tool",
                category="mcp",
                domains=["mcp"],
                enabled=True,
                available=server_ok,
                side_effect=False,
                requires_approval=True,
                description=str((tool_meta or {}).get("description") or "").strip(),
                input_schema={"type": "object", "additionalProperties": True},
                output_schema=_infer_output_schema(pseudo_name, category="mcp", domains=["mcp"], side_effect=False),
                planner_hints={
                    "use_when": [f"Need a capability provided specifically by MCP server '{server_name}'."],
                    "avoid_when": ["A builtin or custom skill can satisfy the task directly."],
                    "requires_previous_tool": [],
                    "produces": ["mcp_result"],
                    "consumes": ["mcp_arguments"],
                    "preferred_followups": [],
                    "conflicts_with": [],
                    "max_calls_per_plan": 3,
                },
                policy=_infer_policy(pseudo_name, category="mcp", domains=["mcp"], side_effect=False, requires_approval=True),
                metadata={"server": server_name, "tool": tool_name},
            )

    return catalog


def _load_builtin_descriptions() -> dict[str, str]:
    builtin_file = TOOLS_DIR / "_builtin.json"
    if not builtin_file.exists():
        return {}
    try:
        payload = json.loads(builtin_file.read_text(encoding="utf-8"))
    except Exception:
        return {}
    descriptions: dict[str, str] = {}
    for item in payload:
        if not isinstance(item, dict):
            continue
        name = _normalize_tool_name(str(item.get("name") or "").strip())
        if not name:
            continue
        descriptions[name] = str(item.get("description") or "").strip()
    return descriptions


def _infer_category(name: str) -> str:
    if name.startswith("mcp"):
        return "mcp"
    if name in _READ_ONLY_TOOL_NAMES:
        return "read"
    if name in _SIDE_EFFECT_TOOL_NAMES:
        return "write"
    return "utility"


def _infer_domains(name: str) -> list[str]:
    if name.startswith(("outlook_", "gmail_")):
        return ["email"]
    if name.startswith(("google_read_calendar", "google_create_calendar_event", "google_update_calendar_event", "google_delete_calendar_event")):
        return ["calendar"]
    if "calendar" in name:
        return ["calendar"]
    if any(token in name for token in ("drive", "onedrive", "workspace", "search", "fetch", "browser", "preview_web_app", "extract_source")):
        return ["search"]
    if name.startswith("mcp"):
        return ["mcp"]
    return ["general"]


def _infer_side_effect(name: str) -> bool:
    if name in _READ_ONLY_TOOL_NAMES:
        return False
    if name in _SIDE_EFFECT_TOOL_NAMES:
        return True
    return any(token in name for token in ("send", "create", "update", "delete", "upload", "post", "click", "type", "write", "print"))


def _infer_availability(name: str, settings: dict[str, Any], *, use_outlook: bool, gmail_available: bool) -> bool:
    if name.startswith("outlook_"):
        return bool(use_outlook)
    if name.startswith(("gmail_", "google_", "google_drive_")):
        return bool(gmail_available or settings.get("google_enabled", False))
    if name.startswith("onedrive_"):
        return bool(settings.get("microsoft_enabled", True))
    if name == "slack_post_message":
        return bool(settings.get("slack_enabled", False))
    if name == "whatsapp_send_message":
        return bool(settings.get("whatsapp_enabled", False))
    if name.startswith("mcp"):
        return True
    return True


def _dynamic_input_schema(tool: dict | None) -> dict[str, Any]:
    args_desc = str((tool or {}).get("args_desc") or "").strip()
    schema: dict[str, Any] = {"type": "object", "additionalProperties": True}
    if args_desc:
        schema["description"] = args_desc
    return schema


def _infer_input_schema(name: str) -> dict[str, Any]:
    schema_index = _builtin_input_schema_index()
    schema = schema_index.get(name)
    if schema:
        return dict(schema)
    return {"type": "object", "additionalProperties": True}


def _infer_output_schema(name: str, *, category: str, domains: list[str], side_effect: bool) -> dict[str, Any]:
    resource_type = _infer_primary_resource(name, domains)
    result: dict[str, Any] = {
        "type": "object",
        "properties": {
            "status": {"type": "string", "enum": ["ok", "error"]},
            "message": {"type": "string"},
            "records": {"type": "array", "description": f"Structured {resource_type} records."},
            "artifacts": {"type": "array", "description": "Generated files, attachments, screenshots, or saved outputs."},
            "metrics": {"type": "object", "additionalProperties": True},
        },
    }
    if side_effect or category == "write":
        result["properties"]["receipt"] = {"type": "object", "additionalProperties": True}
    return result


def _infer_planner_hints(name: str, *, category: str, domains: list[str], side_effect: bool) -> dict[str, Any]:
    primary_domain = domains[0] if domains else "general"
    produces = _infer_produced_resources(name, primary_domain, category)
    consumes = _infer_consumed_resources(name)
    preferred_followups = _infer_preferred_followups(name)
    conflicts_with = _infer_conflicts(name)
    use_when = [_default_use_when(name, primary_domain, category, side_effect)]
    avoid_when = _default_avoid_when(name, primary_domain, category)
    requires_previous_tool = _infer_required_prerequisites(consumes)
    return {
        "use_when": use_when,
        "avoid_when": avoid_when,
        "requires_previous_tool": requires_previous_tool,
        "produces": produces,
        "consumes": consumes,
        "preferred_followups": preferred_followups,
        "conflicts_with": conflicts_with,
        "max_calls_per_plan": 1 if side_effect else 2,
    }


def _infer_policy(name: str, *, category: str, domains: list[str], side_effect: bool, requires_approval: bool) -> dict[str, Any]:
    return {
        "approval_mode": "user" if requires_approval else "none",
        "data_access": _infer_data_access(name, domains, category),
        "destructive": _is_destructive_tool(name),
        "idempotent": _is_idempotent_tool(name, side_effect),
    }


def _builtin_input_schema_index() -> dict[str, dict[str, Any]]:
    schemas: dict[str, dict[str, Any]] = {}
    for schema_group in (
        _OLLAMA_TOOL_SCHEMAS,
        _OUTLOOK_TOOL_SCHEMAS,
        _GMAIL_TOOL_SCHEMAS,
        _DRIVE_TOOL_SCHEMAS,
        _ONEDRIVE_TOOL_SCHEMAS,
        _BROWSER_TOOL_SCHEMAS,
    ):
        for schema in schema_group:
            fn = dict(schema.get("function") or {})
            name = _normalize_tool_name(str(fn.get("name") or "").strip())
            params = fn.get("parameters")
            if name and isinstance(params, dict):
                schemas[name] = params
    for name, schema in _MESSAGING_TOOL_SCHEMAS.items():
        params = ((schema or {}).get("function") or {}).get("parameters")
        normalized = _normalize_tool_name(name)
        if normalized and isinstance(params, dict):
            schemas[normalized] = params
    return schemas


def _infer_primary_resource(name: str, domains: list[str]) -> str:
    if name.startswith(("outlook_", "gmail_")):
        return "email"
    if "calendar" in name:
        return "calendar"
    if any(token in name for token in ("drive", "onedrive", "file", "workspace")):
        return "file"
    if name.startswith("browser_"):
        return "browser"
    if domains:
        return domains[0]
    return "general"


def _infer_produced_resources(name: str, domain: str, category: str) -> list[str]:
    if name.endswith(("_search_emails", "_get_inbox")):
        return ["email_records", "message_ids"]
    if name.endswith("_get_email"):
        return ["email_body", "attachments"]
    if name.endswith("_read_calendar"):
        return ["calendar_records", "event_ids"]
    if any(token in name for token in ("upload", "write_file")):
        return ["artifacts"]
    if name.startswith("browser_"):
        return ["browser_state"]
    if category == "write":
        return ["mutation_receipt"]
    if domain == "search":
        return ["search_results"]
    return [f"{domain}_result"]


def _infer_consumed_resources(name: str) -> list[str]:
    schema = _infer_input_schema(name)
    properties = dict(schema.get("properties") or {})
    consumes: list[str] = []
    if "message_id" in properties:
        consumes.append("message_id")
    if "event_id" in properties:
        consumes.append("event_id")
    if "file_id" in properties:
        consumes.append("file_id")
    if "query" in properties or "source" in properties or "path" in properties:
        consumes.append("lookup_input")
    return consumes


def _infer_preferred_followups(name: str) -> list[str]:
    if name.endswith(("_search_emails", "_get_inbox")):
        prefix = name.split("_", 1)[0]
        return [f"{prefix}_get_email"]
    if name.endswith("_get_email"):
        prefix = name.split("_", 1)[0]
        return ["analyze_attachments", f"{prefix}_reply_email"]
    if name.endswith("_read_calendar"):
        prefix = "google" if name.startswith("google_") else "outlook"
        return [f"{prefix}_update_calendar_event", f"{prefix}_delete_calendar_event"]
    return []


def _infer_conflicts(name: str) -> list[str]:
    if name.endswith("_search_emails"):
        prefix = name.split("_", 1)[0]
        return [f"{prefix}_get_inbox"]
    if name.endswith("_get_inbox"):
        prefix = name.split("_", 1)[0]
        return [f"{prefix}_search_emails"]
    return []


def _default_use_when(name: str, domain: str, category: str, side_effect: bool) -> str:
    if name.endswith("_search_emails"):
        return "Need to search email by topic, subject, sender, or keyword."
    if name.endswith("_get_inbox"):
        return "Need a recent inbox or folder listing without topic filtering."
    if name.endswith("_get_email"):
        return "Need the full body or attachment details of a specific email."
    if name.endswith("_read_calendar"):
        return "Need to inspect existing calendar events before answering or mutating them."
    if side_effect:
        return f"Need to perform a {domain} action that changes state."
    if category == "read":
        return f"Need live {domain} data before answering."
    return "Need this capability to progress the plan."


def _default_avoid_when(name: str, domain: str, category: str) -> list[str]:
    if name.endswith("_get_inbox"):
        return ["Avoid when the user asked for a topic or keyword search."]
    if name.endswith("_search_emails"):
        return ["Avoid when the user only wants a generic inbox listing."]
    if name.endswith(("_update_email", "_delete_email", "_reply_email", "_update_calendar_event", "_delete_calendar_event")):
        return ["Avoid when the target item is still ambiguous and no exact id is available."]
    if category == "write":
        return [f"Avoid when the requested {domain} action is still ambiguous."]
    return ["Avoid when the answer can already be given from structured context."]


def _infer_required_prerequisites(consumes: list[str]) -> list[str]:
    prerequisites: list[str] = []
    if "message_id" in consumes:
        prerequisites.append("message_lookup")
    if "event_id" in consumes:
        prerequisites.append("calendar_lookup")
    if "file_id" in consumes:
        prerequisites.append("file_lookup")
    return prerequisites


def _infer_data_access(name: str, domains: list[str], category: str) -> list[str]:
    accesses: list[str] = []
    if any(domain == "email" for domain in domains):
        accesses.append("mailbox_write" if category == "write" else "mailbox_read")
    if any(domain == "calendar" for domain in domains):
        accesses.append("calendar_write" if category == "write" else "calendar_read")
    if any(domain == "search" for domain in domains):
        accesses.append("search_write" if category == "write" else "search_read")
    if name.startswith("browser_"):
        accesses.append("browser_control")
    if name.startswith("mcp"):
        accesses.append("mcp_access")
    if not accesses:
        accesses.append("general")
    return accesses


def _is_destructive_tool(name: str) -> bool:
    return any(token in name for token in ("delete", "bulk_action", "move_email"))


def _is_idempotent_tool(name: str, side_effect: bool) -> bool:
    if not side_effect:
        return True
    if _is_destructive_tool(name):
        return False
    return any(token in name for token in ("update", "read", "search", "get", "list"))


# ── Ollama-native tool schemas (JSON function-calling format) ──────────────────

# Schemas mirror the tool signatures from _build_tools_desc() in main.py.
# Only include the top-level builtin tools; dynamic/custom/MCP tools stay
# text-prompt only because their schemas aren't known at compile time.

_OLLAMA_TOOL_SCHEMAS: list[dict] = [
    {"type": "function", "function": {"name": "web_search", "description": "Search the web for information.", "parameters": {"type": "object", "required": ["query"], "properties": {"query": {"type": "string", "description": "Search query"}, "count": {"type": "integer", "description": "Max results (default 20)"}}}}},
    {"type": "function", "function": {"name": "fetch_url", "description": "Download and extract text from a webpage.", "parameters": {"type": "object", "required": ["url"], "properties": {"url": {"type": "string", "description": "URL to fetch"}}}}},
    {"type": "function", "function": {"name": "extract_source", "description": "Extract text or transcripts from a URL or local file. Supports webpages, PDFs, YouTube, podcasts.", "parameters": {"type": "object", "required": ["source"], "properties": {"source": {"type": "string", "description": "URL or local file path"}, "max_chars": {"type": "integer", "description": "Max characters to extract (default 24000)"}}}}},
    {"type": "function", "function": {"name": "run_python", "description": "Execute Python code; returns stdout/stderr. win32com.client (pywin32) is available for Outlook/COM.", "parameters": {"type": "object", "required": ["code"], "properties": {"code": {"type": "string", "description": "Python code to execute"}}}}},
    {"type": "function", "function": {"name": "run_shell", "description": "Run a shell command (git, npm, pip, curl, etc.).", "parameters": {"type": "object", "required": ["command"], "properties": {"command": {"type": "string", "description": "Shell command to run"}, "timeout": {"type": "integer", "description": "Timeout in seconds (default 30)"}}}}},
    {"type": "function", "function": {"name": "http_request", "description": "Make an HTTP request to any API.", "parameters": {"type": "object", "required": ["method", "url"], "properties": {"method": {"type": "string", "description": "HTTP method"}, "url": {"type": "string", "description": "Request URL"}, "headers": {"type": "object", "description": "Request headers"}, "body": {"type": "string", "description": "Request body"}, "timeout": {"type": "integer", "description": "Timeout in seconds"}}}}},
    {"type": "function", "function": {"name": "fetch_webhook", "description": "Fetch data from a pre-configured webhook by name. Use this to query named data sources configured in Settings > Integrations > Webhooks.", "parameters": {"type": "object", "required": ["webhook_name"], "properties": {"webhook_name": {"type": "string", "description": "Name of the webhook to call (as configured in settings)"}, "params": {"type": "object", "description": "Query parameters (GET) or JSON body (POST) to send"}}}}},
    {"type": "function", "function": {"name": "write_file", "description": "Save a file to output/.", "parameters": {"type": "object", "required": ["filename", "content"], "properties": {"filename": {"type": "string", "description": "Filename"}, "content": {"type": "string", "description": "File content"}}}}},
    {"type": "function", "function": {"name": "read_file", "description": "Read a file from output/.", "parameters": {"type": "object", "required": ["filename"], "properties": {"filename": {"type": "string", "description": "Filename to read"}}}}},
    {"type": "function", "function": {"name": "list_files", "description": "List files currently in output/ (up to count, default 250).", "parameters": {"type": "object", "properties": {"count": {"type": "integer", "description": "Max files to list (default 250)"}}}}},
    {"type": "function", "function": {"name": "read_workspace_file", "description": "Read any file in the project workspace by relative path. Tolerates case differences and suggests near-matches if not found.", "parameters": {"type": "object", "required": ["path"], "properties": {"path": {"type": "string", "description": "Relative file path (e.g. 'backend/main.py')"}}}}},
    {"type": "function", "function": {"name": "search_workspace", "description": "Search the workspace. By default the pattern is matched as a case-insensitive substring; if it looks like a path or filename (contains '/', '\\' or a file extension) it is matched against filenames. Set regex=true for regex content search, or mode='filename'/'content' to force the search type.", "parameters": {"type": "object", "required": ["pattern"], "properties": {"pattern": {"type": "string", "description": "Filename, path, or substring to search for"}, "glob": {"type": "string", "description": "File glob filter for content search (default '**/*')"}, "max_results": {"type": "integer", "description": "Max results (default 250)"}, "regex": {"type": "boolean", "description": "Treat pattern as regex (content mode only)"}, "mode": {"type": "string", "description": "'auto' (default), 'filename', or 'content'"}}}}},
    {"type": "function", "function": {"name": "recall_memory", "description": "Recall full details of a past session from deep memory.", "parameters": {"type": "object", "properties": {"session_id": {"type": "string", "description": "Session ID to recall"}, "query": {"type": "string", "description": "Search query for memory"}}}}},
    {"type": "function", "function": {"name": "describe_image", "description": "Describe what is visible in an image (visual content, objects, scene, text on the image, colours). Use this whenever the user asks WHAT IS ON / WHAT DOES the image SHOW, or for any image understanding beyond raw metadata. Accepts a URL, an absolute path, a workspace-relative path, or a filename inside output/, attachments/ or downloads/. Requires a vision-capable model (Gemini, OpenAI, NVIDIA, or a local Ollama vision model).", "parameters": {"type": "object", "required": ["source"], "properties": {"source": {"type": "string", "description": "Image URL, file path, or filename"}, "prompt": {"type": "string", "description": "Optional question or focus for the description (default: full general description)"}, "model": {"type": "string", "description": "Optional explicit vision model id (e.g. 'gemini/gemini-2.5-flash'). Auto-picked when omitted."}}}}},
    {"type": "function", "function": {"name": "create_skill", "description": "Write and permanently register a new reusable skill. Code must contain: def run(args: dict) -> str", "parameters": {"type": "object", "required": ["name", "description", "args_schema", "code"], "properties": {"name": {"type": "string"}, "description": {"type": "string"}, "args_schema": {"type": "string"}, "code": {"type": "string"}, "overwrite": {"type": "boolean"}}}}},
    {"type": "function", "function": {"name": "update_skill", "description": "Update an existing reusable skill. Name and code are required; description/args_schema are optional and keep existing values when omitted.", "parameters": {"type": "object", "required": ["name", "code"], "properties": {"name": {"type": "string"}, "description": {"type": "string"}, "args_schema": {"type": "string"}, "code": {"type": "string"}, "approved": {"type": "boolean"}}}}},
    {"type": "function", "function": {"name": "schedule_goal", "description": "Schedule a goal to be executed as a future agent run.", "parameters": {"type": "object", "required": ["goal"], "properties": {"goal": {"type": "string", "description": "Goal to execute"}, "delay_minutes": {"type": "integer", "description": "Delay in minutes (default 5)"}}}}},
]

_OUTLOOK_TOOL_SCHEMAS: list[dict] = [
    {"type": "function", "function": {"name": "outlook_get_inbox", "description": "List recent emails from Outlook (no keyword filtering). Use only for listing inbox contents or recent emails. For topic/keyword searches use outlook_search_emails instead.", "parameters": {"type": "object", "properties": {"count": {"type": "integer", "description": "Max emails (default 250)"}, "unread_only": {"type": "boolean"}, "folder": {"type": "string"}, "mailbox": {"type": "string"}, "today_only": {"type": "boolean"}, "since": {"type": "string", "description": "YYYY-MM-DD"}, "until": {"type": "string", "description": "YYYY-MM-DD"}}}}},
    {"type": "function", "function": {"name": "outlook_send_email", "description": "Send an email via local Outlook.", "parameters": {"type": "object", "required": ["to", "subject", "body"], "properties": {"to": {"type": "string"}, "subject": {"type": "string"}, "body": {"type": "string"}, "cc": {"type": "string"}, "attachments": {"type": "string", "description": "Comma-separated file paths to attach (from output/, attachments/, or downloads/ folders)"}}}}},
    {"type": "function", "function": {"name": "outlook_update_email", "description": "Update an Outlook draft email by Message ID.", "parameters": {"type": "object", "required": ["message_id"], "properties": {"message_id": {"type": "string"}, "to": {"type": "string"}, "subject": {"type": "string"}, "body": {"type": "string"}, "cc": {"type": "string"}}}}},
    {"type": "function", "function": {"name": "outlook_delete_email", "description": "Delete an Outlook email by Message ID.", "parameters": {"type": "object", "required": ["message_id"], "properties": {"message_id": {"type": "string"}}}}},
    {"type": "function", "function": {"name": "outlook_read_calendar", "description": "Read upcoming calendar events from local Outlook.", "parameters": {"type": "object", "properties": {"days_ahead": {"type": "integer", "description": "Days to look ahead (default 7)"}, "since": {"type": "string", "description": "YYYY-MM-DD"}, "until": {"type": "string", "description": "YYYY-MM-DD"}}}}},
    {"type": "function", "function": {"name": "outlook_create_calendar_event", "description": "Create a new Outlook calendar event.", "parameters": {"type": "object", "required": ["summary", "start"], "properties": {"summary": {"type": "string"}, "start": {"type": "string", "description": "ISO-8601 local datetime"}, "end": {"type": "string"}, "description": {"type": "string"}, "location": {"type": "string"}, "attendees": {"type": "string", "description": "Comma-separated emails"}}}}},
    {"type": "function", "function": {"name": "outlook_update_calendar_event", "description": "Update an existing Outlook calendar event.", "parameters": {"type": "object", "required": ["event_id"], "properties": {"event_id": {"type": "string"}, "summary": {"type": "string"}, "start": {"type": "string"}, "end": {"type": "string"}, "description": {"type": "string"}, "location": {"type": "string"}}}}},
    {"type": "function", "function": {"name": "outlook_delete_calendar_event", "description": "Delete an Outlook calendar event by Event ID.", "parameters": {"type": "object", "required": ["event_id"], "properties": {"event_id": {"type": "string"}}}}},
    {"type": "function", "function": {"name": "outlook_search_emails", "description": "Search emails in Outlook by keyword. ALWAYS use this (not outlook_get_inbox) when the user asks about a topic, subject, sender, or keyword.", "parameters": {"type": "object", "required": ["query"], "properties": {"query": {"type": "string", "description": "Search keyword(s) — pass the topic or subject to search for"}, "count": {"type": "integer"}, "mailbox": {"type": "string"}, "folder": {"type": "string"}, "today_only": {"type": "boolean"}, "since": {"type": "string"}, "until": {"type": "string"}}}}},
    {"type": "function", "function": {"name": "outlook_get_email", "description": "Fetch the full content of a single Outlook email by Message ID. Use this when the user wants to read an entire email body. Set include_attachments=true when the user asks what is in an attachment or attached file. MANDATORY: call outlook_get_inbox or outlook_search_emails FIRST to obtain the exact Message ID.", "parameters": {"type": "object", "required": ["message_id"], "properties": {"message_id": {"type": "string", "description": "The Message ID from a prior inbox or search listing"}, "include_attachments": {"type": "boolean", "description": "When true, also extract and analyze supported attachments for this exact email"}, "max_attachments": {"type": "integer", "description": "Maximum number of non-inline attachments to analyze (default 4)"}}}}},
    {"type": "function", "function": {"name": "outlook_get_tasks", "description": "Fetch open tasks / to-do items from Outlook (Microsoft To-Do). Use this when the user asks about their tasks, to-do list, open action items, or wants to create follow-up reminders based on pending tasks. Do NOT use email search tools to look up tasks.", "parameters": {"type": "object", "properties": {"count": {"type": "integer", "description": "Max tasks to return (default 20)"}, "include_completed": {"type": "boolean", "description": "Include completed tasks (default false)"}}}}},
]

_GMAIL_TOOL_SCHEMAS: list[dict] = [
    {"type": "function", "function": {"name": "gmail_get_inbox", "description": "List recent emails from Gmail (no keyword filtering). For topic/keyword searches use gmail_search_emails instead.", "parameters": {"type": "object", "properties": {"count": {"type": "integer"}, "unread_only": {"type": "boolean"}, "folder": {"type": "string"}, "today_only": {"type": "boolean"}, "since": {"type": "string", "description": "YYYY-MM-DD"}, "until": {"type": "string", "description": "YYYY-MM-DD"}}}}},
    {"type": "function", "function": {"name": "gmail_send_email", "description": "Send an email via Gmail.", "parameters": {"type": "object", "required": ["to", "subject", "body"], "properties": {"to": {"type": "string"}, "subject": {"type": "string"}, "body": {"type": "string"}, "cc": {"type": "string"}, "attachments": {"type": "string", "description": "Comma-separated file paths to attach (from output/, attachments/, or downloads/ folders)"}}}}},
    {"type": "function", "function": {"name": "gmail_update_email", "description": "Update a Gmail draft email by Message ID.", "parameters": {"type": "object", "required": ["message_id"], "properties": {"message_id": {"type": "string"}, "to": {"type": "string"}, "subject": {"type": "string"}, "body": {"type": "string"}, "cc": {"type": "string"}}}}},
    {"type": "function", "function": {"name": "gmail_delete_email", "description": "Delete a Gmail email by Message ID.", "parameters": {"type": "object", "required": ["message_id"], "properties": {"message_id": {"type": "string"}}}}},
    {"type": "function", "function": {"name": "gmail_search_emails", "description": "Search emails in Gmail.", "parameters": {"type": "object", "required": ["query"], "properties": {"query": {"type": "string"}, "count": {"type": "integer"}, "folder": {"type": "string"}}}}},
    {"type": "function", "function": {"name": "gmail_get_email", "description": "Fetch the full content of a single Gmail email by Message ID. Use this when the user wants to read an entire email body. Set include_attachments=true when the user asks what is in an attachment or attached file. MANDATORY: call gmail_get_inbox or gmail_search_emails FIRST to obtain the exact Message ID.", "parameters": {"type": "object", "required": ["message_id"], "properties": {"message_id": {"type": "string", "description": "The Message ID from a prior inbox or search listing"}, "include_attachments": {"type": "boolean", "description": "When true, also extract and analyze supported attachments for this exact email"}, "max_attachments": {"type": "integer", "description": "Maximum number of non-inline attachments to analyze (default 4)"}}}}},
    {"type": "function", "function": {"name": "google_read_calendar", "description": "Read upcoming calendar events from Google Calendar.", "parameters": {"type": "object", "properties": {"days_ahead": {"type": "integer"}, "since": {"type": "string", "description": "YYYY-MM-DD"}, "until": {"type": "string", "description": "YYYY-MM-DD"}}}}},
    {"type": "function", "function": {"name": "google_create_calendar_event", "description": "Create a new Google Calendar event.", "parameters": {"type": "object", "required": ["summary", "start", "end"], "properties": {"summary": {"type": "string"}, "start": {"type": "string"}, "end": {"type": "string"}, "description": {"type": "string"}, "location": {"type": "string"}, "attendees": {"type": "string"}}}}},
    {"type": "function", "function": {"name": "google_update_calendar_event", "description": "Update an existing Google Calendar event.", "parameters": {"type": "object", "required": ["event_id"], "properties": {"event_id": {"type": "string"}, "summary": {"type": "string"}, "start": {"type": "string"}, "end": {"type": "string"}, "description": {"type": "string"}, "location": {"type": "string"}}}}},
    {"type": "function", "function": {"name": "google_delete_calendar_event", "description": "Delete a Google Calendar event.", "parameters": {"type": "object", "required": ["event_id"], "properties": {"event_id": {"type": "string"}}}}},
]

_DRIVE_TOOL_SCHEMAS: list[dict] = [
    {"type": "function", "function": {"name": "google_drive_list", "description": "List files in a Google Drive folder.", "parameters": {"type": "object", "properties": {"folder_id": {"type": "string"}, "count": {"type": "integer"}}}}},
    {"type": "function", "function": {"name": "google_drive_search", "description": "Search files in Google Drive.", "parameters": {"type": "object", "required": ["query"], "properties": {"query": {"type": "string"}, "count": {"type": "integer"}}}}},
    {"type": "function", "function": {"name": "google_drive_read", "description": "Read a file from Google Drive.", "parameters": {"type": "object", "required": ["file_id"], "properties": {"file_id": {"type": "string"}}}}},
    {"type": "function", "function": {"name": "google_drive_upload", "description": "Upload a file to Google Drive.", "parameters": {"type": "object", "required": ["name", "content"], "properties": {"name": {"type": "string"}, "content": {"type": "string"}, "folder_id": {"type": "string"}, "mime_type": {"type": "string"}}}}},
]

_ONEDRIVE_TOOL_SCHEMAS: list[dict] = [
    {"type": "function", "function": {"name": "onedrive_list", "description": "List files in a OneDrive folder.", "parameters": {"type": "object", "properties": {"folder_path": {"type": "string"}, "count": {"type": "integer"}}}}},
    {"type": "function", "function": {"name": "onedrive_search", "description": "Search files in OneDrive.", "parameters": {"type": "object", "required": ["query"], "properties": {"query": {"type": "string"}, "count": {"type": "integer"}}}}},
    {"type": "function", "function": {"name": "onedrive_read", "description": "Read a file from OneDrive.", "parameters": {"type": "object", "required": ["file_id"], "properties": {"file_id": {"type": "string"}}}}},
    {"type": "function", "function": {"name": "onedrive_upload", "description": "Upload a file to OneDrive.", "parameters": {"type": "object", "required": ["name", "content"], "properties": {"name": {"type": "string"}, "content": {"type": "string"}, "folder_path": {"type": "string"}}}}},
]

_BROWSER_TOOL_SCHEMAS: list[dict] = [
    {"type": "function", "function": {"name": "browser_navigate", "description": "Navigate the browser to a URL.", "parameters": {"type": "object", "required": ["url"], "properties": {"url": {"type": "string"}, "wait_until": {"type": "string"}}}}},
    {"type": "function", "function": {"name": "browser_click", "description": "Click an element matching the CSS selector.", "parameters": {"type": "object", "required": ["selector"], "properties": {"selector": {"type": "string"}}}}},
    {"type": "function", "function": {"name": "browser_type", "description": "Type text into an input element.", "parameters": {"type": "object", "required": ["selector", "text"], "properties": {"selector": {"type": "string"}, "text": {"type": "string"}, "press_enter": {"type": "boolean"}}}}},
    {"type": "function", "function": {"name": "browser_screenshot", "description": "Take a screenshot of the current page.", "parameters": {"type": "object", "properties": {}}}},
    {"type": "function", "function": {"name": "browser_get_text", "description": "Extract text from the current page.", "parameters": {"type": "object", "properties": {"selector": {"type": "string"}, "max_chars": {"type": "integer"}}}}},
    {"type": "function", "function": {"name": "browser_eval", "description": "Execute JavaScript in the browser.", "parameters": {"type": "object", "required": ["js_code"], "properties": {"js_code": {"type": "string"}}}}},
]

_MESSAGING_TOOL_SCHEMAS: dict[str, dict] = {
    "whatsapp_send_message": {"type": "function", "function": {"name": "whatsapp_send_message", "description": "Send a WhatsApp message.", "parameters": {"type": "object", "required": ["recipient", "text"], "properties": {"recipient": {"type": "string", "description": "Phone number with country code, no + prefix"}, "text": {"type": "string"}}}}},
    "slack_post_message": {"type": "function", "function": {"name": "slack_post_message", "description": "Post a message to a Slack channel.", "parameters": {"type": "object", "required": ["channel", "text"], "properties": {"channel": {"type": "string"}, "text": {"type": "string"}, "thread_ts": {"type": "string"}}}}},
    "mcp_call": {"type": "function", "function": {"name": "mcp_call", "description": "Call a tool on a connected MCP server.", "parameters": {"type": "object", "required": ["server", "tool"], "properties": {"server": {"type": "string"}, "tool": {"type": "string"}, "arguments": {"type": "object"}}}}},
}


def build_ollama_tool_schemas(
    *,
    use_outlook: bool = False,
    gmail_available: bool = False,
    whatsapp_available: bool = False,
    google_drive_available: bool = False,
    onedrive_available: bool = False,
    browser_available: bool = False,
    slack_available: bool = False,
    disabled_tools: set[str] | None = None,
    blocked_tools: set[str] | None = None,
    webhooks: list[dict] | None = None,
) -> list[dict]:
    """Build the tools array for Ollama /api/chat native tool calling."""
    _disabled = disabled_tools or set()
    _blocked = blocked_tools or set()
    _skip = _disabled | _blocked

    schemas: list[dict] = []
    for s in _OLLAMA_TOOL_SCHEMAS:
        name = s["function"]["name"]
        if name in _skip:
            continue
        # Enrich fetch_webhook with actual configured webhook names
        if name == "fetch_webhook" and webhooks:
            import copy
            s = copy.deepcopy(s)
            wh_names = [w.get("name", "?") for w in webhooks]
            wh_details = "; ".join(
                w.get("name", "?") + (f" ({w['description']})" if w.get("description") else "")
                for w in webhooks
            )
            s["function"]["description"] = (
                f"Fetch data from a pre-configured webhook. "
                f"Available webhooks: {wh_details}"
            )
            s["function"]["parameters"]["properties"]["webhook_name"]["enum"] = wh_names
        schemas.append(s)
    if use_outlook:
        for s in _OUTLOOK_TOOL_SCHEMAS:
            if s["function"]["name"] not in _skip:
                schemas.append(s)
    if gmail_available:
        for s in _GMAIL_TOOL_SCHEMAS:
            if s["function"]["name"] not in _skip:
                schemas.append(s)
    if google_drive_available:
        for s in _DRIVE_TOOL_SCHEMAS:
            if s["function"]["name"] not in _skip:
                schemas.append(s)
    if onedrive_available:
        for s in _ONEDRIVE_TOOL_SCHEMAS:
            if s["function"]["name"] not in _skip:
                schemas.append(s)
    if browser_available:
        for s in _BROWSER_TOOL_SCHEMAS:
            if s["function"]["name"] not in _skip:
                schemas.append(s)
    if whatsapp_available and "whatsapp_send_message" not in _skip:
        schemas.append(_MESSAGING_TOOL_SCHEMAS["whatsapp_send_message"])
    if slack_available and "slack_post_message" not in _skip:
        schemas.append(_MESSAGING_TOOL_SCHEMAS["slack_post_message"])
    if "mcp_call" not in _skip:
        schemas.append(_MESSAGING_TOOL_SCHEMAS["mcp_call"])
    return schemas