"""
LLM Profile Manager.

Manages named LLM configuration profiles that define:
- Provider type (OpenAI, Anthropic, Azure, Custom)
- Model name
- Temperature and other parameters
- API key

Profiles can be defined in config.json under core.llm.profiles.
"""

import json
import os
import re
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)


@dataclass
class EnvVarHint:
    """Information about a profile's env var."""
    name: str       # e.g., "KOLLABOR_CLAUDE_TOKEN"
    is_set: bool    # True if env var exists and is non-empty


@dataclass
class LLMProfile:
    """
    Configuration profile for LLM settings.

    Attributes:
        name: Profile identifier
        provider: Provider type ("openai", "anthropic", "azure_openai", "custom")
        model: Model name/identifier
        temperature: Sampling temperature (0.0-1.0)
        max_tokens: Maximum tokens to generate (None = no limit)
        timeout: Request timeout in milliseconds (0 = no timeout)
        description: Human-readable description
        extra_headers: Additional HTTP headers to include
        api_key: API key for provider

    API keys are resolved via environment variables using pattern:
    KOLLABOR_{PROFILE_NAME}_API_KEY (e.g., KOLLABOR_CLAUDE_API_KEY)
    or stored directly in config.json.
    """

    name: str
    provider: str  # "openai", "anthropic", "azure_openai", "custom"
    model: str = ""
    temperature: float = 0.7
    max_tokens: Optional[int] = None
    timeout: int = 0
    description: str = ""
    extra_headers: Dict[str, str] = field(default_factory=dict)
    api_key: str = field(default="", repr=False)
    base_url: str = ""  # For custom providers
    top_p: Optional[float] = None  # Nucleus sampling (0.0-1.0)
    streaming: bool = True  # Enable streaming responses
    supports_tools: bool = True  # Enable tool/function calling

    def _get_env_key(self, field: str) -> str:
        """Generate env var key for this profile and field.

        Normalizes profile name: strip whitespace, all non-alphanumeric chars become underscore.
        Examples:
            my-local-llm -> KOLLABOR_MY_LOCAL_LLM_{FIELD}
            my.profile   -> KOLLABOR_MY_PROFILE_{FIELD}
            My Profile!  -> KOLLABOR_MY_PROFILE__{FIELD}
            "  fast  "   -> KOLLABOR_FAST_{FIELD}
        """
        # Strip whitespace, replace all non-alphanumeric with underscore, then uppercase
        name_stripped = self.name.strip()
        name_normalized = re.sub(r'[^a-zA-Z0-9]', '_', name_stripped).upper()
        return f"KOLLABOR_{name_normalized}_{field}"

    def _get_env_value(self, field: str) -> Optional[str]:
        """Get env var value, treating empty/whitespace-only as unset.

        Returns:
            The env var value if set and non-empty, None otherwise.
            Note: "0" is a valid value and will be returned (not treated as falsy).
        """
        env_key = self._get_env_key(field)
        env_val = os.environ.get(env_key)
        # Check for None (unset) or empty/whitespace-only
        if env_val is None or not env_val.strip():
            return None
        return env_val





    def get_model(self) -> str:
        """Get model, checking env var first. REQUIRED field."""
        env_val = self._get_env_value("MODEL")
        if env_val:
            return env_val
        if self.model:
            return self.model
        # Both sources empty - warn user
        logger.warning(f"Profile '{self.name}': No model configured. "
                       f"Set {self._get_env_key('MODEL')} or configure in config.json")
        return ""

    def get_max_tokens(self) -> Optional[int]:
        """Get max tokens, checking env var first. OPTIONAL field."""
        env_key = self._get_env_key("MAX_TOKENS")
        env_val = self._get_env_value("MAX_TOKENS")
        if env_val:
            try:
                return int(env_val)
            except ValueError:
                logger.warning(f"Profile '{self.name}': {env_key}='{env_val}' is not a valid integer, "
                               f"using config value")
        return self.max_tokens  # Returns None if not configured (uses API default)

    def get_temperature(self) -> float:
        """Get temperature, checking env var first. OPTIONAL field (default: 0.7)."""
        env_key = self._get_env_key("TEMPERATURE")
        env_val = self._get_env_value("TEMPERATURE")
        if env_val:
            try:
                return float(env_val)
            except ValueError:
                logger.warning(f"Profile '{self.name}': {env_key}='{env_val}' is not a valid float, "
                               f"using config value")
        return self.temperature if self.temperature is not None else 0.7

    def get_timeout(self) -> int:
        """Get timeout, checking env var first. OPTIONAL field (default: 30000ms).

        Note: 0 means no timeout (infinity), not a fallback value.
        """
        env_key = self._get_env_key("TIMEOUT")
        env_val = self._get_env_value("TIMEOUT")
        if env_val is not None:
            try:
                return int(env_val)
            except ValueError:
                logger.warning(f"Profile '{self.name}': {env_key}='{env_val}' is not a valid integer, "
                               f"using config value")
        # 0 is valid (no timeout), only use default if truly None
        if self.timeout is not None:
            return self.timeout
        return 30000

    def get_endpoint(self) -> str:
        """Get endpoint URL, checking env var first. OPTIONAL field."""
        env_val = self._get_env_value("BASE_URL")
        if env_val:
            return env_val
        return self.base_url or ""


    def get_api_key(self) -> str:
        """Get API key, checking env var first. OPTIONAL field."""
        env_val = self._get_env_value("API_KEY")
        if env_val:
            return env_val
        return self.api_key or ""

    def get_top_p(self) -> Optional[float]:
        """Get top_p, checking env var first. OPTIONAL field."""
        env_val = self._get_env_value("TOP_P")
        if env_val:
            try:
                return float(env_val)
            except ValueError:
                pass
        return self.top_p

    def get_streaming(self) -> bool:
        """Get streaming setting, checking env var first. Default: True."""
        env_val = self._get_env_value("STREAMING")
        if env_val is not None:
            return env_val.lower() in ("true", "1", "yes", "on")
        return self.streaming

    def get_supports_tools(self) -> bool:
        """Get supports_tools setting, checking env var first. Default: True."""
        env_val = self._get_env_value("SUPPORTS_TOOLS")
        if env_val is not None:
            return env_val.lower() in ("true", "1", "yes", "on")
        return self.supports_tools

    def get_env_var_hints(self) -> Dict[str, EnvVarHint]:
        """Get env var names and status for this profile."""
        fields = [
            "MODEL", "PROVIDER", "BASE_URL", "API_KEY",
            "MAX_TOKENS", "TEMPERATURE", "TIMEOUT", "TOP_P",
            "STREAMING", "SUPPORTS_TOOLS",
            "DESCRIPTION", "EXTRA_HEADERS"
        ]
        return {
            field.lower(): EnvVarHint(
                name=self._get_env_key(field),
                is_set=self._get_env_value(field) is not None
            )
            for field in fields
        }

    def get_provider(self) -> str:
        """Get provider, checking env var first. OPTIONAL field (default: openai).

        Supported values: openai, anthropic, azure_openai, custom, openrouter
        """
        env_val = self._get_env_value("PROVIDER")
        if env_val:
            return env_val.lower()
        return self.provider or "openai"

    def get_provider_type(self) -> str:
        """
        Get provider type from profile.

        Returns:
            Provider type string from the profile's provider field.
            (Uses get_provider() which checks env var first.)
        """
        return self.get_provider()



    def to_dict(self) -> Dict[str, Any]:
        """Convert profile to dictionary representation.

        Always uses provider format (all profiles must have provider field).
        Uses getter methods to resolve env var overrides.
        """
        result = {
            "name": self.name,
            "provider": self.get_provider(),
            "model": self.get_model(),
            "temperature": self.get_temperature(),
            "max_tokens": self.get_max_tokens(),
            "timeout": self.get_timeout(),
            "description": self.description,
        }

        # Include api_key (resolved from env var if set)
        api_key = self.get_api_key()
        if api_key:
            result["api_key"] = api_key

        # Include base_url for custom providers
        base_url = self.get_endpoint()
        if base_url:
            result["base_url"] = base_url

        # Include optional fields
        top_p = self.get_top_p()
        if top_p is not None:
            result["top_p"] = top_p

        if self.extra_headers:
            result["extra_headers"] = self.extra_headers

        # Feature flags
        result["streaming"] = self.get_streaming()
        result["supports_tools"] = self.get_supports_tools()

        return result

    @classmethod
    def from_dict(cls, name: str, data: Dict[str, Any]) -> "LLMProfile":
        """
        Create profile from dictionary.

        Silently ignores unknown fields for forward compatibility.

        Args:
            name: Profile name
            data: Profile configuration dictionary (must have provider field)

        Returns:
            LLMProfile instance

        Raises:
            ValueError: If provider field is missing
        """
        # Provider field is required
        provider = data.get("provider")
        if not provider:
            raise ValueError(f"Profile '{name}' is missing required 'provider' field")

        return cls(
            name=name,
            provider=provider,
            api_key=data.get("api_key", ""),
            model=data.get("model", ""),
            temperature=data.get("temperature", 0.7),
            max_tokens=data.get("max_tokens"),
            timeout=data.get("timeout", 0),
            description=data.get("description", ""),
            extra_headers=data.get("extra_headers", {}),
            base_url=data.get("base_url", ""),
            top_p=data.get("top_p"),
            streaming=data.get("streaming", True),
            supports_tools=data.get("supports_tools", True),
        )


