import importlib
from collections import defaultdict, deque
from loguru import logger

__g_mappings__ = defaultdict(dict)

def __find_path__(from_type, to_type):
    """Find a path from one type to another using BFS."""
    queue = deque([(from_type, [from_type])])
    visited = set()

    while queue:
        current, path = queue.popleft()

        if current in visited:
            continue

        visited.add(current)

        if current == to_type:
            return path

        for t in __g_mappings__[current]:
            if t not in path:
                queue.append((t, path + [t]))

        # We can try casting from parents of current to type
        for t in current.__bases__:
            if t not in path:
                queue.append((t, path + [t]))

    return None


def add_acast(from_type, to_type, function):
    """Register a function that casts from one type to another."""
    if to_type in __g_mappings__[from_type]:
        # raise ValueError(f"Mapping {from_type} - {to_type} already exists")
        logger.debug(f"Overwriting existing cast: {from_type} -> {to_type}")
    __g_mappings__[from_type][to_type] = function


def acaster(from_type, to_type):
    """Decorator for a function that casts from one type to another.

    Args:
        from_type (_type_): _description_
        to_type (_type_): _description_
    """

    def decorator(func):
        # Run function with default input
        try:
            res = func()
            assert type(res) is to_type, f"Function {func} does not return type {to_type}"
            add_acast(from_type, to_type, func)
            logger.debug(f"Registered acast from {from_type} to {to_type} with {func}")
        except Exception as e:
            logger.error(f"Failed to register acast from {from_type} to {to_type} with {func}: {e}")
        return func
    
    return decorator


def acast(instance, *to_type_list, debug=False):
    from_type = instance.__class__

    if debug:
        logger.debug(f"acast from {from_type} to {to_type_list}")

    for to_type in to_type_list:

        if from_type != to_type:

            path = __find_path__(from_type, to_type)
            if debug:
                logger.debug(f"acast path from {from_type} to {to_type}: {path}")

            if path is not None:

                for i in range(len(path) - 1):
                    s = path[i]
                    t = path[i + 1]

                    if debug:
                        logger.debug(f"acast step {from_type} -> {to_type}: {s} -> {t}")

                    # If mapping exists, use it
                    if s in __g_mappings__ and t in __g_mappings__[s]:
                        instance = __g_mappings__[s][t](instance)
                        # continue

                    # If it's a parent, do nothing
                    # if isinstance(instance, t):
                    #     continue

                from_type = t
                continue

            else:
                # If there is no path, do this for dicts and lists
                # This can be a bit confusing, if we map dict to str, what should we do?
                # a) dict -> obj -> str
                # b) below
                if from_type is dict:
                    return {k: acast(v, *to_type_list, debug=debug) for k, v in instance.items()}

                if from_type is list:
                    return [acast(v, *to_type_list, debug=debug) for v in instance]

            if from_type not in __g_mappings__:
                raise ValueError(f"Mapping from {from_type} not found")

            if to_type not in __g_mappings__[from_type]:
                raise ValueError(f"Mapping to {to_type} not found in {from_type}")

    return instance

def add_acast_if_supported(lib1, lib2, t1, t2, f):
    try:
        lib1 = importlib.import_module(lib1)
        lib2 = importlib.import_module(lib2)
        add_acast(getattr(lib1, t1), getattr(lib2, t2), f)
    except ImportError:
        pass