"""Agent management and creation for the CLI."""

from __future__ import annotations

import os
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING, Any

from deepagents import create_deep_agent
from deepagents.backends import CompositeBackend, LocalShellBackend
from deepagents.backends.filesystem import FilesystemBackend
from deepagents.middleware import MemoryMiddleware, SkillsMiddleware
from langgraph.checkpoint.memory import InMemorySaver

if TYPE_CHECKING:
    from collections.abc import Callable, Sequence

    from deepagents.backends.sandbox import SandboxBackendProtocol
    from deepagents.middleware.subagents import CompiledSubAgent, SubAgent
    from langchain.agents.middleware import InterruptOnConfig
    from langchain.agents.middleware.types import AgentState
    from langchain.messages import ToolCall
    from langchain.tools import BaseTool
    from langchain_core.language_models import BaseChatModel
    from langgraph.checkpoint.base import BaseCheckpointSaver
    from langgraph.pregel import Pregel
    from langgraph.runtime import Runtime

from icarus.config.app_config import (
    config,
    settings,
)
from icarus.integrations.sandbox_factory import get_default_working_dir
from icarus.runtime.context import UserContext
from icarus.runtime.local_context import LocalContextMiddleware, _ExecutableBackend
from icarus.runtime.memory_context import MemoryStructureMiddleware
from icarus.runtime.pim_context import PIMContextMiddleware

# Check if PIM deps (vobject, icalendar) are actually installed.
# pim_context.py itself uses only base deps (pathlib), but the PIM tools
# and middleware are only useful when icarus[pim] is installed.
try:
    import icarus.pim  # noqa: F401
    _HAS_PIM = True
except ImportError:
    _HAS_PIM = False
from icarus.runtime.subagents import list_subagents
from icarus.runtime.time_context import TimeContextMiddleware

DEFAULT_AGENT_NAME = "agent"
"""The default agent name used when no `-a` flag is provided."""


def get_system_prompt(
    assistant_id: str,
    sandbox_type: str | None = None,
    permissions_config: "PermissionConfig | None" = None,
) -> str:
    """Get the base system prompt for the agent.

    Loads the immutable system prompt from `system_prompt.md` and
    interpolates dynamic sections (model identity, working directory,
    skills path).  Memory file structure instructions are now injected
    via :class:`~icarus.runtime.memory_context.MemoryStructureMiddleware`.

    Args:
        assistant_id: The agent identifier for path references
        sandbox_type: Type of sandbox provider
            (`'daytona'`, `'langsmith'`, `'modal'`, `'runloop'`).

            If `None`, agent is operating in local mode.
        permissions_config: Optional folder permission config. When
            ``allow_all`` is False, the allowed folders are injected
            into the system prompt so the agent knows its boundaries.

    Returns:
        The system prompt string
    """
    template = (Path(__file__).parent.parent / "assets" / "system_prompt.md").read_text()

    skills_path = f"~/.icarus/{assistant_id}/skills/"

    # Build model identity section
    model_identity_section = ""
    if settings.model_name:
        model_identity_section = (
            f"### Model Identity\n\nYou are running as model `{settings.model_name}`"
        )
        if settings.model_provider:
            model_identity_section += f" (provider: {settings.model_provider})"
        model_identity_section += ".\n"
        if settings.model_context_limit:
            model_identity_section += (
                f"Your context window is {settings.model_context_limit:,} tokens.\n"
            )
        model_identity_section += "\n"

    # Build working directory section (local vs sandbox)
    if sandbox_type:
        working_dir = get_default_working_dir(sandbox_type)
        working_dir_section = (
            f"### Current Working Directory\n\n"
            f"You are operating in a **remote Linux sandbox** at `{working_dir}`.\n\n"
            f"All code execution and file operations happen in this sandbox "
            f"environment.\n\n"
            f"**Important:**\n"
            f"- The CLI is running locally on the user's machine, but you execute "
            f"code remotely\n"
            f"- Use `{working_dir}` as your working directory for all operations\n\n"
        )
    else:
        cwd = Path.cwd()
        working_dir_section = (
            f"### Current Working Directory\n\n"
            f"The filesystem backend is currently operating in: `{cwd}`\n\n"
            f"### File System and Paths\n\n"
            f"**IMPORTANT - Path Handling:**\n"
            f"- All file paths must be absolute paths (e.g., `{cwd}/file.txt`)\n"
            f"- Use the working directory to construct absolute paths\n"
            f"- Example: To create a file in your working directory, "
            f"use `{cwd}/research_project/file.md`\n"
            f"- Never use relative paths - always construct full absolute paths\n\n"
        )

    # Build permissions section (only when restrictions are active)
    permissions_section = ""
    if permissions_config and not permissions_config.allow_all:
        folder_list = "\n".join(
            f"- `{f}`" for f in permissions_config.allowed_folders
        )
        permissions_section = (
            "### Workspace Access\n\n"
            "You have access to the following folders:\n"
            f"{folder_list}\n\n"
            "Use glob, grep, and read_file to search and read files. "
            "Do not attempt to access files outside these folders.\n\n"
        )

    return (
        template.replace("{model_identity_section}", model_identity_section)
        .replace("{working_dir_section}", working_dir_section)
        .replace("{permissions_section}", permissions_section)
        .replace("{memory_section}", "")
        .replace("{skills_path}", skills_path)
    )


