"""A utility class to read, parse, and instantiate Kurier lines."""

import configparser
import json
import os
import re
from typing import Any

import defusedxml.ElementTree as ET
import xmltodict
import yaml

try:
    import tomllib
except ImportError:
    import tomli as tomllib

from ..core.basekurier import BaseKurier
from ..core.bus_types import BusType
from ..core.config_types import ConfigType
from ..hermes import Hermes
from ..utilities.serializers import resolve_serializer
from .models.azureservicebus import AzureServiceBusConfig
from .models.kafka import KafkaConfig
from .models.rabbitmq import RabbitMQConfig


class Configurator:
    """
    A utility class to read, parse, and instantiate Kurier lines
    from various configuration formats (JSON, TOML, YAML, INI/CFG, XML).
    """

    _BUS_CLASS_MAP: dict[str, tuple[str | type[Any], type[Any] | None]] = {}
    _BUS_EXTRAS_MAP: dict[str, str] = {}

    @classmethod
    def register_transport(
        cls,
        name: str,
        class_or_path: str | type[Any],
        config_class: type[Any] | None,
        extra_requires: str | None = None,
    ):
        """
        Registers a new transport type.

        Args:
            name (str): The name of the transport (e.g., 'rabbitmq', 'redis').
            class_or_path: The transport class itself, or a dotted import path
                string for lazy loading (e.g., ``"mypackage.redis.RedisLine"``).
            config_class (Type[Any] | None): The configuration model class (Pydantic or dataclass).
            extra_requires (str | None): Optional pip extra requirement string (e.g., 'kurier-py[rabbitmq]').
        """
        cls._BUS_CLASS_MAP[name] = (class_or_path, config_class)
        if extra_requires:
            cls._BUS_EXTRAS_MAP[name] = extra_requires

    _ENV_VAR_PATTERN = re.compile(r"\$\{(?P<name>[A-Za-z_][A-Za-z0-9_]*)(?::-(?P<default>[^}]*))?\}")

    @staticmethod
    def _interpolate_env_vars(raw_config: str) -> str:
        """Replaces ${VAR} and ${VAR:-default} placeholders with environment variable values.

        Raises:
            ValueError: If any ``${VAR}`` placeholder references an unset
                environment variable and no default value is provided.
        """
        unresolved: list[str] = []

        def _replace(match: re.Match) -> str:
            name = match.group("name")
            default = match.group("default")
            value = os.environ.get(name)
            if value is not None:
                return value
            if default is not None:
                return default
            unresolved.append(name)
            return match.group(0)

        result = Configurator._ENV_VAR_PATTERN.sub(_replace, raw_config)

        if unresolved:
            names = ", ".join(unresolved)
            raise ValueError(f"Unresolved environment variables (no default provided): {names}")

        return result

    @staticmethod
    def _is_valid_cfg_or_ini(raw_string: str) -> bool:
        """Checks if a string is a valid INI/CFG format."""
        config = configparser.ConfigParser()
        try:
            config.read_string(raw_string)
            return True
        except configparser.Error:
            return False
        except Exception:
            return False

    @staticmethod
    def _is_valid_json(raw_string: str) -> bool:
        """Checks if a string is a valid JSON format."""
        try:
            json.loads(raw_string)
            return True
        except json.JSONDecodeError:
            return False
        except Exception:
            return False

    @staticmethod
    def _is_valid_toml(raw_string: str) -> bool:
        """Checks if a string is a valid TOML format."""
        try:
            tomllib.loads(raw_string)
            return True
        except Exception:  # tomllib.TomlDecodeError is often caught by generic Exception
            return False

    @staticmethod
    def _is_valid_xml(raw_string: str) -> bool:
        """Checks if a string is a valid XML format."""
        try:
            ET.fromstring(raw_string)
            return True
        except ET.ParseError:
            return False
        except Exception:
            return False

    @staticmethod
    def _is_valid_yaml(raw_string: str) -> bool:
        """Checks if a string is a valid YAML format."""
        try:
            yaml.safe_load(raw_string)  # Use safe_load for security
            return True
        except yaml.YAMLError:
            return False
        except Exception:
            return False

    @staticmethod
    def get_config_format(raw_format: str) -> ConfigType:
        """Detects the configuration format of a given string."""
        if Configurator._is_valid_json(raw_format):
            return ConfigType.JSON
        if Configurator._is_valid_toml(raw_format):
            return ConfigType.TOML
        if Configurator._is_valid_cfg_or_ini(raw_format):
            return ConfigType.CFG_OR_INI
        if Configurator._is_valid_xml(raw_format):
            return ConfigType.XML
        if Configurator._is_valid_yaml(raw_format):
            return ConfigType.YAML
        return ConfigType.NOT_SUPPORTED

    @staticmethod
    def _parse_ini_config(raw_config: str) -> dict[str, Any]:
        config = configparser.ConfigParser()
        config.read_string(raw_config)
        parsed = {s: dict(config.items(s)) for s in config.sections()}
        return parsed

    @staticmethod
    def _parse_json_config(raw_config: str) -> dict[str, Any]:
        return json.loads(raw_config)

    @staticmethod
    def _parse_toml_config(raw_config: str) -> dict[str, Any]:
        return tomllib.loads(raw_config)

    @staticmethod
    def _parse_xml_config(raw_config: str) -> dict[str, Any]:
        # Validate with defusedxml first to reject malicious payloads (XXE, bombs)
        ET.fromstring(raw_config)
        return dict(xmltodict.parse(raw_config))

    @staticmethod
    def _parse_yaml_config(raw_config: str) -> dict[str, Any]:
        return yaml.safe_load(raw_config)

    @staticmethod
    def _read_config_source(config_source: str) -> str:
        resolved = os.path.realpath(config_source)
        if not os.path.exists(resolved):
            raise FileNotFoundError(f"Config file not found: {config_source}")
        if not os.path.isfile(resolved):
            raise ValueError(f"Config source is not a file: {config_source}")

        Hermes.debug(f"Reading configuration from file: {resolved}")
        with open(resolved, encoding="utf-8") as f:
            content = f.read()
            return content

    @staticmethod
    def _parse_configs(
        raw_config_content: str,
    ) -> tuple[ConfigType, dict[str, Any]]:
        config_format = Configurator.get_config_format(raw_config_content)

        parsed_config: dict[str, Any] = {}
        match config_format:
            case ConfigType.CFG_OR_INI:
                parsed_config = Configurator._parse_ini_config(raw_config_content)
            case ConfigType.JSON:
                parsed_config = Configurator._parse_json_config(raw_config_content)
            case ConfigType.TOML:
                parsed_config = Configurator._parse_toml_config(raw_config_content)
            case ConfigType.XML:
                parsed_config = Configurator._parse_xml_config(raw_config_content)
            case ConfigType.YAML:
                parsed_config = Configurator._parse_yaml_config(raw_config_content)
            case ConfigType.NOT_SUPPORTED:
                raise ValueError("Invalid or unsupported config format provided.")

        return config_format, parsed_config

    @classmethod
    def _resolve_transport_class(cls, class_or_path: str | type[Any]) -> type[Any]:
        """Resolve a transport class from either a direct reference or a dotted import path.

        When *class_or_path* is already a class it is returned directly.
        When it is a string, it is dynamically imported — but only if
        the path has been explicitly registered via ``register_transport``.
        """
        if isinstance(class_or_path, type):
            return class_or_path

        allowed_paths = {path for path, _ in cls._BUS_CLASS_MAP.values() if isinstance(path, str)}
        if class_or_path not in allowed_paths:
            raise ImportError(f"Unauthorized class path: {class_or_path!r}. Only registered transport paths are allowed.")

        parts = class_or_path.rsplit(".", 1)
        if len(parts) < 2:
            raise ImportError(f"Invalid class path: {class_or_path}")
        module_path, class_name = parts
        module = __import__(module_path, fromlist=[class_name])
        return getattr(module, class_name)

    @classmethod
    def _process_single_bus_instance(
        cls,
        bus_type_name: str,
        instance_config_data: dict[str, Any],
    ) -> tuple[str, BaseKurier] | None:
        """Helper to process and instantiate a single line instance."""

        bus_class_or_path, config_class = cls._BUS_CLASS_MAP[bus_type_name]

        try:
            bus_class = cls._resolve_transport_class(bus_class_or_path)
        except ImportError as e:
            extra_name = cls._BUS_EXTRAS_MAP.get(bus_type_name)
            if extra_name:
                Hermes.fatal(f"Failed to import line implementation for '{bus_type_name}'. Ensure you have installed the required extra: 'pip install {extra_name}'. Error: {e}")
            else:
                Hermes.fatal(f"Failed to import line implementation for '{bus_type_name}': {e}")
            return None

        bus_instance_name = instance_config_data.get("name")
        if not bus_instance_name:
            Hermes.error(f"Instance under '{bus_type_name}' is missing the required 'name' field. Skipping this instance.")
            return None

        try:
            if config_class is None:
                serializer = resolve_serializer(instance_config_data.get("serializer"), instance_config_data.get("cloudevents_source"))
                line_kwargs = dict(instance_config_data)
                line_kwargs.pop("serializer", None)
                line_kwargs.pop("cloudevents_source", None)
                # Some transports (like InMemory) might not need a config class,
                # but usually they at least take kwargs.
                bus_instance = bus_class(**line_kwargs, serializer=serializer)
            else:
                config_instance = config_class(**instance_config_data)
                serializer = resolve_serializer(config_instance.serializer, config_instance.cloudevents_source)
                bus_instance = bus_class(config=config_instance, serializer=serializer)

            Hermes.info(f"Successfully configured and instantiated {bus_type_name} line: '{bus_instance.name}'.")

            return bus_instance.name, bus_instance
        except Exception as e:
            Hermes.fatal(f"Failed to instantiate line '{bus_instance_name}' (type: {bus_type_name}): {e}")
            return None

    @classmethod
    def _create_buses_from_parsed_config(cls, parsed_config: dict[str, Any]) -> dict[str, BaseKurier]:
        """
        Instantiates line transports based on the parsed configuration dictionary.
        """
        created_buses: dict[str, BaseKurier] = {}

        for (
            bus_type_key_str,
            config_data_or_instances_dict,
        ) in parsed_config.items():
            # Normalize key to lower case to match registration keys
            bus_type_key = bus_type_key_str.lower()

            if bus_type_key not in cls._BUS_CLASS_MAP:
                error_msg = f"Line type '{bus_type_key}' is defined in config but not supported/registered."
                Hermes.warn(error_msg)
                raise ValueError(error_msg)

            if not isinstance(config_data_or_instances_dict, dict):
                error_msg = f"Skipping top-level section '{bus_type_key}' as its format is not a dictionary."
                Hermes.warn(error_msg)
                raise ValueError(error_msg)

            if "name" in config_data_or_instances_dict:
                # Flat format: single instance configuration
                result = cls._process_single_bus_instance(bus_type_key, config_data_or_instances_dict)
                if result:
                    bus_name, bus_instance = result
                    created_buses[bus_name] = bus_instance
            else:
                # Nested format: dictionary of named instances
                for (
                    instance_key,
                    instance_config_data,
                ) in config_data_or_instances_dict.items():
                    if not isinstance(instance_config_data, dict):
                        Hermes.warn(f"Warning: Skipping instance '{instance_key}' under '{bus_type_key}'.")
                        continue

                    # Ensure 'name' is present, defaulting to instance_key if missing
                    if "name" not in instance_config_data:
                        instance_config_data["name"] = instance_key

                    result = cls._process_single_bus_instance(bus_type_key, instance_config_data)

                    if result:
                        bus_name, bus_instance = result
                        created_buses[bus_name] = bus_instance

        return created_buses

    @classmethod
    async def init_transports(cls, config_source: str):
        """
        Initializes and creates Kurier lines from a given configuration source.
        The source can be a file path or a raw configuration string.

        Args:
            config_source (str): Path to the configuration file, or the raw
                                 configuration string content.

        Returns:
            dict[str, BaseKurier]: A dictionary of instantiated line objects,
                                where keys are line names.

        Raises:
            FileNotFoundError: If the config_source is a file path but doesn't exist.
            ValueError: If the config format is unsupported or parsing fails.
            Exception: For errors during line instantiation.
        """

        Hermes.info("Configurator: Initializing lines from configuration source.")
        raw_config_content: str

        if not config_source:
            raise ValueError("Invalid config source: Empty string.")

        if os.path.exists(config_source) and os.path.isfile(config_source):
            raw_config_content = cls._read_config_source(config_source)
        else:
            raw_config_content = config_source

        raw_config_content = cls._interpolate_env_vars(raw_config_content)

        config_format, parsed_config = cls._parse_configs(raw_config_content)

        if not isinstance(parsed_config, dict):
            raise ValueError("Invalid config format: Parsed config is not a dictionary.")

        if config_format == ConfigType.XML:
            parsed_config = next(iter(parsed_config.values()))

        elif config_format == ConfigType.CFG_OR_INI:
            cleand_config = {}

            for key, value in parsed_config.items():
                if "." in key:
                    bus = key.split(".")[0]
                    bus_name = key.split(".")[1]

                    cleand_config.setdefault(bus, {})
                    cleand_config[bus].setdefault(bus_name, value)

                else:
                    cleand_config[key] = value

            parsed_config = cleand_config

        Hermes.info(f"Configurator: Detected format: {config_format.name}. Sections: {list(parsed_config.keys())}")

        created_buses = cls._create_buses_from_parsed_config(parsed_config)

        Hermes.info(f"Configurator: Initialized {len(created_buses)} line(s).")

        return created_buses


