from __future__ import annotations

__package__ = "abxpkg"

import os
import sys

# Keep typing-only imports off the warm CLI path.
TYPE_CHECKING = False
if TYPE_CHECKING:
    from collections.abc import Iterator
    from typing import Any


_PACKAGE_DIR = os.path.dirname(__file__)
# Pydantic auto-loads third-party plugins from the active Python environment.
# In CLI/shebang mode that can pull unrelated packages like logfire into every
# model import, adding hundreds of milliseconds before abxpkg touches its own
# cache. abxpkg does not rely on those plugins, so keep model construction local.
os.environ.setdefault("PYDANTIC_DISABLE_PLUGINS", "1")
_PROVIDER_MODULE_NAMES_CACHE = None
_EXPORT_MODULES_BY_NAME_CACHE = None
_PROVIDER_CLASS_NAMES_BY_NAME_CACHE = None
_PROVIDER_CLASS_LITERAL_FIELDS_BY_NAME_CACHE = None
_PROVIDER_NAMES_BY_INSTALLER_BIN_CACHE = None
_ALL_PUBLIC_EXPORT_NAMES_CACHE = None

_CORE_EXPORT_MODULES = (
    "base_types",
    "semver",
    "logging",
    "exceptions",
    "binprovider",
    "binary",
)

# This is provider precedence, not an export registry. Exported symbols are
# discovered from the modules themselves so adding/removing public classes or
# helpers does not require keeping a second hand-written symbol table in sync.
_PROVIDER_NAME_PRIORITY = (
    "env",
    "node",
    "bash",
    "brew",
    "nix",
    "docker",
    "uv",
    "pip",
    "npm",
    "pnpm",
    "puppeteer",
    "playwright",
    "bun",
    "yarn",
    "deno",
    "chromewebstore",
    "apt",
    "gem",
    "goget",
    "cargo",
    "ansible",
    "pyinfra",
)

# Most built-in providers are enabled on every supported host. Keep only the
# exceptions here so warm CLI paths can select providers without opening and
# parsing every provider module before reading the executable cache.
_PROVIDER_DEFAULT_PLATFORMS = {
    "node": ("darwin", "linux"),
    "apt": ("linux",),
    "ansible": (),
    "pyinfra": (),
}

_provider_singletons: dict[str, object] = {}


def prepare_script_exec_plan(script_path, *, env=None) -> bool:
    from .click_cli import prepare_script_exec_plan as prepare

    return prepare(script_path, env=env)


def _provider_class(provider):
    return provider if isinstance(provider, type) else type(provider)


def _module_path(module_name: str) -> str:
    return os.path.join(_PACKAGE_DIR, f"{module_name}.py")


def _provider_module_names() -> tuple[str, ...]:
    global _PROVIDER_MODULE_NAMES_CACHE
    if _PROVIDER_MODULE_NAMES_CACHE is not None:
        return _PROVIDER_MODULE_NAMES_CACHE
    discovered = {
        filename[:-3]
        for filename in os.listdir(_PACKAGE_DIR)
        if filename.startswith("binprovider_") and filename.endswith(".py")
    }
    priority_modules = tuple(
        f"binprovider_{provider_name}"
        for provider_name in _PROVIDER_NAME_PRIORITY
        if f"binprovider_{provider_name}" in discovered
    )
    _PROVIDER_MODULE_NAMES_CACHE = (
        "binprovider",
        *priority_modules,
        *tuple(sorted(discovered - set(priority_modules))),
    )
    return _PROVIDER_MODULE_NAMES_CACHE


def _public_names_in_module(module_name: str) -> tuple[str, ...]:
    names: list[str] = []
    try:
        with open(_module_path(module_name), encoding="utf-8") as source_file:
            for raw_line in source_file:
                if not raw_line or raw_line[0].isspace():
                    continue
                line = raw_line.split("#", 1)[0].strip()
                if line.startswith(("class ", "def ")):
                    name = line.split(None, 1)[1].split("(", 1)[0].split(":", 1)[0]
                elif "=" in line:
                    name = line.split("=", 1)[0].strip()
                    if ":" in name:
                        name = name.split(":", 1)[0].strip()
                else:
                    continue
                if name.isidentifier() and not name.startswith("_"):
                    names.append(name)
    except OSError:
        return ()
    return tuple(dict.fromkeys(names))


def _export_modules_by_name() -> dict[str, str]:
    global _EXPORT_MODULES_BY_NAME_CACHE
    if _EXPORT_MODULES_BY_NAME_CACHE is not None:
        return _EXPORT_MODULES_BY_NAME_CACHE
    exports: dict[str, str] = {}
    for module_name in (*_CORE_EXPORT_MODULES, *_provider_module_names()):
        for export_name in _public_names_in_module(module_name):
            exports.setdefault(export_name, module_name)
    _EXPORT_MODULES_BY_NAME_CACHE = exports
    return _EXPORT_MODULES_BY_NAME_CACHE


def _import_module(module_name: str):
    import importlib

    return importlib.import_module(f".{module_name}", __name__)


