#!/usr/bin/env python
from __future__ import print_function

import os
import argparse
import sys

import openidc_client
import requests.exceptions

import odcs.client.odcs
import json

env_config = {
    "fedora": {
        "prod": {"server_url": "https://odcs.fedoraproject.org"},
        "staging": {"server_url": "https://odcs.stg.fedoraproject.org"},
    },
    "redhat": {
        "prod": {"server_url": "https://odcs.engineering.redhat.com"},
        "staging": {"server_url": "https://odcs.stage.engineering.redhat.com"},
    },
}

id_provider_config = {
    "prod": "https://id.fedoraproject.org/openidc/",
    "staging": "https://id.stg.fedoraproject.org/openidc/",
}

parser = argparse.ArgumentParser(
    description="""\
%(prog)s - Command line client.

If you have problems authenticating with OpenID Connect, try:

  $ rm -rf ~/.openidc/

Example usage:

""",
    formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
    "--redhat",
    action="store_const",
    const="redhat",
    default="fedora",
    dest="infra",
    help="Use internal ODCS infra environment. If omitted, Fedora Infra will "
    "be used by default.",
)
parser.add_argument(
    "--staging",
    action="store_const",
    const="staging",
    default="prod",
    dest="env",
    help="Use Fedora Infra or internal staging environment, which depends on "
    "if --redhat is specified. If omitted, production environment will "
    "be used.",
)
parser.add_argument("--server", default=None, help="Use custom ODCS server.")
parser.add_argument(
    "--token", default=None, help="OpenIDC token to use or path to token file"
)
parser.add_argument(
    "--no-wait",
    action="store_true",
    help="When used, odcs client will not wait for the action to finish.",
)
parser.add_argument(
    "-q", "--quiet", action="store_true", help="Run without detailed log messages"
)
parser.add_argument("--watch", action="store_true", help="Watch compose logs")

subparsers = parser.add_subparsers(
    description="These commands you can use to operate composes with ODCS"
)


KNOWN_ARGS = {
    "--flag": dict(
        default=[], action="append", help="Flag to pass to influence the compose."
    ),
    "--target-dir": dict(default="", help="Name of the server-side target directory."),
    "--result": dict(
        default=[],
        action="append",
        help="Results of a compose to influence the compose.",
    ),
    "--sigkey": dict(
        default=[],
        action="append",
        help="ODCS will require that all packages are signed by this "
        'signing key ID. Example: "FD431D51". You may use this option '
        "multiple times to specify multiple key IDs. ODCS will choose "
        "signed packages according to the order of the key IDs that "
        'you specify here. Use "--sigkey none" to allow unsigned '
        "packages. If you do not specify any --sigkey option, ODCS "
        "will use the default signing key list (defined on the server).",
    ),
    "--koji-event": dict(
        default=None, help="Koji event for populating package set", type=int
    ),
    "--arch": dict(
        default=[], action="append", help="Koji arch to build the compose for."
    ),
    "--module-defaults-url": dict(
        default="",
        metavar="module_defaults_url",
        help="URL to git repository with module defaults.",
    ),
    "--module-defaults-commit": dict(
        default="",
        metavar="module_defaults_commit",
        help="Git commit/branch from which to take the module defaults.",
    ),
    "--modular-tag": dict(
        default=[],
        action="append",
        metavar="modular_koji_tags",
        help="Koji tag with module builds.",
    ),
    "--lookaside-repo": dict(
        default=[],
        action="append",
        metavar="lookaside_repos",
        help="Specify lookaside repositories.",
    ),
    "--scratch-module": dict(
        default=[],
        action="append",
        metavar="scratch_modules",
        help="Scratch modules to be included in the compose with format N:S:V:C",
    ),
    "--label": dict(default=None, help="Label for raw_config compose."),
    "--compose-type": dict(default=None, help="Compose type for raw_config compose."),
    "--build": dict(
        default=[], action="append", help="Builds to be included in the compose."
    ),
}


def _add_arguments(parser, *args):
    for arg in args:
        try:
            parser.add_argument(arg, **KNOWN_ARGS[arg])
        except KeyError:
            raise ValueError("Unknown argument %s." % arg)
    return parser


create_command_deprecated = """
Deprecated: Please use create-* commands instead of the deprecated create command.
The create command will be removed and bugs with it are not going to be fixed.
"""
create_parser = subparsers.add_parser(
    "create",
    help="Low-level command to create a new compose (Deprecated)",
    description=create_command_deprecated,
)
create_parser.set_defaults(command="create")
create_parser.add_argument(
    "source_type",
    default=None,
    choices=["tag", "module", "raw_config", "pulp", "build"],
    help="Type for the source, for example: tag.",
)
create_parser.add_argument(
    "source",
    default="",
    help="Source for the compose. May be a koji tag or a "
    "whitespace separated list of modules.",
)
create_parser.add_argument(
    "packages",
    metavar="package",
    nargs="*",
    help="Packages to be included in the compose.",
)
create_parser.add_argument(
    "builds", metavar="build", nargs="*", help="Builds to be included in the compose."
)
_add_arguments(
    create_parser,
    "--result",
    "--sigkey",
    "--koji-event",
    "--arch",
    "--module-defaults-url",
    "--module-defaults-commit",
    "--modular-tag",
    "--lookaside-repo",
    "--label",
    "--compose-type",
    "--target-dir",
    "--flag",
    "--scratch-module",
)


