import ast
import configparser
import os
import re
import sys
import tarfile
import tempfile
import zipfile
from contextlib import contextmanager
from functools import lru_cache
from pathlib import Path
from tempfile import NamedTemporaryFile, TemporaryDirectory
from typing import TYPE_CHECKING, Any, AnyStr, Dict, List, Mapping, Optional, Sequence, Tuple, Union
from urllib.parse import unquote, urlparse, urlsplit, urlunparse, urlunsplit

from pipenv.exceptions import PipenvUsageError

# Lazy: ``pipenv.patched.pip._internal.commands.install.InstallCommand``
# pulls in ~79 ms of pip-internal command/network/cli machinery on
# every ``pipenv`` startup.  Only used in :func:`get_pip_command`.
# Other ``_internal.*`` symbols below (``Link``, ``Downloader``,
# ``PipSession``, ``parse_requirements``, the request constructors,
# ``InstallRequirement``, ``hide_url``, ``VcsSupport``) are all
# function-scope-only and lazy-imported inside their callers so
# ``import pipenv.utils.dependencies`` does not force the heavy
# pip-internal chain.
from pipenv.patched.pip._internal.req.req_install import InstallRequirement
from pipenv.patched.pip._vendor import tomli
from pipenv.patched.pip._vendor.distlib.util import COMPARE_OP
from pipenv.patched.pip._vendor.packaging.markers import Marker
from pipenv.patched.pip._vendor.packaging.requirements import Requirement
from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.pip._vendor.packaging.version import parse

if TYPE_CHECKING:
    from pipenv.patched.pip._internal.commands.install import InstallCommand
from pipenv.utils import err
from pipenv.utils.fileutils import (
    create_tracked_tempdir,
)
from pipenv.utils.requirements import redact_auth_from_url

# ``pipenv.utils.unpack`` drags in
# ``pipenv.patched.pip._internal.operations.prepare`` →
# ``pipenv.patched.pip._internal.network.download`` (~26 ms cum) on
# every import.  It's only needed when ``determine_package_name``
# falls into its remote-link branch, so the import is deferred to
# that branch.
from .constants import (
    INSTALLABLE_EXTENSIONS,
    RELEVANT_PROJECT_FILES,
    REMOTE_SCHEMES,
    SCHEME_LIST,
    VCS_LIST,
    VCS_SCHEMES,
)
from .indexes import parse_indexes, prepare_pip_source_args
from .internet import get_host_and_port, is_valid_url
from .markers import PipenvMarkers
from .pip import get_trusted_hosts


def get_version(pipfile_entry):
    if str(pipfile_entry) == "{}" or is_star(pipfile_entry):
        return ""

    if hasattr(pipfile_entry, "keys") and "version" in pipfile_entry:
        if is_star(pipfile_entry.get("version")):
            return ""
        version = pipfile_entry.get("version")
        if version is None:
            version = ""
        return version.strip().lstrip("(").rstrip(")")

    if isinstance(pipfile_entry, str):
        return pipfile_entry.strip().lstrip("(").rstrip(")")
    return ""


def python_version(path_to_python):
    from pipenv.vendor.pythonfinder.utils import get_python_version

    if not path_to_python:
        return None
    try:
        version = get_python_version(path_to_python)
    except Exception:
        return None
    return version


def clean_pkg_version(version):
    """Uses pip to prepare a package version string, from our internal version."""
    return pep440_version(str(version).replace("==", ""))


def get_lockfile_section_using_pipfile_category(category):
    if category == "dev-packages":
        return "develop"
    elif category == "packages":
        return "default"
    return category


def get_pipfile_category_using_lockfile_section(category):
    if category == "develop":
        return "dev-packages"
    elif category == "default":
        return "packages"
    return category


class HackedPythonVersion:
    """A hack, which allows us to tell resolver which version of Python we're using."""

    def __init__(self, python_path):
        self.python_path = python_path

    def __enter__(self):
        if self.python_path:
            os.environ["PIP_PYTHON_PATH"] = str(self.python_path)

    def __exit__(self, *args):
        pass


def get_canonical_names(packages):
    """Canonicalize a list of packages and return a set of canonical names"""
    from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name

    if not isinstance(packages, Sequence):
        if not isinstance(packages, str):
            return packages
        packages = [packages]
    return {canonicalize_name(pkg) for pkg in packages if pkg}


def pep440_version(version):
    """Normalize version to PEP 440 standards"""
    return str(parse(version))


def pep423_name(name):
    """Normalize package name to PEP 423 style standard.

    Lowercases ``name`` and rewrites ``_`` to ``-`` for plain
    distribution names. If ``name`` contains a VCS scheme token
    (``git``, ``svn``, ``hg``, ``bzr``) or URL scheme token
    (``http://``, ``https://``, ``ftp://``, ``ftps://``, ``file://``)
    the underscore rewrite is skipped so URL/VCS specifiers are not
    mangled (e.g. ``git+ssh://host/path/some_repo``).

    Note: prior to the W4 cleanup this function had an inverted
    predicate (``any(token not in name ...)``) that made the early
    return the common case and the underscore-preserving branch
    unreachable. The predicate has been corrected; for bare package
    names (the dominant call pattern) the observable result is
    unchanged.
    """
    name = name.lower()
    if not any(token in name for token in (VCS_LIST + SCHEME_LIST)):
        return name.replace("_", "-")
    return name


def translate_markers(pipfile_entry):
    from pipenv.patched.pip._vendor.packaging.markers import default_environment

    allowed_marker_keys = ["markers"] + list(default_environment().keys())
    provided_keys = list(pipfile_entry.keys()) if hasattr(pipfile_entry, "keys") else []
    pipfile_markers = set(provided_keys) & set(allowed_marker_keys)
    new_pipfile = dict(pipfile_entry).copy()
    marker_set = set()
    os_name_marker = None
    if "markers" in new_pipfile:
        marker_str = new_pipfile.pop("markers")
        if marker_str:
            marker = str(Marker(marker_str))
            if "extra" not in marker:
                marker_set.add(marker)
    for m in pipfile_markers:
        entry = f"{pipfile_entry[m]}"
        if m != "markers":
            if m != "os_name":
                marker_set.add(str(Marker(f"{m} {entry}")))
            new_pipfile.pop(m)
    if marker_set:
        markers_str = " and ".join(
            f"{s}" if " and " in s else s for s in sorted(dict.fromkeys(marker_set))
        )
        if os_name_marker:
            markers_str = f"({markers_str}) and {os_name_marker}"
        new_pipfile["markers"] = str(Marker(markers_str)).replace('"', "'")
    return new_pipfile


def unearth_hashes_for_dep(project, dep):
    hashes = []

    index_url = "https://pypi.org/simple/"
    source = "pypi"
    for source in project.sources.all:
        if source.get("name") == dep.get("index"):
            index_url = source.get("url")
            break

    # 1 Try to get hashes directly form index
    install_req, markers, _ = install_req_from_pipfile(dep["name"], dep)
    if not install_req or not install_req.req:
        return []
    if "https://pypi.org/simple/" in index_url:
        hashes = project.sources.get_hashes_from_pypi(install_req, source)
    elif index_url:
        hashes = project.sources.get_hashes_from_remote_index_urls(install_req, source)
    if hashes:
        return hashes

    return []


def extract_vcs_url(vcs_url):
    # Remove leading/trailing whitespace
    vcs_url = vcs_url.strip()

    # Check if it's a file URI
    parsed = urlparse(vcs_url)
    if parsed.scheme == "file":
        # For file URIs, we want to keep the entire URL intact
        return vcs_url

    # Remove the package name and '@' if present at the start
    if "@" in vcs_url and not vcs_url.startswith(tuple(f"{vcs}+" for vcs in VCS_LIST)):
        vcs_url = vcs_url.split("@", 1)[1]

    # Remove the VCS prefix (e.g., 'git+')
    for prefix in VCS_LIST:
        vcs_prefix = f"{prefix}+"
        if vcs_url.startswith(vcs_prefix):
            vcs_url = vcs_url[len(vcs_prefix) :]
            break

    # Parse the URL
    parsed = urlparse(vcs_url)

    # Reconstruct the URL, preserving authentication details
    clean_url = urlunparse(
        (
            parsed.scheme,
            parsed.netloc,
            parsed.path,
            "",  # params
            "",  # query
            "",  # fragment
        )
    )

    return clean_url


