from __future__ import annotations

import collections.abc
import dataclasses
import json
import logging
import os
from copy import deepcopy
from dataclasses import fields, is_dataclass
from enum import Enum
from typing import Any, Dict, List, Optional, Type, Union, get_args, get_origin

import yaml

from vidhi.constants import (
    DEFAULT_MAX_COMBINATIONS,
    DICT_KEY_NAME,
    DICT_KEY_TYPE,
    MSG_COMBINATORIAL_EXPLOSION,
    MSG_INVALID_TYPE_VALUE,
    MSG_UNKNOWN_ARGUMENTS,
)
from vidhi.types import ConfigDict

# Primitive types that don't require special handling
PRIMITIVE_TYPES = {int, str, float, bool, type(None)}

# Valid key types for container bracket access
CONTAINER_KEY_TYPES = {str, int, float, bool}


@dataclasses.dataclass
class ContainerFieldInfo:
    """Metadata about a container field for bracket-access CLI parsing.

    Stores the type tree so the parser can validate and resolve paths like
    ``--field[0].subfield value`` at parse time.
    """

    container_kind: str  # "list" or "dict"
    key_type: type  # int for lists, primitive type for dicts
    value_type: Any  # The element/value type
    value_is_dataclass: bool  # Whether value_type has __dataclass_fields__
    value_is_poly: bool  # Whether value_type is a BasePolyConfig subclass
    nested_container: Optional[ContainerFieldInfo] = (
        None  # If value is itself a container
    )


def _build_container_info(field_type: Any) -> ContainerFieldInfo:
    """Build a ContainerFieldInfo by introspecting the type tree.

    Recursively resolves nested containers (e.g., Dict[str, List[DataClass]]).

    Args:
        field_type: A container type like List[X] or Mapping[K, V]

    Returns:
        A ContainerFieldInfo describing the container structure

    Raises:
        ValueError: If a dict/mapping key type is not primitive
    """
    from vidhi.base_poly_config import BasePolyConfig

    args = get_args(field_type)

    if is_list(field_type):
        inner_type = _strip_optional(args[0]) if args else Any
        nested = None
        value_is_dc = hasattr(inner_type, "__dataclass_fields__")
        value_is_poly = _issubclass_safe(inner_type, BasePolyConfig)

        if is_container(inner_type):
            nested = _build_container_info(inner_type)

        return ContainerFieldInfo(
            container_kind="list",
            key_type=int,
            value_type=inner_type,
            value_is_dataclass=value_is_dc,
            value_is_poly=value_is_poly,
            nested_container=nested,
        )

    elif is_dict(field_type):
        if len(args) < 2:
            return ContainerFieldInfo(
                container_kind="dict",
                key_type=str,
                value_type=Any,
                value_is_dataclass=False,
                value_is_poly=False,
            )

        key_type = args[0]
        val_type = _strip_optional(args[1])

        if not is_valid_container_key_type(key_type):
            raise ValueError(
                f"Dict/Mapping key type must be a primitive (str, int, float, bool, Enum), "
                f"got {key_type} in type {field_type}"
            )

        nested = None
        value_is_dc = hasattr(val_type, "__dataclass_fields__")
        value_is_poly = _issubclass_safe(val_type, BasePolyConfig)

        if is_container(val_type):
            nested = _build_container_info(val_type)

        return ContainerFieldInfo(
            container_kind="dict",
            key_type=key_type,
            value_type=val_type,
            value_is_dataclass=value_is_dc,
            value_is_poly=value_is_poly,
            nested_container=nested,
        )

    raise ValueError(f"Not a container type: {field_type}")


logger = logging.getLogger(__name__)


def get_all_subclasses(cls):
    subclasses = cls.__subclasses__()
    return subclasses + [g for s in subclasses for g in get_all_subclasses(s)]