def _format_write_file_description(
    tool_call: ToolCall, _state: AgentState[Any], _runtime: Runtime[Any]
) -> str:
    """Format write_file tool call for approval prompt.

    Returns:
        Formatted description string for the write_file tool call.
    """
    args = tool_call["args"]
    file_path = args.get("file_path", "unknown")
    content = args.get("content", "")

    action = "Overwrite" if Path(file_path).exists() else "Create"
    line_count = len(content.splitlines())

    return f"File: {file_path}\nAction: {action} file\nLines: {line_count}"


def _format_edit_file_description(
    tool_call: ToolCall, _state: AgentState[Any], _runtime: Runtime[Any]
) -> str:
    """Format edit_file tool call for approval prompt.

    Returns:
        Formatted description string for the edit_file tool call.
    """
    args = tool_call["args"]
    file_path = args.get("file_path", "unknown")
    replace_all = bool(args.get("replace_all", False))

    scope = "all occurrences" if replace_all else "single occurrence"
    return f"File: {file_path}\nAction: Replace text ({scope})"


def _format_web_search_description(
    tool_call: ToolCall, _state: AgentState[Any], _runtime: Runtime[Any]
) -> str:
    """Format web_search tool call for approval prompt.

    Returns:
        Formatted description string for the web_search tool call.
    """
    args = tool_call["args"]
    from icarus.config.display import get_glyphs

    query = args.get("query", "unknown")
    max_results = args.get("max_results", 5)

    return (
        f"Query: {query}\nMax results: {max_results}\n\n"
        f"{get_glyphs().warning}  This will use Tavily API credits"
    )


def _format_fetch_url_description(
    tool_call: ToolCall, _state: AgentState[Any], _runtime: Runtime[Any]
) -> str:
    """Format fetch_url tool call for approval prompt.

    Returns:
        Formatted description string for the fetch_url tool call.
    """
    from icarus.config.display import get_glyphs

    args = tool_call["args"]
    url = args.get("url", "unknown")
    timeout = args.get("timeout", 30)

    return (
        f"URL: {url}\nTimeout: {timeout}s\n\n"
        f"{get_glyphs().warning}  Will fetch and convert web content to markdown"
    )