def add_ssh_scheme_to_git_uri(uri: str | bytes | None) -> str | bytes | None:
    """Clean VCS URIs from pip format, preserving ``None`` unchanged."""
    if uri is None:
        return None
    if isinstance(uri, str):
        # Add scheme for parsing purposes, this is also what pip does
        if uri.startswith("git+") and "://" not in uri:
            uri = uri.replace("git+", "git+ssh://", 1)
            parsed = urlparse(uri)
            if ":" in parsed.netloc:
                netloc, _, path_start = parsed.netloc.rpartition(":")
                path = f"/{path_start}{parsed.path}"
                uri = urlunparse(parsed._replace(netloc=netloc, path=path))
    return uri


def is_vcs(
    pipfile_entry: Mapping[str, Any] | str | None,
) -> bool:
    """Determine if dictionary entry from Pipfile is for a vcs dependency."""
    if isinstance(pipfile_entry, Mapping):
        return any(key for key in pipfile_entry if key in VCS_LIST)

    elif isinstance(pipfile_entry, str):
        if not is_valid_url(pipfile_entry) and pipfile_entry.startswith("git+"):
            pipfile_entry = add_ssh_scheme_to_git_uri(pipfile_entry)

        parsed_entry = urlsplit(pipfile_entry)
        return parsed_entry.scheme in VCS_SCHEMES
    return False


def _file_url_to_relative_path(file_url, base_dir):
    """Convert an absolute ``file://`` URL to a path relative to *base_dir*.

    pip resolves transitive local-file/path sub-dependencies to absolute
    ``file://`` URLs.  The lockfile should store a relative path (matching
    what top-level Pipfile entries carry) so it stays portable across
    machines.  See https://github.com/pypa/pipenv/issues/6119.

    Returns *file_url* unchanged when it is not a ``file://`` URL (e.g. an
    already-relative path, an HTTP URL, or a VCS URL).
    """
    if not isinstance(file_url, str) or not file_url.startswith("file://"):
        return file_url
    from urllib.request import url2pathname

    try:
        parsed = urlparse(file_url)
        local_path = url2pathname(parsed.path)
        rel = os.path.relpath(local_path, base_dir)
        # Normalise to forward slashes for cross-platform lockfile portability.
        return rel.replace(os.sep, "/") if os.sep != "/" else rel
    except (ValueError, TypeError):
        # os.path.relpath raises ValueError on Windows when source and target
        # are on different drives.  Fall back to the original URL.
        return file_url


def clean_resolved_dep(  # noqa: PLR0912
    project, dep, is_top_level=False, current_entry=None
):
    from pipenv.patched.pip._vendor.packaging.requirements import (
        Requirement as PipRequirement,
    )

    name = dep["name"]
    lockfile = {}

    # Evaluate Markers
    if "markers" in dep and dep.get("markers", "").strip():
        if not is_top_level:
            translated = translate_markers(dep).get("markers", "").strip()
            if translated:
                try:
                    lockfile["markers"] = translated
                except TypeError:
                    pass
        else:
            try:
                pipfile_entry = translate_markers(dep)
                if pipfile_entry.get("markers"):
                    lockfile["markers"] = pipfile_entry.get("markers")
            except TypeError:
                pass

    version = dep.get("version", None)
    if version and not version.startswith("=="):
        version = f"=={version}"
    if version == "==*":
        if current_entry:
            version = current_entry.get("version")
            dep["version"] = version
        else:
            version = None

    # Normalise absolute file:// URLs produced by pip's resolver for transitive
    # local-file/path sub-dependencies to a project-relative path.  Top-level
    # Pipfile entries are already overridden with their relative path by
    # get_locked_dep(); only sub-dependencies reach here with a raw
    # "file:///..." URL.  (GH-6119)
    pipfile = getattr(project, "pipfile", None)
    project_dir = getattr(pipfile, "project_directory", None) if pipfile else None
    if project_dir and isinstance(dep.get("file"), str):
        dep["file"] = _file_url_to_relative_path(dep["file"], project_dir)

    is_vcs_or_file = False
    for vcs_type in VCS_LIST:
        if vcs_type in dep:
            vcs_url = dep[vcs_type]
            if "[" in vcs_url and "]" in vcs_url:
                extras_section = vcs_url.split("[").pop().replace("]", "")
                lockfile["extras"] = sorted(
                    [extra.strip() for extra in extras_section.split(",")]
                )

            lockfile[vcs_type] = vcs_url
            lockfile["ref"] = dep.get("ref")
            if "subdirectory" in dep:
                lockfile["subdirectory"] = dep["subdirectory"]
            is_vcs_or_file = True

    if "editable" in dep:
        lockfile["editable"] = dep["editable"]

    preferred_file_keys = ["path", "file"]
    dependency_file_key = next(iter(k for k in preferred_file_keys if k in dep), None)
    if dependency_file_key:
        lockfile[dependency_file_key] = dep[dependency_file_key]
        is_vcs_or_file = True
        # editable is already written above; no need to repeat here

    if version and not is_vcs_or_file:
        if isinstance(version, PipRequirement):
            if version.specifier:
                lockfile["version"] = str(version.specifier)
            if version.extras:
                lockfile["extras"] = sorted(version.extras)
        elif version:
            lockfile["version"] = version

    if dep.get("hashes"):
        lockfile["hashes"] = dep["hashes"]
    elif is_top_level:
        potential_hashes = unearth_hashes_for_dep(project, dep)
        if potential_hashes:
            lockfile["hashes"] = potential_hashes

    if dep.get("index"):
        lockfile["index"] = dep["index"]

    if dep.get("extras"):
        lockfile["extras"] = sorted(dep["extras"])

    # Preserve the no_binary flag so pipenv can re-apply --no-binary on future installs
    if dep.get("no_binary"):
        lockfile["no_binary"] = True

    # In case we lock a uri or a file when the user supplied a path
    # remove the uri or file keys from the entry and keep the path
    if dep and isinstance(dep, dict):
        for k in preferred_file_keys:
            if k in dep.keys():
                lockfile[k] = dep[k]
                break

    if "markers" in dep:
        markers = dep["markers"]
        if markers:
            markers = Marker(markers)
            if not markers.evaluate() and current_entry:
                current_entry.update(lockfile)
                return {name: current_entry}

    return {name: lockfile}


def as_pipfile(dep: InstallRequirement) -> Dict[str, Any]:
    """Create a pipfile entry for the given InstallRequirement."""
    pipfile_dict = {}
    name = dep.name
    version = dep.req.specifier

    # Construct the pipfile entry
    pipfile_dict[name] = {
        "version": str(version),
        "editable": dep.editable,
        "extras": list(dep.extras),
    }

    if dep.link:
        # If it's a VCS link
        if dep.link.is_vcs:
            vcs = dep.link.scheme.split("+")[0]
            pipfile_dict[name][vcs] = dep.link.url_without_fragment
        # If it's a URL link
        elif dep.link.scheme.startswith("http"):
            pipfile_dict[name]["file"] = dep.link.url_without_fragment
        # If it's a local file
        elif dep.link.is_file:
            pipfile_dict[name]["path"] = dep.link.file_path

    # Convert any markers to their string representation
    if dep.markers:
        pipfile_dict[name]["markers"] = str(dep.markers)

    # If a hash is available, add it to the pipfile entry
    if dep.hash_options:
        pipfile_dict[name]["hashes"] = dep.hash_options

    return pipfile_dict


def is_star(val):
    return isinstance(val, str) and val == "*"


def is_pinned(val):
    if isinstance(val, Mapping):
        val = val.get("version")
    return isinstance(val, str) and val.startswith("==")


def is_pinned_requirement(ireq):
    """
    Returns whether an InstallRequirement is a "pinned" requirement.
    """
    if ireq.editable:
        return False

    if ireq.req is None or len(ireq.specifier) != 1:
        return False

    spec = next(iter(ireq.specifier))
    return spec.operator in {"==", "==="} and not spec.version.endswith(".*")


def is_editable_path(path):
    """
    Determine if a path is editable by checking if it's a directory.

    :param path: A path as string or Path object
    :return: True if the path is a directory, False otherwise
    """
    return Path(path).is_dir()


