#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Script to update the OpenAPI client library used by the udm-rest-client
library.
"""

import asyncio
import functools
import inspect
import os
import subprocess  # nosec
import sys
from pathlib import Path
from tempfile import TemporaryDirectory

import click
import docker
import requests
from requests.auth import HTTPBasicAuth
from urllib3.exceptions import InsecureRequestWarning

try:
    from pip import main as pip_main
except ImportError:
    from pip._internal import main as pip_main
if not inspect.isfunction(pip_main):
    pip_main = pip_main.main


DOCKER_IMAGE = "openapitools/openapi-generator-cli:latest-release"
JAR_URL = "https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/4.2.3/openapi-generator-cli-4.2.3.jar"
TARGET_PYTHON_PACKAGE_NAME = "openapi_client_udm"
TARGET_SCHEMA_FILENAME = "udm_openapi.json"
OPENAPI_GENERATE_COMMAND_COMMON = (
    f"generate -g python-legacy --library asyncio --package-name {TARGET_PYTHON_PACKAGE_NAME}"
)
DOCKER_RUN_COMMAND = (
    f"{OPENAPI_GENERATE_COMMAND_COMMON} -i /local/{TARGET_SCHEMA_FILENAME} -o /local/python"
)
JAVA_COMPILE_CMD = (
    f"java -jar {{jar}} {OPENAPI_GENERATE_COMMAND_COMMON} -i {{schema}} -o {{target_dir}}/python"
)
OPENAPI_SCHEMA_URL = "https://{host}/univention/udm/openapi.json"


class OpenAPILibGenerationError(Exception):
    pass


def coro(f):
    """asyncio for click (https://github.com/pallets/click/issues/85)"""

    if sys.version_info.major == 3 and sys.version_info.minor < 7:  # pragma: no cover
        # Python 3.5 and 3.6 compatible code
        f = asyncio.coroutine(f)

        def wrapper(*args, **kwargs):
            loop = asyncio.get_event_loop()
            return loop.run_until_complete(f(*args, **kwargs))

        return functools.update_wrapper(wrapper, f)
    else:
        # Python 3.7+ compatible code
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            return asyncio.run(f(*args, **kwargs))

        return wrapper


def print_error_and_exit(msg, exit_code=1):
    click.secho(msg, fg="red")
    sys.exit(exit_code)


def run_openapi_generator_docker_container(target_dir, quiet, keep_image):
    docker_client = docker.from_env()
    try:
        version = docker_client.version()
        click.echo(
            "Docker daemon: {v[Version]} ({v[ApiVersion]}) {v[Os]} {v[Arch]} {v[KernelVersion]}".format(
                v=version
            )
        )
    except requests.ConnectionError as exc:
        print_error_and_exit(f"Connecting to the Docker daemon: {exc!s}")
    click.echo(f"Downloading/updating Docker image {DOCKER_IMAGE!r} (~136 MiB)...")
    docker_client.images.pull(DOCKER_IMAGE)
    click.echo("Starting Docker container and generating Python code...")
    try:
        # would have liked to append '--verbose' to DOCKER_RUN_COMMAND, but then the JVM will:
        # java.lang.OutOfMemoryError: Java heap space
        docker_log = docker_client.containers.run(
            image=DOCKER_IMAGE,
            command=DOCKER_RUN_COMMAND,
            auto_remove=True,
            user=os.getuid(),
            volumes={target_dir: {"bind": "/local", "mode": "rw"}},
        )
    except docker.errors.ContainerError as exc:
        msg = f"Error in container: {exc!s}"
        if exc.stderr:
            f"{msg}\n{exc.stderr!s}"
        print_error_and_exit(msg)
    if not quiet:
        click.echo(docker_log)
    if keep_image:
        click.echo(f"Keeping Docker image {DOCKER_IMAGE}.")
    else:
        click.echo(f"Removing Docker image {DOCKER_IMAGE}...")
        docker_client.images.remove(DOCKER_IMAGE)


def download_jar(target_path: Path):
    click.echo("Downloading OpenAPI generator JAR (~19 MiB)...")
    try:
        with requests.get(JAR_URL, stream=True) as resp, target_path.open("wb") as fp:
            if resp.status_code != 200:
                print_error_and_exit(
                    f"Error downloading OpenAPI generator JAR from {JAR_URL!r}: [{resp.status_code}] {resp.reason}"
                )
            for chunk in resp.iter_content(chunk_size=8192):
                if chunk:
                    fp.write(chunk)
            click.echo(f"Downloaded {fp.tell() / 1024:.1f} KiB.")
    except requests.ConnectionError as exc:
        print_error_and_exit(str(exc))


def run_openapi_generator_local_java(target_dir: str, quiet: bool, jar_path: Path = None):
    if not jar_path:
        jar_path = Path(target_dir, "openapi-generator.jar")
        download_jar(jar_path)
    cmd = JAVA_COMPILE_CMD.format(
        jar=str(jar_path),
        schema=str(Path(target_dir, TARGET_SCHEMA_FILENAME)),
        target_dir=target_dir,
    ).split()
    click.echo("Starting to generate Python code...")
    process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)  # nosec
    stdout, stderr = process.communicate()
    if process.returncode:
        print_error_and_exit(stdout.decode())
    elif not quiet:
        click.echo(stdout.decode())


