"""Configuration settings for MCA SDK.

This module provides the MCAConfig dataclass for managing SDK configuration
with support for multiple configuration sources.
"""

import logging
import os
import threading
import warnings
from dataclasses import dataclass, field
from ipaddress import AddressValueError, ip_address
from typing import Any, Callable, List, Optional, TypeVar
from urllib.parse import urlparse

from ..utils.exceptions import ConfigurationError

# Module logger
logger = logging.getLogger(__name__)

# Type variable for generic return type
T = TypeVar("T")

# Security warning category


class SecurityWarning(UserWarning):
    """Warning category for security-related issues."""


def sanitize_token(message: str, token: Optional[str]) -> str:
    """Remove token from message if present, replacing with masked version.

    Uses exact string matching (not regex) to avoid performance overhead while
    still being effective for token sanitization. Tokens are typically long,
    unique strings, making false-positive matches extremely unlikely.

    DESIGN DECISION: Simple string replacement is acceptable because:
    1. Registry tokens are 20+ character random strings (very low collision probability)
    2. Regex overhead not justified for this use case
    3. Word boundaries inappropriate (tokens can appear in URLs, headers, etc.)
    4. False positives would only mask identical substrings (acceptable trade-off)

    Args:
        message: Message that may contain token
        token: Token to sanitize (if present in message)

    Returns:
        Message with token replaced by masked version (***XXXX)
    """
    if not token or token not in message:
        return message

    # Show last 4 characters for debugging, mask the rest
    masked = f"***{token[-4:]}" if len(token) > 4 else "****"
    return message.replace(token, masked)


class TokenSanitizingFilter(logging.Filter):
    """SDK-specific logging filter that sanitizes registry tokens from log records.

    SECURITY: Protects against token exposure when SDK code logs tokens directly.
    This filter is OPTIONAL and should ONLY be applied to SDK-specific loggers,
    never to the root logger (that would be library overreach).

    DESIGN RATIONALE - String Replacement Approach:
    This filter uses exact string matching/replacement rather than leveraging
    SensitiveString objects because:

    1. **Defense-in-depth scenario**: Filter catches cases where developers
       explicitly access token.value and then log the actual string:
       `logger.info(f"Token: {config.registry_token.value}")`
       At this point, the logged value is a plain string, not a SensitiveString.

    2. **No alternative exists**: Once .value is accessed and interpolated into
       a log message, we can only sanitize via string replacement.

    3. **Acceptable trade-offs**:
       - Token collision risk: Minimal (tokens are 20+ char random strings)
       - False positive impact: If collision occurs, only masks identical substring
       - Performance: String replacement is O(n), acceptable for logging path

    DESIGN: Thread-safe for use in multi-threaded applications.

    Usage (SDK-internal only):
        filter = TokenSanitizingFilter()
        filter.set_token(config.registry_token)
        # Apply ONLY to SDK logger, not root logger
        logging.getLogger('mca_sdk').addFilter(filter)

    NOT RECOMMENDED for application code - applications should not log tokens directly.
    This filter exists as defense-in-depth for SDK internal logging only.
    """

    def __init__(self) -> None:
        """Initialize the filter with thread-safe token storage."""
        super().__init__()
        self._tokens: List[str] = []
        self._lock = threading.RLock()  # Re-entrant lock for thread safety

    def set_token(self, token: Optional[str]) -> None:
        """Register a token to be sanitized from log messages (thread-safe).

        Args:
            token: Token to sanitize (can be None)
        """
        if not token:
            return

        with self._lock:
            if token not in self._tokens:
                self._tokens.append(token)

    def filter(self, record: logging.LogRecord) -> bool:
        """Sanitize tokens from log record before it's formatted (thread-safe).

        Args:
            record: Log record to sanitize

        Returns:
            True (always pass the record through after sanitization)
        """
        with self._lock:
            # Create a snapshot of tokens to avoid holding lock during sanitization
            tokens_snapshot = list(self._tokens)

        # Sanitize message
        if record.msg and isinstance(record.msg, str):
            for token in tokens_snapshot:
                record.msg = sanitize_token(record.msg, token)

        # Sanitize args (used in formatted messages like "Token: %s")
        if record.args:
            sanitized_args = []
            for arg in record.args:
                if isinstance(arg, str):
                    for token in tokens_snapshot:
                        arg = sanitize_token(arg, token)
                sanitized_args.append(arg)
            record.args = tuple(sanitized_args)

        return True


class SensitiveString:
    """Opaque secret container providing technical enforcement against exposure.

    SECURITY MODEL - Technical Guarantees:
    ✅ NO public .value property - cannot trivially extract raw secret
    ✅ NO hashability (__hash__ = None) - prevents dict keys and fingerprinting
    ✅ NO equality comparison (__eq__ raises) - prevents timing attacks and correlation
    ✅ Callback pattern - raw secret NEVER exists as variable in calling frame

    CALLBACK PATTERN - How to Use Safely:
    Use __unsafe_process_value(processor: Callable[[str], T]) -> T to access the secret.
    The processor function receives the raw secret and returns a result WITHOUT exposing
    the secret to the calling frame.

    SECURE USAGE EXAMPLES:

    1. Bearer Token Authentication:
        headers = token._SensitiveString__unsafe_process_value(
            lambda t: {"Authorization": f"Bearer {t}"}
        )

    2. API Key Header:
        headers = api_key._SensitiveString__unsafe_process_value(
            lambda k: {"X-API-Key": k}
        )

    3. HMAC Signing:
        import hmac
        signature = secret._SensitiveString__unsafe_process_value(
            lambda s: hmac.new(s.encode(), message.encode(), 'sha256').hexdigest()
        )

    4. Complex Processing (use concise lambdas):
        result = token._SensitiveString__unsafe_process_value(
            lambda t: some_auth_library.create_token(t, expires_in=3600)
        )

    DO'S AND DON'TS:

    ✅ DO: Keep processor functions concise and simple
    ✅ DO: Use lambdas for simple operations
    ✅ DO: Pass token directly to library functions
    ✅ DO: Return results immediately

    ❌ DON'T: Assign token to variables inside processor
        # INSECURE - token stored in variable
        token._SensitiveString__unsafe_process_value(
            lambda t: (stored_token := t, create_header(stored_token))
        )

    ❌ DON'T: Log the raw token inside processor
        # INSECURE - token appears in logs
        token._SensitiveString__unsafe_process_value(
            lambda t: (logger.debug(f"Token: {t}"), {"Authorization": f"Bearer {t}"})
        )

    ❌ DON'T: Create complex processor functions with extensive logic
        # RISKY - if exception occurs inside, token in traceback
        def complex_processor(token_val):
            # Many lines of code...
            result = some_operation(token_val)  # Exception here exposes token_val
            return result

    REMAINING RISKS (Processor Function):
    ⚠️ If exception occurs INSIDE the processor function, the token parameter (token_val)
       may appear in that specific frame's traceback. Keep processors simple and defensive.

    ⚠️ This is the responsibility of the code providing the processor. The SensitiveString
       guarantees the token won't leak from the CALLING frame, but cannot control what
       happens inside the processor.

    DESIGN RATIONALE:
    - __eq__ disabled: Prevents timing attacks and cross-context correlation
    - __hash__ disabled: Prevents use as dict key or set member (creates stable fingerprints)
    - str()/repr() masked: Safe display in logs and tracebacks
    - Callback pattern: Minimizes token lifetime and scope to smallest possible window

    LIMITATIONS (Inherent to Python):
    - Can be bypassed via object.__getattribute__(obj, '_secret') - requires C extensions to prevent
    - Assumes trusted runtime environment (not actively bypassing protections)
    - This is as secure as possible in pure Python

    Example:
        token = SensitiveString("secret-token-xyz")
        str(token)     # Returns "***-xyz" (masked)
        repr(token)    # Returns "SensitiveString(***-xyz)" (masked)

        # SAFE: Callback pattern
        headers = token._SensitiveString__unsafe_process_value(
            lambda t: {"Authorization": f"Bearer {t}"}
        )
        # 't' only existed inside lambda, never in this frame's variables
    """

    def __init__(self, value: str):
        """Initialize with secret value.

        Args:
            value: The secret string to protect
        """
        # Use object.__setattr__ to bypass any __setattr__ overrides
        object.__setattr__(self, "_secret", value if value is not None else "")

    def __repr__(self) -> str:
        """Mask value in repr to prevent exposure in tracebacks."""
        secret = object.__getattribute__(self, "_secret")
        if not secret or len(secret) == 0:
            return "SensitiveString(<empty>)"
        elif len(secret) <= 4:
            return "SensitiveString(****)"
        else:
            return f"SensitiveString(***{secret[-4:]})"

    def __str__(self) -> str:
        """Mask value in str to prevent exposure in logs."""
        secret = object.__getattribute__(self, "_secret")
        if not secret or len(secret) == 0:
            return "<empty>"
        elif len(secret) <= 4:
            return "****"
        else:
            return f"***{secret[-4:]}"

    def __unsafe_process_value(self, processor: Callable[[str], T]) -> T:
        """Process secret value via callback WITHOUT exposing it to calling frame.

        SECURITY: This method ensures the raw secret NEVER exists as a variable in
        the calling frame. The secret is passed directly to the processor function,
        existing only in the processor's local scope. If an exception occurs in the
        calling frame, the traceback will NOT contain the raw token.

        This is the ONLY way to access the secret value. By requiring a callback,
        we technically enforce that the secret is used immediately and never stored.

        NAME MANGLING: The double underscore prefix (__unsafe_process_value) triggers
        Python's name mangling, making this method harder to access accidentally.
        From outside the class, it must be called as:
            token._SensitiveString__unsafe_process_value(processor)

        This is intentional - the awkward syntax serves as a warning that you're
        accessing sensitive data. It's not security-through-obscurity (since the
        method is documented), but a usability pattern that makes unsafe operations
        explicit and harder to stumble into by accident.

        Args:
            processor: Callable[[str], T] that receives the raw secret and returns a result

        Returns:
            The result of processor(secret_value)

        Example:
            # SAFE: Secret only exists inside lambda, never in caller's variables
            header = token._SensitiveString__unsafe_process_value(
                lambda token_val: {"Authorization": f"Bearer {token_val}"}
            )

            # Even if exception occurs here, traceback shows NO token
            some_function_that_might_raise()
        """
        secret = object.__getattribute__(self, "_secret")
        # Secret passed directly to processor, never returned to calling frame
        return processor(secret if secret else "")

    def __eq__(self, other: object) -> bool:
        """Prevent equality comparison to avoid timing attacks and correlation.

        Secrets should be opaque - comparison creates security risks:
        - Timing attacks via non-constant-time comparison
        - Cross-context correlation via equality checks
        - Information leakage via truth value

        Raises:
            TypeError: Always - secrets cannot be compared
        """
        raise TypeError(
            "Comparison of SensitiveString objects is disabled for security reasons. "
            "Secrets must be treated as opaque and should not be compared directly. "
            "This prevents timing attacks and cross-context correlation."
        )

    # Explicitly disable hashing to prevent creating stable fingerprints
    # Making secrets hashable would allow:
    # - Use as dict keys or set members
    # - Creation of stable, predictable fingerprints
    # - Cross-context correlation
    # - Information leakage
    # This is intentionally disabled for security.
    __hash__ = None