def _format_task_description(
    tool_call: ToolCall, _state: AgentState[Any], _runtime: Runtime[Any]
) -> str:
    """Format task (subagent) tool call for approval prompt.

    The task tool signature is: task(description: str, subagent_type: str)
    The description contains all instructions that will be sent to the subagent.

    Returns:
        Formatted description string for the task tool call.
    """
    args = tool_call["args"]
    description = args.get("description", "unknown")
    subagent_type = args.get("subagent_type", "unknown")

    # Truncate description if too long for display
    description_preview = description
    if len(description) > 500:  # noqa: PLR2004  # Subagent description length threshold
        description_preview = description[:500] + "..."

    from icarus.config.display import get_glyphs

    glyphs = get_glyphs()
    separator = glyphs.box_horizontal * 40
    warning_msg = "Subagent will have access to file operations and shell commands"
    return (
        f"Subagent Type: {subagent_type}\n\n"
        f"Task Instructions:\n"
        f"{separator}\n"
        f"{description_preview}\n"
        f"{separator}\n\n"
        f"{glyphs.warning}  {warning_msg}"
    )


def _format_execute_description(
    tool_call: ToolCall, _state: AgentState[Any], _runtime: Runtime[Any]
) -> str:
    """Format execute tool call for approval prompt.

    Returns:
        Formatted description string for the execute tool call.
    """
    args = tool_call["args"]
    command = args.get("command", "N/A")
    return f"Execute Command: {command}\nWorking Directory: {Path.cwd()}"


def _format_save_contact_description(
    tool_call: ToolCall, _state: AgentState[Any], _runtime: Runtime[Any]
) -> str:
    """Format save_contact tool call for approval prompt."""
    args = tool_call["args"]
    name = args.get("full_name", "unknown")
    email = args.get("email", "")
    org = args.get("organization", "")
    parts = [f"Contact: {name}"]
    if email:
        parts.append(f"Email: {email}")
    if org:
        parts.append(f"Organization: {org}")
    return "\n".join(parts)


def _format_create_event_description(
    tool_call: ToolCall, _state: AgentState[Any], _runtime: Runtime[Any]
) -> str:
    """Format create_event tool call for approval prompt."""
    args = tool_call["args"]
    summary = args.get("summary", "unknown")
    start = args.get("start", "")
    location = args.get("location", "")
    attendees = args.get("attendees", "")
    parts = [f"Event: {summary}"]
    if start:
        parts.append(f"Start: {start}")
    if location:
        parts.append(f"Location: {location}")
    if attendees:
        parts.append(f"Attendees: {attendees}")
    return "\n".join(parts)


def _format_add_todo_description(
    tool_call: ToolCall, _state: AgentState[Any], _runtime: Runtime[Any]
) -> str:
    """Format add_todo tool call for approval prompt."""
    args = tool_call["args"]
    summary = args.get("summary", "unknown")
    due = args.get("due", "")
    priority = args.get("priority", "medium")
    attendees = args.get("attendees", "")
    assigned_to = args.get("assigned_to", "")
    parts = [f"Task: {summary}", f"Priority: {priority}"]
    if due:
        parts.append(f"Due: {due}")
    if assigned_to:
        parts.append(f"Assigned to: {assigned_to}")
    if attendees:
        parts.append(f"Related people: {attendees}")
    return "\n".join(parts)


def _format_complete_todo_description(
    tool_call: ToolCall, _state: AgentState[Any], _runtime: Runtime[Any]
) -> str:
    """Format complete_todo tool call for approval prompt."""
    args = tool_call["args"]
    summary_or_id = args.get("summary_or_id", "unknown")
    return f"Complete task: {summary_or_id}"