def is_primitive_type(field_type: type) -> bool:
    """Check if the given type is a primitive type.

    Args:
        field_type: The type to check

    Returns:
        True if the type is a primitive (int, str, float, bool, or None)
    """
    return field_type in PRIMITIVE_TYPES


def is_generic_composed_of_primitives(field_type: type) -> bool:
    origin = get_origin(field_type)
    if origin in {
        list,
        dict,
        tuple,
        Union,
        collections.abc.Sequence,
        collections.abc.Mapping,
    }:
        # Check all arguments of the generic type
        args = get_args(field_type)
        return all(is_composed_of_primitives(arg) for arg in args)
    return False


def is_composed_of_primitives(field_type: type) -> bool:
    # Check if the type is a primitive type
    if is_primitive_type(field_type):
        return True

    # Check if the type is a generic type composed of primitives
    if is_generic_composed_of_primitives(field_type):
        return True

    return False


def to_snake_case(name: str) -> str:
    result: list[str] = []
    for i, ch in enumerate(name):
        if ch.isupper():
            # Insert underscore before this uppercase letter when not at start AND:
            # - previous char is lowercase (e.g. "camelCase" → "camel_case"), OR
            # - next char is lowercase, marking end of acronym (e.g. "HTTPResponse" → "http_response")
            if i > 0 and (
                name[i - 1].islower() or (i + 1 < len(name) and name[i + 1].islower())
            ):
                result.append("_")
            result.append(ch.lower())
        else:
            result.append(ch)
    return "".join(result)


def is_optional(field_type: Any) -> bool:
    import types

    origin = get_origin(field_type)
    return (origin is Union or isinstance(field_type, types.UnionType)) and type(
        None
    ) in get_args(field_type)


def is_list(field_type: Any) -> bool:
    # Check if the field type is a List or Sequence
    origin = get_origin(field_type)
    return origin is list or origin is collections.abc.Sequence


def is_dict(field_type: Any) -> bool:
    # Check if the field type is a Dict or Mapping
    origin = get_origin(field_type)
    return origin is dict or origin is collections.abc.Mapping


def is_container(field_type: Any) -> bool:
    """Check if the field type is a list or dict (including Sequence/Mapping)."""
    return is_list(field_type) or is_dict(field_type)


def is_valid_container_key_type(key_type: Any) -> bool:
    """Check if a type is valid as a container key (primitive or Enum).

    Container keys in bracket-access syntax must be coercible from a CLI string token.
    """
    if key_type in CONTAINER_KEY_TYPES:
        return True
    return isinstance(key_type, type) and issubclass(key_type, Enum)


def is_bool(field_type: Any) -> bool:
    return field_type is bool


def get_inner_type(field_type: type) -> type:
    return next(t for t in get_args(field_type) if t is not type(None))


def is_subclass(cls, parent: type) -> bool:
    return hasattr(cls, "__bases__") and parent in cls.__bases__