def dependency_as_pip_install_line(
    dep_name: str,
    dep: Union[str, Mapping],
    include_hashes: bool,
    include_markers: bool,
    include_index: bool,
    indexes: list,
    constraint: bool = False,
):
    if isinstance(dep, str):
        if is_star(dep):
            return dep_name
        elif not COMPARE_OP.match(dep):
            return f"{dep_name}=={dep}"
        return f"{dep_name}{dep}"
    # Translate shorthand marker keys (e.g. sys_platform, platform_machine, os_name)
    # into the canonical "markers" key so that the pip line includes them and the
    # resolver / installer can evaluate them.  See #5884.
    dep = translate_markers(dep)
    line = []
    is_constraint = False
    vcs = next(iter([vcs for vcs in VCS_LIST if vcs in dep]), None)
    if not vcs:
        for k in ["file", "path"]:
            if k in dep:
                extras = ""
                if "extras" in dep:
                    extras = f"[{','.join(dep['extras'])}]"
                location = dep["file"] if "file" in dep else dep["path"]
                # Prepend -e for editable local-path deps.  We intentionally
                # avoid an is_editable_path() filesystem check here because:
                #  1. The directory may not exist in the current environment
                #     (e.g. CI machines that only run `pipenv sync`).
                #  2. The `editable` flag in the dep dict is the authoritative
                #     source of truth, set at `pipenv install` time.
                # Remote HTTP/HTTPS URLs are never editable.
                if dep.get("editable") and not location.startswith(
                    ("http:", "https:", "ftp:")
                ):
                    line.append("-e")
                if location.startswith(("http:", "https:")):
                    req_str = f"{dep_name}{extras} @ {location}"
                else:
                    req_str = f"{location}{extras}"
                # Add markers for file/path dependencies
                if include_markers and dep.get("markers"):
                    req_str = f'{req_str}; {dep["markers"]}'
                line.append(req_str)
                break
        else:
            # Normal/Named Requirements
            is_constraint = True
            line.append(dep_name)
            if "extras" in dep:
                line[-1] += f"[{','.join(dep['extras'])}]"
            if "version" in dep:
                version = dep["version"]
                if version and not is_star(version):
                    if not COMPARE_OP.match(version):
                        version = f"=={version}"
                    line[-1] += version
            if include_markers and dep.get("markers"):
                line[-1] = f'{line[-1]}; {dep["markers"]}'

            if include_hashes and dep.get("hashes"):
                line.extend([f" --hash={hash}" for hash in dep["hashes"]])

            if include_index:
                if dep.get("index"):
                    indexes = [s for s in indexes if s.get("name") == dep["index"]]
                else:
                    indexes = [indexes[0]] if indexes else []
            index_list = prepare_pip_source_args(indexes)
            line.extend(index_list)
    elif vcs and vcs in dep:  # VCS Requirements
        extras = ""
        ref = ""
        if dep.get("ref"):
            ref = f"@{dep['ref']}"
        if "extras" in dep:
            extras = f"[{','.join(dep['extras'])}]"
        include_vcs = "" if f"{vcs}+" in dep[vcs] else f"{vcs}+"
        vcs_url = dep[vcs]
        # legacy format is the only format supported for editable installs https://github.com/pypa/pip/issues/9106
        if is_editable_path(dep[vcs]) or "file://" in dep[vcs]:
            if "#egg=" not in dep[vcs]:
                # Extras must not be in the #egg= fragment (pip validates it as
                # a PEP 508 project name). Append extras after the egg fragment.
                git_req = f"-e {include_vcs}{dep[vcs]}{ref}#egg={dep_name}"
            else:
                git_req = f"-e {include_vcs}{dep[vcs]}{ref}"
            if "subdirectory" in dep:
                git_req += f"&subdirectory={dep['subdirectory']}"
            git_req += extras
            # Note: Legacy -e format doesn't support inline markers
            # Markers are handled separately in the resolution process
        else:
            if "#egg=" in vcs_url:
                vcs_url = vcs_url.split("#egg=")[0]
            git_req = f"{dep_name}{extras} @ {include_vcs}{vcs_url}{ref}"
            if "subdirectory" in dep:
                git_req += f"#subdirectory={dep['subdirectory']}"
            # Add markers for VCS dependencies (PEP 508 format supports this)
            if include_markers and dep.get("markers"):
                git_req = f'{git_req}; {dep["markers"]}'

        line.append(git_req)

    if constraint and not is_constraint:
        pip_line = ""
    else:
        pip_line = " ".join(line)
    return pip_line


def convert_deps_to_pip(
    deps,
    indexes=None,
    include_hashes=True,
    include_markers=True,
    include_index=False,
):
    """ "Converts a Pipfile-formatted dependency to a pip-formatted one."""
    dependencies = {}
    if indexes is None:
        indexes = []
    for dep_name, dep in deps.items():
        req = dependency_as_pip_install_line(
            dep_name, dep, include_hashes, include_markers, include_index, indexes
        )
        dependencies[dep_name] = req
    return dependencies


def parse_metadata_file(content: str):
    """
    Parse a METADATA file to get the package name.

    Parameters:
    content (str): Contents of the METADATA file.

    Returns:
    str: Name of the package or None if not found.
    """

    for line in content.splitlines():
        if line.startswith("Name:"):
            return line.split("Name: ")[1].strip()

    return None


def parse_pkginfo_file(content: str):
    """
    Parse a PKG-INFO file to get the package name.

    Parameters:
    content (str): Contents of the PKG-INFO file.

    Returns:
    str: Name of the package or None if not found.
    """
    for line in content.splitlines():
        if line.startswith("Name:"):
            return line.split("Name: ")[1].strip()

    return None


def parse_setup_file(content):
    # A dictionary to store variable names and their values
    variables = {}
    try:
        tree = ast.parse(content)
        for node in ast.walk(tree):
            # Extract variable assignments and store them
            if isinstance(node, ast.Assign):
                for target in node.targets:
                    if isinstance(target, ast.Name):
                        if isinstance(node.value, ast.Constant) and isinstance(
                            node.value.value, str
                        ):
                            variables[target.id] = node.value.value

            # Check function calls to extract the 'name' attribute from the setup function
            if isinstance(node, ast.Call):
                if (
                    getattr(node.func, "id", "") == "setup"
                    or isinstance(node.func, ast.Attribute)
                    and node.func.attr == "setup"
                ):
                    for keyword in node.keywords:
                        if keyword.arg == "name":
                            # If it's a variable, retrieve its value
                            if (
                                isinstance(keyword.value, ast.Name)
                                and keyword.value.id in variables
                            ):
                                return variables[keyword.value.id]
                            # Otherwise, check if it's directly provided
                            elif isinstance(keyword.value, ast.Constant) and isinstance(
                                keyword.value.value, str
                            ):
                                return keyword.value.value
                            # Additional handling for Python versions and specific ways of defining the name
                            elif sys.version_info < (3, 9) and isinstance(
                                keyword.value, ast.Subscript
                            ):
                                if (
                                    isinstance(keyword.value.value, ast.Name)
                                    and keyword.value.value.id == "about"
                                ):
                                    if isinstance(
                                        keyword.value.slice, ast.Index
                                    ) and isinstance(
                                        keyword.value.slice.value, ast.Constant
                                    ):
                                        if isinstance(
                                            keyword.value.slice.value.value, str
                                        ):
                                            return keyword.value.slice.value.value
                                # Handle ast.Constant for the subscript value
                                if isinstance(keyword.value, ast.Constant) and isinstance(
                                    keyword.value.value, str
                                ):
                                    return keyword.value.value
                            elif sys.version_info >= (3, 9) and isinstance(
                                keyword.value, ast.Subscript
                            ):
                                if (
                                    isinstance(keyword.value.value, ast.Name)
                                    and isinstance(keyword.value.slice, ast.Constant)
                                    and keyword.value.value.id == "about"
                                ):
                                    if isinstance(keyword.value.slice.value, str):
                                        return keyword.value.slice.value
    except ValueError:
        pass  # We will not exec unsafe code to determine the name pre-resolver

    return None


def parse_cfg_file(content):
    config = configparser.ConfigParser()
    config.read_string(content)
    try:
        return config["metadata"]["name"]
    except configparser.NoSectionError:
        return None
    except KeyError:
        return None


def parse_toml_file(content):
    toml_dict = tomli.loads(content)
    if "project" in toml_dict and "name" in toml_dict["project"]:
        return toml_dict["project"]["name"]
    if "tool" in toml_dict and "poetry" in toml_dict["tool"]:
        return toml_dict["tool"]["poetry"]["name"]

    return None


def find_package_name_from_tarball(tarball_filepath):
    if tarball_filepath.startswith("file://") and os.name != "nt":
        tarball_filepath = tarball_filepath[7:]
    with tarfile.open(tarball_filepath, "r") as tar_ref:
        for filename in tar_ref.getnames():
            if filename.endswith(RELEVANT_PROJECT_FILES):
                with tar_ref.extractfile(filename) as file:
                    possible_name = find_package_name_from_filename(filename, file)
                    if possible_name:
                        return possible_name


