"""DataIntegration python management commands."""

import sys
from dataclasses import asdict
from re import match

import click
from click import UsageError
from cmem.cmempy.workspace.python import (
    install_package_by_file,
    install_package_by_name,
    list_packages,
    list_plugins,
    uninstall_package,
    update_plugins,
)

from cmem_cmemc import completion
from cmem_cmemc.commands import CmemcCommand, CmemcGroup
from cmem_cmemc.context import ApplicationContext
from cmem_cmemc.utils import get_published_packages


def _get_package_id(module_name: str) -> str:
    """Return package identifier."""
    return module_name.split(".")[0].replace("_", "-")


def _looks_like_a_package(package: str) -> bool:
    """Check if a string looks like a package requirement string."""
    if match(r"^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*((==|<=|>=|>|<).*)?$", package):
        return True
    return False


@click.command(cls=CmemcCommand, name="install")
@click.argument(
    "PACKAGE",
    shell_complete=completion.installable_packages,
    type=click.Path(readable=True, allow_dash=False, dir_okay=False),
)
@click.pass_obj
def install_command(app: ApplicationContext, package: str) -> None:
    """Install a python package to the workspace.

    This command is essentially a `pip install` in the remote python
    environment.

    You can install a package by uploading a source distribution
    .tar.gz file, by uploading a build distribution .whl file, or by
    specifying a package name, i.e., a pip requirement specifier with a
    package name available on pypi.org (e.g. `requests==2.27.1`).

    Note: The tab-completion of this command lists only public packages from
    pypi.org and not from additional or changed python package repositories you
    may have configured on the server.
    """
    app.echo_info(f"Install package {package} ... ", nl=False)
    try:
        response = install_package_by_file(package_file=package)
    except FileNotFoundError as error:
        if not _looks_like_a_package(package):
            raise ValueError(
                f"{package} does not look like a package name or requirement "
                "string, and a file with this name also does not exists."
            ) from error
        response = install_package_by_name(package_name=package)

    # DI >= 24.1 has a combine 'output' key, before 24.1 'standardOutput' and 'errorOutput' existed
    output: list[str] = []
    output.extend(response.get("output", "").splitlines())
    output.extend(response.get("standardOutput", "").splitlines())
    output.extend(response.get("errorOutput", "").splitlines())
    for output_line in output:
        app.echo_debug(output_line)

    if response["success"]:
        app.echo_success("done")
    else:
        app.echo_error("error")
        for output_line in output:
            app.echo_error(output_line)
    app.echo_debug("Updated Plugins: " + str(update_plugins()))


@click.command(cls=CmemcCommand, name="uninstall")
@click.argument("PACKAGE_NAME", nargs=-1, shell_complete=completion.installed_package_names)
@click.option(
    "-a",
    "--all",
    "all_",
    is_flag=True,
    help="This option removes all installed packages from the system,"
    " leaving only the pre-installed mandatory packages in the environment.",
)
@click.pass_obj
def uninstall_command(app: ApplicationContext, package_name: str, all_: bool) -> None:
    """Uninstall a python packages from the workspace.

    This command is essentially a `pip uninstall` in the remote
    python environment.
    """
    if all_:
        app.echo_info("Wiping Python environment ... ", nl=False)
        uninstall_package(package_name="--all")
        app.echo_success("done")
        app.echo_debug("Updated Plugins: " + str(update_plugins()))
        return

    if not package_name:
        raise UsageError(
            "Either give at least one package name or wipe the whole python"
            " environment with the '--all' option."
        )
    packages = list_packages()
    app.echo_debug(packages)
    for _ in package_name:
        app.echo_info(f"Uninstall package {_} ... ", nl=False)
        if _ not in [package["name"] for package in packages]:
            app.echo_error("not installed")
            app.echo_debug("Updated Plugins: " + str(update_plugins()))
            sys.exit(1)
        response = uninstall_package(package_name=_)
        output: list[str] = []
        output.extend(response.get("output", "").splitlines())
        output.extend(response.get("standardOutput", "").splitlines())
        output.extend(response.get("errorOutput", "").splitlines())
        for output_line in output:
            app.echo_debug(output_line)
        if response["success"]:
            app.echo_success("done")
        else:
            app.echo_error("error")
            app.echo_debug(response.content.decode())
    app.echo_debug("Updated Plugins: " + str(update_plugins()))