def dataclass_to_dict(obj: Any) -> Any:
    """Recursively convert a dataclass to a JSON-serializable dictionary.

    This function converts dataclass instances (and any nested structures) into
    dictionaries composed only of primitives, lists, and dicts. This is useful
    for:
    - Saving configurations to YAML/JSON files
    - Serializing for APIs
    - Round-trip testing (dataclass → dict → dataclass)

    Special handling:
    - Dataclasses: Converted to dictionaries recursively
    - Lists/tuples: Elements converted recursively
    - Enums: Converted to their value (or name if value isn't JSON-serializable)
    - Polymorphic configs: Adds a "type" key with the enum name
    - Primitives: Returned as-is

    Args:
        obj: The object to convert. Can be a dataclass, list, dict, enum, or primitive.

    Returns:
        A JSON-serializable structure (dict, list, or primitive). Dataclasses
        become dicts with field names as keys.

    Example:
        >>> @frozen_dataclass
        >>> class DatabaseConfig:
        >>>     host: str
        >>>     port: int = 5432
        >>>
        >>> db = DatabaseConfig(host="localhost")
        >>> dataclass_to_dict(db)
        {'host': 'localhost', 'port': 5432}
        >>>
        >>> # Polymorphic example:
        >>> storage = S3Storage(bucket="my-bucket")
        >>> dataclass_to_dict(storage)
        {'bucket': 'my-bucket', 'type': 'S3'}  # 'type' added automatically
    """
    # lists and tuples
    if isinstance(obj, list):
        return [dataclass_to_dict(item) for item in obj]

    # dicts
    if isinstance(obj, dict):
        return {k: dataclass_to_dict(v) for k, v in obj.items()}

    # enums
    if isinstance(obj, Enum):
        return (
            obj.value
            if isinstance(obj.value, (str, int, float, bool, type(None)))
            else obj.name
        )

    # dataclasses
    if is_dataclass(obj):
        data = {}
        for field in fields(obj):
            value = getattr(obj, field.name)
            data[field.name] = dataclass_to_dict(value)

        for key, value in obj.__dict__.items():
            # Exclude private/internal attributes and already-processed fields
            if key not in data and not key.startswith("_"):
                data[key] = dataclass_to_dict(value)

        if hasattr(obj, "get_type") and callable(getattr(obj, "get_type", None)):
            type_val = obj.get_type()  # type: ignore[attr-defined]
            # Store enum value for polymorphic types (e.g., "lru" not "LRU")
            # This matches typical YAML/JSON config conventions
            if hasattr(type_val, "value") and isinstance(type_val.value, str):
                data[DICT_KEY_TYPE] = type_val.value
            elif hasattr(type_val, "name"):
                data[DICT_KEY_TYPE] = type_val.name.lower()
            else:
                data[DICT_KEY_TYPE] = str(type_val)
        elif hasattr(obj, "get_name") and callable(getattr(obj, "get_name", None)):
            data[DICT_KEY_NAME] = obj.get_name()  # type: ignore[attr-defined]
        return data

    # all other values (primitives and non-serializable types like torch.dtype)
    return obj


def _serialize_for_output(obj: Any) -> Any:
    """Convert a value to a JSON/YAML serializable form.

    This is used by to_json() and to_yaml() to handle non-serializable types.
    """
    if isinstance(obj, dict):
        return {k: _serialize_for_output(v) for k, v in obj.items()}
    if isinstance(obj, (list, tuple)):
        return [_serialize_for_output(item) for item in obj]
    if isinstance(obj, Enum):
        return (
            obj.value
            if isinstance(obj.value, (str, int, float, bool, type(None)))
            else obj.name
        )
    if isinstance(obj, (str, int, float, bool, type(None))):
        return obj
    # Non-serializable types (torch.dtype, numpy types, etc.) -> string
    return str(obj)


def dataclass_to_json(obj: Any) -> str:
    """Convert a dataclass to a JSON string.

    Handles non-serializable types by converting them to strings.
    """
    import json

    d = dataclass_to_dict(obj)
    return json.dumps(_serialize_for_output(d), indent=2)


def dataclass_to_yaml(obj: Any) -> str:
    """Convert a dataclass to a YAML string.

    Handles non-serializable types by converting them to strings.
    """
    import yaml

    d = dataclass_to_dict(obj)
    return yaml.dump(_serialize_for_output(d), default_flow_style=False)


