#!/usr/bin/env python

""" freshmaker-cli - Read-only client for Freshmaker REST API


Example usage:
    - List all the events handled by Freshmaker on redhat staging instance:
        ./freshmaker-cli --redhat --stage
    - List particular events based on the event id:
        ./freshmaker-cli --redhat --stage 1 2 3
    - Search for events matching particular event key:
        ./freshmaker-cli --redhat --stage --search-key 29110
    - Search for events which lead built particular NVR in Koji:
        ./freshmaker-cli --redhat --stage --rebuilt-nvr NVR
    - Search for events which upated particular NVR:
        ./freshmaker-cli --redhat --stage --original-nvr NVR
    - Search for events building particular artifact name
        ./freshmaker-cli --redhat --stage --name httpd24-docker
    - Search for events in particular state
        ./freshmaker-cli --redhat --stage --state skipped
"""

from __future__ import print_function

import textwrap
import argparse
import sys
import requests
from tabulate import tabulate

# Disable InsecureRequestWarning
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)


EVENT_TYPES = [
    "Module build state changed",
    "Module git definition changed",
    "RPM git definition changed",
    "Internal Freshmaker event type",
    "Container image git definition changed",
    "Bodhi update added to stable",
    "Koji task state changed",
    "Koji RPM signed",
    "Errata advisory RPMS signed",
    "Brew container task state changed",
    "Errata advisory state changed",
    "Manual rebuild triggerred",
    "ODCS compose state changed",
]


EVENT_STATES = [
    "init",
    "building",
    "complete",
    "failed",
    "skipped",
    ]

def split_nvr_epoch(nvre):
    """Split nvre to N-V-R and E.

    @param nvre: E:N-V-R or N-V-R:E string
    @type nvre: str
    @return: (N-V-R, E)
    @rtype: (str, str)
    """

    if ":" in nvre:
        if nvre.count(":") != 1:
            raise ValueError("Invalid NVRE: %s" % nvre)

        nvr, epoch = nvre.rsplit(":", 1)
        if "-" in epoch:
            if "-" not in nvr:
                # switch nvr with epoch
                nvr, epoch = epoch, nvr
            else:
                # it's probably N-E:V-R format, handle it after the split
                nvr, epoch = nvre, ""
    else:
        nvr, epoch = nvre, ""

    return (nvr, epoch)


def parse_nvr(nvre):
    """Split N-V-R into a dictionary.

    @param nvre: N-V-R:E, E:N-V-R or N-E:V-R string
    @type nvre: str
    @return: {name, version, release, epoch}
    @rtype: dict
    """

    if "/" in nvre:
        nvre = nvre.split("/")[-1]

    nvr, epoch = split_nvr_epoch(nvre)

    nvr_parts = nvr.rsplit("-", 2)
    if len(nvr_parts) != 3:
        raise ValueError("Invalid NVR: %s" % nvr)

    # parse E:V
    if epoch == "" and ":" in nvr_parts[1]:
        epoch, nvr_parts[1] = nvr_parts[1].split(":", 1)

    # check if epoch is empty or numeric
    if epoch != "":
        try:
            int(epoch)
        except ValueError:
            raise ValueError("Invalid epoch '%s' in '%s'" % (epoch, nvr))

    result = dict(zip(["name", "version", "release"], nvr_parts))
    result["epoch"] = epoch
    return result

def get_api_url(deployment, env):
    API_URLS = {
        "redhat": {
            "prod": "https://freshmaker.host.prod.eng.bos.redhat.com",
            "stage": "https://freshmaker.host.stage.eng.bos.redhat.com",
            "qe": "https://freshmaker.host.qe.eng.pek2.redhat.com",
            "dev": "https://freshmaker-dev.cloud.upshift.engineering.redhat.com",
        },
        "fedora": {}
    }

    if deployment not in API_URLS:
        return None

    envs = API_URLS[deployment]
    if env not in envs:
        return None

    return envs[env]


def _get_freshmaker_items(url, params, limit=None):
    """
    Sends the GET request to `url` with URL params `params`. Returns the
    "items" list of returned JSON data. When `limit` is set, returns only N
    items.
    """
    params["page"] = 1
    if not limit or limit > 100:
        params["per_page"] = 100
    else:
        params["per_page"] = limit

    ret_items = []
    while True:
        print("Getting messages from Freshmaker, page", params["page"])
        r = requests.get(url, verify=False, params=params)
        ret = r.json()
        items = ret["items"]
        if not items:
            break
        ret_items += items

        if limit:
            items_count = len(ret_items)
            if items_count >= limit or items_count == 0:
                break
            elif limit - items_count < params["per_page"]:
                params["per_page"] = limit - items_count

        params["page"] = params["page"] + 1
    return ret_items


def get_events_from_builds(api_url, limit=None, **kwargs):
    params = {"order_by": "-event_id"}
    for key, value in kwargs.items():
        if value:
            params[key] = value
    builds = _get_freshmaker_items("%s/api/1/builds" % api_url, params,
                                   limit=limit)
    # Remove duplicates and sort by event_id.
    ids = list(set([build["event_id"] for build in builds]))
    ids.sort()
    return get_freshmaker_events_by_ids(api_url, ids)


