"""
Environment-only settings that are loaded early and never stored in database.

These settings are:
1. Required before database initialization
2. Used for testing/CI configuration
3. System bootstrap configuration

They are accessed through SettingsManager but always read from environment variables.

Why some settings must be environment-only:
- Bootstrap settings (paths, encryption keys) are needed to initialize the database itself
- Database configuration settings must be available before connecting to the database
- Testing flags need to be checked before any database operations occur
- CI/CD variables control build-time behavior before the application starts

These settings cannot be stored in the database because they are prerequisites for
accessing the database. This creates a bootstrapping requirement where certain
configuration must come from the environment to establish the system state needed
to access persisted settings.
"""

import os
from pathlib import Path
from typing import Any, Dict, Optional, List, Set, TypeVar, Generic
from abc import ABC, abstractmethod
from loguru import logger

from .exceptions import (
    EnvironmentPathNotFoundError,
    EnvironmentValueRangeError,
    InvalidEnvironmentValueError,
    MissingEnvironmentVariableError,
)

T = TypeVar("T")


class EnvSetting(ABC, Generic[T]):
    """Base class for all environment settings."""

    def __init__(
        self,
        key: str,
        description: str,
        default: Optional[T] = None,
        required: bool = False,
        deprecated_env_var: Optional[str] = None,
    ):
        self.key = key
        # Auto-generate env_var from key
        # e.g., "testing.test_mode" -> "LDR_TESTING_TEST_MODE"
        self.env_var = "LDR_" + key.upper().replace(".", "_")
        self.description = description
        self.default = default
        self.required = required
        self.deprecated_env_var = deprecated_env_var

    def get_value(self) -> Optional[T]:
        """Get the value from environment with type conversion."""
        raw = self._get_raw_value()
        if raw is None:
            if self.required and self.default is None:
                raise MissingEnvironmentVariableError(self.env_var)
            return self.default
        return self._convert_value(raw)

    @abstractmethod
    def _convert_value(self, raw: str) -> T:
        """Convert raw string value to the appropriate type."""
        pass

    def _get_raw_value(self) -> Optional[str]:
        """Get raw string value from environment.

        Checks the canonical env var first. If not set and a deprecated
        alias is configured, falls back to the deprecated name with a
        warning guiding users to migrate.
        """
        value = os.environ.get(self.env_var)
        if value is not None:
            return value

        if self.deprecated_env_var:
            deprecated_value = os.environ.get(self.deprecated_env_var)
            if deprecated_value is not None:
                logger.warning(
                    f"Environment variable '{self.deprecated_env_var}' is deprecated "
                    f"and will be removed in a future release. "
                    f"Please use '{self.env_var}' instead."
                )
                return deprecated_value

        return None

    @property
    def is_set(self) -> bool:
        """Check if the environment variable is set."""
        return self.env_var in os.environ

    def __repr__(self) -> str:
        """String representation for debugging."""
        return f"{self.__class__.__name__}(key='{self.key}', env_var='{self.env_var}')"


class BooleanSetting(EnvSetting[bool]):
    """Boolean environment setting."""

    def __init__(
        self,
        key: str,
        description: str,
        default: bool = False,
        deprecated_env_var: Optional[str] = None,
    ):
        super().__init__(
            key, description, default, deprecated_env_var=deprecated_env_var
        )

    def _convert_value(self, raw: str) -> bool:
        """Convert string to boolean."""
        return raw.lower() in ("true", "1", "yes", "on", "enabled")


class StringSetting(EnvSetting[str]):
    """String environment setting."""

    def __init__(
        self,
        key: str,
        description: str,
        default: Optional[str] = None,
        required: bool = False,
        deprecated_env_var: Optional[str] = None,
    ):
        super().__init__(
            key,
            description,
            default,
            required,
            deprecated_env_var=deprecated_env_var,
        )

    def _convert_value(self, raw: str) -> str:
        """Return string value as-is."""
        return raw


class IntegerSetting(EnvSetting[int]):
    """Integer environment setting."""

    def __init__(
        self,
        key: str,
        description: str,
        default: Optional[int] = None,
        min_value: Optional[int] = None,
        max_value: Optional[int] = None,
        deprecated_env_var: Optional[str] = None,
    ):
        super().__init__(
            key, description, default, deprecated_env_var=deprecated_env_var
        )
        self.min_value = min_value
        self.max_value = max_value

    def _convert_value(self, raw: str) -> Optional[int]:
        """Convert string to integer with validation."""
        try:
            value = int(raw)
        except ValueError:
            logger.warning(
                f"Invalid integer value '{raw}' for {self.env_var}, using default: {self.default}"
            )
            return self.default

        if self.min_value is not None and value < self.min_value:
            raise EnvironmentValueRangeError(
                self.env_var, value, min_val=self.min_value
            )
        if self.max_value is not None and value > self.max_value:
            raise EnvironmentValueRangeError(
                self.env_var, value, max_val=self.max_value
            )
        return value


