# fmt: off
"""PEP 517 build backend wrapper for pre-building Cython for wheel."""

from __future__ import annotations

import os
import sysconfig
from collections.abc import Iterator
from contextlib import contextmanager, nullcontext, suppress
from functools import partial
from pathlib import Path
from shutil import copytree
from sys import implementation as _system_implementation
from sys import stderr as _standard_error_stream
from tempfile import TemporaryDirectory
from typing import Union
from warnings import warn as _warn_that

from setuptools.build_meta import build_sdist as _setuptools_build_sdist
from setuptools.build_meta import build_wheel as _setuptools_build_wheel
from setuptools.build_meta import (
    get_requires_for_build_wheel as _setuptools_get_requires_for_build_wheel,
)
from setuptools.build_meta import (
    prepare_metadata_for_build_wheel as _setuptools_prepare_metadata_for_build_wheel,
)

try:
    from setuptools.build_meta import build_editable as _setuptools_build_editable
except ImportError:
    _setuptools_build_editable = None  # type: ignore[assignment]


# isort: split
from distutils.command.install import install as _distutils_install_cmd
from distutils.core import Distribution as _DistutilsDistribution
from distutils.dist import DistributionMetadata as _DistutilsDistributionMetadata

with suppress(ImportError):
    # NOTE: Only available for wheel builds that bundle C-extensions. Declared
    # NOTE: by `get_requires_for_build_wheel()` and
    # NOTE: `get_requires_for_build_editable()`, when `pure-python`
    # NOTE: is not passed.
    from Cython.Build.Cythonize import main as _cythonize_cli_cmd

from ._compat import chdir_cm
from ._cython_configuration import get_local_cython_config as _get_local_cython_config
from ._cython_configuration import (
    make_cythonize_cli_args_from_config as _make_cythonize_cli_args_from_config,
)
from ._cython_configuration import patched_env as _patched_cython_env
from ._transformers import sanitize_rst_roles

__all__ = (  # noqa: WPS410
    'build_sdist',
    'build_wheel',
    'get_requires_for_build_wheel',
    'prepare_metadata_for_build_wheel',
    *(
        () if _setuptools_build_editable is None  # type: ignore[redundant-expr]
        else (
            'build_editable',
            'get_requires_for_build_editable',
            'prepare_metadata_for_build_editable',
        )
    ),
)

_ConfigDict = dict[str, Union[str, list[str], None]]


CYTHON_TRACING_CONFIG_SETTING = 'with-cython-tracing'
"""Config setting name toggle to include line tracing to C-exts."""

CYTHON_TRACING_ENV_VAR = 'YARL_CYTHON_TRACING'
"""Environment variable name toggle used to opt out of making C-exts."""

PURE_PYTHON_CONFIG_SETTING = 'pure-python'
"""Config setting name toggle that is used to opt out of making C-exts."""

PURE_PYTHON_ENV_VAR = 'YARL_NO_EXTENSIONS'
"""Environment variable name toggle used to opt out of making C-exts."""

IS_CPYTHON = _system_implementation.name == "cpython"
"""A flag meaning that the current interpreter implementation is CPython."""

PURE_PYTHON_MODE_CLI_FALLBACK = not IS_CPYTHON
"""A fallback for ``pure-python`` is not set."""


def _is_truthy_setting_value(setting_value: str) -> bool:
    truthy_values = {'', None, 'true', '1', 'on'}
    return setting_value.lower() in truthy_values


def _get_setting_value(
        config_settings: _ConfigDict | None = None,
        config_setting_name: str | None = None,
        env_var_name: str | None = None,
        *,
        default: bool = False,
) -> bool:
    user_provided_setting_sources = (
        (config_settings, config_setting_name, (KeyError, TypeError)),
        (os.environ, env_var_name, KeyError),
    )
    for src_mapping, src_key, lookup_errors in user_provided_setting_sources:
        if src_key is None:
            continue

        with suppress(lookup_errors):  # type: ignore[arg-type]
            return _is_truthy_setting_value(src_mapping[src_key])  # type: ignore[arg-type,index]

    return default


def _make_pure_python(config_settings: _ConfigDict | None = None) -> bool:
    return _get_setting_value(
        config_settings,
        PURE_PYTHON_CONFIG_SETTING,
        PURE_PYTHON_ENV_VAR,
        default=PURE_PYTHON_MODE_CLI_FALLBACK,
    )