def _load_export(name: str):
    module_name = _export_modules_by_name().get(name)
    if module_name is None:
        raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
    value = getattr(_import_module(module_name), name)
    globals()[name] = value
    return value


def _provider_name_from_class_name(class_name: str) -> str:
    return class_name.removesuffix("Provider").lower()


def _provider_class_names_by_name() -> dict[str, tuple[str, str]]:
    global _PROVIDER_CLASS_NAMES_BY_NAME_CACHE
    if _PROVIDER_CLASS_NAMES_BY_NAME_CACHE is not None:
        return _PROVIDER_CLASS_NAMES_BY_NAME_CACHE
    provider_classes: dict[str, tuple[str, str]] = {}
    for module_name in _provider_module_names():
        for class_name in _public_names_in_module(module_name):
            if not class_name.endswith("Provider") or class_name == "BinProvider":
                continue
            provider_classes[_provider_name_from_class_name(class_name)] = (
                module_name,
                class_name,
            )
    priority = {
        provider_name: provider_classes[provider_name]
        for provider_name in _PROVIDER_NAME_PRIORITY
        if provider_name in provider_classes
    }
    for provider_name in sorted(provider_classes):
        priority.setdefault(provider_name, provider_classes[provider_name])
    _PROVIDER_CLASS_NAMES_BY_NAME_CACHE = priority
    return _PROVIDER_CLASS_NAMES_BY_NAME_CACHE


def _provider_class_literal_fields_by_name() -> dict[str, dict[str, Any]]:
    """Read provider-owned literal class metadata without importing providers."""
    global _PROVIDER_CLASS_LITERAL_FIELDS_BY_NAME_CACHE
    if _PROVIDER_CLASS_LITERAL_FIELDS_BY_NAME_CACHE is not None:
        return _PROVIDER_CLASS_LITERAL_FIELDS_BY_NAME_CACHE

    import ast

    metadata: dict[str, dict[str, Any]] = {}
    source_modules: dict[str, list[str] | None] = {}
    field_names = {"INSTALLER_BIN"}
    for provider_name, (
        module_name,
        class_name,
    ) in _provider_class_names_by_name().items():
        provider_metadata: dict[str, Any] = {}
        if module_name not in source_modules:
            try:
                with open(_module_path(module_name), encoding="utf-8") as source_file:
                    source_modules[module_name] = source_file.readlines()
            except OSError:
                source_modules[module_name] = None
        source_lines = source_modules[module_name]
        if source_lines is None:
            metadata[provider_name] = provider_metadata
            continue

        in_provider_class = False
        line_index = 0
        while line_index < len(source_lines):
            raw_line = source_lines[line_index]
            if raw_line.startswith(f"class {class_name}("):
                in_provider_class = True
                line_index += 1
                continue
            if in_provider_class and raw_line.startswith("class "):
                break
            if (
                not in_provider_class
                or not raw_line.startswith("    ")
                or raw_line.startswith("        ")
            ):
                line_index += 1
                continue

            stripped = raw_line.strip()
            if stripped.startswith(("def ", "async def ")):
                break
            if stripped.startswith("class "):
                line_index += 1
                continue
            target_text, separator, value_text = stripped.partition("=")
            target_name = target_text.split(":", 1)[0].strip()
            if not separator or target_name not in field_names:
                line_index += 1
                continue

            literal_lines = [value_text.strip()]
            delimiter_balance = sum(
                literal_lines[0].count(opening) - literal_lines[0].count(closing)
                for opening, closing in (("(", ")"), ("[", "]"), ("{", "}"))
            )
            while delimiter_balance > 0 and line_index + 1 < len(source_lines):
                line_index += 1
                continuation = source_lines[line_index].strip()
                literal_lines.append(continuation)
                delimiter_balance += sum(
                    continuation.count(opening) - continuation.count(closing)
                    for opening, closing in (("(", ")"), ("[", "]"), ("{", "}"))
                )
            try:
                provider_metadata[target_name] = ast.literal_eval(
                    "\n".join(literal_lines),
                )
            except (SyntaxError, TypeError, ValueError):
                pass
            line_index += 1
        metadata[provider_name] = provider_metadata

    _PROVIDER_CLASS_LITERAL_FIELDS_BY_NAME_CACHE = metadata
    return _PROVIDER_CLASS_LITERAL_FIELDS_BY_NAME_CACHE


def _provider_names_by_installer_bin() -> dict[str, str]:
    global _PROVIDER_NAMES_BY_INSTALLER_BIN_CACHE
    if _PROVIDER_NAMES_BY_INSTALLER_BIN_CACHE is None:
        _PROVIDER_NAMES_BY_INSTALLER_BIN_CACHE = {
            str(fields["INSTALLER_BIN"]): provider_name
            for provider_name, fields in _provider_class_literal_fields_by_name().items()
            if fields.get("INSTALLER_BIN")
        }
    return _PROVIDER_NAMES_BY_INSTALLER_BIN_CACHE