def _add_interrupt_on() -> dict[str, InterruptOnConfig]:
    """Configure human-in-the-loop interrupt settings for all gated tools.

    Every tool that can have side effects or access external resources
    (shell execution, file writes/edits, web search, URL fetch, task
    delegation) is gated behind an approval prompt unless auto-approve
    is enabled.

    Returns:
        Dictionary mapping tool names to their interrupt configuration.
    """
    execute_interrupt_config: InterruptOnConfig = {
        "allowed_decisions": ["approve", "reject"],
        "description": _format_execute_description,  # type: ignore[typeddict-item]  # Callable description narrower than TypedDict expects
    }

    write_file_interrupt_config: InterruptOnConfig = {
        "allowed_decisions": ["approve", "reject"],
        "description": _format_write_file_description,  # type: ignore[typeddict-item]  # Callable description narrower than TypedDict expects
    }

    edit_file_interrupt_config: InterruptOnConfig = {
        "allowed_decisions": ["approve", "reject"],
        "description": _format_edit_file_description,  # type: ignore[typeddict-item]  # Callable description narrower than TypedDict expects
    }

    web_search_interrupt_config: InterruptOnConfig = {
        "allowed_decisions": ["approve", "reject"],
        "description": _format_web_search_description,  # type: ignore[typeddict-item]  # Callable description narrower than TypedDict expects
    }

    fetch_url_interrupt_config: InterruptOnConfig = {
        "allowed_decisions": ["approve", "reject"],
        "description": _format_fetch_url_description,  # type: ignore[typeddict-item]  # Callable description narrower than TypedDict expects
    }

    task_interrupt_config: InterruptOnConfig = {
        "allowed_decisions": ["approve", "reject"],
        "description": _format_task_description,  # type: ignore[typeddict-item]  # Callable description narrower than TypedDict expects
    }

    result = {
        "execute": execute_interrupt_config,
        "write_file": write_file_interrupt_config,
        "edit_file": edit_file_interrupt_config,
        "web_search": web_search_interrupt_config,
        "fetch_url": fetch_url_interrupt_config,
        "task": task_interrupt_config,
    }

    # PIM write tools — gate behind approval (only if PIM is installed)
    if _HAS_PIM:
        result["save_contact"] = {
            "allowed_decisions": ["approve", "reject"],
            "description": _format_save_contact_description,  # type: ignore[typeddict-item]
        }
        result["create_event"] = {
            "allowed_decisions": ["approve", "reject"],
            "description": _format_create_event_description,  # type: ignore[typeddict-item]
        }
        result["add_todo"] = {
            "allowed_decisions": ["approve", "reject"],
            "description": _format_add_todo_description,  # type: ignore[typeddict-item]
        }
        result["complete_todo"] = {
            "allowed_decisions": ["approve", "reject"],
            "description": _format_complete_todo_description,  # type: ignore[typeddict-item]
        }

    return result


