"""
Custom exceptions and warnings for SafeConfig.

This module defines all exceptions raised by the SafeConfig library,
giving callers fine-grained control over error handling.
"""

from __future__ import annotations


class ConfigurationError(Exception):
    """Base exception for all SafeConfig errors.

    All library-specific exceptions inherit from this class, so callers
    can catch everything with a single ``except ConfigurationError`` clause.
    """


class KeyNotFoundError(ConfigurationError):
    """Raised when a requested configuration key does not exist.

    Attributes:
        key: The key that was not found.
        searched_sources: List of sources that were searched.
    """

    def __init__(
        self,
        key: str,
        searched_sources: list[str] | None = None,
    ) -> None:
        self.key = key
        self.searched_sources = searched_sources or []
        sources = ", ".join(self.searched_sources) if self.searched_sources else "all sources"
        super().__init__(f"Key '{key}' not found in {sources}.")


class MissingRequiredKeyError(ConfigurationError):
    """Raised during startup when one or more required keys are absent.

    Attributes:
        missing_keys: Set of keys that are missing.
    """

    def __init__(self, missing_keys: set[str]) -> None:
        self.missing_keys = missing_keys
        formatted = ", ".join(f"'{k}'" for k in sorted(missing_keys))
        super().__init__(
            f"Required configuration key(s) are missing: {formatted}. "
            "Set them in your .env file or environment before starting the application."
        )


class EncryptionError(ConfigurationError):
    """Raised when encryption of a value fails.

    Attributes:
        key: The configuration key being encrypted.
        reason: Human-readable explanation of the failure.
    """

    def __init__(self, key: str, reason: str) -> None:
        self.key = key
        self.reason = reason
        super().__init__(f"Failed to encrypt value for key '{key}': {reason}")


class DecryptionError(ConfigurationError):
    """Raised when decryption of a value fails.

    This usually means the master key has changed or the stored
    ciphertext is corrupted.

    Attributes:
        key: The configuration key being decrypted.
        reason: Human-readable explanation of the failure.
    """

    def __init__(self, key: str, reason: str) -> None:
        self.key = key
        self.reason = reason
        super().__init__(
            f"Failed to decrypt value for key '{key}': {reason}. "
            "The master key may have changed or the value is corrupted."
        )


class InvalidEnvironmentError(ConfigurationError):
    """Raised when an unrecognised environment name is provided.

    Attributes:
        environment: The invalid environment string supplied.
        valid_environments: Collection of valid environment names.
    """

    def __init__(self, environment: str, valid_environments: list[str]) -> None:
        self.environment = environment
        self.valid_environments = valid_environments
        super().__init__(
            f"Unknown environment '{environment}'. "
            f"Valid choices are: {', '.join(valid_environments)}."
        )


class MasterKeyError(ConfigurationError):
    """Raised when the master encryption key cannot be loaded or is invalid.

    Attributes:
        reason: Human-readable explanation of the failure.
    """

    def __init__(self, reason: str) -> None:
        self.reason = reason
        super().__init__(f"Master key error: {reason}")


class ExposedKeyWarning(UserWarning):
    """Warning emitted when a potential secret is found hard-coded in source files.

    This is a *warning*, not an exception, so it does not abort execution.
    Suppress it with ``warnings.filterwarnings("ignore", category=ExposedKeyWarning)``
    if the detection produces false positives.
    """