def _is_loopback_hostname(hostname: str) -> bool:
    """Check if a hostname is a loopback address.

    Handles:
    - Literal localhost
    - IPv4 loopback (127.0.0.0/8)
    - IPv6 loopback (::1)
    - Case-insensitive matching
    - IPv6 bracket notation

    Args:
        hostname: The hostname to check

    Returns:
        True if hostname is a loopback address, False otherwise
    """
    if not hostname:
        return False

    # Normalize to lowercase for case-insensitive comparison (RFC 4343)
    hostname = hostname.lower().strip()

    # Remove IPv6 brackets if present
    if hostname.startswith("[") and hostname.endswith("]"):
        hostname = hostname[1:-1]

    # Check for literal "localhost"
    if hostname == "localhost":
        return True

    # Check if it's an IP address in loopback range
    try:
        addr = ip_address(hostname)
        return addr.is_loopback
    except (AddressValueError, ValueError):
        # Not a valid IP address, treat as domain name
        return False


def _validate_endpoint_url(
    url: str, endpoint_name: str, require_format_check: bool = False, allow_insecure: bool = False
) -> None:
    """Validate endpoint URL format and security.

    Performs comprehensive validation:
    1. Protocol validation (must be http:// or https://)
    2. Optional format validation for better error messages
    3. Security validation (HTTPS required for non-localhost)

    Args:
        url: The URL to validate
        endpoint_name: Name for error messages (e.g., "collector_endpoint", "registry_url")
        require_format_check: If True, provide enhanced error messages for malformed URLs
        allow_insecure: If True, allow HTTP for non-localhost (development only)

    Raises:
        ConfigurationError: If validation fails
    """
    # Step 1: Validate URL has http:// or https:// protocol
    if not (url.startswith("http://") or url.startswith("https://")):
        # Enhanced error message for registry URLs with format check
        if require_format_check:
            # Check if it looks like a URL at all (has dot or colon)
            if "." not in url and ":" not in url:
                raise ConfigurationError(
                    f"Invalid {endpoint_name} format: {url}. "
                    "Expected a valid URL with http:// or https:// protocol."
                )
            else:
                raise ConfigurationError(
                    f"{endpoint_name} must use http:// or https:// protocol. Got: {url}"
                )
        else:
            # Standard error for endpoints
            raise ConfigurationError(
                f"{endpoint_name} must start with http:// or https://. Got: {url}"
            )

    # Step 2: Security validation - require HTTPS for non-localhost
    if url.startswith("http://"):
        parsed = urlparse(url)
        hostname = parsed.hostname or ""

        if not _is_loopback_hostname(hostname):
            if not allow_insecure:
                raise ConfigurationError(
                    f"{endpoint_name} must use HTTPS for non-localhost endpoints. "
                    f"HTTP transmits data unencrypted. Got: {url}"
                )
            else:
                # Log warning when using insecure HTTP in development
                import logging

                logger = logging.getLogger(__name__)
                logger.warning(
                    f"{endpoint_name} using insecure HTTP: {url}. "
                    f"Only use allow_insecure_collector=True in development/testing."
                )


