#!/usr/bin/env python3

import os, sys
import traceback
import re, json, subprocess, requests
import time
from datetime import datetime

#from dateutil import parser

def print_err(*args):
    print(*args, file=sys.stderr, flush=True)

## JSON-RPC
def json_rpc(url, method, params=[]):
    if url == None:
        url = os.getenv("HMY_URL", "http://localhost:9500/")

    headers = {'Content-Type':'application/json'}
    req = {"jsonrpc":"2.0","method":method,"params":params,"id":1337}
    resp = requests.post(url, data=json.dumps(req), headers=headers)
    if resp.status_code != 200 or len(resp.content) == 0:
        print_err(f"{method} failed: {resp.status_code}: {resp.content.decode()}")
        return []
    return resp.json()

def get_hmy_block_number(url=None, old=None):
    resp = json_rpc(url, "hmyv2_blockNumber")
    if not 'result' in resp:
        print_err("blockNumber", resp)
        return old
    return resp['result']

def get_hmy_tx(tx, url=None):
    resp = json_rpc(url, "hmyv2_getTransactionByHash", params=[tx])
    if not 'result' in resp:
        print_err(f"getTransactionByHash({tx})", resp)
        return None
    return resp['result']

def get_hmy_block(num, url=None):
    resp = json_rpc(url, "hmyv2_getBlockByNumber", params=[num, {"fullTx":True,"inclTx":True,"InclStaking":True}])
    if not 'result' in resp:
        print_err(f"getBlockByNumber({num})", resp)
        return None
    return resp['result']

#def get_hmy_peer_count(url=None, old=None):
#    resp = json_rpc(url, "net_peerCount")
#    if not 'result' in resp:
#        print_err("net_peerCount", resp)
#        return old
#    return int(resp['result'], 0)

def get_hmy_status(url=None):
    resp = json_rpc(url, "hmyv2_syncing")
    if not 'result' in resp:
        print_err("syncing", resp)
        return 'Error'
    return 'Syncing' if resp['result'] else 'Synced'

def get_hmy_metadata(url=None):
    resp = json_rpc(url, "hmy_getNodeMetadata")
    if not 'result' in resp:
        print_err("getNodeMetadata", resp)
        return
    return resp['result']

#def get_hmy_epoch(url=None, old=None):
#    resp = json_rpc(url, "hmyv2_getEpoch")
#    if not 'result' in resp:
#        print_err("getEpoch", resp)
#        return old
#    return resp['result']

# FIXME: use centralized external ref? This is way out of scope for blockheight so not sure.
# needs to query a node on shard 0
# FIXME: also if our shard is 0 we can query our own node
def get_hmy_validator_info(validator, url="https://api.s0.t.hmny.io/"):
    resp = json_rpc(url, "hmyv2_getValidatorInformation", params=[validator])
    if not 'result' in resp:
        print_err("getValidatorInformation", resp)
        return
    return resp['result']

# FIXME: use centralized external ref? This is way out of scope for blockheight so not sure.
# needs to query a node on shard 0
# FIXME: also if our shard is 0 we can query our own node
def get_hmy_all_validator_info(url="https://api.s0.t.hmny.io/"):
    resp = json_rpc(url, "hmyv2_getAllValidatorInformation", params=[-1])
    if not 'result' in resp:
        print_err("getAllValidatorInformation", resp)
        return
    return resp['result']

# FIXME: use centralized external ref
def get_hmy_highest_block_number(shard, old=None):
    return get_hmy_block_number(url=f"https://api.s{shard}.t.hmny.io/", old=old)

# FIXME: use centralized external ref
def get_hmy_latest_version(shard=0):
    meta = get_hmy_metadata(f"https://api.s{shard}.t.hmny.io/")
    version = meta['version'].split(',')[1].strip()
    return version.split(' ')[1].strip()

#def get_version():
#    home = os.getenv("HARMONY_HOME", "/data/harmony")
#    cmd = f"{home}/harmony -version 2>&1 | cut -f 6 -d ' '"
#    return subprocess.check_output(cmd, shell=True).decode('utf-8').strip()

