import importlib
import importlib.metadata
import shutil
import site
from pathlib import Path
from textwrap import dedent

from .._mock_package import _add_in_memory_distribution, _remove_in_memory_distribution

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


def add_mock_package(
    name: str,
    version: str,
    *,
    modules: dict[str, str | None] | None = None,
    persistent: bool = False,
) -> None:
    """
    Add a mock version of a package to the package dictionary.

    This means that if it is a dependency, it is skipped on install.

    By default a single empty module is installed with the same
    name as the package. You can alternatively give one or more modules to make a
    set of named modules.

    The modules parameter is usually a dictionary mapping module name to module text.

    .. code-block:: python

        {
            "mylovely_module":'''
            def module_method(an_argument):
                print("This becomes a module level argument")

            module_value = "this value becomes a module level variable"
            print("This is run on import of module")
            '''
        }

    If you are adding the module in non-persistent mode, you can also pass functions
    which are used to initialize the module on loading (as in `importlib.abc.loader.exec_module` ).
    This allows you to do things like use `unittest.mock.MagicMock` classes for modules.

    .. code-block:: python

        def init_fn(module):
            module.dict["WOO"]="hello"
            print("Initing the module now!")

        ...

        {
            "mylovely_module": init_fn
        }

    Parameters
    ----------
    name :

        Package name to add

    version :

        Version of the package. This should be a semantic version string,
        e.g. 1.2.3

    modules :

        Dictionary of module_name:string pairs.
        The string contains the source of the mock module or is blank for
        an empty module.

    persistent :

        If this is True, modules will be written to the file system, so they
        persist between runs of python (assuming the file system persists).
        If it is False, modules will be stored inside micropip in memory only.
    """

    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]:
    """
    List all mock packages currently installed.
    """
    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:
    """
    Remove a mock package.
    """

    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)
