"""Agent runtime factory for yaacli.

This module provides factory functions to create AgentRuntime configured
for TUI usage. It wraps the SDK's create_agent() with TUI-specific
configuration and integrates Browser and MCP toolsets.

Example:
    from yaacli.runtime import create_tui_runtime
    from yaacli.browser import BrowserManager
    from yaacli.config import ConfigManager

    config_manager = ConfigManager()
    config = config_manager.load_config()
    mcp_config = config_manager.load_mcp_config()

    async with BrowserManager(config.browser) as browser:
        runtime = create_tui_runtime(
            config=config,
            mcp_config=mcp_config,
            browser_manager=browser,
        )
        async with runtime:
            # Use runtime.agent, runtime.ctx, runtime.env
            pass
"""

from __future__ import annotations

from importlib import resources
from pathlib import Path
from typing import TYPE_CHECKING, Any, cast

from pydantic_ai import DeferredToolRequests, ModelSettings
from pydantic_ai.output import OutputSpec

from ya_agent_sdk.agents.main import AgentRuntime, create_agent
from ya_agent_sdk.context import ModelCapability, ModelConfig, ToolConfig
from ya_agent_sdk.presets import resolve_model_cfg, resolve_model_settings
from ya_agent_sdk.subagents import SubagentConfig, load_subagents_from_dir
from ya_agent_sdk.toolsets.core.base import BaseTool
from ya_agent_sdk.toolsets.core.content import tools as content_tools
from ya_agent_sdk.toolsets.core.context import tools as context_tools
from ya_agent_sdk.toolsets.core.document import tools as document_tools
from ya_agent_sdk.toolsets.core.enhance import tools as enhance_tools
from ya_agent_sdk.toolsets.core.filesystem import tools as filesystem_tools
from ya_agent_sdk.toolsets.core.multimodal import tools as multimodal_tools
from ya_agent_sdk.toolsets.core.shell import tools as shell_tools
from ya_agent_sdk.toolsets.core.subagent import tools as subagent_tools
from ya_agent_sdk.toolsets.core.web import tools as web_tools
from ya_agent_sdk.toolsets.skills.toolset import SHARED_SKILLS_DIR_NAME, SkillToolset
from ya_agent_sdk.toolsets.tool_proxy.toolset import ToolProxyToolset
from ya_agent_sdk.toolsets.tool_search import create_best_strategy
from yaacli.browser import BrowserManager
from yaacli.config import ConfigManager, MCPConfig, SubagentsConfig, YaacliConfig
from yaacli.environment import TUIEnvironment
from yaacli.guards import attach_loop_guard
from yaacli.logging import get_logger
from yaacli.mcp import build_mcp_servers, extract_mcp_descriptions, extract_optional_mcps
from yaacli.session import TUIContext
from yaacli.toolsets.background import background_tools

if TYPE_CHECKING:
    from pydantic_ai.toolsets import AbstractToolset

logger = get_logger(__name__)


def _resolve_model_cfg(model_cfg_input: str | dict[str, Any] | None) -> ModelConfig:
    """Resolve model_cfg from preset name or dict to ModelConfig instance.

    Handles conversion of capabilities from list[str] to set[ModelCapability].

    Args:
        model_cfg_input: Preset name (e.g., 'claude_200k'), dict, or None.

    Returns:
        ModelConfig instance.
    """
    if model_cfg_input is None:
        return ModelConfig()

    # Use SDK's resolve_model_cfg to get dict
    cfg_dict = resolve_model_cfg(model_cfg_input)
    if cfg_dict is None:
        return ModelConfig()

    # Convert capabilities from list[str] to set[ModelCapability] if present
    if "capabilities" in cfg_dict:
        caps = cfg_dict["capabilities"]
        if isinstance(caps, (list, set)):
            cfg_dict["capabilities"] = {ModelCapability(c) if isinstance(c, str) else c for c in caps}

    return ModelConfig(**cfg_dict)


def _load_system_prompt(config: YaacliConfig) -> str:
    """Load system prompt from config or built-in default.

    Priority:
    1. Custom file path from config.general.system_prompt_file
    2. Built-in default from templates/system_prompt.md
    """
    if config.general.system_prompt_file:
        prompt_path = Path(config.general.system_prompt_file).expanduser()
        if prompt_path.exists():
            logger.debug("Loading system prompt from: %s", prompt_path)
            return prompt_path.read_text(encoding="utf-8")
        logger.warning("System prompt file not found: %s, using default", prompt_path)

    # Load built-in default
    template_files = resources.files("yaacli").joinpath("templates")
    default_prompt = template_files.joinpath("system_prompt.md").read_text(encoding="utf-8")
    logger.debug("Using built-in system prompt")
    return default_prompt


