import importlib.metadata
import logging
import os
import re
import shutil
import tempfile
import zipfile
from pathlib import Path
from typing import Dict, List, Optional

import axipy
import packaging
import packaging.requirements
import packaging.version
from axipy.utils import get_dependencies_folder
from packaging.requirements import InvalidRequirement, Requirement

from .axparser.dependencies import Dependencies, PipDependencyInstaller

__all__: List[str] = [
    "PackageDependency",
]

EMPTY = re.compile(r"\s")


class PackageDependency:
    """
    Сервисный класс проверки факта установки пакетов, а также установки новых пакетов.

    .. literalinclude:: /../../tests/doc_examples/utl/test_example_package.py
        :caption: Проверка наличия пакета и поиск файла в нем.
        :pyobject: test_run_example_query_package
        :lines: 2-
        :dedent: 4
    """

    @staticmethod
    def check_package_installed(name: str) -> dict:
        """
        Производит проверку на существование пакета.

        Args:
            name: Наименование пакета или каталог, в котором расположен файл со списком requirements.txt

        Returns:
            dict, где ключ - это имя пакета, а значение - его путь. Если путь не найден, то значение будет None
        """

        def get_paths(n: str) -> Dict[str, Optional[str]]:
            try:
                dist = importlib.metadata.distribution(n)
                return {n: dist.locate_file(".")}
            except importlib.metadata.PackageNotFoundError:
                return {n: None}

        res = {}
        deps = Dependencies.find(Path(name))
        if deps is not None:
            for p in deps.packages():
                res.update(get_paths(p))
        else:
            res = get_paths(name)
        return res

    @staticmethod
    def list_package_files(name: str) -> list:
        """
        Возвращаем список файлов пакетов.

        Args:
            name: Наименование пакета или каталог, в котором расположен файл со списком requirements.txt
        """

        def files(n: str) -> List:
            try:
                dist = importlib.metadata.distribution(n)
                return [dist.locate_file(f) for f in dist.files]
            except importlib.metadata.PackageNotFoundError:
                return []

        res = []
        deps = Dependencies.find(Path(name))
        if deps is not None:
            for p in deps.packages():
                res.extend(files(p))
        else:
            res = files(name)
        return res

    @staticmethod
    def install_dependences(name: str):
        """
        Производит установку пакетов.

        Args:
            name: Наименование пакета или каталог, в котором расположен файл со списком requirements.txt
        """
        dependencies_destination = get_dependencies_folder()
        deps_installer = PipDependencyInstaller()
        deps = Dependencies.find(Path(name))
        if deps is not None:
            deps_installer.install(deps, dependencies_destination)
        else:
            deps_installer.install_by_name(name, dependencies_destination)

    @staticmethod
    def has_dependences(filename: str) -> bool:
        """Есть ли в плагине файл с зависимостями."""
        if filename.lower().endswith("requirements.txt"):
            return True
        elif filename.lower().endswith(".axp"):
            with zipfile.ZipFile(filename) as archive:
                for file in archive.namelist():
                    if file.lower().endswith("requirements.txt"):
                        return True
        elif filename.lower().endswith(".whl"):
            with zipfile.ZipFile(filename) as archive:
                for file in archive.namelist():
                    if file.endswith("METADATA"):
                        for line in archive.open(file).read().decode("utf-8").split("\n"):
                            if "requires-dist" in line.lower():
                                return True
        return False

    @staticmethod
    def _install_deps(module_rootdir: str) -> None:
        # install deps
        deps = Dependencies.find(Path(module_rootdir))
        if deps:
            deps_installer = PipDependencyInstaller()
            try:
                deps_installer.install(deps, axipy.get_dependencies_folder())
            except Exception as e:
                logging.info(str(e))

    @staticmethod
    def _needs_deps_installation(filename: str) -> bool:
        """
        Проверяет, нужна ли установка зависимостей для плагина. Проверка рекурсивная.

        Args:
            filename: Может быть файлом *.axp или папкой плагина.
        """
        if not axipy.mainwindow.is_valid:
            return False
        path_filename = Path(filename)
        if path_filename.suffix == ".axp":
            with zipfile.ZipFile(filename) as archive:
                for file in archive.namelist():
                    if file.lower() == "requirements.txt":
                        list_lines = archive.open(file).read().decode("utf-8").split("\n")
                        return PackageDependency._needs_deps_installation_impl(list_lines)
        elif path_filename.is_dir():
            for elem in path_filename.iterdir():
                if elem.name.lower() == "requirements.txt":
                    req_file = elem
                    with open(req_file, encoding="UTF-8", mode="r") as f:
                        list_lines = f.readlines()
                    return PackageDependency._needs_deps_installation_impl(list_lines)
        else:
            raise ValueError(filename)

        return False

    @staticmethod
    def _needs_deps_installation_impl(requirements_lines: List[str]) -> bool:
        for line in requirements_lines:
            try:
                line = EMPTY.sub("", line)
                r = Requirement(line)
            except InvalidRequirement:
                continue
            else:
                result = PackageDependency._check_req_installed_recursive(r)
                if not result:
                    # Req not installed, so deps installation needed
                    return True

        return False

    @staticmethod
    def _check_req_installed_recursive(req: packaging.requirements.Requirement) -> bool:
        try:
            dist = importlib.metadata.Distribution.from_name(req.name)
        except importlib.metadata.PackageNotFoundError:
            logging.info(f"{req} not found.")
            return False
        if dist.version is None:
            logging.error(f'Can not detect version for {req.name} package.')
        v = packaging.version.Version(dist.version)
        if not req.specifier.contains(v):
            logging.info(f"Wrong version, needs: {req}, but found {v}.")
            return False

        req_list: List[str] = dist.metadata.get_all("Requires-Dist", [])
        for elem in req_list:
            try:
                r = packaging.requirements.Requirement(elem)
                if r.marker is None:
                    result = PackageDependency._check_req_installed_recursive(r)
                    if not result:
                        return False
            except packaging.requirements.InvalidRequirement:
                logging.info(f"InvalidRequirement, {elem}")
                return False

        return True

    @staticmethod
    def download_dependences(filename: str, outfile: str):
        """
        Загрузка зависимых пакетов.

        Args:
            filename: Файл requirements.txt, либо он же из *.axp, либо файл *.whl
            outfile: Выходной файл архива или каталог, куда будут скопированы загруженные файлы
        """
        with tempfile.TemporaryDirectory() as tempdir:
            fn = None
            if filename.lower().endswith("requirements.txt"):
                fn = filename
            elif filename.lower().endswith(".whl"):
                fn = filename
            elif filename.lower().endswith(".axp"):
                with zipfile.ZipFile(filename) as archive:
                    for file in archive.namelist():
                        if file.lower().endswith("requirements.txt"):
                            archive.extract(file, tempdir)
                            p = Path(tempdir) / file
                            fn = p.absolute().as_posix()
                            break
                if fn is None:
                    raise ValueError("Dependencies was not detected")
            if fn is not None:
                deps_installer = PipDependencyInstaller()
                p_down = Path(Path(tempdir) / "whl")
                p_down.mkdir()
                deps_installer.download(fn, p_down.absolute())
                if not os.path.isdir(outfile):
                    with zipfile.ZipFile(outfile, "w", zipfile.ZIP_DEFLATED) as zip_file:
                        for entry in p_down.rglob("*"):
                            zip_file.write(entry, entry.relative_to(p_down))
                else:
                    for entry in p_down.rglob("*"):
                        shutil.copy(entry, outfile)

            else:
                raise ValueError("Invalid input parameter")

    @staticmethod
    def install_downloaded_dependences(filename: str):
        """
        Установка зависимых пакетов из архива.

        Args:
            filename: Файл архива с файлами зависимостей
        """
        if not filename.lower().endswith(".zip"):
            raise ValueError("File is not zip archive")
        deps_installer = PipDependencyInstaller()
        with tempfile.TemporaryDirectory() as tempdir:
            with zipfile.ZipFile(filename) as archive:
                archive.extractall(tempdir)
                for file in Path(tempdir).rglob("*"):
                    deps_installer.install_by_name(file, get_dependencies_folder())

    @staticmethod
    def download_and_extract_dependences(input_dir: str, output_dir: str):
        """
        Загрузка зависимых пакетов всех файлов *.axp в каталоге.

        Args:
            input_dir: Входной каталог с файлами *.axp
            output_dir: Выходной каталог с распакованными зависимыми пакетами
        """
        with tempfile.TemporaryDirectory() as tempdir:
            for entry_axp in Path(input_dir).rglob("*.axp"):
                try:
                    PackageDependency.download_dependences(str(entry_axp), tempdir)
                except ValueError:
                    pass  # no any deps
            for entry_whl in Path(tempdir).rglob("*.whl"):
                with zipfile.ZipFile(entry_whl) as archive:
                    archive.extractall(output_dir)

    @staticmethod
    def extract_axp_to_folder(input_dir: str, output_dir: str):
        for entry_axp in Path(input_dir).rglob("*.axp"):
            with zipfile.ZipFile(entry_axp, "r") as zip_ref:
                zip_ref.extractall(output_dir)
