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

import os
import shutil
import sys
import tempfile
from pathlib import Path

from deepagents import create_deep_agent

# Detect if running on Windows
IS_WINDOWS = sys.platform == "win32"
from deepagents.backends import CompositeBackend
from deepagents.backends.filesystem import FilesystemBackend
from deepagents.backends.store import StoreBackend
# Optional middleware - may not be available in all versions of deepagents
try:
    from deepagents.middleware import MemoryMiddleware, SkillsMiddleware
    HAS_MIDDLEWARE = True
except ImportError:
    MemoryMiddleware = None
    SkillsMiddleware = None
    HAS_MIDDLEWARE = False
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.checkpoint.memory import InMemorySaver
from langgraph.pregel import Pregel
from langgraph.runtime import Runtime
from langgraph.store.base import BaseStore

from ieum.config import (
    COLORS,
    config,
    console,
    get_client_info,
    get_default_coding_instructions,
    settings,
)
from ieum.local_context import LocalContextMiddleware
from ieum.shell import ShellMiddleware


def list_agents() -> None:
    """List all available agents."""
    agents_dir = settings.user_ieum_dir

    if not agents_dir.exists() or not any(agents_dir.iterdir()):
        console.print("[yellow]No agents found.[/yellow]")
        console.print(
            "[dim]Agents will be created in ~/.ieum/ when you first use them.[/dim]",
            style=COLORS["dim"],
        )
        return

    console.print("\n[bold]Available Agents:[/bold]\n", style=COLORS["primary"])

    for agent_path in sorted(agents_dir.iterdir()):
        if agent_path.is_dir():
            agent_name = agent_path.name
            agent_md = agent_path / "AGENTS.md"

            if agent_md.exists():
                console.print(f"  • [bold]{agent_name}[/bold]", style=COLORS["primary"])
                console.print(f"    {agent_path}", style=COLORS["dim"])
            else:
                console.print(
                    f"  • [bold]{agent_name}[/bold] [dim](incomplete)[/dim]", style=COLORS["tool"]
                )
                console.print(f"    {agent_path}", style=COLORS["dim"])

    console.print()


def reset_agent(agent_name: str, source_agent: str | None = None) -> None:
    """Reset an agent to default or copy from another agent."""
    agents_dir = settings.user_ieum_dir
    agent_dir = agents_dir / agent_name

    if source_agent:
        source_dir = agents_dir / source_agent
        source_md = source_dir / "AGENTS.md"

        if not source_md.exists():
            console.print(
                f"[bold red]Error:[/bold red] Source agent '{source_agent}' not found "
                "or has no AGENTS.md"
            )
            return

        source_content = source_md.read_text()
        action_desc = f"contents of agent '{source_agent}'"
    else:
        source_content = get_default_coding_instructions()
        action_desc = "default"

    if agent_dir.exists():
        shutil.rmtree(agent_dir)
        console.print(f"Removed existing agent directory: {agent_dir}", style=COLORS["tool"])

    agent_dir.mkdir(parents=True, exist_ok=True)
    agent_md = agent_dir / "AGENTS.md"
    agent_md.write_text(source_content)

    console.print(f"✓ Agent '{agent_name}' reset to {action_desc}", style=COLORS["primary"])
    console.print(f"Location: {agent_dir}\n", style=COLORS["dim"])