class PathSetting(StringSetting):
    """Path environment setting with validation."""

    def __init__(
        self,
        key: str,
        description: str,
        default: Optional[str] = None,
        must_exist: bool = False,
        create_if_missing: bool = False,
    ):
        super().__init__(key, description, default)
        self.must_exist = must_exist
        self.create_if_missing = create_if_missing

    def get_value(self) -> Optional[str]:
        """Get path value with optional validation/creation."""
        path_str = super().get_value()
        if path_str is None:
            return None

        # Use pathlib for path operations
        path = Path(path_str).expanduser()
        # Expand environment variables manually since pathlib doesn't have expandvars
        # Note: os.path.expandvars is kept here as there's no pathlib equivalent
        # noqa: PLR0402 - Suppress pathlib check for this line
        path_str = os.path.expandvars(str(path))
        path = Path(path_str).resolve()

        if self.create_if_missing and not path.exists():
            try:
                path.mkdir(parents=True, exist_ok=True)
            except OSError:
                logger.warning("Failed to create directory")
        elif self.must_exist and not path.exists():
            # Only raise if explicitly required to exist
            raise EnvironmentPathNotFoundError(self.env_var, path)

        return str(path)


class SecretSetting(StringSetting):
    """Secret/sensitive environment setting."""

    def __init__(
        self,
        key: str,
        description: str,
        default: Optional[str] = None,
        required: bool = False,
    ):
        super().__init__(key, description, default, required)

    def __repr__(self) -> str:
        """Hide the value in string representation."""
        return f"SecretSetting(key='{self.key}', value=***)"

    def __str__(self) -> str:
        """Hide the value in string conversion."""
        value = "SET" if self.is_set else "NOT SET"
        return f"{self.key}=<{value}>"


class EnumSetting(EnvSetting[str]):
    """Enum-style setting with allowed values."""

    def __init__(
        self,
        key: str,
        description: str,
        allowed_values: Set[str],
        default: Optional[str] = None,
        case_sensitive: bool = False,
        deprecated_env_var: Optional[str] = None,
    ):
        super().__init__(
            key, description, default, deprecated_env_var=deprecated_env_var
        )
        self.allowed_values = allowed_values
        self.case_sensitive = case_sensitive

        # Store lowercase versions for case-insensitive comparison
        if not case_sensitive:
            self._allowed_lower = {v.lower() for v in allowed_values}
            # Create a mapping from lowercase to original case
            self._canonical_map = {v.lower(): v for v in allowed_values}

    def _convert_value(self, raw: str) -> str:
        """Convert and validate value against allowed values."""
        if self.case_sensitive:
            if raw not in self.allowed_values:
                raise InvalidEnvironmentValueError(
                    self.env_var, raw, list(self.allowed_values)
                )
            return raw
        # Case-insensitive matching
        raw_lower = raw.lower()
        if raw_lower not in self._allowed_lower:
            raise InvalidEnvironmentValueError(
                self.env_var, raw, list(self.allowed_values)
            )
        # Return the canonical version (from allowed_values)
        return self._canonical_map[raw_lower]


class SettingsRegistry:
    """Registry for all environment settings."""

    def __init__(self):
        self._settings: Dict[str, EnvSetting] = {}
        self._categories: Dict[str, List[EnvSetting]] = {}

    def register_category(self, category: str, settings: List[EnvSetting]):
        """Register a category of settings."""
        self._categories[category] = settings
        for setting in settings:
            self._settings[setting.key] = setting

    def get(self, key: str, default: Optional[Any] = None) -> Any:
        """
        Get a setting value.

        Args:
            key: Setting key (e.g., "testing.test_mode")
            default: Default value if not set or on error

        Returns:
            Setting value or default
        """
        setting = self._settings.get(key)
        if not setting:
            return default

        try:
            value = setting.get_value()
            # Use provided default if setting returns None
            return value if value is not None else default
        except ValueError:
            logger.warning(
                "Validation error for setting '{}', using default", key
            )
            return default

    def get_setting_object(self, key: str) -> Optional[EnvSetting]:
        """Get the setting object itself for introspection."""
        return self._settings.get(key)

    def is_env_only(self, key: str) -> bool:
        """Check if a key is an env-only setting."""
        return key in self._settings

    def get_env_var(self, key: str) -> Optional[str]:
        """Get the environment variable name for a key."""
        setting = self._settings.get(key)
        return setting.env_var if setting else None

    def get_all_env_vars(self) -> Dict[str, str]:
        """Get all environment variables and descriptions."""
        return {
            setting.env_var: setting.description
            for setting in self._settings.values()
        }

    def get_category_settings(self, category: str) -> List[EnvSetting]:
        """Get all settings in a category."""
        return self._categories.get(category, [])

    def get_bootstrap_vars(self) -> Dict[str, str]:
        """Get bootstrap environment variables (bootstrap + db_config)."""
        result = {}
        for category in ["bootstrap", "db_config"]:
            for setting in self._categories.get(category, []):
                result[setting.env_var] = setting.description
        return result

    def get_testing_vars(self) -> Dict[str, str]:
        """Get testing environment variables."""
        result = {}
        for setting in self._categories.get("testing", []):
            result[setting.env_var] = setting.description
        return result

    def list_all_settings(self) -> List[str]:
        """List all registered setting keys."""
        return list(self._settings.keys())


# Export list for better IDE discovery
__all__ = [
    "EnvSetting",
    "BooleanSetting",
    "StringSetting",
    "IntegerSetting",
    "PathSetting",
    "SecretSetting",
    "EnumSetting",
    "SettingsRegistry",
]
