from collections.abc import Callable, Hashable, Iterable
from operator import attrgetter, itemgetter
from typing import TypeVar, cast

T = TypeVar("T")
H = TypeVar("H", bound=Hashable)


def dep_sort(  # noqa: C901 (allow complex function)
    objects: Iterable[T],
    *,
    namegetter: str | Callable[[T], H],
    depgetter: str | Callable[[T], Iterable[H]],
    verbose: bool = False,
) -> list[T]:
    """
    Sort list of objects such that dependency constraints are satisfied.
    """
    objects = list(objects)
    if not objects:
        return []
    getter: type[itemgetter] | type[attrgetter] = (
        itemgetter if isinstance(objects[0], dict) else attrgetter
    )
    _namegetter = cast(
        Callable[[T], H], getter(namegetter) if isinstance(namegetter, str) else namegetter
    )
    _depgetter = cast(
        Callable[[T], Iterable[H]], getter(depgetter) if isinstance(depgetter, str) else depgetter
    )

    def safe_depgetter(ob: T) -> tuple[H, ...]:
        try:
            return tuple(_depgetter(ob))
        except Exception:
            return tuple()

    registry: dict[H, T] = {_namegetter(t): t for t in objects}
    ids = list(registry)
    dep_dict: dict[H, tuple[H, ...]] = {h: safe_depgetter(t) for h, t in registry.items()}

    def place_after(to_move: H, deps: tuple[H, ...], id_list: list) -> bool:
        if verbose:
            print(f"\nto_move: {to_move}; deps: {','.join(map(str, deps))}")
            print(f"Before:  {id_list}")
        if not deps:
            return False
        idx1 = id_list.index(to_move)
        idx2 = max(map(id_list.index, deps))
        if idx1 > idx2:
            return False
        id_list.pop(idx1)
        id_list.insert(idx2, to_move)
        if verbose:
            print(f"idx1: {idx1}, idx2: {idx2}")
            print(f"After:  {id_list}")
        return True

    print(ids)
    count = 0
    maxcount = sum(range(len(ids) + 1))
    while count < maxcount:
        change_tracker = []
        for _id, _deps in dep_dict.items():
            changed = place_after(_id, _deps, ids)
            change_tracker.append(changed)
        unchanged = not any(change_tracker)
        print(ids)
        if unchanged:
            return [registry[i] for i in ids]
        count += 1

    raise ValueError("Graph contains a cycle.")
