"""
Tools package: LangGraph-style tools for Mugi.
Entry point: get_tools(). Tools are split across file_ops, pdf_tools, web_tools,
clipboard, system, vision_ocr, research, and ocr_backends (tesseract, kraken).
CLI tools (brew, ffmpeg, run_installed_cli, install_cli_package) live in
tools/cli/ and are loaded lazily only when include_cli=True.

_collect_tools() gathers all @tool functions from base submodules; get_tools() filters
by app prefs (e.g. shell_execute) and optionally appends CLI tools.

Requires the project root to be on sys.path first (mugi_backend.py does this)
so "tools" resolves to this package, not another of the same name elsewhere.
"""
from __future__ import annotations

import importlib.util
import os
import sys

_TOOLS_LIST: list = []
_CLI_TOOLS_CACHE: list | None = None

# Tool names per submodule (LangChain @tool creates callables with .name and .args_schema)
_TOOL_NAMES_BY_MODULE = {
    "file_ops": [
        "read_file", "list_directory", "delete_file", "copy_file", "move_file", "split_text_file", "merge_text_files", "save_to_file",
        "write_text_file", "append_to_text_file", "patch_user_profile", "patch_user_model",
        "propose_user_model_update", "list_pending_proposals",
        "read_file_chunk", "submit_ocr_chunk_corrections",
        "correct_ocr_text",
        "get_file_metadata", "verify_file_exists", "get_default_save_location", "suggest_save_filename",
        "commit_workspace_evolution",
    ],
    "pdf_tools": ["extract_pdf_text", "split_pdf", "merge_pdfs"],
    "web_tools": ["search_web", "search_web_public", "read_webpage", "summarize_url", "screenshot_webpage", "download_file", "capture_network_traffic"],
    "web_act": ["run_web_script", "write_web_act_file"],
    "browser_extension_tools": ["get_captured_browser_page", "request_browser_page"],
    "clipboard": [
        "get_clipboard_text", "set_clipboard_text", "get_clipboard_image", "copy_image_to_clipboard",
        "copy_image_from_url_to_clipboard", "copy_current_clipboard_image_again", "view_image",
    ],
    "system": [
        "shell_execute", "get_current_directory", "run_python_code", "show_display_window",
        "search_filesystem", "capture_screenshot", "compress_files", "extract_archive",
        "get_current_time", "inspect_system_activity", "run_host_diagnostics",
    ],
    "vision_ocr": ["ocr_scanned_pdf", "ocr_scanned_pdf_to_file", "ocr_image_text"],
    "tesseract": ["ocr_complex_document", "ocr_complex_document_to_file"],
    "kraken": ["ocr_with_kraken_api", "ocr_with_kraken_api_to_file"],
    "memory_tools": [
        "append_to_memory", "migrate_memory_to_shards", "get_memory_size", "summarize_old_memory", "archive_old_memory",
        "write_session_state", "read_session_state", "get_daily_memory_paths",
        "append_working_buffer", "get_recent_feedback", "search_memory",
        "refresh_memory",
    ],
    "research": [
        "deep_web_search",
        "read_and_analyze_page",
        "analyze_multiple_sources",
        "append_research_scratch",
        "save_research_findings",
        "suggest_follow_up_queries",
        "reflect_research_state",
        "plan_research_subquestions",
        "generate_final_research_report",
    ],
    "task_plan_tools": [
        "update_task_plan",
        "clear_task_plan",
        "get_task_plan",
    ],
    "kanban_tools": [
        # Planner-tier: drop work onto the durable, cross-profile board.
        "kanban_create_task",
        "kanban_create_batch",
        "kanban_list_tasks",
        "kanban_show_task",
        "kanban_list_runs",
        "kanban_cancel_task",
        # Worker-tier (claim-scoped): only meaningful inside spawned workers.
        # Registered globally so the tool table is consistent; the tools
        # self-reject when called outside their claim scope. Operator
        # verbs (block / unblock / set_assignee / dispatcher_*) are NOT
        # exposed here — they live on HTTP/MCP behind auth.
        "kanban_post_summary",
        "kanban_post_progress",
    ],
    "capture_tools": [
        "capture_to_inbox",
        "list_captures",
        "process_capture_inbox",
        "list_pattern_connections",
        "list_pattern_action_cards",
        "list_capture_projects",
        "create_capture_project",
        "file_capture",
        "tag_capture",
        "process_capture_project",
        "list_project_captures",
        "list_project_notes",
        "search_vault_notes",
        "list_recent_vault_notes",
        "get_note",
        "create_vault_note",
        "update_vault_note",
        "append_to_vault_note",
        "summarize_capture_project",
    ],
    "vault_graph_tools": [
        "vault_graph_neighbors",
        "vault_graph_linked_mentions",
        "vault_graph_list_suggestions",
        "vault_graph_refresh_suggestions",
        "vault_graph_confirm_edge",
        "vault_graph_reject_edge",
        "vault_graph_subgraph",
        "vault_graph_insights",
    ],
    "git_workspace": [
        "git_project_status",
        "git_project_restore",
        "git_project_commit",
        "git_project_push",
    ],
    "calendar_tools": ["get_upcoming_calendar_events"],
    "plugin_creation": [
        "create_plugin",
        "create_dashboard_plugin",
        "validate_plugin",
        "diagnose_plugin_sandbox_migration",
    ],
    "watchlist_tools": [
        "add_watch_target",
        "list_watch_targets",
        "remove_watch_target",
        "compare_watch_baseline",
    ],
    "reminders_tools": [
        "get_reminders",
        "add_reminder",
        "remind_me_in",
        "remind_at",
    ],
    "email_tools": ["list_inbox", "compose_email_draft"],
    "feed_tools": [
        "add_feed",
        "list_feeds",
        "remove_feed",
        "check_feeds",
    ],
    "weather_tools": [
        "get_weather",
    ],
    "subagent_tools": ["delegate_task"],
    "delegation_scratch_tools": ["append_delegation_scratch", "read_delegation_scratch"],
    "mesh_tools": ["contact_peer", "list_mesh_peers", "mesh_conversation_status", "mesh_send_files"],
    "background_tools": [
        "run_in_background",
        "check_background_task",
        "stop_background_task",
        "list_background_tasks",
    ],
    "skills_tools": [
        "skills_list",
        "skill_view",
        "skill_manage",
    ],
    "drive_tools": [
        "list_drives",
        "resolve_drive",
        "dismiss_drive",
        "create_drive",
    ],
    "session_search_tools": [
        "search_sessions",
    ],
    "knowledge_tools": [
        "digest_corpus",
        "list_knowledge",
        "query_knowledge",
        "ask_studied_corpus",
        "read_knowledge_source",
        "explore_knowledge_connections",
        "search_memory_and_knowledge_corpora",
    ],
    "knowledge_graph_tools": [
        "knowledge_graph_subgraph",
        "knowledge_graph_neighbors",
        "knowledge_graph_list_suggestions",
        "knowledge_graph_confirm_edge",
        "knowledge_graph_reject_edge",
        "knowledge_graph_reindex_corpus",
        "knowledge_graph_gitnexus_hint",
    ],
    "schedule_tools": [
        "schedule_task",
        "list_scheduled_tasks",
        "cancel_scheduled_task",
    ],
    "timer_tools": [
        "schedule_timer",
        "cancel_timer",
        "list_timers",
    ],
    "tool_index": [
        "find_tools",
    ],
    "macos_spotlight_tools": [
        "spotlight_search",
    ],
    "recents_tools": [
        "get_recent_files",
    ],
    "hyperframes_tools": [
        "hyperframes_create_video",
        "hyperframes_render",
        "hyperframes_lint",
        "hyperframes_doctor",
    ],
}