class ProfileManager:
    """
    Manages LLM configuration profiles.

    Features:
    - Built-in default profiles (default, fast, claude, openai)
    - User-defined profiles from config.json
    - Active profile switching
    - Adapter instantiation for profiles
    """

    # Built-in default profiles (all use provider format)
    DEFAULT_PROFILES: Dict[str, Dict[str, Any]] = {
        "default": {
            "provider": "custom",
            "base_url": "http://localhost:1234/v1",
            "model": "qwen/qwen3-4b",
            "temperature": 0.7,
            "description": "Local LLM for general use",
        },
        "fast": {
            "provider": "custom",
            "base_url": "http://localhost:1234/v1",
            "model": "qwen/qwen3-0.6b",
            "temperature": 0.3,
            "description": "Fast local model for quick queries",
        },
        "claude": {
            "provider": "anthropic",
            "model": "claude-sonnet-4-20250514",
            "temperature": 0.7,
            "max_tokens": 4096,
            "description": "Anthropic Claude for complex tasks",
        },
        "openai": {
            "provider": "openai",
            "model": "gpt-4-turbo",
            "temperature": 0.7,
            "max_tokens": 4096,
            "description": "OpenAI GPT-4 for general tasks",
        },
    }

    def __init__(self, config=None):
        """
        Initialize profile manager.

        Args:
            config: Configuration object with get() method
        """
        self.config = config
        self._profiles: Dict[str, LLMProfile] = {}
        self._active_profile_name: str = "default"
        self._load_profiles()
        # Note: Default profile initialization is now handled by config_utils.initialize_config()
        # which runs earlier in app startup and creates global/local config with profiles

    def reload(self) -> None:
        """Reload profiles from config file."""
        self._profiles.clear()
        self._load_profiles()
        logger.info("Profiles reloaded from config")

    def _load_profiles(self) -> None:
        """Load profiles from defaults and config file.

        Reads directly from config FILE (not cached config object) to ensure
        we always get the latest saved values.
        """
        # Start with built-in defaults
        for name, data in self.DEFAULT_PROFILES.items():
            self._profiles[name] = LLMProfile.from_dict(name, data)

        # Read profiles directly from config file (not cached config object)
        # This ensures we get the latest saved values after save_profile_values_to_config
        user_profiles, active_profile, default_profile = self._read_profiles_from_file()

        if user_profiles:
            for name, data in user_profiles.items():
                if isinstance(data, dict):
                    self._profiles[name] = LLMProfile.from_dict(name, data)
                    logger.debug(f"Loaded user profile: {name}")

        # Load active profile (last used) - takes priority
        if active_profile and active_profile in self._profiles:
            self._active_profile_name = active_profile
        elif default_profile and default_profile in self._profiles:
            self._active_profile_name = default_profile

        logger.info(
            f"Loaded {len(self._profiles)} profiles, active: {self._active_profile_name}"
        )

    def _read_profiles_from_file(self) -> tuple:
        """Read profiles directly from global config file.

        Profiles are user-level settings and only stored globally.

        Returns:
            Tuple of (profiles_dict, active_profile, default_profile)
        """
        global_config = Path.home() / ".kollabor-cli" / "config.json"

        if global_config.exists():
            try:
                config_data = json.loads(global_config.read_text(encoding="utf-8"))
                llm_config = config_data.get("core", {}).get("llm", {})
                profiles = llm_config.get("profiles", {})
                active = llm_config.get("active_profile")
                default = llm_config.get("default_profile", "default")

                if profiles:
                    logger.debug(f"Loaded profiles from: {global_config}")
                    return profiles, active, default
            except Exception as e:
                logger.warning(f"Failed to read profiles from {global_config}: {e}")

        # Fallback to config object if file read fails
        if self.config:
            return (
                self.config.get("core.llm.profiles", {}),
                self.config.get("core.llm.active_profile"),
                self.config.get("core.llm.default_profile", "default")
            )

        return {}, None, "default"

    def get_profile(self, name: str) -> Optional[LLMProfile]:
        """
        Get a profile by name.

        Args:
            name: Profile name

        Returns:
            LLMProfile or None if not found
        """
        return self._profiles.get(name)

    def get_active_profile(self) -> LLMProfile:
        """
        Get the currently active profile.

        Returns:
            Active LLMProfile (falls back to "default" if needed)
        """
        profile = self._profiles.get(self._active_profile_name)
        if not profile:
            logger.warning(
                f"Active profile '{self._active_profile_name}' not found, "
                "falling back to 'default'"
            )
            profile = self._profiles.get("default")
            if not profile:
                # Create minimal default profile
                profile = LLMProfile(
                    name="default",
                    api_url="http://localhost:1234",
                    model="default",
                )
        return profile

    def set_active_profile(self, name: str, persist: bool = True) -> bool:
        """
        Set the active profile.

        If profile doesn't exist but env vars are set (KOLLABOR_{NAME}_MODEL),
        auto-creates the profile from env vars.

        If profile exists AND env vars are set, env vars override the stored config
        (creates a new profile from env vars, replacing the stored one).

        Args:
            name: Profile name to activate
            persist: If True, save the selection to config for next startup

        Returns:
            True if successful, False if profile not found and can't be created
        """
        # Always try to create/update from env vars first (env vars take priority)
        if self._try_create_profile_from_env(name):
            logger.info(f"Profile '{name}' loaded from environment variables")
        elif name not in self._profiles:
            logger.error(f"Profile not found: {name}")
            return False

        old_profile = self._active_profile_name
        self._active_profile_name = name
        logger.info(f"Switched profile: {old_profile} -> {name}")

        # Persist to config so it survives restart
        if persist:
            self._save_active_profile_to_config(name)

        return True

    def _try_create_profile_from_env(self, name: str) -> bool:
        """
        Try to create a profile from environment variables.

        Looks for KOLLABOR_{NAME}_* env vars and creates profile if found.
        Required: at least MODEL must be set.

        Supported env vars:
            KOLLABOR_{NAME}_MODEL          - Model name (required)
            KOLLABOR_{NAME}_PROVIDER       - Provider type (default: custom)
            KOLLABOR_{NAME}_BASE_URL       - API endpoint URL
            KOLLABOR_{NAME}_API_KEY        - API key/token
            KOLLABOR_{NAME}_MAX_TOKENS     - Max tokens (integer)
            KOLLABOR_{NAME}_TEMPERATURE    - Temperature (float, 0.0-2.0)
            KOLLABOR_{NAME}_TIMEOUT        - Timeout in ms (integer)
            KOLLABOR_{NAME}_TOP_P          - Nucleus sampling (float, 0.0-1.0)
            KOLLABOR_{NAME}_STREAMING      - Enable streaming (true/false)
            KOLLABOR_{NAME}_SUPPORTS_TOOLS - Enable tool calling (true/false)
            KOLLABOR_{NAME}_DESCRIPTION    - Human-readable description
            KOLLABOR_{NAME}_EXTRA_HEADERS  - JSON object of extra headers

        Args:
            name: Profile name to create

        Returns:
            True if profile was created successfully
        """
        import os
        import re

        # Normalize name for env var lookup
        name_normalized = re.sub(r'[^a-zA-Z0-9]', '_', name.strip()).upper()
        prefix = f"KOLLABOR_{name_normalized}_"

        # Check for required field (MODEL)
        model = os.environ.get(f"{prefix}MODEL", "").strip()
        if not model:
            return False

        # Get string fields
        base_url = os.environ.get(f"{prefix}BASE_URL", "").strip()
        api_key = os.environ.get(f"{prefix}API_KEY", "").strip()
        provider = os.environ.get(f"{prefix}PROVIDER", "").strip() or "custom"
        description = os.environ.get(f"{prefix}DESCRIPTION", "").strip()

        # Parse numeric fields
        max_tokens = None
        max_tokens_str = os.environ.get(f"{prefix}MAX_TOKENS", "").strip()
        if max_tokens_str:
            try:
                max_tokens = int(max_tokens_str)
            except ValueError:
                logger.warning(f"Invalid MAX_TOKENS value: {max_tokens_str}")

        temperature = 0.7
        temp_str = os.environ.get(f"{prefix}TEMPERATURE", "").strip()
        if temp_str:
            try:
                temperature = float(temp_str)
            except ValueError:
                logger.warning(f"Invalid TEMPERATURE value: {temp_str}")

        timeout = 30000
        timeout_str = os.environ.get(f"{prefix}TIMEOUT", "").strip()
        if timeout_str:
            try:
                timeout = int(timeout_str)
            except ValueError:
                logger.warning(f"Invalid TIMEOUT value: {timeout_str}")

        top_p = None
        top_p_str = os.environ.get(f"{prefix}TOP_P", "").strip()
        if top_p_str:
            try:
                top_p = float(top_p_str)
            except ValueError:
                logger.warning(f"Invalid TOP_P value: {top_p_str}")

        # Parse boolean fields
        streaming = True
        streaming_str = os.environ.get(f"{prefix}STREAMING", "").strip().lower()
        if streaming_str:
            streaming = streaming_str in ("true", "1", "yes", "on")

        supports_tools = True
        tools_str = os.environ.get(f"{prefix}SUPPORTS_TOOLS", "").strip().lower()
        if tools_str:
            supports_tools = tools_str in ("true", "1", "yes", "on")

        # Parse extra headers (JSON)
        extra_headers = {}
        headers_str = os.environ.get(f"{prefix}EXTRA_HEADERS", "").strip()
        if headers_str:
            try:
                extra_headers = json.loads(headers_str)
                if not isinstance(extra_headers, dict):
                    extra_headers = {}
                    logger.warning(f"EXTRA_HEADERS must be a JSON object")
            except json.JSONDecodeError:
                logger.warning(f"Invalid EXTRA_HEADERS JSON: {headers_str}")

        # Create the profile
        profile = LLMProfile(
            name=name,
            provider=provider,
            model=model,
            base_url=base_url,
            api_key=api_key,
            max_tokens=max_tokens,
            temperature=temperature,
            timeout=timeout,
            top_p=top_p,
            streaming=streaming,
            supports_tools=supports_tools,
            description=description or f"Created from env vars",
            extra_headers=extra_headers,
        )

        self._profiles[name] = profile
        logger.info(f"Created profile '{name}' from env vars: model={model}, provider={provider}")
        return True

    def _save_active_profile_to_config(self, name: str) -> bool:
        """
        Save the active profile name to global config.json.

        Profiles are user-wide settings, so they're saved to global config
        (~/.kollabor-cli/config.json) to be available across all projects.

        Args:
            name: Profile name to save as active

        Returns:
            True if saved successfully
        """
        try:
            # Profiles are user-wide, always save to global config
            config_path = Path.home() / ".kollabor-cli" / "config.json"

            if not config_path.exists():
                logger.warning(f"Config file not found: {config_path}")
                return False

            config_data = json.loads(config_path.read_text(encoding="utf-8"))

            # Ensure core.llm exists
            if "core" not in config_data:
                config_data["core"] = {}
            if "llm" not in config_data["core"]:
                config_data["core"]["llm"] = {}

            # Save active profile
            config_data["core"]["llm"]["active_profile"] = name

            config_path.write_text(
                json.dumps(config_data, indent=2, ensure_ascii=False),
                encoding="utf-8"
            )

            logger.debug(f"Saved active profile to config: {name}")
            return True

        except Exception as e:
            logger.error(f"Failed to save active profile to config: {e}")
            return False

    def save_profile_values_to_config(self, profile: "LLMProfile", local: bool = False) -> bool:
        """
        Save a profile's values to config.json.

        Args:
            profile: Profile to save
            local: If True, save to local project config; else save to global

        Returns:
            True if saved successfully
        """
        try:
            if local:
                # Save to local project config
                config_path = Path.cwd() / ".kollabor-cli" / "config.json"
                config_path.parent.mkdir(parents=True, exist_ok=True)
            else:
                # Save to global config
                config_path = Path.home() / ".kollabor-cli" / "config.json"

            # Read existing config or create new
            if config_path.exists():
                config_data = json.loads(config_path.read_text(encoding="utf-8"))
            else:
                config_data = {}

            # Ensure structure exists
            if "core" not in config_data:
                config_data["core"] = {}
            if "llm" not in config_data["core"]:
                config_data["core"]["llm"] = {}
            if "profiles" not in config_data["core"]["llm"]:
                config_data["core"]["llm"]["profiles"] = {}

            # Convert profile to dict (excluding name since it's the key)
            profile_dict = {
                "provider": profile.provider,
                "model": profile.model,
                "temperature": profile.temperature,
                "timeout": profile.timeout,
                "description": profile.description,
            }

            # Only include optional fields if set
            if profile.max_tokens is not None:
                profile_dict["max_tokens"] = profile.max_tokens
            if profile.base_url:
                profile_dict["base_url"] = profile.base_url
            if profile.api_key:
                profile_dict["api_key"] = profile.api_key
            if profile.top_p is not None:
                profile_dict["top_p"] = profile.top_p
            if not profile.streaming:  # Only save if non-default
                profile_dict["streaming"] = profile.streaming
            if not profile.supports_tools:  # Only save if non-default
                profile_dict["supports_tools"] = profile.supports_tools
            if profile.extra_headers:
                profile_dict["extra_headers"] = profile.extra_headers

            # Save profile
            config_data["core"]["llm"]["profiles"][profile.name] = profile_dict

            config_path.write_text(
                json.dumps(config_data, indent=2, ensure_ascii=False),
                encoding="utf-8"
            )

            location = "local" if local else "global"
            logger.info(f"Saved profile '{profile.name}' to {location} config: {config_path}")
            return True

        except Exception as e:
            logger.error(f"Failed to save profile to config: {e}")
            return False

    def list_profiles(self) -> List[LLMProfile]:
        """
        List all available profiles.

        Returns:
            List of LLMProfile instances
        """
        return list(self._profiles.values())

    def get_profile_names(self) -> List[str]:
        """
        Get list of profile names.

        Returns:
            List of profile name strings
        """
        return list(self._profiles.keys())

    def add_profile(self, profile: LLMProfile) -> bool:
        """
        Add a new profile.

        Args:
            profile: LLMProfile to add

        Returns:
            True if added, False if name already exists
        """
        if profile.name in self._profiles:
            logger.warning(f"Profile already exists: {profile.name}")
            return False

        self._profiles[profile.name] = profile
        logger.info(f"Added profile: {profile.name}")
        return True

    def create_profile(
        self,
        name: str,
        base_url: str = "",
        model: str = "",
        api_key: str = None,
        temperature: float = 0.7,
        max_tokens: int = None,
        provider: str = "custom",
        supports_tools: bool = True,
        description: str = "",
        timeout: int = 0,
        streaming: bool = True,
        save_to_config: bool = False
    ) -> Optional[LLMProfile]:
        """
        Create and add a new profile.

        Args:
            name: Profile name
            base_url: API endpoint URL
            model: Model name
            api_key: API key (optional)
            temperature: Temperature setting (default 0.7)
            max_tokens: Max tokens limit (optional)
            provider: Provider type (default "custom")
            supports_tools: Enable tool calling (default True)
            description: Profile description
            timeout: Request timeout in milliseconds (0 = no timeout)
            streaming: Enable streaming responses (default True)
            save_to_config: Whether to save to config file

        Returns:
            Created LLMProfile if successful, None if name already exists
        """
        if name in self._profiles:
            logger.warning(f"Profile already exists: {name}")
            return None

        profile = LLMProfile(
            name=name,
            provider=provider,
            model=model,
            temperature=temperature,
            max_tokens=max_tokens,
            description=description,
            api_key=api_key or "",
            base_url=base_url,
            supports_tools=supports_tools,
            timeout=timeout,
            streaming=streaming,
        )

        self._profiles[name] = profile
        logger.info(f"Created profile: {name}")

        if save_to_config:
            self.save_profile_values_to_config(profile)

        return profile

    def remove_profile(self, name: str) -> bool:
        """
        Remove a profile.

        Cannot remove built-in profiles or the current active profile.

        Args:
            name: Profile name to remove

        Returns:
            True if removed, False if protected or not found
        """
        if name in self.DEFAULT_PROFILES:
            logger.error(f"Cannot remove built-in profile: {name}")
            return False

        if name == self._active_profile_name:
            logger.error(f"Cannot remove active profile: {name}")
            return False

        if name not in self._profiles:
            logger.error(f"Profile not found: {name}")
            return False

        del self._profiles[name]
        logger.info(f"Removed profile: {name}")
        return True

    def update_profile(
        self,
        original_name: str,
        new_name: str = None,
        base_url: str = None,
        model: str = None,
        api_key: str = None,
        temperature: float = None,
        provider: str = None,
        supports_tools: bool = None,
        description: str = None,
        save_to_config: bool = False
    ) -> bool:
        """
        Update an existing profile.

        Args:
            original_name: Current name of the profile to update
            new_name: New name for the profile (optional, for renaming)
            base_url: API endpoint URL
            model: Model name
            api_key: API key (None to keep existing)
            temperature: Temperature setting
            provider: Provider type (openai, anthropic, etc.)
            supports_tools: Enable tool/function calling
            description: Profile description
            save_to_config: Whether to persist changes to config file

        Returns:
            True if updated successfully, False otherwise
        """
        if original_name not in self._profiles:
            logger.error(f"Profile not found: {original_name}")
            return False

        profile = self._profiles[original_name]

        # Update fields if provided
        if base_url is not None:
            profile.base_url = base_url
        if model is not None:
            profile.model = model
        if api_key is not None:
            profile.api_key = api_key
        if temperature is not None:
            profile.temperature = temperature
        if provider is not None:
            profile.provider = provider
        if supports_tools is not None:
            profile.supports_tools = supports_tools
        if description is not None:
            profile.description = description

        # Handle rename
        if new_name and new_name != original_name:
            if new_name in self._profiles:
                logger.error(f"Cannot rename: profile '{new_name}' already exists")
                return False
            profile.name = new_name
            self._profiles[new_name] = profile
            del self._profiles[original_name]

            # Update active profile name if needed
            if self._active_profile_name == original_name:
                self._active_profile_name = new_name

            logger.info(f"Renamed profile: {original_name} -> {new_name}")

        # Save to config if requested
        if save_to_config:
            self.save_profile_values_to_config(profile)

        logger.info(f"Updated profile: {profile.name}")
        return True

    def delete_profile(self, name: str) -> bool:
        """
        Delete a profile from memory and config file.

        Cannot delete built-in profiles or the current active profile.

        Args:
            name: Profile name to delete

        Returns:
            True if deleted successfully, False otherwise
        """
        if name in self.DEFAULT_PROFILES:
            logger.error(f"Cannot delete built-in profile: {name}")
            return False

        if name == self._active_profile_name:
            logger.error(f"Cannot delete active profile: {name}")
            return False

        if name not in self._profiles:
            logger.error(f"Profile not found: {name}")
            return False

        # Remove from memory
        del self._profiles[name]

        # Remove from config file
        self._delete_profile_from_config(name)

        logger.info(f"Deleted profile: {name}")
        return True

    def _delete_profile_from_config(self, name: str) -> bool:
        """
        Delete a profile from global config.json.

        Profiles are user-wide settings, so they're deleted from global config
        (~/.kollabor-cli/config.json).

        Args:
            name: Profile name to delete

        Returns:
            True if deleted successfully from config
        """
        try:
            # Profiles are user-wide, always use global config
            config_path = Path.home() / ".kollabor-cli" / "config.json"

            if not config_path.exists():
                logger.warning(f"Config file not found: {config_path}")
                return True  # No config file, nothing to delete

            # Load current config
            config_data = json.loads(config_path.read_text(encoding="utf-8"))

            # Check if profile exists in config
            profiles = config_data.get("core", {}).get("llm", {}).get("profiles", {})
            if name not in profiles:
                logger.debug(f"Profile '{name}' not in config file")
                return True  # Not in config, nothing to delete

            # Remove profile from config
            del config_data["core"]["llm"]["profiles"][name]

            # Write back
            config_path.write_text(
                json.dumps(config_data, indent=2, ensure_ascii=False),
                encoding="utf-8"
            )

            logger.info(f"Deleted profile from config: {name}")
            return True

        except Exception as e:
            logger.error(f"Failed to delete profile from config: {e}")
            return False

    def get_adapter_for_profile(
        self, profile: Optional[LLMProfile] = None
    ):
        """
        Get the appropriate API adapter for a profile.

        Args:
            profile: Profile to get adapter for (default: active profile)

        Returns:
            Configured API adapter instance
        """
        if profile is None:
            profile = self.get_active_profile()

        if profile.tool_format == "anthropic":
            return AnthropicAdapter(base_url=profile.api_url)
        else:
            return OpenAIAdapter(base_url=profile.api_url)



    def is_active(self, name: str) -> bool:
        """
        Check if a profile is the active one.

        Args:
            name: Profile name

        Returns:
            True if this is the active profile
        """
        return name == self._active_profile_name

    @property
    def active_profile_name(self) -> str:
        """Get the name of the active profile."""
        return self._active_profile_name

    def _get_normalized_name(self, name: str) -> str:
        """Get normalized profile name for env var prefix.

        Strips whitespace and replaces all non-alphanumeric characters with
        underscores, then uppercases the result.

        Args:
            name: The profile name to normalize

        Returns:
            Normalized name suitable for env var prefix

        Examples:
            "my-profile" -> "MY_PROFILE"
            "my.profile" -> "MY_PROFILE"
            "My Profile!" -> "MY_PROFILE_"
            "  fast  " -> "FAST"
        """
        return re.sub(r'[^a-zA-Z0-9]', '_', name.strip()).upper()

    def _check_name_collision(self, new_name: str, exclude_name: Optional[str] = None) -> Optional[str]:
        """Check if new profile name would collide with existing profiles.

        Two profile names collide if they normalize to the same env var prefix,
        which would cause them to share the same environment variables.

        Args:
            new_name: The proposed profile name
            exclude_name: Profile name to exclude from check (for renames)

        Returns:
            Name of colliding profile if collision found, None otherwise.
        """
        new_normalized = self._get_normalized_name(new_name)
        for existing_name in self._profiles:
            if existing_name == exclude_name:
                continue
            if self._get_normalized_name(existing_name) == new_normalized:
                return existing_name
        return None

    def get_profile_summary(self, name: Optional[str] = None) -> str:
        """
        Get a human-readable summary of a profile.

        Args:
            name: Profile name (default: active profile)

        Returns:
            Formatted summary string
        """
        profile = self._profiles.get(name) if name else self.get_active_profile()
        if not profile:
            return f"Profile '{name}' not found"

        hints = profile.get_env_var_hints()
        token_status = "[set]" if hints["token"].is_set else "[not set]"

        native_mode = "native" if profile.get_native_tool_calling() else "xml"
        lines = [
            f"Profile: {profile.name}",
            f"  Endpoint: {profile.get_endpoint() or '(not configured)'}",
            f"  Model: {profile.get_model() or '(not configured)'}",
            f"  Token: {hints['token'].name} {token_status}",
            f"  Temperature: {profile.get_temperature()}",
            f"  Max Tokens: {profile.get_max_tokens() or '(API default)'}",
            f"  Timeout: {profile.get_timeout()}ms",
            f"  Tool Format: {profile.get_tool_format()}",
            f"  Tool Calling: {native_mode}",
        ]
        if profile.description:
            lines.append(f"  Description: {profile.description}")

        return "\n".join(lines)


