import os
import sys
from pathlib import Path

from pipenv import environments
from pipenv.__version__ import __version__
from pipenv.cli.options import (
    CONTEXT_SETTINGS,
    PipenvGroup,
    common_options,
    deploy_option,
    general_options,
    install_options,
    lock_options,
    pass_state,
    pypi_mirror_option,
    python_option,
    site_packages_option,
    sync_options,
    system_option,
    uninstall_options,
    upgrade_options,
    verbose_option,
)
from pipenv.utils import console, err
from pipenv.utils.environment import load_dot_env
from pipenv.utils.processes import subprocess_run
from pipenv.vendor.click import (
    Choice,
    argument,
    edit,
    group,
    option,
    pass_context,
    version_option,
)

with console.capture() as capture:
    console.print("[bold]pipenv[/bold]", end="")

prog_name = capture.get()

subcommand_context = CONTEXT_SETTINGS.copy()
subcommand_context.update({"ignore_unknown_options": True, "allow_extra_args": True})
subcommand_context_no_interspersion = subcommand_context.copy()
subcommand_context_no_interspersion["allow_interspersed_args"] = False


@group(cls=PipenvGroup, invoke_without_command=True, context_settings=CONTEXT_SETTINGS)
@option("--where", is_flag=True, default=False, help="Output project home information.")
@option("--venv", is_flag=True, default=False, help="Output virtualenv information.")
@option(
    "--py", is_flag=True, default=False, help="Output Python interpreter information."
)
@option(
    "--envs", is_flag=True, default=False, help="Output Environment Variable options."
)
@option("--rm", is_flag=True, default=False, help="Remove the virtualenv.")
@option("--bare", is_flag=True, default=False, help="Minimal output.")
@option("--man", is_flag=True, default=False, help="Display manpage.")
@option(
    "--support",
    is_flag=True,
    help="Output diagnostic information for use in GitHub issues.",
)
@general_options
@version_option(prog_name=prog_name, version=__version__)
@pass_state
@pass_context
def cli(
    ctx,
    state,
    where=False,
    venv=False,
    py=False,
    envs=False,
    rm=False,
    bare=False,
    man=False,
    support=None,
    help=False,
    site_packages=None,
    **kwargs,
):
    from pipenv.utils.shell import system_which

    load_dot_env(state.project, quiet=state.quiet)

    from pipenv.routines.clear import do_clear
    from pipenv.utils.display import format_help
    from pipenv.utils.project import ensure_project
    from pipenv.utils.virtualenv import cleanup_virtualenv, do_where, warn_in_virtualenv

    if "PIPENV_COLORBLIND" in os.environ:
        err.print(
            "PIPENV_COLORBLIND is deprecated, use NO_COLOR"
            " per https://no-color.org/ instead",
        )

    if man:
        if system_which("man"):
            path = Path(__file__).parent.parent / "pipenv.1"
            os.execle(system_which("man"), "man", str(path), os.environ)
            return 0
        else:
            err.print(
                "man does not appear to be available on your system.", style="bold yellow"
            )
            return 1
    if envs:
        console.print(
            "The following environment variables can be set, to do various things:\n"
        )
        for key in state.project.__dict__:
            if key.startswith("PIPENV"):
                console.print(f"  - {key}", style="bold")
        console.print(
            "\nYou can learn more at:\n   "
            "[green]https://pipenv.pypa.io/en/latest/advanced/#configuration-with-environment-variables[/green]",
        )
        return 0
    warn_in_virtualenv(state.project)
    if ctx.invoked_subcommand is None:
        # --where was passed...
        if where:
            do_where(state.project, bare=True)
            return 0
        elif py:
            do_py(state.project, ctx=ctx, bare=True)
            return 0
        # --support was passed...
        elif support:
            from ..help import get_pipenv_diagnostics

            get_pipenv_diagnostics(state.project)
            return 0
        # --clear was passed...
        elif state.clear:
            do_clear(state.project)
            return 0
        # --venv was passed...
        elif venv:
            # There is no virtualenv yet.
            if not state.project.virtualenv_exists:
                err.print(
                    "[red]No virtualenv has been created for this project[/red]"
                    f"[bold]{state.project.project_directory}[/bold]"
                    " [red]yet![/red]"
                )
                ctx.abort()
            else:
                print(state.project.virtualenv_location)
                return 0
        # --rm was passed...
        elif rm:
            # Abort if --system (or running in a virtualenv).
            if state.project.s.PIPENV_USE_SYSTEM or environments.is_in_virtualenv():
                console.print(
                    "You are attempting to remove a virtualenv that "
                    "Pipenv did not create. Aborting.",
                    style="red",
                )
                ctx.abort()
            if state.project.virtualenv_exists:
                loc = state.project.virtualenv_location
                console.print(
                    f"[bold]Removing virtualenv[/bold] ([green]{loc}[green])..."
                )

                with console.status("Running..."):
                    # Remove the virtualenv.
                    cleanup_virtualenv(state.project, bare=True)
                return 0
            else:
                err.print(
                    "No virtualenv has been created for this project yet!",
                    style="red bold",
                )
                ctx.abort()
    # --python was passed...
    if (state.python) or state.site_packages:
        ensure_project(
            state.project,
            python=state.python,
            warn=True,
            site_packages=state.site_packages,
            pypi_mirror=state.pypi_mirror,
            clear=state.clear,
        )
    elif ctx.invoked_subcommand is None:
        console.print(format_help(ctx.get_help()))


