from pathlib import Path
from typing import List, Optional

from pipx import constants
from pipx.commands.common import package_name_from_spec, run_post_install_actions
from pipx.constants import EXIT_CODE_INSTALL_VENV_EXISTS, EXIT_CODE_OK, ExitCode
from pipx.interpreter import DEFAULT_PYTHON
from pipx.util import pipx_wrap
from pipx.venv import Venv, VenvContainer


def install(
    venv_dir: Optional[Path],
    package_names: Optional[List[str]],
    package_specs: List[str],
    local_bin_dir: Path,
    local_man_dir: Path,
    python: Optional[str],
    pip_args: List[str],
    venv_args: List[str],
    verbose: bool,
    *,
    force: bool,
    reinstall: bool,
    include_dependencies: bool,
    preinstall_packages: Optional[List[str]],
    suffix: str = "",
) -> ExitCode:
    """Returns pipx exit code."""
    # package_spec is anything pip-installable, including package_name, vcs spec,
    #   zip file, or tar.gz file.
    python_flag_was_passed = python is not None

    python = python or DEFAULT_PYTHON

    package_names = package_names or []
    if len(package_names) != len(package_specs):
        package_names = [
            package_name_from_spec(package_spec, python, pip_args=pip_args, verbose=verbose)
            for package_spec in package_specs
        ]

    for package_name, package_spec in zip(package_names, package_specs):
        if venv_dir is None:
            venv_container = VenvContainer(constants.PIPX_LOCAL_VENVS)
            venv_dir = venv_container.get_venv_dir(f"{package_name}{suffix}")

        try:
            exists = venv_dir.exists() and bool(next(venv_dir.iterdir()))
        except StopIteration:
            exists = False

        venv = Venv(venv_dir, python=python, verbose=verbose)
        if exists:
            if not reinstall and force and python_flag_was_passed:
                print(
                    pipx_wrap(
                        f"""
                        --python is ignored when --force is passed.
                        If you want to reinstall {package_name} with {python},
                        run `pipx reinstall {package_spec} --python {python}` instead.
                        """
                    )
                )
            if force:
                print(f"Installing to existing venv {venv.name!r}")
                pip_args = ["--force-reinstall"] + pip_args
            else:
                print(
                    pipx_wrap(
                        f"""
                        {venv.name!r} already seems to be installed. Not modifying
                        existing installation in '{venv_dir}'. Pass '--force'
                        to force installation.
                        """
                    )
                )
                return EXIT_CODE_INSTALL_VENV_EXISTS

        try:
            # Enable installing shared library `pip` with `pipx`
            override_shared = package_name == "pip"
            venv.create_venv(venv_args, pip_args, override_shared)
            for dep in preinstall_packages or []:
                dep_name = package_name_from_spec(dep, python, pip_args=pip_args, verbose=verbose)
                venv.upgrade_package_no_metadata(dep_name, [])
            venv.install_package(
                package_name=package_name,
                package_or_url=package_spec,
                pip_args=pip_args,
                include_dependencies=include_dependencies,
                include_apps=True,
                is_main_package=True,
                suffix=suffix,
            )
            run_post_install_actions(
                venv,
                package_name,
                local_bin_dir,
                local_man_dir,
                venv_dir,
                include_dependencies,
                force=force,
            )
        except (Exception, KeyboardInterrupt):
            print()
            venv.remove_venv()
            raise

        # Reset venv_dir to None ready to install the next package in the list
        venv_dir = None

    # Any failure to install will raise PipxError, otherwise success
    return EXIT_CODE_OK