def get_system_prompt(
    assistant_id: str,
    client_environment: str = "TUI",
    working_directory: str | None = None,
) -> str:
    """Get the base system prompt for the agent.

    Args:
        assistant_id: The agent identifier for path references
        client_environment: Client environment type - "TUI" or "WEB"
        working_directory: Explicit working directory.  Falls back to
            ``Path.cwd()`` when not provided.

    Returns:
        The system prompt string (without AGENTS.md content)
    """
    agent_dir_path = f"~/.ieum/{assistant_id}"

    # Client environment info (OS, connection type)
    client_info = get_client_info(client_environment)

    cwd = Path(working_directory) if working_directory else Path.cwd()
    if IS_WINDOWS:
        # Windows-specific working directory section
        working_dir_section = f"""### Current Working Directory

The filesystem backend is currently operating in: `{cwd}`

### Operating System

You are running on **Windows** with **PowerShell** as the shell.

### File System and Paths

**IMPORTANT - Virtual Path Handling:**
- File tools (read_file, write_file, edit_file, ls, glob, grep) use **virtual paths** starting with `/`
- The root `/` maps to the current working directory: `{cwd}`
- Example: `/file.txt` → `{cwd}\\file.txt`
- Example: `/src/main.py` → `{cwd}\\src\\main.py`
- **NEVER use Windows paths like `D:/...` or `C:/...` in file tools**

**Path Examples:**
- ✅ Correct: `read_file(file_path="/README.md")`
- ✅ Correct: `write_file(file_path="/src/app.py", content="...")`
- ❌ Wrong: `read_file(file_path="D:/project/README.md")`

### Shell Commands (PowerShell)

Shell commands (`execute`) use **real Windows paths**, not virtual paths.

| Task | PowerShell Command | Unix Equivalent |
|------|-------------------|-----------------|
| List files | `Get-ChildItem` or `dir` | `ls` |
| Read file | `Get-Content` | `cat` |
| Search in files | `Select-String` | `grep` |
| Copy | `Copy-Item` | `cp` |
| Move | `Move-Item` | `mv` |
| Delete | `Remove-Item` | `rm` |
| Current dir | `Get-Location` | `pwd` |
| Env variable | `$env:VAR` | `$VAR` |

"""
    else:
        # Unix/Linux/macOS working directory section
        working_dir_section = f"""### Current Working Directory

The filesystem backend is currently operating in: `{cwd}`

### File System and Paths

**IMPORTANT - Virtual Path Handling:**
- File tools (read_file, write_file, edit_file, ls, glob, grep) use **virtual paths** starting with `/`
- The root `/` maps to the current working directory: `{cwd}`
- Example: `/file.txt` → `{cwd}/file.txt`
- **NEVER use absolute system paths in file tools**

**Path Examples:**
- ✅ Correct: `read_file(file_path="/README.md")`
- ✅ Correct: `write_file(file_path="/src/app.py", content="...")`

"""

    return (
        client_info
        + working_dir_section
        + f"""### Skills Directory

Your skills are stored at: `{agent_dir_path}/skills/`
Skills may contain scripts or supporting files. When executing skill scripts with bash, use the real filesystem path:
Example: `bash python {agent_dir_path}/skills/web-research/script.py`

### Long-Term Memory Storage

Files written to `/.memories/` are stored persistently across all conversations:
- Use `/.memories/` for information that should persist across sessions
- Example: `/.memories/user_preferences.md` for user preferences
- Example: `/.memories/project_notes.md` for project-specific knowledge
- These files are stored in a database, not the local filesystem
- Contents persist even when starting a new conversation thread

### Human-in-the-Loop Tool Approval

Some tool calls require user approval before execution. When a tool call is rejected by the user:
1. Accept their decision immediately - do NOT retry the same command
2. Explain that you understand they rejected the action
3. Suggest an alternative approach or ask for clarification
4. Never attempt the exact same rejected command again

Respect the user's decisions and work with them collaboratively.

### Web Search Tool Usage

When you use the web_search tool:
1. The tool will return search results with titles, URLs, and content excerpts
2. You MUST read and process these results, then respond naturally to the user
3. NEVER show raw JSON or tool results directly to the user
4. Synthesize the information from multiple sources into a coherent answer
5. Cite your sources by mentioning page titles or URLs when relevant
6. If the search doesn't find what you need, explain what you found and ask clarifying questions

The user only sees your text responses - not tool results. Always provide a complete, natural language answer after using web_search.

### Todo List Management

When using the write_todos tool:
1. Keep the todo list MINIMAL - aim for 3-6 items maximum
2. Only create todos for complex, multi-step tasks that truly need tracking
3. Break down work into clear, actionable items without over-fragmenting
4. For simple tasks (1-2 steps), just do them directly without creating todos
5. When first creating a todo list for a task, ALWAYS ask the user if the plan looks good before starting work
   - Create the todos, let them render, then ask: "Does this plan look good?" or similar
   - Wait for the user's response before marking the first todo as in_progress
   - If they want changes, adjust the plan accordingly
6. Update todo status promptly as you complete each item

The todo list is a planning tool - use it judiciously to avoid overwhelming the user with excessive task tracking.

### Asking the User Questions (ask_user_question)

You have access to `ask_user_question` - an interactive questionnaire tool that pauses your execution and presents a multi-choice form to the user.

**When to use:**
- Before starting a task with multiple valid approaches (e.g., "Which caching strategy?", "Which framework?")
- When requirements are ambiguous and you need specific user preferences
- When implementation choices significantly affect the outcome
- At the START of complex tasks to gather key decisions upfront

**When NOT to use:**
- For simple yes/no questions - just ask in plain text
- When the user already gave clear instructions
- For trivial implementation details you can decide yourself
- Repeatedly for the same topic - ask once, then proceed

**Quality guidelines:**
- Keep questions to 1-3 per call (don't overwhelm)
- Each question should have 2-4 meaningful, distinct options
- Headers should be short (max 12 chars): "캐싱 전략", "DB 선택", "인증 방식"
- Option labels should be concise (1-5 words): "Redis", "PostgreSQL", "JWT 인증"
- Descriptions should explain trade-offs, not just define the option
- Use `multi_select: true` only when multiple options can genuinely coexist"""
    )


