"""User configuration management for whai."""

import contextlib
import os
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple, Type

# Use tomllib for Python 3.11+, tomli for older versions
if sys.version_info >= (3, 11):
    import tomllib
else:
    import tomli as tomllib

import tomli_w

from whai.constants import (
    CONFIG_FILENAME,
    DEFAULT_MODEL_OPENAI,
    DEFAULT_PROVIDER,
    DEFAULT_ROLE_NAME,
    ENV_WHAI_TEST_MODE,
)
from whai.logging_setup import get_logger

logger = get_logger(__name__)


@contextlib.contextmanager
def _suppress_stdout_stderr():
    """Context manager to suppress stdout and stderr output."""
    with open(os.devnull, "w", encoding="utf-8") as devnull:
        old_stdout = sys.stdout
        old_stderr = sys.stderr
        try:
            sys.stdout = devnull
            sys.stderr = devnull
            yield
        finally:
            sys.stdout = old_stdout
            sys.stderr = old_stderr


class MissingConfigError(RuntimeError):
    """Raised when configuration file is missing and not in ephemeral mode."""

    pass


class InvalidProviderConfigError(ValueError):
    """Raised when provider configuration contains invalid values."""

    pass


# ============================================================================
# Validation Result Dataclass
# ============================================================================


@dataclass
class ValidationResult:
    """Result of provider configuration validation."""

    is_valid: bool
    checks_performed: List[str]
    issues: List[str]
    details: Dict[str, Any]


# ============================================================================
# Provider Configuration Dataclasses
# ============================================================================


