#     Copyright 2026, Kay Hayen, mailto:kay.hayen@gmail.com find license text at end of file


"""Python flavors specifics.

This abstracts the Python variants from different people. There is not just
CPython, but Anaconda, Debian, pyenv, Apple, lots of people who make Python
in a way the requires technical differences, e.g. static linking, LTO, or
DLL presence, link paths, etc.

"""

import os
import sys

from nuitka.utils.FileOperations import (
    areSamePaths,
    isFilenameBelowPath,
    isFilenameSameAsOrBelowPath,
)
from nuitka.utils.Utils import (
    isAlpineLinux,
    isAndroidBasedLinux,
    isArchBasedLinux,
    isFedoraBasedLinux,
    isLinux,
    isMacOS,
    isPosixWindows,
    isWin32Windows,
    withNoDeprecationWarning,
)

from .PythonVersions import (
    getInstalledPythonRegistryPaths,
    getRunningPythonDLLPath,
    getSystemPrefixPath,
    isStaticallyLinkedPython,
    python_version,
    python_version_str,
)


def isMonolithPy():
    """Is this our own fork of CPython named MonolithPy."""

    # spell-checker: ignore monolithpy

    if python_version >= 0x300:
        return sys.implementation.name == "monolithpy"
    else:
        return sys.subversion[0] == "monolithpy"


_is_anaconda = None


def isAnacondaPython():
    """Detect if Python variant Anaconda"""

    # singleton, pylint: disable=global-statement
    global _is_anaconda

    if _is_anaconda is None:
        _is_anaconda = os.path.exists(os.path.join(sys.prefix, "conda-meta"))

    return _is_anaconda


def isApplePython():
    if not isMacOS():
        return False

    # Python2 on 10.15 or higher
    if "+internal-os" in sys.version:
        return True

    # Older macOS had that
    if isFilenameSameAsOrBelowPath(path="/usr/bin/", filename=getSystemPrefixPath()):
        return True
    # Newer macOS has that
    if isFilenameSameAsOrBelowPath(
        path="/Library/Developer/CommandLineTools/", filename=getSystemPrefixPath()
    ):
        return True

    # Xcode has that on macOS, we consider it an Apple Python for now, it might
    # be more usable than Apple Python, we but we delay that.
    if isFilenameSameAsOrBelowPath(
        path="/Applications/Xcode.app/Contents/Developer/",
        filename=getSystemPrefixPath(),
    ):
        return True

    return False


_is_pyenv_homebrew_python = None


def isPyenvHomebrewPython():
    """Detect if this is a Pyenv Python built with Homebrew dependencies."""
    # Singleton, pylint: disable=global-statement
    global _is_pyenv_homebrew_python

    if _is_pyenv_homebrew_python is None:
        if not isMacOS() or not isPyenvPython():
            _is_pyenv_homebrew_python = False
        else:
            _is_pyenv_homebrew_python = False
            import sysconfig

            for var_value in sysconfig.get_config_vars().values():
                if var_value is not None:
                    var_value_str = str(var_value)
                    if (
                        "/opt/homebrew" in var_value_str
                        or "/usr/local/opt" in var_value_str
                        or "/usr/local/Homebrew" in var_value_str
                    ):
                        _is_pyenv_homebrew_python = True
                        break

    return _is_pyenv_homebrew_python


def isHomebrewPython():
    # spell-checker: ignore sitecustomize
    if not isMacOS():
        return False

    if isGithubActionsPython():
        return True

    candidate = os.path.join(
        getSystemPrefixPath(), "lib", "python" + python_version_str, "sitecustomize.py"
    )

    if os.path.exists(candidate):
        with open(candidate, "rb") as site_file:
            line = site_file.readline()

        if b"Homebrew" in line:
            return True

    return False


def getHomebrewInstallPath():
    assert isHomebrewPython() or isPyenvHomebrewPython()

    candidate = getSystemPrefixPath()

    while candidate != "/":
        if os.path.isdir(os.path.join(candidate, "Cellar")):
            return candidate

        candidate = os.path.dirname(candidate)

    # Expect above to work, except for when pyenv is installing to outside of it.
    assert isPyenvHomebrewPython()

    if os.path.isdir("/opt/homebrew/Cellar"):
        return "/opt/homebrew"

    if os.path.isdir("/usr/local/Cellar"):
        return "/usr/local"

    sys.exit("Error, failed to locate homebrew installation path.")


_is_mac_ports_python = None


