#!/usr/bin/python

import json
import socket
import sys
import time
import xml.etree.ElementTree as ET

import homefarm as hf

###############################################################################
#
# these functions handle reporting for the report generator
def format_eta(eta):
    eta = float(eta)
    if eta > 3600:
        h = int(eta / 3600)
        m = int((eta - 3600 * h) / 60)
        return "{:3d}h{:02d}m".format(h, m)
    else:
        m = int(eta / 60)
        s = int(eta - 60 * m)
        return "{:3d}m{:02d}s".format(m, s)


def format_dl(dl):
    od = 1
    dl = int(float(dl))
    t = int(time.time())
    dl = dl - t
    if dl < 0:
        od = -1
        dl = abs(dl)

    if dl > 86400:
        d = int(dl / 86400)
        h = int((dl - 86400 * d) / 3600)
        return "{:3d}d{:02d}h".format(d * od, h)
    elif dl > 3600:
        h = int(dl / 3600)
        m = int((dl - 3600 * h) / 60)
        return "{:3d}h{:02d}m".format(h * od, m)
    else:
        m = int(dl / 60)
        s = int(dl - 60 * m)
        return "{:3d}m{:02d}s".format(m * od, s)


# report() is the report generator function. if JSON is requested, it
# simply dumps the collected data, as-is. otherwise, a human-readable
# report of all active tasks is created
def report(tmpfile, hosts, conf):
    of = open(tmpfile, 'w')
    if conf['json']:
        json.dump(hosts, of)
        of.close()
        sys.exit(0)

    hostcount = 0
    for host, projects in hosts.items():
        if hostcount > 0:
            of.write("\n\n\n")
        of.write("================================================================================\n")
        of.write(f"{host}\n")
        of.write("================================================================================\n")
        if len(projects) == 0:
            of.write("No work from this project (yet?)\n")
            continue

        projcount = 0
        for projname, p in projects.items():
            if projname == 'err':
                if  p is not None:
                    of.write(f"{projname}: {p}\n")
                continue

            # print a summary for the project
            if projcount > 0:
                of.write("--------------------------------------------------------------------------------\n")
            of.write(f"{projname}\t  User: {p['username']}\t  State: {p['state']}\n")
            of.write(f"Tasks: {p['taskcount']}\t  Active: {p['taskactive']}\t  Credit/RAC: {int(p['hostcred'])}/{int(p['hostrac'])}\n")
            projcount += 1
            # if there are no active tasks for this project, don't do
            # detail reporting
            if p['taskactive'] == 0:
                continue
            # ok, we have active tasks. print details.
            of.write("\n")
            of.write("     Workunit                                     Stat  Prog    ETA      Dline\n")
            of.write("     -------------------------------------------  ----  ------  -------  -------\n")
            # sort tasks by completion percentage and iterate over them,
            # printing details for active ones
            wunum = 1
            for name, t in {k: v for k, v in sorted(p['tasks'].items(), reverse=True, key=lambda item: item[1]['done'])}.items():
                if not t['active']:
                    continue
                of.write(f"{wunum:3d}  {name[:43]:43}  {t['astate']:4}  {t['done']:6.2%}  {format_eta(t['cpu_eta'])}  {format_dl(t['deadline'])}\n")
                wunum += 1
        hostcount += 1
    of.close()



###############################################################################

hosts = {}
conf = { 'host': None, 'json': False, 'compact': False, 'active': False }
tasks = None

sys.argv.pop(0)
tmpfile = sys.argv.pop(0)

# handle optional args. sys.argv will arrive with only one arg, due to
# our calling format.
for arg in sys.argv[0].split():
    if arg == "json":
        conf['json'] = True
    elif arg == "compact":
        conf['compact'] = True
    elif arg == "active":
        conf['active'] = True
    elif arg == "nopage":
        pass
    else:
        conf['host'] = arg


with open("/homefarm/.boincguikeys.json", 'r') as f:
    hosts = json.load(f)