def find_package_name_from_zipfile(zip_filepath):
    if zip_filepath.startswith("file://") and os.name != "nt":
        zip_filepath = zip_filepath[7:]
    with zipfile.ZipFile(zip_filepath, "r") as zip_ref:
        for filename in zip_ref.namelist():
            if filename.endswith(RELEVANT_PROJECT_FILES):
                with zip_ref.open(filename) as file:
                    possible_name = find_package_name_from_filename(file.name, file)
                    if possible_name:
                        return possible_name


def find_package_name_from_directory(directory):
    parsed_url = urlparse(directory)
    # On Windows, urlparse interprets 'C:\path' as scheme='c', path='\path'
    # We need to detect this case and use the original directory string
    if parsed_url.scheme and len(parsed_url.scheme) == 1 and os.name == "nt":
        # Single-letter scheme on Windows is likely a drive letter (C:, D:, etc.)
        directory_path = Path(directory)
    elif parsed_url.scheme:
        directory_path = Path(parsed_url.path)
    else:
        directory_path = Path(directory)

    # Handle egg fragment for direct dependencies
    if "#egg=" in str(directory_path):
        expected_name = str(directory_path).split("#egg=")[1]
        return expected_name

    # Windows path normalization for file:// URLs
    directory_str = str(directory_path)
    if os.name == "nt":
        if directory_str.startswith("\\") and (
            ":\\" in directory_str or ":/" in directory_str
        ):
            directory_path = Path(directory_str[1:])
        if directory_str.startswith("\\\\"):
            directory_path = Path(directory_str[1:])

    # Metadata files that can be found in .egg-info or .dist-info directories
    # Note: setup.py, setup.cfg, and pyproject.toml are only checked in the root
    # directory (via RELEVANT_PROJECT_FILES). We only recurse into .egg-info and
    # .dist-info directories for METADATA/PKG-INFO files.
    metadata_files = ("METADATA", "PKG-INFO")

    try:
        # Sort contents - files first, then directories
        directory_contents = sorted(
            directory_path.iterdir(), key=lambda x: (x.is_dir(), x.name)
        )

        for path in directory_contents:
            if path.is_file():
                if path.name.endswith(RELEVANT_PROJECT_FILES):
                    with path.open("rb") as file:
                        possible_name = find_package_name_from_filename(path.name, file)
                        if possible_name:
                            return possible_name
            elif path.is_dir():
                # Only recurse into .egg-info or .dist-info directories for metadata
                # Do NOT recurse into other directories (e.g., tests/, src/) to avoid
                # picking up setup() calls from test files or other non-project files
                if path.name.endswith((".egg-info", ".dist-info")):
                    for metadata_path in path.iterdir():
                        if (
                            metadata_path.is_file()
                            and metadata_path.name in metadata_files
                        ):
                            with metadata_path.open("rb") as file:
                                possible_name = find_package_name_from_filename(
                                    metadata_path.name, file
                                )
                                if possible_name:
                                    return possible_name
    except (FileNotFoundError, PermissionError):
        # Handle cases where the directory doesn't exist or isn't accessible
        pass

    return None


def ensure_path_is_relative(file_path):
    """Return *file_path* as a path relative to the current working directory.

    Local paths that are the same directory as, or a subdirectory of, the
    current working directory are prefixed with ``./`` so that pip and pipenv
    always treat them as local filesystem paths rather than package names.
    Paths that traverse upward (``../sibling``) are returned as-is because
    they already carry the necessary ``..`` prefix.
    """
    abs_path = Path(file_path).resolve()
    current_dir = Path.cwd()

    # On Windows, different drives cannot share a relative path.
    if abs_path.drive != current_dir.drive:
        return str(abs_path)

    try:
        # Normalise to forward slashes for cross-platform pip compatibility.
        rel = abs_path.relative_to(current_dir).as_posix()
        # "." means the current directory itself — leave it unchanged.
        # Paths starting with ".." are already unambiguous.
        # Everything else (same-dir or subdirectory) gets a "./" prefix so
        # pip treats it as a filesystem path, not a bare package name.
        if rel != "." and not rel.startswith(".."):
            rel = "./" + rel
        return rel
    except ValueError:
        # Path is not a subdirectory of current_dir — compute manually.
        common_parts = 0
        for part_a, part_b in zip(abs_path.parts, current_dir.parts):
            if part_a == part_b:
                common_parts += 1
            else:
                break

        up_levels = [".."] * (len(current_dir.parts) - common_parts)
        rel_parts = up_levels + list(abs_path.parts[common_parts:])
        relative_path = Path(*rel_parts)
        # Use POSIX separators to stay consistent with the try branch above.
        return relative_path.as_posix()


def determine_path_specifier(package: InstallRequirement):
    if package.link:
        if package.link.scheme in ["http", "https"]:
            return package.link.url_without_fragment
        if package.link.scheme == "file":
            return ensure_path_is_relative(package.link.file_path)


def determine_vcs_specifier(package: InstallRequirement):
    if package.link and package.link.scheme in VCS_SCHEMES:
        vcs_specifier = package.link.url_without_fragment
        return vcs_specifier


def get_vcs_backend(vcs_type):
    from pipenv.patched.pip._internal.vcs.versioncontrol import VcsSupport

    backend = VcsSupport().get_backend(vcs_type)
    return backend


def generate_temp_dir_path():
    # Create a temporary directory using mkdtemp
    temp_dir = tempfile.mkdtemp()
    # Remove the created directory
    os.rmdir(temp_dir)
    return temp_dir


def get_pip_command() -> "InstallCommand":
    """Get pip's InstallCommand for configuration management and defaults."""
    # Use pip's parser for pip.conf management and defaults.
    # General options (find_links, index_url, extra_index_url, trusted_host,
    # and pre) are deferred to pip.
    from pipenv.patched.pip._internal.commands.install import InstallCommand

    pip_command = InstallCommand(
        name="InstallCommand", summary="pipenv pip Install command."
    )
    return pip_command


def determine_vcs_revision_hash(
    package: InstallRequirement, vcs_type: str, revision: str
):
    from pipenv.patched.pip._internal.utils.misc import hide_url

    try:  # Windows python 3.7 will sometimes raise PermissionError cleaning up
        checkout_directory = generate_temp_dir_path()
        repo_backend = get_vcs_backend(vcs_type)
        repo_backend.obtain(checkout_directory, hide_url(package.link.url), verbosity=1)
        return repo_backend.get_revision(checkout_directory)
    except Exception as e:
        err.print(
            f"Error {e} obtaining {vcs_type} revision hash for {package}; falling back to {revision}."
        )
        return revision


@lru_cache(maxsize=None)
def determine_package_name(package: InstallRequirement):
    req_name = None
    if package.name:
        req_name = package.name
    elif "#egg=" in str(package):
        req_name = str(package).split("#egg=")[1]
        req_name = req_name.split("[")[0]
    elif " @ " in str(package):
        req_name = str(package).split("@ ")[0]
        req_name = req_name.split("[")[0]
    elif package.link and package.link.scheme in REMOTE_SCHEMES:
        from pipenv.patched.pip._internal.network.download import Downloader
        from pipenv.utils.unpack import unpack_url

        try:  # Windows python 3.7 will sometimes raise PermissionError cleaning up
            with TemporaryDirectory() as td:
                cmd = get_pip_command()
                options, _ = cmd.parser.parse_args([])
                session = cmd._build_session(options)
                session.resume_retries = 5
                local_file = unpack_url(
                    link=package.link,
                    location=td,
                    download=Downloader(session, "off"),
                    verbosity=1,
                )
                if local_file.path.endswith(".whl") or local_file.path.endswith(".zip"):
                    req_name = find_package_name_from_zipfile(local_file.path)
                elif local_file.path.endswith(".tar.gz") or local_file.path.endswith(
                    ".tar.bz2"
                ):
                    req_name = find_package_name_from_tarball(local_file.path)
                else:
                    req_name = find_package_name_from_directory(local_file.path)
        except PermissionError:
            pass
    elif package.link and package.link.scheme in [
        "bzr+file",
        "git+file",
        "hg+file",
        "svn+file",
    ]:
        parsed_url = urlparse(package.link.url)
        repository_path = parsed_url.path
        repository_path = repository_path.rsplit("@", 1)[
            0
        ]  # extract the actual directory path
        repository_path = repository_path.split("#egg=")[0]
        req_name = find_package_name_from_directory(repository_path)
    elif package.link and package.link.scheme == "file":
        if package.link.file_path.endswith(".whl") or package.link.file_path.endswith(
            ".zip"
        ):
            req_name = find_package_name_from_zipfile(package.link.file_path)
        elif package.link.file_path.endswith(
            ".tar.gz"
        ) or package.link.file_path.endswith(".tar.bz2"):
            req_name = find_package_name_from_tarball(package.link.file_path)
        else:
            req_name = find_package_name_from_directory(package.link.file_path)
    if req_name:
        return req_name
    else:
        raise ValueError(f"Could not determine package name from {package}")


