from __future__ import annotations

__all__ = (
    "fix_package_names",
    "install_lazy_importer",
    "set_deprecated_aliases",
)

import ast
import inspect
import sys
import warnings
from importlib import import_module
from types import ModuleType
from typing import Any


def install_lazy_importer() -> bool:
    module_globals = sys._getframe(1).f_globals
    module_name = module_globals["__name__"]
    module_prefix = module_name + "."
    module = sys.modules[module_name]
    lazy_map, deprecated_aliases = _build_lazy_map(module)
    names = sorted(lazy_map)

    # Delete symbols that are not part of the API
    del module_globals["TYPE_CHECKING"]
    del module_globals["install_lazy_importer"]

    if not lazy_map and not deprecated_aliases:
        return False

    def __getattr__(name: str) -> Any:
        if new_name := deprecated_aliases.get(name):
            emit_deprecation_warning(module_name, name, new_name)
            target_mod, target_attr = new_name.rsplit(".", 1)
        else:
            try:
                target_mod, target_attr = lazy_map[name]
            except KeyError:
                raise AttributeError(
                    f"module {module_name!r} has no attribute {name!r}"
                ) from None

        imported = import_module(target_mod, module_name)
        value = getattr(imported, target_attr)

        # patch the module name to match
        if (
            getattr(value, "__module__", "").startswith(module_prefix)
            and name not in deprecated_aliases
        ):
            value.__module__ = module_name

        module_globals[name] = value
        return value

    def __dir__() -> list[str]:
        return names

    module_globals["__dir__"] = __dir__
    module_globals["__getattr__"] = __getattr__
    module_globals.pop("fix_package_names", None)
    module_globals.pop("set_deprecated_aliases", None)
    return True


def fix_package_names() -> None:
    module_globals = sys._getframe(1).f_globals
    module_prefix = module_globals["__name__"] + "."
    del module_globals[fix_package_names.__name__]
    for value in module_globals.values():
        if modname := getattr(value, "__module__", ""):
            if modname.startswith(module_prefix):
                parts = modname.split(".")
                value.__module__ = ".".join(
                    part for part in parts if not part.startswith("_")
                )


def emit_deprecation_warning(module_name: str, name: str, target: str) -> None:
    warnings.warn(
        f"The {module_name}.{name} alias is deprecated, use {target} instead.",
        DeprecationWarning,
        stacklevel=3,
    )


def set_deprecated_aliases(aliases: dict[str, str]) -> None:
    module_globals = sys._getframe(1).f_globals
    module_name = module_globals["__name__"]
    del module_globals[set_deprecated_aliases.__name__]

    def __getattr__(name: str) -> Any:
        try:
            target = aliases[name]
        except KeyError:
            raise AttributeError(
                f"module {module_name!r} has no attribute {name!r}"
            ) from None

        emit_deprecation_warning(module_name, name, target)
        target_modname, attrname = target.rsplit(".", 1)
        module = import_module(target_modname)
        return getattr(module, attrname)

    sys.modules[module_name].__dict__["__getattr__"] = __getattr__


def _build_lazy_map(
    module: ModuleType,
) -> tuple[dict[str, tuple[str, str]], dict[str, str]]:
    try:
        source = inspect.getsource(module)
    except OSError:
        return {}, {}

    tree = compile(source, module.__file__ or "", "exec", ast.PyCF_ONLY_AST)
    assert isinstance(tree, ast.Module)
    out: dict[str, tuple[str, str]] = {}
    deprecated_aliases: dict[str, str] = {}

    for node in tree.body:
        if not isinstance(node, ast.If) or not _is_type_checking_block(node.test):
            continue

        for stmt in node.body:
            match stmt:
                case ast.ImportFrom():
                    base = "." * stmt.level + (stmt.module or "")
                    for alias in stmt.names:
                        if alias.name == "*":
                            raise RuntimeError("star imports not supported")

                        exported = alias.asname or alias.name
                        out[exported] = (base, alias.name)
                case ast.Expr() if isinstance(stmt.value, ast.Call):
                    call = stmt.value
                    if (
                        isinstance(call.func, ast.Name)
                        and call.func.id == "set_deprecated_aliases"
                    ):
                        arg0 = call.args[0]
                        assert isinstance(arg0, ast.Dict)
                        for key, value in zip(arg0.keys, arg0.values, strict=True):
                            assert isinstance(key, ast.Constant)
                            assert isinstance(key.value, str)
                            assert isinstance(value, ast.Constant)
                            assert isinstance(value.value, str)
                            deprecated_aliases[key.value] = value.value

    return out, deprecated_aliases


def _is_type_checking_block(test: ast.AST) -> bool:
    if not isinstance(test, ast.BoolOp):
        return False

    subtest = test.values[0]
    match subtest:
        case ast.Name():
            return subtest.id == "TYPE_CHECKING"
        case ast.Attribute():
            return (
                isinstance(subtest.value, ast.Name)
                and subtest.value.id == "typing"
                and subtest.attr == "TYPE_CHECKING"
            )
        case _:
            return False
