"""Configuration, constants, and model creation for the CLI."""

import locale
import os
import re
import sys
import uuid
from dataclasses import dataclass
from pathlib import Path


class NoLLMConfiguredError(Exception):
    """Raised when no LLM provider is configured."""

    pass


def get_package_path(relative_path: str = "") -> Path:
    """Get absolute path to package resource, works for dev and PyInstaller bundle.

    Args:
        relative_path: Path relative to the ieum package directory

    Returns:
        Absolute path to the resource
    """
    if getattr(sys, "frozen", False):
        # Running in PyInstaller bundle
        base_path = Path(sys._MEIPASS) / "ieum"  # noqa: SLF001
    else:
        # Running in normal Python environment
        base_path = Path(__file__).parent
    return base_path / relative_path if relative_path else base_path

import dotenv
from rich.console import Console

from ieum._version import __version__

# Load environment variables with priority: Project .env > User .env > System
# Load in reverse order with override=True so higher priority overrides lower

# 1. System environment variables are already in os.environ

# 2. Load user-level .env (from ~/.ieum/.env) - overrides system
_user_env_path = Path.home() / ".ieum" / ".env"
if _user_env_path.exists():
    dotenv.load_dotenv(_user_env_path, override=True)

# 3. Load project-level .env (from current directory) - overrides user and system
_project_env_path = Path.cwd() / ".env"
if _project_env_path.exists():
    dotenv.load_dotenv(_project_env_path, override=True)
else:
    dotenv.load_dotenv(override=True)  # Try default path

# Fix Windows console encoding for Unicode support
if sys.platform == "win32":
    # Enable UTF-8 mode for Windows console
    if sys.stdout and hasattr(sys.stdout, "reconfigure"):
        try:
            sys.stdout.reconfigure(encoding="utf-8", errors="replace")
        except Exception:
            pass
    if sys.stderr and hasattr(sys.stderr, "reconfigure"):
        try:
            sys.stderr.reconfigure(encoding="utf-8", errors="replace")
        except Exception:
            pass

# CRITICAL: Override LANGSMITH_PROJECT to route agent traces to separate project
# LangSmith reads LANGSMITH_PROJECT at invocation time, so we override it here
# and preserve the user's original value for shell commands
_ieum_project = os.environ.get("IEUM_LANGSMITH_PROJECT")
_original_langsmith_project = os.environ.get("LANGSMITH_PROJECT")
if _ieum_project:
    # Override LANGSMITH_PROJECT for agent traces
    os.environ["LANGSMITH_PROJECT"] = _ieum_project

# Now safe to import LangChain modules
from langchain_core.language_models import BaseChatModel

# Color scheme
COLORS = {
    "primary": "#10b981",
    "dim": "#6b7280",
    "user": "#ffffff",
    "agent": "#10b981",
    "thinking": "#34d399",
    "tool": "#fbbf24",
}

# ASCII art banner - with Windows-safe fallback
_IEUM_ASCII_UNICODE = f"""
██╗███████╗██╗   ██╗███╗   ███╗
██║██╔════╝██║   ██║████╗ ████║
██║█████╗  ██║   ██║██╔████╔██║
██║██╔══╝  ██║   ██║██║╚██╔╝██║
██║███████╗╚██████╔╝██║ ╚═╝ ██║
╚═╝╚══════╝ ╚═════╝ ╚═╝     ╚═╝
                          v{__version__}
"""

_IEUM_ASCII_SIMPLE = f"""
 ___ _____ _   _ __  __
|_ _| ____| | | |  \\/  |
 | ||  _| | | | | |\\/| |
 | || |___| |_| | |  | |
|___|_____|\\___/|_|  |_|
                  v{__version__}
"""


def _supports_cjk() -> bool:
    """Check if terminal likely supports CJK characters.

    Returns:
        True if the terminal encoding supports CJK (Korean, Japanese, Chinese)
        characters, False otherwise.
    """
    try:
        encoding = locale.getpreferredencoding(False).lower().replace("-", "")
    except Exception:
        return False
    return encoding in ("utf8", "euckr", "eucjp", "gb2312", "gbk", "big5")


def _get_ascii_banner() -> str:
    """Get the appropriate ASCII banner for the current platform."""
    # Check if we can safely render Unicode
    if sys.platform == "win32":
        # On Windows, check if the terminal supports Unicode
        try:
            # Try to encode the Unicode banner
            _IEUM_ASCII_UNICODE.encode(sys.stdout.encoding or "utf-8")
            return _IEUM_ASCII_UNICODE
        except (UnicodeEncodeError, LookupError, AttributeError):
            return _IEUM_ASCII_SIMPLE
    return _IEUM_ASCII_UNICODE


IEUM_ASCII = _get_ascii_banner()

# Interactive commands - uses i18n for translations
from ieum.i18n import t as _t