def find_package_name_from_filename(filename, file):
    # Extract basename to handle both full paths (from archives) and basenames (from directories)
    basename = Path(filename).name

    if basename == "METADATA":
        content = file.read().decode()
        possible_name = parse_metadata_file(content)
        if possible_name:
            return possible_name

    if basename == "PKG-INFO":
        content = file.read().decode()
        possible_name = parse_pkginfo_file(content)
        if possible_name:
            return possible_name

    if basename == "setup.py":
        content = file.read().decode()
        possible_name = parse_setup_file(content)
        if possible_name:
            return possible_name

    if basename == "setup.cfg":
        content = file.read().decode()
        possible_name = parse_cfg_file(content)
        if possible_name:
            return possible_name

    if basename == "pyproject.toml":
        content = file.read().decode()
        possible_name = parse_toml_file(content)
        if possible_name:
            return possible_name
    return None


def create_link(link):
    # type: (AnyStr) -> Link
    from pipenv.patched.pip._internal.models.link import Link

    if not isinstance(link, str):
        raise TypeError("must provide a string to instantiate a new link")

    return Link(link)


def get_link_from_line(line):
    """Parse link information from given requirement line. Return a
    6-tuple:

    - `vcs_type` indicates the VCS to use (e.g. "git"), or None.
    - `prefer` is either "file", "path" or "uri", indicating how the
        information should be used in later stages.
    - `relpath` is the relative path to use when recording the dependency,
        instead of the absolute path/URI used to perform installation.
        This can be None (to prefer the absolute path or URI).
    - `path` is the absolute file path to the package. This will always use
        forward slashes. Can be None if the line is a remote URI.
    - `uri` is the absolute URI to the package. Can be None if the line is
        not a URI.
    - `link` is an instance of :class:`pipenv.patched.pip._internal.index.Link`,
        representing a URI parse result based on the value of `uri`.
    This function is provided to deal with edge cases concerning URIs
    without a valid netloc. Those URIs are problematic to a straight
    ``urlsplit` call because they cannot be reliably reconstructed with
    ``urlunsplit`` due to a bug in the standard library:
    >>> from urllib.parse import urlsplit, urlunsplit
    >>> urlunsplit(urlsplit('git+file:///this/breaks'))
    'git+file:/this/breaks'
    >>> urlunsplit(urlsplit('file:///this/works'))
    'file:///this/works'
    See `https://bugs.python.org/issue23505#msg277350`.
    """

    # Git allows `git@github.com...` lines that are not really URIs.
    # Add "ssh://" so we can parse correctly, and restore afterward.
    fixed_line = add_ssh_scheme_to_git_uri(line)  # type: str

    # We can assume a lot of things if this is a local filesystem path.
    if "://" not in fixed_line:
        p = Path(fixed_line).absolute()  # type: Path
        p.as_posix()  # type: Optional[str]
        uri = p.as_uri()  # type: str
        link = create_link(uri)  # type: Link
        return link

    # This is an URI. We'll need to perform some elaborated parsing.
    parsed_url = urlsplit(fixed_line)  # type: SplitResult

    # Split the VCS part out if needed.
    original_scheme = parsed_url.scheme  # type: str
    if "+" in original_scheme:
        vcs_type, _, scheme = original_scheme.partition("+")
        parsed_url = parsed_url._replace(scheme=scheme)  # type: ignore
    else:
        pass

    # Re-attach VCS prefix to build a Link.
    link = create_link(
        urlunsplit(parsed_url._replace(scheme=original_scheme))  # type: ignore
    )

    return link


def has_name_with_extras(requirement):
    pattern = r"^([a-zA-Z0-9_-]+(\[[a-zA-Z0-9_-]+\])?) @ .*"
    match = re.match(pattern, requirement)
    return match is not None


def expand_env_variables(line) -> AnyStr:
    """Expand the env vars in a line following pip's standard.
    https://pip.pypa.io/en/stable/reference/pip_install/#id10.

    Matches environment variable-style values in '${MY_VARIABLE_1}' with
    the variable name consisting of only uppercase letters, digits or
    the '_'
    """

    def replace_with_env(match):
        value = os.getenv(match.group(1))
        return value if value else match.group()

    return re.sub(r"\$\{([A-Z0-9_]+)\}", replace_with_env, line)


def expansive_install_req_from_line(
    pip_line: str,
    comes_from: Optional[Union[str, InstallRequirement]] = None,
    *,
    use_pep517: Optional[bool] = None,
    isolated: bool = False,
    global_options: Optional[List[str]] = None,
    hash_options: Optional[Dict[str, List[str]]] = None,
    constraint: bool = False,
    line_source: Optional[str] = None,
    user_supplied: bool = False,
    config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
    expand_env: bool = False,
) -> (InstallRequirement, str):
    """Create an InstallRequirement from a pip-style requirement line.
    InstallRequirement is a pip internal construct that represents an installable requirement,
    and is used as an intermediary between the pip command and the resolver.
    :param pip_line: A pip-style requirement line.
    :param comes_from: The path to the requirements file the line was found in.
    :param use_pep517: Whether to use PEP 517/518 when installing the
        requirement.
    :param isolated: Whether to isolate the requirements when installing them. (likely unused)
    :param global_options: Extra global options to be used when installing the install req (likely unused)
    :param hash_options: Extra hash options to be used when installing the install req (likely unused)
    :param constraint: Whether the requirement is a constraint.
    :param line_source: The source of the line (e.g. "requirements.txt").
    :param user_supplied: Whether the requirement was directly provided by the user.
    :param config_settings: Configuration settings to be used when installing the install req (likely unused)
    :param expand_env: Whether to expand environment variables in the line. (definitely used)
    :return: A tuple of the InstallRequirement and the name of the package (if determined).
    """
    from pipenv.patched.pip._internal.req.constructors import (
        install_req_from_editable,
        parse_req_from_line,
    )

    name = None
    # Only strip outer quotes if the entire line is quoted, not internal quotes
    pip_line = pip_line.lstrip(" ")
    if pip_line.startswith("'") and pip_line.endswith("'") and pip_line.count("'") == 2:
        pip_line = pip_line[1:-1]

    # Handle paths with escaped spaces
    if os.path.exists(os.path.expanduser(pip_line.replace("\\ ", " "))):
        pip_line = pip_line.replace("\\ ", " ")

    for new_req_symbol in ("@ ", " @ "):  # Check for new style pip lines
        if new_req_symbol in pip_line:
            pip_line_parts = pip_line.split(new_req_symbol, 1)
            name = pip_line_parts[0]
            pip_line = pip_line_parts[1]
    if pip_line.startswith("-e "):  # Editable requirements
        pip_line = pip_line.split("-e ")[1]
        return install_req_from_editable(pip_line, line_source), name

    if expand_env:
        pip_line = expand_env_variables(pip_line)

    vcs_part = pip_line
    vcs_markers = None
    for vcs in VCS_LIST:
        if vcs_part.startswith(f"{vcs}+"):
            # Extract markers from VCS line if present (e.g., "git+https://...@ref; sys_platform == 'win32'")
            if "; " in vcs_part:
                vcs_url, markers_str = vcs_part.split("; ", 1)
                from pipenv.patched.pip._vendor.packaging.markers import Marker

                vcs_markers = Marker(markers_str.strip())
                vcs_part = vcs_url
            link = get_link_from_line(vcs_part)
            install_req = InstallRequirement(
                None,
                comes_from,
                link=link,
                markers=vcs_markers,
                isolated=isolated,
                hash_options=hash_options,
                constraint=constraint,
                user_supplied=user_supplied,
            )
            return install_req, name
    if urlparse(pip_line).scheme in ("http", "https", "file") or any(
        pip_line.endswith(s) for s in INSTALLABLE_EXTENSIONS
    ):
        parts = parse_req_from_line(pip_line, line_source)
    else:
        # It's a requirement
        if "--index" in pip_line:
            pip_line = pip_line.split("--index")[0]
        if " -i " in pip_line:
            pip_line = pip_line.split(" -i ")[0]
        # handle local version identifiers (like the ones torch uses in their public index)
        if "+" in pip_line:
            pip_line = pip_line.split("+")[0]
        parts = parse_req_from_line(pip_line, line_source)

    install_req = InstallRequirement(
        parts.requirement,
        comes_from,
        link=parts.link,
        markers=parts.markers,
        isolated=isolated,
        hash_options=hash_options,
        config_settings=config_settings,
        constraint=constraint,
        extras=parts.extras,
        user_supplied=user_supplied,
    )
    return install_req, name