create_tag_parser = subparsers.add_parser(
    "create-tag", help="Create new compose from Koji tag."
)
create_tag_parser.set_defaults(command="create-tag")
create_tag_parser.add_argument("tag", default="", help="Koji tag name.")
create_tag_parser.add_argument(
    "packages",
    metavar="package",
    nargs="*",
    help="Koji packages to be included in the compose.",
)
_add_arguments(
    create_tag_parser,
    "--sigkey",
    "--koji-event",
    "--arch",
    "--module-defaults-url",
    "--module-defaults-commit",
    "--modular-tag",
    "--lookaside-repo",
    "--target-dir",
    "--build",
    "--flag",
    "--scratch-module",
)


create_module_parser = subparsers.add_parser(
    "create-module", help="Create new compose from modules."
)
create_module_parser.set_defaults(command="create-module")
create_module_parser.add_argument(
    "modules",
    metavar="modules",
    nargs="*",
    help="List of modules in N:S, N:S:V or N:S:V:C format.",
)
create_module_parser.add_argument(
    "--base-module-br-name",
    help="The name of a base module the module buildrequires",
)
create_module_parser.add_argument(
    "--base-module-br-stream",
    help="The stream of a base module the module buildrequires",
)
create_module_parser.add_argument(
    "--base-module-br-stream-version-lte",
    type=int,
    help=(
        "The numeric value of the stream version (el8.3.1 is 80301, f33 is 33) of "
        "the buildrequired module specified by --base-module-br-name must be less "
        "than equal to the given value."
    ),
)
create_module_parser.add_argument(
    "--base-module-br-stream-version-gte",
    type=int,
    help=(
        "The numeric value of the stream version (el8.3.1 is 80301, f33 is 33) of "
        "the buildrequired module specified by --base-module-br-name must be greater "
        "than equal to the given value."
    ),
)
_add_arguments(
    create_module_parser,
    "--sigkey",
    "--arch",
    "--modular-tag",
    "--module-defaults-url",
    "--module-defaults-commit",
    "--lookaside-repo",
    "--target-dir",
    "--flag",
    "--scratch-module",
)


create_pulp_parser = subparsers.add_parser(
    "create-pulp", help="Create new compose from Pulp content_sets."
)
create_pulp_parser.set_defaults(command="create-pulp")
create_pulp_parser.add_argument(
    "content_sets",
    metavar="content_set",
    nargs="+",
    help="Content sets to be included in the compose.",
)
_add_arguments(create_pulp_parser, "--target-dir", "--flag")


create_raw_config_parser = subparsers.add_parser(
    "create-raw-config", help="Create new compose from Pungi raw configuration."
)
create_raw_config_parser.set_defaults(command="create-raw-config")
create_raw_config_parser.add_argument(
    "raw_config_name", help="Name of raw_config compose as defined in ODCS Server."
)
create_raw_config_parser.add_argument(
    "raw_config_commit", help="Commit or branch name to get raw_config from."
)
_add_arguments(
    create_raw_config_parser,
    "--sigkey",
    "--label",
    "--compose-type",
    "--koji-event",
    "--target-dir",
    "--build",
)


create_build_parser = subparsers.add_parser(
    "create-build", help="Create new compose from Koji builds."
)
create_build_parser.set_defaults(command="create-build")
create_build_parser.add_argument(
    "builds", metavar="NVR", nargs="+", help="Koji builds NVRs."
)
_add_arguments(create_build_parser, "--sigkey", "--flag", "--target-dir")


wait_parser = subparsers.add_parser("wait", help="wait for a compose to finish")
wait_parser.set_defaults(command="wait")
wait_parser.add_argument("compose_id", default=None, help="ODCS compose id")
wait_parser.add_argument("--watch", action="store_true", help="Watch compose logs")


delete_parser = subparsers.add_parser("delete", help="delete compose")
delete_parser.set_defaults(command="delete")
delete_parser.add_argument("compose_id", default=None, help="ODCS compose id")

renew_parser = subparsers.add_parser("renew", help="renew compose")
renew_parser.set_defaults(command="renew")
renew_parser.add_argument("compose_id", default=None, help="ODCS compose id")

get_parser = subparsers.add_parser("get", help="get compose info")
get_parser.set_defaults(command="get")
get_parser.add_argument("compose_id", default=None, help="ODCS compose id")

about_parser = subparsers.add_parser("about", help="Get information about ODCS server")
about_parser.set_defaults(command="about")

args = parser.parse_args()

if not hasattr(args, "command"):
    parser.print_help()
    sys.exit(0)

if args.server is None:
    odcs_url = env_config[args.infra][args.env]["server_url"]
else:
    odcs_url = args.server