data = {}
for host, key in hosts.items():
    if conf['host'] != None and host != conf['host']:
        continue
    data[host] = {'err': None}

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        try:
            s.connect((host, 31416))
        except Exception as err:
            data[host]['err'] = f"could not connect to '{host}': {err}"
            continue
        # try to auth
        authed = hf.auth_conn(s, key)
        if not authed:
            data[host]['err'] = f"connected but could not auth on '{host}' with key '{key}'"
            continue


        # first, get projects data
        req = "<boinc_gui_rpc_request><get_project_status/></boinc_gui_rpc_request>\003"
        s.sendall(req.encode('utf-8'))
        reply = ET.fromstring(hf.recv_msg(s))
        for project in reply.iter('project'):
            # and extract a few bits on a per-project basis. some of
            # this is trivia, but some of it will let us process the
            # tasks meaningfully
            p = {}
            name = project.find('project_name').text
            p['url'] = project.find('master_url').text
            p['username'] = project.find('user_name').text
            p['usercred'] = int(float(project.find('user_total_credit').text))
            p['userrac'] = int(float(project.find('user_expavg_credit').text))
            p['hostcred'] = int(float(project.find('host_total_credit').text))
            p['hostrac'] = int(float(project.find('host_expavg_credit').text))
            try:
                state = project.find('suspended_via_gui')
                if state is not None:
                    p['state'] = "Suspended"
            except:
                pass
            try:
                state = project.find('dont_request_more_work')
                if state is not None:
                    p['state'] = "Nomorework"
            except:
                pass
            if 'state' not in p:
                p['state'] = "Active"
            data[host][name] = p

        # now just grab the task data and stow it. we'll process it
        # later on
        req = "<boinc_gui_rpc_request><get_results/></boinc_gui_rpc_request>\003"
        s.sendall(req.encode('utf-8'))
        tasks = ET.fromstring(hf.recv_msg(s))

    projcount = 0
    projects = data[host]
    for projname in sorted(projects):
        if projname == 'err':
            continue

        task_count = 0  # how many tasks in this project
        task_active = 0 # how many active tasks
        wunum = 1       # counter for printing task details
        ptasks = {}     # dict of tasks for this project

        # loop over the tasks, extracting interesting info
        for task in tasks.iter('result'):
            # only process tasks belonging to the current project
            if projects[projname]['url'] != task.find('project_url').text:
                continue
            task_count += 1
            t = {}
            t['cpu_eta'] = task.find('estimated_cpu_time_remaining').text
            t['state'] =  task.find('state').text
            t['deadline'] = task.find('report_deadline').text
            # if the task is active
            try:
                active_data = task.find('active_task')
                if active_data is None:
                    raise
                task_active += 1
                t['active'] = True
                t['done'] = float(active_data.find('fraction_done').text)
                t['cpu_elapsed'] = active_data.find('current_cpu_time').text
                astate = float(active_data.find('active_task_state').text)
                # set a human-readable active state
                if astate == 0:
                    t['astate'] = "Paus"
                elif astate == 1:
                    t['astate'] = "Run"
                elif astate == 5:
                    t['astate'] = "Abrt"
                elif astate == 8:
                    t['astate'] = "Quit"
                elif astate == 9:
                    t['astate'] = "Susp"
                else: # 10
                    t['astate'] = "Copy"
            except:
                t['active'] = False
                t['done'] = 0
                t['astate'] = None
                t['cpu_elapsed'] = 0
            # add the task to the list if 'active' was passed in and
            # the task is active, or add regardless if 'active' was
            # not passed
            if conf['active']:
                if t['active']:
                    ptasks[task.find('name').text] = t
            else:
                ptasks[task.find('name').text] = t
        data[host][projname]['taskcount'] = task_count
        data[host][projname]['taskactive'] = task_active
        data[host][projname]['tasks'] = ptasks

report(tmpfile, data, conf)
