import importlib.metadata
import json
from collections.abc import Iterator
from copy import deepcopy
from importlib.metadata import Distribution
from typing import Any

from ._utils import get_dist_info
from ._vendored.packaging.src.packaging.requirements import Requirement
from ._vendored.packaging.src.packaging.utils import canonicalize_name


def freeze_lockfile(
    lockfile_packages: dict[str, dict[str, Any]], lockfile_info: dict[str, str]
) -> str:
    return json.dumps(freeze_data(lockfile_packages, lockfile_info))


def freeze_data(
    lockfile_packages: dict[str, dict[str, Any]], lockfile_info: dict[str, str]
) -> dict[str, Any]:
    packages = deepcopy(lockfile_packages)
    packages.update(load_pip_packages(lockfile_packages))

    # Sort
    packages = dict(sorted(packages.items()))
    return {
        "info": lockfile_info,
        "packages": packages,
    }


def load_pip_packages(
    lockfile_packages: dict[str, dict[str, Any]],
) -> Iterator[tuple[str, dict[str, Any]]]:
    distributions = importlib.metadata.distributions()
    pip_packages = [load_pip_package(dist, lockfile_packages) for dist in distributions]

    return (package_item(pkg) for pkg in pip_packages if is_valid(pkg))


def package_item(entry: dict[str, Any]) -> tuple[str, dict[str, Any]]:
    return canonicalize_name(entry["name"]), entry


def is_valid(entry: dict[str, Any]) -> bool:
    return entry["file_name"] is not None


def load_pip_package(
    dist: importlib.metadata.Distribution,
    lockfile_packages: dict[str, dict[str, Any]],
) -> dict[str, Any]:
    name = dist.name
    version = dist.version
    url = dist.read_text("PYODIDE_URL")
    sha256 = dist.read_text("PYODIDE_SHA256")
    imports = (dist.read_text("top_level.txt") or "").split()
    requires = dist.read_text("PYODIDE_REQUIRES")
    if not requires:
        fix_package_dependencies(name, lockfile_packages)
        requires = dist.read_text("PYODIDE_REQUIRES")
    depends = json.loads(requires or "[]")

    return dict(
        name=name,
        version=version,
        file_name=url,
        install_dir="site",
        sha256=sha256,
        imports=imports,
        depends=depends,
    )


def fix_package_dependencies(
    package_name: str,
    lockfile_packages: dict[str, dict[str, Any]] | None = None,
    *,
    extras: list[str | None] | None = None,
) -> None:
    """Check and fix the list of dependencies for this package

    If you have manually installed a package and dependencies from wheels,
    the dependencies will not be correctly setup in the package list
    or the pyodide lockfile generated by freezing. This method checks
    if the dependencies are correctly set in the package list and will
    add missing dependencies.

    Parameters
    ----------
    package_name (string):
        The name of the package to check.

    lockfile_packages (dict):
        The lockfile packages to check against.

    extras (list):
        List of extras for this package.

    """
    if lockfile_packages and package_name in lockfile_packages:
        # don't check things that are in original repository
        return

    dist = Distribution.from_name(package_name)

    package_requires = dist.requires
    if package_requires is None:
        # no dependencies - we're good to go
        return

    url = dist.read_text("PYODIDE_URL")

    # If it wasn't installed with micropip / pyodide, then we
    # can't do anything with it.
    if url is None:
        return

    # Get current list of pyodide requirements
    requires = dist.read_text("PYODIDE_REQUIRES")

    if requires:
        depends = json.loads(requires)
    else:
        depends = []

    if extras is None:
        extras = [None]
    else:
        extras = extras + [None]
    for r in package_requires:
        req = Requirement(r)
        req_extras = req.extras
        req_marker = req.marker
        req_name = canonicalize_name(req.name)
        needs_requirement = False
        if req_marker is not None:
            for e in extras:
                if req_marker.evaluate(None if e is None else {"extra": e}):
                    needs_requirement = True
                    break
        else:
            needs_requirement = True

        if needs_requirement:
            fix_package_dependencies(req_name, extras=list(req_extras))

            if req_name not in depends:
                depends.append(req_name)

    # write updated depends to PYODIDE_DEPENDS
    (get_dist_info(dist) / "PYODIDE_REQUIRES").write_text(
        json.dumps(sorted(x for x in depends))
    )