if args.infra == "fedora":
    if args.token:
        if os.path.exists(args.token):
            with open(args.token, "r") as token_file:
                token = token_file.readline().strip()
        else:
            token = args.token
    else:
        id_provider = id_provider_config[args.env]

        # Get the auth token using the OpenID client.
        oidc = openidc_client.OpenIDCClient(
            "odcs",
            id_provider,
            {"Token": "Token", "Authorization": "Authorization"},
            "odcs-authorizer",
            "notsecret",
        )

        scopes = [
            "openid",
            "https://id.fedoraproject.org/scope/groups",
            "https://pagure.io/odcs/new-compose",
            "https://pagure.io/odcs/renew-compose",
            "https://pagure.io/odcs/delete-compose",
        ]
        try:
            token = oidc.get_token(scopes, new_token=True)
            token = oidc.report_token_issue()
        except requests.exceptions.HTTPError as e:
            print(e.response.text)
            raise

    client = odcs.client.odcs.ODCS(
        odcs_url,
        auth_mech=odcs.client.odcs.AuthMech.OpenIDC,
        openidc_token=token,
    )
else:
    client = odcs.client.odcs.ODCS(
        odcs_url,
        auth_mech=odcs.client.odcs.AuthMech.Kerberos,
    )

request_args = {}
if getattr(args, "flag", False):
    request_args["flags"] = args.flag
if getattr(args, "arch", False):
    request_args["arches"] = args.arch
if getattr(args, "lookaside_repo", False):
    request_args["lookaside_repos"] = args.lookaside_repo
if getattr(args, "label", False):
    request_args["label"] = args.label
if getattr(args, "compose_type", False):
    request_args["compose_type"] = args.compose_type
if getattr(args, "target_dir", False):
    request_args["target_dir"] = args.target_dir

try:
    args.sigkey = [key.replace("none", "") for key in getattr(args, "sigkey", [])]
    if args.command == "create":
        print(create_command_deprecated, file=sys.stderr)
        result = client.new_compose(
            source=args.source,
            source_type=args.source_type,
            packages=args.packages,
            results=args.result,
            sigkeys=args.sigkey,
            koji_event=args.koji_event,
            builds=args.builds,
            modular_koji_tags=args.modular_tag,
            module_defaults_url=args.module_defaults_url,
            module_defaults_commit=args.module_defaults_commit,
            scratch_modules=args.scratch_module,
            **request_args
        )
    elif args.command == "create-tag":
        source = odcs.client.odcs.ComposeSourceTag(
            args.tag,
            args.packages,
            args.build,
            args.sigkey,
            args.koji_event,
            args.modular_tag,
            args.module_defaults_url,
            args.module_defaults_commit,
            args.scratch_module,
        )
        result = client.request_compose(source, **request_args)
    elif args.command == "create-module":
        if not args.modules and not args.scratch_module:
            create_module_parser.error("Please give a module or --scratch-module")
        source = odcs.client.odcs.ComposeSourceModule(
            args.modules,
            args.sigkey,
            args.module_defaults_url,
            args.module_defaults_commit,
            args.scratch_module,
            base_module_br_name=args.base_module_br_name,
            base_module_br_stream=args.base_module_br_stream,
            base_module_br_stream_version_lte=args.base_module_br_stream_version_lte,
            base_module_br_stream_version_gte=args.base_module_br_stream_version_gte,
            modular_koji_tags=args.modular_tag,
        )
        result = client.request_compose(source, **request_args)
    elif args.command == "create-pulp":
        source = odcs.client.odcs.ComposeSourcePulp(args.content_sets)
        result = client.request_compose(source, **request_args)
    elif args.command == "create-raw-config":
        source = odcs.client.odcs.ComposeSourceRawConfig(
            args.raw_config_name,
            args.raw_config_commit,
            args.koji_event,
            sigkeys=args.sigkey,
            builds=args.build,
        )
        result = client.request_compose(source, **request_args)
    elif args.command == "create-build":
        source = odcs.client.odcs.ComposeSourceBuild(args.builds, args.sigkey)
        result = client.request_compose(source, **request_args)
    elif args.command == "wait":
        result = {"id": int(args.compose_id)}
    elif args.command == "delete":
        args.no_wait = True
        result = client.delete_compose(args.compose_id)
    elif args.command == "renew":
        result = client.renew_compose(args.compose_id)
    elif args.command == "get":
        result = client.get_compose(args.compose_id)
    elif args.command == "about":
        result = client.about()
    else:
        print("Unknown command %s" % args.command)
except requests.exceptions.HTTPError:
    # error message gets printed in ODCS class.
    sys.exit(-1)

if args.no_wait or args.command == "about":
    print(json.dumps(result, indent=4, sort_keys=True))
else:
    if not args.quiet:
        print(
            "Waiting for command %s on compose %d to finish."
            % (args.command, result["id"])
        )
    try:
        result = client.wait_for_compose(result["id"], 3600, watch_logs=args.watch)
    except (KeyboardInterrupt, SystemExit):
        pass

    print(json.dumps(result, indent=4, sort_keys=True))