def _format_write_file_description(
    tool_call: ToolCall, _state: AgentState, _runtime: Runtime
) -> str:
    """Format write_file tool call for approval prompt."""
    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, _runtime: Runtime
) -> str:
    """Format edit_file tool call for approval prompt."""
    args = tool_call["args"]
    file_path = args.get("file_path", "unknown")
    replace_all = bool(args.get("replace_all", False))

    return (
        f"File: {file_path}\n"
        f"Action: Replace text ({'all occurrences' if replace_all else 'single occurrence'})"
    )


def _format_web_search_description(
    tool_call: ToolCall, _state: AgentState, _runtime: Runtime
) -> str:
    """Format web_search tool call for approval prompt."""
    args = tool_call["args"]
    query = args.get("query", "unknown")
    max_results = args.get("max_results", 5)

    return f"Query: {query}\nMax results: {max_results}\n\n⚠️  This will use Tavily API credits"


def _format_fetch_url_description(
    tool_call: ToolCall, _state: AgentState, _runtime: Runtime
) -> str:
    """Format fetch_url tool call for approval prompt."""
    args = tool_call["args"]
    url = args.get("url", "unknown")
    timeout = args.get("timeout", 30)

    return f"URL: {url}\nTimeout: {timeout}s\n\n⚠️  Will fetch and convert web content to markdown"


def _format_shell_description(tool_call: ToolCall, _state: AgentState, _runtime: Runtime) -> str:
    """Format shell tool call for approval prompt."""
    args = tool_call["args"]
    command = args.get("command", "N/A")
    shell_type = "PowerShell" if IS_WINDOWS else "Shell"
    return f"{shell_type} Command: {command}\nWorking Directory: {Path.cwd()}"


def _add_interrupt_on() -> dict[str, InterruptOnConfig]:
    """Configure human-in-the-loop interrupt_on settings for destructive tools."""
    shell_interrupt_config: InterruptOnConfig = {
        "allowed_decisions": ["approve", "reject"],
        "description": _format_shell_description,
    }

    write_file_interrupt_config: InterruptOnConfig = {
        "allowed_decisions": ["approve", "reject"],
        "description": _format_write_file_description,
    }

    edit_file_interrupt_config: InterruptOnConfig = {
        "allowed_decisions": ["approve", "reject"],
        "description": _format_edit_file_description,
    }

    web_search_interrupt_config: InterruptOnConfig = {
        "allowed_decisions": ["approve", "reject"],
        "description": _format_web_search_description,
    }

    fetch_url_interrupt_config: InterruptOnConfig = {
        "allowed_decisions": ["approve", "reject"],
        "description": _format_fetch_url_description,
    }

    return {
        "shell": shell_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,
    }