@dataclass
class MCAConfig:
    """Configuration for MCA SDK.

    Supports loading from multiple sources with precedence:
    kwargs > environment variables > YAML file > defaults

    Attributes:
        service_name: Name of the service (required)
        model_id: Unique model identifier
        model_version: Model version string
        team_name: Team responsible for the model
        model_type: Type of model (internal, generative, vendor)

        collector_endpoint: OTLP collector endpoint
        collector_timeout: Timeout for collector requests in seconds

        buffering_enabled: Whether to enable buffering
        max_queue_size: Maximum number of items in queue
        retry_attempts: Number of retry attempts
        base_delay: Initial retry delay in seconds (default: 1.0)
        max_delay: Maximum retry delay cap in seconds (default: 30.0)
        backoff_multiplier: Exponential backoff multiplier (default: 2.0)
        persist_queue: Whether to persist queue to disk
        persist_path: Path for queue persistence

        strict_validation: Whether to enforce strict validation
        metric_export_interval_ms: Interval for metric export in milliseconds
        log_batch_size: Batch size for log export
        trace_batch_size: Batch size for trace export

        registry_url: URL of the model registry service
        registry_token: Bearer token for registry authentication
        refresh_interval_secs: Interval for refreshing config from registry
        prefer_registry: Whether registry values override local config
        deployment_id: Optional deployment identifier for registry lookup

        gcp_trace_enabled: Enable GCP Cloud Trace exporter
        gcp_project_id: GCP project ID for Cloud Trace

    Environment Variables:
        MCA_GCP_TRACE_ENABLED: Enable GCP Cloud Trace exporter (true/false)
        MCA_GCP_PROJECT_ID: GCP project ID for Cloud Trace (overrides credentials)
    """

    # Model metadata (required)
    service_name: str

    # Model metadata (optional)
    model_id: Optional[str] = None
    model_version: str = "0.3.0"
    team_name: Optional[str] = None
    model_type: str = "regression"  # regression, time-series, classification, generative, agentic
    model_category: str = "internal"  # internal or vendor (Epic 2 taxonomy)

    # Collector settings
    # SECURITY: Default to HTTP for localhost only
    # Production deployments should explicitly set HTTPS endpoint via MCA_COLLECTOR_ENDPOINT
    collector_endpoint: str = "http://localhost:4318"
    collector_timeout: int = 10
    collector_protocol: str = "http"  # "http" or "grpc" (Story 6.4 - mTLS typically uses gRPC)

    # Vendor Bridge settings
    vendor_api_url: Optional[str] = None
    vendor_name: Optional[str] = None
    vendor_bridge_poll_interval: float = 30.0
    vendor_bridge_max_iterations: Optional[int] = None

    # Buffering
    buffering_enabled: bool = False  # Disabled by default for backward compatibility
    max_queue_size: int = 1000
    retry_attempts: int = 3  # RetryPolicy attempts per export
    max_queue_retry_attempts: int = 3  # Cross-attempt limit (dequeue attempts)
    base_delay: float = 1.0  # Initial retry delay in seconds
    max_delay: float = 30.0  # Maximum retry delay cap (AC#3 - capped at 30 seconds)
    backoff_multiplier: float = 2.0  # Exponential backoff multiplier
    # Queue persistence (Story 3-4)
    persist_queue: bool = (
        False  # Disabled by default - issues HIPAA compliance warning when enabled
    )
    # NOTE: Exporters use signal-specific defaults (queue_metrics.json, queue_logs.json, queue_traces.json)
    # This setting is only used if explicitly provided to override the exporter defaults
    # SECURITY (Story 3.15): Path validation occurs at persistence time (buffering layer)
    # to prevent path traversal and unauthorized PHI exposure. Config intentionally accepts
    # any path for deployment flexibility; runtime validation enforces whitelisted directories.
    persist_path: str = field(default_factory=lambda: os.path.expanduser("~/.mca_sdk/queue.pkl"))
    persist_min_disk_space_mb: float = 10.0  # Minimum free disk space (MB) required for persistence
    persist_allowed_dirs: Optional[str] = (
        None  # Comma-separated list of additional allowed directories
    )

    # Circuit breaker settings (Story 3-3)
    circuit_breaker_enabled: bool = True  # Enable circuit breaker pattern
    circuit_breaker_fail_max: int = 5  # Consecutive failures before opening circuit
    circuit_breaker_timeout: int = 60  # Cool-down period in seconds before half-open

    # Development/Testing: Allow HTTP for trusted internal networks
    # WARNING: Only use in development. Production should always use HTTPS.
    allow_insecure_collector: bool = False  # Default: enforce HTTPS
    allow_insecure_registry: bool = False  # Default: enforce HTTPS

    # Validation
    strict_validation: bool = True

    # Debug mode
    debug_mode: bool = False  # Disabled by default for production

    # Telemetry attribute validation limits (Story 3.6)
    telemetry_max_attribute_length: int = 1024  # Maximum length for attribute values (chars)
    telemetry_max_attribute_count: int = 100  # Maximum number of attributes per event

    # Export intervals
    metric_export_interval_ms: int = 5000
    log_batch_size: int = 512  # Maximum batch size for log export
    trace_batch_size: int = 512  # Maximum batch size for trace export

    # Optional vendor-specific fields
    llm_provider: Optional[str] = None
    llm_model: Optional[str] = None

    # Registry integration
    registry_url: Optional[str] = None
    # SECURITY: Token stored as SensitiveString to prevent traceback exposure
    # Will be automatically wrapped on assignment via __setattr__
    registry_token: Optional[str] = None  # From env only, never logged
    refresh_interval_secs: int = 600  # 10 minutes
    prefer_registry: bool = True  # Registry > local when True
    deployment_id: Optional[str] = None  # Optional deployment indirection

    # Operational timeouts (Story 1.13.6 - Finding 9)
    version_check_timeout: float = 2.0  # Version check timeout in seconds (fast, non-blocking)
    cleanup_thread_poll_interval: float = (
        300.0  # Cleanup thread poll interval in seconds (5 minutes)
    )

    # Vendor Bridge Settings
    vendor_bridge_state_file: Optional[str] = None

    # GCP Authentication (Story 6-5)
    gcp_credentials_path: Optional[str] = None  # Path to service account JSON file
    gcp_credentials_json: Optional[str] = None  # Service account JSON as string (from env var)

    # GCP Cloud Logging (Story 6-2)
    gcp_logging_enabled: bool = False  # Enable GCP Cloud Logging export
    gcp_project_id: Optional[str] = (
        None  # GCP project ID (required if gcp_logging_enabled=True or gcp_trace_enabled=True)
    )
    gcp_log_name: Optional[str] = None  # Cloud Logging log name (defaults to service_name)
    gcp_log_min_severity: str = (
        "INFO"  # Minimum log severity to export (DEBUG, INFO, WARNING, ERROR, CRITICAL)
    )
    gcp_resource_type: str = "global"  # GCP monitored resource type
    gcp_log_timeout: float = 60.0  # Cloud Logging API timeout in seconds (default: 60s)

    # GCP Cloud Trace settings
    gcp_trace_enabled: bool = (
        False  # Enable GCP Cloud Trace exporter - exports traces directly to Cloud Trace
    )

    # Prometheus exporter settings (Story 6.1)
    prometheus_enabled: bool = False  # Disabled by default
    prometheus_port: int = 9464  # Standard Prometheus metrics port
    prometheus_host: str = "127.0.0.1"  # Bind to localhost for security

    # mTLS settings (Story 6.4)
    mtls_enabled: bool = False  # Disabled by default
    client_cert_path: Optional[str] = None  # Path to client certificate (PEM format)
    client_key_path: Optional[str] = None  # Path to client private key (PEM format)
    ca_cert_path: Optional[str] = None  # Path to CA certificate for server verification

    # SECURITY: Immutable set of sensitive field names requiring SensitiveString wrapping
    # frozenset prevents runtime modification (e.g., _SENSITIVE_FIELDS.clear())
    # Add new sensitive fields here instead of modifying __setattr__ logic
    _SENSITIVE_FIELDS = frozenset({"registry_token", "gcp_credentials_json"})

    def __setattr__(self, name: str, value: Any) -> None:
        """Intercept attribute assignment to enforce SensitiveString wrapping for sensitive fields.

        SECURITY: Automatically wraps sensitive fields in SensitiveString to provide
        defense-in-depth against accidental exposure.

        LIMITATIONS: This provides procedural protection, not technical guarantees:
        - Can be bypassed via object.__setattr__(config, name, value)
        - Only protects the wrapper; .value returns plain string that can leak in tracebacks
        """
        if (
            name in self._SENSITIVE_FIELDS
            and value is not None
            and not isinstance(value, SensitiveString)
        ):
            # Auto-wrap sensitive field in SensitiveString
            value = SensitiveString(value)
        super().__setattr__(name, value)

        # SECURITY: Apply filter when registry_token is set (after __init__)
        # This ensures filter is applied even when token is assigned after object creation
        if name == "registry_token" and value is not None and hasattr(self, "service_name"):
            # hasattr check ensures __init__ has completed (avoid issues during object construction)
            self._apply_token_sanitizing_filter()

    def __post_init__(self):
        """Validate configuration after initialization."""
        self._ensure_token_security()
        self._validate_types()
        self._validate_service_name()
        self._migrate_legacy_taxonomy()
        self._validate_taxonomy()
        self._validate_retry_config()
        self._validate_hipaa_compliance()
        self._validate_circuit_breaker()
        self._validate_endpoints()
        self._validate_file_paths()
        self._validate_gcp_logging()
        self._validate_mtls()

    def _validate_types(self) -> None:
        """Validate field types match expected types."""
        # Validate boolean fields dynamically using dataclass field introspection
        import dataclasses
        import typing

        for dc_field in dataclasses.fields(self):
            # Check if field type annotation is bool or Optional[bool]
            # Handle: bool, 'bool', Optional[bool], Union[bool, None]
            field_type = dc_field.type
            origin = typing.get_origin(field_type)
            args = typing.get_args(field_type)

            is_bool_field = (
                field_type is bool
                or field_type == "bool"
                or
                # Handle Optional[bool] which is Union[bool, None]
                (origin is typing.Union and bool in args)
            )

            if is_bool_field:
                value = getattr(self, dc_field.name)
                # Allow None for Optional[bool] fields
                if value is not None and not isinstance(value, bool):
                    raise ConfigurationError(
                        f"{dc_field.name} must be a boolean (true/false), got {type(value).__name__}: {value}"
                    )

    def _ensure_token_security(self) -> None:
        """Wrap registry token in SensitiveString and apply filter."""
        if self.registry_token is not None and not isinstance(self.registry_token, SensitiveString):
            self.registry_token = SensitiveString(self.registry_token)

        if self.registry_token is not None:
            self._apply_token_sanitizing_filter()

    def _validate_service_name(self) -> None:
        """Ensure service_name is provided and valid."""
        if not self.service_name:
            raise ConfigurationError(
                "service_name is required. "
                "Set via: MCA_SERVICE_NAME environment variable, YAML config file, or pass as kwarg to MCAConfig()"
            )

        # Validate type
        if not isinstance(self.service_name, str):
            raise ConfigurationError(
                f"service_name must be a string, got {type(self.service_name).__name__}: {self.service_name}"
            )

        # Validate not whitespace-only
        if not self.service_name.strip():
            raise ConfigurationError(
                "service_name cannot be whitespace-only. Provide a valid service name."
            )

    def _migrate_legacy_taxonomy(self) -> None:
        """Auto-migrate legacy model_type values."""
        from ..utils.routing import migrate_legacy_model_type

        unambiguous_legacy_values = ["internal", "vendor"]
        if self.model_type not in unambiguous_legacy_values:
            return

        original_model_type = self.model_type

        warnings.warn(
            f"DEPRECATED: model_type='{original_model_type}' is deprecated. "
            f"The SDK now uses a two-field taxonomy: model_category + model_type. "
            f"Your configuration will be automatically migrated, but please update to: "
            f"model_category='internal' or 'vendor', model_type='regression', 'time-series', 'classification', 'generative', or 'agentic'. "
            f"See migration guide for details.",
            DeprecationWarning,
            stacklevel=4,
        )

        category, new_type = migrate_legacy_model_type(original_model_type)
        self.model_category = category
        self.model_type = new_type

        logging.info(
            "Auto-migrated legacy model_type: '%s' → model_category='%s', model_type='%s'",
            original_model_type,
            category,
            new_type,
        )

    def _validate_taxonomy(self) -> None:
        """Validate model_category and model_type."""
        if self.model_category not in ["internal", "vendor"]:
            raise ConfigurationError(
                f"model_category must be one of: internal, vendor. Got: {self.model_category}"
            )

        valid_model_types = ["regression", "time-series", "classification", "generative", "agentic"]
        if self.model_type not in valid_model_types:
            raise ConfigurationError(
                f"model_type must be one of: {', '.join(valid_model_types)}. Got: {self.model_type}"
            )

        if self.model_type == "generative" and self.model_category == "internal":
            logging.info(
                "Configuration uses model_type='generative' with default model_category='internal'. "
                "For clarity in the two-field taxonomy, consider explicitly setting both fields: "
                "model_category='internal', model_type='generative'. "
                "This ensures your configuration intent is clear and future-proof."
            )

    def _validate_retry_config(self) -> None:
        """Validate retry/backoff parameters."""
        if self.base_delay <= 0:
            raise ConfigurationError(f"base_delay must be positive. Got: {self.base_delay}")

        if self.max_delay < self.base_delay:
            raise ConfigurationError(
                f"max_delay ({self.max_delay}) must be >= base_delay ({self.base_delay})"
            )

        if self.backoff_multiplier <= 1.0:
            raise ConfigurationError(
                f"backoff_multiplier must be > 1.0. Got: {self.backoff_multiplier}"
            )

        # Validate numeric field types and values
        if not isinstance(self.collector_timeout, (int, float)) or self.collector_timeout <= 0:
            raise ConfigurationError(
                f"collector_timeout must be a positive number. Got: {self.collector_timeout}"
            )

        if not isinstance(self.max_queue_size, int) or self.max_queue_size <= 0:
            raise ConfigurationError(
                f"max_queue_size must be a positive integer. Got: {self.max_queue_size}"
            )

        if not isinstance(self.retry_attempts, int) or self.retry_attempts < 0:
            raise ConfigurationError(
                f"retry_attempts must be a non-negative integer. Got: {self.retry_attempts}"
            )

    def _validate_hipaa_compliance(self) -> None:
        """Validate HIPAA-related settings."""
        if not self.persist_queue:
            return

        platform_warning = ""
        if os.name == "nt":
            platform_warning = (
                "\nWARNING: Windows detected - file permissions (0o600/0o700) do NOT provide "
                "the same security as UNIX systems. Use filesystem-level encryption or ACLs. "
            )

        warnings.warn(
            "Queue persistence enabled WITHOUT encryption at rest. "
            "Persisted telemetry may contain PHI and is stored UNENCRYPTED. "
            "This is intended for development/testing only. "
            "For production use with PHI data, implement encryption at rest "
            "or ensure all telemetry is sanitized before instrumentation. "
            f"{platform_warning}"
            "See HIPAA compliance documentation for details.",
            SecurityWarning,
            stacklevel=3,
        )

        logging.warning(
            f"Disk persistence enabled at: {self.persist_path}. "
            f"Ensure this path has appropriate filesystem-level encryption "
            f"if telemetry may contain PHI."
        )

    def _validate_circuit_breaker(self) -> None:
        """Validate circuit breaker configuration."""
        if self.circuit_breaker_fail_max <= 0:
            raise ConfigurationError(
                f"circuit_breaker_fail_max must be positive. Got: {self.circuit_breaker_fail_max}"
            )

        if self.circuit_breaker_timeout <= 0:
            raise ConfigurationError(
                f"circuit_breaker_timeout must be positive. Got: {self.circuit_breaker_timeout}"
            )

        # Validate telemetry attribute limits for truncation logic safety
        TRUNCATE_SUFFIX_LENGTH = 11  # len("[truncated]")
        if self.telemetry_max_attribute_length < TRUNCATE_SUFFIX_LENGTH:
            logger.warning(
                f"telemetry_max_attribute_length ({self.telemetry_max_attribute_length}) is very small. "
                f"Values will be truncated without suffix."
            )

    def _validate_endpoints(self) -> None:
        """Validate endpoint URLs."""
        if self.collector_endpoint:
            _validate_endpoint_url(
                self.collector_endpoint,
                "collector_endpoint",
                allow_insecure=self.allow_insecure_collector,
            )

        if self.registry_url:
            _validate_endpoint_url(
                self.registry_url,
                "registry_url",
                require_format_check=True,
                allow_insecure=self.allow_insecure_registry,
            )

        # Validate Prometheus port is in valid range (1-65535)
        if not (1 <= self.prometheus_port <= 65535):
            raise ConfigurationError(
                f"prometheus_port must be between 1 and 65535. Got: {self.prometheus_port}"
            )

        # Validate vendor bridge settings
        self._validate_vendor_bridge()

    def _validate_vendor_bridge(self) -> None:
        """Validate vendor bridge configuration."""
        import os

        # Warn if vendor bridge state file is not configured in production
        if self.vendor_api_url and not self.vendor_bridge_state_file:
            env = os.getenv("ENVIRONMENT", "development").lower()
            if env in ("production", "prod", "staging"):
                logger.warning(
                    "vendor_bridge_state_file is not configured in production environment. "
                    "Cumulative metrics will be lost on restart. Set MCA_VENDOR_BRIDGE_STATE_FILE "
                    "to a persistent volume path (e.g., /var/lib/mca/state.db)"
                )

    def _validate_file_paths(self) -> None:
        """Validate file paths with enhanced security checks.

        Defense-in-depth: Provides early validation at config time
        with clear, actionable error messages. Runtime validation
        occurs in buffering layer.

        Validates:
        - Path traversal prevention (canonicalized paths)
        - Preliminary whitelist check (with suggestions)
        - Production /tmp warning

        Raises:
            ConfigurationError: If path validation fails
        """
        import os
        import tempfile

        for field_name in ["persist_path", "vendor_bridge_state_file"]:
            path = getattr(self, field_name)
            if path is None:
                continue

            # Check for path traversal in ORIGINAL path (early detection)
            if ".." in path:
                raise ConfigurationError(
                    f"{field_name} contains path traversal sequence (..): {path}. "
                    f"Use absolute paths or paths starting with: ~/.mca_sdk/, /var/lib/mca/"
                )

            # Expand and canonicalize
            expanded = os.path.expanduser(path)
            try:
                # Use realpath to resolve symlinks, then check again
                canonical = os.path.realpath(expanded)
            except (OSError, ValueError) as e:
                raise ConfigurationError(f"Cannot resolve {field_name}: {path}. Error: {e}")

            # Build whitelist (static + custom)
            static_allowed = [
                os.path.expanduser("~/.mca_sdk"),
                "/var/lib/mca",
                "/tmp/mca_sdk",
                tempfile.gettempdir(),  # For testing
            ]

            # Add custom directories from config
            custom_dirs = []
            if self.persist_allowed_dirs:
                custom_dirs = [d.strip() for d in self.persist_allowed_dirs.split(",")]
                static_allowed.extend(custom_dirs)

            # Check if path is within allowed directories
            # Normalize separators and case for cross-platform comparison (handles Windows
            # forward/backslash mismatch between os.path.expanduser and os.path.realpath)
            canonical_norm = os.path.normcase(os.path.normpath(canonical))
            is_allowed = any(
                canonical_norm.startswith(os.path.normcase(os.path.normpath(prefix)))
                for prefix in static_allowed
            )

            if not is_allowed:
                # Provide helpful suggestion
                suggestions = [d for d in static_allowed if not d.startswith("/tmp")][:3]
                raise ConfigurationError(
                    f"{field_name} must be within allowed directories. "
                    f"Got: {path} (resolves to: {canonical}). "
                    f"Try one of: {', '.join(suggestions)}. "
                    f"To allow custom directories, set MCA_PERSIST_ALLOWED_DIRS."
                )

            # Warning for /tmp usage (cross-platform: normalize before prefix check)
            canonical_for_tmp = os.path.normcase(canonical)
            if canonical_for_tmp.startswith(os.path.normcase("/tmp")):
                env = os.getenv("ENVIRONMENT", "").lower()
                if env in ("production", "prod"):
                    logger.warning(
                        f"SECURITY WARNING: {field_name} uses /tmp in production. "
                        f"/tmp is world-writable and may be insecure. "
                        f"Consider using ~/.mca_sdk/ or /var/lib/mca/ instead."
                    )

    def _validate_gcp_logging(self) -> None:
        """Validate GCP Cloud Logging configuration.

        Story 6.2: Implement GCP Cloud Logging Exporter
        Validates GCP logging parameters and requirements.
        """
        if not self.gcp_logging_enabled:
            return

        # Require project_id when logging is enabled
        if not self.gcp_project_id:
            raise ConfigurationError(
                "gcp_project_id is required when gcp_logging_enabled=True. "
                "Set via MCA_GCP_PROJECT_ID environment variable or pass as kwarg."
            )

        # Validate project_id format
        import re

        # GCP project IDs: 6-30 chars, lowercase, alphanumeric + hyphens, may contain colons for legacy
        if not re.match(r"^[a-z][a-z0-9:-]{4,}[a-z0-9]$", self.gcp_project_id):
            raise ConfigurationError(
                f"Invalid GCP project_id: {self.gcp_project_id}. "
                "Project IDs must be 6-30 lowercase letters, numbers, and hyphens, "
                "starting with a letter."
            )

        # Validate log severity
        valid_severities = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
        if self.gcp_log_min_severity not in valid_severities:
            raise ConfigurationError(
                f"Invalid log severity: {self.gcp_log_min_severity}. "
                f"Must be one of: {', '.join(valid_severities)}"
            )

        # Set default log name if not provided
        if not self.gcp_log_name:
            self.gcp_log_name = self.service_name

        # Validate log name format (alphanumeric, hyphens, underscores only)
        if not re.match(r"^[a-zA-Z0-9_-]+$", self.gcp_log_name):
            raise ConfigurationError(
                f"Invalid gcp_log_name: {self.gcp_log_name}. "
                "Log names must contain only alphanumeric characters, hyphens, and underscores."
            )

    def _validate_mtls(self) -> None:
        """Validate mTLS configuration.

        Story 6.4: Implement mTLS Configuration for OTLP Communication
        Validates mTLS parameters and certificate requirements.
        """
        # Validate collector_protocol
        if self.collector_protocol not in ["http", "grpc"]:
            raise ConfigurationError(
                f"collector_protocol must be 'http' or 'grpc'. Got: {self.collector_protocol}"
            )

        if not self.mtls_enabled:
            return

        # Require all certificate paths when mTLS is enabled
        if not self.client_cert_path:
            raise ConfigurationError(
                "client_cert_path is required when mtls_enabled=True. "
                "Set via MCA_CLIENT_CERT_PATH environment variable or pass as kwarg."
            )

        if not self.client_key_path:
            raise ConfigurationError(
                "client_key_path is required when mtls_enabled=True. "
                "Set via MCA_CLIENT_KEY_PATH environment variable or pass as kwarg."
            )

        if not self.ca_cert_path:
            raise ConfigurationError(
                "ca_cert_path is required when mtls_enabled=True. "
                "Set via MCA_CA_CERT_PATH environment variable or pass as kwarg."
            )

        # Warn if mTLS is enabled with HTTP protocol (mTLS typically uses gRPC)
        if self.collector_protocol == "http":
            logger.warning(
                "mTLS is enabled with HTTP protocol. "
                "mTLS is typically used with gRPC protocol. "
                "Set collector_protocol='grpc' or MCA_COLLECTOR_PROTOCOL=grpc for optimal security."
            )

    def __repr__(self) -> str:
        """String representation with masked sensitive fields.

        SECURITY: Masks registry_token to prevent accidental exposure in logs,
        error messages, or debugging output.

        Returns:
            String representation with sensitive fields masked
        """
        fields = []
        for field_name in self.__dataclass_fields__:
            value = getattr(self, field_name)

            # Mask sensitive fields - SensitiveString handles masking automatically
            if field_name == "registry_token" and value:
                if isinstance(value, SensitiveString):
                    # SensitiveString's str() already returns masked value
                    value = str(value)
                else:
                    # Fallback for non-SensitiveString tokens (defense-in-depth)
                    value = f"***{value[-4:]}" if len(value) > 4 else "****"

            fields.append(f"{field_name}={value!r}")

        return f"MCAConfig({', '.join(fields)})"

    def __str__(self) -> str:
        """String representation delegates to __repr__ for consistent masking.

        SECURITY: Ensures token masking works for both str() and repr() calls.
        """
        return self.__repr__()

    def _apply_token_sanitizing_filter(self) -> None:
        """Apply TokenSanitizingFilter to SDK logger (defense-in-depth).

        SECURITY: This provides additional protection by sanitizing tokens from
        SDK internal logs. Applied automatically when registry_token is configured.

        NOTE: This is SDK-internal only and should NOT affect application loggers.
        The filter is applied to 'mca_sdk' logger hierarchy, not root logger.
        """
        if not isinstance(self.registry_token, SensitiveString):
            return  # No token to protect

        # Get SDK logger (not root logger - avoid library overreach)
        sdk_logger = logging.getLogger("mca_sdk")

        # Check if filter already applied (avoid duplicates)
        for existing_filter in sdk_logger.filters:
            if isinstance(existing_filter, TokenSanitizingFilter):
                # Filter already exists - just update token
                existing_filter.set_token(
                    self.registry_token._SensitiveString__unsafe_process_value(lambda t: t)
                )
                return

        # Create and apply new filter
        token_filter = TokenSanitizingFilter()
        token_filter.set_token(
            self.registry_token._SensitiveString__unsafe_process_value(lambda t: t)
        )
        sdk_logger.addFilter(token_filter)
        logger.debug("Applied TokenSanitizingFilter to mca_sdk logger")

    @classmethod
    def from_env(cls, prefix: str = "MCA_") -> "MCAConfig":
        """Load configuration from environment variables.

        Environment variables should be prefixed with MCA_ by default.
        Example: MCA_SERVICE_NAME, MCA_MODEL_ID, etc.

        Args:
            prefix: Prefix for environment variables (default: "MCA_")

        Returns:
            MCAConfig instance populated from environment variables
        """

        def get_env(key: str, default=None):
            """Get environment variable with prefix."""
            return os.getenv(f"{prefix}{key}", default)

        def get_env_bool(key: str, default: bool) -> bool:
            """Get boolean environment variable with strict validation."""
            value = get_env(key)
            if value is None:
                return default

            value_lower = value.lower()
            if value_lower in ("true", "1", "yes"):
                return True
            elif value_lower in ("false", "0", "no"):
                return False
            else:
                raise ConfigurationError(
                    f"Invalid boolean value for {prefix}{key}: '{value}'. "
                    f"Valid values: true, false, 1, 0, yes, no"
                )

        def get_env_int(key: str, default: int) -> int:
            """Get integer environment variable."""
            value = get_env(key)
            if value is None:
                return default
            try:
                return int(value)
            except ValueError:
                raise ConfigurationError(f"Invalid integer value for {prefix}{key}: {value}")

        def get_env_float(key: str, default: float) -> float:
            """Get float environment variable."""
            value = get_env(key)
            if value is None:
                return default
            try:
                return float(value)
            except ValueError:
                raise ConfigurationError(f"Invalid float value for {prefix}{key}: {value}")

        def get_env_list(key: str, default: List[str]) -> List[str]:
            """Get list environment variable (comma-separated)."""
            value = get_env(key)
            if value is None:
                return default
            return [item.strip() for item in value.split(",")]

        return cls(
            service_name=get_env("SERVICE_NAME", ""),
            model_id=get_env("MODEL_ID"),
            model_version=get_env("MODEL_VERSION", "0.3.0"),
            team_name=get_env("TEAM_NAME"),
            model_type=get_env("MODEL_TYPE", "regression"),
            model_category=get_env("MODEL_CATEGORY", "internal"),
            collector_endpoint=get_env("COLLECTOR_ENDPOINT", "http://localhost:4318"),
            collector_timeout=get_env_int("COLLECTOR_TIMEOUT", 10),
            collector_protocol=get_env("COLLECTOR_PROTOCOL", "http"),
            buffering_enabled=get_env_bool("BUFFERING_ENABLED", False),
            max_queue_size=get_env_int("MAX_QUEUE_SIZE", 1000),
            retry_attempts=get_env_int("RETRY_ATTEMPTS", 3),
            base_delay=get_env_float("BASE_DELAY", 1.0),
            max_delay=get_env_float("MAX_DELAY", 30.0),
            backoff_multiplier=get_env_float("BACKOFF_MULTIPLIER", 2.0),
            persist_queue=get_env_bool("PERSIST_QUEUE", False),
            # SECURITY: Use secure user-specific directory instead of world-readable /tmp
            persist_path=get_env("PERSIST_PATH", os.path.expanduser("~/.mca_sdk/queue.pkl")),
            persist_allowed_dirs=get_env("PERSIST_ALLOWED_DIRS"),
            circuit_breaker_enabled=get_env_bool("CIRCUIT_BREAKER_ENABLED", True),
            circuit_breaker_fail_max=get_env_int("CIRCUIT_BREAKER_FAIL_MAX", 5),
            circuit_breaker_timeout=get_env_int("CIRCUIT_BREAKER_TIMEOUT", 60),
            strict_validation=get_env_bool("STRICT_VALIDATION", True),
            debug_mode=get_env_bool("DEBUG", False),
            telemetry_max_attribute_length=get_env_int("TELEMETRY_MAX_ATTRIBUTE_LENGTH", 1024),
            telemetry_max_attribute_count=get_env_int("TELEMETRY_MAX_ATTRIBUTE_COUNT", 100),
            metric_export_interval_ms=get_env_int("METRIC_EXPORT_INTERVAL_MS", 5000),
            # Match dataclass defaults for consistency
            log_batch_size=get_env_int("LOG_BATCH_SIZE", 512),
            trace_batch_size=get_env_int("TRACE_BATCH_SIZE", 512),
            vendor_name=get_env("VENDOR_NAME"),
            llm_provider=get_env("LLM_PROVIDER"),
            llm_model=get_env("LLM_MODEL"),
            registry_url=get_env("REGISTRY_URL"),
            registry_token=get_env("REGISTRY_TOKEN"),
            refresh_interval_secs=get_env_int("REFRESH_SECS", 600),
            prefer_registry=get_env_bool("PREFER_REGISTRY", True),
            deployment_id=get_env("DEPLOYMENT_ID"),
            version_check_timeout=get_env_float("VERSION_CHECK_TIMEOUT", 2.0),
            cleanup_thread_poll_interval=get_env_float("CLEANUP_THREAD_POLL_INTERVAL", 300.0),
            allow_insecure_collector=get_env_bool("ALLOW_INSECURE_COLLECTOR", False),
            allow_insecure_registry=get_env_bool("ALLOW_INSECURE_REGISTRY", False),
            vendor_api_url=get_env("VENDOR_API_URL", "http://localhost:8080/metrics"),
            vendor_bridge_max_iterations=(
                get_env_int("VENDOR_BRIDGE_MAX_ITERATIONS", None)
                if get_env("VENDOR_BRIDGE_MAX_ITERATIONS")
                else None
            ),
            vendor_bridge_state_file=get_env("VENDOR_BRIDGE_STATE_FILE"),
            vendor_bridge_poll_interval=get_env_float("VENDOR_BRIDGE_POLL_INTERVAL", 30.0),
            gcp_credentials_path=get_env("GCP_CREDENTIALS_PATH"),
            gcp_credentials_json=get_env("GCP_CREDENTIALS_JSON"),
            # GCP Cloud Logging (Story 6.2)
            gcp_logging_enabled=get_env_bool("GCP_LOGGING_ENABLED", False),
            gcp_project_id=get_env("GCP_PROJECT_ID"),
            gcp_log_name=get_env("GCP_LOG_NAME"),
            gcp_log_min_severity=get_env("GCP_LOG_MIN_SEVERITY", "INFO"),
            gcp_resource_type=get_env("GCP_RESOURCE_TYPE", "global"),
            gcp_log_timeout=get_env_float("GCP_LOG_TIMEOUT", 60.0),
            # GCP Cloud Trace
            gcp_trace_enabled=get_env_bool("GCP_TRACE_ENABLED", False),
            # Prometheus
            prometheus_enabled=get_env_bool("PROMETHEUS_ENABLED", False),
            prometheus_port=get_env_int("PROMETHEUS_PORT", 9464),
            prometheus_host=get_env("PROMETHEUS_HOST", "127.0.0.1"),
            # mTLS (Story 6.4)
            mtls_enabled=get_env_bool("MTLS_ENABLED", False),
            client_cert_path=get_env("CLIENT_CERT_PATH"),
            client_key_path=get_env("CLIENT_KEY_PATH"),
            ca_cert_path=get_env("CA_CERT_PATH"),
        )

    @classmethod
    def from_file(cls, path: str) -> "MCAConfig":
        """Load configuration from YAML file.

        Args:
            path: Path to YAML configuration file

        Returns:
            MCAConfig instance populated from file

        Raises:
            ConfigurationError: If file cannot be read or parsed
        """
        try:
            import yaml
        except ImportError:
            raise ConfigurationError(
                "pyyaml is required for loading config from file. "
                "Install with: pip install pyyaml"
            )

        try:
            with open(path, "r") as f:
                data = yaml.safe_load(f)
        except FileNotFoundError:
            raise ConfigurationError(f"Configuration file not found: {path}")
        except yaml.YAMLError as e:
            raise ConfigurationError(f"Invalid YAML in configuration file: {e}")

        if not isinstance(data, dict):
            raise ConfigurationError("Configuration file must contain a YAML dictionary")

        # SECURITY: Pure whitelist validation (default-deny security posture)
        # This prevents accidental inclusion of secrets in YAML files
        #
        # DESIGN: This is a WHITELIST, not a blacklist:
        # - ALL keys must be in _ALLOWED_KEYS to be accepted
        # - Unknown keys are ALWAYS rejected, regardless of their content
        # - The secret-like pattern check below is ONLY for error messaging
        #   (provides more helpful error for common mistakes)
        # - This is default-deny: if it's not explicitly allowed, it's blocked
        #
        # SECURITY: Pure whitelist validation (default-deny security posture)
        # Dynamically generated from dataclass fields to prevent drift when new fields are added
        _ALLOWED_KEYS = set(cls.__dataclass_fields__.keys())

        # SECURITY: Explicitly block sensitive fields in YAML files
        # These fields MUST be provided via environment variables only
        _YAML_BLOCKED_FIELDS = {"registry_token"}

        for key in data.keys():
            # Check if this is a sensitive field that must use env vars
            if key in _YAML_BLOCKED_FIELDS:
                raise ConfigurationError(
                    f"Security violation: Configuration key '{key}' appears to contain sensitive data. "
                    f"Secrets must be provided via environment variables (e.g., MCA_REGISTRY_TOKEN), "
                    f"not YAML files. This prevents accidental exposure in version control."
                )

            if key not in _ALLOWED_KEYS:
                # Enhanced error message for secret-like keys (better UX, not security logic)
                # NOTE: The key is already blocked because it's not in _ALLOWED_KEYS
                # This pattern check is ONLY to provide a more helpful error message
                key_lower = key.lower().replace("-", "_")
                if any(
                    pattern in key_lower
                    for pattern in ["token", "secret", "password", "key", "credential"]
                ):
                    raise ConfigurationError(
                        f"Security violation: Configuration key '{key}' appears to contain sensitive data. "
                        f"Secrets must be provided via environment variables (e.g., MCA_REGISTRY_TOKEN), "
                        f"not YAML files. This prevents accidental exposure in version control."
                    )
                else:
                    raise ConfigurationError(
                        f"Unknown configuration key: '{key}'. "
                        f"Valid keys: {', '.join(sorted(_ALLOWED_KEYS))}. "
                        f"If this is a new field, it must be added to the whitelist in from_file()."
                    )

        return cls.from_dict(data)

    @classmethod
    def from_dict(cls, data: dict) -> "MCAConfig":
        """Load configuration from dictionary.

        Args:
            data: Dictionary with configuration values

        Returns:
            MCAConfig instance populated from dictionary
        """
        # Only pass keys that are valid MCAConfig attributes
        valid_keys = {f.name for f in cls.__dataclass_fields__.values()}
        filtered_data = {k: v for k, v in data.items() if k in valid_keys}
        return cls(**filtered_data)

    @staticmethod
    def _get_env_value(key: str, env_prefix: str) -> Optional[str]:
        """Get raw environment variable value.

        Args:
            key: Configuration field name
            env_prefix: Prefix for environment variables (e.g., "MCA_")

        Returns:
            Environment variable value or None if not set
        """
        # Convert key to uppercase to match from_env() behavior and standard env var conventions
        return os.getenv(f"{env_prefix}{key.upper()}")

    @staticmethod
    def _get_env_bool(key: str, env_prefix: str) -> Optional[bool]:
        """Get boolean environment variable with strict validation.

        Args:
            key: Configuration field name
            env_prefix: Prefix for environment variables

        Returns:
            Boolean value or None if not set

        Raises:
            ConfigurationError: If value is not a valid boolean
        """
        value = MCAConfig._get_env_value(key, env_prefix)
        if value is None:
            return None

        value_lower = value.lower()
        if value_lower in ("true", "1", "yes"):
            return True
        elif value_lower in ("false", "0", "no"):
            return False
        else:
            raise ConfigurationError(
                f"Invalid boolean value for {env_prefix}{key.upper()}: '{value}'. "
                f"Valid values: true, false, 1, 0, yes, no"
            )

    @staticmethod
    def _get_env_int(key: str, env_prefix: str) -> Optional[int]:
        """Get integer environment variable.

        Args:
            key: Configuration field name
            env_prefix: Prefix for environment variables

        Returns:
            Integer value or None if not set

        Raises:
            ConfigurationError: If value cannot be parsed as integer
        """
        value = MCAConfig._get_env_value(key, env_prefix)
        if value is None:
            return None
        try:
            return int(value)
        except ValueError:
            raise ConfigurationError(f"Invalid integer value for {env_prefix}{key}: {value}")

    @staticmethod
    def _get_env_float(key: str, env_prefix: str) -> Optional[float]:
        """Get float environment variable.

        Args:
            key: Configuration field name
            env_prefix: Prefix for environment variables

        Returns:
            Float value or None if not set

        Raises:
            ConfigurationError: If value cannot be parsed as float
        """
        value = MCAConfig._get_env_value(key, env_prefix)
        if value is None:
            return None
        try:
            return float(value)
        except ValueError:
            raise ConfigurationError(f"Invalid float value for {env_prefix}{key}: {value}")

    @classmethod
    def _load_from_yaml(cls, config_file: Optional[str], env_prefix: str) -> dict:
        """Load configuration from YAML file.

        Args:
            config_file: Optional path to YAML file
            env_prefix: Environment variable prefix for MCA_CONFIG_FILE

        Returns:
            Dictionary with configuration from YAML file

        Raises:
            ConfigurationError: If YAML contains registry_token or file is invalid
        """
        config_dict = {}
        yaml_file = config_file or os.getenv(f"{env_prefix}CONFIG_FILE")

        if yaml_file and os.path.exists(yaml_file):
            try:
                yaml_config = cls.from_file(yaml_file)
                # Extract all attributes from the YAML config
                for field_name in cls.__dataclass_fields__:
                    config_dict[field_name] = getattr(yaml_config, field_name)

                # SECURITY: Check if registry_token was loaded from YAML
                if config_dict.get("registry_token") is not None:
                    raise ConfigurationError(
                        "Security violation: registry_token must be provided via environment variable "
                        "(MCA_REGISTRY_TOKEN) only, not YAML configuration files. "
                        "This prevents accidental token exposure in version control."
                    )
            except ConfigurationError as e:
                raise ConfigurationError(f"Failed to load configuration from {yaml_file}: {e}")

        return config_dict

    @classmethod
    def _load_from_env(cls, env_prefix: str) -> dict:
        """Load configuration from environment variables.

        Args:
            env_prefix: Prefix for environment variables (e.g., "MCA_")

        Returns:
            Dictionary with configuration from environment variables
        """
        config_dict = {}

        # Get raw env values and apply type conversion
        for field_name in cls.__dataclass_fields__:
            env_value = cls._get_env_value(field_name, env_prefix)

            if env_value is not None:
                # Type conversion based on field type
                if field_name in [
                    "buffering_enabled",
                    "persist_queue",
                    "strict_validation",
                    "prefer_registry",
                    "debug_mode",
                    "circuit_breaker_enabled",
                    "allow_insecure_collector",
                    "allow_insecure_registry",
                    "gcp_logging_enabled",
                    "gcp_trace_enabled",
                    "prometheus_enabled",
                    "mtls_enabled",
                ]:
                    config_dict[field_name] = cls._get_env_bool(field_name, env_prefix)
                elif field_name in [
                    "collector_timeout",
                    "max_queue_size",
                    "retry_attempts",
                    "telemetry_max_attribute_length",
                    "telemetry_max_attribute_count",
                    "metric_export_interval_ms",
                    "log_batch_size",
                    "trace_batch_size",
                    "refresh_interval_secs",
                    "circuit_breaker_fail_max",
                    "circuit_breaker_timeout",
                ]:
                    config_dict[field_name] = cls._get_env_int(field_name, env_prefix)
                elif field_name in [
                    "base_delay",
                    "max_delay",
                    "backoff_multiplier",
                    "version_check_timeout",
                    "cleanup_thread_poll_interval",
                    "vendor_bridge_poll_interval",
                    "persist_min_disk_space_mb",
                ]:
                    config_dict[field_name] = cls._get_env_float(field_name, env_prefix)
                else:
                    # String fields
                    config_dict[field_name] = env_value

        return config_dict

    @staticmethod
    def _validate_security_constraints(kwargs: dict) -> None:
        """Validate security constraints on kwargs.

        Args:
            kwargs: Configuration parameters from kwargs

        Raises:
            ConfigurationError: If security constraints are violated
        """
        # SECURITY: Never accept registry_token via kwargs
        if "registry_token" in kwargs:
            raise ConfigurationError(
                "Security violation: registry_token must be provided via environment variable "
                "(MCA_REGISTRY_TOKEN) only, not kwargs. This prevents accidental token exposure in code."
            )

    @staticmethod
    def _resolve_secrets(config_dict: dict, env_prefix: str) -> dict:
        """Resolve secret:// references to actual values from GCP Secret Manager.

        This method detects secret:// URLs in configuration values and fetches
        the actual secret values from GCP Secret Manager. If Secret Manager is
        unavailable, it falls back to environment variables.

        **SINGLETON LIFECYCLE:** This method creates a singleton SecretManagerClient
        that persists for the application lifetime. The singleton:
        - Lives until process exit (atexit handler registered automatically)
        - Shares cache across all config loads (memory efficient)
        - Holds GCP client connection and thread locks
        - Never cleaned up automatically during runtime

        **IMPLICATIONS:**
        - Long-running services: Cache grows with unique secret URLs, never cleared
        - Testing: Use `reset_secret_manager_for_testing()` from mca_sdk.security._testing
        - Manual cleanup: Call `client.clear_cache()` to free memory if needed
        - Container shutdown: Atexit fires on SIGTERM (Python 3.9+) but register
          explicit signal handler for guaranteed cleanup in Kubernetes/Docker

        Example:
            config_dict = {"registry_token": "secret://my-project/token/latest"}
            resolved = _resolve_secrets(config_dict, "MCA_")
            # resolved = {"registry_token": "actual-token-value"}
            # Singleton SecretManagerClient now exists until process exit

        Args:
            config_dict: Configuration dictionary potentially containing secret:// URLs
            env_prefix: Prefix for environment variables (e.g., "MCA_")

        Returns:
            Configuration dictionary with secrets resolved to actual values

        Raises:
            ConfigurationError: If secret resolution fails without fallback

        Security:
            - Secret values are never logged
            - Secrets cached in memory with TTL
            - Falls back to environment variables in local development
            - Uses singleton client for efficiency
        """
        has_secret_urls = False

        # Scan for secret:// URLs
        for key, value in config_dict.items():
            if isinstance(value, str) and value.startswith("secret://"):
                has_secret_urls = True
                break

        # Early exit if no secret URLs found
        if not has_secret_urls:
            return config_dict

        # Import locally only when needed (after checking for secret URLs)
        # This makes the google-cloud-secret-manager dependency optional
        try:
            from ..security.secret_manager import SecretManagerClient, SecretManagerError
        except ImportError as e:
            raise ConfigurationError(
                "secret:// URLs detected in configuration but google-cloud-secret-manager is not installed. "
                "Install with: pip install mca-sdk[gcp] or pip install google-cloud-secret-manager"
            ) from e

        # Get singleton client instance (efficient - reuses connection and cache)
        secret_client = SecretManagerClient.get_instance()
        logger.debug(
            "Detected secret:// URLs in configuration, using SecretManagerClient singleton"
        )

        # Resolve each secret URL
        for key, value in list(
            config_dict.items()
        ):  # Use list() to avoid modification during iteration
            if isinstance(value, str) and value.startswith("secret://"):
                # Fallback env var follows MCA naming convention
                # Example: registry_token → MCA_REGISTRY_TOKEN
                fallback_env = f"{env_prefix}{key.upper()}"

                try:
                    # Fetch secret (may use cache, may use fallback)
                    resolved_value = secret_client.fetch_secret(value, fallback_env=fallback_env)
                    config_dict[key] = resolved_value

                    # Log successful resolution (URL is safe to log, value is NOT)
                    logger.debug(f"Resolved secret URL for config key '{key}': {value}")

                except SecretManagerError as e:
                    # Wrap in ConfigurationError for consistency
                    raise ConfigurationError(
                        f"Failed to resolve secret for config key '{key}': {e}. "
                        f"Secret URL: {value}. "
                        f"Ensure GCP credentials are configured (GOOGLE_APPLICATION_CREDENTIALS) "
                        f"and secret exists, or set fallback environment variable {fallback_env}."
                    )

        return config_dict

    @classmethod
    def load(
        cls, config_file: Optional[str] = None, env_prefix: str = "MCA_", **kwargs
    ) -> "MCAConfig":
        """Load configuration from multiple sources with precedence chain.

        Precedence order (highest to lowest):
        1. kwargs/constructor arguments
        2. Environment variables (MCA_* prefix by default)
        3. YAML configuration file
        4. Default values

        Note on Registry Integration:
            Registry configuration refresh happens AFTER initial config load via
            MCAClient._hydrate_from_registry(). The registry provides supplementary
            runtime metadata (thresholds, extra_resource) but does NOT participate
            in the load() precedence chain. See MCAClient documentation for registry
            refresh behavior.

        Args:
            config_file: Optional path to YAML configuration file. If not provided,
                        checks MCA_CONFIG_FILE environment variable, then tries standard locations.
            env_prefix: Prefix for environment variables (default: "MCA_")
            **kwargs: Configuration parameters to override all other sources

        Returns:
            MCAConfig instance with merged configuration from all sources

        Security:
            registry_token MUST be provided via environment variable (MCA_REGISTRY_TOKEN) only.
            Passing registry_token via kwargs or YAML configuration files will raise
            ConfigurationError to prevent accidental token exposure in code, logs, or
            version control.

        Raises:
            ConfigurationError: If required fields are missing or invalid
        """
        # Step 2: Load from YAML file if available
        config_dict = cls._load_from_yaml(config_file, env_prefix)

        # Step 3: Override with environment variables
        config_dict.update(cls._load_from_env(env_prefix))

        # Step 4: Resolve secret:// URLs to actual secret values
        config_dict = cls._resolve_secrets(config_dict, env_prefix)

        # Step 5: Validate security constraints
        cls._validate_security_constraints(kwargs)

        # Step 6: Override with kwargs (highest precedence)
        # Filter None values - None means "not provided", not "set to None"
        # This enables wrapper functions to work correctly:
        # def init(model_id=None):
        #     return MCAConfig.load(model_id=model_id)
        non_none_kwargs = {k: v for k, v in kwargs.items() if v is not None}
        config_dict.update(non_none_kwargs)

        # Step 7: Ensure service_name is present (even if empty) so validation can catch it
        if "service_name" not in config_dict:
            config_dict["service_name"] = ""

        # Step 8: Create final config and validate
        return cls(**config_dict)


@dataclass
class LoggingConfig:
    """Configuration for GCP Cloud Logging integration.

    Groups GCP-specific logging parameters to reduce function signature complexity.
    Used internally by setup_logger_provider to configure Cloud Logging exporter.

    Attributes:
        gcp_logging_enabled: Whether to enable GCP Cloud Logging exporter
        gcp_project_id: GCP project ID (required if enabled)
        gcp_log_name: Cloud Logging log name (required if enabled)
        gcp_resource_type: GCP monitored resource type (default: "global")
        gcp_credentials: GCP credentials object (optional, uses ADC if None)
        gcp_log_timeout: Cloud Logging API timeout in seconds (default: 60s)
    """

    gcp_logging_enabled: bool = False
    gcp_project_id: Optional[str] = None
    gcp_log_name: Optional[str] = None
    gcp_resource_type: str = "global"
    gcp_credentials: Optional[Any] = None
    gcp_log_timeout: float = 60.0