def _load_subagent_configs(
    subagents_config: SubagentsConfig,
    config_dir: Path | None = None,
) -> list[SubagentConfig]:
    """Load subagent configs from user config directory.

    Subagents are loaded from ~/.yaacli/subagents/
    and filtered based on the disabled list in config.

    Args:
        subagents_config: Subagents configuration with disabled list and overrides.
        config_dir: Config directory (defaults to ConfigManager.DEFAULT_CONFIG_DIR).

    Returns:
        List of SubagentConfig objects.
    """
    if config_dir is None:
        config_dir = ConfigManager.DEFAULT_CONFIG_DIR

    subagents_dir = config_dir / "subagents"
    if not subagents_dir.exists():
        logger.debug("Subagents directory not found: %s", subagents_dir)
        return []

    # Load all subagents from directory
    all_configs = load_subagents_from_dir(subagents_dir)
    logger.debug("Found %d subagents in %s", len(all_configs), subagents_dir)

    # Filter out disabled subagents
    disabled_set = set(subagents_config.disabled)
    enabled_configs: list[SubagentConfig] = []

    for name, cfg in all_configs.items():
        if name in disabled_set:
            logger.debug("Subagent disabled: %s", name)
            continue

        # Apply overrides if any
        if name in subagents_config.overrides:
            override = subagents_config.overrides[name]
            if override.model is not None:
                cfg = cfg.model_copy(update={"model": override.model})
            if override.model_settings is not None:
                cfg = cfg.model_copy(update={"model_settings": override.model_settings})
            logger.debug("Applied overrides for subagent: %s", name)

        enabled_configs.append(cfg)

    logger.info("Loaded %d subagents (disabled: %d)", len(enabled_configs), len(disabled_set))
    return enabled_configs


