import importlib.util
import os
import re
import shutil
import site
import sys
from contextlib import contextmanager
from pathlib import Path

from cx_Freeze import build_exe

from dev_tools.build.deps.script import build_deps
from dev_tools.build.freeze.cx.options import NanoBuildOptions
from dev_tools.build.freeze.cx.verify import BundlingVerifier
from dev_tools.build.freeze.data import get_build_data_files
from dev_tools.build.licensing.script import build_3rd_party_licensing
from dev_tools.build.sign.file import sign_file_for_authenticity
from dev_tools.build.utility import dev_cache_dir
from dev_tools.picoture.script import build_picoture


class build_exe_nano(build_exe):
    def run(self):
        self._clean_site()
        self._optimize_site()
        self._check_site_metadata()

        # build embedded modules with own dependencies
        picoture_jar_path = build_picoture(force=True)

        exe_dir = Path(self.build_exe).resolve()
        if exe_dir.exists():
            shutil.rmtree(exe_dir)

        with self._check_specify_version():
            with self._controlled_path_environment():
                build_exe.run(self)

        if not exe_dir.is_dir():
            raise Exception("build missing?")
        NanoBuildOptions.EXE_DIR = exe_dir

        # build dependencies
        build_deps(dev_cache_dir(), exe_dir / "deps", picoture_jar_path, skip_signing=NanoBuildOptions.SKIP_SIGNING)

        # NOTE: On darwin verify is done after restructuring for distribution.
        if not sys.platform.startswith("darwin"):
            # NOTE: If there is no "freezer" on self, just go the super class execution and make it so, ffs it is
            #   created and discarded in a single scope so we can't keep it, such is life, jkm.
            BundlingVerifier(Path(exe_dir), self.freezer.bin_path_excludes).run()

        # copy extra data files
        for file in get_build_data_files():
            shutil.copy(file, exe_dir / file.name)

        # build additional legal information
        build_3rd_party_licensing(exe_dir)

        # remove unnecessary files
        for root, folders, files in os.walk(exe_dir / "lib/clarabot_nano/resource/locale"):
            for file in files:
                if file.endswith(".pot") or file.endswith(".po"):
                    Path(root, file).unlink()
        if not NanoBuildOptions.DEVELOPMENT_BUILD:
            shutil.rmtree(exe_dir / "lib/clarabot_nano/development")

        if not NanoBuildOptions.SKIP_SIGNING:
            # sign executables
            sign_file_for_authenticity(*[exe_dir / ex.target_name for ex in self.distribution.executables])

    def _clean_site(self):
        # Clear sitecustomize and bytecode cache of removed packages to be safe.
        site_was_clean = True
        for site_dir in site.getsitepackages():
            site_dir = Path(site_dir)
            site_customize_path = site_dir / "sitecustomize.py"
            if not site_customize_path.exists() or site_customize_path.read_bytes():
                site_was_clean = False
                site_customize_path.write_text("", encoding="utf-8")
            for item in site_dir.iterdir():
                if item.is_dir():
                    item_contents = list(item.iterdir())
                    if len(item_contents) == 1 and item_contents[0].name == "__pycache__":
                        site_was_clean = False
                        shutil.rmtree(item)

        if not site_was_clean:
            raise Exception("Site cleared, try again...")

        # Some of the testing libs polluted the distribution with pth-imports, so keep them out.
        for test_lib_name in ["clarabot_testing", "pytest", "pytest_cov", "pytest_asyncio"]:
            if importlib.util.find_spec(test_lib_name) is not None:
                raise Exception(f"Testing library must not be in build environment: {test_lib_name}")

    def _optimize_site(self):
        # Delete anything that is not needed and cx-freeze would include it.
        self._strip_site_wx()

    def _strip_site_wx(self):
        try:
            import wx
        except ImportError:
            return
        root = Path(os.path.dirname(os.path.abspath(wx.__file__)))
        backup = root.parent / "_x_wx"
        backup.mkdir(exist_ok=True)
        # for name in dirs_to_move:
        py_names = "aui|dataview|glcanvas|grid|html|html2|media|propgrid|ribbon|richtext|stc|webkit|xrc"
        py_ext = "pyd" if sys.platform.startswith("win") else "so"
        py_re = re.compile(rf"^((_(?:{py_names})\..+\.{py_ext})|((?:{py_names})\.pyi?))$")
        lib_names = "aui|gl|html|media|propgrid|qa|ribbon|richtext|stc|webview|xrc"
        if sys.platform.startswith("linux"):
            lib_ext = r"so(?:\.\d+)?"
        else:
            lib_ext = "dll" if sys.platform.startswith("win") else "dylib"
        lib_prefix = "wxmsw" if sys.platform.startswith("win") else "libwx_"
        lib_re = re.compile(rf"^({lib_prefix}.+_(?:{lib_names})[_-].+\.{lib_ext})$")
        for item in root.iterdir():
            name = item.name
            if not py_re.match(name) and not lib_re.match(name):
                continue
            source = root / name
            if not source.exists():
                continue
            target = backup / name
            if target.exists():
                raise Exception(f"Backup already exists when stripping wx: {target}")
            shutil.move(source, target)

    def _check_site_metadata(self):
        # May only build if all packages are installed from wheels. (because of metadata parsing bug)
        for site_path in site.getsitepackages():
            for item in Path(site_path).iterdir():
                if item.name.endswith(".egg-info"):
                    raise Exception(
                        "The importlib_metadata (cx_Freeze dependency) cannot parse egg-info properly. "
                        f"Reinstall egg packages after 'wheel' is installed! {item.name}"
                    )

    @contextmanager
    def _controlled_path_environment(self):
        normal_path = os.environ["PATH"]
        if sys.platform.startswith("win"):
            # Only include DLLs from first party places
            os.environ["PATH"] = os.pathsep.join(
                [os.path.dirname(sys.executable), r"C:\Windows\system32", r"C:\Windows"]
            )
        try:
            yield
        finally:
            os.environ["PATH"] = normal_path

    @contextmanager
    def _check_specify_version(self):
        # Setup overrides and deployment-customizations before setup().
        # Extend/verify the version in the package init.
        tag_version, git_version = NanoBuildOptions.TAG_VERSION, NanoBuildOptions.GIT_VERSION
        pkg_init_path = Path(importlib.util.find_spec("clarabot_nano").origin)
        init_source = pkg_init_path.read_bytes().decode()
        _init_locals = {}
        exec(init_source, globals(), _init_locals)
        if _init_locals["__version__"] != tag_version:
            raise Exception(
                f"Tag version in package {_init_locals['__version__']!r} does not match git-tag {tag_version!r}"
            )
        init_dist = init_source.replace(f'__version__ = "{tag_version}"', f'__version__ = "{tag_version}{git_version}"')
        pkg_init_path.write_bytes(init_dist.encode())
        _init_locals2 = {}
        exec(pkg_init_path.read_bytes().decode(), globals(), _init_locals2)
        if _init_locals2["__version__"] != tag_version + git_version:
            raise Exception(
                f"Tag version is incorrect after extension:\n"
                f"{_init_locals2['__version__']!r} vs {tag_version + git_version!r}"
            )
        try:
            yield tag_version, git_version
        finally:
            pkg_init_path.write_bytes(init_source.encode())