def _get_commands() -> dict[str, str]:
    """Build command descriptions from i18n translations."""
    return {
        "help": _t("config.commands.help"),
        "new": _t("config.commands.new"),
        "resume": _t("config.commands.resume"),
        "clear": _t("config.commands.clear"),
        "remember": _t("config.commands.remember"),
        "skills": _t("config.commands.skills"),
        "skill": _t("config.commands.skill"),
        "models": _t("config.commands.models"),
        "model": _t("config.commands.model"),
        "ralph-loop": _t("config.commands.ralph_loop"),
        "tokens": _t("config.commands.tokens"),
        "threads": _t("config.commands.threads"),
        "version": _t("config.commands.version"),
        "quit": _t("config.commands.quit"),
        "exit": _t("config.commands.exit"),
    }


COMMANDS = _get_commands()


# Maximum argument length for display
MAX_ARG_LENGTH = 150

# Agent configuration
config = {"recursion_limit": 1000}

# Rich console instance
# On Windows, disable legacy mode to support Unicode characters
console = Console(
    highlight=False,
    legacy_windows=False if sys.platform == "win32" else None,
    force_terminal=True if sys.platform == "win32" else None,
)


def _find_project_root(start_path: Path | None = None) -> Path | None:
    """Find the project root by looking for .git directory.

    Walks up the directory tree from start_path (or cwd) looking for a .git
    directory, which indicates the project root.

    Args:
        start_path: Directory to start searching from. Defaults to current working directory.

    Returns:
        Path to the project root if found, None otherwise.
    """
    current = Path(start_path or Path.cwd()).resolve()

    # Walk up the directory tree
    for parent in [current, *list(current.parents)]:
        git_dir = parent / ".git"
        if git_dir.exists():
            return parent

    return None


def _find_project_agent_md(project_root: Path) -> list[Path]:
    """Find project-specific AGENTS.md file(s).

    Checks two locations and returns ALL that exist:
    1. project_root/.ieum/AGENTS.md
    2. project_root/AGENTS.md

    Both files will be loaded and combined if both exist.

    Args:
        project_root: Path to the project root directory.

    Returns:
        List of paths to project AGENTS.md files (may contain 0, 1, or 2 paths).
    """
    paths = []

    # Check .ieum/AGENTS.md (preferred)
    ieum_md = project_root / ".ieum" / "AGENTS.md"
    if ieum_md.exists():
        paths.append(ieum_md)

    # Check root AGENTS.md (fallback, but also include if both exist)
    root_md = project_root / "AGENTS.md"
    if root_md.exists():
        paths.append(root_md)

    return paths