def create_tui_runtime(
    config: YaacliConfig,
    mcp_config: MCPConfig | None = None,
    browser_manager: BrowserManager | None = None,
    *,
    working_dir: Path | None = None,
    system_prompt: str | None = None,
) -> AgentRuntime[TUIContext, str | DeferredToolRequests, TUIEnvironment]:
    """Create AgentRuntime configured for TUI.

    This function wraps the SDK's create_agent() with TUI-specific
    configuration, integrating:
    - TUIEnvironment with ProcessManager
    - TUIContext with SteeringManager
    - MCP servers from configuration
    - Browser toolset if available
    - Steering guard for output validation

    Args:
        config: YAACLI CLI configuration.
        mcp_config: MCP server configuration. If None, no MCP servers are added.
        browser_manager: Optional browser manager. If available and started,
            its toolset will be included.
        working_dir: Working directory for the environment. Defaults to cwd.
        system_prompt: Custom system prompt. If None, uses default.

    Returns:
        AgentRuntime configured for TUI usage. Use as async context manager.

    Example:
        runtime = create_tui_runtime(config, mcp_config, browser)
        async with runtime:
            async with stream_agent(runtime, "Hello") as stream:
                async for event in stream:
                    print(event)
    """
    # Collect toolsets
    # Order matters: ToolProxyToolset must be LAST so its two fixed proxy
    # tools (search_tools, call_tool) occupy stable positions at the end
    # of the model's tool list, maximizing prompt cache hit rates.
    toolsets: list[AbstractToolset[Any]] = [
        SkillToolset(toolset_id="skills", extra_dir_names=[SHARED_SKILLS_DIR_NAME]),
    ]

    # Add browser toolset if available (before ToolProxyToolset)
    if browser_manager and browser_manager.is_available:
        browser_toolset = browser_manager.get_browser_toolset()
        if browser_toolset:
            toolsets.append(browser_toolset)
            logger.info("Added browser toolset (cdp_url=%s)", browser_manager.cdp_url)

    # Add MCP servers wrapped in ToolProxyToolset for on-demand invocation.
    # This is intentionally last: proxy tools occupy stable positions at
    # the end of the tool list, keeping all existing tool positions stable.
    if mcp_config:
        mcp_servers = build_mcp_servers(mcp_config, need_approval_mcps=config.tools.need_approval_mcps)
        if mcp_servers:
            mcp_descriptions = extract_mcp_descriptions(mcp_config)
            optional_mcps = extract_optional_mcps(mcp_config)
            mcp_proxy = ToolProxyToolset(
                toolsets=mcp_servers,
                namespace_descriptions=mcp_descriptions if mcp_descriptions else None,
                search_strategy=create_best_strategy(),
                optional_namespaces=optional_mcps if optional_mcps else None,
            )
            toolsets.append(mcp_proxy)
            logger.info(
                "Added %d MCP servers via ToolProxyToolset (descriptions: %d)",
                len(mcp_servers),
                len(mcp_descriptions),
            )

    # Environment configuration
    # Include global config dir in allowed_paths so agent can modify configs directly.
    # Include ~/.agents for shared skills following the Agent Skills open standard.
    # Order matters for skill priority (later = higher priority):
    #   ~/.agents < ~/.yaacli < project dir
    global_config_dir = ConfigManager.DEFAULT_CONFIG_DIR
    # Ensure .gitignore exists in config dir to keep session data out of file tree context
    ConfigManager().ensure_config_dir()
    shared_agents_dir = Path.home() / ".agents"
    env_kwargs: dict[str, Any] = {}
    if working_dir:
        env_kwargs["default_path"] = working_dir
        env_kwargs["allowed_paths"] = [shared_agents_dir, global_config_dir, working_dir]
    else:
        cwd = Path.cwd()
        env_kwargs["default_path"] = cwd
        env_kwargs["allowed_paths"] = [shared_agents_dir, global_config_dir, cwd]

    # Shell environment isolation: configurable via config.toml
    env_kwargs["include_os_env"] = config.include_os_env

    # Model configuration - resolve from preset name or dict
    model_cfg = _resolve_model_cfg(config.general.model_cfg)
    if config.general.model_cfg:
        logger.debug(f"Using model_cfg: {config.general.model_cfg}")

    # Resolve model settings from preset name or dict
    model_settings = resolve_model_settings(config.general.model_settings)
    if model_settings:
        logger.debug(f"Using model settings: {config.general.model_settings} -> {model_settings}")

    # Tool configuration - setup media hooks if S3 is configured
    tool_config_kwargs: dict[str, Any] = {}

    if config.media.s3.enabled and config.media.s3.bucket:
        try:
            from ya_agent_sdk.media import S3MediaConfig, create_s3_media_hook

            s3_config = S3MediaConfig(
                bucket=config.media.s3.bucket,
                region=config.media.s3.region,
                access_key_id=config.media.s3.access_key_id,
                secret_access_key=config.media.s3.secret_access_key,
                endpoint_url=config.media.s3.endpoint_url,
                prefix=config.media.s3.prefix,
                url_mode=config.media.s3.url_mode,
                cdn_base_url=config.media.s3.cdn_base_url,
                presign_expires_seconds=config.media.s3.presign_expires_seconds,
                force_path_style=config.media.s3.force_path_style,
            )
            hook = create_s3_media_hook(s3_config)

            # Bedrock does not support image url, and our image processing is better than vendor
            # TODO: Maybe we can upload after processing in history_processor?
            # tool_config_kwargs["image_to_url_hook"] = hook

            # Use url hook for video, prevent too large requests
            tool_config_kwargs["video_to_url_hook"] = hook
            logger.info(
                "S3 media upload enabled: bucket=%s, url_mode=%s",
                config.media.s3.bucket,
                config.media.s3.url_mode,
            )
        except ImportError:
            logger.warning("S3 media upload requires boto3. Install with: pip install ya-agent-sdk[s3]")

    tool_config = ToolConfig(**tool_config_kwargs)
    if config.tools.need_approval:
        logger.debug("Tools requiring approval: %s", config.tools.need_approval)
    if config.tools.need_approval_mcps:
        logger.debug("MCP servers requiring approval: %s", config.tools.need_approval_mcps)

    # Load subagent configs from user config directory
    subagent_configs = _load_subagent_configs(config.subagents)

    # Core tools
    core_tools: list[type[BaseTool]] = [
        *content_tools,
        *context_tools,
        *document_tools,
        *enhance_tools,
        *filesystem_tools,
        *multimodal_tools,
        *shell_tools,
        *web_tools,
    ]

    # Build final tools list
    all_tools: list[type[BaseTool]] = [
        *core_tools,
        *subagent_tools,  # SubagentInfoTool for introspection
        *background_tools,  # SpawnDelegateTool for async subagent execution
    ]

    # Load system prompt
    effective_system_prompt = system_prompt or _load_system_prompt(config)

    # Output type - SDK's message_bus_guard automatically handles pending messages
    output_type: OutputSpec[str | DeferredToolRequests] = [str, DeferredToolRequests]
    # Create runtime using SDK factory
    # Use unified_subagents=True to create single 'delegate' tool for all subagents
    # output_retries must be large enough to support loop iterations + normal retries
    max_loop = config.general.max_loop_iterations
    output_retries = max_loop + 5

    # Pass config [shell_env] for shell command execution
    extra_ctx_kwargs: dict[str, Any] | None = None
    if config.shell_env:
        extra_ctx_kwargs = {"shell_env": dict(config.shell_env)}

    runtime = create_agent(
        model=config.general.model or None,
        model_settings=cast(ModelSettings, model_settings),
        output_type=output_type,
        env=TUIEnvironment,
        env_kwargs=env_kwargs,
        context_type=TUIContext,
        model_cfg=model_cfg,
        tools=all_tools,
        tool_config=tool_config,
        toolsets=toolsets if toolsets else None,
        system_prompt=effective_system_prompt,
        need_user_approve_tools=config.tools.need_approval or None,
        need_user_approve_mcps=config.tools.need_approval_mcps or None,
        subagent_configs=subagent_configs if subagent_configs else None,
        unified_subagents=True,
        output_retries=output_retries,
        extra_context_kwargs=extra_ctx_kwargs,
    )

    # Attach loop guard for /loop command support
    attach_loop_guard(runtime.agent)

    logger.info(
        "Created TUI runtime: model=%s, toolsets=%d, output_retries=%d",
        config.general.model,
        len(toolsets),
        output_retries,
    )

    return runtime