# CLI tool names — used for filtering without importing the CLI package.
# These are only included when include_cli=True in get_tools().
_CLI_TOOL_NAMES = frozenset([
    # brew
    "ensure_mugi_homebrew", "mugi_brew_install",
    # ffmpeg
    "get_media_info", "convert_video", "extract_audio", "convert_audio",
    "trim_media", "concat_media", "extract_frame", "merge_audio_video",
    "remove_audio", "extract_subtitles", "burn_subtitles", "split_media",
    "change_speed", "rotate_video", "images_to_video", "extract_frames_interval",
    "normalize_audio",
    # generic runner and pip install
    "run_installed_cli", "install_cli_package",
])

# Tool names in the "media" tier: included only when has_media_attachment is True (PDF/image attached).
# Note: ffmpeg tools have moved to the CLI tier; they are gated by include_cli, not include_media.
_MEDIA_TOOL_NAMES = frozenset(
    _TOOL_NAMES_BY_MODULE["pdf_tools"]
    + _TOOL_NAMES_BY_MODULE["vision_ocr"]
    + _TOOL_NAMES_BY_MODULE["tesseract"]
    + _TOOL_NAMES_BY_MODULE["kraken"]
    + ["read_file_chunk", "submit_ocr_chunk_corrections"]
)

WEB_ACT_TOOL_NAMES = frozenset({"run_web_script", "write_web_act_file"})

