"""
Configuration validation for ETL pipelines.

Provides validation of extract, transform, and load configurations
using Pydantic models with clear, actionable error messages.
"""

from __future__ import annotations

from typing import Any

from pydantic import ValidationError as PydanticValidationError

from pycharter.etl_generator.config_models import (
    SettingsConfig,
    TransformConfig,
    parse_extract_config,
    parse_load_config,
    parse_settings_config,
    parse_transform_config,
)
from pycharter.shared.errors import ConfigValidationError


class ConfigValidator:
    """
    Validates ETL pipeline configurations using Pydantic models.

    Provides clear error messages to help users fix configuration issues.

    Usage:
        validator = ConfigValidator()

        # Validate individual configs
        validator.validate_extract(extract_config)
        validator.validate_transform(transform_config)
        validator.validate_load(load_config)

        # Validate combined pipeline
        validator.validate_pipeline(pipeline_config)

        # Check if valid without raising
        is_valid, errors = validator.check_extract(extract_config)
    """

    def __init__(self, strict: bool = True):
        """
        Initialize the validator.

        Args:
            strict: If True, raise errors on validation failure.
                   If False, return errors without raising.
        """
        self.strict = strict

    def _format_pydantic_errors(
        self,
        error: PydanticValidationError,
        config_type: str,
    ) -> list[dict[str, Any]]:
        """Convert Pydantic validation errors to our format."""
        errors = []
        for err in error.errors():
            path = ".".join(str(p) for p in err["loc"]) or "(root)"
            errors.append(
                {
                    "path": path,
                    "message": self._format_error_message(err, config_type),
                    "type": err["type"],
                }
            )
        return errors

    def _format_error_message(self, error: dict[str, Any], config_type: str) -> str:
        """Format a Pydantic error into a user-friendly message."""
        error_type = error["type"]
        msg = error["msg"]

        # Improve common error messages
        if error_type == "missing":
            field = error["loc"][-1] if error["loc"] else "unknown"
            if field == "type":
                valid_types = {
                    "extract": "http, file, database, cloud_storage",
                    "load": "postgres, postgresql, sqlite, file, cloud_storage",
                }
                types = valid_types.get(config_type, "")
                if types:
                    return f"Missing required 'type' field. Must be one of: {types}"
            return f"Missing required field: {field}"

        elif error_type == "enum":
            # Extract allowed values from the input
            ctx = error.get("ctx", {})
            expected = ctx.get("expected", "")
            return f"Invalid value. Allowed values: {expected}"

        elif error_type in ("string_type", "int_type", "float_type", "bool_type"):
            expected = error_type.replace("_type", "")
            return f"Expected {expected}"

        elif error_type == "extra_forbidden":
            field = error["loc"][-1] if error["loc"] else "unknown"
            return f"Unknown field: '{field}'"

        elif error_type == "value_error":
            return str(msg)

        return msg

    def _validate_with_pydantic(
        self,
        config: dict[str, Any],
        config_type: str,
        parse_func,
    ) -> tuple[bool, list[dict[str, Any]]]:
        """Validate config using Pydantic model."""
        try:
            parse_func(config)
            return True, []
        except PydanticValidationError as e:
            return False, self._format_pydantic_errors(e, config_type)
        except ValueError as e:
            # Handle parse_* function ValueErrors (e.g., unknown type)
            return False, [
                {
                    "path": "(root)",
                    "message": str(e),
                    "type": "value_error",
                }
            ]

    def validate_extract(
        self,
        config: dict[str, Any],
        config_path: str | None = None,
    ) -> None:
        """
        Validate extract configuration.

        Args:
            config: Extract configuration dict
            config_path: Optional path to config file (for error messages)

        Raises:
            ConfigValidationError: If validation fails and strict=True
        """
        is_valid, errors = self._validate_with_pydantic(
            config, "extract", parse_extract_config
        )

        if not is_valid and self.strict:
            raise ConfigValidationError(
                "Invalid extract configuration",
                errors=errors,
                config_type="extract",
                config_path=config_path,
            )

    def validate_transform(
        self,
        config: dict[str, Any],
        config_path: str | None = None,
    ) -> None:
        """
        Validate transform configuration.

        Args:
            config: Transform configuration dict
            config_path: Optional path to config file (for error messages)

        Raises:
            ConfigValidationError: If validation fails and strict=True
        """
        # Transform is optional, empty config is valid
        if not config:
            return

        is_valid, errors = self._validate_with_pydantic(
            config, "transform", lambda c: TransformConfig(**c)
        )

        if not is_valid and self.strict:
            raise ConfigValidationError(
                "Invalid transform configuration",
                errors=errors,
                config_type="transform",
                config_path=config_path,
            )

    def validate_load(
        self,
        config: dict[str, Any],
        config_path: str | None = None,
    ) -> None:
        """
        Validate load configuration.

        Args:
            config: Load configuration dict
            config_path: Optional path to config file (for error messages)

        Raises:
            ConfigValidationError: If validation fails and strict=True
        """
        is_valid, errors = self._validate_with_pydantic(
            config, "load", parse_load_config
        )

        if not is_valid and self.strict:
            raise ConfigValidationError(
                "Invalid load configuration",
                errors=errors,
                config_type="load",
                config_path=config_path,
            )

    def validate_settings(
        self,
        config: dict[str, Any],
        config_path: str | None = None,
    ) -> None:
        """
        Validate settings configuration.

        Args:
            config: Settings configuration dict
            config_path: Optional path to config file (for error messages)

        Raises:
            ConfigValidationError: If validation fails and strict=True
        """
        is_valid, errors = self._validate_with_pydantic(
            config, "settings", parse_settings_config
        )

        if not is_valid and self.strict:
            raise ConfigValidationError(
                "Invalid settings configuration",
                errors=errors,
                config_type="settings",
                config_path=config_path,
            )

    def validate_pipeline(
        self,
        config: dict[str, Any],
        config_path: str | None = None,
    ) -> None:
        """
        Validate complete pipeline configuration.

        Args:
            config: Pipeline configuration dict with extract, transform, load sections
            config_path: Optional path to config file (for error messages)

        Raises:
            ConfigValidationError: If validation fails and strict=True
        """
        errors = []

        # Validate extract (required)
        if "extract" not in config:
            errors.append(
                {
                    "path": "extract",
                    "message": "Missing required 'extract' section",
                    "type": "missing",
                }
            )
        else:
            is_valid, extract_errors = self._validate_with_pydantic(
                config["extract"], "extract", parse_extract_config
            )
            for e in extract_errors:
                e["path"] = (
                    f"extract.{e['path']}" if e["path"] != "(root)" else "extract"
                )
            errors.extend(extract_errors)

        # Validate transform (optional)
        if "transform" in config and config["transform"]:
            transform_config = config["transform"]
            # Wrap if it's just simple ops
            if isinstance(transform_config, (list, dict)):
                if isinstance(transform_config, list) or not any(
                    k in transform_config
                    for k in ("transform", "jsonata", "custom_function")
                ):
                    transform_config = {"transform": transform_config}

            is_valid, transform_errors = self._validate_with_pydantic(
                transform_config, "transform", lambda c: TransformConfig(**c)
            )
            for e in transform_errors:
                e["path"] = (
                    f"transform.{e['path']}" if e["path"] != "(root)" else "transform"
                )
            errors.extend(transform_errors)

        # Validate load (required)
        if "load" not in config:
            errors.append(
                {
                    "path": "load",
                    "message": "Missing required 'load' section",
                    "type": "missing",
                }
            )
        else:
            is_valid, load_errors = self._validate_with_pydantic(
                config["load"], "load", parse_load_config
            )
            for e in load_errors:
                e["path"] = f"load.{e['path']}" if e["path"] != "(root)" else "load"
            errors.extend(load_errors)

        if errors and self.strict:
            raise ConfigValidationError(
                "Invalid pipeline configuration",
                errors=errors,
                config_type="pipeline",
                config_path=config_path,
            )

    def check_extract(
        self,
        config: dict[str, Any],
    ) -> tuple[bool, list[dict[str, Any]]]:
        """Check extract config without raising. Returns (is_valid, errors)."""
        return self._validate_with_pydantic(config, "extract", parse_extract_config)

    def check_transform(
        self,
        config: dict[str, Any],
    ) -> tuple[bool, list[dict[str, Any]]]:
        """Check transform config without raising. Returns (is_valid, errors)."""
        if not config:
            return True, []
        return self._validate_with_pydantic(
            config, "transform", lambda c: TransformConfig(**c)
        )

    def check_load(
        self,
        config: dict[str, Any],
    ) -> tuple[bool, list[dict[str, Any]]]:
        """Check load config without raising. Returns (is_valid, errors)."""
        return self._validate_with_pydantic(config, "load", parse_load_config)

    def check_settings(
        self,
        config: dict[str, Any],
    ) -> tuple[bool, list[dict[str, Any]]]:
        """Check settings config without raising. Returns (is_valid, errors)."""
        return self._validate_with_pydantic(config, "settings", parse_settings_config)