@dataclass
class Settings:
    """Global settings and environment detection for ieum.

    This class is initialized once at startup and provides access to:
    - Local LLM server configuration (Ollama, LMStudio, vLLM)
    - Current project information
    - Tool availability (e.g., Tavily)
    - Observability (Langfuse)
    - File system paths

    Attributes:
        project_root: Current project root directory (if in a git project)
        ollama_base_url: Ollama server base URL
        lmstudio_base_url: LMStudio server base URL
        vllm_base_url: vLLM server base URL
    """

    # Local LLM configuration (Ollama, LMStudio, vLLM)
    ollama_base_url: str | None = None
    lmstudio_base_url: str | None = None
    vllm_base_url: str | None = None

    # Cloud LLM API keys
    openai_api_key: str | None = None
    anthropic_api_key: str | None = None
    google_api_key: str | None = None
    groq_api_key: str | None = None
    mistral_api_key: str | None = None
    cohere_api_key: str | None = None
    nvidia_api_key: str | None = None
    openrouter_api_key: str | None = None
    litellm_api_key: str | None = None
    deepseek_api_key: str | None = None
    moonshot_api_key: str | None = None
    qwen_api_key: str | None = None  # Also used for tongyi
    zhipu_api_key: str | None = None
    upstage_api_key: str | None = None
    friendli_api_key: str | None = None

    # Tool API keys
    tavily_api_key: str | None = None

    # LangSmith configuration (observability)
    langchain_api_key: str | None = None  # LANGCHAIN_API_KEY for LangSmith
    langchain_tracing_v2: bool = False  # LANGCHAIN_TRACING_V2=true enables tracing
    langchain_project: str | None = None  # LANGCHAIN_PROJECT for project name
    ieum_langchain_project: str | None = None  # For IEUM agent tracing (override via IEUM_LANGSMITH_PROJECT)
    user_langchain_project: str | None = None  # Original LANGSMITH_PROJECT for user code

    # Langfuse configuration (observability)
    langfuse_public_key: str | None = None
    langfuse_secret_key: str | None = None
    langfuse_host: str | None = None  # Default: https://cloud.langfuse.com (or LANGFUSE_BASE_URL)

    # Model configuration
    model_name: str | None = None  # Currently active model name
    model_provider: str | None = None  # Provider (ollama, lmstudio, vllm)

    # Project information
    project_root: Path | None = None

    @classmethod
    def from_environment(cls, *, start_path: Path | None = None) -> "Settings":
        """Create settings by detecting the current environment.

        Args:
            start_path: Directory to start project detection from (defaults to cwd)

        Returns:
            Settings instance with detected configuration
        """
        # Detect local LLM servers (Ollama, LMStudio, vLLM)
        # These use OpenAI-compatible APIs
        ollama_base_url = os.environ.get("OLLAMA_BASE_URL")
        lmstudio_base_url = os.environ.get("LMSTUDIO_BASE_URL")
        vllm_base_url = os.environ.get("VLLM_BASE_URL")

        # Cloud LLM API keys
        openai_api_key = os.environ.get("OPENAI_API_KEY")
        anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY")
        google_api_key = os.environ.get("GOOGLE_API_KEY")
        groq_api_key = os.environ.get("GROQ_API_KEY")
        mistral_api_key = os.environ.get("MISTRAL_API_KEY")
        cohere_api_key = os.environ.get("COHERE_API_KEY") or os.environ.get("CO_API_KEY")
        nvidia_api_key = os.environ.get("NVIDIA_API_KEY")
        openrouter_api_key = os.environ.get("OPENROUTER_API_KEY")
        litellm_api_key = os.environ.get("LITELLM_API_KEY")
        deepseek_api_key = os.environ.get("DEEPSEEK_API_KEY")
        moonshot_api_key = os.environ.get("MOONSHOT_API_KEY")
        qwen_api_key = os.environ.get("DASHSCOPE_API_KEY") or os.environ.get("QWEN_API_KEY")
        zhipu_api_key = os.environ.get("ZHIPUAI_API_KEY") or os.environ.get("ZHIPU_API_KEY")
        upstage_api_key = os.environ.get("UPSTAGE_API_KEY")
        friendli_api_key = os.environ.get("FRIENDLI_TOKEN") or os.environ.get("FRIENDLI_API_KEY")

        # Tool API keys
        tavily_key = os.environ.get("TAVILY_API_KEY")

        # Detect LangSmith configuration
        langchain_api_key = os.environ.get("LANGCHAIN_API_KEY")
        langchain_tracing_v2 = os.environ.get("LANGCHAIN_TRACING_V2", "").lower() == "true"
        langchain_project = os.environ.get("LANGCHAIN_PROJECT")
        ieum_langchain_project = os.environ.get("IEUM_LANGSMITH_PROJECT")
        user_langchain_project = _original_langsmith_project

        # Detect Langfuse configuration (observability)
        langfuse_public_key = os.environ.get("LANGFUSE_PUBLIC_KEY")
        langfuse_secret_key = os.environ.get("LANGFUSE_SECRET_KEY")
        # Support LANGFUSE_BASE_URL (recommended), LANGFUSE_HOST, and LANGFUSE_BASEURL
        langfuse_host = (
            os.environ.get("LANGFUSE_BASE_URL")
            or os.environ.get("LANGFUSE_HOST")
            or os.environ.get("LANGFUSE_BASEURL")
        )

        # Detect project
        project_root = _find_project_root(start_path)

        return cls(
            ollama_base_url=ollama_base_url,
            lmstudio_base_url=lmstudio_base_url,
            vllm_base_url=vllm_base_url,
            openai_api_key=openai_api_key,
            anthropic_api_key=anthropic_api_key,
            google_api_key=google_api_key,
            groq_api_key=groq_api_key,
            mistral_api_key=mistral_api_key,
            cohere_api_key=cohere_api_key,
            nvidia_api_key=nvidia_api_key,
            openrouter_api_key=openrouter_api_key,
            litellm_api_key=litellm_api_key,
            deepseek_api_key=deepseek_api_key,
            moonshot_api_key=moonshot_api_key,
            qwen_api_key=qwen_api_key,
            zhipu_api_key=zhipu_api_key,
            upstage_api_key=upstage_api_key,
            friendli_api_key=friendli_api_key,
            tavily_api_key=tavily_key,
            langchain_api_key=langchain_api_key,
            langchain_tracing_v2=langchain_tracing_v2,
            langchain_project=langchain_project,
            ieum_langchain_project=ieum_langchain_project,
            user_langchain_project=user_langchain_project,
            langfuse_public_key=langfuse_public_key,
            langfuse_secret_key=langfuse_secret_key,
            langfuse_host=langfuse_host,
            project_root=project_root,
        )

    @property
    def has_ollama(self) -> bool:
        """Check if Ollama is configured (base URL set)."""
        return self.ollama_base_url is not None

    @property
    def has_lmstudio(self) -> bool:
        """Check if LMStudio is configured (base URL set)."""
        return self.lmstudio_base_url is not None

    @property
    def has_vllm(self) -> bool:
        """Check if vLLM is configured (base URL set)."""
        return self.vllm_base_url is not None

    def has_provider(self, provider: str) -> bool:
        """Check if a specific provider is configured."""
        key_map = {
            "ollama": self.ollama_base_url,
            "lmstudio": self.lmstudio_base_url,
            "vllm": self.vllm_base_url,
            "openai": self.openai_api_key,
            "anthropic": self.anthropic_api_key,
            "google": self.google_api_key,
            "groq": self.groq_api_key,
            "mistral": self.mistral_api_key,
            "cohere": self.cohere_api_key,
            "nvidia": self.nvidia_api_key,
            "openrouter": self.openrouter_api_key,
            "litellm": self.litellm_api_key,
            "deepseek": self.deepseek_api_key,
            "moonshot": self.moonshot_api_key,
            "qwen": self.qwen_api_key,
            "tongyi": self.qwen_api_key,  # Same as qwen
            "zhipu": self.zhipu_api_key,
            "upstage": self.upstage_api_key,
            "friendli": self.friendli_api_key,
        }
        return key_map.get(provider) is not None

    @property
    def has_openrouter(self) -> bool:
        """Check if OpenRouter is configured (API key set)."""
        return self.openrouter_api_key is not None

    @property
    def has_local_llm(self) -> bool:
        """Check if any local LLM server is configured."""
        return self.ollama_base_url is not None or self.lmstudio_base_url is not None or self.vllm_base_url is not None

    @property
    def has_cloud_llm(self) -> bool:
        """Check if any cloud LLM provider is configured."""
        return any([
            self.openai_api_key, self.anthropic_api_key, self.google_api_key,
            self.groq_api_key, self.mistral_api_key, self.cohere_api_key,
            self.nvidia_api_key, self.openrouter_api_key, self.litellm_api_key,
            self.deepseek_api_key, self.moonshot_api_key, self.qwen_api_key,
            self.zhipu_api_key, self.upstage_api_key, self.friendli_api_key,
        ])

    @property
    def has_any_llm(self) -> bool:
        """Check if any LLM provider is configured (local or cloud)."""
        return self.has_local_llm or self.has_cloud_llm

    @property
    def has_tavily(self) -> bool:
        """Check if Tavily API key is configured."""
        return self.tavily_api_key is not None

    @property
    def has_langsmith(self) -> bool:
        """Check if LangSmith is configured (requires API key)."""
        return self.langchain_api_key is not None

    @property
    def has_ieum_langchain_project(self) -> bool:
        """Check if IEUM LangChain project name is configured."""
        return self.ieum_langchain_project is not None

    @property
    def has_langfuse(self) -> bool:
        """Check if Langfuse is configured (requires public and secret keys)."""
        return self.langfuse_public_key is not None and self.langfuse_secret_key is not None

    @property
    def has_project(self) -> bool:
        """Check if currently in a git project."""
        return self.project_root is not None

    @property
    def user_ieum_dir(self) -> Path:
        """Get the base user-level .ieum directory.

        Returns:
            Path to ~/.ieum
        """
        return Path.home() / ".ieum"

    def get_user_agent_md_path(self, agent_name: str) -> Path:
        """Get user-level AGENTS.md path for a specific agent.

        Returns path regardless of whether the file exists.

        Args:
            agent_name: Name of the agent

        Returns:
            Path to ~/.ieum/{agent_name}/AGENTS.md
        """
        return Path.home() / ".ieum" / agent_name / "AGENTS.md"

    def get_project_agent_md_path(self) -> Path | None:
        """Get project-level AGENTS.md path.

        Returns path regardless of whether the file exists.

        Returns:
            Path to {project_root}/.ieum/AGENTS.md, or None if not in a project
        """
        if not self.project_root:
            return None
        return self.project_root / ".ieum" / "AGENTS.md"

    @staticmethod
    def _is_valid_agent_name(agent_name: str) -> bool:
        """Validate prevent invalid filesystem paths and security issues."""
        if not agent_name or not agent_name.strip():
            return False
        # Allow only alphanumeric, hyphens, underscores, and whitespace
        return bool(re.match(r"^[a-zA-Z0-9_\-\s]+$", agent_name))

    def get_agent_dir(self, agent_name: str) -> Path:
        """Get the global agent directory path.

        Args:
            agent_name: Name of the agent

        Returns:
            Path to ~/.ieum/{agent_name}
        """
        if not self._is_valid_agent_name(agent_name):
            msg = (
                f"Invalid agent name: {agent_name!r}. "
                "Agent names can only contain letters, numbers, hyphens, underscores, and spaces."
            )
            raise ValueError(msg)
        return Path.home() / ".ieum" / agent_name

    def ensure_agent_dir(self, agent_name: str) -> Path:
        """Ensure the global agent directory exists and return its path.

        Args:
            agent_name: Name of the agent

        Returns:
            Path to ~/.ieum/{agent_name}
        """
        if not self._is_valid_agent_name(agent_name):
            msg = (
                f"Invalid agent name: {agent_name!r}. "
                "Agent names can only contain letters, numbers, hyphens, underscores, and spaces."
            )
            raise ValueError(msg)
        agent_dir = self.get_agent_dir(agent_name)
        agent_dir.mkdir(parents=True, exist_ok=True)
        return agent_dir

    def ensure_project_ieum_dir(self) -> Path | None:
        """Ensure the project .ieum directory exists and return its path.

        Returns:
            Path to project .ieum directory, or None if not in a project
        """
        if not self.project_root:
            return None

        project_ieum_dir = self.project_root / ".ieum"
        project_ieum_dir.mkdir(parents=True, exist_ok=True)
        return project_ieum_dir

    def get_user_skills_dir(self, agent_name: str) -> Path:
        """Get user-level skills directory path for a specific agent.

        Args:
            agent_name: Name of the agent

        Returns:
            Path to ~/.ieum/{agent_name}/skills/
        """
        return self.get_agent_dir(agent_name) / "skills"

    def ensure_user_skills_dir(self, agent_name: str) -> Path:
        """Ensure user-level skills directory exists and return its path.

        Args:
            agent_name: Name of the agent

        Returns:
            Path to ~/.ieum/{agent_name}/skills/
        """
        skills_dir = self.get_user_skills_dir(agent_name)
        skills_dir.mkdir(parents=True, exist_ok=True)
        return skills_dir

    def get_project_skills_dir(self) -> Path | None:
        """Get project-level skills directory path.

        Returns:
            Path to {project_root}/.ieum/skills/, or None if not in a project
        """
        if not self.project_root:
            return None
        return self.project_root / ".ieum" / "skills"

    def ensure_project_skills_dir(self) -> Path | None:
        """Ensure project-level skills directory exists and return its path.

        Returns:
            Path to {project_root}/.ieum/skills/, or None if not in a project
        """
        if not self.project_root:
            return None
        skills_dir = self.get_project_skills_dir()
        skills_dir.mkdir(parents=True, exist_ok=True)
        return skills_dir

    @property
    def default_model(self) -> str | None:
        """Get the default model name from environment."""
        if self.has_ollama and os.environ.get("OLLAMA_MODEL"):
            return f"ollama/{os.environ.get('OLLAMA_MODEL')}"
        if self.has_lmstudio and os.environ.get("LMSTUDIO_MODEL"):
            return f"lmstudio/{os.environ.get('LMSTUDIO_MODEL')}"
        if self.has_vllm and os.environ.get("VLLM_MODEL"):
            return f"vllm/{os.environ.get('VLLM_MODEL')}"
        return None