@cli.command(
    short_help="Installs provided packages and adds them to Pipfile, or (if no packages are given), installs all packages from Pipfile.",
    context_settings=subcommand_context,
)
@system_option
@deploy_option
@site_packages_option
@install_options
@pass_state
def install(state, **kwargs):
    """Installs provided packages and adds them to Pipfile,
    or (if no packages are given), installs all packages from Pipfile."""
    from pipenv.routines.install import do_install

    do_install(
        state.project,
        dev=state.installstate.dev,
        python=state.python,
        pypi_mirror=state.pypi_mirror,
        system=state.system,
        ignore_pipfile=state.installstate.ignore_pipfile,
        requirementstxt=state.installstate.requirementstxt,
        pre=state.installstate.pre,
        deploy=state.installstate.deploy,
        index=state.index,
        packages=state.installstate.packages,
        editable_packages=state.installstate.editables,
        site_packages=state.site_packages,
        extra_pip_args=state.installstate.extra_pip_args,
        pipfile_categories=state.installstate.categories,
        skip_lock=state.installstate.skip_lock,
    )


@cli.command(
    short_help="Resolves provided packages and adds them to Pipfile, or (if no packages are given), merges results to Pipfile.lock",
    context_settings=subcommand_context,
)
@system_option
@site_packages_option
@install_options
@upgrade_options
@pass_state
def upgrade(state, **kwargs):
    from pipenv.routines.update import upgrade
    from pipenv.utils.project import ensure_project

    ensure_project(
        state.project,
        python=state.python,
        pypi_mirror=state.pypi_mirror,
        warn=(not state.quiet),
        site_packages=state.site_packages,
        clear=state.clear,
    )

    upgrade(
        state.project,
        pre=state.installstate.pre,
        packages=state.installstate.packages,
        editable_packages=state.installstate.editables,
        categories=state.installstate.categories,
        index_url=state.index,
        dev=state.installstate.dev,
        system=state.system,
        lock_only=state.installstate.lock_only,
        extra_pip_args=state.installstate.extra_pip_args,
    )


@cli.command(
    short_help="Uninstalls a provided package and removes it from Pipfile.",
    context_settings=subcommand_context,
)
@option(
    "--all-dev",
    is_flag=True,
    default=False,
    help="Uninstall all package from [dev-packages].",
)
@option(
    "--all",
    is_flag=True,
    default=False,
    help="Purge all package(s) from virtualenv. Does not edit Pipfile.",
)
@uninstall_options
@pass_state
@pass_context
def uninstall(ctx, state, all_dev=False, all=False, **kwargs):
    """Uninstalls a provided package and removes it from Pipfile."""
    from pipenv.routines.uninstall import do_uninstall

    pre = state.installstate.pre

    retcode = do_uninstall(
        state.project,
        packages=state.installstate.packages,
        editable_packages=state.installstate.editables,
        python=state.python,
        system=state.system,
        lock=False,
        all_dev=all_dev,
        all=all,
        pre=pre,
        pypi_mirror=state.pypi_mirror,
        categories=state.installstate.categories,
        ctx=ctx,
    )
    if retcode:
        sys.exit(retcode)


