import os
import re
from typing import Tuple
from urllib.parse import quote, unquote

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,
)
from pipenv.patched.pip._internal.utils.misc import _transform_url, split_auth_from_netloc
from pipenv.utils.constants import VCS_LIST
from pipenv.utils.indexes import parse_indexes
from pipenv.utils.internet import get_host_and_port
from pipenv.utils.pip import get_trusted_hosts


def redact_netloc(netloc: str) -> Tuple[str]:
    """
    Replace the sensitive data in a netloc with "****", if it exists, unless it's an environment variable.

    For example:
        - "user:pass@example.com" returns "user:****@example.com"
        - "accesstoken@example.com" returns "****@example.com"
        - "${ENV_VAR}:pass@example.com" returns "${ENV_VAR}:****@example.com" if ${ENV_VAR} is an environment variable
    """
    netloc, (user, password) = split_auth_from_netloc(netloc)
    if user is None:
        return (netloc,)
    if password is None:
        # Check if user is an environment variable
        if not re.match(r"\$\{\w+\}", user):
            # If not, redact the user
            user = "****"
        password = ""
    else:
        # Check if password is an environment variable
        if not re.match(r"\$\{\w+\}", password):
            # If not, redact the password
            password = ":****"
        else:
            # If it is, leave it as is
            password = ":" + password
        user = quote(user)
    return (f"{user}{password}@{netloc}",)


def redact_auth_from_url(url: str) -> str:
    """Replace the password in a given url with ****."""
    return _transform_url(url, redact_netloc)[0]


def normalize_name(pkg) -> str:
    """Given a package name, return its normalized, non-canonicalized form."""
    return pkg.replace("_", "-").lower()


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.
    if r:
        assert os.path.isfile(r)
    # Default path, if none is provided.
    if r is None:
        r = project.requirements_location
    with open(r) as f:
        contents = f.read()
    if categories is None:
        categories = []
    indexes = []
    trusted_hosts = []
    # Find and add extra indexes.
    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))
    for f in parse_requirements(r, 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)
                    )

                if categories:
                    for category in categories:
                        project.add_package_to_pipfile(
                            package, package_string, dev=dev, category=category
                        )
                else:
                    project.add_package_to_pipfile(package, package_string, dev=dev)
            else:
                package_string = str(package.req)
                if package.markers:
                    package_string += f" ; {package.markers}"
                if categories:
                    for category in categories:
                        project.add_package_to_pipfile(
                            package, package_string, dev=dev, category=category
                        )
                else:
                    project.add_package_to_pipfile(package, package_string, dev=dev)
    indexes = sorted(set(indexes))
    trusted_hosts = sorted(set(trusted_hosts))
    for index in indexes:
        add_index_to_pipfile(project, index, trusted_hosts)
    project.recase_pipfile()


def add_index_to_pipfile(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.add_index_to_pipfile(index, verify_ssl=require_valid_https)
    return index_name


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


def requirement_from_lockfile(
    package_name, package_info, include_hashes=True, include_markers=True
):
    from pipenv.utils.dependencies import is_editable_path, is_star, normalize_vcs_url

    # 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)
            ):
                pip_line = f"-e {include_vcs}{vcs_url}{ref_str}{egg_fragment}{extras}"
                pip_line += f"&subdirectory={subdirectory}" if subdirectory else ""
            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 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", "").replace("==", "")
    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
