from typing import Any

import toml

from pydantic import ValidationError

from data_exchange_agent import custom_exceptions
from data_exchange_agent.config.base_config import BaseConfig
from data_exchange_agent.config.sections.application import ApplicationConfig
from data_exchange_agent.config.sections.connections import BaseConnectionConfig
from data_exchange_agent.config.sections.connections.connection_registry import (
    ConnectionRegistry,
)
from data_exchange_agent.config.sections.task_sources import (
    TaskSourceConfig,
    TaskSourceRegistry,
)
from data_exchange_agent.constants import config_keys


def _format_validation_errors(errors: list[Any], config_section: str = "") -> str:
    """
    Format Pydantic validation errors into a user-friendly message.

    Args:
        errors: List of error dictionaries from ValidationError.errors()
        config_section: Optional prefix for the config section (e.g., "connections.source.sqlserver")

    Returns:
        Formatted error message string

    """
    formatted_errors = []
    for error in errors:
        # Build the field path
        loc_parts = [str(loc) for loc in error.get("loc", [])]
        field_path = ".".join(loc_parts) if loc_parts else "root"

        # Add config section prefix if provided
        if config_section:
            full_path = (
                f"{config_section}.{field_path}"
                if field_path != "root"
                else config_section
            )
        else:
            full_path = field_path

        # Get the error message
        msg = error.get("msg", "Unknown error")

        formatted_errors.append(f"  - {full_path}: {msg}")

    return "\n".join(formatted_errors)


def _handle_validation_error(
    e: ValidationError, config_section: str, toml_file_path: str | None = None
) -> None:
    """
    Handle a Pydantic ValidationError by formatting and raising a ConfigurationError.

    Args:
        e: The ValidationError exception
        config_section: The config section where the error occurred
        toml_file_path: Optional path to the TOML file for better error messages

    Raises:
        ConfigurationError: Always raises with formatted error messages

    """
    error_count = len(e.errors())
    errors_word = "error" if error_count == 1 else "errors"
    formatted_errors = _format_validation_errors(e.errors(), config_section)

    file_info = f" in '{toml_file_path}'" if toml_file_path else ""
    raise custom_exceptions.ConfigurationError(
        f"{error_count} validation {errors_word} found{file_info}:\n{formatted_errors}"
    ) from e