# Global settings instance
settings = Settings.from_environment()


def reload_settings() -> None:
    """Reload settings from environment variables.

    Call this after changing environment variables with /env set
    to apply changes without restarting.
    """
    global settings
    settings = Settings.from_environment()


class SessionState:
    """Holds mutable session state (auto-approve mode, etc)."""

    def __init__(self, auto_approve: bool = False, no_splash: bool = False) -> None:
        self.auto_approve = auto_approve
        self.no_splash = no_splash
        self.exit_hint_until: float | None = None
        self.exit_hint_handle = None
        self.thread_id = str(uuid.uuid4())

    def toggle_auto_approve(self) -> bool:
        """Toggle auto-approve and return new state."""
        self.auto_approve = not self.auto_approve
        return self.auto_approve


def get_default_coding_instructions() -> str:
    """Get the default coding agent instructions.

    These are the immutable base instructions that cannot be modified by the agent.
    Long-term memory (AGENTS.md) is handled separately by the middleware.
    """
    default_prompt_path = get_package_path("default_agent_prompt.md")
    return default_prompt_path.read_text(encoding="utf-8")


def get_client_info(environment: str = "TUI") -> str:
    """Get client environment information for system prompt.

    Args:
        environment: Client environment type - "TUI" (terminal) or "WEB" (browser)

    Returns:
        Formatted string with OS and environment info for system prompt
    """
    import platform

    os_name = platform.system()  # Windows, Linux, Darwin
    os_version = platform.release()
    os_arch = platform.machine()  # x86_64, arm64, etc.

    # Map OS names to more readable versions
    os_display = {
        "Windows": "Windows",
        "Darwin": "macOS",
        "Linux": "Linux",
    }.get(os_name, os_name)

    env_display = {
        "TUI": _t("config.client.env_tui"),
        "WEB": _t("config.client.env_web"),
    }.get(environment, environment)

    title = _t("config.client.title")
    os_label = _t("config.client.os")
    env_label = _t("config.client.env")
    python_label = _t("config.client.python")

    return f"""{title}

- {os_label}: {os_display} {os_version} ({os_arch})
- {env_label}: {env_display}
- {python_label}: {platform.python_version()}
"""