LOCK_HEADER = """\
#
# These requirements were autogenerated by pipenv
# To regenerate from the project's Pipfile, run:
#
#    pipenv requirements {options}
#
"""


LOCK_DEV_NOTE = """\
# Note: in pipenv 2020.x, "--dev" changed to emit both default and development
# requirements. To emit only development requirements, pass "--dev-only".
"""


@cli.command(short_help="Generates Pipfile.lock.", context_settings=CONTEXT_SETTINGS)
@lock_options
@pass_state
@pass_context
def lock(ctx, state, **kwargs):
    """Generates Pipfile.lock."""
    from pipenv.routines.lock import do_lock
    from pipenv.utils.project import ensure_project

    # Ensure that virtualenv is available.
    # Note that we don't pass clear on to ensure_project as it is also
    # handled in do_lock
    ensure_project(
        state.project,
        python=state.python,
        pypi_mirror=state.pypi_mirror,
        warn=(not state.quiet),
        site_packages=state.site_packages,
    )
    pre = state.installstate.pre
    do_lock(
        state.project,
        clear=state.clear,
        pre=pre,
        pypi_mirror=state.pypi_mirror,
        write=True,
        quiet=state.quiet,
        categories=state.installstate.categories,
    )


@cli.command(
    short_help="Spawns a shell within the virtualenv.",
    context_settings=subcommand_context,
)
@option(
    "--fancy",
    is_flag=True,
    default=False,
    help="Run in shell in fancy mode. Make sure the shell have no path manipulating"
    " scripts. Run $pipenv shell for issues with compatibility mode.",
)
@option(
    "--anyway",
    is_flag=True,
    default=False,
    help="Always spawn a sub-shell, even if one is already spawned.",
)
@option(
    "--quiet", is_flag=True, help="Quiet standard output, except vulnerability report."
)
@argument("shell_args", nargs=-1)
@pypi_mirror_option
@python_option
@pass_state
def shell(state, fancy=False, shell_args=None, anyway=False, quiet=False):
    """Spawns a shell within the virtualenv."""
    from pipenv.routines.shell import do_shell

    # Prevent user from activating nested environments.
    if "PIPENV_ACTIVE" in os.environ:
        # If PIPENV_ACTIVE is set, VIRTUAL_ENV should always be set too.
        venv_name = os.environ.get("VIRTUAL_ENV", "UNKNOWN_VIRTUAL_ENVIRONMENT")
        if not anyway:
            err.print(
                f"Shell for [green bold]{venv_name}[/green bold] "
                "[bold]already activated[/bold].\n"
                "New shell not activated to avoid nested environments."
            )
            sys.exit(1)

    # Use fancy mode for Windows, pwsh, or when Oh My Posh is detected.
    # Oh My Posh interferes with the VIRTUAL_ENV variable when using the
    # pexpect-based compat mode. See: https://github.com/pypa/pipenv/issues/6226
    if (
        os.name == "nt"
        or Path(os.environ.get("PIPENV_SHELL") or "").name == "pwsh"
        or Path(os.environ.get("SHELL") or "").name == "pwsh"
        or os.environ.get("POSH_THEME")  # Oh My Posh detected
    ):
        fancy = True
    do_shell(
        state.project,
        python=state.python,
        fancy=fancy,
        shell_args=shell_args,
        pypi_mirror=state.pypi_mirror,
        quiet=quiet,
    )


@cli.command(
    short_help="Outputs the activation command for the virtualenv.",
    context_settings=CONTEXT_SETTINGS,
)
@pypi_mirror_option
@python_option
@pass_state
def activate(state):
    """Outputs the shell command to activate the virtualenv.

    Unlike 'pipenv shell' which spawns a subshell, this command prints
    the activation command that can be evaluated in your current shell.

    \b
    Usage examples:
        $ eval $(pipenv activate)          # Bash/Zsh
        $ eval (pipenv activate)           # Fish
        $ Invoke-Expression (pipenv activate)  # PowerShell

    \b
    You can create a shell alias for convenience:
        alias penv='eval $(pipenv activate)'

    This approach is similar to Poetry's 'poetry env activate' command.
    """
    from pipenv.shells import ShellDetectionFailure, _get_activate_script, detect_info
    from pipenv.utils.project import ensure_project

    # Ensure virtualenv exists
    ensure_project(
        state.project,
        python=state.python,
        validate=False,
        pypi_mirror=state.pypi_mirror,
    )

    if not state.project.virtualenv_exists:
        err.print(
            "No virtualenv has been created for this project yet!\n"
            "Run [green bold]pipenv install[/green bold] to create one.",
            style="red bold",
        )
        sys.exit(1)

    try:
        shell_type, shell_cmd = detect_info(state.project)
    except ShellDetectionFailure:
        err.print(
            "Unable to detect shell. Set PIPENV_SHELL environment variable.",
            style="red bold",
        )
        sys.exit(1)

    venv_path = state.project.virtualenv_location
    activate_cmd = _get_activate_script(shell_cmd, venv_path)

    # Output to stdout (strip leading space used for history hiding in shell command)
    print(activate_cmd.strip())