# Tier-0 tools are always exposed to parent agents in tiered mode.
# Specialized tools are discovered on demand via find_tools.
TIER_0_TOOLS = frozenset({
    "read_file",
    "write_text_file",
    "append_to_text_file",
    "patch_user_profile",
    "patch_user_model",
    "propose_user_model_update",
    "list_pending_proposals",
    "save_to_file",
    "list_directory",
    "search_web",
    "read_webpage",
    "get_captured_browser_page",
    "request_browser_page",
    "shell_execute",
    "get_current_directory",
    "get_current_time",
    "append_to_memory",
    "migrate_memory_to_shards",
    "search_memory",
    "get_clipboard_text",
    "set_clipboard_text",
    "delegate_task",
    "read_delegation_scratch",
    "contact_peer",
    "list_mesh_peers",
    "mesh_send_files",
    "cancel_scheduled_task",
    "list_scheduled_tasks",
    "schedule_task",
    "skills_list",
    "skill_view",
    "skill_manage",
    "find_tools",
    "update_task_plan",
    "clear_task_plan",
    "get_task_plan",
    "kanban_create_task",
    "kanban_create_batch",
    "kanban_list_tasks",
    "kanban_show_task",
    "kanban_list_runs",
    "kanban_cancel_task",
    "convene_council",
    "query_knowledge",
    "ask_studied_corpus",
    "read_knowledge_source",
    "search_memory_and_knowledge_corpora",
    "explore_knowledge_connections",
})

# Core diet (tool-schema subsetting): the slim always-on subset of TIER_0_TOOLS
# carried in every foreground chat request. Everything else in tier 0 stays
# executable and discoverable (find_tools / invocation-by-name) and joins the
# per-thread working set on first use (utils/tool_working_set.py). Keep this
# list small — each schema here is paid for in every round of every chat.
# Policy-gated names (shell_execute, convene_council) self-remove in get_tools()
# when disabled, so listing them here costs nothing for users who turned them off.
CORE_TOOLS = frozenset({
    # files
    "read_file",
    "write_text_file",
    "append_to_text_file",
    "save_to_file",
    "list_directory",
    "get_current_directory",
    # web
    "search_web",
    "read_webpage",
    # memory + the person
    "search_memory",
    "append_to_memory",
    "patch_user_model",
    # time
    "get_current_time",
    # in-conversation task plan
    "update_task_plan",
    "get_task_plan",
    "clear_task_plan",
    # durable work entry points
    "kanban_create_batch",
    "kanban_list_tasks",
    # awareness: discovery + skills
    "find_tools",
    "skills_list",
    # policy-gated
    "shell_execute",
    "convene_council",
})