def normalize_editable_path_for_pip(path_str):
    """Normalize an editable package path for pip."""
    # Windows paths need to be converted to POSIX paths otherwise path
    # separators (back slashes) are interpreted as escape characters.
    return path_str.replace(os.path.sep, "/")


def file_path_from_pipfile(path_str, pipfile_entry):
    """Creates an installable file path from a pipfile entry.
    Handles local and remote paths, files and directories;
    supports extras and editable specification.
    Outputs a pip installable line.
    """
    if path_str.startswith(("http:", "https:", "ftp:")):
        req_str = path_str
    else:
        req_str = ensure_path_is_relative(path_str)

    if pipfile_entry.get("extras"):
        req_str = f"{req_str}[{','.join(pipfile_entry['extras'])}]"
    if pipfile_entry.get("editable", False):
        req_str = f"-e {normalize_editable_path_for_pip(req_str)}"

    return req_str


def normalize_vcs_url(vcs_url):
    """Return vcs_url and possible vcs_ref from a given vcs_url."""
    # We have to handle the fact that some vcs urls have a ref in them
    # and some have a netloc with a username and password in them, and some have both
    vcs_ref = ""
    if "@" in vcs_url:
        parsed_url = urlparse(vcs_url)
        if "@" in parsed_url.path:
            url_parts = vcs_url.rsplit("@", 1)
            vcs_url = url_parts[0]
            vcs_ref = url_parts[1]
    return vcs_url, vcs_ref


class VCSURLProcessor:
    """Handles processing and environment variable expansion in VCS URLs."""

    ENV_VAR_PATTERN = re.compile(r"\${([^}]+)}|\$([a-zA-Z_][a-zA-Z0-9_]*)")

    @classmethod
    def expand_env_vars(cls, value: str) -> str:
        """
        Expands environment variables in a string, with detailed error handling.
        Supports both ${VAR} and $VAR syntax.
        """

        def _replace_var(match):
            var_name = match.group(1) or match.group(2)
            if var_name not in os.environ:
                raise PipenvUsageError(
                    f"Environment variable '${var_name}' not found. "
                    "Please ensure all required environment variables are set."
                )
            return os.environ[var_name]

        try:
            return cls.ENV_VAR_PATTERN.sub(_replace_var, value)
        except Exception as e:
            raise PipenvUsageError(f"Error expanding environment variables: {str(e)}")

    @classmethod
    def process_vcs_url(cls, url: str) -> str:
        """
        Processes a VCS URL, expanding environment variables in individual components.
        Handles URLs of the form: vcs+protocol://username:password@hostname/path
        """
        parsed = urlparse(url)

        # Process each component separately
        netloc_parts = parsed.netloc.split("@")
        if len(netloc_parts) > 1:
            # Handle auth information
            auth, host = netloc_parts
            if ":" in auth:
                username, password = auth.split(":")
                username = cls.expand_env_vars(username)
                password = cls.expand_env_vars(password)
                auth = f"{username}:{password}"
            else:
                auth = cls.expand_env_vars(auth)
            netloc = f"{auth}@{host}"
        else:
            netloc = cls.expand_env_vars(parsed.netloc)

        # Reconstruct URL with processed components
        processed_parts = list(parsed)
        processed_parts[1] = netloc  # Update netloc
        processed_parts[2] = cls.expand_env_vars(parsed.path)  # Update path

        return urlunparse(tuple(processed_parts))


def _validate_pipfile_entry(name: str, pipfile_dict: Dict[str, Any]) -> None:
    """Warn or raise when a Pipfile entry contains unrecognised keys.

    This catches common mistakes such as writing ``commit = "hash"`` instead
    of ``ref = "hash"`` for VCS dependencies.
    """
    from pipenv.vendor.plette.models.packages import KNOWN_PACKAGE_KEYS

    unknown = set(pipfile_dict.keys()) - KNOWN_PACKAGE_KEYS
    if unknown:
        raise PipenvUsageError(
            f"Unrecognized option(s) in Pipfile for package '{name}': "
            f"{', '.join(sorted(unknown))}. "
            "Valid options include: version, extras, editable, markers, "
            "ref, git, svn, hg, bzr, path, file, index, subdirectory, "
            "hashes, no_binary, skip_resolver, and PEP 508 marker keys."
        )


def install_req_from_pipfile(name: str, pipfile: Dict[str, Any]) -> Tuple[Any, Any, str]:
    """
    Creates an InstallRequirement from a name and a pipfile entry.
    Enhanced to handle environment variables within VCS URLs.
    """
    _pipfile = {}
    vcs = None

    if hasattr(pipfile, "keys"):
        _pipfile = dict(pipfile).copy()
        # Validate that all keys are recognised before processing.
        _validate_pipfile_entry(name, _pipfile)
    else:
        vcs = next(iter([vcs for vcs in VCS_LIST if pipfile.startswith(f"{vcs}+")]), None)
        if vcs is not None:
            _pipfile[vcs] = pipfile
        else:  # normal named requirement
            _pipfile["version"] = pipfile

    extras = _pipfile.get("extras", [])
    extras_str = f"[{','.join(extras)}]" if extras else ""

    if not vcs:
        vcs = next(iter([vcs for vcs in VCS_LIST if vcs in _pipfile]), None)

    if vcs:
        try:
            vcs_url = _pipfile[vcs]
            subdirectory = _pipfile.get("subdirectory", "")
            if subdirectory:
                subdirectory = f"#subdirectory={subdirectory}"

            # Process VCS URL with environment variable handling
            vcs_url, fallback_ref = normalize_vcs_url(vcs_url)
            ref = _pipfile.get("ref", fallback_ref)

            # Construct requirement string
            req_str = f"{vcs_url}@{ref}{extras_str}"
            if not req_str.startswith(f"{vcs}+"):
                req_str = f"{vcs}+{req_str}"

            if _pipfile.get("editable", False):
                req_str = f"-e {name}{extras_str} @ {req_str}{subdirectory}"
            else:
                req_str = f"{name}{extras_str} @ {req_str}{subdirectory}"

        except PipenvUsageError as e:
            raise PipenvUsageError(
                f"Error processing VCS URL for requirement '{name}': {str(e)}"
            )
    else:
        # Handle non-VCS requirements (unchanged)
        req_str = handle_non_vcs_requirement(name, _pipfile, extras_str)

    # Create InstallRequirement
    install_req, _ = expansive_install_req_from_line(
        req_str,
        comes_from=None,
        use_pep517=False,
        isolated=False,
        hash_options={"hashes": _pipfile.get("hashes", [])},
        constraint=False,
        expand_env=True,
    )

    markers = PipenvMarkers.from_pipfile(name, _pipfile)
    return install_req, markers, req_str


def handle_non_vcs_requirement(
    name: str, _pipfile: Dict[str, Any], extras_str: str
) -> str:
    """Helper function to handle non-VCS requirements."""
    if "path" in _pipfile:
        return file_path_from_pipfile(_pipfile["path"], _pipfile)
    elif "file" in _pipfile:
        return file_path_from_pipfile(_pipfile["file"], _pipfile)
    else:
        version = get_version(_pipfile)
        if is_star(version) or version == "==*":
            version = ""
        elif version and not any(
            version.startswith(op) for op in ("==", ">=", "<=", "~=", "!=", ">", "<")
        ):
            # A bare version like "2.23.0" has no operator; normalise to "==2.23.0"
            # so that pip/the resolver treats it as a pinned constraint rather
            # than silently ignoring it.
            version = f"=={version}"

        req_str = f"{name}{extras_str}{version}"
        markers = PipenvMarkers.from_pipfile(name, _pipfile)
        if markers:
            req_str = f"{req_str};{markers}"
        return req_str


def from_pipfile(name, pipfile):
    install_req, markers, req_str = install_req_from_pipfile(name, pipfile)
    if markers:
        markers = str(markers)
        install_req.markers = Marker(markers)

    # Construct the requirement string for your Requirement class
    extras_str = ""
    if install_req.req and install_req.req.extras:
        extras_str = f"[{','.join(install_req.req.extras)}]"
    specifier = install_req.req.specifier if install_req.req else ""
    req_str = f"{install_req.name}{extras_str}{specifier}"
    if install_req.markers:
        req_str += f"; {install_req.markers}"

    # Create the Requirement instance
    cls_inst = Requirement(req_str)

    return cls_inst