@cli.command(
    short_help="Spawns a command installed into the virtualenv.",
    context_settings=subcommand_context_no_interspersion,
)
@system_option
@common_options
@argument("command")
@argument("args", nargs=-1)
@pass_state
def run(state, command, args):
    """Spawns a command installed into the virtualenv."""
    from pipenv.routines.shell import do_run

    do_run(
        state.project,
        command=command,
        args=args,
        python=state.python,
        pypi_mirror=state.pypi_mirror,
        system=state.system,
    )


@cli.command(
    short_help="Checks for PyUp Safety security vulnerabilities and against"
    " PEP 508 markers provided in Pipfile.",
    context_settings=subcommand_context,
)
@option(
    "--db",
    nargs=1,
    default=lambda: os.environ.get("PIPENV_SAFETY_DB"),
    help="Path or URL to a PyUp Safety vulnerabilities database."
    " Default: ENV PIPENV_SAFETY_DB or None.",
)
@option(
    "--ignore",
    "-i",
    multiple=True,
    help="Ignore specified vulnerability during PyUp Safety checks.",
)
@option(
    "--output",
    type=Choice(["default", "json", "full-report", "bare", "screen", "text", "minimal"]),
    default="default",
    help="Translates to --json, --full-report or --bare from PyUp Safety check",
)
@option(
    "--key",
    help="Safety API key from PyUp.io for scanning dependencies against a live"
    " vulnerabilities database. Leave blank for scanning against a"
    " database that only updates once a month.",
)
@option(
    "--quiet", is_flag=True, help="Quiet standard output, except vulnerability report."
)
@option("--policy-file", default="", help="Define the policy file to be used")
@option(
    "--exit-code/--continue-on-error",
    default=True,
    help="Output standard exit codes. Default: --exit-code",
)
@option(
    "--audit-and-monitor/--disable-audit-and-monitor",
    default=True,
    help="Send results back to pyup.io for viewing on your dashboard. Requires an API key.",
)
@option(
    "--project",
    default=None,
    help="Project to associate this scan with on pyup.io. Defaults to a canonicalized github style name if available, otherwise unknown",
)
@option(
    "--save-json",
    default="",
    help="Path to where output file will be placed, if the path is a directory, "
    "Safety will use safety-report.json as filename. Default: empty",
)
@option(
    "--use-installed",
    is_flag=True,
    help="Whether to use the lockfile as input to check (instead of result from pip list).",
)
@option(
    "--categories",
    is_flag=False,
    default="",
    help="Use the specified categories from the lockfile as input to check.",
)
@option(
    "--auto-install",
    is_flag=True,
    default=False,
    help="Automatically install safety if not already installed.",
)
@option(
    "--scan",
    is_flag=True,
    default=False,
    help="Use the new scan command instead of the deprecated check command.",
)
@common_options
@system_option
@pass_state
def check(
    state,
    db=None,
    ignore=None,
    output="screen",
    key=None,
    quiet=False,
    exit_code=True,
    policy_file="",
    save_json="",
    audit_and_monitor=True,
    project=None,
    use_installed=False,
    categories="",
    auto_install=False,
    scan=False,
    **kwargs,
):
    """DEPRECATED: Checks for PyUp Safety security vulnerabilities and against PEP 508 markers provided in Pipfile.

    This command is deprecated. Please use 'pipenv audit' instead, which uses pip-audit
    for vulnerability scanning and requires no API key.

    Alternatively, use --scan to use Safety's scan command (requires API key).
    """

    from pipenv.routines.check import do_check

    do_check(
        state.project,
        python=state.python,
        system=state.system,
        db=db,
        ignore=ignore,
        output=output,
        key=key,
        quiet=quiet,
        verbose=state.verbose,
        exit_code=exit_code,
        policy_file=policy_file,
        save_json=save_json,
        audit_and_monitor=audit_and_monitor,
        safety_project=project,
        pypi_mirror=state.pypi_mirror,
        use_installed=use_installed,
        categories=categories,
        auto_install=auto_install,
        scan=scan,
    )