class State:
    def __init__(self):
        self.time = datetime.now().isoformat()
        self.level = "info"
        self.version = None

        self.peer_count = 0
        self.id = None

        self.block_number = 0
        self.highest_block_number = 0
        self.blocks_behind = None
        self.latest_hash = None
        self.latest_block_time = None
        self.is_validator = True
        self.current_epoch = None
        self.last_validated_block_number = None
        self.last_validated_block_hash = None
        self.latest_validated_block_time = None

        self.status = "Offline"
        self.message = "Offline"

        self.harmony = {}
        self.update()

    def update(self):
        try:
            self.unsafe_update()
        except:
            exc_type, exc_value, exc_traceback = sys.exc_info()
            traceback.print_exception(exc_type, exc_value, exc_traceback, limit=4, file=sys.stderr)
            sys.stderr.flush()
            self.message = f"{exc_type}: {exc_value}"
            self.bump_level('fatal')

        # clean up self
        for i in [k for k,v in vars(self).items() if v==None]:
            delattr(self, i)

    def unsafe_update(self):
        self.time = datetime.now().isoformat()
        self.level = 'info'

        self.status = get_hmy_status()
        self.message = self.status

        meta = get_hmy_metadata()
        #print(meta)
        version = meta['version'].split(',')[1].strip()
        self.version = version.split(' ')[1].strip()

        self.peer_count = meta['p2p-connectivity']['connected']
        self.block_number = get_hmy_block_number(old=self.block_number)
        self.current_epoch = meta['current-epoch']

        # TODO
        #self.last_validated_block_number =
        #self.last_validated_block_hash =
        #self.latest_validated_block_time =

        self.harmony['blskeys'] = ','.join(meta['blskey'])
        shard = meta['shard-id']
        if shard<0:
            print_err("Failed to find shard")
            return
        self.harmony['shard'] = shard

        self.id = f"shard-{shard}"
        self.latest_version = get_hmy_latest_version(shard)
        self.needs_update = self.version != self.latest_version

        self.highest_block_number = get_hmy_highest_block_number(shard, old=self.highest_block_number)
        if self.highest_block_number > 0:
            self.blocks_behind = self.highest_block_number - self.block_number
        if self.blocks_behind > 100:
            self.bump_level('error')
        elif self.blocks_behind > 10:
            self.bump_level('warn')

        if self.blocks_behind > 1:
            self.status = 'Syncing'
            self.message = f"{self.status}: {self.blocks_behind} blocks behind"

        latest_block = get_hmy_block(self.block_number)
        if latest_block!=None and 'hash' in latest_block:
            self.latest_hash = latest_block['hash']
            self.latest_block_time = datetime.fromtimestamp(latest_block['timestamp']).isoformat()
        else:
            print_err("Failed to find latest block")

        v = self.get_my_v_info()
        if v:
            for i in 'current-epoch-performance', 'currently-in-committee', 'epos-status', 'booted-status':
                if i in v:
                    self.harmony['validator'][i] = v[i]
            if 'total-delegation' in v:
                self.harmony['validator']['total-delegation'] = v['total-delegation']/1.0E18

            # Mark none by default so if we don't have it it is cleaned up below.
            self.harmony['current_epoch_signing_percent'] = None
            self.harmony['newly_missed'] = None

            if 'current-epoch-performance' in v:
                current = v['current-epoch-performance']['current-epoch-signing-percent']
                signed = float(current['current-epoch-signed'])
                to_sign = current['current-epoch-to-sign']
                if to_sign > 0:
                    missed = to_sign-signed
                    if 'missed' in self.harmony and missed >= self.harmony['missed']:
                        self.harmony['newly_missed'] = missed - self.harmony['missed']
                    self.harmony['missed'] = missed
                    self.harmony['current_epoch_signing_percent'] = {
                        # make a string so we don't have logstash int/float confusion
                        'str': str(round(signed/to_sign*100, 2)),
                        # make a pct field so we can fix a type with an ES template
                        'pct': signed/to_sign
                    }
            if 'lifetime' in v:
                l = v['lifetime']
                if 'epoch-apr' in l:
                    del l['epoch-apr']
                if 'epoch-blocks' in l:
                    del l['epoch-blocks']
                if 'reward-accumulated' in l:
                    l['reward-accumulated'] = l['reward-accumulated']/1.0E18
                self.harmony['validator']['lifetime'] = l
                blocks = v['lifetime']['blocks']
#               missed = blocks['to-sign'] - blocks['signed']
#               if 'lifetime-missed' in self.harmony:
#                   self.harmony['liftime-newly-missed'] = missed - self.harmony['liftime-missed']
#               self.harmony['liftime-missed'] = missed

        # clean up harmony
        for i in [k for k,vv in self.harmony.items() if vv==None]:
            del self.harmony[i]

    def get_my_v_info(self):
        # only do this once, it is expensive
        if 'validator' not in self.harmony:
            if 'blskeys' not in self.harmony:
                return

            # find our first blskey in the list of all validators
            bls = self.harmony['blskeys'].split(',')[0]
            all_v = get_hmy_all_validator_info()
            if not all_v:
                return

            match = [v for v in all_v if bls in v['validator']['bls-public-keys']]
            if len(match) != 1 or 'validator' not in match[0]:
                return
            self.harmony['validator'] = {}
            self.harmony['validator']['address'] = match[0]['validator']['address']
            ret = match[0]
        else:
            ret = get_hmy_validator_info(self.harmony['validator']['address'])

        # clean up ret
        for i in [k for k,v in ret.items() if v==None]:
            del ret[i]

        return ret

    def __str__(self):
        return json.dumps(self.__dict__)

    def bump_level(self, level):
        Levels = {'info': 0, 'warn': 1, 'error': 2, 'fatal': 3}
        if self.level == None or Levels[level]>Levels[self.level]:
            self.level = level

def main():
    output = os.getenv("COLLECTOR_LOG", None)
    period = int(os.getenv("COLLECTOR_PERIOD", 0))

    state = State()

    while True:
        if output == None:
            print(state)
        else:
            with open(output, "a") as myfile:
                myfile.write(str(state)+"\n")
                myfile.close()

        if period <= 0:
            break;

        time.sleep(period)
        state.update()

if __name__ == "__main__":
    main()

# vim: expandtab