def get_constraints_from_deps(deps):
    """Get constraints from dictionary-formatted dependency"""
    constraints = set()
    for dep_name, dep_version in deps.items():
        c = None
        # Constraints cannot contain extras
        dep_name = dep_name.split("[", 1)[0]
        canonical = canonicalize_name(dep_name)
        # Creating a constraint as a canonical name plus a version specifier
        if isinstance(dep_version, str):
            if dep_version and not is_star(dep_version):
                if COMPARE_OP.match(dep_version) is None:
                    dep_version = f"=={dep_version}"
                c = f"{canonical}{dep_version}"
            else:
                c = canonical
        elif not any(k in dep_version for k in ["path", "file", "uri"]):
            if dep_version.get("skip_resolver") is True:
                continue
            version = dep_version.get("version", None)
            if version and not is_star(version):
                if COMPARE_OP.match(version) is None:
                    version = f"=={version}"
                c = f"{canonical}{version}"
            else:
                c = canonical
        if c:
            constraints.add(c)
    return constraints


def get_constraints_from_resolved_deps(resolved_deps):
    """Get pinned constraints from resolved lockfile data (including transitive deps).

    Unlike get_constraints_from_deps which uses Pipfile specs (only direct deps),
    this extracts exact version pins from already-resolved lockfile entries,
    which includes transitive dependencies.

    :param dict resolved_deps: A lockfile section dict, e.g. lockfile["default"]
    :return: A set of constraint strings like {"sqlalchemy==1.4.5", "greenlet==1.0.0"}
    :rtype: set
    """
    constraints = set()
    for dep_name, dep_info in resolved_deps.items():
        if not isinstance(dep_info, dict):
            continue
        # Skip path/file/VCS deps - they can't be expressed as simple constraints
        if any(k in dep_info for k in ("path", "file", "git", "hg", "svn", "bzr")):
            continue
        version = dep_info.get("version")
        if version:
            canonical = canonicalize_name(dep_name)
            constraints.add(f"{canonical}{version}")
    return constraints


def prepare_constraint_file(
    constraints,
    directory=None,
    sources=None,
    pip_args=None,
):
    if not directory:
        directory = create_tracked_tempdir(suffix="-requirements", prefix="pipenv-")

    constraints = set(constraints)
    constraints_file = NamedTemporaryFile(
        mode="w",
        prefix="pipenv-",
        suffix="-constraints.txt",
        dir=directory,
        delete=False,
    )

    if sources and pip_args:
        skip_args = ("build-isolation", "use-pep517", "cache-dir")
        args_to_add = [
            arg for arg in pip_args if not any(bad_arg in arg for bad_arg in skip_args)
        ]
        requirementstxt_sources = " ".join(args_to_add) if args_to_add else ""
        requirementstxt_sources = requirementstxt_sources.replace(" --", "\n--")
        constraints_file.write(f"{requirementstxt_sources}\n")

    if constraints:
        constraints_file.write("\n".join(constraints))
    constraints_file.close()
    return constraints_file.name


def is_required_version(version, specified_version):
    """Check to see if there's a hard requirement for version
    number provided in the Pipfile.
    """
    # Certain packages may be defined with multiple values.
    if isinstance(specified_version, dict):
        specified_version = specified_version.get("version", "")
    if specified_version.startswith("=="):
        return version.strip() == specified_version.split("==")[1].strip()

    return True


def is_editable(pipfile_entry):
    """Check whether a Pipfile or lockfile package entry is editable.

    Accepts either a mapping (``{"editable": True, ...}``) or a string
    (``"-e ./pkg"``).  Any other input is treated as non-editable.
    """
    if isinstance(pipfile_entry, Mapping):
        return pipfile_entry.get("editable", False) is True
    if isinstance(pipfile_entry, str):
        return pipfile_entry.startswith("-e ")
    return False


@contextmanager
def locked_repository(requirement):
    if not requirement.is_vcs:
        return
    src_dir = create_tracked_tempdir(prefix="pipenv-", suffix="-src")
    with requirement.req.locked_vcs_repo(src_dir=src_dir) as repo:
        yield repo


BAD_PACKAGES = (
    "distribute",
    "pip",
    "pkg-resources",
    "setuptools",
    "wheel",
)


def _merge_into(target, source):
    """Recursively merge ``source`` into ``target`` in place, last-write-wins.

    Mapping-valued keys on both sides are merged recursively; everything
    else (including lists / tomlkit ``Array`` values) is overwritten by
    the value from ``source``. Tomlkit container types
    (``Table``/``InlineTable``) subclass ``dict`` and so are merged like
    any other mapping; the container type already present on ``target``
    is preserved, which is what callers downstream of
    ``tomlkit_value_to_python`` expect.
    """
    for key, value in source.items():
        existing = target.get(key)
        if isinstance(existing, Mapping) and isinstance(value, Mapping):
            _merge_into(existing, value)
        else:
            target[key] = value
    return target


def _new_container_like(mapping):
    """Return a fresh empty mapping that matches ``mapping``'s top-level
    container shape where it's safe to do so.

    Plain dicts (and ``dict`` subclasses that accept a no-arg constructor)
    are recreated as the same class. Tomlkit ``Table``/``InlineTable``
    instances cannot be constructed with no args, so they fall back to
    a plain ``dict`` -- the previous boltons-based implementation also
    handed back a plain dict at the top level for these inputs in the
    real call sites (``dict(packages_table)`` is always taken first).
    """
    cls = mapping.__class__
    if cls is dict:
        return {}
    try:
        return cls()
    except TypeError:
        return {}


def merge_items(target_list, sourced=False):
    """Recursively merge a list of Pipfile-category dicts, last-write-wins.

    Only the ``sourced=False`` path has callers in pipenv; the
    ``sourced=True`` branch is preserved for API parity but is not
    exercised by any in-tree call site.

    For an empty ``target_list``, returns ``None`` to match the
    historical behaviour of the boltons-backed implementation.
    """
    if not target_list:
        return None if not sourced else (None, {})

    iterator = iter(target_list)
    if sourced:
        source_map = {}
        first_name, first = next(iterator)
        merged = _new_container_like(first)
        _merge_into(merged, first)
        for key in merged:
            source_map[(key,)] = first_name
        for t_name, target in iterator:
            _merge_into(merged, target)
            for key in target:
                source_map[(key,)] = t_name
        return merged, source_map

    first = next(iterator)
    merged = _new_container_like(first)
    _merge_into(merged, first)
    for target in iterator:
        _merge_into(merged, target)
    return merged


def import_requirements(project, r=None, dev=False, categories=None):
    # Parse requirements.txt file with Pip's parser.
    # Pip requires a `PipSession` which is a subclass of requests.Session.
    # Since we're not making any network calls, it's initialized to nothing.
    from pipenv.patched.pip._internal.network.session import PipSession
    from pipenv.patched.pip._internal.req import parse_requirements
    from pipenv.patched.pip._internal.req.constructors import (
        install_req_from_parsed_requirement,
    )

    if r and not Path(r).is_file():
        raise OSError(f"Requirements file not found: {r}")
    # Default path, if none is provided.
    if r is None:
        r = project.pipfile.requirements_location
    with open(r) as f:
        contents = f.read()
    if categories is None:
        categories = []

    # Collect indexes and trusted hosts first
    indexes = []
    trusted_hosts = []
    for line in contents.split("\n"):
        index, extra_index, trusted_host, _ = parse_indexes(line.strip(), strict=True)
        if index:
            indexes = [index]
        if extra_index:
            indexes.append(extra_index)
        if trusted_host:
            trusted_hosts.append(get_host_and_port(trusted_host))

    # Collect all packages for batch processing
    packages_to_add = []
    req_path = str(r) if isinstance(r, Path) else r

    for f in parse_requirements(req_path, session=PipSession()):
        package = install_req_from_parsed_requirement(f)
        if package.name not in BAD_PACKAGES:
            if package.link is not None:
                if package.editable:
                    package_string = f"-e {package.link}"
                else:
                    package_string = unquote(
                        redact_auth_from_url(package.original_link.url)
                    )
            else:
                package_string = str(package.req)
                if package.markers:
                    package_string += f" ; {package.markers}"

            packages_to_add.append((package, package_string))

    # Batch add all packages to Pipfile
    if packages_to_add:
        project.pipfile.add_packages_batch(
            packages_to_add, dev=dev, categories=categories
        )

    # Add indexes after packages
    indexes = sorted(set(indexes))
    trusted_hosts = sorted(set(trusted_hosts))
    for index in indexes:
        add_index_to_pipfile_with_trust_check(project, index, trusted_hosts)

    # Recase pipfile once at the end
    project.pipfile.recase()