def _collect_tools() -> None:
    """Load base tool modules from this package's directory; bind sys.modules['tools'] so imports resolve here."""
    global _TOOLS_LIST
    if _TOOLS_LIST:
        return
    tools_dir = os.path.dirname(os.path.abspath(__file__))
    pkg = __package__ or "tools"
    this_package = sys.modules[pkg]
    seen: set[str] = set()

    def load_one(qualname: str, filepath: str):
        existing = sys.modules.get(qualname)
        if existing is not None:
            return existing
        spec = importlib.util.spec_from_file_location(qualname, filepath)
        if not spec or not spec.loader:
            return None
        mod = importlib.util.module_from_spec(spec)
        sys.modules[qualname] = mod
        sys.modules[pkg] = this_package
        if qualname.startswith(pkg + ".ocr_backends."):
            parent_name = pkg + ".ocr_backends"
            if parent_name not in sys.modules:
                import types
                parent = types.ModuleType(parent_name)
                parent.__path__ = [os.path.join(tools_dir, "ocr_backends")]
                sys.modules[parent_name] = parent
            else:
                parent = sys.modules[parent_name]
            setattr(parent, qualname.split(".")[-1], mod)
        try:
            spec.loader.exec_module(mod)
        except Exception as e:
            import logging as _logging
            _logging.getLogger(__name__).warning(
                "Failed to load tool module %s: %s", getattr(spec, "name", "?"), e
            )
            return None
        return mod

    # CLI tools (brew, ffmpeg, runner) are NOT in this list —
    # they are loaded lazily by _get_cli_tools() when include_cli=True.
    modules_to_load = [
        ("file_ops", f"{pkg}.file_ops", os.path.join(tools_dir, "file_ops.py")),
        ("pdf_tools", f"{pkg}.pdf_tools", os.path.join(tools_dir, "pdf_tools.py")),
        ("web_tools", f"{pkg}.web_tools", os.path.join(tools_dir, "web_tools.py")),
        ("web_act", f"{pkg}.web_act", os.path.join(tools_dir, "web_act.py")),
        ("browser_extension_tools", f"{pkg}.browser_extension_tools", os.path.join(tools_dir, "browser_extension_tools.py")),
        ("clipboard", f"{pkg}.clipboard", os.path.join(tools_dir, "clipboard.py")),
        ("system", f"{pkg}.system", os.path.join(tools_dir, "system.py")),
        ("vision_ocr", f"{pkg}.vision_ocr", os.path.join(tools_dir, "vision_ocr.py")),
        ("tesseract", f"{pkg}.ocr_backends.tesseract", os.path.join(tools_dir, "ocr_backends", "tesseract.py")),
        ("kraken", f"{pkg}.ocr_backends.kraken", os.path.join(tools_dir, "ocr_backends", "kraken.py")),
        ("memory_tools", f"{pkg}.memory_tools", os.path.join(tools_dir, "memory_tools.py")),
        ("research", f"{pkg}.research", os.path.join(tools_dir, "research.py")),
        ("capture_tools", f"{pkg}.capture_tools", os.path.join(tools_dir, "capture_tools.py")),
        ("vault_graph_tools", f"{pkg}.vault_graph_tools", os.path.join(tools_dir, "vault_graph_tools.py")),
        ("task_plan_tools", f"{pkg}.task_plan_tools", os.path.join(tools_dir, "task_plan_tools.py")),
        ("kanban_tools", f"{pkg}.kanban_tools", os.path.join(tools_dir, "kanban_tools.py")),
        ("council_tools", f"{pkg}.council_tools", os.path.join(tools_dir, "council_tools.py")),
        ("git_workspace", f"{pkg}.git_workspace", os.path.join(tools_dir, "git_workspace.py")),
        ("calendar_tools", f"{pkg}.calendar_tools", os.path.join(tools_dir, "calendar_tools.py")),
        ("plugin_creation", f"{pkg}.plugin_creation", os.path.join(tools_dir, "plugin_creation.py")),
        ("watchlist_tools", f"{pkg}.watchlist_tools", os.path.join(tools_dir, "watchlist_tools.py")),
        ("reminders_tools", f"{pkg}.reminders_tools", os.path.join(tools_dir, "reminders_tools.py")),
        ("feed_tools", f"{pkg}.feed_tools", os.path.join(tools_dir, "feed_tools.py")),
        ("weather_tools", f"{pkg}.weather_tools", os.path.join(tools_dir, "weather_tools.py")),
        ("email_tools", f"{pkg}.email_tools", os.path.join(tools_dir, "email_tools.py")),
        ("subagent_tools", f"{pkg}.subagent_tools", os.path.join(tools_dir, "subagent_tools.py")),
        ("delegation_scratch_tools", f"{pkg}.delegation_scratch_tools", os.path.join(tools_dir, "delegation_scratch_tools.py")),
        ("mesh_tools", f"{pkg}.mesh_tools", os.path.join(tools_dir, "mesh_tools.py")),
        ("background_tools", f"{pkg}.background_tools", os.path.join(tools_dir, "background_tools.py")),
        ("skills_tools", f"{pkg}.skills_tools", os.path.join(tools_dir, "skills_tools.py")),
        ("drive_tools", f"{pkg}.drive_tools", os.path.join(tools_dir, "drive_tools.py")),
        ("session_search_tools", f"{pkg}.session_search_tools", os.path.join(tools_dir, "session_search_tools.py")),
        ("knowledge_tools", f"{pkg}.knowledge_tools", os.path.join(tools_dir, "knowledge_tools.py")),
        ("knowledge_graph_tools", f"{pkg}.knowledge_graph_tools", os.path.join(tools_dir, "knowledge_graph_tools.py")),
        ("schedule_tools", f"{pkg}.schedule_tools", os.path.join(tools_dir, "schedule_tools.py")),
        ("timer_tools", f"{pkg}.timer_tools", os.path.join(tools_dir, "timer_tools.py")),
        ("tool_index", f"{pkg}.tool_index", os.path.join(tools_dir, "tool_index.py")),
        ("macos_spotlight_tools", f"{pkg}.macos_spotlight_tools", os.path.join(tools_dir, "macos_spotlight_tools.py")),
        ("recents_tools", f"{pkg}.recents_tools", os.path.join(tools_dir, "recents_tools.py")),
        ("hyperframes_tools", f"{pkg}.hyperframes_tools", os.path.join(tools_dir, "hyperframes_tools.py")),
    ]
    for _short, qualname, filepath in modules_to_load:
        if not os.path.isfile(filepath):
            continue
        try:
            mod = load_one(qualname, filepath)
        except Exception:
            continue
        if mod is None:
            continue
        want = _TOOL_NAMES_BY_MODULE.get(_short, [])
        for name in want:
            obj = mod.__dict__.get(name)
            if obj is None:
                continue
            if not hasattr(obj, "args_schema"):
                continue
            invokable = callable(obj) or (hasattr(obj, "invoke") and callable(getattr(obj, "invoke")))
            if not invokable:
                continue
            tool_name = getattr(obj, "name", None) or name
            if tool_name not in seen:
                seen.add(tool_name)
                if getattr(obj, "name", None) is None:
                    try:
                        obj.name = tool_name
                    except Exception:
                        pass
                _TOOLS_LIST.append(obj)


