"""Hot reload utilities for Goose modules."""

from __future__ import annotations

import importlib
import sys

from goose.core.config import GooseConfig


def collect_submodules(package_name: str) -> list[str]:
    """Find all loaded modules under a package prefix."""
    prefix = f"{package_name}."
    matches = []
    for name in sys.modules:
        if name == package_name or name.startswith(prefix):
            matches.append(name)
    return matches


def reload_module(module_name: str) -> None:
    """Reload a single module by name.

    If the module file was deleted, removes it from sys.modules.
    Raises import-time errors (syntax errors, missing deps) so they propagate.
    Only AttributeError/TypeError during reload are suppressed.
    """
    module = sys.modules.get(module_name)
    if module is not None:
        try:
            importlib.reload(module)
        except ModuleNotFoundError as exc:
            if exc.name == module_name:
                sys.modules.pop(module_name, None)
            else:
                raise
        except (AttributeError, TypeError):
            pass


def _build_dependency_graph(modules: set[str]) -> dict[str, set[str]]:
    """Build a mapping of module -> modules it imports from (within the set)."""
    deps: dict[str, set[str]] = {}
    for module_name in modules:
        module = sys.modules.get(module_name)
        if module is None:
            deps[module_name] = set()
            continue
        imported_from = set()
        for name in dir(module):
            attr = getattr(module, name, None)
            attr_module = getattr(attr, "__module__", None)
            if attr_module and attr_module != module_name and attr_module in modules:
                imported_from.add(attr_module)
        deps[module_name] = imported_from
    return deps


def _topological_sort(modules: set[str], deps: dict[str, set[str]]) -> list[str]:
    """Sort modules so dependencies come before dependents."""
    reloaded: set[str] = set()
    reload_order: list[str] = []

    while len(reloaded) < len(modules):
        progress = False
        for module_name in modules:
            if module_name in reloaded:
                continue
            if deps[module_name] <= reloaded:
                reload_order.append(module_name)
                reloaded.add(module_name)
                progress = True
        if not progress:
            # Circular dependency - add remaining in any order
            reload_order.extend(m for m in modules if m not in reloaded)
            break

    return reload_order


def reload_source_modules(*, extra_exclude_suffixes: list[str] | None = None) -> None:
    """Reload all configured source modules and refresh the GooseApp.

    Collects modules from reload_targets, excludes those in reload_exclude,
    reloads them in dependency order, and refreshes the GooseApp instance.

    Args:
        extra_exclude_suffixes: Additional module suffixes to exclude (e.g., [".conftest"]).
    """
    config = GooseConfig()

    reload_exclude = config.compute_reload_exclude()
    extra_suffixes = extra_exclude_suffixes or []

    modules = {
        mod
        for target in config.reload_targets
        for mod in collect_submodules(target)
        if not any(mod == exc or mod.startswith(f"{exc}.") for exc in reload_exclude)
        and not any(mod.endswith(suffix) for suffix in extra_suffixes)
    }

    if modules:
        deps = _build_dependency_graph(modules)
        for module_name in _topological_sort(modules, deps):
            reload_module(module_name)

        config.refresh_app()


__all__ = ["collect_submodules", "reload_module", "reload_source_modules"]
