import contextlib
import logging
import os
import site
import warnings
from collections.abc import Sequence
from importlib import metadata
from pathlib import Path
from shutil import rmtree

from platformdirs import user_cache_dir

from npe2._inspection._from_npe1 import manifest_from_npe1
from npe2.manifest import PackageMetadata

from .schema import PluginManifest, discovery_blocked

logger = logging.getLogger(__name__)
ADAPTER_CACHE = Path(user_cache_dir("napari", "napari")) / "npe2" / "adapter_manifests"
NPE2_NOCACHE = "NPE2_NOCACHE"


def clear_cache(names: Sequence[str] = ()) -> list[Path]:
    """Clear cached NPE1Adapter manifests.

    Parameters
    ----------
    names : Sequence[str], optional
        selection of plugin names to clear, by default, all will be cleared

    Returns
    -------
    List[Path]
        List of filepaths cleared
    """
    _cleared: list[Path] = []
    if ADAPTER_CACHE.exists():
        if names:
            for f in ADAPTER_CACHE.glob("*.yaml"):
                if any(f.name.startswith(f"{n}_") for n in names):
                    f.unlink()
                    _cleared.append(f)
        else:
            _cleared = list(ADAPTER_CACHE.iterdir())
            rmtree(ADAPTER_CACHE)
    return _cleared


class NPE1Adapter(PluginManifest):
    """PluginManifest subclass that acts as an adapter for 1st gen plugins.

    During plugin discovery, packages that provide a first generation
    'napari.plugin' entry_point (but do *not* provide a second generation
    'napari.manifest' entrypoint) will be stored as `NPE1Adapter` manifests
    in the `PluginManager._npe1_adapters` list.

    This class is instantiated with only a distribution object, but lacks
    contributions at construction time.  When `self.contributions` is accesses for the
    first time, `_load_contributions` is called triggering and import and indexing of
    all plugin modules using the same logic as `npe2 convert`.  After import, the
    discovered contributions are cached in a manifest for use in future sessions.
    (The cache can be cleared using `npe2 cache --clear [plugin-name]`).



    Parameters
    ----------
    dist : metadata.Distribution
        A Distribution object for a package installed in the environment. (Minimally,
        the distribution object must implement the `metadata` and `entry_points`
        attributes.).  It will be passed to `manifest_from_npe1`
    """

    _is_loaded: bool = False
    _dist: metadata.Distribution

    def __init__(self, dist: metadata.Distribution):
        """_summary_"""
        meta = PackageMetadata.from_dist_metadata(dist.metadata)
        super().__init__(
            name=dist.metadata["Name"], package_metadata=meta, npe1_shim=True
        )
        self._dist = dist

    def __getattribute__(self, __name: str):
        if __name == "contributions":
            self._load_contributions()
        return super().__getattribute__(__name)

    def _load_contributions(self, save=True) -> None:
        """import and inspect package contributions."""
        if self._is_loaded:
            return
        self._is_loaded = True  # if we fail once, we still don't try again.
        if self._cache_path().exists() and not os.getenv(NPE2_NOCACHE):
            mf = PluginManifest.from_file(self._cache_path())
            self.contributions = mf.contributions
            logger.debug("%r npe1 adapter loaded from cache", self.name)
            return

        with discovery_blocked():
            try:
                mf = manifest_from_npe1(self._dist, adapter=True)
            except Exception as e:
                warnings.warn(
                    "Error importing contributions for first-generation "
                    f"napari plugin {self.name!r}: {e}",
                    stacklevel=2,
                )
                return

            self.contributions = mf.contributions
            logger.debug("%r npe1 adapter imported", self.name)

        if save and not _is_editable_install(self._dist):
            with contextlib.suppress(OSError):
                self._save_to_cache()

    def _save_to_cache(self):
        cache_path = self._cache_path()
        cache_path.parent.mkdir(exist_ok=True, parents=True)
        cache_path.write_text(self.yaml())

    def _cache_path(self) -> Path:
        """Return cache path for manifest corresponding to distribution."""
        return _cached_adapter_path(self.name, self.package_version or "")

    def _serialized_data(self, **kwargs):
        if not self._is_loaded:  # pragma: no cover
            self._load_contributions(save=False)
        return super()._serialized_data(**kwargs)


def _cached_adapter_path(name: str, version: str) -> Path:
    """Return cache path for manifest corresponding to distribution."""
    return ADAPTER_CACHE / f"{name}_{version}.yaml"


def _is_editable_install(dist: metadata.Distribution) -> bool:
    """Return True if dist is installed as editable.

    i.e: if the package isn't in site-packages or user site-packages.
    """
    root = str(dist.locate_file(""))
    installed_paths = [*site.getsitepackages(), site.getusersitepackages()]
    return all(loc not in root for loc in installed_paths)