def expand_dict(
    d: Dict, *, max_combinations: int = DEFAULT_MAX_COMBINATIONS
) -> List[Dict]:
    """
    Recursively expand a configuration dictionary that may contain lists into
    a list of dictionaries representing every combination in the Cartesian
    product of the list elements. Lists may appear at any depth of the
    configuration tree. Nested dictionaries are handled recursively.

    Args:
        d: Configuration dictionary to expand.
        max_combinations: Upper bound on generated combinations to prevent
            combinatorial explosion. Defaults to DEFAULT_MAX_COMBINATIONS.

    Raises:
        ValueError: If the number of combinations exceeds *max_combinations*.
    """
    variants: List[Dict] = [dict()]

    for key, value in d.items():
        # Figure out all the possible values for this key
        if isinstance(value, list):
            # Each element in the list may be a dictionary that itself needs
            # expansion. Scalars are taken as-is.
            possible_values = []
            for item in value:
                if isinstance(item, dict):
                    possible_values.extend(
                        expand_dict(item, max_combinations=max_combinations)
                    )
                else:
                    possible_values.append(item)
        elif isinstance(value, dict):
            possible_values = expand_dict(value, max_combinations=max_combinations)
        else:
            possible_values = [value]

        # Compose current variants with the new possibilities (cartesian product)
        new_variants: list[dict] = []
        for base in variants:
            for option in possible_values:
                v_copy = deepcopy(base)
                v_copy[key] = option
                new_variants.append(v_copy)
                if len(new_variants) > max_combinations:
                    raise ValueError(
                        MSG_COMBINATORIAL_EXPLOSION.format(
                            count=len(new_variants), max=max_combinations
                        )
                    )
        variants = new_variants

    return variants


def _strip_optional(t: Any) -> Any:
    """Return the inner type if *t* is Optional[T], otherwise return *t* unchanged."""
    if get_origin(t) is Union:
        non_none = [a for a in get_args(t) if a is not type(None)]  # noqa: E721
        if len(non_none) == 1:
            return non_none[0]
    return t


def _issubclass_safe(cls: Any, parent: type) -> bool:
    """Safe variant of issubclass that returns False for non-class *cls*."""
    try:
        return isinstance(cls, type) and issubclass(cls, parent)
    except TypeError:
        return False


def _process_list_field(
    field_type: type, raw_value: list, BasePolyConfig: type
) -> list:
    """Process a list field, recursively handling dataclass items.

    Args:
        field_type: The type of the list (e.g., List[SomeDataclass])
        raw_value: The raw list value from config
        BasePolyConfig: The BasePolyConfig class for type checking

    Returns:
        Processed list with dataclass items instantiated
    """
    inner_type = _strip_optional(get_args(field_type)[0])
    if is_dataclass(inner_type) or _issubclass_safe(inner_type, BasePolyConfig):
        assert isinstance(inner_type, type), f"Expected type, got {type(inner_type)}"
        return [
            create_class_from_dict(inner_type, item) if isinstance(item, dict) else item
            for item in raw_value
        ]
    return raw_value


def _process_dict_field(
    field_type: type, raw_value: dict, BasePolyConfig: type
) -> dict:
    """Process a dict field, recursively handling dataclass values.

    Args:
        field_type: The type of the dict (e.g., Dict[str, SomeDataclass])
        raw_value: The raw dict value from config
        BasePolyConfig: The BasePolyConfig class for type checking

    Returns:
        Processed dict with dataclass values instantiated
    """
    key_type, val_type = get_args(field_type)
    if is_dataclass(val_type) or _issubclass_safe(val_type, BasePolyConfig):
        assert isinstance(val_type, type), f"Expected type, got {type(val_type)}"
        return {
            k: create_class_from_dict(val_type, v) if isinstance(v, dict) else v
            for k, v in raw_value.items()
        }
    return raw_value