def isMacPortsPython():
    """Detect if this is a MacPorts Python."""
    # Singleton, pylint: disable=global-statement
    global _is_mac_ports_python

    if _is_mac_ports_python is None:
        _is_mac_ports_python = False

        if isMacOS():
            _is_mac_ports_python = isFilenameSameAsOrBelowPath(
                path="/opt/local", filename=getSystemPrefixPath()
            ) or isFilenameSameAsOrBelowPath(path="/opt/local", filename=sys.executable)

    return _is_mac_ports_python


def getMacPortsInstallPath():
    assert isMacPortsPython()

    candidate = getSystemPrefixPath()

    while candidate != "/":
        if candidate == "/opt/local":
            return candidate

        if os.path.exists(os.path.join(candidate, "bin", "port")):
            return candidate

        candidate = os.path.dirname(candidate)

    return "/opt/local"


def isRyePython():
    if isMacOS():
        import sysconfig

        # We didn't find a better one, since they do not leave much of any trace
        # otherwise than an unusable "libpython.a"
        # spell-checker: ignore isysroot,flto,ldflags
        value = sysconfig.get_config_var("_OSX_SUPPORT_INITIAL_PY_CORE_LDFLAGS")
        return value is not None and "-flto=thin" in value and "-isysroot" in value

    return False


def isPythonBuildStandalonePython():
    try:
        import sysconfig

        return sysconfig.get_config_var("PYTHON_BUILD_STANDALONE") == 1
    except ImportError:
        return False


def isPyenvPython():
    if isWin32Windows():
        return False

    return os.getenv("PYENV_ROOT") and isFilenameSameAsOrBelowPath(
        path=os.getenv("PYENV_ROOT"), filename=getSystemPrefixPath()
    )


def isMSYS2MingwPython():
    """MSYS2 the MinGW64 variant that is more Win32 compatible."""
    if not isWin32Windows() or "GCC" not in sys.version:
        return False

    import sysconfig

    if python_version >= 0x3B0:
        return "-mingw_" in sysconfig.get_config_var("EXT_SUFFIX")
    else:
        return "-mingw_" in sysconfig.get_config_var("SO")


def isTermuxPython():
    """Is this Termux Android Python."""
    # spell-checker: ignore termux

    if not isAndroidBasedLinux():
        return False

    return "com.termux" in getSystemPrefixPath().split("/")


def isUninstalledPython():
    """Decide if this a Python that doesn't have a system wide DLL installation for its use."""
    # return driven, pylint: disable=too-many-return-statements

    if isDebianPackagePython():
        return False

    if isSelfCompiledPythonUninstalled():
        return True

    if isPythonBuildStandalonePython():
        return True

    if isAnacondaPython() or isWinPython():
        return True

    if os.name == "nt":
        import ctypes.wintypes

        GetSystemDirectory = ctypes.windll.kernel32.GetSystemDirectoryW
        GetSystemDirectory.argtypes = (ctypes.wintypes.LPWSTR, ctypes.wintypes.DWORD)
        GetSystemDirectory.restype = ctypes.wintypes.DWORD

        MAX_PATH = 4096
        buf = ctypes.create_unicode_buffer(MAX_PATH)

        res = GetSystemDirectory(buf, MAX_PATH)
        assert res != 0

        system_path = buf.value
        return not isFilenameBelowPath(
            path=system_path, filename=getRunningPythonDLLPath()
        )

    if isStaticallyLinkedPython():
        return False

    return None


_is_win_python = None


def isWinPython():
    """Is this Python from WinPython."""
    if "WinPython" in sys.version:
        return True

    # singleton, pylint: disable=global-statement
    global _is_win_python

    if _is_win_python is None:
        for element in sys.path:
            if os.path.basename(element) == "site-packages":
                if os.path.exists(os.path.join(element, "WinPython")):
                    _is_win_python = True
                    break
        else:
            _is_win_python = False

    return _is_win_python


_is_debian_package_python = None


def isDebianPackagePython():
    """Is this Python from a debian package."""

    # singleton, pylint: disable=global-statement
    global _is_debian_package_python

    if _is_debian_package_python is None:
        _is_debian_package_python = _isDebianPackagePython()

    return _is_debian_package_python


def _isDebianPackagePython():
    # spell-checker: ignore multiarch

    if not isLinux():
        return False

    if python_version < 0x300:
        return hasattr(sys, "_multiarch")
    else:
        import sysconfig

        # Need to check there for Debian patch.
        if "deb_system" in getattr(sysconfig, "_INSTALL_SCHEMES", {}):
            return True

        with withNoDeprecationWarning():
            try:
                from distutils.dir_util import _multiarch
            except ImportError:
                return False
            else:
                return True


def isFedoraPackagePython():
    """Is the Python from a Fedora package."""
    if not isFedoraBasedLinux():
        return False

    system_prefix_path = getSystemPrefixPath()

    return system_prefix_path == "/usr"