def get_openapi_schema(host: str, user: str, password: str, secure: bool) -> str:
    url = OPENAPI_SCHEMA_URL.format(host=host)

    click.echo(f"Downloading OpenAPI schema from {url!r}...")

    request_kwargs = {}
    if not secure:
        request_kwargs["verify"] = False
        requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
    if user and password:
        request_kwargs["auth"] = HTTPBasicAuth(user, password)

    try:
        resp = requests.get(url, **request_kwargs)
    except requests.ConnectionError as exc:
        print_error_and_exit(str(exc))
    if resp.status_code == 401:
        print_error_and_exit(
            f"The request was not authorized (401). Please provide user credentials to download the OpenAPI schema from {url!r}."
        )
    if resp.status_code != 200:
        print_error_and_exit(
            f"Error downloading OpenAPI schema from {url!r}: [{resp.status_code}] {resp.reason}"
        )
    txt = resp.text
    click.echo(f"Downloaded {len(txt)/1024:.1f} KiB.")
    return txt


@click.command(
    help="Update the OpenAPI client library used by the udm-rest-client "
    "library, using the OpenAPI schema from UCS server 'HOST' (FQDN or IP"
    " address)."
)
@click.argument("host")
@click.option(
    "--generator",
    type=click.Choice(["docker", "java"], case_sensitive=False),
    required=True,
    help="Download and use an OpenAPI Generator either as a Docker container "
    "or as a JAR file. Requires either a running Docker daemon or a local"
    " Java (>=8) installation.",
)
@click.option(
    "--jar",
    type=click.Path(exists=True, readable=True),
    help="OpenAPI Generator JAR file to use with '--generator java'. If not"
    "given, the JAR file will be downloaded automatically.",
)
@click.option(
    "--keep-image/--dont-keep-image",
    default=False,
    help="Whether to keep the OpenAPI Generator Docker image (when '--generator" " docker' is used).",
)
@click.option(
    "--secure/--insecure",
    default=True,
    help="Whether to ignore an SSL verification error.",
)
@click.option(
    "--quiet/--verbose",
    default=True,
    help="Whether to print the output of the OpenAPI Generator to the screen.",
)
@click.option(
    "--system/--user",
    default=True,
    help="Whether to install into a system or the users home directory",
)
@click.option("--username", help="The username to authenticate against the UDM REST API.")
@click.option("--password", help="The password to authenticate against the UDM REST API.")
@coro
async def update_openapi_client(
    host: str,
    generator: str,
    jar: str,
    keep_image: bool,
    secure: bool,
    quiet: bool,
    system: bool,
    username: str,
    password: str,
):
    if not host:
        print_error_and_exit("Address (FQDN or IP address) of UCS host required.")
    if jar and generator != "java":
        print_error_and_exit("The --jar option can only be used together with '--generator java'.")
    if jar:
        jar = Path(jar)
    txt = get_openapi_schema(host, username, password, secure)
    with TemporaryDirectory() as temp_dir, open(
        Path(temp_dir, TARGET_SCHEMA_FILENAME), "w"
    ) as temp_file_fp:
        temp_file_fp.write(txt)
        temp_file_fp.flush()

        click.echo(
            f"Generating OpenAPI client library {TARGET_PYTHON_PACKAGE_NAME!r}"
            f" using {generator!r} mechanism..."
        )
        if generator == "docker":
            run_openapi_generator_docker_container(temp_dir, quiet, keep_image)
        else:
            run_openapi_generator_local_java(temp_dir, quiet, jar)
        click.echo("Installing package via pip...")
        if system:
            pip_main(["install", "--compile", "--upgrade", str(Path(temp_dir, "python/"))])
        else:
            pip_main(
                [
                    "install",
                    "--user",
                    "--compile",
                    "--upgrade",
                    str(Path(temp_dir, "python/")),
                ]
            )


if __name__ == "__main__":
    update_openapi_client()