# All supported providers with their prefixes
PROVIDERS = [
    "ollama", "lmstudio", "vllm",  # Local
    "openai", "anthropic", "google", "groq", "mistral", "cohere", "nvidia",  # Major cloud
    "openrouter", "litellm",  # Aggregators
    "deepseek", "moonshot", "qwen", "tongyi", "zhipu", "upstage", "friendli",  # Others
]


def _detect_provider(model_name: str) -> str | None:
    """Auto-detect provider from model name.

    Args:
        model_name: Model name to detect provider from

    Returns:
        Provider name or None

    Examples:
        - "ollama/llama3" -> "ollama"
        - "anthropic/claude-3" -> "anthropic"
        - "openrouter/anthropic/claude-3.5-sonnet" -> "openrouter"
        - "llama3" -> None (no prefix, will use default provider)
    """
    model_lower = model_name.lower()

    for provider in PROVIDERS:
        if model_lower.startswith(f"{provider}/") or model_lower.startswith(f"{provider}:"):
            return provider

    return None


def _extract_model_name(model_name: str) -> str:
    """Extract the actual model name from a prefixed model string.

    Args:
        model_name: Model name with provider prefix (e.g., "ollama/llama3")

    Returns:
        The model name without the provider prefix (e.g., "llama3")
    """
    for provider in PROVIDERS:
        for sep in ["/", ":"]:
            prefix = f"{provider}{sep}"
            if model_name.lower().startswith(prefix):
                return model_name[len(prefix):]
    return model_name