def isAlpinePackagePython():
    """Is the Python from a Alpine package."""
    if not isAlpineLinux():
        return False

    system_prefix_path = getSystemPrefixPath()

    return system_prefix_path == "/usr"


def isArchPackagePython():
    """Is the Python from a Fedora package."""
    if not isArchBasedLinux():
        return False

    system_prefix_path = getSystemPrefixPath()

    return system_prefix_path == "/usr"


def isCPythonOfficialPackage():
    """Official CPython download, kind of hard to detect since self-compiled doesn't change much."""

    sys_prefix = getSystemPrefixPath()

    # For macOS however, it's very knowable.
    if isMacOS():
        for candidate in (
            "/Library/Frameworks/Python.framework/Versions/",
            "/Library/Frameworks/PythonT.framework/Versions/",
        ):

            if isFilenameBelowPath(path=candidate, filename=sys_prefix):
                return True

    # For Windows, we check registry.
    if isWin32Windows():
        for registry_python_exe in getInstalledPythonRegistryPaths(python_version_str):
            if areSamePaths(sys_prefix, os.path.dirname(registry_python_exe)):
                return True

    return False


_is_self_compiled_python = None


def isSelfCompiledPythonUninstalled():
    # singleton, pylint: disable=global-statement
    global _is_self_compiled_python

    if _is_self_compiled_python is None:
        sys_prefix = getSystemPrefixPath()

        _is_self_compiled_python = os.path.isdir(os.path.join(sys_prefix, "PCbuild"))

    return _is_self_compiled_python


_is_manylinux_python = None


def isManyLinuxPython():
    if not isLinux():
        return False

    # singleton, pylint: disable=global-statement
    global _is_manylinux_python

    sys_prefix = getSystemPrefixPath()

    if _is_manylinux_python is None:
        _is_manylinux_python = os.path.isfile(
            os.path.join(sys_prefix, "..", "static-libs-for-embedding-only.tar.xz")
        )

    return _is_manylinux_python


def isGithubActionsPython():
    # spell-checker: ignore hostedtoolcache

    return os.getenv("GITHUB_ACTIONS") == "true" and getSystemPrefixPath().startswith(
        "/opt/hostedtoolcache/Python"
    )


def isWindowsStorePython():
    if not isWin32Windows():
        return False

    # spell-checker: ignore LOCALAPPDATA
    local_app_data = os.getenv("LOCALAPPDATA")
    if not local_app_data:
        # Not insisting to be any better for those.
        return False

    return isFilenameBelowPath(
        path=os.path.join(local_app_data, "Microsoft", "WindowsApps"),
        filename=sys.executable,
    )


def getPythonFlavorName():
    """For output to the user only."""
    # return driven, pylint: disable=too-many-branches,too-many-return-statements

    if isMonolithPy():
        return "MonolithPy"
    elif isAnacondaPython():
        return "Anaconda Python"
    elif isWinPython():
        return "WinPython"
    elif isDebianPackagePython():
        return "Debian Python"
    elif isFedoraPackagePython():
        return "Fedora Python"
    elif isArchPackagePython():
        return "Arch Python"
    elif isAlpinePackagePython():
        return "Alpine Python"
    elif isGithubActionsPython():
        return "GitHub Actions Python"
    elif isPyenvHomebrewPython():
        return "PyEnv on Homebrew Python"
    elif isPyenvPython():
        return "PyEnv Python"
    elif isMacPortsPython():
        return "MacPorts Python"
    elif isHomebrewPython():
        return "Homebrew Python"
    elif isRyePython():
        return "Rye Python"
    elif isPythonBuildStandalonePython():
        return "Python Build Standalone"
    elif isApplePython():
        return "Apple Python"
    elif isPosixWindows():
        return "MSYS2 Posix"
    elif isMSYS2MingwPython():
        return "MSYS2 MinGW"
    elif isTermuxPython():
        return "Android Termux"
    elif isWindowsStorePython():
        return "Windows Store Python"
    elif isCPythonOfficialPackage():
        return "CPython Official"
    elif isSelfCompiledPythonUninstalled():
        return "Self Compiled Uninstalled"
    elif isManyLinuxPython():
        return "Manylinux Python"
    else:
        return "Unknown"


def hasAcceleratedSupportedFlavor():
    return not isWindowsStorePython()


#     Part of "Nuitka", an optimizing Python compiler that is compatible and
#     integrates with CPython, but also works on its own.
#
#     Licensed under the GNU Affero General Public License, Version 3 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#        https://www.gnu.org/licenses/agpl-3.0.txt
#
#     See also: "Nuitka Runtime Library Exception, Version 1.0" in file
#     "LICENSE-RUNTIME.txt" for additional permissions granted under Section 7.
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
