"""Custom YAML tags for Vidhi configuration files.

Provides two custom YAML tags:

- ``!expand`` — marks a list for Cartesian product sweep expansion.
- ``!include`` — inlines the contents of another YAML file.
"""

import os
from typing import Any, Set

import yaml


class ExpandList(list):
    """Marker subclass of list indicating Cartesian-product expansion.

    When a YAML list is tagged with ``!expand``, the loader returns an
    ``ExpandList`` instead of a plain ``list``.  Downstream explosion
    logic checks for the ``__vidhi_expand__`` attribute to decide
    whether to sweep over the values.
    """

    __vidhi_expand__ = True


_MAX_INCLUDE_DEPTH = 10


def _make_loader(
    *,
    include_stack: Set[str] | None = None,
    include_depth: int = 0,
) -> type:
    """Create a fresh loader class with ``!expand`` and ``!include`` support.

    A new class is created per ``load_yaml_config`` call so that per-file
    state (include stack, depth) does not leak across independent loads.
    """

    _include_stack: Set[str] = include_stack or set()
    _depth = include_depth

    class _VidhiLoader(yaml.SafeLoader):
        pass

    # ── !expand ──────────────────────────────────────────────────────
    def _construct_expand(loader: yaml.SafeLoader, node: yaml.Node) -> ExpandList:
        if not isinstance(node, yaml.SequenceNode):
            raise yaml.YAMLError(
                "!expand can only be applied to a YAML sequence (list), "
                f"got {type(node).__name__}."
            )
        return ExpandList(loader.construct_sequence(node, deep=True))

    _VidhiLoader.add_constructor("!expand", _construct_expand)

    # ── !include ─────────────────────────────────────────────────────
    def _construct_include(loader: yaml.SafeLoader, node: yaml.Node) -> Any:
        if not isinstance(node, yaml.ScalarNode):
            raise yaml.YAMLError(
                "!include expects a scalar (file path), " f"got {type(node).__name__}."
            )

        # Resolve relative to the directory of the *including* file.
        including_file = getattr(loader, "name", None) or ""
        base_dir = (
            os.path.dirname(os.path.abspath(including_file))
            if including_file
            else os.getcwd()
        )
        raw_path = loader.construct_scalar(node)
        resolved = os.path.realpath(os.path.join(base_dir, raw_path))

        # Cycle detection
        if resolved in _include_stack:
            raise yaml.YAMLError(
                f"Circular !include detected: {resolved} is already in "
                f"the include chain: {' -> '.join(sorted(_include_stack))}"
            )

        # Depth guard
        if _depth >= _MAX_INCLUDE_DEPTH:
            raise yaml.YAMLError(
                f"!include depth limit ({_MAX_INCLUDE_DEPTH}) exceeded "
                f"while including {resolved}."
            )

        new_stack = _include_stack | {resolved}

        # Load the included file with a fresh loader that inherits the
        # include stack and incremented depth.
        IncludedLoader = _make_loader(
            include_stack=new_stack,
            include_depth=_depth + 1,
        )

        with open(resolved, "r", encoding="utf-8") as f:
            data = yaml.load(f, Loader=IncludedLoader)  # noqa: S506

        return data if data is not None else {}

    _VidhiLoader.add_constructor("!include", _construct_include)

    return _VidhiLoader