@dataclass
class ProviderConfig:
    """
    Base configuration for LLM providers.

    Contains sensible defaults for all possible provider fields.
    Subclasses define provider_name and override validation as needed.
    """

    api_key: Optional[str] = None
    api_base: Optional[str] = None
    api_version: Optional[str] = None
    default_model: Optional[str] = None

    # Subclasses must define this
    provider_name: str = "unknown"

    def __post_init__(self) -> None:
        """Validate configuration values."""
        self._validate_api_base()
        self._validate_required_fields()

    def _validate_api_base(self) -> None:
        """Validate URL format for api_base if provided."""
        if self.api_base is not None:
            if not isinstance(self.api_base, str) or not self.api_base.strip():
                raise InvalidProviderConfigError(
                    f"{self.provider_name} provider 'api_base' must be a non-empty string if provided."
                )
            # Basic URL validation
            if not (
                self.api_base.startswith("http://")
                or self.api_base.startswith("https://")
            ):
                raise InvalidProviderConfigError(
                    f"{self.provider_name} provider 'api_base' must be a valid HTTP/HTTPS URL, got: {self.api_base}"
                )

    def _validate_non_empty_field(self, field_name: str, field_value: Optional[str]) -> None:
        """Validate that a required field is non-empty."""
        if not field_value or not field_value.strip():
            raise InvalidProviderConfigError(
                f"{self.provider_name} provider requires '{field_name}'"
            )

    def _validate_required_fields(self) -> None:
        """Validate required fields. Override in subclasses if needed."""
        pass

    def _get_masked_key(self, key: Optional[str]) -> str:
        """Get masked version of API key for display."""
        if not key:
            return "MISSING"
        return key[:8] + "..." if len(key) > 8 else "***"

    def get_summary_fields(self) -> Dict[str, str]:
        """
        Get provider-specific fields for summary display.

        Returns a dictionary mapping field names to their display values.
        Sensitive values (like API keys) should be masked.
        Override in subclasses to customize which fields are shown.

        Returns:
            Dictionary of field_name -> display_value
        """
        fields = {}
        fields["model"] = self.default_model or "MISSING"
        fields["key"] = self._get_masked_key(self.api_key)
        return fields

    def validate(
        self, on_progress: Optional[Callable[[str, Optional[bool]], None]] = None
    ) -> ValidationResult:
        """
        Validate this provider configuration using external checks.

        Args:
            on_progress: Optional callback function(message: str, success: Optional[bool])
                        called for each validation step with progress updates.
                        None indicates check in progress, True/False indicate result.

        Returns:
            ValidationResult with validation outcomes.
        """
        if on_progress is None:
            # Default no-op callback
            def _noop_progress(msg: str, success: Optional[bool]) -> None:
                pass

            on_progress = _noop_progress

        checks_performed = ["Field validation"]
        issues = []
        details: Dict[str, Any] = {}

        # Field validation is already done in __post_init__, just report it
        # Instant check - print directly with result
        on_progress("Validating fields", True)
        checks_performed.append("Field validation")

        # Normalize optional API key: treat whitespace-only as missing
        if self.api_key and isinstance(self.api_key, str) and not self.api_key.strip():
            self.api_key = None

        # API key validation (if applicable and non-empty)
        if self.api_key:
            on_progress("Validating API key", None)
            checks_performed.append("API key format")
            try:
                from litellm import check_valid_key

                # Build model name for validation
                model = self._get_litellm_model_name()
                checks_performed.append(f"API key validation ({model})")

                # Check if key is valid (with timeout)
                import socket

                old_timeout = socket.getdefaulttimeout()
                socket.setdefaulttimeout(5.0)

                try:
                    # Suppress LiteLLM output during validation
                    with _suppress_stdout_stderr():
                        is_key_valid = check_valid_key(
                            model=model, api_key=self.api_key
                        )
                    if not is_key_valid:
                        issues.append(
                            f"API key validation failed for {self.provider_name}"
                        )
                        on_progress("Validating API key", False)
                    else:
                        details["api_key_valid"] = True
                        on_progress("Validating API key", True)
                except Exception as e:
                    issues.append(f"Could not validate API key: {str(e)}")
                    on_progress("Validating API key", False)
                finally:
                    socket.setdefaulttimeout(old_timeout)

            except ImportError:
                issues.append("LiteLLM not available for validation")
                on_progress("Validating API key", False)
            except Exception as e:
                issues.append(f"API key validation error: {str(e)}")
                on_progress("Validating API key", False)

        # Model validation
        if self.default_model:
            on_progress("Validating model", None)
            checks_performed.append("Model configuration")
            details["default_model"] = self.default_model

            model_valid, model_issue = self._validate_model()

            if not model_valid and model_issue:
                issues.append(model_issue)
                on_progress("Validating model", False)
            else:
                on_progress("Validating model", True)

        # API base validation (for providers with an API base)
        if self.api_base:
            on_progress("Validating API base connectivity", None)
            checks_performed.append("API base configuration")
            details["api_base"] = self.api_base

            try:
                import urllib.error
                import urllib.request

                # Provider-specific connectivity endpoint:
                # - Ollama exposes model tags via /api/tags (returns 404 on /models)
                # - LM Studio and other OpenAI-compatible providers expose /models
                if self.provider_name == "ollama":
                    models_url = f"{self.api_base.rstrip('/')}/api/tags"
                else:
                    models_url = f"{self.api_base.rstrip('/')}/models"
                req = urllib.request.Request(models_url, method="GET")
                urllib.request.urlopen(req, timeout=5)
                details["api_base_reachable"] = True
                on_progress("Validating API base connectivity", True)
            except Exception as e:
                issues.append(f"API base not reachable: {str(e)}")
                details["api_base_reachable"] = False
                on_progress("Validating API base connectivity", False)

        is_valid = len(issues) == 0

        return ValidationResult(
            is_valid=is_valid,
            checks_performed=checks_performed,
            issues=issues,
            details=details,
        )

    def _validate_model(self) -> Tuple[bool, Optional[str]]:
        """
        Validate the model configuration.

        Returns:
            Tuple of (model_valid: bool, model_issue: Optional[str])
            If model_valid is False, model_issue contains the reason.

        This method can be overridden by subclasses to provide provider-specific
        model validation logic.
        """
        if not self.default_model:
            return True, None

        model_valid = False
        model_issue = None

        # Try to validate model using LiteLLM's get_model_info
        try:
            from litellm import get_model_info

            # Get model name in LiteLLM format
            model = self._get_litellm_model_name()

            # Use get_model_info to check if model exists
            # This raises an exception if the model doesn't exist
            try:
                # Suppress LiteLLM output during validation
                with _suppress_stdout_stderr():
                    get_model_info(model)
                # If we get here, model exists
                model_valid = True
            except Exception as e:
                # Model doesn't exist or not recognized by LiteLLM
                error_msg = str(e)
                if "isn't mapped yet" in error_msg or "not found" in error_msg.lower():
                    model_issue = f"Model '{self.default_model}' is not recognized"
                    model_valid = False
                else:
                    # Unknown error - assume valid (could be network issue, etc.)
                    model_issue = f"Could not validate model: {error_msg}"
                    model_valid = True
        except Exception as e:
            model_issue = f"Could not validate model: {str(e)}"
            # Assume valid if validation fails (new models, etc.)
            model_valid = True

        return model_valid, model_issue

    def _strip_provider_prefix(self, model: str, prefix: str) -> str:
        """
        Strip provider prefix from model name if present.

        Args:
            model: The model name (may include prefix).
            prefix: The prefix to strip (e.g., 'gemini/', 'lm_studio/').

        Returns:
            Model name without the prefix.
        """
        if model and model.startswith(prefix):
            return model[len(prefix) :]
        return model

    def _get_litellm_model_name(self) -> str:
        """Get the model name formatted for LiteLLM validation. Override in subclasses."""
        return self.default_model or "default"

    def sanitize_model_name(self, model: str) -> str:
        """
        Sanitize/transform a model name for use with LiteLLM.

        This method can be overridden by subclasses to handle provider-specific
        model name transformations (e.g., adding prefixes, stripping prefixes).

        Args:
            model: The raw model name to sanitize.

        Returns:
            The sanitized model name, by default returns the model unchanged.
        """
        return model

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for TOML serialization, excluding None values."""
        result: Dict[str, Any] = {}
        if self.api_key is not None:
            result["api_key"] = self.api_key
        if self.api_base is not None:
            result["api_base"] = self.api_base
        if self.api_version is not None:
            result["api_version"] = self.api_version
        if self.default_model is not None:
            result["default_model"] = self.default_model
        return result

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "ProviderConfig":
        """Create ProviderConfig from dictionary."""
        return cls(
            api_key=data.get("api_key"),
            api_base=data.get("api_base"),
            api_version=data.get("api_version"),
            default_model=data.get("default_model"),
        )


@dataclass
class OpenAIConfig(ProviderConfig):
    """Configuration for OpenAI provider."""

    provider_name: str = "openai"

    def _validate_required_fields(self) -> None:
        """Validate OpenAI-specific requirements."""
        self._validate_non_empty_field("api_key", self.api_key)
        self._validate_non_empty_field("default_model", self.default_model)


@dataclass
class AnthropicConfig(ProviderConfig):
    """Configuration for Anthropic provider."""

    provider_name: str = "anthropic"

    def _validate_required_fields(self) -> None:
        """Validate Anthropic-specific requirements."""
        self._validate_non_empty_field("api_key", self.api_key)
        self._validate_non_empty_field("default_model", self.default_model)


@dataclass
class GeminiConfig(ProviderConfig):
    """Configuration for Gemini provider."""

    provider_name: str = "gemini"

    def __post_init__(self) -> None:
        """Strip gemini/ prefix from model name if present."""
        super().__post_init__()
        if self.default_model:
            self.default_model = self._strip_provider_prefix(self.default_model, "gemini/")

    def _validate_required_fields(self) -> None:
        """Validate Gemini-specific requirements."""
        self._validate_non_empty_field("api_key", self.api_key)
        self._validate_non_empty_field("default_model", self.default_model)

    def _get_litellm_model_name(self) -> str:
        """Get model name with gemini/ prefix."""
        model = self.default_model or "default"
        return f"gemini/{model}"

    def sanitize_model_name(self, model: str) -> str:
        """Strip gemini/ prefix if present, then add it back for LiteLLM."""
        base_model = self._strip_provider_prefix(model, "gemini/")
        return f"gemini/{base_model}"

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "GeminiConfig":
        """Create GeminiConfig from dictionary, stripping gemini/ prefix if present."""
        default_model = data.get("default_model")
        if default_model and default_model.startswith("gemini/"):
            default_model = default_model[len("gemini/") :]
        return cls(
            api_key=data.get("api_key"),
            api_base=data.get("api_base"),
            api_version=data.get("api_version"),
            default_model=default_model,
        )


@dataclass
class AzureOpenAIConfig(ProviderConfig):
    """Configuration for Azure OpenAI provider."""

    provider_name: str = "azure_openai"

    def _validate_required_fields(self) -> None:
        """Validate Azure OpenAI-specific requirements."""
        self._validate_non_empty_field("api_key", self.api_key)
        self._validate_non_empty_field("api_base", self.api_base)
        self._validate_non_empty_field("api_version", self.api_version)
        self._validate_non_empty_field("default_model", self.default_model)

    def get_summary_fields(self) -> Dict[str, str]:
        """Get Azure OpenAI-specific fields for summary."""
        fields = {}
        fields["model"] = self.default_model or "MISSING"
        fields["key"] = self._get_masked_key(self.api_key)
        fields["api_base"] = self.api_base or "MISSING"
        fields["api_version"] = self.api_version or "MISSING"
        return fields

    def _get_litellm_model_name(self) -> str:
        """Get model name with azure/ prefix."""
        return f"azure/{self.default_model or 'default'}"


@dataclass
class OllamaConfig(ProviderConfig):
    """Configuration for Ollama provider (no API key required)."""

    provider_name: str = "ollama"

    def __post_init__(self) -> None:
        """Strip ollama/ or ollama_chat/ prefix from model name if present."""
        super().__post_init__()
        if self.default_model:
            self.default_model = self._strip_provider_prefix(self.default_model, "ollama/")
            self.default_model = self._strip_provider_prefix(self.default_model, "ollama_chat/")

    def _validate_required_fields(self) -> None:
        """Validate Ollama-specific requirements."""
        self._validate_non_empty_field("api_base", self.api_base)
        self._validate_non_empty_field("default_model", self.default_model)

    def get_summary_fields(self) -> Dict[str, str]:
        """Get Ollama-specific fields for summary."""
        fields = {}
        fields["model"] = self.default_model or "MISSING"
        fields["api_base"] = self.api_base or "MISSING"
        # API key is optional for Ollama, only show if present
        if self.api_key:
            fields["key"] = self._get_masked_key(self.api_key)
        return fields

    def _get_litellm_model_name(self) -> str:
        """Get model name with ollama_chat/ prefix (recommended by LiteLLM for better responses)."""
        return f"ollama_chat/{self.default_model or 'default'}"

    def sanitize_model_name(self, model: str) -> str:
        """Strip ollama/ or ollama_chat/ prefix if present, then add ollama_chat/ for LiteLLM."""
        base_model = self._strip_provider_prefix(model, "ollama/")
        base_model = self._strip_provider_prefix(base_model, "ollama_chat/")
        return f"ollama_chat/{base_model}"

    def _get_available_models(self) -> List[str]:
        """
        Get list of available model names from Ollama's /api/tags endpoint.
        
        Returns:
            List of model names (empty list if query fails or no models available)
        """
        if not self.api_base:
            return []
        
        try:
            import json
            import urllib.error
            import urllib.request

            # Query Ollama's /api/tags endpoint
            models_url = f"{self.api_base.rstrip('/')}/api/tags"
            req = urllib.request.Request(models_url, method="GET")

            with urllib.request.urlopen(req, timeout=5) as response:
                data = json.loads(response.read().decode())
                return [model.get("name", "") for model in data.get("models", [])]
        except Exception:
            # Network error, JSON decode error, or other error - return empty list
            return []

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "OllamaConfig":
        """Create OllamaConfig from dictionary, stripping ollama/ or ollama_chat/ prefix if present."""
        default_model = data.get("default_model")
        if default_model:
            if default_model.startswith("ollama/"):
                default_model = default_model[len("ollama/") :]
            elif default_model.startswith("ollama_chat/"):
                default_model = default_model[len("ollama_chat/") :]
        return cls(
            api_key=data.get("api_key"),
            api_base=data.get("api_base"),
            api_version=data.get("api_version"),
            default_model=default_model,
        )


@dataclass
class LMStudioConfig(ProviderConfig):
    """
    Configuration for LM Studio provider.
    
    LM Studio uses the official 'lm_studio/' prefix in LiteLLM with
    LM_STUDIO_API_BASE and optional LM_STUDIO_API_KEY environment variables.
    """

    provider_name: str = "lm_studio"

    def __post_init__(self) -> None:
        """Strip lm_studio/ or openai/ prefix from model name if present."""
        super().__post_init__()
        if self.default_model:
            # Strip lm_studio/ prefix if present
            self.default_model = self._strip_provider_prefix(self.default_model, "lm_studio/")
            # Also strip openai/ prefix if present (for backward compatibility)
            self.default_model = self._strip_provider_prefix(self.default_model, "openai/")

    def _validate_required_fields(self) -> None:
        """Validate LM Studio-specific requirements."""
        self._validate_non_empty_field("api_base", self.api_base)
        self._validate_non_empty_field("default_model", self.default_model)

    def get_summary_fields(self) -> Dict[str, str]:
        """Get LM Studio-specific fields for summary."""
        fields = {}
        fields["model"] = self.default_model or "MISSING"
        fields["api_base"] = self.api_base or "MISSING"
        # API key is optional for LM Studio, only show if present
        if self.api_key:
            fields["key"] = self._get_masked_key(self.api_key)
        return fields

    def _get_litellm_model_name(self) -> str:
        """Get model name with lm_studio/ prefix (official LiteLLM approach)."""
        return f"lm_studio/{self.default_model or 'default'}"

    def sanitize_model_name(self, model: str) -> str:
        """
        Transform a model name to the format required by LiteLLM for LM Studio.

        LM Studio uses the official 'lm_studio/' prefix in LiteLLM.
        This method handles model names that may come with prefixes and ensures
        they're formatted correctly as 'lm_studio/{model}'.

        Args:
            model: The model name to transform (may include 'lm_studio/' or 'openai/' prefix).

        Returns:
            Model name formatted as 'lm_studio/{base_model}' for LiteLLM.
        """
        # Strip 'lm_studio/' prefix if present
        base_model = self._strip_provider_prefix(model, "lm_studio/")
        # Also strip 'openai/' prefix if present (for backward compatibility with old configs)
        base_model = self._strip_provider_prefix(base_model, "openai/")
        # Format as 'lm_studio/{model}' for LiteLLM (official approach)
        return f"lm_studio/{base_model}"

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "LMStudioConfig":
        """Create LMStudioConfig from dictionary, stripping lm_studio/ or openai/ prefix if present."""
        default_model = data.get("default_model")
        if default_model:
            # Strip lm_studio/ prefix if present
            if default_model.startswith("lm_studio/"):
                default_model = default_model[len("lm_studio/") :]
            # Also strip openai/ prefix if present (for backward compatibility)
            elif default_model.startswith("openai/"):
                default_model = default_model[len("openai/") :]
        return cls(
            api_key=data.get("api_key"),
            api_base=data.get("api_base"),
            api_version=data.get("api_version"),
            default_model=default_model,
        )

    def _get_available_models(self) -> List[str]:
        """
        Get list of available model IDs from LM Studio's /models endpoint.
        
        Returns:
            List of model IDs (empty list if query fails or no models available)
        """
        if not self.api_base:
            return []
        
        try:
            import json
            import urllib.error
            import urllib.request

            # Query LM Studio's /models endpoint (OpenAI-compatible)
            models_url = f"{self.api_base.rstrip('/')}/models"
            req = urllib.request.Request(
                models_url, headers={"Content-Type": "application/json"}, method="GET"
            )

            with urllib.request.urlopen(req, timeout=5) as response:
                data = json.loads(response.read().decode())
                return [model.get("id") for model in data.get("data", [])]
        except Exception:
            # Network error, JSON decode error, or other error - return empty list
            return []

    def _validate_model(self) -> Tuple[bool, Optional[str]]:
        """
        Validate the model by querying LM Studio's /models endpoint directly.

        LM Studio models are dynamically loaded locally and may not be in
        LiteLLM's model registry, so we validate against the actual LM Studio API.

        Returns:
            Tuple of (model_valid: bool, model_issue: Optional[str])
        """
        if not self.default_model:
            return True, None

        if not self.api_base:
            # Can't validate without API base
            return True, None

        available_models = self._get_available_models()

        if self.default_model in available_models:
            return True, None
        else:
            if available_models:
                available_str = ", ".join(available_models[:5]) + (
                    "..." if len(available_models) > 5 else ""
                )
                issue = (
                    f"Model '{self.default_model}' not found in LM Studio. "
                    f"Available models: {available_str}"
                )
            else:
                issue = "No models found in LM Studio"
            return False, issue


def get_provider_class(name: str) -> Type[ProviderConfig]:
    """
    Get the appropriate ProviderConfig subclass for a provider name.

    Args:
        name: Provider name (e.g., 'openai', 'anthropic')

    Returns:
        ProviderConfig subclass

    Raises:
        ValueError: If provider name is unknown
    """
    provider_classes = {
        "openai": OpenAIConfig,
        "anthropic": AnthropicConfig,
        "gemini": GeminiConfig,
        "azure_openai": AzureOpenAIConfig,
        "ollama": OllamaConfig,
        "lm_studio": LMStudioConfig,
    }

    if name not in provider_classes:
        raise ValueError(
            f"Unknown provider '{name}'. "
            f"Supported providers: {', '.join(provider_classes.keys())}"
        )

    return provider_classes[name]


# ============================================================================
# Main Configuration Dataclasses
# ============================================================================


@dataclass
class LLMConfig:
    """Configuration for LLM providers."""

    default_provider: Optional[str]
    providers: Dict[str, ProviderConfig]

    def __post_init__(self) -> None:
        """Validate that default_provider exists in providers if set."""
        if self.default_provider is not None and self.default_provider not in self.providers:
            logger.warning(
                f"Default provider '{self.default_provider}' is not configured in providers. "
                f"Available providers: {list(self.providers.keys()) if self.providers else 'none'}"
            )

    def get_provider(self, name: str) -> Optional[ProviderConfig]:
        """Get a provider configuration by name."""
        return self.providers.get(name)

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for TOML serialization."""
        result: Dict[str, Any] = {}
        if self.default_provider is not None:
            result["default_provider"] = self.default_provider
        # Add each provider's config
        for name, provider_config in self.providers.items():
            result[name] = provider_config.to_dict()
        return result

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "LLMConfig":
        """Create LLMConfig from dictionary."""
        # Get default_provider from data, or None if not present
        default_provider = data.get("default_provider")

        # Parse providers
        providers: Dict[str, ProviderConfig] = {}
        for key, value in data.items():
            if key == "default_provider" or not isinstance(value, dict):
                continue

            # Get the appropriate provider class and instantiate
            try:
                provider_class = get_provider_class(key)
                providers[key] = provider_class.from_dict(value)
            except (ValueError, InvalidProviderConfigError) as e:
                logger.warning(f"Skipping invalid provider '{key}': {e}")

        config = cls(
            default_provider=default_provider,
            providers=providers,
        )
        return config