def resolve_container_value(raw_value: Any, container_info: Any) -> Any:
    """Recursively resolve a raw value into typed container instances.

    Converts nested dicts assembled from bracket-path CLI assignments (or from
    YAML/JSON inline) into properly typed container values with dataclass instances.

    For list containers, raw_value can be:
    - A dict with integer keys (from bracket parsing): {0: {...}, 1: {...}} -> [inst0, inst1]
    - An actual list (from YAML/JSON inline): [{...}, {...}] -> [inst0, inst1]

    For dict containers, raw_value is a dict where values may need resolution.

    Args:
        raw_value: The raw assembled value (nested dict/list from parsing)
        container_info: A ContainerFieldInfo describing the container type tree

    Returns:
        A properly typed container value with dataclass instances constructed
    """
    if raw_value is None:
        return None

    if container_info.container_kind == "list":
        if isinstance(raw_value, dict):
            # Dict with string or int keys from bracket parsing: {"0": ..., "1": ...} -> [...]
            if not raw_value:
                return []
            # Build (int_index, original_key) pairs for sorting
            indexed = [(int(k), k) for k in raw_value.keys()]
            indexed.sort(key=lambda x: x[0])
            sorted_indices = [idx for idx, _ in indexed]
            # Validate contiguous indices
            expected = list(range(len(sorted_indices)))
            if sorted_indices != expected:
                raise ValueError(
                    f"Sparse list indices not supported. "
                    f"Got indices {sorted_indices}, expected contiguous {expected}"
                )
            raw_list = [raw_value[orig_key] for _, orig_key in indexed]
        elif isinstance(raw_value, list):
            raw_list = raw_value
        else:
            return raw_value

        return [_resolve_element(elem, container_info) for elem in raw_list]

    elif container_info.container_kind == "dict":
        if not isinstance(raw_value, dict):
            return raw_value

        result = {}
        for key, val in raw_value.items():
            typed_key = container_info.key_type(key)
            result[typed_key] = _resolve_element(val, container_info)
        return result

    return raw_value


def _resolve_element(elem: Any, container_info: Any) -> Any:
    """Resolve a single container element according to the container's value type."""
    if container_info.nested_container is not None:
        # Value is itself a container — recurse
        return resolve_container_value(elem, container_info.nested_container)

    if container_info.value_is_dataclass or container_info.value_is_poly:
        if isinstance(elem, dict):
            # Coerce string values from CLI to field types before instantiation
            coerced = _coerce_dict_for_dataclass(elem, container_info.value_type)
            return create_class_from_dict(container_info.value_type, coerced)
        return elem

    # Primitive value — coerce type
    if isinstance(elem, str) and container_info.value_type is not str:
        return container_info.value_type(elem)
    return elem


def _coerce_dict_for_dataclass(raw_dict: dict, dc_type: type) -> dict:
    """Coerce string values in a dict to match dataclass field types.

    CLI bracket-access produces all-string leaf values. This function walks
    the dict and converts strings to the expected field types (int, float, bool)
    based on the dataclass type annotations.
    """
    if not hasattr(dc_type, "__dataclass_fields__"):
        return raw_dict

    coerced = {}
    dc_fields = {f.name: f for f in fields(dc_type)}

    for key, value in raw_dict.items():
        if key not in dc_fields:
            coerced[key] = value
            continue

        field_type = _strip_optional(dc_fields[key].type)

        if isinstance(value, str):
            # Coerce string to the expected type
            if field_type is int:
                coerced[key] = int(value)
            elif field_type is float:
                coerced[key] = float(value)
            elif field_type is bool:
                coerced[key] = value.lower() in ("true", "1", "yes")
            else:
                coerced[key] = value
        elif isinstance(value, dict) and hasattr(field_type, "__dataclass_fields__"):
            # Recursively coerce nested dataclass dicts
            coerced[key] = _coerce_dict_for_dataclass(value, field_type)
        elif isinstance(value, dict) and is_container(field_type):
            # Nested container inside a dataclass — resolve it now
            inner_info = _build_container_info(field_type)
            coerced[key] = resolve_container_value(value, inner_info)
        else:
            coerced[key] = value

    return coerced