@cli.command(
    short_help="Audits packages for security vulnerabilities using pip-audit.",
    context_settings=subcommand_context,
)
@option(
    "--output",
    "-f",
    type=Choice(["columns", "json", "cyclonedx-json", "cyclonedx-xml", "markdown"]),
    default="columns",
    help="Output format for audit results.",
)
@option(
    "--vulnerability-service",
    "-s",
    type=Choice(["pypi", "osv"]),
    default="pypi",
    help="Vulnerability service to query (pypi or osv).",
)
@option(
    "--ignore",
    "-i",
    multiple=True,
    help="Ignore a specific vulnerability by ID (can be used multiple times).",
)
@option(
    "--fix",
    is_flag=True,
    default=False,
    help="Automatically upgrade packages with known vulnerabilities.",
)
@option(
    "--dry-run",
    is_flag=True,
    default=False,
    help="Collect dependencies but do not audit (or with --fix: audit but do not fix).",
)
@option(
    "--strict",
    is_flag=True,
    default=False,
    help="Fail if dependency collection fails on any dependency.",
)
@option(
    "--skip-editable",
    is_flag=True,
    default=False,
    help="Skip auditing editable packages.",
)
@option(
    "--no-deps",
    is_flag=True,
    default=False,
    help="Don't perform dependency resolution (requires pinned requirements).",
)
@option(
    "--local",
    is_flag=True,
    default=False,
    help="Only audit packages in the local environment.",
)
@option(
    "--desc",
    is_flag=True,
    default=False,
    help="Include descriptions for each vulnerability.",
)
@option(
    "--aliases",
    is_flag=True,
    default=False,
    help="Include alias IDs (CVE, GHSA) for each vulnerability.",
)
@option(
    "--output-file",
    "-o",
    default=None,
    help="Output results to the given file.",
)
@option(
    "--quiet",
    is_flag=True,
    default=False,
    help="Quiet mode - minimal output.",
)
@option(
    "--locked",
    is_flag=True,
    default=False,
    help="Audit lockfiles (pyproject.toml/pylock.toml) instead of the environment.",
)
@common_options
@system_option
@pass_state
def audit(
    state,
    output="columns",
    vulnerability_service="pypi",
    ignore=None,
    fix=False,
    dry_run=False,
    strict=False,
    skip_editable=False,
    no_deps=False,
    local=False,
    desc=False,
    aliases=False,
    output_file=None,
    quiet=False,
    locked=False,
    **kwargs,
):
    """Audit packages for known security vulnerabilities using pip-audit.

    This command scans your environment for packages with known vulnerabilities
    using the Python Packaging Advisory Database (PyPI) or Open Source
    Vulnerabilities (OSV) database.

    Examples:

        pipenv audit

        pipenv audit --fix

        pipenv audit -f json

        pipenv audit --ignore PYSEC-2021-123

        pipenv audit --locked  # Audit pyproject.toml/pylock.toml
    """
    from pipenv.routines.audit import do_audit

    do_audit(
        state.project,
        python=state.python,
        system=state.system,
        output=output,
        quiet=quiet,
        verbose=state.verbose,
        strict=strict,
        ignore=ignore,
        fix=fix,
        dry_run=dry_run,
        skip_editable=skip_editable,
        no_deps=no_deps,
        local_only=local,
        vulnerability_service=vulnerability_service,
        descriptions=desc,
        aliases=aliases,
        output_file=output_file,
        pypi_mirror=state.pypi_mirror,
        use_lockfile=locked,
    )