@dataclass
class RolesConfig:
    """Configuration for roles."""

    default_role: str = DEFAULT_ROLE_NAME

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for TOML serialization."""
        return {"default_role": self.default_role}


@dataclass
class WhaiConfig:
    """Main whai configuration."""

    llm: LLMConfig
    roles: RolesConfig

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for TOML serialization."""
        return {
            "llm": self.llm.to_dict(),
            "roles": self.roles.to_dict(),
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "WhaiConfig":
        """Create WhaiConfig from dictionary."""
        llm_data = data.get("llm", {})
        roles_data = data.get("roles", {})

        return cls(
            llm=LLMConfig.from_dict(llm_data),
            roles=RolesConfig(
                default_role=roles_data.get("default_role", DEFAULT_ROLE_NAME)
            ),
        )

    @classmethod
    def from_file(cls, path: Path) -> "WhaiConfig":
        """Load configuration from a file (TOML format)."""
        with open(path, "rb") as f:
            data = tomllib.load(f)
        return cls.from_dict(data)

    def to_file(self, path: Path) -> None:
        """Save configuration to a file (TOML format)."""
        path.parent.mkdir(parents=True, exist_ok=True)
        with open(path, "wb") as f:
            tomli_w.dump(self.to_dict(), f)


def get_config_dir() -> Path:
    """Get the whai configuration directory."""
    if os.name == "nt":  # Windows
        config_base = Path(
            os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming")
        )
    else:  # Unix-like
        config_base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))

    return config_base / "whai"


