import sys
from collections import namedtuple
from collections.abc import Mapping
from pathlib import Path

from pipenv.patched.pip._vendor.packaging.utils import (
    InvalidSdistFilename,
    InvalidWheelFilename,
    canonicalize_name,
    parse_sdist_filename,
    parse_wheel_filename,
)
from pipenv.patched.pip._vendor.packaging.version import parse as parse_version
from pipenv.routines.context import RoutineContext
from pipenv.routines.lock import do_lock
from pipenv.utils import console, err
from pipenv.utils.dependencies import (
    as_pipfile,
    expansive_install_req_from_line,
    get_version,
    pep423_name,
)


def _get_lockfile_entry_version(lockfile_entry):
    version = get_version(lockfile_entry)
    if version:
        return parse_version(version.replace("==", ""))

    if not isinstance(lockfile_entry, Mapping):
        return None

    file_path = lockfile_entry.get("file") or lockfile_entry.get("path")
    if not file_path:
        return None

    file_name = Path(file_path).name
    try:
        return parse_wheel_filename(file_name)[1]
    except InvalidWheelFilename:
        pass

    try:
        return parse_sdist_filename(file_name)[1]
    except InvalidSdistFilename:
        return None


def do_outdated(project, pypi_mirror=None, pre=False, clear=False):
    packages = {}
    package_info = namedtuple("PackageInfo", ["name", "installed", "available"])

    installed_packages = project.environment.get_installed_packages()
    outdated_packages = {
        canonicalize_name(pkg.name): package_info(
            pkg.name, parse_version(pkg.version), pkg.latest_version
        )
        for pkg in project.environment.get_outdated_packages()
    }
    reverse_deps = {
        canonicalize_name(name): deps
        for name, deps in project.environment.reverse_dependencies().items()
    }
    for result in installed_packages:
        dep, _ = expansive_install_req_from_line(f"{result.name}=={result.version}")
        packages.update(as_pipfile(dep))

    updated_packages = {}
    lock_ctx = RoutineContext.from_cli(
        clear=clear,
        pre=pre,
        write=False,
        pypi_mirror=pypi_mirror,
    )
    lockfile = do_lock(project, lock_ctx)
    for category in project.pipfile.get_package_categories(for_lockfile=True):
        for package, lockfile_entry in lockfile.get(category, {}).items():
            resolved_version = _get_lockfile_entry_version(lockfile_entry)
            if resolved_version is not None:
                updated_packages[pep423_name(package)] = resolved_version
    outdated = []
    skipped = []
    for package in packages.keys():  # noqa: PLC0206
        norm_name = pep423_name(package)
        if norm_name in updated_packages.keys():
            version = packages[package]
            if isinstance(version, Mapping):
                version = parse_version(version.get("version", "").replace("==", ""))
            else:
                version = parse_version(version.replace("==", ""))
            if updated_packages[norm_name] != version:
                outdated.append(
                    package_info(package, str(version), str(updated_packages[norm_name]))
                )
            elif canonicalize_name(package) in outdated_packages:
                skipped.append(outdated_packages[canonicalize_name(package)])
    for package, old_version, new_version in skipped:
        for category in project.pipfile.get_package_categories():
            name_in_pipfile = project.pipfile.get_package_name(
                package, category=category
            )
            pipfile_section = project.pipfile.get_section(category)
            if name_in_pipfile and name_in_pipfile in pipfile_section:
                required = ""
                version = get_version(pipfile_section[name_in_pipfile])
                rdeps = reverse_deps.get(canonicalize_name(package))
                if isinstance(rdeps, Mapping) and "required" in rdeps:
                    required = " {rdeps['required']} required"
                if version:
                    pipfile_version_text = f" ({version} set in Pipfile)"
                else:
                    pipfile_version_text = " (Unpinned in Pipfile)"
                err.print(
                    f"Skipped Update of Package {package!s}:"
                    f" {old_version!s} installed,{required!s}{pipfile_version_text!s}, "
                    f"{new_version!s} available.",
                    style="yellow",
                )
    for package, old_version, new_version in set(outdated).union(set(skipped)):
        console.print(
            f"Package {package!r} out-of-date: {old_version!r} installed, {new_version!r} available."
        )
    if not outdated:
        console.print("All packages are up to date!", style="green bold")
    sys.exit(bool(outdated))