@cli.command(short_help="Runs lock, then sync.", context_settings=CONTEXT_SETTINGS)
@option("--bare", is_flag=True, default=False, help="Minimal output.")
@option("--outdated", is_flag=True, default=False, help="List out-of-date dependencies.")
@option("--dry-run", is_flag=True, default=None, help="List out-of-date dependencies.")
@install_options
@upgrade_options
@pass_state
@pass_context
def update(ctx, state, bare=False, dry_run=None, outdated=False, **kwargs):
    """Runs lock when no packages are specified, or upgrade, and then sync."""
    from pipenv.routines.update import do_update

    do_update(
        state.project,
        python=state.python,
        site_packages=state.site_packages,
        clear=state.clear,
        pre=state.installstate.pre,
        pypi_mirror=state.pypi_mirror,
        system=False,
        packages=state.installstate.packages,
        editable_packages=state.installstate.editables,
        dev=state.installstate.dev,
        bare=bare,
        extra_pip_args=state.installstate.extra_pip_args,
        categories=state.installstate.categories,
        index_url=state.index,
        quiet=state.quiet,
        dry_run=dry_run,
        outdated=outdated,
        lock_only=state.installstate.lock_only,
    )


@cli.command(
    short_help="Displays currently-installed dependency graph information.",
    context_settings=CONTEXT_SETTINGS,
)
@option("--bare", is_flag=True, default=False, help="Minimal output.")
@option("--json", is_flag=True, default=False, help="Output JSON.")
@option("--json-tree", is_flag=True, default=False, help="Output JSON in nested tree.")
@option("--reverse", is_flag=True, default=False, help="Reversed dependency graph.")
@pass_state
def graph(state, bare=False, json=False, json_tree=False, reverse=False):
    """Displays currently-installed dependency graph information."""
    from pipenv.routines.graph import do_graph

    do_graph(state.project, bare=bare, json=json, json_tree=json_tree, reverse=reverse)


@cli.command(
    short_help="View a given module in your editor.",
    name="open",
    context_settings=CONTEXT_SETTINGS,
)
@common_options
@argument("module", nargs=1)
@pass_state
def run_open(state, module, *args, **kwargs):
    """View a given module in your editor.

    This uses the EDITOR environment variable. You can temporarily override it,
    for example:

        EDITOR=atom pipenv open requests
    """
    from pipenv.utils.project import ensure_project
    from pipenv.utils.virtualenv import inline_activate_virtual_environment

    # Ensure that virtualenv is available.
    ensure_project(
        state.project,
        python=state.python,
        validate=False,
        pypi_mirror=state.pypi_mirror,
    )
    c = subprocess_run(
        [
            state.project._which("python"),
            "-c",
            f"import {module}; print({module}.__file__)",
        ]
    )
    if c.returncode:
        console.print("Module not found!", style="red")
        sys.exit(1)
    if "__init__.py" in c.stdout:
        p = Path(c.stdout.strip().rstrip("cdo")).parent
    else:
        p = c.stdout.strip().rstrip("cdo")
    console.print(f"Opening {p!r} in your EDITOR.", style="bold")
    inline_activate_virtual_environment(state.project)
    edit(filename=p)
    return 0


@cli.command(
    short_help="Installs all packages specified in Pipfile.lock.",
    context_settings=CONTEXT_SETTINGS,
)
@system_option
@option("--bare", is_flag=True, default=False, help="Minimal output.")
@sync_options
@site_packages_option
@pass_state
@pass_context
def sync(ctx, state, bare=False, user=False, unused=False, **kwargs):
    """Installs all packages specified in Pipfile.lock."""
    from pipenv.routines.sync import do_sync

    retcode = do_sync(
        state.project,
        dev=state.installstate.dev,
        python=state.python,
        bare=bare,
        clear=state.clear,
        pypi_mirror=state.pypi_mirror,
        system=state.system,
        extra_pip_args=state.installstate.extra_pip_args,
        categories=state.installstate.categories,
        site_packages=state.site_packages,
    )
    if retcode:
        ctx.abort()


@cli.command(
    short_help="Uninstalls all packages not specified in Pipfile.lock.",
    context_settings=CONTEXT_SETTINGS,
)
@option("--bare", is_flag=True, default=False, help="Minimal output.")
@option("--dry-run", is_flag=True, default=False, help="Just output unneeded packages.")
@verbose_option
@python_option
@pass_state
def clean(state, dry_run=False, bare=False, user=False):
    """Uninstalls all packages not specified in Pipfile.lock."""
    from pipenv.routines.clean import do_clean

    do_clean(
        state.project,
        python=state.python,
        dry_run=dry_run,
        system=state.system,
    )