# Keep old name for backwards compatibility
_extract_local_model_name = _extract_model_name


def _get_ollama_context_length(model_name: str, base_url: str) -> int | None:
    """Fetch context length from Ollama API.

    Args:
        model_name: The model name (without provider prefix)
        base_url: Ollama base URL (may include /v1 suffix)

    Returns:
        Context length in tokens, or None if unavailable
    """
    import requests

    try:
        # Remove /v1 suffix if present - Ollama /api/show is at the root
        api_base = base_url.rstrip("/")
        if api_base.endswith("/v1"):
            api_base = api_base[:-3]

        # Ollama /api/show endpoint returns model info
        resp = requests.post(
            f"{api_base}/api/show",
            json={"name": model_name},
            timeout=5,
        )
        if resp.status_code == 200:
            data = resp.json()
            model_info = data.get("model_info", {})
            # Context length is stored as {architecture}.context_length
            for key, value in model_info.items():
                if key.endswith(".context_length") and isinstance(value, int):
                    return value
    except Exception:
        pass
    return None


def create_model(model_name_override: str | None = None, *, update_settings: bool = True) -> BaseChatModel:
    """Create the appropriate model based on local LLM configuration.

    Uses ChatOpenAI with OpenAI-compatible API for all local LLM servers.

    Args:
        model_name_override: Optional model name to use instead of environment variable.
            Use prefixes: ollama/model, lmstudio/model, vllm/model

    Returns:
        ChatOpenAI instance configured for the local LLM server

    Raises:
        SystemExit if no LLM server is configured or model provider can't be determined

    Examples:
        create_model("ollama/llama3")    # Ollama
        create_model("lmstudio/qwen2")   # LMStudio
        create_model("vllm/mistral")     # vLLM
    """
    # Determine provider and model
    if model_name_override:
        # Use provided model, auto-detect provider
        provider = _detect_provider(model_name_override)
        if not provider:
            console.print(
                f"[bold red]Error:[/bold red] Could not detect provider from model name: {model_name_override}"
            )
            console.print("\nSupported providers: " + ", ".join(PROVIDERS))
            console.print("\nExamples:")
            console.print("  ieum --model ollama/llama3")
            console.print("  ieum --model openai/gpt-4o")
            console.print("  ieum --model anthropic/claude-sonnet-4")
            console.print("  ieum --model openrouter/anthropic/claude-sonnet-4")
            sys.exit(1)

        # Check if provider is configured
        if not settings.has_provider(provider):
            env_vars = {
                "ollama": "OLLAMA_BASE_URL",
                "lmstudio": "LMSTUDIO_BASE_URL",
                "vllm": "VLLM_BASE_URL",
                "openai": "OPENAI_API_KEY",
                "anthropic": "ANTHROPIC_API_KEY",
                "google": "GOOGLE_API_KEY",
                "groq": "GROQ_API_KEY",
                "mistral": "MISTRAL_API_KEY",
                "cohere": "COHERE_API_KEY",
                "nvidia": "NVIDIA_API_KEY",
                "openrouter": "OPENROUTER_API_KEY",
                "litellm": "LITELLM_API_KEY",
                "deepseek": "DEEPSEEK_API_KEY",
                "moonshot": "MOONSHOT_API_KEY",
                "qwen": "DASHSCOPE_API_KEY",
                "tongyi": "DASHSCOPE_API_KEY",
                "zhipu": "ZHIPUAI_API_KEY",
                "upstage": "UPSTAGE_API_KEY",
                "friendli": "FRIENDLI_TOKEN",
            }
            env_var = env_vars.get(provider, f"{provider.upper()}_API_KEY")
            console.print(f"[bold red]Error:[/bold red] Provider '{provider}' requires {env_var}")
            sys.exit(1)

        model_name = model_name_override
    # Use environment variable defaults (check in priority order)
    # Local LLM servers first
    elif settings.has_provider("ollama") and os.environ.get("OLLAMA_MODEL"):
        provider = "ollama"
        model_name = f"ollama/{os.environ.get('OLLAMA_MODEL')}"
    elif settings.has_provider("lmstudio") and os.environ.get("LMSTUDIO_MODEL"):
        provider = "lmstudio"
        model_name = f"lmstudio/{os.environ.get('LMSTUDIO_MODEL')}"
    elif settings.has_provider("vllm") and os.environ.get("VLLM_MODEL"):
        provider = "vllm"
        model_name = f"vllm/{os.environ.get('VLLM_MODEL')}"
    # Cloud providers
    elif settings.has_provider("openai") and os.environ.get("OPENAI_MODEL"):
        provider = "openai"
        model_name = f"openai/{os.environ.get('OPENAI_MODEL')}"
    elif settings.has_provider("anthropic") and os.environ.get("ANTHROPIC_MODEL"):
        provider = "anthropic"
        model_name = f"anthropic/{os.environ.get('ANTHROPIC_MODEL')}"
    elif settings.has_provider("openrouter") and os.environ.get("OPENROUTER_MODEL"):
        provider = "openrouter"
        model_name = f"openrouter/{os.environ.get('OPENROUTER_MODEL')}"
    # Fallback: provider has API key but no *_MODEL env var → use default model
    elif settings.has_provider("anthropic"):
        provider = "anthropic"
        model_name = "anthropic/claude-sonnet-4-5-20250929"
    elif settings.has_provider("openai"):
        provider = "openai"
        model_name = "openai/gpt-4o"
    elif settings.has_provider("google"):
        provider = "google"
        model_name = "google/gemini-2.0-flash"
    elif settings.has_provider("groq"):
        provider = "groq"
        model_name = "groq/llama-3.3-70b-versatile"
    elif settings.has_provider("deepseek"):
        provider = "deepseek"
        model_name = "deepseek/deepseek-chat"
    elif settings.has_provider("openrouter"):
        provider = "openrouter"
        model_name = "openrouter/anthropic/claude-sonnet-4-5-20250929"
    elif settings.has_provider("mistral"):
        provider = "mistral"
        model_name = "mistral/mistral-large-latest"
    elif settings.has_provider("ollama"):
        provider = "ollama"
        model_name = "ollama/llama3"
    else:
        raise NoLLMConfiguredError(
            "No LLM configured. Use /env to set API keys or run with --model option."
        )

    # Store model info in settings for display (only for main model, not subagents)
    if update_settings:
        settings.model_name = model_name
        settings.model_provider = provider

    actual_model = _extract_model_name(model_name)

    # === Local LLM providers (OpenAI-compatible API) ===
    if provider == "ollama":
        from langchain_openai import ChatOpenAI
        base = settings.ollama_base_url.rstrip("/")
        ollama_url = base if base.endswith("/v1") else base + "/v1"
        context_length = _get_ollama_context_length(actual_model, settings.ollama_base_url)
        profile = {"max_input_tokens": context_length} if context_length else None
        return ChatOpenAI(
            model=actual_model, base_url=ollama_url, api_key="ollama", temperature=0, profile=profile
        )

    if provider == "lmstudio":
        from langchain_openai import ChatOpenAI
        base = settings.lmstudio_base_url.rstrip("/")
        lmstudio_url = base if base.endswith("/v1") else base + "/v1"
        return ChatOpenAI(
            model=actual_model, base_url=lmstudio_url, api_key="lmstudio", temperature=0
        )

    if provider == "vllm":
        from langchain_openai import ChatOpenAI
        base = settings.vllm_base_url.rstrip("/")
        vllm_url = base if base.endswith("/v1") else base + "/v1"
        return ChatOpenAI(
            model=actual_model, base_url=vllm_url,
            api_key=os.environ.get("VLLM_API_KEY", "vllm"), temperature=0
        )

    # === Cloud LLM providers ===
    if provider == "openai":
        from langchain_openai import ChatOpenAI
        return ChatOpenAI(model=actual_model, api_key=settings.openai_api_key, temperature=0)

    if provider == "anthropic":
        from langchain_anthropic import ChatAnthropic
        return ChatAnthropic(model=actual_model, api_key=settings.anthropic_api_key, temperature=0)

    if provider == "google":
        from langchain_google_genai import ChatGoogleGenerativeAI
        return ChatGoogleGenerativeAI(model=actual_model, google_api_key=settings.google_api_key, temperature=0)

    if provider == "groq":
        from langchain_groq import ChatGroq
        return ChatGroq(model=actual_model, api_key=settings.groq_api_key, temperature=0)

    if provider == "mistral":
        from langchain_mistralai import ChatMistralAI
        return ChatMistralAI(model=actual_model, api_key=settings.mistral_api_key, temperature=0)

    if provider == "cohere":
        from langchain_cohere import ChatCohere
        return ChatCohere(model=actual_model, cohere_api_key=settings.cohere_api_key, temperature=0)

    if provider == "nvidia":
        from langchain_nvidia_ai_endpoints import ChatNVIDIA
        return ChatNVIDIA(model=actual_model, api_key=settings.nvidia_api_key, temperature=0)

    if provider == "deepseek":
        from langchain_openai import ChatOpenAI
        return ChatOpenAI(
            model=actual_model, base_url="https://api.deepseek.com/v1",
            api_key=settings.deepseek_api_key, temperature=0
        )

    if provider == "moonshot":
        from langchain_openai import ChatOpenAI
        return ChatOpenAI(
            model=actual_model, base_url="https://api.moonshot.cn/v1",
            api_key=settings.moonshot_api_key, temperature=0
        )

    if provider in ("qwen", "tongyi"):
        from langchain_community.chat_models.tongyi import ChatTongyi
        return ChatTongyi(model=actual_model, dashscope_api_key=settings.qwen_api_key, temperature=0)

    if provider == "zhipu":
        from langchain_community.chat_models import ChatZhipuAI
        return ChatZhipuAI(model=actual_model, api_key=settings.zhipu_api_key, temperature=0)

    if provider == "upstage":
        from langchain_upstage import ChatUpstage
        return ChatUpstage(model=actual_model, api_key=settings.upstage_api_key, temperature=0)

    if provider == "friendli":
        from langchain_community.chat_models.friendli import ChatFriendli
        return ChatFriendli(model=actual_model, friendli_token=settings.friendli_api_key, temperature=0)

    if provider == "litellm":
        from langchain_community.chat_models.litellm import ChatLiteLLM
        return ChatLiteLLM(model=actual_model, api_key=settings.litellm_api_key, temperature=0)

    # === Aggregators ===
    if provider == "openrouter":
        from langchain_openai import ChatOpenAI
        return ChatOpenAI(
            model=actual_model, base_url="https://openrouter.ai/api/v1",
            api_key=settings.openrouter_api_key, temperature=0
        )

    # Should never reach here
    console.print(f"[bold red]Error:[/bold red] Unknown provider: {provider}")
    sys.exit(1)