@click.command(cls=CmemcCommand, name="list")
@click.option("--raw", is_flag=True, help="Outputs raw JSON.")
@click.option(
    "--id-only",
    is_flag=True,
    help="Lists only package identifier. " "This is useful for piping the IDs into other commands.",
)
@click.option(
    "--available",
    is_flag=True,
    help="Instead of listing installed packages, this option lists installable packages"
    " from pypi.org, which are prefixed with 'cmem-plugin-' and so are most likely"
    " Corporate Memory plugin packages.",
)
@click.pass_obj
def list_command(app: ApplicationContext, raw: bool, id_only: bool, available: bool) -> None:
    """List installed python packages.

    This command is essentially a `pip list` in the remote python environment.

    It outputs a table of python package identifiers with version information.
    """
    if available:
        published_packages = get_published_packages()

        if raw:
            app.echo_info_json([asdict(_) for _ in published_packages])
            return

        if id_only:
            for _ in published_packages:
                app.echo_info(_.name)
            return

        table_published = [
            (_.name, _.version, str(_.published)[:10], _.description) for _ in published_packages
        ]
        app.echo_info_table(
            table_published, headers=["Name", "Version", "Published", "Description"], sort_column=0
        )
        return

    installed_packages = list_packages()
    if raw:
        app.echo_info_json(installed_packages)
        return

    if id_only:
        for package in installed_packages:
            app.echo_info(package["name"])
        return

    table_installed = [(_["name"], _["version"]) for _ in installed_packages]
    app.echo_info_table(table_installed, headers=["Name", "Version"], sort_column=0)


@click.command(cls=CmemcCommand, name="list-plugins")
@click.option("--raw", is_flag=True, help="Outputs raw JSON.")
@click.option("--id-only", is_flag=True, help="Lists only plugin identifier.")
@click.option("--package-id-only", is_flag=True, help="Lists only plugin package identifier.")
@click.pass_obj
def list_plugins_command(
    app: ApplicationContext, raw: bool, id_only: bool, package_id_only: bool
) -> None:
    """List installed workspace plugins.

    This commands lists all discovered plugins.

    Note: The plugin discovery is restricted to package prefix (`cmem-`).
    """
    raw_output = list_plugins()
    try:
        plugins = raw_output["plugins"]  # DI >= 22.1.1 output
    except TypeError:
        plugins = raw_output  # DI <= 22.1 output

    if raw:
        app.echo_info_json(plugins)
        return

    if id_only:
        for plugin in sorted(plugins, key=lambda k: k["id"].lower()):
            app.echo_info(plugin["id"])
        return

    if package_id_only:
        package_ids = set()
        for plugin in plugins:
            package_ids.add(_get_package_id(plugin["moduleName"]))
        for package_id in sorted(package_ids):
            app.echo_info(package_id)
        return

    table = [
        (
            _["id"],
            _get_package_id(_["moduleName"]),
            _["pluginType"],
            _["label"],
        )
        for _ in sorted(plugins, key=lambda k: k["id"].lower())
    ]
    app.echo_info_table(table, headers=["ID", "Package ID", "Type", "Label"], sort_column=0)
    if "error" in raw_output:
        app.echo_error(raw_output["error"])


@click.command(cls=CmemcCommand, name="open")
@click.argument(
    "PACKAGE",
    shell_complete=completion.published_package_names,
)
@click.pass_obj
def open_command(app: ApplicationContext, package: str) -> None:
    """Open a package pypi.org page in the browser.

    With this command, you can open the pypi.org page of a published package in your
    browser. From there, you can follow links, review the version history as well
    as the origin of the package, and read the provided documentation.
    """
    full_url = f"https://pypi.org/project/{package}/"
    app.echo_debug(f"Open {package}: {full_url}")
    click.launch(full_url)


@click.group(cls=CmemcGroup)
def python() -> CmemcGroup:  # type: ignore[empty-body]
    """List, install, or uninstall python packages.

    Python packages are used to extend the DataIntegration workspace
    with python plugins. To get a list of installed packages, execute the
    list command.

    Warning: Installing packages from unknown sources is not recommended.
    Plugins are not verified for malicious code.
    """


python.add_command(install_command)
python.add_command(uninstall_command)
python.add_command(list_command)
python.add_command(list_plugins_command)
python.add_command(open_command)