@cli.command(
    short_help="Lists scripts in current environment config.",
    context_settings=subcommand_context_no_interspersion,
)
@common_options
@pass_state
def scripts(state):
    """Lists scripts in current environment config."""
    if not state.project.pipfile_exists:
        err.print("No Pipfile present at project home.")
        sys.exit(1)
    scripts = state.project.parsed_pipfile.get("scripts", {})
    first_column_width = max(len(word) for word in ["Command"] + list(scripts))
    second_column_width = max(len(word) for word in ["Script"] + list(scripts.values()))
    lines = [f"{command:<{first_column_width}}  Script" for command in ["Command"]]
    lines.append(f"{'-' * first_column_width}  {'-' * second_column_width}")
    lines.extend(
        f"{name:<{first_column_width}}  {script}" for name, script in scripts.items()
    )
    console.print("\n".join(line for line in lines))


@cli.command(
    short_help="Verify the hash in Pipfile.lock is up-to-date.",
    context_settings=CONTEXT_SETTINGS,
)
@pass_state
def verify(state):
    """Verify the hash in Pipfile.lock is up-to-date."""
    if not state.project.pipfile_exists:
        err.print("No Pipfile present at project home.")
        sys.exit(1)
    if state.project.get_lockfile_hash() != state.project.calculate_pipfile_hash():
        err.print(
            "Pipfile.lock is out-of-date. Run [yellow bold]$ pipenv lock[/yellow bold] to update."
        )
        sys.exit(1)
    console.print("Pipfile.lock is up-to-date.", style="green")
    sys.exit(0)


@cli.command(
    short_help="Generate a requirements.txt from Pipfile.lock.",
    context_settings=CONTEXT_SETTINGS,
)
@option("--dev", is_flag=True, default=False, help="Also add development requirements.")
@option(
    "--dev-only", is_flag=True, default=False, help="Only add development requirements."
)
@option("--hash", is_flag=True, default=False, help="Add package hashes.")
@option("--exclude-markers", is_flag=True, default=False, help="Exclude markers.")
@option(
    "--categories",
    is_flag=False,
    default="",
    help="Only add requirement of the specified categories.",
)
@option(
    "--from-pipfile",
    is_flag=True,
    default=False,
    help="Only include dependencies from Pipfile (excludes transitive deps).",
)
@option(
    "--no-lock",
    is_flag=True,
    default=False,
    help="Use version specifiers from Pipfile instead of locked versions. "
    "Useful for generating flexible requirements for libraries.",
)
@pass_state
def requirements(
    state,
    dev=False,
    dev_only=False,
    hash=False,
    exclude_markers=False,
    categories="",
    from_pipfile=False,
    no_lock=False,
):
    from pipenv.routines.requirements import generate_requirements

    # --no-lock implies --from-pipfile (only direct deps make sense without lock)
    if no_lock:
        from_pipfile = True

    generate_requirements(
        project=state.project,
        dev=dev,
        dev_only=dev_only,
        include_hashes=hash,
        include_markers=not exclude_markers,
        categories=categories,
        from_pipfile=from_pipfile,
        no_lock=no_lock,
    )


