# ---------------------------------------------------------------------------
# lazy import  (lazy_import)
# ---------------------------------------------------------------------------
nullProxy_logger = logging.getLogger("abstract.lazy_import")


class nullProxy:
    """Safe, chainable, callable placeholder returned for missing modules/attrs."""

    def __init__(self, name, path=(), fallback=None):
        self._name = name
        self._path = path
        self.fallback = fallback

    def __getattr__(self, attr):
        return nullProxy(self._name, self._path + (attr,))

    def __call__(self, *args, **kwargs):
        if self.fallback is not None:
            try:
                return self.fallback(*args, **kwargs)
            except Exception as e:
                nullProxy_logger.info("%s", e)
        nullProxy_logger.warning(
            "[lazy_import] Call to missing module/attr: %s.%s args=%s kwargs=%s",
            self._name, ".".join(self._path), args, kwargs,
        )
        return None

    def __repr__(self):
        full = ".".join((self._name, *self._path))
        return f"<nullProxy {full}>"

    def __bool__(self):
        return False


@lru_cache(maxsize=None)
def lazy_import_single(name, fallback=None):
    """Import ``name`` safely, returning a :class:`nullProxy` if unavailable."""
    if name in sys.modules:
        return sys.modules[name]
    try:
        return importlib.import_module(name)
    except Exception as e:
        nullProxy_logger.warning("[lazy_import] Failed to import '%s': %s", name, e)
        return nullProxy(name, fallback=fallback)


def get_lazy_attr(module_name, *attrs, fallback=None):
    obj = lazy_import(module_name, fallback=fallback)
    for attr in attrs:
        try:
            obj = getattr(obj, attr)
        except Exception:
            return nullProxy(module_name, attrs, fallback=fallback)
    return obj


def lazy_import(name, *attrs, fallback=None):
    """
    Import a module (and optionally walk into ``attrs``) safely.

    Returns the module/attribute, or a :class:`nullProxy` placeholder if the
    import fails — so the result is always safe to reference.
    """
    if attrs:
        return get_lazy_attr(name, *attrs, fallback=fallback)
    return lazy_import_single(name, fallback=fallback)

# ---------------------------------------------------------------------------
# lazy_module  (TRUE deferred import — imports on first attribute access/call,
# unlike lazy_import which imports immediately-but-safely)
# ---------------------------------------------------------------------------
class _LazyModuleProxy:
    """Defer ``import name`` until the first attribute access or call.

    Degrades to :class:`nullProxy` if the import ultimately fails, so a missing
    optional dependency neither loads at import time nor crashes at import time.
    """
    __slots__ = ("_lm_name", "_lm_fallback", "_lm_mod")

    def __init__(self, name, fallback=None):
        object.__setattr__(self, "_lm_name", name)
        object.__setattr__(self, "_lm_fallback", fallback)
        object.__setattr__(self, "_lm_mod", None)

    def _lm_resolve(self):
        mod = object.__getattribute__(self, "_lm_mod")
        if mod is None:
            name = object.__getattribute__(self, "_lm_name")
            if name in sys.modules:
                mod = sys.modules[name]
            else:
                try:
                    mod = importlib.import_module(name)
                except Exception as e:
                    nullProxy_logger.warning("[lazy_module] failed to import '%s': %s", name, e)
                    mod = nullProxy(name, fallback=object.__getattribute__(self, "_lm_fallback"))
            object.__setattr__(self, "_lm_mod", mod)
        return mod

    def __getattr__(self, attr):
        return getattr(self._lm_resolve(), attr)

    def __call__(self, *args, **kwargs):
        return self._lm_resolve()(*args, **kwargs)

    def __repr__(self):
        return f"<lazy_module {object.__getattribute__(self, '_lm_name')!r}>"


def lazy_module(name, fallback=None):
    """Return a proxy that imports ``name`` on FIRST use (true deferral).

    Unlike :func:`lazy_import` (imports immediately, nullProxy only on failure),
    this stays unloaded until the first attribute access or call::

        pd = lazy_module("pandas")     # pandas NOT imported here
        df = pd.DataFrame(rows)        # imported now, on first use
    """
    return _LazyModuleProxy(name, fallback=fallback)