def get_config_path() -> Path:
    """Get the path to the configuration file."""
    return (get_config_dir() / CONFIG_FILENAME).resolve()


def load_config(path: Optional[Path] = None) -> WhaiConfig:
    """
    Load configuration from default path or specified path.

    Args:
        path: Optional path to config file. If None, uses default path.

    Returns:
        WhaiConfig instance containing configuration settings.

    Raises:
        MissingConfigError: If config file doesn't exist.
    """
    if path is None:
        config_file = get_config_path()

        # Handle missing config file - return default config for tests
        if not config_file.exists():
            # Check for test mode (only honor when actually running pytest)
            is_test_mode_env = os.getenv(ENV_WHAI_TEST_MODE) == "1"
            is_running_pytest = "PYTEST_CURRENT_TEST" in os.environ

            if is_test_mode_env and is_running_pytest:
                logger.warning(
                    "Config missing; returning ephemeral defaults for test mode"
                )
                # Return minimal test config
                return WhaiConfig(
                    llm=LLMConfig(
                        default_provider=DEFAULT_PROVIDER,
                        providers={
                            "openai": OpenAIConfig(
                                api_key="test-key",
                                default_model=DEFAULT_MODEL_OPENAI,
                            )
                        },
                    ),
                    roles=RolesConfig(default_role=DEFAULT_ROLE_NAME),
                )

            raise MissingConfigError(
                f"Configuration file not found at {config_file}. "
                f"Run 'whai --interactive-config' to create your configuration."
            )

        path = config_file

    logger.debug("Configuration loaded from %s", path, extra={"category": "config"})
    return WhaiConfig.from_file(path)


def save_config(config: WhaiConfig, path: Optional[Path] = None) -> None:
    """
    Save configuration to default path or specified path.

    Args:
        config: WhaiConfig instance to save.
        path: Optional path to save to. If None, uses default path.
    """
    if path is None:
        path = get_config_path()

    config.to_file(path)
    logger.info("Configuration saved to %s", path)