def create_cli_agent(
    model: str | BaseChatModel,
    assistant_id: str,
    *,
    tools: list[BaseTool] | None = None,
    system_prompt: str | None = None,
    auto_approve: bool = False,
    enable_memory: bool = True,
    enable_skills: bool = True,
    enable_shell: bool = True,
    enable_mcp: bool = True,
    checkpointer: BaseCheckpointSaver | None = None,
    store: BaseStore | None = None,
    client_environment: str = "TUI",
    working_directory: str | None = None,
) -> tuple[Pregel, CompositeBackend]:
    """Create a CLI-configured agent with flexible options.

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

    Args:
        model: LLM model to use (e.g., "ollama/qwen3:4b")
        assistant_id: Agent identifier for memory/state storage
        tools: Additional tools to provide to agent
        system_prompt: Override the default system prompt. If None, generates one
                      based on assistant_id.
        auto_approve: If True, automatically approves all tool calls without human
                     confirmation. Useful for automated workflows.
        enable_memory: Enable MemoryMiddleware for persistent memory
        enable_skills: Enable SkillsMiddleware for custom agent skills
        enable_shell: Enable ShellMiddleware for local shell execution
        enable_mcp: Enable MCP (Model Context Protocol) tools from configured servers
        checkpointer: Optional checkpointer for session persistence. If None, uses
                     InMemorySaver (no persistence across CLI invocations).
        store: Optional store for cross-thread persistent memories (long-term memory).
               If None, memories are not persisted across sessions.
        client_environment: Client environment type - "TUI" (terminal) or "WEB" (browser)
        working_directory: Explicit working directory path.  When provided, the
            agent uses this directory as its root instead of ``Path.cwd()``.
            This avoids the need for ``os.chdir`` calls in multi-session
            server environments.

    Returns:
        2-tuple of (agent_graph, backend)
        - agent_graph: Configured LangGraph Pregel instance ready for execution
        - composite_backend: CompositeBackend for file operations
    """
    # Resolve the effective working directory
    cwd = Path(working_directory) if working_directory else Path.cwd()

    tools = list(tools) if tools else []

    # Load MCP tools from configured servers
    if enable_mcp:
        try:
            from ieum.mcp import get_mcp_tools
            mcp_tools = get_mcp_tools()
            if mcp_tools:
                tools.extend(mcp_tools)
        except Exception:
            # MCP loading failed, continue without MCP tools
            pass

    # Load IEUM advanced tools (git, ast, review, docs, indexing)
    try:
        from ieum.tools import ALL_TOOLS
        tools.extend(ALL_TOOLS)
    except Exception:
        # IEUM tools loading failed, continue without them
        pass

    # 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():
            source_content = get_default_coding_instructions()
            agent_md.write_text(source_content, encoding="utf-8")

    # 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()

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

    # Add memory middleware (if available)
    if enable_memory and HAS_MIDDLEWARE and MemoryMiddleware is not None:
        memory_sources = [str(settings.get_user_agent_md_path(assistant_id))]
        project_agent_md = settings.get_project_agent_md_path()
        if project_agent_md:
            memory_sources.append(str(project_agent_md))

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

    # Add skills middleware (if available)
    if enable_skills and HAS_MIDDLEWARE and SkillsMiddleware is not None:
        from ieum.skills.load import get_builtin_skills_dir

        # Priority: builtin < user < project (later sources override earlier)
        sources = [str(get_builtin_skills_dir()), str(skills_dir)]
        if project_skills_dir:
            sources.append(str(project_skills_dir))

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

    # Local filesystem backend with cwd as root and virtual_mode enabled
    # virtual_mode=True maps virtual paths (e.g., /file.txt) to real paths (e.g., D:\cah\file.txt)
    # This provides consistent path handling across Windows/Unix and blocks path traversal
    backend = FilesystemBackend(root_dir=str(cwd), virtual_mode=True)

    # Local context middleware (git info, directory tree, etc.)
    agent_middleware.append(LocalContextMiddleware())

    # Add shell middleware
    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

        agent_middleware.append(
            ShellMiddleware(
                workspace_root=str(cwd),
                env=shell_env,
            )
        )

    # Note: SummarizationMiddleware is included by default in the deepagents SDK (>=0.4.0).
    # It runs in the model node via wrap_model_call and retains full message history
    # in graph state, compressing only when approaching token limits.
    # The model's profile (max_input_tokens) drives fractional triggers.

    # Get or use custom system prompt
    if system_prompt is None:
        system_prompt = get_system_prompt(
            assistant_id=assistant_id,
            client_environment=client_environment,
            working_directory=str(cwd),
        )

    # Configure interrupt_on based on auto_approve setting
    if auto_approve:
        # No interrupts - all tools run automatically
        interrupt_on = {}
    else:
        # Full HITL for destructive operations
        interrupt_on = _add_interrupt_on()

    # Set up composite backend with routing
    # Route large tool results to temp directory to avoid polluting the working directory
    large_results_backend = FilesystemBackend(
        root_dir=tempfile.mkdtemp(prefix="ieum_large_results_"),
        virtual_mode=True,
    )
    conversation_history_backend = FilesystemBackend(
        root_dir=tempfile.mkdtemp(prefix="ieum_conversation_history_"),
        virtual_mode=True,
    )

    # Base routes without StoreBackend (for return value used by FileOpTracker)
    # Use dot-prefix to make these hidden folders
    base_routes = {
        "/.large_tool_results/": large_results_backend,
        "/.conversation_history/": conversation_history_backend,
    }

    # CompositeBackend for return value (used for file diff operations)
    composite_backend = CompositeBackend(
        default=backend,
        routes=base_routes,
    )

    # Backend factory that includes StoreBackend for /.memories/ route
    # StoreBackend requires runtime, so we create it inside the factory
    def backend_factory(runtime: Runtime) -> CompositeBackend:
        """Create CompositeBackend with StoreBackend for persistent /.memories/ storage."""
        routes = {
            "/.large_tool_results/": large_results_backend,
            "/.conversation_history/": conversation_history_backend,
            "/.memories/": StoreBackend(runtime),  # Persistent cross-thread storage
        }
        return CompositeBackend(
            default=backend,
            routes=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=backend_factory,  # Pass factory instead of direct backend
        middleware=agent_middleware,
        interrupt_on=interrupt_on,
        checkpointer=final_checkpointer,
        store=store,
    ).with_config(config)
    return agent, composite_backend
