"""
Configuration loader for Openfund Agent

This module provides functionality to load and validate configuration from YAML files
with support for environment variable substitution.
"""

import os
import re
from pathlib import Path
from typing import Any, Dict, Optional, List
import yaml
from dataclasses import dataclass, field


class ConfigValidationError(Exception):
    """Raised when configuration validation fails"""

    pass


class ConfigLoadError(Exception):
    """Raised when configuration loading fails"""

    pass


@dataclass
class ExchangeConfig:
    """Exchange configuration"""

    name: str
    api_key: str
    api_secret: str
    api_password: Optional[str] = None
    testnet: bool = False
    timeout: int = 30000


@dataclass
class ClaudeConfig:
    """Claude API configuration"""

    api_key: str
    model_version: str = "claude-3-5-sonnet-20241022"
    max_tokens: int = 4096
    temperature: float = 0.7


@dataclass
class DataCollectionConfig:
    """Data collection configuration"""

    cache_ttl_minutes: int = 60
    retry_max_attempts: int = 3
    retry_delay_seconds: float = 1.0
    supported_timeframes: List[str] = field(
        default_factory=lambda: ["1m", "5m", "15m", "1h", "4h", "1d"]
    )
    default_limit: int = 500


@dataclass
class StrategyConfig:
    """Strategy configuration"""

    default_strategy: str = "BestTopDownStrategy"
    execution_timeout: int = 300
    max_concurrent_strategies: int = 3


@dataclass
class PerformanceConfig:
    """Performance analysis configuration"""

    analysis_period: str = "daily"
    min_trades_for_analysis: int = 10
    risk_free_rate: float = 0.0


@dataclass
class RiskConfig:
    """Risk control configuration"""

    max_iteration_per_day: int = 1
    max_drawdown_threshold: float = 0.15
    min_win_rate_threshold: float = 0.45
    core_logic_review_required: bool = True
    backtest_performance_decline_threshold: float = 0.10
    live_loss_threshold: float = 0.05


@dataclass
class ValidationConfig:
    """Validation configuration"""

    docker_image: str = "openfund-agent-sandbox:latest"
    docker_timeout: int = 300
    run_pytest: bool = True
    run_black_check: bool = True
    run_backtest: bool = True
    backtest_data_days: int = 30


@dataclass
class GitConfig:
    """Git configuration"""

    auto_commit: bool = True
    auto_tag: bool = True
    tag_prefix: str = "agent-v"


@dataclass
class NotificationConfig:
    """Notification configuration"""

    enabled: bool = True
    feishu_webhook: Optional[str] = None
    notify_on_risk: bool = True
    notify_on_iteration_complete: bool = True
    notify_on_error: bool = True


@dataclass
class EvolutionConfig:
    """Evolution configuration"""

    max_iterations: int = 10
    goal_metric: str = "sharpe_ratio"
    goal_threshold: float = 2.0
    iteration_interval_hours: int = 24


@dataclass
class DatabaseConfig:
    """Database configuration"""

    type: str = "sqlite"
    path: str = "data/openfund_agent.db"


@dataclass
class LoggingConfig:
    """Logging configuration"""

    level: str = "INFO"
    format: str = "json"
    file_path: str = "logs/openfund_agent.log"
    max_file_size_mb: int = 100
    backup_count: int = 5


@dataclass
class AgentConfig:
    """Complete agent configuration"""

    exchange: ExchangeConfig
    claude: ClaudeConfig
    data_collection: DataCollectionConfig
    strategy: StrategyConfig
    performance: PerformanceConfig
    risk: RiskConfig
    validation: ValidationConfig
    git: GitConfig
    notification: NotificationConfig
    evolution: EvolutionConfig
    database: DatabaseConfig
    logging: LoggingConfig