def validate_model_capabilities(model: BaseChatModel, model_name: str) -> None:
    """Validate that the model has required capabilities for IEUM.

    Checks the model's profile (if available) to ensure it supports tool calling, which
    is required for agent functionality. Issues warnings for models without profiles or
    with limited context windows.

    Args:
        model: The instantiated model to validate.
        model_name: Model name for error/warning messages.

    Raises:
        SystemExit: If model profile explicitly indicates `tool_calling=False`.

    Note:
        This validation is best-effort. Models without profiles will pass with a warning.
    """
    profile = getattr(model, "profile", None)

    if profile is None:
        # Model doesn't have profile data - warn but allow
        console.print(
            f"[dim][yellow]Note:[/yellow] No capability profile for '{model_name}'. "
            "Cannot verify tool calling support.[/dim]"
        )
        return

    if not isinstance(profile, dict):
        return

    # Check required capability: tool_calling
    tool_calling = profile.get("tool_calling")
    if tool_calling is False:
        console.print(
            f"[bold red]Error:[/bold red] Model '{model_name}' does not support tool calling."
        )
        console.print(
            "\nDeep Agents requires tool calling for agent functionality. "
            "Please choose a model that supports tool calling."
        )
        console.print("\nSee MODELS.md for supported models.")
        sys.exit(1)

    # Warn about potentially limited context (< 8k tokens)
    max_input_tokens = profile.get("max_input_tokens")
    if max_input_tokens and max_input_tokens < 8000:  # noqa: PLR2004
        console.print(
            f"[dim][yellow]Warning:[/yellow] Model '{model_name}' has limited context "
            f"({max_input_tokens:,} tokens). Agent performance may be affected.[/dim]"
        )