class ProviderClassByName(dict):
    """Mapping that preserves the public dict API while importing lazily.

    Provider modules are relatively expensive because some wrap external
    package managers. Script execution frequently needs only a few named
    providers, so resolving all classes just to validate/index one provider
    burns the hot-path cache win before provider caches are even consulted.
    """

    def __init__(self) -> None:
        super().__init__()
        self._class_names = _provider_class_names_by_name()

    def __missing__(self, provider_name: str):
        module_name, class_name = self._class_names[provider_name]
        provider_class = getattr(_import_module(module_name), class_name)
        self[provider_name] = provider_class
        return provider_class

    def __contains__(self, provider_name: object) -> bool:
        return provider_name in self._class_names

    def __iter__(self) -> Iterator[str]:
        return iter(self._class_names)

    def __len__(self) -> int:
        return len(self._class_names)

    def keys(self):
        return self._class_names.keys()

    def _load_all(self) -> None:
        for provider_name in self._class_names:
            self[provider_name]

    def items(self):
        self._load_all()
        return super().items()

    def values(self):
        self._load_all()
        return super().values()

    def get(self, provider_name: object, default: Any = None) -> Any:
        if provider_name not in self._class_names:
            return default
        return self[provider_name]


class ProviderClassByInstallerBin(dict):
    """Lazy provider-class mapping keyed by the executable it bootstraps with."""

    def __init__(self) -> None:
        super().__init__()
        self._provider_names = _provider_names_by_installer_bin()

    def __missing__(self, installer_bin: str):
        provider_name = self._provider_names[installer_bin]
        provider_classes = globals().get("PROVIDER_CLASS_BY_NAME")
        if provider_classes is None:
            provider_classes = _provider_class_by_name()
            globals()["PROVIDER_CLASS_BY_NAME"] = provider_classes
        provider_class = provider_classes[provider_name]
        self[installer_bin] = provider_class
        return provider_class

    def __contains__(self, installer_bin: object) -> bool:
        return installer_bin in self._provider_names

    def __iter__(self) -> Iterator[str]:
        return iter(self._provider_names)

    def __len__(self) -> int:
        return len(self._provider_names)

    def keys(self):
        return self._provider_names.keys()

    def _load_all(self) -> None:
        for installer_bin in self._provider_names:
            self[installer_bin]

    def items(self):
        self._load_all()
        return super().items()

    def values(self):
        self._load_all()
        return super().values()

    def get(self, installer_bin: object, default: Any = None) -> Any:
        if installer_bin not in self._provider_names:
            return default
        return self[installer_bin]


def _all_providers():
    providers = []
    for module_name, class_name in _provider_class_names_by_name().values():
        providers.append(getattr(_import_module(module_name), class_name))
    return providers


def _all_provider_names() -> list[str]:
    return list(_PROVIDER_NAME_PRIORITY)


def _all_provider_class_names() -> list[str]:
    return [class_name for _, class_name in _provider_class_names_by_name().values()]


def _default_provider_names() -> list[str]:
    operating_system = sys.platform
    return [
        provider_name
        for provider_name in _PROVIDER_NAME_PRIORITY
        if operating_system
        in _PROVIDER_DEFAULT_PLATFORMS.get(provider_name, (operating_system,))
    ]


def _provider_class_by_name():
    return ProviderClassByName()


def _provider_class_by_installer_bin():
    return ProviderClassByInstallerBin()


_COMPUTED_EXPORTS = {
    "ALL_PROVIDERS": _all_providers,
    "ALL_PROVIDER_NAMES": _all_provider_names,
    "ALL_PROVIDER_CLASS_NAMES": _all_provider_class_names,
    "DEFAULT_PROVIDER_NAMES": _default_provider_names,
    "PROVIDER_CLASS_BY_NAME": _provider_class_by_name,
    "PROVIDER_CLASS_BY_INSTALLER_BIN": _provider_class_by_installer_bin,
}


def __getattr__(name: str) -> Any:
    if name == "__all__":
        value = list(_all_public_export_names())
        globals()[name] = value
        return value
    if name in _COMPUTED_EXPORTS:
        value = _COMPUTED_EXPORTS[name]()
        globals()[name] = value
        return value
    if name in _provider_class_names_by_name():
        if name not in _provider_singletons:
            _provider_singletons[name] = _provider_class_by_name()[name]()
        return _provider_singletons[name]
    return _load_export(name)


def _all_public_export_names() -> tuple[str, ...]:
    global _ALL_PUBLIC_EXPORT_NAMES_CACHE
    if _ALL_PUBLIC_EXPORT_NAMES_CACHE is not None:
        return _ALL_PUBLIC_EXPORT_NAMES_CACHE
    names = [
        *_COMPUTED_EXPORTS,
        *_export_modules_by_name(),
    ]
    _ALL_PUBLIC_EXPORT_NAMES_CACHE = tuple(dict.fromkeys(names))
    return _ALL_PUBLIC_EXPORT_NAMES_CACHE