class TomlConfig(BaseConfig):
    """Configuration class for TOML file settings."""

    def __init__(self):
        """Initialize TOML configuration."""
        self.selected_task_source: str | None = None
        self.application: ApplicationConfig | None = None
        self.task_source: TaskSourceConfig | None = None
        self.connections: dict[str, dict[str, BaseConnectionConfig]] = {
            config_keys.SOURCE: {},
            config_keys.TARGET: {},
        }
        self._current_toml_path: str | None = None

    def __repr__(self) -> str:
        """Return string representation of TOML configuration."""
        return (
            f"TomlConfig(selected_task_source={self.selected_task_source}, "
            f"application={self.application}, "
            f"task_source={self.task_source}, "
            f"connections={self.connections})"
        )

    def load_config(self, path_or_dict: str | dict[str, Any]) -> None:
        """
        Load TOML configuration from file or dictionary and load it into the configuration object.

        Args:
            path_or_dict: Path to the TOML file or dictionary

        """
        if isinstance(path_or_dict, str):
            self._load_config_from_toml(path_or_dict)
        elif isinstance(path_or_dict, dict):
            self._load_config_from_dict(path_or_dict)
        elif hasattr(path_or_dict, "__dict__"):
            self._load_config_from_dict(path_or_dict.__dict__)
        else:
            raise TypeError(
                "path_or_dict must be a string, dict or have a __dict__ attribute"
            )

    def _load_config_from_toml(self, toml_file_path: str) -> None:
        """
        Load TOML configuration from file and load it into the configuration object.

        Args:
            toml_file_path: Path to the TOML file

        """
        self._current_toml_path = toml_file_path
        try:
            config = toml.load(toml_file_path)
            self._load_config_from_dict(config)
        except toml.TomlDecodeError as e:
            raise custom_exceptions.ConfigurationError(
                f"Failed to load configuration file '{toml_file_path}'."
                " Please check if the file is a valid TOML file.\n"
                f"  - Parse error: {e}"
            ) from e
        except OSError as e:
            raise custom_exceptions.ConfigurationError(
                f"Failed to load configuration file '{toml_file_path}'."
                " Please check if the file exists and is readable."
            ) from e
        except custom_exceptions.ConfigurationError:
            # Re-raise ConfigurationError as-is (already formatted)
            raise
        except ValidationError as e:
            # This catches any ValidationError not caught by the specific loaders
            _handle_validation_error(e, "configuration", toml_file_path)
        except (TypeError, ValueError) as e:
            raise custom_exceptions.ConfigurationError(
                f"Invalid configuration data in file '{toml_file_path}'.\n"
                f"  - {type(e).__name__}: {e}"
            ) from e
        except Exception as e:
            raise custom_exceptions.ConfigurationError(
                f"Unexpected error while loading configuration file '{toml_file_path}':\n"
                f"  - {type(e).__name__}: {e}"
            ) from e
        finally:
            self._current_toml_path = None

    def _load_config_from_dict(self, config: dict[str, Any]) -> None:
        """
        Load TOML configuration from dictionary and load it into the configuration object.

        Args:
            config: Dictionary containing configuration data

        """
        # Load selected task source and source connection
        if config_keys.SELECTED_TASK_SOURCE in config:
            self.selected_task_source = config[config_keys.SELECTED_TASK_SOURCE]

        # Load application configuration
        self._load_application_config(config)

        # Load task source configuration (dynamic based on type)
        self._load_task_source_config(config)

        # Load connection configuration (dynamic based on type)
        self._load_connections_config(config)

        # Validate cross-section dependencies
        self._validate_configuration()

    def _load_application_config(self, config: dict[str, Any]) -> None:
        """Load application configuration section."""
        if config_keys.APPLICATION in config:
            app_config = config[config_keys.APPLICATION]
            try:
                self.application = ApplicationConfig(
                    max_parallel_tasks=app_config.get(config_keys.MAX_PARALLEL_TASKS),
                    task_fetch_interval=app_config.get(config_keys.TASK_FETCH_INTERVAL),
                    debug_mode=app_config.get(config_keys.DEBUG_MODE),
                    affinity=app_config.get(config_keys.AFFINITY),
                    snowflake_database_for_metadata=app_config.get(
                        config_keys.SNOWFLAKE_DATABASE_FOR_METADATA
                    ),
                    snowflake_schema_for_data_migration_metadata=app_config.get(
                        config_keys.SNOWFLAKE_SCHEMA_FOR_DATA_MIGRATION_METADATA
                    ),
                )
            except ValidationError as e:
                _handle_validation_error(
                    e, config_keys.APPLICATION, self._current_toml_path
                )

    def _load_task_source_config(self, config: dict[str, Any]) -> None:
        """Load task source configuration section."""
        # If selected_task_source is set, validate and load the corresponding task source config
        if self.selected_task_source is not None:
            if not TaskSourceRegistry.is_registered(self.selected_task_source):
                available = TaskSourceRegistry.list_types()
                raise custom_exceptions.ConfigurationError(
                    f"Unknown task source type '{self.selected_task_source}'. "
                    f"Available types: {', '.join(available)}"
                )

            # Validate that the task_source section exists with the selected type
            if config_keys.TASK_SOURCE not in config:
                raise custom_exceptions.ConfigurationError(
                    f"'{config_keys.SELECTED_TASK_SOURCE}' is set to '{self.selected_task_source}' but "
                    f"'[{config_keys.TASK_SOURCE}.{self.selected_task_source}]' section is missing"
                )

            if self.selected_task_source not in config[config_keys.TASK_SOURCE]:
                raise custom_exceptions.ConfigurationError(
                    f"'{config_keys.SELECTED_TASK_SOURCE}' is set to '{self.selected_task_source}' but "
                    f"'[{config_keys.TASK_SOURCE}.{self.selected_task_source}]' section is missing"
                )

            task_source_config = config[config_keys.TASK_SOURCE][
                self.selected_task_source
            ]
            config_section = f"{config_keys.TASK_SOURCE}.{self.selected_task_source}"
            try:
                self.task_source = TaskSourceRegistry.create(
                    self.selected_task_source, **task_source_config
                )
            except ValidationError as e:
                _handle_validation_error(e, config_section, self._current_toml_path)

        # If task_source section exists but selected_task_source is not set, raise an error
        elif config_keys.TASK_SOURCE in config:
            raise custom_exceptions.ConfigurationError(
                f"'{config_keys.SELECTED_TASK_SOURCE}' must be set to use '{config_keys.TASK_SOURCE}' configuration"
            )

    def _load_connections_config(self, config: dict[str, Any]) -> None:
        """Load connections configuration section."""
        if config_keys.CONNECTIONS in config:
            connections_config = config[config_keys.CONNECTIONS]

            # Load source connections
            if config_keys.SOURCE in connections_config:
                for conn_type, conn_config in connections_config[
                    config_keys.SOURCE
                ].items():
                    config_section = (
                        f"{config_keys.CONNECTIONS}.{config_keys.SOURCE}.{conn_type}"
                    )
                    try:
                        self.connections[config_keys.SOURCE][conn_type] = (
                            ConnectionRegistry.create(conn_type, **conn_config)
                        )
                    except ValidationError as e:
                        _handle_validation_error(
                            e, config_section, self._current_toml_path
                        )

            # Load target connections
            if config_keys.TARGET in connections_config:
                for conn_type, conn_config in connections_config[
                    config_keys.TARGET
                ].items():
                    config_section = (
                        f"{config_keys.CONNECTIONS}.{config_keys.TARGET}.{conn_type}"
                    )
                    try:
                        self.connections[config_keys.TARGET][conn_type] = (
                            ConnectionRegistry.create(conn_type, **conn_config)
                        )
                    except ValidationError as e:
                        _handle_validation_error(
                            e, config_section, self._current_toml_path
                        )

    def _validate_configuration(self) -> None:
        """Validate cross-section configuration dependencies."""
        # Validate that at least one source connection is configured
        source_connections = self.connections.get(config_keys.SOURCE, {})
        if not source_connections:
            raise custom_exceptions.ConfigurationError(
                f"No source connections configured. "
                f"At least one '[{config_keys.CONNECTIONS}.{config_keys.SOURCE}.*]' section is required."
            )

        # Validate that at least one target connection is configured
        target_connections = self.connections.get(config_keys.TARGET, {})
        if not target_connections:
            raise custom_exceptions.ConfigurationError(
                f"No target connections configured. "
                f"At least one '[{config_keys.CONNECTIONS}.{config_keys.TARGET}.*]' section is required."
            )
