import inspect
import sys

from typing import Dict, Union, Optional

_yoshio_COUNT = 0
_yoshio_COUNTERS = {}

def countdown(k: int) -> None:
    """ Terminate condition, calling this function `k` times, cannot be unset
    """
    global _yoshio_COUNT
    # Set if not set, otherwise just countdown
    if _yoshio_COUNT == 0:
        _yoshio_COUNT = k
    _yoshio_COUNT -= 1
    if _yoshio_COUNT < 0:
        exit()

def reset_countdown(k: int) -> None:
    """ Reset current counter
    """
    global _yoshio_COUNT
    _yoshio_COUNT = 0

def countdowns(k: int) -> None:
    """ Multiple condition terminate condition
    """
    callsite_serialized = str(_find_call_site())

    # If never seen location called from, just set and return
    if callsite_serialized not in _yoshio_COUNTERS:
        _yoshio_COUNTERS[callsite_serialized] = k
        return

    _yoshio_COUNTERS[callsite_serialized] -= 1

    if _yoshio_COUNTERS[callsite_serialized] == 0:
        exit()


def reset_countdowns() -> None:
    """ Reset counters
    """
    global _yoshio_COUNTERS
    _yoshio_COUNTERS = {}

def _find_call_site() -> Dict[str, Union[str, int]]:
    # Get the current stack
    stack = inspect.stack()

    # The first element is the current function, the second element is the callsite
    caller_caller = stack[2]
    caller = stack[1]  # This is actually because we created temporary function
    frame = caller_caller[0]
    info = {
        "filename": frame.f_code.co_filename,
        "line_no": frame.f_lineno,
        "function": frame.f_code.co_name
    }

    return info


def print_parent_call_tree(
    max_unique: int = 6,
    stream = sys.stderr,
    skip_names=("ancestors", "print_parent_call_tree"),
) -> None:
    """
    Prints a fancy outermost→innermost call tree, marking recursive nodes (*),
    but does NOT display frames whose function names appear in `skip_names`.
    """
    stack = inspect.stack()

    parents: List[Dict[str, Union[str, int]]] = []
    seen_funcs = set()
    recursive_funcs = set()
    loop_detected = False

    for frame_info in stack:
        func_name = frame_info.function

        if func_name in seen_funcs:
            # loop / recursion
            loop_detected = True
            recursive_funcs.add(func_name)
            continue

        seen_funcs.add(func_name)

        parents.append({
            "filename": frame_info.filename,
            "line_no": frame_info.lineno,
            "function": func_name,
        })

        if len(seen_funcs) >= max_unique:
            break

    if not parents:
        print("[call tree] <no frames>", file=stream)
        return

    # Reverse to outermost → innermost
    parents = parents[::-1]

    # ---- FILTER FOR PRINTING ----
    visible = [p for p in parents if p["function"] not in skip_names]
    if not visible:
        print("[call tree] <no non-helper frames>", file=stream)
        return

    print("Call tree (outermost → innermost):", file=stream)

    # root
    root = visible[0]
    root_name = root["function"]
    root_suffix = " *" if root_name in recursive_funcs else ""
    print(
        f'  {root_name}{root_suffix}  ({root["filename"]}:{root["line_no"]})',
        file=stream,
    )

    # children
    for depth, frame in enumerate(visible[1:], start=1):
        indent = "  " + "   " * (depth - 1)
        connector = "└──"
        name = frame["function"]
        suffix = " *" if name in recursive_funcs else ""
        print(
            f'{indent}{connector} {name}{suffix}  '
            f'({frame["filename"]}:{frame["line_no"]})',
            file=stream,
        )
    print(
        f'  {indent}{connector}  ancestors()',
        file=stream,
    )

    if loop_detected and recursive_funcs:
        funcs_list = ", ".join(sorted(recursive_funcs))
        print(
            f"\n⚠ detected recursive / looping calls involving: {funcs_list} "
            f"(repeated frames omitted; marked with *)",
            file=stream,
        )

def where():
    print(_find_call_site(), file=sys.stderr)

def ancestors():
    print_parent_call_tree()

def dirx(obj, needle: Optional[str] = None):
    print([x for x in dir(obj) if needle is None or needle in x], file=sys.stderr)

# Add dirx
# pandas equivalent of dirx, may be same function