def _get_cli_tools() -> list:
    """Lazily import and return all CLI tools. Result is cached after first call."""
    global _CLI_TOOLS_CACHE
    if _CLI_TOOLS_CACHE is not None:
        return _CLI_TOOLS_CACHE
    try:
        from tools.cli import get_cli_tools
        _CLI_TOOLS_CACHE = get_cli_tools()
    except Exception:
        _CLI_TOOLS_CACHE = []
    return _CLI_TOOLS_CACHE


def get_tools(include_media: bool = True, include_cli: bool = False):
    """Return tool list filtered by app prefs and tier flags.

    include_media: when False, exclude PDF/OCR/read_file_chunk tools.
    include_cli: when True, append CLI tools (brew, ffmpeg, run_installed_cli, install_cli_package).
                 CLI tools are never loaded or included unless this is True — they don't burn context space.

    Shell (shell_execute) is excluded when allow_shell_execute is False in app prefs.
    Evolution-only tools (commit_workspace_evolution, create_plugin, create_dashboard_plugin) are excluded when evolution is off.
    """
    _collect_tools()
    from tool_policy import get_tool_exposure_policy

    exposure_policy = get_tool_exposure_policy()
    allow_shell = bool(exposure_policy.allow_shell_execute)
    tools = list(_TOOLS_LIST)

    # Append CLI tools if requested (lazy import)
    if include_cli:
        tools = tools + _get_cli_tools()

    if not allow_shell:
        tools = [t for t in tools if getattr(t, "name", None) != "shell_execute"]

    try:
        from tools.web_act import web_act_enabled
        if not web_act_enabled():
            tools = [t for t in tools if getattr(t, "name", None) not in WEB_ACT_TOOL_NAMES]
    except Exception:
        tools = [t for t in tools if getattr(t, "name", None) not in WEB_ACT_TOOL_NAMES]

    _EVOLUTION_ONLY_TOOLS = {
        "commit_workspace_evolution",
        "create_plugin",
        "create_dashboard_plugin",
        "validate_plugin",
    }
    try:
        from core.config import evolution_enabled
        if not evolution_enabled():
            tools = [t for t in tools if getattr(t, "name", None) not in _EVOLUTION_ONLY_TOOLS]
    except Exception:
        tools = [t for t in tools if getattr(t, "name", None) not in _EVOLUTION_ONLY_TOOLS]

    if not include_media:
        tools = [t for t in tools if getattr(t, "name", None) not in _MEDIA_TOOL_NAMES]

    try:
        from utils.council_triggers import council_tool_available

        if not council_tool_available():
            tools = [t for t in tools if getattr(t, "name", None) != "convene_council"]
    except Exception:
        tools = [t for t in tools if getattr(t, "name", None) != "convene_council"]

    return tools