@cli.command(
    short_help="Manage PEP 751 pylock.toml files.",
    context_settings=CONTEXT_SETTINGS,
)
@option(
    "--generate",
    is_flag=True,
    default=False,
    help="Generate pylock.toml from Pipfile.lock.",
)
@option(
    "--from-pyproject",
    is_flag=True,
    default=False,
    help="Generate pylock.toml skeleton from pyproject.toml.",
)
@option(
    "--validate",
    is_flag=True,
    default=False,
    help="Validate an existing pylock.toml file.",
)
@option(
    "--output",
    "-o",
    default=None,
    help="Output file path (default: pylock.toml in project directory).",
)
@option(
    "--dev-groups",
    default="dev",
    help="Comma-separated list of dependency group names for dev packages.",
)
@common_options
@pass_state
def pylock(
    state,
    generate=False,
    from_pyproject=False,
    validate=False,
    output=None,
    dev_groups="dev",
):
    """Manage PEP 751 pylock.toml files.

    Generate, validate, or convert pylock.toml files.

    Examples:

        pipenv pylock --generate

        pipenv pylock --from-pyproject

        pipenv pylock --validate
    """
    from pipenv.utils.pylock import PylockFile, PylockFormatError, PylockVersionError

    project = state.project

    # Parse dev_groups
    groups = [g.strip() for g in dev_groups.split(",") if g.strip()]

    if generate:
        # Generate from Pipfile.lock
        if not project.lockfile_exists:
            err.print("[bold red]No Pipfile.lock found.[/bold red]")
            sys.exit(1)

        try:
            output_path = output or project.pylock_output_path
            pylock_file = PylockFile.from_lockfile(
                lockfile_path=project.lockfile_location,
                pylock_path=output_path,
                dev_groups=groups,
            )
            pylock_file.write()
            console.print(
                f"[bold green]Generated pylock.toml at {output_path}[/bold green]"
            )
        except Exception as e:
            err.print(f"[bold red]Error generating pylock.toml: {e}[/bold red]")
            sys.exit(1)

    elif from_pyproject:
        # Generate skeleton from pyproject.toml
        pyproject_path = Path(project.project_directory) / "pyproject.toml"
        if not pyproject_path.exists():
            err.print("[bold red]No pyproject.toml found.[/bold red]")
            sys.exit(1)

        try:
            output_path = output or project.pylock_output_path
            pylock_file = PylockFile.from_pyproject(
                pyproject_path=pyproject_path,
                pylock_path=output_path,
            )
            pylock_file.write()
            console.print(
                f"[bold green]Generated pylock.toml skeleton at {output_path}[/bold green]"
            )
            console.print(
                "[yellow]Note: This is a skeleton file. Package versions and hashes "
                "need to be resolved by running 'pipenv lock'.[/yellow]"
            )
        except Exception as e:
            err.print(f"[bold red]Error generating pylock.toml: {e}[/bold red]")
            sys.exit(1)

    elif validate:
        # Validate existing pylock.toml
        pylock_path = project.pylock_location
        if not pylock_path:
            err.print("[bold red]No pylock.toml found.[/bold red]")
            sys.exit(1)

        try:
            pylock_file = PylockFile.from_path(pylock_path)
            console.print(
                f"[bold green]✓ Valid pylock.toml (version {pylock_file.lock_version})[/bold green]"
            )
            console.print(f"  Created by: {pylock_file.created_by}")
            console.print(f"  Packages: {len(pylock_file.packages)}")
            if pylock_file.requires_python:
                console.print(f"  Requires Python: {pylock_file.requires_python}")
            if pylock_file.extras:
                console.print(f"  Extras: {', '.join(pylock_file.extras)}")
            if pylock_file.dependency_groups:
                console.print(
                    f"  Dependency Groups: {', '.join(pylock_file.dependency_groups)}"
                )
        except PylockVersionError as e:
            err.print(f"[bold red]Version error: {e}[/bold red]")
            sys.exit(1)
        except PylockFormatError as e:
            err.print(f"[bold red]Format error: {e}[/bold red]")
            sys.exit(1)
        except Exception as e:
            err.print(f"[bold red]Error validating pylock.toml: {e}[/bold red]")
            sys.exit(1)

    else:
        # Default: show status
        pylock_path = project.pylock_location
        if pylock_path:
            try:
                pylock_file = PylockFile.from_path(pylock_path)
                console.print(f"[bold]pylock.toml[/bold]: {pylock_path}")
                console.print(f"  Version: {pylock_file.lock_version}")
                console.print(f"  Created by: {pylock_file.created_by}")
                console.print(f"  Packages: {len(pylock_file.packages)}")
            except Exception as e:
                err.print(f"[yellow]Found pylock.toml but could not parse: {e}[/yellow]")
        else:
            console.print("[dim]No pylock.toml found.[/dim]")
            console.print(
                "Use [bold]pipenv pylock --generate[/bold] to create one from Pipfile.lock"
            )
            console.print(
                "Use [bold]pipenv pylock --from-pyproject[/bold] to create from pyproject.toml"
            )


if __name__ == "__main__":
    cli()


def do_py(project, ctx=None, system=False, bare=False):
    if not project.virtualenv_exists:
        err.print(
            "[red]No virtualenv has been created for this project[/red] "
            f"[yellow bold]{project.project_directory}[/yellow bold] "
            "[red] yet![/red]"
        )
        ctx.abort()

    try:
        (print if bare else console.print)(project._which("python", allow_global=system))
    except AttributeError:
        console.print("No project found!", style="red")
        ctx.abort()
