# subagent_routing.py — Routing heuristics for subagent-first tool dispatch.
#
# Decides which tool calls should be delegated to subagents vs executed
# directly in the main agent thread.  Priorities: efficiency and answer clarity.
from __future__ import annotations

import logging
import time
from uuid import uuid4
from typing import Any

log = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Tools that should ALWAYS run directly (never delegated)
# ---------------------------------------------------------------------------

# Stateful / UI / main-thread-only tools: these depend on the main agent's
# conversation state, require immediate side effects visible to the same turn,
# or are trivially fast (delegation overhead > tool runtime).
FORCE_DIRECT_TOOLS: frozenset[str] = frozenset({
    # Memory / session state — needs main-thread context continuity
    "append_to_memory", "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",
    "search_memory_and_knowledge_corpora",
    "list_drives", "resolve_drive", "dismiss_drive", "create_drive",
    # Clipboard — OS-level, must run on main thread
    "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",
    # Subagent management (avoid recursion)
    "delegate_task", "run_in_background", "check_background_task",
    "stop_background_task", "list_background_tasks",
    # Trivially fast / deterministic utilities
    "get_current_directory", "get_current_time", "get_file_metadata",
    "verify_file_exists", "get_default_save_location", "suggest_save_filename",
    # Display / UI
    "show_display_window", "capture_screenshot",
    # Vault notes (fast local I/O, used in multi-step reasoning)
    "create_vault_note",
    "append_to_vault_note",
    "update_vault_note",
    "get_note",
    "list_project_notes",
    "search_vault_notes",
    "list_recent_vault_notes",
    "vault_graph_neighbors", "vault_graph_linked_mentions", "vault_graph_list_suggestions",
    "vault_graph_subgraph", "vault_graph_insights",
    "vault_graph_refresh_suggestions", "vault_graph_confirm_edge", "vault_graph_reject_edge",
    # Research scratch (managed by research flow, not delegated independently)
    "append_research_scratch", "save_research_findings",
    "generate_final_research_report",
    # Calendar / reminders / feeds (fast local/network, state-dependent)
    "get_upcoming_calendar_events",
    "get_reminders", "add_reminder", "remind_me_in", "remind_at",
    "add_feed", "list_feeds", "remove_feed", "check_feeds",
    # Email (interactive)
    "list_inbox", "compose_email_draft",
    # Watchlist (state-dependent)
    "add_watch_target", "list_watch_targets", "remove_watch_target",
    "compare_watch_baseline",
    # Plugin creation / evolution
    "commit_workspace_evolution", "create_plugin", "create_dashboard_plugin",
    # Git (interactive, needs context)
    "git_project_status", "git_project_restore", "git_project_commit", "git_project_push",
    # Weather (trivially fast API call)
    "get_weather",
    # Network capture (already handled as realtime)
    "capture_network_traffic",
})

# ---------------------------------------------------------------------------
# Tools eligible for subagent delegation — grouped by profile
# ---------------------------------------------------------------------------

# Maps tool_name -> preferred subagent profile for delegation.
TOOL_TO_PROFILE: dict[str, str] = {
    # Web research / reading
    "search_web": "web_research_light",
    "search_web_public": "web_research_light",
    "read_webpage": "web_research_light",
    "summarize_url": "web_research_light",
    "download_file": "web_research_light",
    "deep_web_search": "research",
    "read_and_analyze_page": "web_research_light",
    "analyze_multiple_sources": "web_research_light",
    "suggest_follow_up_queries": "web_research_light",
    "screenshot_webpage": "web_research_light",
    # File reading / code exploration
    "read_file": "code_readonly",
    "read_file_chunk": "code_readonly",
    "list_directory": "code_readonly",
    "search_filesystem": "code_readonly",
    # PDF / extraction
    "extract_pdf_text": "extraction",
    "split_pdf": "extraction",
    "merge_pdfs": "extraction",
    # OCR
    "ocr_scanned_pdf": "extraction",
    "ocr_scanned_pdf_to_file": "extraction",
    "ocr_image_text": "extraction",
    "ocr_complex_document": "extraction",
    "ocr_complex_document_to_file": "extraction",
    "ocr_with_kraken_api": "extraction",
    "ocr_with_kraken_api_to_file": "extraction",
    # File operations (cautious)
    "save_to_file": "ops_cautious",
    "write_text_file": "ops_cautious",
    "append_to_text_file": "ops_cautious",
    "delete_file": "ops_cautious",
    "copy_file": "ops_cautious",
    "move_file": "ops_cautious",
    "split_text_file": "ops_cautious",
    "merge_text_files": "ops_cautious",
    "compress_files": "ops_cautious",
    "extract_archive": "ops_cautious",
    # Shell
    "shell_execute": "ops_cautious",
    "run_python_code": "ops_cautious",
    # OCR corrections
    "submit_ocr_chunk_corrections": "extraction",
    "correct_ocr_text": "extraction",
}

# Phase 1 tools: safe, read-only, no side effects.  Enabled first.
PHASE_1_TOOLS: frozenset[str] = frozenset({
    "search_web", "search_web_public", "read_webpage", "summarize_url",
    "read_and_analyze_page", "analyze_multiple_sources",
    "suggest_follow_up_queries", "screenshot_webpage", "download_file",
    "deep_web_search",
    "read_file", "read_file_chunk", "list_directory", "search_filesystem",
})