def _include_cython_line_tracing(
        config_settings: _ConfigDict | None = None,
        *,
        default: bool = False,
) -> bool:
    return _get_setting_value(
        config_settings,
        CYTHON_TRACING_CONFIG_SETTING,
        CYTHON_TRACING_ENV_VAR,
        default=default,
    )


@contextmanager
def patched_distutils_cmd_install() -> Iterator[None]:
    """Make `install_lib` of `install` cmd always use `platlib`.

    :yields: None
    """
    # Without this, build_lib puts stuff under `*.data/purelib/` folder
    orig_finalize = _distutils_install_cmd.finalize_options

    def new_finalize_options(self: _distutils_install_cmd) -> None:
        self.install_lib = self.install_platlib
        orig_finalize(self)

    _distutils_install_cmd.finalize_options = new_finalize_options  # type: ignore[method-assign]
    try:
        yield
    finally:
        _distutils_install_cmd.finalize_options = orig_finalize  # type: ignore[method-assign]


@contextmanager
def patched_dist_has_ext_modules() -> Iterator[None]:
    """Make `has_ext_modules` of `Distribution` always return `True`.

    :yields: None
    """
    # Without this, build_lib puts stuff under `*.data/platlib/` folder
    orig_func = _DistutilsDistribution.has_ext_modules

    _DistutilsDistribution.has_ext_modules = lambda *args, **kwargs: True  # type: ignore[method-assign]
    try:
        yield
    finally:
        _DistutilsDistribution.has_ext_modules = orig_func  # type: ignore[method-assign]


@contextmanager
def patched_dist_get_long_description() -> Iterator[None]:
    """Make `has_ext_modules` of `Distribution` always return `True`.

    :yields: None
    """
    # Without this, build_lib puts stuff under `*.data/platlib/` folder
    _orig_func = _DistutilsDistributionMetadata.get_long_description

    def _get_sanitized_long_description(self: _DistutilsDistributionMetadata) -> str:
        assert self.long_description is not None
        return sanitize_rst_roles(self.long_description)

    _DistutilsDistributionMetadata.get_long_description = (  # type: ignore[method-assign]
        _get_sanitized_long_description
    )
    try:
        yield
    finally:
        _DistutilsDistributionMetadata.get_long_description = _orig_func  # type: ignore[method-assign]


def _exclude_dir_path(
    excluded_dir_path: Path,
    visited_directory: str,
    _visited_dir_contents: list[str],
) -> list[str]:
    """Prevent recursive directory traversal."""
    # This stops the temporary directory from being copied
    # into self recursively forever.
    # Ref: https://github.com/aio-libs/yarl/issues/992
    visited_directory_subdirs_to_ignore = [
        subdir
        for subdir in _visited_dir_contents
        if excluded_dir_path == Path(visited_directory) / subdir
    ]
    if visited_directory_subdirs_to_ignore:
        print(
            f'Preventing `{excluded_dir_path !s}` from being '
            'copied into itself recursively...',
            file=_standard_error_stream,
        )
    return visited_directory_subdirs_to_ignore


@contextmanager
def _in_temporary_directory(src_dir: Path) -> Iterator[None]:
    with TemporaryDirectory(prefix='.tmp-yarl-pep517-') as tmp_dir:
        tmp_dir_path = Path(tmp_dir)
        root_tmp_dir_path = tmp_dir_path.parent
        _exclude_tmpdir_parent = partial(_exclude_dir_path, root_tmp_dir_path)

        with chdir_cm(tmp_dir):
            tmp_src_dir = tmp_dir_path / 'src'
            copytree(
                src_dir,
                tmp_src_dir,
                ignore=_exclude_tmpdir_parent,
                symlinks=True,
            )
            os.chdir(tmp_src_dir)
            yield


