#!/usr/bin/env python
#
# Displays a summary of Taskomatic activities in progress
#
# Copyright (c) 2016 SUSE LLC
#
# This software is licensed to you under the GNU General Public License,
# version 2 (GPLv2). There is NO WARRANTY for this software, express or
# implied, including the implied warranties of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
# along with this software; if not, see
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
#

import datetime
import time
import struct
import StringIO
import signal
import sys
import curses

from spacewalk.server import rhnSQL

def get_tasko_runs(maximum_age):
    """Returns data about recent Taskomatic task runs from the database."""
    task_query = rhnSQL.prepare("""
        SELECT
            task.name AS name,
            run.id AS id,
            run.start_time AS start_time,
            run.end_time AS end_time,
            schedule.data AS data

            FROM rhnTaskoRun run
                JOIN rhnTaskoSchedule schedule ON schedule.id = run.schedule_id
                JOIN rhnTaskoTemplate template ON template.id = run.template_id
                JOIN rhnTaskoTask task ON task.id = template.task_id

            WHERE
                run.start_time IS NOT NULL
                    AND (run.end_time IS NULL OR run.end_time > :timelimit)

            ORDER BY end_time DESC NULLS FIRST, start_time ASC
    """);
    # trim those older than 1 minute
    task_query.execute(timelimit = datetime.datetime.now() - datetime.timedelta(seconds=maximum_age))

    # HACK: simulate fetchall_dict() in such a way BLOBs are only read once
    # (otherwise we get exceptions)
    result = []
    row = task_query.fetchone_dict()
    while row is not None:
        row["data"] =  rhnSQL.read_lob(row["data"])
        result.append(row)
        row = task_query.fetchone_dict()
    return result

def get_channel_names(ids):
    """Gets the channel names corresponding to channel ids from the database."""
    if len(ids) == 0:
        return []

    query = rhnSQL.prepare("""
        SELECT DISTINCT label
            FROM rhnChannel
            WHERE id IN ({0})
            ORDER BY label
    """.format(",".join(ids)));
    query.execute()

    return [tuple[0] for tuple in query.fetchall()]

def get_current_repodata_channel_names():
    """Gets the channel names of currenlty running repodata tasks from the database."""
    query = rhnSQL.prepare("""
        SELECT DISTINCT channel_label
            FROM rhnRepoRegenQueue
            WHERE next_action IS NULL
            ORDER BY channel_label
    """);
    query.execute()

    return [row[0] for row in query.fetchall()]

def extract_channel_ids(bytes):
    """Extracts channel ids from a Java Map in serialized form."""
    # HACK: this heuristicallty looks for strings, which are marked with 't',
    # two bytes for the length and the string chars themselves. If they
    # represent numbers, we assume they are channel_ids
    # (currently this is the case)
    java_strings = []
    io = StringIO.StringIO(bytes)
    while True:
        char = io.read(1)
        if char == "":
            break
        elif char == "t":
            oldpos = io.tell()
            try:
                length = struct.unpack(">H", io.read(2))[0]
                java_string = struct.unpack(">{0}s".format(length), io.read(length))
                java_strings += java_string
            except struct.error:
                pass # not a real string, ignore
            io.seek(oldpos)
    # of those found, filter the ones looking like a number
    return [java_string for java_string in java_strings if java_string.isdigit()]

def format_run(run):
    """Formats data from a Taskomatic run in human-friendly form."""
    if run["end_time"]:
        run["since"] = format_date_delta(run["start_time"], run["end_time"])
        run["status"] = "(finished)"
    else:
        run["since"] = format_date_delta(run["start_time"], datetime.datetime.now())
        run["status"] = ""

    run["channel_name"] = ""
    if run["data"]:
        channel_names = get_channel_names(extract_channel_ids(run["data"]))
        run["channel_name"] = format_multiple_names(channel_names)

    if run["name"] == "channel-repodata" and run["status"] == "":
        run["channel_name"] = format_multiple_names(get_current_repodata_channel_names())

    return "{id:11d} {name:>30} {since:>13} {status:10} {channel_name:>60}".format(**run)

def format_multiple_names(names):
    """Formats an array so that it does not take up too much screen space."""
    if len(names) == 0:
        return ""
    elif len(names) == 1:
        return names[0]
    else:
        one = names[datetime.datetime.now().second % len(names)]
        if len(names) == 1:
            return "{0} and 1 other".format(one)
        else:
            return "{0} and {1} others".format(one, len(names) -1)

def format_date_delta(start, end):
    """Formats a time delta in human-friently form."""
    td = end.replace(tzinfo=None) - start.replace(tzinfo=None)
    seconds = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
    return "{0}s".format(seconds)

def main(screen):
    """Computes and displays runs every second."""
    rhnSQL.initDB()

    # exit gracefully on ctrl-c
    signal.signal(signal.SIGINT, lambda signal, frame: sys.exit(0))

    # hide cursor
    curses.curs_set(0)
    while True:
        screen.erase()

        rows = (
            ["{0:>11} {1:>30} {2:>13} {3:10} {4:>60}".format("RUN ID", "TASK NAME", "ELAPSED TIME", "", "CHANNEL")] +
            [format_run(run) for run in get_tasko_runs(60)]
        )

        for index, row in enumerate(rows):
            if index < screen.getmaxyx()[0]:
                screen.addstr(index, 0, row)

        screen.refresh()
        time.sleep(1)

curses.wrapper(main)