def get_tools_tiered(
    include_media: bool = True,
    include_cli: bool = False,
    tier0_only: bool = True,
):
    """Return all tools or only Tier-0 tools for progressive disclosure."""
    tools = get_tools(include_media=include_media, include_cli=include_cli)
    if not tier0_only:
        return tools
    return [t for t in tools if (getattr(t, "name", None) or "") in TIER_0_TOOLS]


# Re-export callbacks and OCR dispatch (used by aiagent and app)
from tools.system import (
    get_pending_shell_confirm,
    complete_shell_confirm,
    get_pending_shell_confirm_from_file,
    complete_shell_confirm_file,
    get_pending_ytdl_cookies_from_file,
    complete_ytdl_cookies_file,
)
from tools._logging import (
    set_terminal_logger,
    set_input_status_callback,
    set_live_view_callback,
    emit_live_view_event,
    set_chat_event_callback,
    emit_chat_event,
    set_plugin_stream_callback,
    get_plugin_stream_callback,
    emit_plugin_stream_event,
    set_tool_stop_check_callback,
    should_stop_tool,
    is_tool_realtime,
)
from tools.ocr_dispatch import choose_ocr_method, _ocr_runner_complex

# Optional: re-export Kraken OCR tools and venv installer for direct use
try:
    from tools.ocr_backends.kraken import (
        ocr_with_kraken_api,
        ocr_with_kraken_api_to_file,
        ensure_kraken_venv,
        remove_kraken_venv,
    )
except ImportError:
    ocr_with_kraken_api = None  # type: ignore
    ocr_with_kraken_api_to_file = None  # type: ignore
    ensure_kraken_venv = None  # type: ignore
    remove_kraken_venv = None  # type: ignore

__all__ = [
    "get_tools",
    "WEB_ACT_TOOL_NAMES",
    "get_tools_tiered",
    "TIER_0_TOOLS",
    "CORE_TOOLS",
    "set_terminal_logger",
    "set_input_status_callback",
    "set_live_view_callback",
    "emit_live_view_event",
    "set_chat_event_callback",
    "emit_chat_event",
    "set_plugin_stream_callback",
    "get_plugin_stream_callback",
    "emit_plugin_stream_event",
    "set_tool_stop_check_callback",
    "should_stop_tool",
    "is_tool_realtime",
    "get_pending_shell_confirm",
    "complete_shell_confirm",
    "get_pending_shell_confirm_from_file",
    "complete_shell_confirm_file",
    "get_pending_ytdl_cookies_from_file",
    "complete_ytdl_cookies_file",
    "choose_ocr_method",
    "_ocr_runner_complex",
    "ocr_with_kraken_api",
    "ocr_with_kraken_api_to_file",
    "ensure_kraken_venv",
    "remove_kraken_venv",
]