@contextmanager
def maybe_prebuild_c_extensions(
        line_trace_cython_when_unset: bool = False,
        build_inplace: bool = False,
        config_settings: _ConfigDict | None = None,
) -> Iterator[None]:
    """Pre-build C-extensions in a temporary directory, when needed.

    This context manager also patches metadata, setuptools and distutils.

    :param build_inplace: Whether to copy and chdir to a temporary location.
    :param config_settings: :pep:`517` config settings mapping.

    """
    cython_line_tracing_requested = _include_cython_line_tracing(
        config_settings,
        default=line_trace_cython_when_unset,
    )
    is_pure_python_build = _make_pure_python(config_settings)

    if is_pure_python_build:
        print("*********************", file=_standard_error_stream)
        print("* Pure Python build *", file=_standard_error_stream)
        print("*********************", file=_standard_error_stream)

        if cython_line_tracing_requested:
            _warn_that(
                f'The `{CYTHON_TRACING_CONFIG_SETTING !s}` setting requesting '
                'Cython line tracing is set, but building C-extensions is not. '
                'This option will not have any effect for in the pure-python '
                'build mode.',
                RuntimeWarning,
                stacklevel=999,
            )

        yield
        return

    print("**********************", file=_standard_error_stream)
    print("* Accelerated build *", file=_standard_error_stream)
    print("**********************", file=_standard_error_stream)
    if not IS_CPYTHON:
        _warn_that(
            'Building C-extensions under the runtimes other than CPython is '
            'unsupported and will likely fail. Consider passing the '
            f'`{PURE_PYTHON_CONFIG_SETTING !s}` PEP 517 config setting.',
            RuntimeWarning,
            stacklevel=999,
        )

    build_dir_ctx = (
        nullcontext() if build_inplace
        else _in_temporary_directory(src_dir=Path.cwd().resolve())
    )
    with build_dir_ctx:
        config = _get_local_cython_config()

        cythonize_args = _make_cythonize_cli_args_from_config(config)
        with _patched_cython_env(config['env'], cython_line_tracing_requested):
            _cythonize_cli_cmd(cythonize_args)  # type: ignore[no-untyped-call]
        with patched_distutils_cmd_install():
            with patched_dist_has_ext_modules():
                yield


@patched_dist_get_long_description()
def build_wheel(
    wheel_directory: str,
    config_settings: _ConfigDict | None = None,
    metadata_directory: str | None = None,
) -> str:
    """Produce a built wheel.

    This wraps the corresponding ``setuptools``' build backend hook.

    :param wheel_directory: Directory to put the resulting wheel in.
    :param config_settings: :pep:`517` config settings mapping.
    :param metadata_directory: :file:`.dist-info` directory path.

    """
    with maybe_prebuild_c_extensions(
            line_trace_cython_when_unset=False,
            build_inplace=False,
            config_settings=config_settings,
    ):
        return _setuptools_build_wheel(
            wheel_directory=wheel_directory,
            config_settings=config_settings,
            metadata_directory=metadata_directory,
        )


@patched_dist_get_long_description()
def build_editable(
    wheel_directory: str,
    config_settings: _ConfigDict | None = None,
    metadata_directory: str | None = None,
) -> str:
    """Produce a built wheel for editable installs.

    This wraps the corresponding ``setuptools``' build backend hook.

    :param wheel_directory: Directory to put the resulting wheel in.
    :param config_settings: :pep:`517` config settings mapping.
    :param metadata_directory: :file:`.dist-info` directory path.

    """
    with maybe_prebuild_c_extensions(
            line_trace_cython_when_unset=True,
            build_inplace=True,
            config_settings=config_settings,
    ):
        return _setuptools_build_editable(
            wheel_directory=wheel_directory,
            config_settings=config_settings,
            metadata_directory=metadata_directory,
        )


def get_requires_for_build_wheel(
    config_settings: _ConfigDict | None = None,
) -> list[str]:
    """Determine additional requirements for building wheels.

    :param config_settings: :pep:`517` config settings mapping.

    """
    is_pure_python_build = _make_pure_python(config_settings)

    if not is_pure_python_build and not IS_CPYTHON:
        _warn_that(
            'Building C-extensions under the runtimes other than CPython is '
            'unsupported and will likely fail. Consider passing the '
            f'`{PURE_PYTHON_CONFIG_SETTING !s}` PEP 517 config setting.',
            RuntimeWarning,
            stacklevel=999,
        )

    if is_pure_python_build:
        c_ext_build_deps = []
    elif sysconfig.get_config_var('Py_GIL_DISABLED'):
        c_ext_build_deps = ['Cython ~= 3.1.0a1']
    else:
        c_ext_build_deps = ['Cython >= 3.0.12']

    return _setuptools_get_requires_for_build_wheel(
        config_settings=config_settings,
    ) + c_ext_build_deps


build_sdist = patched_dist_get_long_description()(_setuptools_build_sdist)
get_requires_for_build_editable = get_requires_for_build_wheel
prepare_metadata_for_build_wheel = patched_dist_get_long_description()(
    _setuptools_prepare_metadata_for_build_wheel,
)
prepare_metadata_for_build_editable = prepare_metadata_for_build_wheel
