"""Utility module for mapping and accessing nested data structures through path expressions.

Provides functions for parsing dot/bracket-separated paths and traversing
nested dictionaries, objects, and lists to retrieve values.
"""

from typing import Any, Dict, List, Optional, Union, Generator
from functools import reduce
import re

PATH_DELIMITERS = [",", ".", "[", "]"]
_SPLIT_PATTERN = re.compile("|".join(map(re.escape, PATH_DELIMITERS)))


def parse_path(path: Union[str, List[str]]) -> Generator[str, None, None]:
    """Parse a path string or list into individual keys.

    Supports dot notation (``a.b.c``), bracket notation (``a[0]``),
    and comma-separated paths.

    Args:
        path: The path to parse — either a string or list of strings.

    Yields:
        Individual keys from the path.
    """
    if isinstance(path, str):
        path = _split_and_trim(path)
    for key in path:
        if isinstance(key, str):
            yield from _split_and_trim(key)
        else:
            yield key


def _split_and_trim(string: str) -> List[str]:
    """Split a string by path delimiters and trim empty results.

    Args:
        string: The string to split.

    Returns:
        List of non-empty substrings.
    """
    return list(filter(None, _SPLIT_PATTERN.split(string)))


def getpath(
    obj: Union[Dict, List, Any],
    path: Union[str, List[str]],
    default: Optional[Any] = None,
) -> Any:
    """Retrieve a value from a nested structure using a path.

    Traverses both dict keys and object attributes, falling back
    to indexing for list/tuple access.

    Examples:
        >>> getpath({"a": {"b": 42}}, "a.b")
        42
        >>> getpath({"items": [1, 2, 3]}, "items[1]")
        2
        >>> getpath({}, "missing.key", "default")
        'default'

    Args:
        obj: The object to search (dict, list, or object with attributes).
        path: The dot/bracket path to follow.
        default: Value to return if the path does not exist.

    Returns:
        The value found at the path or the default value.
    """
    for key in parse_path(path):
        try:
            obj = getattr(obj, key) if hasattr(obj, key) else obj[key]
        except (KeyError, IndexError, TypeError, AttributeError):
            return default
        if obj is None:
            break
    return obj


def get_nested_attributes(root: Any, path: str, default: Optional[Any] = None) -> Any:
    """Get nested attributes from an object using a dot-separated path.

    Unlike :func:`getpath`, this only traverses object attributes (via ``getattr``),
    not dict keys.

    Args:
        root: The root object to search.
        path: The dot-separated attribute path.
        default: Value to return if any attribute in the path does not exist.

    Returns:
        The value found at the path or the default value.
    """
    try:
        return reduce(getattr, [root] + path.split("."))
    except AttributeError:
        return default