def _process_polymorphic_field(field_type: type, raw_value: Any) -> Any:
    """Process a polymorphic config field.

    Args:
        field_type: The base polymorphic type
        raw_value: The raw value (dict with 'type' key, or type discriminator)

    Returns:
        Instantiated subclass based on the type discriminator
    """
    if isinstance(raw_value, dict):
        type_val = raw_value.get(DICT_KEY_TYPE)
        if type_val is not None:
            subclass = _match_subclass_by_type(field_type, type_val)
            sub_dict = {k: v for k, v in raw_value.items() if k != DICT_KEY_TYPE}
        else:
            subclass = field_type
            sub_dict = raw_value
        return create_class_from_dict(subclass, sub_dict)
    else:
        # raw_value directly specifies the type discriminator
        subclass = _match_subclass_by_type(field_type, raw_value)
        return subclass()


def _match_subclass_by_type(parent: type, type_val: Any) -> type:
    """Return the class (parent or subclass) whose ``get_type()`` equals *type_val*.

    Checks the parent class itself first, then searches subclasses. This
    handles the case where a concrete polymorphic class is used directly
    as a field type (e.g., after polymorphic dispatch already resolved the
    variant at a higher level).

    The comparison is performed with a bit of leniency: we allow Enum values,
    strings matching Enum names (case-insensitive) or the string or repr of the
    Enum value itself.

    Args:
        parent: The parent class to search (including itself and its subclasses)
        type_val: The type value to match

    Returns:
        The matching class

    Raises:
        ValueError: If no class matches the type value
    """
    # Include parent itself in the search if it has get_type()
    candidates = []
    if hasattr(parent, "get_type"):
        try:
            parent.get_type()
            candidates.append(parent)
        except NotImplementedError:
            pass
    candidates.extend(get_all_subclasses(parent))

    valid_types = []

    for subclass in candidates:
        subtype = subclass.get_type()
        type_name = subtype.name if hasattr(subtype, "name") else str(subtype)
        valid_types.append(type_name.lower())

        if subtype == type_val:
            return subclass
        # Handle Enum → str comparisons in either direction
        if hasattr(subtype, "name") and isinstance(type_val, str):
            # Match by enum name (e.g., "COST_AWARE_EXP_DECAY")
            if subtype.name.lower() == type_val.lower():
                return subclass
            # Match by enum value (e.g., "cost_aware_exp_decay")
            if hasattr(subtype, "value") and isinstance(subtype.value, str):
                if subtype.value.lower() == type_val.lower():
                    return subclass
        if isinstance(subtype, str) and isinstance(type_val, str):
            if subtype.lower() == type_val.lower():
                return subclass

    raise ValueError(
        MSG_INVALID_TYPE_VALUE.format(
            parent=parent.__name__,
            type_val=type_val,
            valid_types=", ".join(sorted(valid_types)) or "none",
        )
    )