def create_cli_agent(
    model: str | BaseChatModel,
    assistant_id: str,
    *,
    tools: Sequence[BaseTool | Callable | dict[str, Any]] | None = None,
    sandbox: SandboxBackendProtocol | None = None,
    sandbox_type: str | None = None,
    system_prompt: str | None = None,
    auto_approve: bool = False,
    enable_memory: bool = True,
    enable_skills: bool = True,
    enable_shell: bool = True,
    checkpointer: BaseCheckpointSaver | None = None,
    memory_dir: Path,
    ctx: UserContext | None = None,
) -> tuple[Pregel, CompositeBackend]:
    """Create a CLI-configured agent with flexible options.

    This is the main entry point for creating an ICARUS CLI agent, usable
    both internally and from external code (e.g., benchmarking frameworks).

    Args:
        model: LLM model to use (e.g., `'anthropic:claude-sonnet-4-5-20250929'`)
        assistant_id: Agent identifier for memory/state storage
        tools: Additional tools to provide to agent
        sandbox: Optional sandbox backend for remote execution
            (e.g., `ModalBackend`).

            If `None`, uses local filesystem + shell.
        sandbox_type: Type of sandbox provider
            (`'daytona'`, `'langsmith'`, `'modal'`, `'runloop'`).
            Used for system prompt generation.
        system_prompt: Override the default system prompt.

            If `None`, generates one based on `sandbox_type` and `assistant_id`.
        auto_approve: If `True`, no tools trigger human-in-the-loop
            interrupts — all calls (shell execution, file writes/edits,
            web search, URL fetch) run automatically.
        memory_dir: Path to the user's persistent memory directory.
            MEMORY.md is added to memory sources and the system prompt
            includes memory instructions.

            If `False`, tools pause for user confirmation via the approval menu.
            See `_add_interrupt_on` for the full list of gated tools.
        enable_memory: Enable `MemoryMiddleware` for persistent memory
        enable_skills: Enable `SkillsMiddleware` for custom agent skills
        enable_shell: Enable shell execution via `LocalShellBackend`
            (only in local mode). When enabled, the `execute` tool is available.
        checkpointer: Optional checkpointer for session persistence.

            If `None`, uses `InMemorySaver` (no persistence across
            CLI invocations).

    Returns:
        2-tuple of `(agent_graph, backend)`

            - `agent_graph`: Configured LangGraph Pregel instance ready
                for execution
            - `composite_backend`: `CompositeBackend` for file operations
    """
    tools = tools or []

    # Setup agent directory for persistent memory (if enabled)
    if enable_memory or enable_skills:
        agent_dir = settings.ensure_agent_dir(assistant_id)
        agent_md = agent_dir / "AGENTS.md"
        if not agent_md.exists():
            # Create empty file for user customizations
            # Base instructions are loaded fresh from get_system_prompt()
            agent_md.touch()

    # Skills directories (if enabled)
    skills_dir = None
    project_skills_dir = None
    if enable_skills:
        skills_dir = settings.ensure_user_skills_dir(assistant_id)
        project_skills_dir = settings.get_project_skills_dir()

    # Load custom subagents from filesystem
    custom_subagents: list[SubAgent | CompiledSubAgent] = []
    user_agents_dir = settings.get_user_agents_dir(assistant_id)
    project_agents_dir = settings.get_project_agents_dir()

    for subagent_meta in list_subagents(
        user_agents_dir=user_agents_dir,
        project_agents_dir=project_agents_dir,
    ):
        subagent: SubAgent = {
            "name": subagent_meta["name"],
            "description": subagent_meta["description"],
            "system_prompt": subagent_meta["system_prompt"],
        }
        if subagent_meta["model"]:
            subagent["model"] = subagent_meta["model"]
        custom_subagents.append(subagent)

    # Build middleware stack based on enabled features
    agent_middleware = []

    # Add memory middleware
    if enable_memory:
        memory_sources = []
        # MEMORY.md — persistent user memory (auto-loaded every turn)
        memory_md = memory_dir / "MEMORY.md"
        if not memory_md.exists():
            memory_md.write_text(
                "# ICARUS Memory\n\n"
                "<!-- Write facts, preferences, and key information here -->\n"
            )
        memory_sources.append(str(memory_md))
        # AGENTS.md — agent instructions/personality
        memory_sources.append(str(settings.get_user_agent_md_path(assistant_id)))
        memory_sources.extend(str(p) for p in settings.get_project_agent_md_path())

        agent_middleware.append(
            MemoryMiddleware(
                backend=FilesystemBackend(),
                sources=memory_sources,
            )
        )

    # Add skills middleware
    if enable_skills:
        # Built-in first (lowest precedence), then plugins, then user,
        # then project (highest)
        sources = [str(settings.get_built_in_skills_dir())]

        # Add all plugin skill directories
        plugins_base = settings.get_plugins_base_dir()
        if plugins_base.exists():
            for plugin_dir in sorted(plugins_base.iterdir()):
                plugin_skills = plugin_dir / "skills"
                if plugin_skills.is_dir():
                    sources.append(str(plugin_skills))

        sources.append(str(skills_dir))
        if project_skills_dir:
            sources.append(str(project_skills_dir))

        agent_middleware.append(
            SkillsMiddleware(
                backend=FilesystemBackend(),
                sources=sources,
            )
        )

    # Load folder permissions (local mode only — sandbox handles its own isolation)
    from icarus.config.permissions import PermissionConfig
    permissions_config: PermissionConfig | None = None

    # CONDITIONAL SETUP: Local vs Remote Sandbox
    if sandbox is None:
        # ========== LOCAL MODE ==========
        if enable_shell:
            # Create environment for shell commands
            # Restore user's original LANGSMITH_PROJECT so their code traces separately
            shell_env = os.environ.copy()
            if settings.user_langchain_project:
                shell_env["LANGSMITH_PROJECT"] = settings.user_langchain_project

            # Use LocalShellBackend for filesystem + shell execution.
            # The SDK's FilesystemMiddleware exposes per-command timeout
            # on the execute tool natively.
            backend = LocalShellBackend(
                root_dir=Path.cwd(),
                inherit_env=True,
                env=shell_env,
            )
        else:
            # No shell access - use plain FilesystemBackend
            backend = FilesystemBackend()

        # Wrap with folder permission enforcement when restrictions are active
        permissions_config = PermissionConfig.load()
        if not permissions_config.allow_all:
            from icarus.runtime.backend import PermissionedBackend
            backend = PermissionedBackend(inner=backend, config=permissions_config)
    else:
        # ========== REMOTE SANDBOX MODE ==========
        backend = sandbox  # Remote sandbox (ModalBackend, etc.)
        # Note: Shell middleware not used in sandbox mode
        # File operations and execute tool are provided by the sandbox backend

    # Local context middleware (git info, directory tree, etc.)
    # Uses backend.execute() so it works in both local shell and remote sandbox modes.
    # Only enabled when the backend supports shell execution.
    if isinstance(backend, _ExecutableBackend):
        agent_middleware.append(LocalContextMiddleware(backend=backend))

    # Memory structure middleware — tells the agent WHERE things live
    # (MEMORY.md, daily logs, transcripts, voice profiles).
    # Complements the SDK's MemoryMiddleware which teaches WHEN to remember.
    agent_middleware.append(MemoryStructureMiddleware(memory_dir=memory_dir))

    # PIM context middleware — tells the agent what PIM data is available
    # (contact count, upcoming events, open tasks). Requires icarus[pim].
    if _HAS_PIM and ctx is not None:
        agent_middleware.append(
            PIMContextMiddleware(
                contacts_dir=ctx.contacts_dir,
                calendar_dir=ctx.calendar_dir,
                todos_dir=ctx.todos_dir,
            )
        )

    # Time context middleware — last so the time section appears at the very
    # end of the system prompt, closest to the conversation.
    agent_middleware.append(TimeContextMiddleware())

    # Get or use custom system prompt
    if system_prompt is None:
        system_prompt = get_system_prompt(
            assistant_id=assistant_id,
            sandbox_type=sandbox_type,
            permissions_config=permissions_config,
        )

    # Configure interrupt_on based on auto_approve setting
    interrupt_on: dict[str, bool | InterruptOnConfig] | None = None
    if auto_approve:  # noqa: SIM108  # if-else more readable for interrupt_on config
        # No interrupts - all tools run automatically
        interrupt_on = {}
    else:
        # Full HITL for destructive operations
        interrupt_on = _add_interrupt_on()  # type: ignore[assignment]  # InterruptOnConfig is compatible at runtime

    # Set up composite backend with routing
    # For local FilesystemBackend, route large tool results to /tmp to avoid polluting
    # the working directory. For sandbox backends, no special routing is needed.
    if sandbox is None:
        # Local mode: Route large results to a unique temp directory
        large_results_backend = FilesystemBackend(
            root_dir=tempfile.mkdtemp(prefix="icarus_large_results_"),
            virtual_mode=True,
        )
        conversation_history_backend = FilesystemBackend(
            root_dir=tempfile.mkdtemp(prefix="icarus_conversation_history_"),
            virtual_mode=True,
        )
        composite_backend = CompositeBackend(
            default=backend,
            routes={
                "/large_tool_results/": large_results_backend,
                "/conversation_history/": conversation_history_backend,
            },
        )
    else:
        # Sandbox mode: No special routing needed
        composite_backend = CompositeBackend(
            default=backend,
            routes={},
        )

    # Create the agent
    # Use provided checkpointer or fallback to InMemorySaver
    final_checkpointer = checkpointer if checkpointer is not None else InMemorySaver()
    agent = create_deep_agent(
        model=model,
        system_prompt=system_prompt,
        tools=tools,
        backend=composite_backend,
        middleware=agent_middleware,
        interrupt_on=interrupt_on,
        checkpointer=final_checkpointer,
        subagents=custom_subagents or None,
    ).with_config(config)
    return agent, composite_backend