def get_freshmaker_events(api_url, search_key=None, rebuilt_nvr=None,
                          original_nvr=None, name=None, states=None,
                          limit=None):
    if rebuilt_nvr or original_nvr or name:
        return get_events_from_builds(
            api_url, rebuilt_nvr=rebuilt_nvr, original_nvr=original_nvr,
            name=name, limit=limit)

    params = {}
    if states:
        params["state"] = [EVENT_STATES.index(state) for state in states]
    if search_key:
        params["search_key"] = search_key

    events = _get_freshmaker_items("%s/api/1/events" % api_url, params,
                                   limit=limit)
    events.reverse()
    return events


def get_freshmaker_events_by_ids(api_url, ids):
    events = []
    for i in ids:
        print("Getting Freshmaker event id", i)
        url = '%s/api/1/events/%s' % (api_url, str(i))

        r = requests.get(url, verify=False)
        ret = r.json()
        if not "error" in ret:
            events += [ret]
        else:
            print(ret["error"])

    events = sorted(events, key=lambda k: k['id'])
    return events


def print_events(events):
    table = [["ID", "Event type", "State", "Event key", "Number of builds", "State reason"]]
    for event in events:
        row = [event["id"], EVENT_TYPES[event["event_type_id"]],
               EVENT_STATES[event["state"]], event["search_key"][:20],
               len(event["builds"]), event["state_reason"]]
        table.append(row)
    print(tabulate(table, headers="firstrow"))


def make_tree_dict(child_parent_mapping):
    """
    Takes the list of tuples with child-parent mapping and constructs a tree
    represented by nested dict.
    """
    has_parent = set()
    all_items = {}
    for child, parent in child_parent_mapping:
        if parent not in all_items:
            all_items[parent] = {}
        if child not in all_items:
            all_items[child] = {}
        all_items[parent][child] = all_items[child]
        has_parent.add(child)

    result = {}
    for key, value in all_items.items():
        if key not in has_parent:
            result[key] = value
    return result


def print_detailed_events(events, states=None):
    """
    Prints detailed information about events - this mainly means the list of
    artifacts build as part of each event.
    """
    for event in events:
        print("Event id:", event["id"])

        # Temporary list with child-parent mapping between the builds.
        builds_mapping = []
        # Temporary dict to find build based on rebuilt_nvr.
        builds_by_nvr = {}
        for build in event["builds"]:
            builds_mapping.append(
                (build["rebuilt_nvr"], build["build_args"]["parent"]))
            builds_by_nvr[build["rebuilt_nvr"]] = build

        tree = make_tree_dict(builds_mapping)

        def _prepare_table(subtree, level = 0):
            """
            Function called recursively for each level of tree generating the
            rows with builds which can be later consumed by `tabulate()`
            method.
            """
            rows = []
            for nvr, children in subtree.items():
                if nvr in builds_by_nvr:
                    build = builds_by_nvr[nvr]

                    if states:
                        if build['state_name'].lower() not in states:
                            continue

                    nvr = parse_nvr(build["original_nvr"])
                    new_nvr = parse_nvr(build["rebuilt_nvr"])

                    padding = "-" * (level * 2) + ">" + " " * (8 - level * 2)
                    row = [padding + str(build["id"]), build["build_id"],
                           build["state_name"], nvr["name"],
                           "%s-%s" % (nvr["version"], nvr["release"]),
                           "%s-%s" % (new_nvr["version"], new_nvr["release"]),
                           build["state_reason"]]
                    rows.append(row)
                rows += _prepare_table(children, level + 1)

            return rows

        table = [["Build ID", "Task ID", "State", "Name", "NVR",
                  "Rebuilt NVR", "State Reason"]]
        table += _prepare_table(tree)

        print(tabulate(table, headers="firstrow"))


parser = argparse.ArgumentParser(
    description=textwrap.dedent(__doc__),
    formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
    '--stage', default=False, action='store_true',
    help="Use the staging environment.")
parser.add_argument(
    '--qe', default=False, action='store_true',
    help="Use the qe environment.")
parser.add_argument(
    '--dev', default=False, action='store_true',
    help="Use the dev environment.")
parser.add_argument(
    '--redhat', default=False, action='store_true',
    help="Use the redhat instances.")
parser.add_argument(
    'events', metavar='event', nargs='*',
    help='Event ids to be queried')
parser.add_argument(
    '--search-key', default=None,
    help="Search for events matching the search_key")
parser.add_argument(
    '--rebuilt-nvr', default=None,
    help="NVR of rebuilt artifact to search for")
parser.add_argument(
    '--original-nvr', default=None,
    help="NVR of original artifact to search for")
parser.add_argument(
    '--name', default=None,
    help="Name of artifact to search for")
parser.add_argument(
    '--state', default=None, action="append",
    help="Limit the query only for events/builds in given state")
parser.add_argument(
    '--limit', default=10, type=int,
    help="Return only last N events")
args = parser.parse_args()

if args.redhat:
    deployment = "redhat"
else:
    deployment = "fedora"

if args.stage:
    env = "stage"
elif args.qe:
    env = "qe"
elif args.dev:
    env = "dev"
else:
    env = "prod"

api_url = get_api_url(deployment, env)
if not api_url:
    print("There is no Freshmaker instance for deployment %s and env %s" %( deployment, env))
    sys.exit(1)

if not args.events:
    if not args.state:
        args.state = ["init", "building", "complete", "failed"]
    events = get_freshmaker_events(api_url, search_key=args.search_key,
                                   rebuilt_nvr=args.rebuilt_nvr,
                                   original_nvr=args.original_nvr,
                                   name=args.name, states=args.state,
                                   limit=args.limit)
    print_events(events)
else:
    events = get_freshmaker_events_by_ids(api_url, args.events)
    print_detailed_events(events, states=args.state)