def create_class_from_dict(cls: Type[Any], config_dict: Optional[ConfigDict]) -> Any:
    """Recursively instantiate a dataclass from a dictionary.

    This function converts a dictionary (typically loaded from YAML/JSON) into
    a dataclass instance, handling:
    - Primitive types (int, str, float, bool) - used directly
    - Nested dataclasses - instantiated recursively
    - Lists and dicts containing dataclasses - processed recursively
    - Polymorphic configs - subclass selected via "type" key
    - Optional fields - can be missing from the dictionary

    Args:
        cls: The dataclass type to instantiate
        config_dict: Dictionary containing configuration values. Can be None,
            in which case None is returned.

    Returns:
        An instance of cls populated with values from config_dict.
        Returns config_dict directly if cls is not a dataclass.

    Raises:
        TypeError: If config_dict contains keys that don't correspond to
            dataclass fields (except for BasePolyConfig subclasses)
        ValueError: If a polymorphic "type" value doesn't match any subclass

    Example:
        >>> @frozen_dataclass
        >>> class DatabaseConfig:
        >>>     host: str
        >>>     port: int = 5432
        >>>
        >>> config_dict = {"host": "localhost", "port": 3306}
        >>> db = create_class_from_dict(DatabaseConfig, config_dict)
        >>> # Returns: DatabaseConfig(host="localhost", port=3306)
        >>>
        >>> # Polymorphic example:
        >>> config_dict = {"type": "s3", "bucket": "my-bucket"}
        >>> storage = create_class_from_dict(BaseStorage, config_dict)
        >>> # Returns: S3Storage(bucket="my-bucket")
    """
    from vidhi.base_poly_config import BasePolyConfig

    # Fast path: if cls is not a dataclass return config_dict as is
    if (
        not is_dataclass(cls)
        or config_dict is None
        or not isinstance(config_dict, dict)
    ):
        logger.debug(
            "create_class_from_dict fast path for %s (value type: %s)",
            cls,
            type(config_dict).__name__,
        )
        # Caller will assign directly.
        return config_dict

    # Handle polymorphic dispatch at top level if cls is a BasePolyConfig
    # and config_dict has a "type" key
    if _issubclass_safe(cls, BasePolyConfig) and DICT_KEY_TYPE in config_dict:
        type_val = config_dict[DICT_KEY_TYPE]
        subclass = _match_subclass_by_type(cls, type_val)
        sub_dict = {k: v for k, v in config_dict.items() if k != DICT_KEY_TYPE}
        return create_class_from_dict(subclass, sub_dict)

    # Warn about any unexpected keys in the config dictionary
    # Skip this check for BasePolyConfig subclasses since they may have a 'type' key
    # and subclass-specific fields that don't exist on the base class
    if not _issubclass_safe(cls, BasePolyConfig):
        known_fields = {f.name for f in fields(cls)}
        extra_keys = set(config_dict) - known_fields
        if extra_keys:
            logger.error(
                "create_class_from_dict: unknown arguments for %s: %s",
                cls.__name__,
                sorted(extra_keys),
            )
            raise TypeError(
                MSG_UNKNOWN_ARGUMENTS.format(
                    class_name=cls.__name__, extra_keys=sorted(extra_keys)
                )
            )

    kwargs: dict[str, Any] = {}

    for field in fields(cls):
        if field.name not in config_dict:
            logger.debug(
                "Field '%s' not supplied for %s; using default.",
                field.name,
                cls.__name__,
            )
            continue

        raw_value = config_dict[field.name]
        field_type = _strip_optional(field.type)

        # Handle list containers with potential dataclass items
        if is_list(field_type) and isinstance(raw_value, list):
            kwargs[field.name] = _process_list_field(
                field_type, raw_value, BasePolyConfig
            )
            continue

        # Handle dict containers with potential dataclass values
        if is_dict(field_type) and isinstance(raw_value, dict):
            kwargs[field.name] = _process_dict_field(
                field_type, raw_value, BasePolyConfig
            )
            continue

        # Handle polymorphic config fields
        if _issubclass_safe(field_type, BasePolyConfig):
            kwargs[field.name] = _process_polymorphic_field(field_type, raw_value)
            continue

        # Handle nested dataclass (non-polymorphic)
        if is_dataclass(field_type):
            assert isinstance(
                field_type, type
            ), f"Expected type, got {type(field_type)}"
            kwargs[field.name] = (
                create_class_from_dict(field_type, raw_value)
                if isinstance(raw_value, dict)
                else raw_value
            )
            continue

        # Primitive or unknown – assign directly
        kwargs[field.name] = raw_value

    try:
        return cls(**kwargs)
    except TypeError as e:
        logger.error(
            "Failed to instantiate %s with %d kwargs (keys: %s). Error: %s",
            cls.__name__,
            len(kwargs),
            sorted(kwargs.keys()),
            e,
        )
        raise


_MAX_CONFIG_FILE_SIZE = 10 * 1024 * 1024  # 10 MB
_ALLOWED_CONFIG_EXTENSIONS = {".yaml", ".yml", ".json"}


