#!/usr/bin/env python
# Version 2021-08-24
"""
For Multitech Gateways with the following firmware versions:
- 1.4.16, Python 2.7.3
- 5.0.1-AEP, Python 2.7.12
- 5.1.2, Python 2.7.12
- 5.2.1, Python 2.7.12
"""
from __future__ import print_function
import sys
import json
import urllib2
import traceback
import datetime

# LoRa network server URLs
WHITELIST_URL = 'http://127.0.0.1/api/loraNetwork/whitelist/devices'
DEVICES_URL = 'http://127.0.0.1/api/lora/devices'
COMMAND_SAVE_URL = 'http://127.0.0.1/api/command/save'
LORA_RESTART_URL = 'http://127.0.0.1/api/lora/restart'

# Utilities for DELETE requests


class DeleteRequest(urllib2.Request):
    def __init__(self, *args, **kwargs):
        urllib2.Request.__init__(self, *args, **kwargs)

    def get_method(self, *args, **kwargs):
        return 'DELETE'


class PostRequest(urllib2.Request):
    """Helper to create POST requests"""

    def __init__(self, *args, **kwargs):
        urllib2.Request.__init__(self, *args, **kwargs)

    def get_method(self):
        return 'POST'

    def get_data(self):
        return self.data if self.data else ''

    def has_data(self):
        return True

# Utilities for LoRa device management


def lora_get_devices_whitelist():
    request = urllib2.Request(WHITELIST_URL)
    response = urllib2.urlopen(request)
    data = json.load(response)
    return data['result']


def lora_get_devices():
    request = urllib2.Request(DEVICES_URL)
    response = urllib2.urlopen(request)
    data = json.load(response)
    return data['result']


def lora_add_device(device_config):
    request = PostRequest(WHITELIST_URL, data=json.dumps(device_config), headers={'Content-Type': 'application/json'})
    response = urllib2.urlopen(request)
    data = json.load(response)
    return data


def lora_delete_device_from_whitelist(device_index):
    request = DeleteRequest('%s/%s' % (WHITELIST_URL, device_index))
    response = urllib2.urlopen(request)
    data = json.load(response)
    return data


def lora_delete_device_from_devicelist(deveui):
    request = DeleteRequest('%s/%s' % (DEVICES_URL, deveui))
    response = urllib2.urlopen(request)
    data = json.load(response)
    return data


def lora_restart_network_server():
    request = PostRequest(COMMAND_SAVE_URL, data='')
    response = urllib2.urlopen(request)
    data = json.load(response)
    lora_restart_request = PostRequest(LORA_RESTART_URL, data='')
    urllib2.urlopen(lora_restart_request)
    return data

# Here we parse the existing key list


def get_old_keys_whitelist(log):

    old_keys_output = None
    try:
        # Notice: if get device fails then we cannot run update so it is ok to stop execution here
        old_keys_output = lora_get_devices_whitelist()
    except Exception as e:
        print('Failed to get whitelisted keys:', e, file=log)

    old_keys = {}

    if not old_keys_output:  # No old whitelisted keys exist
        return old_keys

    for old_key_index, old_key in enumerate(old_keys_output):
        try:
            old_key[u'deveui'] = old_key[u'deveui'].lower().replace('-', '')
            old_key[u'index'] = old_key_index
            old_keys[old_key[u'deveui']] = old_key
        except Exception as e:
            print('Failed to parse old key', old_key, e, file=log)

    return old_keys

# Here we parse the existing key list


def get_old_device_keys(log):

    old_keys_output = None
    try:
        old_keys_output = lora_get_devices()
    except Exception as e:
        print('Failed to get devices keys:', e, file=log)

    old_device_keys = {}

    if not old_keys_output:  # No old keys exist
        return old_device_keys

    for old_key in old_keys_output:
        try:
            old_key[u'deveui'] = old_key[u'deveui'].lower().replace('-', '')
            old_device_keys[old_key[u'deveui']] = old_key
        except Exception as e:
            print('Failed to parse old key:', old_key, e, file=log)

    return old_device_keys

# Parse the keys given from Node-Red as a command line argument


def get_new_keys(args, log):
    new_keys = {}

    for dev_eui, app_eui, app_key in [args[i:i + 3] for i in xrange(0, len(args), 3)]:
        try:
            dev_eui = unicode(dev_eui.lower().replace('-', ''))
            new_keys[dev_eui] = {
                u'deveui': unicode(dev_eui).lower(),
                u'appeui': unicode(app_eui).lower(),
                u'appkey': unicode(app_key).lower(),
            }
        except Exception as e:
            print('Failed to parse new key', dev_eui, e, file=log)

    return new_keys

# Add keys to lora and return added keys


