#!/usr/bin/python

from __future__ import print_function
import gc
import sys
import json
import time
# import base64
import signal
import subprocess
import paho.mqtt.client as mqtt

# def mqtt_onlog(client, userdata, level, buf):
#     print("{}".format(buf))
#     return True

gl_loop = True
gl_devsn = None
gl_appkey = 'mqttforward'
def get_mqttforward_extdat(fwd_cfg, ext_dat):
    if ext_dat is None:
        return False
    try:
        mfcfg = json.loads(ext_dat)
    except (TypeError, ValueError, StopIteration) as valerr:
        print("Error, invalid mqttforward extdat")
        return False
    if not isinstance(mfcfg, dict):
        print("Error, invalid mqttforward extdat: {}".format(ext_dat))
        return False
    tenant = mfcfg.get("tenant")
    gwsn = mfcfg.get("gwsn")
    if tenant is None or gwsn is None:
        return False
    service_post = mfcfg.get('service_post') and True or False
    property_post = mfcfg.get('property_post') and True or False
    if not service_post and not property_post:
        return False
    fwd_cfg[gwsn] = {'tenant': tenant, 'service_post': service_post, 'property_post': property_post}
    return True
def get_mqttforward_config():
    try:
        fcfg = open("/app/node/nodes_cfg.json", "rb")
        nodecfg = json.load(fcfg); fcfg.close()
        # ftem = open("/app/node/templates_cfg.json", "rb")
        # tempcfg = json.load(ftem); ftem.close()
    except (IOError, ValueError) as ioe:
        print(ioe)
        return None
    nodes_cfg = nodecfg.get("nodes_cfg")
    if nodes_cfg is None or not isinstance(nodes_cfg, list):
        print("Error, cannot get nodes_cfg")
        return None
    fwdcfg, hasext = dict(), 0
    for node in nodes_cfg:
        if not isinstance(node, dict):
            print("Error, not dictionary: " + str(type(node)))
            continue
        app_key = node.get("app_key")
        if app_key is None or app_key != gl_appkey:
            continue
        extdat = node.get('ext_data')
        if get_mqttforward_extdat(fwdcfg, extdat):
            hasext += 1
    if hasext > 0:
        return fwdcfg
    return None
def load_mqtt_config(mcfg):
    mdft = dict()
    mdft["host"] = "mqtt.lnxall.com"
    mdft["port"] = 3883
    mdft["user"] = 'localuser'
    mdft["pass"] = 'dywl@galaxy'
    if mcfg is None:
        mcfg = '/app/config/mqtt_server.json'
    try:
        mfil = open(mcfg, "rb")
        mcfgs = json.load(mfil);
        mfil.close()
    except (IOError, ValueError) as ioe:
        print(ioe)
        mcfgs = mdft
    if not isinstance(mcfgs, dict):
        mcfgs = mdft
    return mcfgs