def load_yaml_config(file_path: str):
    """Load a YAML configuration file and return its contents.

    The function performs a series of robustness checks and provides
    informative log messages for each failure mode.

    1. Verifies the file exists and is readable.
    2. Validates the file extension and size.
    3. Resolves symlinks to prevent path traversal attacks.
    4. Attempts to parse using ``yaml.safe_load``.
    5. On YAML parse errors, falls back to ``json.loads`` (helpful when the
       file is actually JSON or a subset thereof).
    6. Returns the parsed content, which can be either a dictionary (mapping)
       or a list. Lists are automatically wrapped in a dictionary with a
       special key to maintain compatibility with the configuration machinery.

    Parameters
    ----------
    file_path : str
        Path to the YAML/JSON configuration file.

    Returns
    -------
    dict
        Parsed configuration. If the top-level is a list, it's wrapped
        as {'_list': <the_list>}.

    Raises
    ------
    FileNotFoundError
        If *file_path* does not exist.
    PermissionError
        If the file cannot be read.
    ValueError
        If the file extension is not one of .yaml, .yml, .json.
    yaml.YAMLError | json.JSONDecodeError
        If the file cannot be parsed as YAML or JSON.
    ValueError
        If the top-level parsed object is neither a mapping nor a list.
    """
    # Resolve symlinks to prevent path traversal via symlink attacks
    file_path = os.path.realpath(file_path)

    # Validate file extension
    _, ext = os.path.splitext(file_path)
    if ext.lower() not in _ALLOWED_CONFIG_EXTENSIONS:
        raise ValueError(
            f"Unsupported config file extension '{ext}'. "
            f"Allowed: {', '.join(sorted(_ALLOWED_CONFIG_EXTENSIONS))}"
        )

    # check file
    if not os.path.exists(file_path):
        logger.error("Configuration file '%s' does not exist.", file_path)
        raise FileNotFoundError(f"Configuration file '{file_path}' not found.")

    # Check file size to prevent memory exhaustion
    file_size = os.path.getsize(file_path)
    if file_size > _MAX_CONFIG_FILE_SIZE:
        raise ValueError(
            f"Configuration file '{file_path}' is too large "
            f"({file_size:,} bytes, max {_MAX_CONFIG_FILE_SIZE:,} bytes)."
        )

    try:
        with open(file_path, "r", encoding="utf-8") as f:
            raw_content = f.read()
    except Exception as exc:
        logger.error("Failed to read configuration file '%s': %s", file_path, exc)
        raise

    # try yaml first
    try:
        data = yaml.safe_load(raw_content)
    except yaml.YAMLError as yaml_err:
        logger.warning(
            "YAML parsing error in '%s': %s – attempting JSON fallback.",
            file_path,
            yaml_err,
        )
        # json fallback
        try:
            data = json.loads(raw_content)
            logger.info("File '%s' parsed successfully as JSON.", file_path)
        except json.JSONDecodeError as json_err:
            logger.error(
                "Failed to parse '%s' as either YAML or JSON: %s", file_path, json_err
            )
            # original YAML error for clarity
            raise yaml_err from None

    # handle empty file (safe_load returns None)
    if data is None:
        logger.warning(
            "Configuration file '%s' is empty. Treating as an empty mapping.",
            file_path,
        )
        data = {}

    # handle list at top level by wrapping it
    if isinstance(data, list):
        logger.info(
            "Configuration file '%s' contains a list at the top level. "
            "Wrapping it with '_list' key.",
            file_path,
        )
        return {"_list": data}

    # ensure the loaded data is a mapping
    if not isinstance(data, dict):
        logger.error(
            "Configuration file '%s' must contain either a mapping or a list "
            "at the top level (got %s).",
            file_path,
            type(data).__name__,
        )
        raise ValueError(
            f"Configuration file {file_path} must contain either a mapping "
            f"or a list at the top level, got {type(data).__name__}."
        )
    return data
