from __future__ import annotations

import abc
import fnmatch
from itertools import chain
from operator import methodcaller as method
from pathlib import Path
from textwrap import dedent

from virtualenv.create.describe import Python3Supports
from virtualenv.create.via_global_ref.builtin.ref import ExePathRefToDest, PathRefToDest
from virtualenv.create.via_global_ref.store import is_store_python

from .common import CPython, CPythonPosix, CPythonWindows, is_mac_os_framework, is_macos_brew


class CPython3(CPython, Python3Supports, abc.ABC):
    """CPython 3 or later."""


class CPython3Posix(CPythonPosix, CPython3):
    @classmethod
    def can_describe(cls, interpreter):
        return (
            is_mac_os_framework(interpreter) is False
            and is_macos_brew(interpreter) is False
            and super().can_describe(interpreter)
        )

    def env_patch_text(self):
        text = super().env_patch_text()
        if self.pyvenv_launch_patch_active(self.interpreter):
            text += dedent(
                """
                # for https://github.com/python/cpython/pull/9516, see https://github.com/pypa/virtualenv/issues/1704
                import os
                if "__PYVENV_LAUNCHER__" in os.environ:
                    del os.environ["__PYVENV_LAUNCHER__"]
                """,
            )
        return text

    @classmethod
    def pyvenv_launch_patch_active(cls, interpreter):
        ver = interpreter.version_info
        return interpreter.platform == "darwin" and ((3, 7, 8) > ver >= (3, 7) or (3, 8, 3) > ver >= (3, 8))


class CPython3Windows(CPythonWindows, CPython3):
    """CPython 3 on Windows."""

    @classmethod
    def setup_meta(cls, interpreter):
        if is_store_python(interpreter):  # store python is not supported here
            return None
        return super().setup_meta(interpreter)

    @classmethod
    def sources(cls, interpreter):
        if cls.has_shim(interpreter):
            refs = cls.executables(interpreter)
        else:
            refs = chain(
                cls.executables(interpreter),
                cls.dll_and_pyd(interpreter),
                cls.python_zip(interpreter),
            )
        yield from refs

    @classmethod
    def executables(cls, interpreter):
        sources = super().sources(interpreter)
        if interpreter.version_info >= (3, 13):
            # Create new refs with corrected launcher paths
            updated_sources = []
            for ref in sources:
                if ref.src.name == "python.exe":
                    launcher_path = ref.src.with_name("venvlauncher.exe")
                    if launcher_path.exists():
                        new_ref = ExePathRefToDest(
                            launcher_path, dest=ref.dest, targets=[ref.base, *ref.aliases], must=ref.must, when=ref.when
                        )
                        updated_sources.append(new_ref)
                        continue
                elif ref.src.name == "pythonw.exe":
                    w_launcher_path = ref.src.with_name("venvwlauncher.exe")
                    if w_launcher_path.exists():
                        new_ref = ExePathRefToDest(
                            w_launcher_path,
                            dest=ref.dest,
                            targets=[ref.base, *ref.aliases],
                            must=ref.must,
                            when=ref.when,
                        )
                        updated_sources.append(new_ref)
                        continue
                # Keep the original ref unchanged
                updated_sources.append(ref)
            return updated_sources
        return sources

    @classmethod
    def has_shim(cls, interpreter):
        return interpreter.version_info.minor >= 7 and cls.shim(interpreter) is not None  # noqa: PLR2004

    @classmethod
    def shim(cls, interpreter):
        root = Path(interpreter.system_stdlib) / "venv" / "scripts" / "nt"
        # Before 3.13 the launcher was called python.exe, after is venvlauncher.exe
        # https://github.com/python/cpython/issues/112984
        exe_name = "venvlauncher.exe" if interpreter.version_info >= (3, 13) else "python.exe"
        shim = root / exe_name
        if shim.exists():
            return shim
        return None

    @classmethod
    def host_python(cls, interpreter):
        if cls.has_shim(interpreter):
            # starting with CPython 3.7 Windows ships with a venvlauncher.exe that avoids the need for dll/pyd copies
            # it also means the wrapper must be copied to avoid bugs such as https://bugs.python.org/issue42013
            return cls.shim(interpreter)
        return super().host_python(interpreter)

    @classmethod
    def dll_and_pyd(cls, interpreter):
        folders = [Path(interpreter.system_executable).parent]

        # May be missing on some Python hosts.
        # See https://github.com/pypa/virtualenv/issues/2368
        dll_folder = Path(interpreter.system_prefix) / "DLLs"
        if dll_folder.is_dir():
            folders.append(dll_folder)

        for folder in folders:
            for file in folder.iterdir():
                if file.suffix in {".pyd", ".dll"}:
                    yield PathRefToDest(file, cls.to_bin)

    @classmethod
    def python_zip(cls, interpreter):
        """
        "python{VERSION}.zip" contains compiled *.pyc std lib packages, where
        "VERSION" is `py_version_nodot` var from the `sysconfig` module.
        :see: https://docs.python.org/3/using/windows.html#the-embeddable-package
        :see: `discovery.py_info.PythonInfo` class (interpreter).
        :see: `python -m sysconfig` output.

        :note: The embeddable Python distribution for Windows includes
        "python{VERSION}.zip" and "python{VERSION}._pth" files. User can
        move/rename *zip* file and edit `sys.path` by editing *_pth* file.
        Here the `pattern` is used only for the default *zip* file name!
        """  # noqa: D205
        pattern = f"*python{interpreter.version_nodot}.zip"
        matches = fnmatch.filter(interpreter.path, pattern)
        matched_paths = map(Path, matches)
        existing_paths = filter(method("exists"), matched_paths)
        path = next(existing_paths, None)
        if path is not None:
            yield PathRefToDest(path, cls.to_bin)


__all__ = [
    "CPython3",
    "CPython3Posix",
    "CPython3Windows",
]