def validate_config(
    config: dict[str, Any],
    config_type: str = "pipeline",
    config_path: str | None = None,
    strict: bool = True,
) -> tuple[bool, list[dict[str, Any]]]:
    """
    Convenience function to validate a config.

    Args:
        config: Configuration dict
        config_type: One of "extract", "transform", "load", "settings", "pipeline"
        config_path: Optional path to config file
        strict: If True, raise on validation failure

    Returns:
        Tuple of (is_valid, errors)

    Raises:
        ConfigValidationError: If strict=True and validation fails
    """
    validator = ConfigValidator(strict=strict)

    if config_type == "extract":
        validator.validate_extract(config, config_path)
        return validator.check_extract(config)
    elif config_type == "transform":
        validator.validate_transform(config, config_path)
        return validator.check_transform(config)
    elif config_type == "load":
        validator.validate_load(config, config_path)
        return validator.check_load(config)
    elif config_type == "settings":
        validator.validate_settings(config, config_path)
        return validator.check_settings(config)
    elif config_type == "pipeline":
        validator.validate_pipeline(config, config_path)
        # Return combined check
        errors = []
        if "extract" in config:
            _, e = validator.check_extract(config["extract"])
            errors.extend(e)
        if "transform" in config:
            _, e = validator.check_transform(config.get("transform", {}))
            errors.extend(e)
        if "load" in config:
            _, e = validator.check_load(config["load"])
            errors.extend(e)
        return len(errors) == 0, errors
    else:
        raise ValueError(f"Unknown config_type: {config_type}")