def _register_default_transports() -> None:
    """Register the built-in transports using lazy imports.

    Each transport class is resolved at instantiation time (not import time)
    so that optional dependencies are only required when actually used.
    """
    from ..transports import _LAZY_IMPORTS

    for class_name, module_path in _LAZY_IMPORTS.items():
        full_path = f"{module_path}.{class_name}"
        bus_type_value: str | None = None
        config_class: type[Any] | None = None
        extra: str | None = None

        if class_name == "RabbitMQLine":
            bus_type_value = BusType.RABBIT_MQ.value
            config_class = RabbitMQConfig
            extra = "kurier-py[rabbitmq]"
        elif class_name == "KafkaLine":
            bus_type_value = BusType.KAFKA.value
            config_class = KafkaConfig
            extra = "kurier-py[kafka]"
        elif class_name == "AzureLine":
            bus_type_value = BusType.AZURE.value
            config_class = AzureServiceBusConfig
            extra = "kurier-py[azure]"

        if bus_type_value:
            Configurator.register_transport(bus_type_value, full_path, config_class, extra)

    # InMemoryLine is always available — import directly
    from ..transports.inmemory.bus import InMemoryLine

    Configurator.register_transport(BusType.IN_MEMORY.value, InMemoryLine, None)


_register_default_transports()