# Phase 2: extraction workflows.
PHASE_2_TOOLS: frozenset[str] = frozenset({
    "extract_pdf_text", "split_pdf", "merge_pdfs",
    "ocr_scanned_pdf", "ocr_scanned_pdf_to_file",
    "ocr_image_text",
    "ocr_complex_document", "ocr_complex_document_to_file",
    "ocr_with_kraken_api", "ocr_with_kraken_api_to_file",
    "submit_ocr_chunk_corrections", "correct_ocr_text",
})

# Phase 3: operational tools (write/shell).
PHASE_3_TOOLS: frozenset[str] = frozenset({
    "save_to_file", "write_text_file", "append_to_text_file",
    "delete_file", "copy_file", "move_file",
    "split_text_file", "merge_text_files",
    "compress_files", "extract_archive",
    "shell_execute", "run_python_code",
})


def _get_routing_config() -> dict:
    try:
        from core.config import get_subagent_config

        return get_subagent_config()
    except Exception:
        log.warning("Failed to load subagent routing config; routing disabled", exc_info=True)
        return {}


def _routing_phase(cfg: dict) -> int:
    try:
        phase = int(cfg.get("routing_phase", 3) or 3)
    except Exception:
        phase = 3
    return max(1, min(phase, 3))


def direct_execution_blocked_after_routing_failure(tool_name: str) -> bool:
    """Phase 3 tools must not fall back to parent direct execution."""
    return tool_name in PHASE_3_TOOLS


def should_route_to_subagent(
    tool_name: str,
    args: dict | None = None,
    *,
    subagent_mode: bool = False,
    is_realtime: bool = False,
) -> tuple[bool, str | None]:
    """Decide whether a tool call should be delegated to a subagent.

    Returns (should_delegate, profile_name).
    """
    if subagent_mode:
        return False, None

    if is_realtime:
        return False, None

    if tool_name in FORCE_DIRECT_TOOLS:
        return False, None

    cfg = _get_routing_config()
    if not cfg.get("subagent_first_routing", False):
        return False, None

    if not cfg.get("enabled", True):
        return False, None

    profile = TOOL_TO_PROFILE.get(tool_name)
    if profile is None:
        return False, None

    phase = _routing_phase(cfg)
    if tool_name in PHASE_1_TOOLS:
        return (phase >= 1), profile if phase >= 1 else None
    if tool_name in PHASE_2_TOOLS:
        return (phase >= 2), profile if phase >= 2 else None
    if tool_name in PHASE_3_TOOLS:
        return (phase >= 3), profile if phase >= 3 else None

    return False, None


def route_tool_to_subagent(
    tool_name: str,
    args_str: str,
    profile: str,
    thread_id: str,
    on_log: Any = None,
    policy_context: Any | None = None,
) -> str | None:
    """Execute a tool call via subagent delegation. Returns result string, or None on failure."""
    t0 = time.monotonic()
    args_dict: dict[str, Any] = {}
    args_json_for_task = "{}"
    args_summary = ""
    try:
        import json
        args_dict = json.loads(args_str) if isinstance(args_str, str) and args_str else {}
        if not isinstance(args_dict, dict):
            args_dict = {}
        try:
            from tool_arg_normalizer import normalize_tool_args
            args_dict = normalize_tool_args(tool_name, dict(args_dict))
        except Exception as norm_exc:
            log.warning("tool arg normalize failed for %s: %s", tool_name, norm_exc)
        args_json_for_task = json.dumps(args_dict, ensure_ascii=False)
        args_summary = ", ".join(f"{k}={repr(v)[:80]}" for k, v in list(args_dict.items())[:3])
    except Exception:
        args_summary = args_str[:100] if args_str else ""

    task = (
        f"Execute exactly one tool call.\n"
        f"Tool: {tool_name}\n"
        f"Arguments JSON (use this exact JSON payload):\n{args_json_for_task}\n\n"
        "Use native structured function tool_calls with the Arguments JSON above.\n\n"
        f"Quick summary: {args_summary}\n\n"
        f"Return the tool's output directly. Be concise and factual."
    )

    if on_log:
        on_log("AGENT", f"Routing {tool_name} to subagent (profile={profile})")

    try:
        from subagent_runner import run_subagent_structured, get_current_iteration_budget
        if policy_context is None:
            try:
                from tool_policy import get_current_policy_context
                policy_context = get_current_policy_context()
            except Exception:
                policy_context = None
        _iteration_budget = get_current_iteration_budget()
        result = run_subagent_structured(
            task_description=task,
            agent_type=profile,
            policy_context=policy_context,
            iteration_budget=_iteration_budget,
            parent_thread_id=thread_id,
            delegation_job_id=f"routing_{uuid4().hex[:10]}",
        )
        elapsed = time.monotonic() - t0

        _emit_routing_metric(tool_name, profile, elapsed, result.success, result.timed_out)

        if result.success:
            return result.output
        else:
            if on_log:
                on_log("WARN", f"Subagent routing failed for {tool_name}: {(result.error or '')[:200]}; falling back to direct.")
            return None
    except Exception as exc:
        if on_log:
            on_log("WARN", f"Subagent routing error for {tool_name}: {exc}; falling back to direct.")
        return None


def _emit_routing_metric(
    tool_name: str,
    profile: str,
    elapsed_seconds: float,
    success: bool,
    timed_out: bool,
) -> None:
    """Emit a routing metric to the conversation trace (if enabled)."""
    try:
        from tools.conversation_trace import is_trace_enabled, emit_subagent_routing
        if not is_trace_enabled():
            return
        from persistence import get_current_agent_thread_id
        tid = get_current_agent_thread_id() or "unknown"
        emit_subagent_routing(tid, tool_name, profile, elapsed_seconds, success, timed_out)
    except Exception as exc:
        log.warning("routing metric emit failed for %s: %s", tool_name, exc)