class ConfigLoader:
    """
    Configuration loader with YAML support and environment variable substitution

    This class loads configuration from YAML files and supports environment variable
    substitution using the ${VAR_NAME} syntax. It also validates the configuration
    to ensure all required fields are present and have valid values.
    """

    ENV_VAR_PATTERN = re.compile(r"\$\{([^}]+)\}")

    def __init__(self, config_path: Optional[str] = None, env: str = "development"):
        """
        Initialize the configuration loader

        Args:
            config_path: Path to configuration file. If None, uses default path
            env: Environment name (development, production)
        """
        self.env = env

        if config_path:
            self.config_path = Path(config_path)
        else:
            # Default to config/{env}.yaml
            base_dir = Path(__file__).parent.parent.parent
            self.config_path = base_dir / "config" / f"{env}.yaml"

        self._raw_config: Optional[Dict[str, Any]] = None
        self._config: Optional[AgentConfig] = None

    def load(self) -> AgentConfig:
        """
        Load and validate configuration

        Returns:
            AgentConfig: Validated configuration object

        Raises:
            ConfigLoadError: If configuration file cannot be loaded
            ConfigValidationError: If configuration validation fails
        """
        # Load raw YAML
        self._raw_config = self._load_yaml()

        # Substitute environment variables
        self._raw_config = self._substitute_env_vars(self._raw_config)

        # Validate and parse configuration
        self._config = self._parse_config(self._raw_config)

        # Validate configuration
        self._validate_config(self._config)

        return self._config

    def _load_yaml(self) -> Dict[str, Any]:
        """
        Load YAML configuration file

        Returns:
            Dict containing raw configuration

        Raises:
            ConfigLoadError: If file cannot be loaded
        """
        if not self.config_path.exists():
            raise ConfigLoadError(f"Configuration file not found: {self.config_path}")

        try:
            with open(self.config_path, "r", encoding="utf-8") as f:
                config = yaml.safe_load(f)

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

            return config
        except yaml.YAMLError as e:
            raise ConfigLoadError(f"Failed to parse YAML configuration: {e}")
        except Exception as e:
            raise ConfigLoadError(f"Failed to load configuration file: {e}")

    def _substitute_env_vars(self, config: Any) -> Any:
        """
        Recursively substitute environment variables in configuration

        Args:
            config: Configuration value (can be dict, list, str, etc.)

        Returns:
            Configuration with environment variables substituted
        """
        if isinstance(config, dict):
            return {k: self._substitute_env_vars(v) for k, v in config.items()}
        elif isinstance(config, list):
            return [self._substitute_env_vars(item) for item in config]
        elif isinstance(config, str):
            return self._substitute_env_var_in_string(config)
        else:
            return config

    def _substitute_env_var_in_string(self, value: str) -> str:
        """
        Substitute environment variables in a string

        Args:
            value: String that may contain ${VAR_NAME} patterns

        Returns:
            String with environment variables substituted
        """

        def replace_match(match):
            var_name = match.group(1)
            env_value = os.environ.get(var_name)

            if env_value is None:
                # Keep the placeholder if environment variable is not set
                return match.group(0)

            return env_value

        return self.ENV_VAR_PATTERN.sub(replace_match, value)

    def _parse_config(self, raw_config: Dict[str, Any]) -> AgentConfig:
        """
        Parse raw configuration into typed configuration objects

        Args:
            raw_config: Raw configuration dictionary

        Returns:
            AgentConfig: Typed configuration object

        Raises:
            ConfigValidationError: If required fields are missing
        """
        try:
            # Parse exchange config
            exchange_data = raw_config.get("exchange", {})
            exchange = ExchangeConfig(
                name=exchange_data.get("name", "okx"),
                api_key=exchange_data.get("api_key", ""),
                api_secret=exchange_data.get("api_secret", ""),
                api_password=exchange_data.get("api_password"),
                testnet=exchange_data.get("testnet", False),
                timeout=exchange_data.get("timeout", 30000),
            )

            # Parse Claude config
            claude_data = raw_config.get("claude", {})
            claude = ClaudeConfig(
                api_key=claude_data.get("api_key", ""),
                model_version=claude_data.get("model_version", "claude-3-5-sonnet-20241022"),
                max_tokens=claude_data.get("max_tokens", 4096),
                temperature=claude_data.get("temperature", 0.7),
            )

            # Parse data collection config
            data_collection_data = raw_config.get("data_collection", {})
            data_collection = DataCollectionConfig(
                cache_ttl_minutes=data_collection_data.get("cache_ttl_minutes", 60),
                retry_max_attempts=data_collection_data.get("retry_max_attempts", 3),
                retry_delay_seconds=data_collection_data.get("retry_delay_seconds", 1.0),
                supported_timeframes=data_collection_data.get(
                    "supported_timeframes", ["1m", "5m", "15m", "1h", "4h", "1d"]
                ),
                default_limit=data_collection_data.get("default_limit", 500),
            )

            # Parse strategy config
            strategy_data = raw_config.get("strategy", {})
            strategy = StrategyConfig(
                default_strategy=strategy_data.get("default_strategy", "BestTopDownStrategy"),
                execution_timeout=strategy_data.get("execution_timeout", 300),
                max_concurrent_strategies=strategy_data.get("max_concurrent_strategies", 3),
            )

            # Parse performance config
            performance_data = raw_config.get("performance", {})
            performance = PerformanceConfig(
                analysis_period=performance_data.get("analysis_period", "daily"),
                min_trades_for_analysis=performance_data.get("min_trades_for_analysis", 10),
                risk_free_rate=performance_data.get("risk_free_rate", 0.0),
            )

            # Parse risk config
            risk_data = raw_config.get("risk", {})
            risk = RiskConfig(
                max_iteration_per_day=risk_data.get("max_iteration_per_day", 1),
                max_drawdown_threshold=risk_data.get("max_drawdown_threshold", 0.15),
                min_win_rate_threshold=risk_data.get("min_win_rate_threshold", 0.45),
                core_logic_review_required=risk_data.get("core_logic_review_required", True),
                backtest_performance_decline_threshold=risk_data.get(
                    "backtest_performance_decline_threshold", 0.10
                ),
                live_loss_threshold=risk_data.get("live_loss_threshold", 0.05),
            )

            # Parse validation config
            validation_data = raw_config.get("validation", {})
            validation = ValidationConfig(
                docker_image=validation_data.get("docker_image", "openfund-agent-sandbox:latest"),
                docker_timeout=validation_data.get("docker_timeout", 300),
                run_pytest=validation_data.get("run_pytest", True),
                run_black_check=validation_data.get("run_black_check", True),
                run_backtest=validation_data.get("run_backtest", True),
                backtest_data_days=validation_data.get("backtest_data_days", 30),
            )

            # Parse git config
            git_data = raw_config.get("git", {})
            git = GitConfig(
                auto_commit=git_data.get("auto_commit", True),
                auto_tag=git_data.get("auto_tag", True),
                tag_prefix=git_data.get("tag_prefix", "agent-v"),
            )

            # Parse notification config
            notification_data = raw_config.get("notification", {})
            notification = NotificationConfig(
                enabled=notification_data.get("enabled", True),
                feishu_webhook=notification_data.get("feishu_webhook"),
                notify_on_risk=notification_data.get("notify_on_risk", True),
                notify_on_iteration_complete=notification_data.get(
                    "notify_on_iteration_complete", True
                ),
                notify_on_error=notification_data.get("notify_on_error", True),
            )

            # Parse evolution config
            evolution_data = raw_config.get("evolution", {})
            evolution = EvolutionConfig(
                max_iterations=evolution_data.get("max_iterations", 10),
                goal_metric=evolution_data.get("goal_metric", "sharpe_ratio"),
                goal_threshold=evolution_data.get("goal_threshold", 2.0),
                iteration_interval_hours=evolution_data.get("iteration_interval_hours", 24),
            )

            # Parse database config
            database_data = raw_config.get("database", {})
            database = DatabaseConfig(
                type=database_data.get("type", "sqlite"),
                path=database_data.get("path", "data/openfund_agent.db"),
            )

            # Parse logging config
            logging_data = raw_config.get("logging", {})
            logging = LoggingConfig(
                level=logging_data.get("level", "INFO"),
                format=logging_data.get("format", "json"),
                file_path=logging_data.get("file_path", "logs/openfund_agent.log"),
                max_file_size_mb=logging_data.get("max_file_size_mb", 100),
                backup_count=logging_data.get("backup_count", 5),
            )

            return AgentConfig(
                exchange=exchange,
                claude=claude,
                data_collection=data_collection,
                strategy=strategy,
                performance=performance,
                risk=risk,
                validation=validation,
                git=git,
                notification=notification,
                evolution=evolution,
                database=database,
                logging=logging,
            )
        except Exception as e:
            raise ConfigValidationError(f"Failed to parse configuration: {e}")

    def _validate_config(self, config: AgentConfig) -> None:
        """
        Validate configuration values

        Args:
            config: Configuration to validate

        Raises:
            ConfigValidationError: If validation fails
        """
        errors = []

        # Validate exchange config
        if not config.exchange.name:
            errors.append("Exchange name is required")
        if not config.exchange.api_key or config.exchange.api_key.startswith("${"):
            errors.append("Exchange API key is required (environment variable not set)")
        if not config.exchange.api_secret or config.exchange.api_secret.startswith("${"):
            errors.append("Exchange API secret is required (environment variable not set)")

        # Validate Claude config
        if not config.claude.api_key or config.claude.api_key.startswith("${"):
            errors.append("Claude API key is required (environment variable not set)")
        if config.claude.max_tokens <= 0:
            errors.append("Claude max_tokens must be positive")
        if not 0 <= config.claude.temperature <= 1:
            errors.append("Claude temperature must be between 0 and 1")

        # Validate data collection config
        if config.data_collection.cache_ttl_minutes <= 0:
            errors.append("Cache TTL must be positive")
        if config.data_collection.retry_max_attempts <= 0:
            errors.append("Retry max attempts must be positive")
        if config.data_collection.default_limit <= 0:
            errors.append("Default limit must be positive")

        # Validate strategy config
        if config.strategy.execution_timeout <= 0:
            errors.append("Strategy execution timeout must be positive")
        if config.strategy.max_concurrent_strategies <= 0:
            errors.append("Max concurrent strategies must be positive")

        # Validate performance config
        if config.performance.analysis_period not in ["daily", "weekly", "monthly"]:
            errors.append("Analysis period must be 'daily', 'weekly', or 'monthly'")
        if config.performance.min_trades_for_analysis <= 0:
            errors.append("Min trades for analysis must be positive")

        # Validate risk config
        if config.risk.max_iteration_per_day <= 0:
            errors.append("Max iteration per day must be positive")
        if not 0 <= config.risk.max_drawdown_threshold <= 1:
            errors.append("Max drawdown threshold must be between 0 and 1")
        if not 0 <= config.risk.min_win_rate_threshold <= 1:
            errors.append("Min win rate threshold must be between 0 and 1")

        # Validate validation config
        if config.validation.docker_timeout <= 0:
            errors.append("Docker timeout must be positive")
        if config.validation.backtest_data_days <= 0:
            errors.append("Backtest data days must be positive")

        # Validate evolution config
        if config.evolution.max_iterations <= 0:
            errors.append("Max iterations must be positive")
        if config.evolution.goal_threshold <= 0:
            errors.append("Goal threshold must be positive")
        if config.evolution.iteration_interval_hours <= 0:
            errors.append("Iteration interval hours must be positive")

        # Validate logging config
        if config.logging.level not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
            errors.append("Logging level must be one of: DEBUG, INFO, WARNING, ERROR, CRITICAL")
        if config.logging.max_file_size_mb <= 0:
            errors.append("Max file size must be positive")
        if config.logging.backup_count < 0:
            errors.append("Backup count must be non-negative")

        if errors:
            raise ConfigValidationError(
                "Configuration validation failed:\n" + "\n".join(f"  - {e}" for e in errors)
            )

    def get_config(self) -> AgentConfig:
        """
        Get the loaded configuration

        Returns:
            AgentConfig: The loaded configuration

        Raises:
            RuntimeError: If configuration has not been loaded yet
        """
        if self._config is None:
            raise RuntimeError("Configuration not loaded. Call load() first.")
        return self._config

    def reload(self) -> AgentConfig:
        """
        Reload configuration from file

        Returns:
            AgentConfig: Reloaded configuration
        """
        return self.load()
