"""Configuration management for the Klira AI SDK.

Provides centralized configuration management with validation, environment variable
parsing, and sensible defaults.
"""

import os
import logging
from dataclasses import dataclass, field
from typing import Optional, List, Dict, Any

# Configure logger for config utilities
logger = logging.getLogger("klira.config")


@dataclass
class KliraConfig:
    """Centralized configuration for the Klira AI SDK.

    This class provides a single source of truth for all SDK configuration,
    with automatic environment variable parsing, validation, and sensible defaults.
    """

    # Core settings
    app_name: str = "KliraApp"
    api_key: Optional[str] = field(default_factory=lambda: os.getenv("KLIRA_API_KEY"))

    # Eval mode settings
    evals_run: Optional[str] = None  # Optional eval run ID (None = regular mode)
    dataset_id: Optional[str] = None  # Optional remote dataset ID for evals

    # OpenTelemetry settings
    opentelemetry_endpoint: Optional[str] = field(
        default_factory=lambda: os.getenv("KLIRA_OPENTELEMETRY_ENDPOINT")
    )
    tracing_enabled: bool = field(
        default_factory=lambda: os.getenv("KLIRA_TRACING_ENABLED", "true").lower()
        == "true"
    )
    trace_content: bool = field(
        default_factory=lambda: os.getenv("KLIRA_TRACE_CONTENT", "true").lower()
        == "true"
    )
    metrics_enabled: bool = field(
        default_factory=lambda: os.getenv("KLIRA_METRICS_ENABLED", "true").lower()
        == "true"
    )

    # Logging settings
    logging_enabled: bool = field(
        default_factory=lambda: os.getenv("KLIRA_LOGGING_ENABLED", "false").lower()
        == "true"
    )

    # Guardrails settings
    policies_path: Optional[str] = field(default=None)
    _explicit_policies_path: bool = field(
        default=False, repr=False
    )  # Track if policies_path was explicitly set
    policy_enforcement: bool = field(
        default_factory=lambda: os.getenv("KLIRA_POLICY_ENFORCEMENT", "true").lower()
        == "true"
    )

    # LLM Fallback Configuration (PROD-243 - all optional, disabled by default)
    llm_fallback_enabled: bool = False  # Default False - user must opt-in
    llm_fallback_provider: Optional[str] = None  # e.g., "openai", "anthropic"
    llm_fallback_model: Optional[str] = None  # e.g., "gpt-4", "claude-3-opus-20240229"
    llm_fallback_api_key: Optional[str] = None  # API key for the provider

    # Fuzzy Matching Configuration (PROD-243)
    fuzzy_similarity_threshold: float = 85.0  # Default 85% similarity

    # Remote Policy Loading Settings (with sensible defaults)
    use_remote_policies: bool = field(
        default_factory=lambda: os.getenv("KLIRA_USE_REMOTE_POLICIES", "true").lower()
        == "true"
    )
    _policies_api_url: str = field(
        default_factory=lambda: os.getenv(
            "KLIRA_POLICIES_API_URL", "https://api.getklira.com/v1/policies"
        )
    )
    _policy_fetch_timeout: int = field(
        default_factory=lambda: int(os.getenv("KLIRA_POLICY_FETCH_TIMEOUT", "10"))
    )
    _policy_fetch_retries: int = field(
        default_factory=lambda: int(os.getenv("KLIRA_POLICY_FETCH_RETRIES", "3"))
    )
    _policy_fallback_enabled: bool = field(
        default_factory=lambda: os.getenv(
            "KLIRA_POLICY_FALLBACK_ENABLED", "true"
        ).lower()
        == "true"
    )

    # Telemetry settings
    telemetry_enabled: bool = field(
        default_factory=lambda: os.getenv("KLIRA_TELEMETRY", "false").lower() == "true"
    )

    # External Tracing Control (Unified Trace Architecture)
    # When True, suppresses external tracing (Traceloop auto-instrumentation, OpenAI native)
    # to produce Klira-only spans for consistent trace hierarchies
    disable_external_tracing: bool = field(
        default_factory=lambda: os.getenv(
            "KLIRA_DISABLE_EXTERNAL_TRACING", "true"
        ).lower()
        == "true"
    )

    # Performance settings
    lazy_loading: bool = field(
        default_factory=lambda: os.getenv("KLIRA_LAZY_LOADING", "true").lower()
        == "true"
    )
    framework_detection_cache_size: int = field(
        default_factory=lambda: int(os.getenv("KLIRA_FRAMEWORK_CACHE_SIZE", "1000"))
    )

    # Debug settings
    debug_mode: bool = field(
        default_factory=lambda: os.getenv("KLIRA_DEBUG", "false").lower() == "true"
    )
    verbose: bool = field(
        default_factory=lambda: os.getenv("KLIRA_VERBOSE", "false").lower() == "true"
    )

    # LLM Logging settings
    log_prompts: bool = field(
        default_factory=lambda: os.getenv("KLIRA_LOG_PROMPTS", "true").lower() == "true"
    )
    prompt_truncation_limit: int = field(
        default_factory=lambda: int(os.getenv("KLIRA_PROMPT_TRUNCATION_LIMIT", "15000"))
    )
    response_truncation_limit: int = field(
        default_factory=lambda: int(
            os.getenv("KLIRA_RESPONSE_TRUNCATION_LIMIT", "1000")
        )
    )

    def __post_init__(self) -> None:
        """Post-initialization processing."""
        # Track if policies_path was explicitly set (from environment or constructor)
        self._explicit_policies_path = self.policies_path is not None

        # Always resolve policies path if not explicitly set (needed for fallback)
        if self.policies_path is None:
            self.policies_path = self._resolve_policies_path()

    def get_telemetry_endpoint(self) -> str:
        """Get OTLP endpoint based on eval mode.

        When evals_run is set, routes traces to /evals/v1/traces.
        When evals_run is None, uses standard /v1/traces endpoint.

        Returns:
            str: The OTLP endpoint URL for trace export.
        """
        base = (self.opentelemetry_endpoint or "https://api.getklira.com").rstrip("/")
        if self.evals_run is not None:
            return f"{base}/evals/v1/traces"
        return f"{base}/v1/traces"

    def is_eval_mode(self) -> bool:
        """Check if SDK is in eval mode.

        Returns:
            bool: True if evals_run is set, False otherwise.
        """
        return self.evals_run is not None

    @property
    def policies_api_url(self) -> str:
        """Get the policies API URL (internal use only)."""
        return self._policies_api_url

    @property
    def should_use_remote_policies(self) -> bool:
        """
        Determine if remote policy loading should be used.

        Returns True if:
        - use_remote_policies is True (default)
        - API key is present
        - policies_path was NOT explicitly set by the user (only defaulted)
        """
        return (
            self.use_remote_policies
            and self.api_key is not None
            and not self._explicit_policies_path
        )

    def _resolve_policies_path(self) -> str:
        """Determines the path to the guardrail policies directory or file.

        Resolution order:
        1. KLIRA_POLICIES_PATH environment variable (can be file or directory).
        2. `./guardrails` directory relative to the current working directory.
        3. The bundled `klira/sdk/guardrails` directory.

        Returns:
            str: The absolute path to the determined policies file or directory.
        """
        # 1. Check environment variable
        env_path_str = os.getenv("KLIRA_POLICIES_PATH")
        if env_path_str:
            abs_env_path = os.path.abspath(env_path_str)
            if os.path.exists(abs_env_path):
                logger.debug(
                    f"Using policies path from KLIRA_POLICIES_PATH: {abs_env_path}"
                )
                return abs_env_path
            else:
                logger.warning(
                    f"KLIRA_POLICIES_PATH ('{env_path_str}') is set but does not exist. "
                    "Falling back to standard locations."
                )

        # 2. Check for local ./guardrails directory
        local_guardrails_path = os.path.abspath(os.path.join(os.getcwd(), "guardrails"))
        if os.path.isdir(local_guardrails_path):
            # Check if it contains any policy files
            policy_files_exist = any(
                f.lower().endswith((".json", ".yaml", ".yml"))
                for f in os.listdir(local_guardrails_path)
                if os.path.isfile(os.path.join(local_guardrails_path, f))
            )
            if policy_files_exist:
                logger.debug(f"Using local policies directory: {local_guardrails_path}")
                return local_guardrails_path
            else:
                logger.debug(
                    f"Local directory '{local_guardrails_path}' exists but contains no policy files. Checking bundled policies."
                )

        # 3. Fallback to the bundled guardrails directory
        try:
            current_dir = os.path.dirname(os.path.abspath(__file__))
            bundled_policy_dir = os.path.join(current_dir, "guardrails")

            if os.path.isdir(bundled_policy_dir):
                # Check if it contains any policy files
                policy_files_exist = any(
                    f.lower().endswith((".json", ".yaml", ".yml"))
                    for f in os.listdir(bundled_policy_dir)
                    if os.path.isfile(os.path.join(bundled_policy_dir, f))
                )
                if policy_files_exist:
                    logger.debug(
                        f"Using bundled policies directory: {bundled_policy_dir}"
                    )
                    return bundled_policy_dir
                else:
                    # Check if remote policies will be used instead
                    if self.should_use_remote_policies:
                        logger.info(
                            f"No local policy files found in {bundled_policy_dir}. "
                            f"Using remote policies from platform (API key configured)."
                        )
                    else:
                        logger.error(
                            f"Bundled policy directory exists but contains no policy files: {bundled_policy_dir}. "
                            "Policy enforcement might not work correctly. "
                            "Configure an API key to use remote policies or provide a policies_path."
                        )
                    return bundled_policy_dir
            else:
                logger.error(
                    f"Bundled policies directory not found at expected location: {bundled_policy_dir}. Policy enforcement might not work correctly."
                )
                return bundled_policy_dir

        except Exception as e:
            logger.error(f"Error determining bundled policy path: {e}", exc_info=True)
            # Fallback to default location
            fallback_path = os.path.join(
                os.path.dirname(os.path.abspath(__file__)), "guardrails"
            )
            logger.warning(f"Falling back to default policy path: {fallback_path}")
            return fallback_path

    def validate(self) -> List[str]:
        """Validate configuration and return list of errors.

        Returns:
            List[str]: List of validation error messages. Empty if valid.
        """
        errors = []

        # API key validation - now mandatory
        if not self.api_key:
            errors.append(
                "API key is required. Get your API key at https://getklira.com"
            )
        elif not self.api_key.startswith("klira_"):
            errors.append(
                "API key must start with 'klira_'. Get your API key at https://getklira.com"
            )

        # Endpoint validation
        if self.opentelemetry_endpoint:
            if not self.opentelemetry_endpoint.startswith(("http://", "https://")):
                errors.append("OpenTelemetry endpoint must be a valid HTTP/HTTPS URL")

        # Policies path validation
        if self.policies_path and not os.path.exists(self.policies_path):
            errors.append(f"Policies path does not exist: {self.policies_path}")

        # Numeric validation
        if self.framework_detection_cache_size < 0:
            errors.append("Framework detection cache size must be non-negative")

        # App name validation
        if not self.app_name or not self.app_name.strip():
            errors.append("App name cannot be empty")

        return errors

    def is_valid(self) -> bool:
        """Check if configuration is valid.

        Returns:
            bool: True if configuration is valid, False otherwise.
        """
        return len(self.validate()) == 0

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

        Returns:
            Dict[str, Any]: Configuration as dictionary.
        """
        return {
            "app_name": self.app_name,
            "api_key": "***" if self.api_key else None,  # Mask sensitive data
            "evals_run": self.evals_run,
            "dataset_id": self.dataset_id,
            "opentelemetry_endpoint": self.opentelemetry_endpoint,
            "tracing_enabled": self.tracing_enabled,
            "trace_content": self.trace_content,
            "metrics_enabled": self.metrics_enabled,
            "logging_enabled": self.logging_enabled,
            "policies_path": self.policies_path,
            "policy_enforcement": self.policy_enforcement,
            "llm_fallback_enabled": self.llm_fallback_enabled,
            "llm_fallback_provider": self.llm_fallback_provider,
            "llm_fallback_model": self.llm_fallback_model,
            "llm_fallback_api_key": "***"
            if self.llm_fallback_api_key
            else None,  # Mask sensitive data
            "fuzzy_similarity_threshold": self.fuzzy_similarity_threshold,
            "telemetry_enabled": self.telemetry_enabled,
            "disable_external_tracing": self.disable_external_tracing,
            "lazy_loading": self.lazy_loading,
            "framework_detection_cache_size": self.framework_detection_cache_size,
            "debug_mode": self.debug_mode,
            "verbose": self.verbose,
            "log_prompts": self.log_prompts,
            "prompt_truncation_limit": self.prompt_truncation_limit,
            "response_truncation_limit": self.response_truncation_limit,
        }

    @classmethod
    def from_dict(cls, config_dict: Dict[str, Any]) -> "KliraConfig":
        """Create configuration from dictionary.

        Args:
            config_dict: Configuration dictionary.

        Returns:
            KliraConfig: Configuration instance.
        """
        # Get valid field names from the dataclass
        from dataclasses import fields

        valid_fields = {f.name for f in fields(cls)}

        # Filter the dictionary to only include valid fields
        filtered_dict = {k: v for k, v in config_dict.items() if k in valid_fields}

        return cls(**filtered_dict)

    @classmethod
    def from_env(cls, **overrides: Any) -> "KliraConfig":
        """Create configuration from environment variables with optional overrides.

        Args:
            **overrides: Override values for specific configuration fields.

        Returns:
            KliraConfig: Configuration instance.
        """
        config = cls()

        # Apply overrides
        for key, value in overrides.items():
            if hasattr(config, key):
                setattr(config, key, value)
                # When policies_path is explicitly set via overrides, also set the
                # _explicit_policies_path flag to True. This is normally done in
                # __post_init__, but setattr bypasses that method since the config
                # is already initialized. This flag controls whether remote policies
                # should be used.
                if key == "policies_path" and value is not None:
                    config._explicit_policies_path = True
            else:
                logger.warning(f"Unknown configuration override: {key}")

        return config


# Global configuration instance
_global_config: Optional[KliraConfig] = None


def get_config() -> KliraConfig:
    """Get the global configuration instance.

    Returns:
        KliraConfig: Global configuration instance.
    """
    global _global_config
    if _global_config is None:
        _global_config = KliraConfig.from_env()
    return _global_config


def set_config(config: KliraConfig) -> None:
    """Set the global configuration instance.

    Args:
        config: Configuration instance to set as global.
    """
    global _global_config
    _global_config = config


def reset_config() -> None:
    """Reset the global configuration to defaults."""
    global _global_config
    _global_config = None


# Backward compatibility functions - these delegate to the new centralized config
def is_tracing_enabled() -> bool:
    """Checks if Klira AI OpenTelemetry tracing is enabled."""
    return get_config().tracing_enabled


def is_content_tracing_enabled() -> bool:
    """Checks if tracing the content (e.g., LLM prompts/responses) is enabled."""
    return get_config().trace_content


def is_metrics_enabled() -> bool:
    """Checks if Klira AI metrics collection is enabled."""
    return get_config().metrics_enabled


def is_logging_enabled() -> bool:
    """Checks if Klira AI SDK's internal logging is enabled."""
    return get_config().logging_enabled


def get_opentelemetry_endpoint() -> Optional[str]:
    """Gets the configured OpenTelemetry collector endpoint URL."""
    return get_config().opentelemetry_endpoint


def get_api_key() -> Optional[str]:
    """Gets the Klira AI API key."""
    return get_config().api_key


def get_policies_path() -> str:
    """Determines the path to the guardrail policies directory or file."""
    config = get_config()
    # Since __post_init__ ensures policies_path is never None, we can safely assert this
    assert (
        config.policies_path is not None
    ), "policies_path should be set by __post_init__"
    return config.policies_path


def is_policy_enforcement_enabled() -> bool:
    """Checks if guardrail policy enforcement is globally enabled."""
    return get_config().policy_enforcement


def is_prompt_logging_enabled() -> bool:
    """Checks if LLM prompt/response logging is enabled."""
    return get_config().log_prompts


def get_prompt_truncation_limit() -> int:
    """Gets the prompt truncation limit."""
    return get_config().prompt_truncation_limit


def get_response_truncation_limit() -> int:
    """Gets the response truncation limit."""
    return get_config().response_truncation_limit
