#!/usr/bin/env python3
import requests
import json
import time
import curses
import math
import traceback


class Monitor:

    _speedSamples = 8
    _globalspeed = []

    def __init__(self, url):
        self.data = dict()
        self.win = curses.initscr()
        curses.start_color()
        curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
        self._url = url
        while len(self._globalspeed) < self._speedSamples:
            self._globalspeed.append((0, 0, 0, 0))

    def __del__(self):
        curses.endwin()

    def on_timer(self, event):
        """called on timer event"""
        self.update_data()

    def jsonrpc(self, meth, params):
        r = requests.post(
            self._url,
            headers={"Content-Type": "application/json", "Host": "localhost"},
            json={
                "jsonrpc": "2.0",
                "id": "0",
                "method": "{}".format(meth),
                "params": params,
            },
        )
        return r.json()

    def update_data(self):
        """update data from lokinet"""
        try:
            j = self.jsonrpc("llarp.admin.dumpstate", {})
            self.data = j["result"]
        except Exception as ex:
            self.data = None

    def _render_path(self, y, path, name):
        """render a path at current position"""
        self.win.move(y, 1)
        self.win.addstr("({}) ".format(name))
        y += 1
        self.win.move(y, 1)
        y += 1
        self.win.addstr("[tx:\t{}]\t[rx:\t{}]".format(self.speedOf(path['txRateCurrent']), self.speedOf(path['rxRateCurrent'])))
        self.win.move(y, 1)
        y += 1
        self.win.addstr("me -> ")
        for hop in path["hops"]:
            self.win.addstr(" {} ->".format(hop["router"][:4]))
        self.win.addstr(" [{} ms latency]".format(path["intro"]["latency"]))
        self.win.addstr(" [{} until expire]".format(self.timeTo(path["expiresAt"])))
        if path["expiresSoon"]:
            self.win.addstr("(expiring)")
        elif path["expired"]:
            self.win.addstr("(expired)")
        return y

    def timeTo(self, ts):
        """ return time until timestamp in seconds formatted"""
        now = time.time() * 1000
        return "{} seconds".format(int((ts - now) / 1000))

    def speedOf(self, rate):
        """turn int speed into string formatted"""
        units = ["B", "KB", "MB", "GB"]
        idx = 0
        while rate > 1000 and idx < len(units):
            rate /= 1000.0
            idx += 1
        return "{} {}ps".format("%.2f" % rate, units[idx])

    def get_all_paths(self):
        for k in self.data['services']:
            status = self.data['services'][k]
            for path in (status['paths'] or []):
                yield path
            for s in (status['remoteSessions'] or []):
                for path in s['paths']:
                    yield path
            for s in (status['snodeSessions'] or []):
                for path in s['paths']:
                    yield path
    
    def display_service(self, y, name, status):
        """display a service at current position"""
        self.win.move(y, 1)
        self.win.addstr("service [{}]".format(name))
        build = status["buildStats"]
        ratio = build["success"] / (build["attempts"] or 1)
        y += 1
        self.win.move(y, 1)
        self.win.addstr("build success: {} %".format(int(100 * ratio)))
        y += 1
        self.win.move(y, 1)
        paths = status["paths"]
        self.win.addstr("paths: {}".format(len(paths)))
        for path in paths:
            y = self._render_path(y, path, "inbound")
        for session in (status["remoteSessions"] or []):
            for path in session["paths"]:
                y = self._render_path(
                    y, path, "[active] {}".format(session["currentConvoTag"])
                )
        for session in (status["snodeSessions"] or []):
            for path in session["paths"]:
                y = self._render_path(y, path, "[snode]")
        return y

        # for k in status:
        #  self.win.move(y + 1, 1)
        #  y += 1
        #  self.win.addstr('{}: {}'.format(k, json.dumps(status[k])))

    def display_links(self, y, data):
        self.txrate = 0
        self.rxrate = 0
        for link in data["outbound"]:
            y += 1
            self.win.move(y, 1)
            self.win.addstr("outbound sessions:")
            y = self.display_link(y, link)
        for link in data["inbound"]:
            y += 1
            self.win.move(y, 1)
            self.win.addstr("inbound sessions:")
            y = self.display_link(y, link)
        y += 2
        self.win.move(y, 1)
        self.win.addstr(
            "throughput:\t\t[{}\ttx]\t[{}\trx]".format(
                self.speedOf(self.txrate), self.speedOf(self.rxrate)
            )
        )
        bloat_tx, bloat_rx = self.calculate_bloat(self.data['links']['outbound'], self.get_all_paths())
        y += 1
        self.win.move(y, 1)
        self.win.addstr("goodput:\t\t[{}\ttx]\t[{}\trx]".format(self.speedOf(self.txrate-bloat_tx), self.speedOf(self.rxrate-bloat_rx)))
        y += 1
        self.win.move(y, 1)
        self.win.addstr("overhead:\t\t[{}\ttx]\t[{}\trx]".format(self.speedOf(bloat_tx), self.speedOf(bloat_rx)))
        

        self._globalspeed.append((self.txrate, self.rxrate, bloat_tx, bloat_rx))
        while len(self._globalspeed) > self._speedSamples:
            self._globalspeed.pop(0)
        return self.display_speedgraph(y + 2, self._globalspeed)

    def display_speedgraph(self, y, samps, maxsz=40):
        """ display global speed graph """
        def scale(x, n):
            while n > 0:
                x /= 2
                n -= 1
            return int(x)

        txmax, rxmax = 1024, 1024
        for tx, rx, _tx, _rx in samps:
            if tx > txmax:
                txmax = tx
            if rx > rxmax:
                rxmax = rx

        rxscale = 0
        while rxmax > maxsz:
            rxscale += 1
            rxmax /= 2

        txscale = 0
        while txmax > maxsz:
            txscale += 1
            txmax /= 2

        def makebar(samp, badsamp, max):
            bar = "#" * (samp - badsamp)
            pad = " " * (max - samp)
            return pad, bar, '#' * badsamp

        txlabelpad = int(txmax / 2)# - 1
        rxlabelpad = int(rxmax / 2)# - 1
        if txlabelpad <= 0:
            txlabelpad = 1
        if rxlabelpad <= 0:
            rxlabelpad = 1
        txlabelpad = " " * txlabelpad
        rxlabelpad = " " * rxlabelpad
        y += 1
        self.win.move(y, 1)
        self.win.addstr(
            "{}tx{}{}rx{}".format(txlabelpad, txlabelpad, rxlabelpad, rxlabelpad)
        )
        for tx, rx, btx, brx in samps:
            y += 1
            self.win.move(y, 1)
            txpad, txbar, btxbar = makebar(scale(tx,txscale),scale(btx,txscale), int(txmax))
            rxpad, rxbar, brxbar = makebar(scale(rx,rxscale),scale(brx,rxscale), int(rxmax))
            self.win.addstr(txpad)
            self.win.addstr(btxbar, curses.color_pair(1))
            self.win.addstr(txbar)
            self.win.addstr('|')
            self.win.addstr(rxbar)
            self.win.addstr(brxbar, curses.color_pair(1))
            self.win.addstr(rxpad)

        return y + 2

    def calculate_bloat(self, links, paths):
        """
        calculate bandwith overhead
        """
        lltx = 0
        llrx = 0
        tx = 0
        rx = 0
        for link in links:
            sessions = link["sessions"]["established"]
            for s in sessions:
                lltx += s['tx']
                llrx += s['rx']
        for path in paths:
            tx += path['txRateCurrent']
            rx += path['rxRateCurrent']
        if lltx > tx:
            lltx -= tx
        if llrx > rx:
            llrx -= rx
        return lltx, llrx
            

    def display_link(self, y, link):
        y += 1
        self.win.move(y, 1)
        sessions = link["sessions"]["established"]
        for s in sessions:
            y = self.display_link_session(y, s)
        return y
            
    def display_link_session(self, y, s):
        y += 1
        self.win.move(y, 1)
        self.txrate += s["txRateCurrent"]
        self.rxrate += s["rxRateCurrent"]
        self.win.addstr(
            "{}\t[{}\ttx]\t[{}\trx]".format(
                s["remoteAddr"], self.speedOf(s["txRateCurrent"]), self.speedOf(s["rxRateCurrent"])
            )
        )
        if (s['txMsgQueueSize'] or 0) > 1:
            self.win.addstr(" [out window: {}]".format(s['txMsgQueueSize']))
            if (s['rxMsgQueueSize'] or 0) > 1:
                self.win.addstr(" [in window: {}]".format(s['rxMsgQueueSize']))
        def display(acks, label, num='acks', dem='packets'):
            if acks[dem] > 0:
                self.win.addstr(" [{}: {}]".format(label, round(float(acks[num]) / float(acks[dem]), 2)))
        if ('recvMACKs' in s) and ('sendMACKs' in s):
            display(s['sendMACKs'], 'out MACK density')
            display(s['recvMACKs'], 'in MACK density')
        d =  {'recvAcks': 'in acks', 'sendAcks': 'out acks', 'recvRTX': 'in RTX', 'sendRTX': 'out RTX'}
        for k in d:
            v = d[k]
            if (k in s) and (s[k] > 0):
                self.win.addstr(" [{}: {}]".format(v, s[k]))
        return y

    def display_dht(self, y, data):
        y += 2
        self.win.move(y, 1)
        self.win.addstr("DHT:")
        y += 1
        self.win.move(y, 1)
        self.win.addstr("introset lookups")
        y = self.display_bucket(y, data["pendingIntrosetLookups"])
        y += 1
        self.win.move(y, 1)
        self.win.addstr("router lookups")
        return self.display_bucket(y, data["pendingRouterLookups"])

    def display_bucket(self, y, data):
        txs = data["tx"]
        self.win.addstr(" ({} lookups)".format(len(txs)))
        for tx in txs:
            y += 1
            self.win.move(y, 1)
            self.win.addstr("search for {}".format(tx["tx"]["target"]))
        return y

    def display_data(self):
        """draw main window"""
        if self.data is not None:
            self.win.addstr(1, 1, "lokinet online")
            # print(self.data)
            services = self.data["services"] or {}
            y = 3
            try:
                y = self.display_links(y, self.data["links"])
                for k in services:
                    y = self.display_service(y, k, services[k])
                y = self.display_dht(y, self.data["dht"])
            except Exception as exc:
                pass
        else:
            self.win.move(1, 1)
            self.win.addstr("lokinet offline")

    def run(self):
        while True:
            self.win.clear()
            self.win.box()
            self.update_data()
            self.display_data()
            self.win.refresh()
            time.sleep(1)


if __name__ == "__main__":
    import sys

    mon = Monitor(
        "http://{}/jsonrpc".format(
            len(sys.argv) > 1 and sys.argv[1] or "127.0.0.1:1190"
        )
    )
    mon.run()
