"""
Manipulations with knownNodes dictionary.
"""

import json
import os
import pickle
import threading
import time

import state
from bmconfigparser import BMConfigParser
from debug import logger
from helper_bootstrap import dns

knownNodesLock = threading.Lock()
knownNodes = {stream: {} for stream in range(1, 4)}

knownNodesTrimAmount = 2000

# forget a node after rating is this low
knownNodesForgetRating = -0.5

knownNodesActual = False

DEFAULT_NODES = (
    state.Peer('5.45.99.75', 8444),
    state.Peer('75.167.159.54', 8444),
    state.Peer('95.165.168.168', 8444),
    state.Peer('85.180.139.241', 8444),
    state.Peer('158.222.217.190', 8080),
    state.Peer('178.62.12.187', 8448),
    state.Peer('24.188.198.204', 8111),
    state.Peer('109.147.204.113', 1195),
    state.Peer('178.11.46.221', 8444)
)

DEFAULT_NODES_ONION = (
    state.Peer('quzwelsuziwqgpt2.onion', 8444),
)


def json_serialize_knownnodes(output):
    """
    Reorganize knownnodes dict and write it as JSON to output
    """
    _serialized = []
    for stream, peers in knownNodes.iteritems():
        for peer, info in peers.iteritems():
            info.update(rating=round(info.get('rating', 0), 2))
            _serialized.append({
                'stream': stream, 'peer': peer._asdict(), 'info': info
            })
    json.dump(_serialized, output, indent=4)


def json_deserialize_knownnodes(source):
    """
    Read JSON from source and make knownnodes dict
    """
    global knownNodesActual  # pylint: disable=global-statement
    for node in json.load(source):
        peer = node['peer']
        info = node['info']
        peer = state.Peer(str(peer['host']), peer.get('port', 8444))
        knownNodes[node['stream']][peer] = info

        if (
            not (knownNodesActual or info.get('self')) and
            peer not in DEFAULT_NODES and
            peer not in DEFAULT_NODES_ONION
        ):
            knownNodesActual = True


def pickle_deserialize_old_knownnodes(source):
    """
    Unpickle source and reorganize knownnodes dict if it's in old format
    the old format was {Peer:lastseen, ...}
    the new format is {Peer:{"lastseen":i, "rating":f}}
    """
    global knownNodes  # pylint: disable=global-statement
    knownNodes = pickle.load(source)
    for stream in knownNodes.keys():
        for node, params in knownNodes[stream].iteritems():
            if isinstance(params, (float, int)):
                addKnownNode(stream, node, params)


def saveKnownNodes(dirName=None):
    if dirName is None:
        dirName = state.appdata
    with knownNodesLock:
        with open(os.path.join(dirName, 'knownnodes.dat'), 'wb') as output:
            json_serialize_knownnodes(output)


def addKnownNode(stream, peer, lastseen=None, is_self=False):
    knownNodes[stream][peer] = {
        "lastseen": lastseen or time.time(),
        "rating": 1 if is_self else 0,
        "self": is_self,
    }


def createDefaultKnownNodes(onion=False):
    past = time.time() - 2418600  # 28 days - 10 min
    for peer in DEFAULT_NODES_ONION if onion else DEFAULT_NODES:
        addKnownNode(1, peer, past)
    saveKnownNodes()


def readKnownNodes():
    try:
        with open(state.appdata + 'knownnodes.dat', 'rb') as source:
            with knownNodesLock:
                try:
                    json_deserialize_knownnodes(source)
                except ValueError:
                    source.seek(0)
                    pickle_deserialize_old_knownnodes(source)
    except (IOError, OSError, KeyError, EOFError):
        logger.debug(
            'Failed to read nodes from knownnodes.dat', exc_info=True)
        createDefaultKnownNodes()

    config = BMConfigParser()

    # your own onion address, if setup
    onionhostname = config.safeGet('bitmessagesettings', 'onionhostname')
    if onionhostname and ".onion" in onionhostname:
        onionport = config.safeGetInt('bitmessagesettings', 'onionport')
        if onionport:
            self_peer = state.Peer(onionhostname, onionport)
            addKnownNode(1, self_peer, is_self=True)
            state.ownAddresses[self_peer] = True


def increaseRating(peer):
    increaseAmount = 0.1
    maxRating = 1
    with knownNodesLock:
        for stream in knownNodes.keys():
            try:
                knownNodes[stream][peer]["rating"] = min(
                    knownNodes[stream][peer]["rating"] + increaseAmount,
                    maxRating
                )
            except KeyError:
                pass


def decreaseRating(peer):
    decreaseAmount = 0.1
    minRating = -1
    with knownNodesLock:
        for stream in knownNodes.keys():
            try:
                knownNodes[stream][peer]["rating"] = max(
                    knownNodes[stream][peer]["rating"] - decreaseAmount,
                    minRating
                )
            except KeyError:
                pass


def trimKnownNodes(recAddrStream=1):
    if len(knownNodes[recAddrStream]) < \
            BMConfigParser().safeGetInt("knownnodes", "maxnodes"):
        return
    with knownNodesLock:
        oldestList = sorted(
            knownNodes[recAddrStream],
            key=lambda x: x['lastseen']
        )[:knownNodesTrimAmount]
        for oldest in oldestList:
            del knownNodes[recAddrStream][oldest]


def cleanupKnownNodes():
    """
    Cleanup knownnodes: remove old nodes and nodes with low rating
    """
    now = int(time.time())
    needToWriteKnownNodesToDisk = False
    dns_done = False
    spawnConnections = not BMConfigParser().safeGetBoolean(
        'bitmessagesettings', 'dontconnect'
    ) and BMConfigParser().safeGetBoolean(
        'bitmessagesettings', 'sendoutgoingconnections')

    with knownNodesLock:
        for stream in knownNodes:
            if stream not in state.streamsInWhichIAmParticipating:
                continue
            keys = knownNodes[stream].keys()
            if len(keys) <= 1:  # leave at least one node
                if not dns_done and spawnConnections:
                    dns()
                    dns_done = True
                continue
            for node in keys:
                try:
                    # scrap old nodes
                    if (now - knownNodes[stream][node]["lastseen"] >
                            2419200):  # 28 days
                        needToWriteKnownNodesToDisk = True
                        del knownNodes[stream][node]
                        continue
                    # scrap old nodes with low rating
                    if (now - knownNodes[stream][node]["lastseen"] > 10800 and
                        knownNodes[stream][node]["rating"] <=
                            knownNodesForgetRating):
                        needToWriteKnownNodesToDisk = True
                        del knownNodes[stream][node]
                        continue
                except TypeError:
                    logger.warning('Error in %s', node)
            keys = []

    # Let us write out the knowNodes to disk
    # if there is anything new to write out.
    if needToWriteKnownNodesToDisk:
        saveKnownNodes()
