import importlib
import importlib.abc
import importlib.metadata
import importlib.util
import shutil
import site
import sys
from collections.abc import Callable
from pathlib import Path
from textwrap import dedent

MOCK_INSTALL_NAME_MEMORY = "micropip in-memory mock package"
MOCK_INSTALL_NAME_PERSISTENT = "micropip mock package"


class MockDistribution(importlib.metadata.Distribution):
    def __init__(self, file_dict, modules):
        self.file_dict = file_dict
        self.modules = modules

    def read_text(self, filename):
        """Attempt to load metadata file given by the name.
        :param filename: The name of the file in the distribution info.
        :return: The text if found, otherwise None.
        """
        if filename in self.file_dict:
            return self.file_dict[filename]
        else:
            return None

    def locate_file(self, path):
        """
        Given a path to a file in this distribution, return a path
        to it.
        """
        return None


_mock_modules: dict[str, str | Callable] = {}
_mock_distributions: dict[str, MockDistribution] = {}


class _MockModuleFinder(importlib.abc.MetaPathFinder, importlib.abc.Loader):
    def __init__(self):
        pass

    def find_distributions(self, context):
        if context.name in _mock_distributions:
            return [_mock_distributions[context.name]]
        elif context.name is None:
            return _mock_distributions.values()
        else:
            return []

    def find_module(self, fullname, path=None):
        spec = self.find_spec(fullname, path)
        if spec is None:
            return None
        return spec

    def create_module(self, spec):
        if spec.name in _mock_modules:
            from types import ModuleType

            module = ModuleType(spec.name)
            module.__path__ = "/micropip_mocks/" + module.__name__.replace(".", "/")
            return module

    def exec_module(self, module):
        init_object = _mock_modules[module.__name__]
        if isinstance(init_object, str):
            # run module init code in the module
            exec(dedent(init_object), module.__dict__)
        elif callable(init_object):
            # run module init function
            init_object(module)

    def find_spec(self, fullname, path=None, target=None):
        if fullname not in _mock_modules.keys():
            return None
        spec = importlib.util.spec_from_loader(fullname, self)
        return spec


_finder = _MockModuleFinder()


def _add_in_memory_distribution(name, metafiles, modules):
    if _finder not in sys.meta_path:
        sys.meta_path = [_finder] + sys.meta_path
    _mock_distributions[name] = MockDistribution(metafiles, modules)
    for name, obj in modules.items():
        _add_mock_module(name, obj)


def _add_mock_module(name, obj):
    _mock_modules[name] = obj


def _remove_in_memory_distribution(name):
    if name in _mock_distributions:
        for module in _mock_distributions[name].modules.keys():
            if module in sys.modules:
                del sys.modules[module]
            del _mock_modules[module]
        del _mock_distributions[name]


def add_mock_package(
    name: str,
    version: str,
    *,
    modules: dict[str, str | None] | None = None,
    persistent: bool = False,
) -> None:
    if modules is None:
        # make a single mock module with this name
        modules = {name: ""}

    # make the metadata
    METADATA = f"""Metadata-Version: 1.1
Name: {name}
Version: {version}
Summary: {name} mock package generated by micropip
Author-email: {name}@micro.pip.non-working-fake-host
"""

    for module_name in modules.keys():
        METADATA += f"Provides: {module_name}\n"

    if persistent:
        # make empty mock modules with the requested names in user site packages
        site_packages = Path(site.getsitepackages()[0])

        # should exist already, but just in case
        site_packages.mkdir(parents=True, exist_ok=True)

        dist_dir = site_packages / f"{name}-{version}.dist-info"
        dist_dir.mkdir(parents=True, exist_ok=False)

        metadata_file = dist_dir / "METADATA"
        record_file = dist_dir / "RECORD"
        installer_file = dist_dir / "INSTALLER"

        file_list = [metadata_file, installer_file]

        metadata_file.write_text(METADATA)
        installer_file.write_text(MOCK_INSTALL_NAME_PERSISTENT)

        for module_name, content in modules.items():
            if not content:
                content = ""

            content = dedent(content)
            path_parts = module_name.split(".")

            dir_path = Path(site_packages, *path_parts)
            dir_path.mkdir(exist_ok=True, parents=True)
            init_file = dir_path / "__init__.py"
            file_list.append(init_file)

            init_file.write_text(content)

        with open(record_file, "w") as f:
            for file in file_list:
                f.write(f"{file},,{file.stat().st_size}\n")
            f.write(f"{record_file},,\n")
    else:
        # make memory mocks of files
        INSTALLER = MOCK_INSTALL_NAME_MEMORY
        metafiles = {"METADATA": METADATA, "INSTALLER": INSTALLER}
        _add_in_memory_distribution(name, metafiles, modules)

    importlib.invalidate_caches()


def list_mock_packages() -> list[str]:
    mock_packages = [
        dist.name
        for dist in importlib.metadata.distributions()
        if dist.read_text("INSTALLER")
        in (MOCK_INSTALL_NAME_PERSISTENT, MOCK_INSTALL_NAME_MEMORY)
    ]
    return mock_packages


def remove_mock_package(name: str) -> None:
    d = importlib.metadata.distribution(name)
    installer = d.read_text("INSTALLER")

    if installer == MOCK_INSTALL_NAME_MEMORY:
        _remove_in_memory_distribution(name)
        return

    elif installer is None or installer != MOCK_INSTALL_NAME_PERSISTENT:
        raise ValueError(
            f"Package {name} doesn't seem to be a micropip mock. \n"
            "Are you sure it was installed with micropip?"
        )

    # a real mock package - kill it
    # remove all files
    folders: set[Path] = set()
    if d.files is not None:
        for file in d.files:
            p = Path(file.locate())
            p.unlink()
            folders.add(p.parent)

    # delete all folders except site_packages
    # (that check is just to avoid killing
    # undesirable things in case of weird micropip errors)
    site_packages = Path(site.getsitepackages()[0])
    for f in folders:
        if f != site_packages:
            shutil.rmtree(f)