# Simple OpenOPC/MQTT client class
class MQTTForward:
    def __init__(self):
        self.client = None # internal MQTT client
        self.cloud_mqtt = None # cloud MQTT client
        self.gwtopics = None # gateway SN forward topics
        self.haserr, self.gwlist = True, None
        # load nodes_cfg.json & templates_cfg.json
        fwdcfg = get_mqttforward_config()
        if fwdcfg is not None:
            self.gwlist = fwdcfg
            self.haserr = False
            self.gwtopics = dict()
            for snkey in fwdcfg.keys():
                self.gwtopics[snkey] = "ipc/{}/{}/device/{}/data/property/post".format(gl_devsn, gl_appkey, snkey)
        # cloud mqtt forward topic for kafka:
        self.ftopic = "ipc/{}/cloud/device/forward/data_filtered/service/post".format(gl_devsn)
    def on_connect(self, client, userdat, flags, rc):
        if client is self.cloud_mqtt:
            print("Cloud MQTT connection established: " + str(rc))
            for gwsn in self.gwlist.keys():
                gwinfo = self.gwlist[gwsn]
                tenant = gwinfo['tenant']
                print("MQTT-Forward for gateway {}: service: {}, property: {}".format(
                    gwsn, gwinfo['service_post'], gwinfo['property_post']))
                topi = "/sys/{}/{}/service".format(tenant, gwsn)
                ret = client.subscribe(topi)
                print("Subscribing topic \"{}\": {}".format(topi, ret))
        else:
            print("Internal MQTT connection established: " + str(rc))
        return True
    def on_ipc_message(self, client, userdata, msg):
        pload = msg.payload # MQTT payload
        print("Internal IPC MQTT message: {} {}".format(msg.topic, msg.payload))
        return True
    def on_cloud_message(self, client, userdata, msg):
        pload = msg.payload # MQTT payload
        # print("Received cloud message, topic: {}, payload:\n{}".format(msg.topic, pload))
        try:
            jload = json.loads(pload)
        except Exception as exc:
            print("Error, failed to decode cloud message: {}".format(exc))
            return False
        # get forward topic for the gateway
        gwsn, ftop, gwinfo = jload.get('gw_sn'), None, None
        if gwsn:
            gwinfo = self.gwlist.get(gwsn)
        if gwinfo and gwinfo.get('service_post'):
            # forward MQTT message from mqtt.lnxall.com to kafka
            self.client.publish(self.ftopic, pload)
        if not gwinfo or not gwinfo.get('property_post'):
            # no need to forward to property_post
            return True
        ################# forward the message to ipc-property ##############################
        ftop = self.gwtopics.get(gwsn)
        if ftop is None:
            print("Error, unknown gw_sn: {}".format(str(gwsn)))
            return False
        tval = jload.get('tags')
        if tval is not None:
            del jload['tags'] # move tags to tag_node
            jload['tag_node'] = json.dumps(tval)
        elif jload.get('tag_node') is None:
            # ipc/+/+/device/+/data/property/post needs to have tag_node field
            return False
        # remove gw_sn and set sn to gw_sn
        jload["sn"] = gwsn; del jload['gw_sn']
        self.client.publish(ftop, json.dumps(jload))
        return True
    def on_disconnect(self, client, userdata, rc):
        global gl_loop
        gl_loop = False
        iscloud = "cloud"
        if client is self.client:
            iscloud = "internal"
        print("{} MQTT connection down: {}".format(iscloud, rc))
        return False
    def cloud_connect(self):
        # connect to cloud_mqtt server
        self.cloud_mqtt = mqtt.Client(protocol=mqtt.MQTTv311)
        self.cloud_mqtt.on_connect = self.on_connect
        self.cloud_mqtt.on_message = self.on_cloud_message
        self.cloud_mqtt.on_disconnect = self.on_disconnect
        # self.cloud_mqtt._on_log = mqtt_onlog
        # read /app/config/mqtt_server.json
        mcfg, rval = load_mqtt_config(None), False
        if mcfg.get('user') is not None:
            self.cloud_mqtt.username_pw_set(mcfg['user'], password=mcfg.get('pass'))
        try:
            self.cloud_mqtt.connect_async(mcfg.get('host'), port=mcfg.get('port'))
            self.cloud_mqtt.reconnect()
        except Exception as excpt:
            print(excpt)
            print("Error, failed to connect to cloud mqtt server")
            return False
        for _ in range(5):
            # wait 2 seconds for connection establishment
            time.sleep(2)
            self.cloud_mqtt.loop(timeout=1.0)
            if self.cloud_mqtt.is_connected():
                rval = True
                break
        return rval
    def main(self):
        global gl_loop
        if self.haserr:
            print("Error, failed to load MQTT forward configs!")
            return False
        # connect to internal IPC MQTT broker
        self.client = mqtt.Client()
        self.client.on_connect = self.on_connect
        self.client.on_message = self.on_ipc_message
        self.client.on_disconnect = self.on_disconnect
        self.client.connect("127.0.0.1")
        # wait 4 seconds for MQTT connection
        self.client.loop(timeout=2)
        self.client.loop(timeout=2)

        if not self.cloud_connect():
            print("Error, failed to connect to cloud MQTT!")
            return False
        while gl_loop:
            if not self.cloud_mqtt.is_connected():
                print("Error, cloud_mqtt disconnected!")
                break
            self.cloud_mqtt.loop(timeout=2)
            if not gl_loop:
                break
            self.client.loop(timeout=2)
        # loop terminates, disconnect
        self.cloud_mqtt.disconnect()
        self.client.disconnect()
        return False
    def __del__(self):
        self.client = None
        self.cloud_mqtt = None
        return True
# Signal handler function for SIGINT & SIGTERM
def opcsighandler(signo, sigfrm):
    global gl_loop
    gl_loop = False
    print("Received a signal: {}".format(signo))
    return True
# function to fetch gateway SN
def get_devsn(argv):
    global gl_devsn
    try:
        pop = subprocess.Popen(argv, stdout=subprocess.PIPE, shell=False)
        pop.wait()
        dev_sn = pop.stdout.read().decode()
    except Exception as exval:
        print(exval)
        return False
    dev_sn = dev_sn.strip("\r\n\t ")
    if "=" in dev_sn:
        dlist = dev_sn.split("=")
        if len(dlist) != 2:
            print("Invalid output for SN: " + dev_sn)
            return False
        dev_sn = dlist[1]
        dev_sn = dev_sn.strip("'")
    if len(dev_sn) == 0:
        return False
    gl_devsn = dev_sn
    return True
if not get_devsn([ 'fw_printenv', 'SN' ]) and not get_devsn(["uci", "-X", "show", "system.system.hostname"]):
    print("Error, cannot get SN")
    sys.exit(1)
# Create an OPC-To-MQTT instance
mqttfor = MQTTForward()
# Register signals: SIGINT & SIGTERM
signal.signal(signal.SIGINT, opcsighandler)
signal.signal(signal.SIGTERM, opcsighandler)
# invoke main function
mqttfor.main()
del mqttfor; gc.collect()
# terminate script with non-zero status
mqttfor = None; sys.exit(1)
