import yaml
import logging
from typing import Dict, Any, Type
from pathlib import Path

from atomsql.orm import fields, Model
from atomsql.utils.exceptions import ImproperlyConfigured
from atomingest.normalization.core import NormalizationConfig
from atomingest.schema.validators import (
    FieldValidationConfig,
    parse_field_validators,
)
from atomingest.schema.decorators import apply_validation_to_schema
from atomingest.core.mixins import ImmutableLedgerMixin
from atomingest.schema.encryption import (
    EncryptedCharField,
    EncryptedTextField,
    EncryptedEmailField,
    EncryptedJSONField,
    EncryptedStringField,
)

logger = logging.getLogger(__name__)


class SchemaLoader:
    """
    Parses YAML ingestion configuration and maps them to AtomSQL schema definitions.
    """

    # Mapping of strings names to the actual AtomSQL field classes
    FIELD_MAPPING = {
        "Field": fields.Field,
        "BooleanField": fields.BooleanField,
        "IntegerField": fields.IntegerField,
        "TimestampField": fields.TimeStampField,
        "StringField": fields.StringField,
        "FloatField": fields.FloatField,
        "CharField": fields.CharField,
        "TextField": fields.TextField,
        "JSONField": fields.JSONField,
        "EmailField": fields.EmailField,
        "URLField": fields.URLField,
        "DateField": fields.DateField,
        "DateTimeField": fields.DateTimeField,
        "DateTimeTZField": fields.DateTimeTZField,
        "UUIDField": fields.UUIDField,
        "EncryptedStringField": EncryptedStringField,
        "EncryptedCharField": EncryptedCharField,
        "EncryptedTextField": EncryptedTextField,
        "EncryptedJSONField": EncryptedJSONField,
        "EncryptedEmailField": EncryptedEmailField,
    }

    def __init__(self):
        self._cache = {}

    def load_from_yaml(self, file_path: str) -> Dict[str, Any]:
        """
        Load and parse the YAML configuration file.
        Args:
            file_path (str): Path to the .yaml or .yml file.
        Returns:
            A dictionary containing:
                - target_table (str): The name of the database table.
                - schema (Dict[str, fields.Field]): A mapping of field names to AtomSQL field instances.
                - config (Dict[str, Any]): The raw configuration dictionary for further processing (hooks, etc)
            Raises:
                ImproperlyConfigured: If the file cannot be read or parsed.
                FileNotFoundError: If the file does not exist.
        """
        path = Path(file_path)
        if not path.exists():
            raise FileNotFoundError(f"Configuration file not found: {file_path}")
        try:
            with open(path, "r") as file:
                config = yaml.safe_load(file)
        except yaml.YAMLError as e:
            raise ImproperlyConfigured(f"Error parsing YAML file: {e}")

        if not isinstance(config, dict):
            raise ImproperlyConfigured(
                "YAML configuration must be a dictionary at the top level."
            )

        if "target_table" not in config:
            raise ImproperlyConfigured("Missing 'target_table' in configuration.")
        if "schema" not in config:
            raise ImproperlyConfigured("Missing 'schema' in configuration.")

        target_table = config["target_table"]
        schema_config = config["schema"]

        if not isinstance(schema_config, dict):
            raise ImproperlyConfigured(
                "'schema' must be a dictionary mapping field names to field definitions."
            )

        parsed_schema, validation_configs = self._parse_schema(schema_config)

        business_keys = config.get("business_key", [])
        if business_keys:
            if not isinstance(business_keys, list):
                raise ImproperlyConfigured(
                    "'business_key' must be a list of field names."
                )
            for key in business_keys:
                if key not in parsed_schema:
                    raise ImproperlyConfigured(
                        f"Business key field '{key}' not found in schema."
                    )
                # Mark the field as part of the business key
            logger.info(f"Idempotency enabled using business keys: {business_keys}")

        normalization_config = None
        # Support both UK and US spellings
        norm_key = "normalization" if "normalization" in config else "normalisation"
        if norm_key in config:
            try:
                normalization_config = NormalizationConfig.from_dict(config[norm_key])
            except ValueError as e:
                raise ImproperlyConfigured(f"Error in '{norm_key}' configuration: {e}")

        logger.info(
            f"Loaded schema for table '{target_table}' with fields: {list(parsed_schema.keys())}"
        )
        if validation_configs:
            logger.info(
                f"Loaded validation rules for fields: {list(validation_configs.keys())}"
            )

        # Parse hooks configuration if present
        hooks_config = None
        if "hooks" in config:
            from atomingest.pipeline.hooks import parse_hooks_config

            hooks_config = parse_hooks_config(config["hooks"])

        return {
            "target_table": target_table,
            "schema": parsed_schema,
            "normalization": normalization_config,
            "validation": validation_configs,
            "hooks": hooks_config,
            "config": config,
        }

    def create_model(self, schema_definition: Dict[str, Any]) -> Type[Model]:
        """
        Dynamically create an AtomSQL Model class based on the provided schema definition.
        Args:
            Schema_definition (Dict[str, Any]): The output from load_from_yaml.
            Must contain 'target_table' and 'schema'.
        Returns:
            Type[Model]: A class inheriting from AtomSQL Model with the defined schema.
        """
        target_table = schema_definition["target_table"]
        field_objects = schema_definition["schema"]

        config = schema_definition.get("config", {}).get("config", {})
        is_immutable = config.get("immutable", False)

        attrs = {
            "__module__": "atomingest.dynamic_models",
        }

        attrs.update(field_objects)
        class_name = f"{target_table.capitalize()}"

        bases = [Model]
        if is_immutable:
            bases.insert(0, ImmutableLedgerMixin)
            logger.info(
                f"Applying ImmutableLedgerMixin to model '{class_name}' for table '{target_table}'"
            )
        model_class = type(class_name, tuple(bases), attrs)

        logger.info(
            f"Created dynamic model class '{class_name}' for table '{target_table}'"
        )

        return model_class

    def _parse_schema(
        self, raw_schema: Dict[str, Any]
    ) -> tuple[Dict[str, fields.Field], Dict[str, FieldValidationConfig]]:
        """
        Convert the YAML 'schema' dictionary into AtomSQL Field objects.

        Also extracts and parses validation rules from field definitions.

        Returns:
            A tuple of (field_schema, validation_configs)
        """
        final_schema = {}
        validation_configs = {}

        for field_name, definition in raw_schema.items():
            if isinstance(definition, str):
                field_type_name = definition
                options = {}
                validation_rules = None
            elif isinstance(definition, dict):
                if "type" not in definition:
                    raise ImproperlyConfigured(
                        f"Field '{field_name}' is missing 'type' definition."
                    )
                field_type_name = definition["type"]
                options = definition.copy()
                del options["type"]

                # Extract validation rules before creating the field
                validation_rules = options.pop("validation", None)
            else:
                raise ImproperlyConfigured(
                    f"Invalid definition for field '{field_name}'."
                )

            if field_type_name not in self.FIELD_MAPPING:
                raise ImproperlyConfigured(
                    f"Unknown field type '{field_type_name}' for field '{field_name}'."
                )

            field_class = self.FIELD_MAPPING[field_type_name]

            try:
                field_instance = field_class(**options)
                final_schema[field_name] = field_instance

                # Parse and apply validation rules if present
                if validation_rules:
                    if not isinstance(validation_rules, list):
                        validation_rules = [validation_rules]
                    try:
                        field_config = parse_field_validators(
                            field_name, validation_rules
                        )
                        validation_configs[field_name] = field_config
                        # Inject validators into the field instance
                        apply_validation_to_schema(
                            {field_name: field_instance},
                            {field_name: validation_rules},
                        )
                        logger.debug(
                            f"Applied {len(field_config.validators)} validators "
                            f"to field '{field_name}'"
                        )
                    except ValueError as e:
                        raise ImproperlyConfigured(
                            f"Invalid validation rules for field '{field_name}': {e}"
                        )
            except TypeError as e:
                raise ImproperlyConfigured(
                    f"Error initializing field '{field_name}': {e}"
                )
            except ValueError as e:
                raise ImproperlyConfigured(
                    f"Invalid value for field '{field_name}': {e}"
                )
        return final_schema, validation_configs