def add_keys(new_keys, old_keys, log):
    added_keys = []
    keys_failed_to_add = []

    for dev_eui, new_key in new_keys.items():
        try:
            dev_eui = dev_eui.lower().replace('-', '')
            if not dev_eui in old_keys:

                try:
                    # Add key to lora
                    device_config = {
                        u'deveui': new_key[u'deveui'],
                        u'appeui': new_key[u'appeui'],
                        u'appkey': new_key[u'appkey'],
                        U'class': U'A',
                    }
                except KeyError as e:
                    print('New key missing a required dict key:', e, file=log)
                    keys_failed_to_add.append(dev_eui)
                    continue

                try:
                    lora_add_device(device_config)
                except Exception as e:
                    print('Failed to add key', dev_eui, e, file=log)
                    keys_failed_to_add.append(dev_eui)
                    continue

                # Adding a new key to list
                added_keys.append(new_key)
        except Exception as e:
            print('Failed to handle added key:', e, file=log)

    return added_keys, keys_failed_to_add

# Remove keys from lora and return removed keys


def remove_keys(new_keys, old_whitelist_keys, old_device_keys, log):
    removed_keys_whitelist = []
    removed_keys_devicelist = []
    keys_failed_to_remove_white_list = []
    keys_failed_to_remove_device_list = []

    # READ CAREFULLY
    # The Whitelist delete API is index based, it will reindex the keys after every request.
    # You need to remove the keys based on the highest index first so that you don't manipulate the position
    # of indexes when removing from the middle of the list.
    sorted_whitelist_keys = sorted(old_whitelist_keys.items(), key=lambda item: item[1][u'index'], reverse=True)
    # remove device keys from whitelist
    for dev_eui, old_key in sorted_whitelist_keys:
        try:
            if not dev_eui in new_keys:

                try:
                    # Remove device information from whitelist (DevEuid, AppEuid, AppKey)
                    lora_delete_device_from_whitelist(old_key[u'index'])
                except Exception as e:
                    error_message = 'HTTP Error %s: %s' % (e.code, e.read())
                    print('Failed to delete whitelisted key from LoRa server:', error_message, file=log)
                    keys_failed_to_remove_white_list.append(dev_eui)
                    continue

                # Removing a key
                removed_keys_whitelist.append(old_key)

        except Exception as e:
            print('Failed to handle remove key', dev_eui, e, file=log)

    # Remove device keys from device list and consecutively device sessions if they exsist.
    for dev_eui, old_key in old_device_keys.items():
        try:
            # Remove device from device list. Associated session is removed by system.
            if not dev_eui in new_keys:  # If device is on device list then remove it.
                try:
                    lora_delete_device_from_devicelist(dev_eui)
                except Exception as e:
                    error_message = 'HTTP Error %s: %s' % (e.code, e.read())
                    print('Failed to delete device from LoRa server', dev_eui, error_message, file=log)
                    keys_failed_to_remove_device_list.append(dev_eui)
                    continue

                # Removing a key
                removed_keys_devicelist.append(old_key)

        except Exception as e:
            print('Failed to remove device key', dev_eui, e, file=log)

    return removed_keys_whitelist, removed_keys_devicelist, keys_failed_to_remove_white_list, keys_failed_to_remove_device_list

# Main entry point


def main(args, log):
    # Get all new and old keys and update them to lora
    old_whitelist_keys = get_old_keys_whitelist(log)
    old_device_keys = get_old_device_keys(log)
    new_keys = get_new_keys(args, log)
    added_keys, keys_failed_to_add = add_keys(new_keys, old_whitelist_keys, log)
    removed_keys_whitelist, removed_keys_devicelist, keys_failed_to_remove_whitelist, keys_failed_to_remove_device_list = remove_keys(new_keys, old_whitelist_keys, old_device_keys, log)

    # Log what was changes
    # Log changes
    print('New keys:', len(new_keys), file=log)
    print('Old whitelist keys:', len(old_whitelist_keys), file=log)
    print('Old devicelist keys:', len(old_device_keys), file=log)
    print('Added keys:', len(added_keys), file=log)
    print('Failed to add keys:', len(keys_failed_to_add), file=log)
    print('Removed whitelist keys:', len(removed_keys_whitelist), file=log)
    print('Failed to remove whitelist keys:', len(keys_failed_to_remove_whitelist), file=log)
    print('Removed devicelist keys:', len(removed_keys_devicelist), file=log)
    print('Failed to remove devicelist keys:', len(keys_failed_to_remove_device_list), file=log)

    # Save and restart if anything was modified
    if len(added_keys) > 0 or len(removed_keys_whitelist) > 0 or len(removed_keys_devicelist) > 0:
        print('Restarting network server...', file=log)
        lora_restart_network_server()
        print('Restarted network server.', file=log)


if __name__ == '__main__':
    try:
        # Overwrite previous log to avoid filling disk
        with open('/var/log/app/lorasync.log', 'w') as log:
            try:
                print('Started sync at', datetime.datetime.now(), file=log)
                main(sys.argv[1:], log)
                print('Successfully completed sync at ', datetime.datetime.now(), file=log)
            except:
                traceback.print_exc(file=sys.stderr)
                traceback.print_exc(file=log)

    except:
        traceback.print_exc(file=sys.stderr)
        raise
