# -*- coding: utf-8 -*-
# Version {{VERSION}}
"""
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
- 6.0.0, Python 3.8
"""

import sys
import json
if sys.version_info[0] < 3:
    import urllib2 as urllib_request
else:
    import urllib.request as urllib_request
import traceback
import datetime
from SKFClient.core.utils.log import log

# 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(urllib_request.Request):
    def __init__(self, *args, **kwargs):
        urllib_request.Request.__init__(self, *args, **kwargs)

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


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

    def __init__(self, *args, **kwargs):
        urllib_request.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 = urllib_request.Request(WHITELIST_URL)
    response = urllib_request.urlopen(request)
    data = json.load(response)
    return data['result']


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


def lora_add_device(device_config):
    post_data = json.dumps(device_config).encode()
    request = PostRequest(WHITELIST_URL, data=post_data, headers={'Content-Type': 'application/json'})
    #request = PostRequest(WHITELIST_URL, data=json.dumps(device_config).encode(), headers={'Content-Type': 'application/json'})
    response = urllib_request.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 = urllib_request.urlopen(request)
    data = json.load(response)
    return data


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


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

# Here we parse the existing key list


def get_old_keys_whitelist():

    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:
        log.error('Failed to get whitelisted keys: {}'.format(e), exc_info=True)

    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['deveui'] = old_key['deveui'].lower().replace('-', '')
            old_key['index'] = old_key_index
            old_keys[old_key['deveui']] = old_key
        except Exception as e:
            log.error('Failed to parse old key {} {}'.format(old_key, e), exc_info=True)

    return old_keys

# Here we parse the existing key list


def get_old_device_keys():

    old_keys_output = None
    try:
        old_keys_output = lora_get_devices()
    except Exception as e:
        log.error('Failed to get devices keys: {}'.format(e), exc_info=True)

    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['deveui'] = old_key['deveui'].lower().replace('-', '')
            old_device_keys[old_key['deveui']] = old_key
        except Exception as e:
            log.error('Failed to parse old key: {} {}'.format(old_key, e), exc_info=True)

    return old_device_keys

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


def get_new_keys(args):
    new_keys = {}

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

    return new_keys

# Add keys to lora and return added keys


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

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

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

                try:
                    lora_add_device(device_config)
                except Exception as e:
                    log.error('Failed to add key {} {}'.format( dev_eui, e), exc_info=True)
                    keys_failed_to_add.append(dev_eui)
                    continue

                # Adding a new key to list
                added_keys.append(new_key)
        except Exception as e:
            log.error('Failed to handle added key: {}'.format(e), exc_info=True)

    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):
    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(list(old_whitelist_keys.items()), key=lambda item: item[1]['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['index'])
                except Exception as e:
                    error_message = 'HTTP Error %s: %s' % (e.code, e.read())
                    log.error('Failed to delete whitelisted key from LoRa server: {}'.format(error_message),
                              exc_info=True)
                    keys_failed_to_remove_white_list.append(dev_eui)
                    continue

                # Removing a key
                removed_keys_whitelist.append(old_key)

        except Exception as e:
            log.error('Failed to handle remove key {} {}'.format(dev_eui, e), exc_info=True)

    # Remove device keys from device list and consecutively device sessions if they exsist.
    for dev_eui, old_key in list(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())
                    log.error('Failed to delete device from LoRa server {} {}'.format(dev_eui, error_message),
                              exc_info=True)
                    keys_failed_to_remove_device_list.append(dev_eui)
                    continue

                # Removing a key
                removed_keys_devicelist.append(old_key)

        except Exception as e:
            log.error('Failed to remove device key {} {}'.format( dev_eui, e), exc_info=True)

    return removed_keys_whitelist, removed_keys_devicelist, keys_failed_to_remove_white_list, keys_failed_to_remove_device_list

def lorasync(new_keys):
    # Get all new and old keys and update them to lora
    old_whitelist_keys = get_old_keys_whitelist()
    old_device_keys = get_old_device_keys()
    #new_keys = get_new_keys(args)
    added_keys, keys_failed_to_add = add_keys(new_keys, old_whitelist_keys)
    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 what was changes
    # Log changes
    log.info('New keys: {}'.format(len(new_keys)))
    log.info('Old whitelist keys: {}'.format(len(old_whitelist_keys)))
    log.info('Old devicelist keys: {}'.format(len(old_device_keys)))
    log.info('Added keys: {}'.format(len(added_keys)))
    log.info('Failed to add keys: {}'.format(len(keys_failed_to_add)))
    log.info('Removed whitelist keys: {}'.format(len(removed_keys_whitelist)))
    log.info('Failed to remove whitelist keys: {}'.format(len(keys_failed_to_remove_whitelist)))
    log.info('Removed devicelist keys: {}'.format(len(removed_keys_devicelist)))
    log.info('Failed to remove devicelist keys: {}'.format(len(keys_failed_to_remove_device_list)))

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

# Main entry point


def main(args):
    # Get all new and old keys and update them to lora
    old_whitelist_keys = get_old_keys_whitelist()
    old_device_keys = get_old_device_keys()
    new_keys = get_new_keys(args)
    added_keys, keys_failed_to_add = add_keys(new_keys, old_whitelist_keys)
    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 what was changes
    # Log changes
    log.info('New keys: {}'.format(len(new_keys)))
    log.info('Old whitelist keys: {}'.format(len(old_whitelist_keys)))
    log.info('Old devicelist keys: {}'.format(len(old_device_keys)))
    log.info('Added keys: {}'.format(len(added_keys)))
    log.info('Failed to add keys: {}'.format(len(keys_failed_to_add)))
    log.info('Removed whitelist keys: {}'.format(len(removed_keys_whitelist)))
    log.info('Failed to remove whitelist keys: {}'.format(len(keys_failed_to_remove_whitelist)))
    log.info('Removed devicelist keys: {}'.format(len(removed_keys_devicelist)))
    log.info('Failed to remove devicelist keys: {}'.format(len(keys_failed_to_remove_device_list)))

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


if __name__ == '__main__':
    try:
        # Overwrite previous log to avoid filling disk
        #with open('/var/log/app/lorasync.log', 'w') as log:
            try:
                log.info('Started sync at {}'.format(datetime.datetime.now()))
                main(sys.argv[1:], log)
                log.info('Successfully completed sync at '.format(datetime.datetime.now()))
            except:
                log.error("Exception", exc_info=True)

    except:
        log.error("Exception", exc_info=True)
        raise
