import inspect
from importlib import import_module
from inspect import isclass
from typing import Protocol, Any
from django.conf import settings


def get_members(path):
    try:
        module = import_module(path)
    except ModuleNotFoundError:
        print('Could not import module "{}"'.format(path))
        return []

    members = [
        (name, member)
        for name, member in inspect.getmembers(module)
        if not isclass(member) or (member.__module__ == module.__name__)
    ]

    return members


class DjangoAppVisitorHandler(Protocol):
    def on_module(
        self, app_name: str, path_parts: list[str], members: list[tuple[str, Any]]
    ) -> None: ...


class DjangoAppVisitor:
    """
    Discovers Python modules under each Django app following conventions:
      - <app>/<module>.py            -> url_prefix "<renamed>/"
      - <app>/<module>/**/*.py       -> url_prefix "<renamed>/<subdirs...>/<module>/"

    Example:
      <app>/<module>/forms/nksn.py   -> url_prefix "<renamed>/forms/nksn/"
                                 module_path "<app>.module.forms.nksn"
    """

    def __init__(
        self,
        *,
        layer: str,
        apps_root: str = "",
    ):
        self.apps_root = apps_root
        self.layer = layer

    def visit(self, handler: DjangoAppVisitorHandler) -> None:
        apps_dir = (
            settings.BASE_DIR / self.apps_root if self.apps_root else settings.BASE_DIR
        )
        if not apps_dir.is_dir():
            apps_dir = settings.BASE_DIR

        module_prefix = f"{self.apps_root}." if self.apps_root else ""

        for app_name in settings.INSTALLED_APPS:
            if app_name.startswith(self.apps_root + "."):
                app_name = app_name[(len(self.apps_root) + 1) :]

            app_dir = apps_dir / app_name
            if not app_dir.exists():
                continue

            app_module = f"{module_prefix}{app_name}"

            # 1) Visit package: <app>/<module>/**/*.py
            layer_dir = app_dir / self.layer
            if layer_dir.is_dir():
                for py_file in layer_dir.rglob("*.py"):
                    if py_file.name == "__init__.py":
                        continue

                    relative_path = py_file.relative_to(layer_dir).with_suffix("")
                    parts = list(relative_path.parts)

                    dotted = ".".join(parts)
                    handler.on_module(
                        app_name,
                        parts,
                        get_members(f"{app_module}.{self.layer}.{dotted}"),
                    )

            # 2) Visit module module file: <app>/module.py
            layer_file = app_dir / f"{self.layer}.py"
            if layer_file.is_file():
                handler.on_module(
                    app_name, [], get_members(f"{app_module}.{self.layer}")
                )