def add_index_to_pipfile_with_trust_check(project, index, trusted_hosts=None):
    # don't require HTTPS for trusted hosts (see: https://pip.pypa.io/en/stable/cli/pip/#cmdoption-trusted-host)
    if trusted_hosts is None:
        trusted_hosts = get_trusted_hosts()

    host_and_port = get_host_and_port(index)
    require_valid_https = not any(
        v in trusted_hosts
        for v in (
            host_and_port,
            host_and_port.partition(":")[
                0
            ],  # also check if hostname without port is in trusted_hosts
        )
    )
    index_name = project.sources.add_index_to_pipfile(index, verify_ssl=require_valid_https)
    return index_name


def requirement_from_lockfile(
    package_name, package_info, include_hashes=True, include_markers=True
):
    # Handle string requirements
    if isinstance(package_info, str):
        if package_info and not is_star(package_info):
            return f"{package_name}=={package_info}"
        else:
            return package_name

    markers = (
        "; {}".format(package_info["markers"])
        if include_markers and "markers" in package_info and package_info["markers"]
        else ""
    )
    os_markers = (
        "; {}".format(package_info["os_markers"])
        if include_markers and "os_markers" in package_info and package_info["os_markers"]
        else ""
    )

    # Handling vcs repositories
    for vcs in VCS_LIST:
        if vcs in package_info:
            vcs_url = package_info[vcs]
            ref = package_info.get("ref", "")
            # We have to handle the fact that some vcs urls have a ref in them
            # and some have a netloc with a username and password in them, and some have both
            vcs_url, fallback_ref = normalize_vcs_url(vcs_url)
            if not ref:
                ref = fallback_ref
            extras = (
                "[{}]".format(",".join(package_info.get("extras", [])))
                if "extras" in package_info
                else ""
            )
            subdirectory = package_info.get("subdirectory", "")
            include_vcs = "" if f"{vcs}+" in vcs_url else f"{vcs}+"
            egg_fragment = "" if "#egg=" in vcs_url else f"#egg={package_name}"
            ref_str = "" if not ref or f"@{ref}" in vcs_url else f"@{ref}"
            if (
                is_editable_path(vcs_url)
                or "file://" in vcs_url
                or package_info.get("editable", False)
            ):
                # Extras must not be in the #egg= fragment (pip validates it as
                # a PEP 508 project name). Append extras after the egg fragment.
                pip_line = f"-e {include_vcs}{vcs_url}{ref_str}{egg_fragment}"
                pip_line += f"&subdirectory={subdirectory}" if subdirectory else ""
                pip_line += extras
            else:
                pip_line = f"{package_name}{extras} @ {include_vcs}{vcs_url}{ref_str}"
                pip_line += f"#subdirectory={subdirectory}" if subdirectory else ""
            return pip_line
    # Handling file-sourced packages
    for k in ["file", "path"]:
        line = []
        if k in package_info:
            path = package_info[k]
            if package_info.get("editable") and is_editable_path(path):
                line.append("-e")
            line.append(f"{package_info[k]}")
            if os_markers:
                line.append(os_markers)
            if markers:
                line.append(markers)
            pip_line = " ".join(line)
            return pip_line

    # Handling packages from standard pypi like indexes
    version = package_info.get("version", "")
    # Skip wildcard versions - they mean "any version" and should not be included
    if is_star(version):
        version = ""
    hashes = (
        f" --hash={' --hash='.join(package_info['hashes'])}"
        if include_hashes and "hashes" in package_info
        else ""
    )
    extras = (
        "[{}]".format(",".join(package_info.get("extras", [])))
        if "extras" in package_info
        else ""
    )
    pip_line = f"{package_name}{extras}{version}{os_markers}{markers}{hashes}"
    return pip_line


def requirements_from_lockfile(deps, include_hashes=True, include_markers=True):
    pip_packages = []

    for package_name, package_info in deps.items():
        pip_package = requirement_from_lockfile(
            package_name, package_info, include_hashes, include_markers
        )

        # Append to the list
        pip_packages.append(pip_package)

    # pip_packages contains the pip-installable lines
    return pip_packages


def requirement_from_pipfile(package_name, package_spec, include_markers=True):
    """Convert a Pipfile package entry to a pip requirements line.

    Uses the version specifiers from the Pipfile (like "*", ">=1.0") rather than
    locked versions. This is useful for generating requirements for libraries
    that need more flexible version constraints.

    Args:
        package_name: The package name
        package_spec: The package specification from Pipfile (str or dict)
        include_markers: Whether to include environment markers

    Returns:
        A pip-installable requirement line
    """
    # Handle simple string specifications like "*", ">=1.0", "==1.0"
    if isinstance(package_spec, str):
        if is_star(package_spec):
            return package_name
        elif package_spec.startswith(("==", ">=", "<=", ">", "<", "~=", "!=")):
            return f"{package_name}{package_spec}"
        else:
            # Assume it's a version without operator, default to ==
            return f"{package_name}=={package_spec}"

    # Handle dict specifications
    if not isinstance(package_spec, dict):
        return package_name

    # Handle VCS dependencies
    for vcs_type in ["git", "hg", "svn", "bzr"]:
        if vcs_type in package_spec:
            vcs_url = package_spec[vcs_type]
            ref = package_spec.get("ref", "")
            subdirectory = package_spec.get("subdirectory", "")
            extras = (
                "[{}]".format(",".join(package_spec.get("extras", [])))
                if "extras" in package_spec
                else ""
            )

            # Build the VCS URL
            ref_str = f"@{ref}" if ref and f"@{ref}" not in vcs_url else ""
            egg_fragment = f"#egg={package_name}" if "#egg=" not in vcs_url else ""

            if (
                is_editable_path(vcs_url)
                or "file://" in vcs_url
                or package_spec.get("editable", False)
            ):
                # Extras must not be in the #egg= fragment (pip validates it as
                # a PEP 508 project name). Append extras after the egg fragment.
                line = f"-e {vcs_type}+{vcs_url}{ref_str}{egg_fragment}"
                if subdirectory:
                    line += f"&subdirectory={subdirectory}"
                line += extras
            else:
                line = f"{package_name}{extras} @ {vcs_type}+{vcs_url}{ref_str}"
                if subdirectory:
                    line += f"#subdirectory={subdirectory}"
            return line

    # Handle file/path dependencies
    for key in ["file", "path"]:
        if key in package_spec:
            path = package_spec[key]
            parts = []
            if package_spec.get("editable") and is_editable_path(path):
                parts.append("-e")
            parts.append(path)

            if include_markers:
                if "markers" in package_spec and package_spec["markers"]:
                    parts.append(f"; {package_spec['markers']}")
            return " ".join(parts)

    # Handle standard version specifications
    version = package_spec.get("version", "")
    extras = (
        "[{}]".format(",".join(package_spec.get("extras", [])))
        if "extras" in package_spec
        else ""
    )

    # Process version
    if is_star(version):
        version_str = ""
    elif version.startswith(("==", ">=", "<=", ">", "<", "~=", "!=")):
        version_str = version
    elif version:
        version_str = f"=={version}"
    else:
        version_str = ""

    # Process markers
    markers = ""
    if include_markers:
        # Handle sys_platform and other common markers
        marker_parts = []
        if "markers" in package_spec and package_spec["markers"]:
            marker_parts.append(package_spec["markers"])
        if "sys_platform" in package_spec and package_spec["sys_platform"]:
            marker_parts.append(f"sys_platform {package_spec['sys_platform']}")

        if marker_parts:
            markers = "; " + " and ".join(marker_parts)

    return f"{package_name}{extras}{version_str}{markers}"


def requirements_from_pipfile(deps, include_markers=True):
    """Convert Pipfile dependencies to pip requirements lines.

    Uses the version specifiers from the Pipfile rather than locked versions.

    Args:
        deps: Dictionary of package names to specifications from Pipfile
        include_markers: Whether to include environment markers

    Returns:
        List of pip-installable requirement lines
    """
    pip_packages = []

    for package_name, package_spec in deps.items():
        pip_package = requirement_from_pipfile(
            package_name, package_spec, include_markers
        )
        if pip_package:
            pip_packages.append(pip_package)

    return pip_packages
