"""
Name: itxpt_avms
Title: 
Author: Cooper
Date: 25/03/2021

Desc:  This module is the implementation of ITxPT AVMS, it's only purpose is to Subscribe to a given service and make
the obtained data available to the parent.  The part of the AVMS spec that is relevant to us is the
PlannedPattern.

If this module needs to handle the other parts of AVMS then it would likely needed to be expanded to handle the
subscriptions of those other parts, as well as parsing, timeouts etc.  In which case, it would make sense to make
this module as generic as possible, and only tell it which particular service to look.

This will not parse any data, only obtain the XML, and possibly convert it to a better format.
"""
import os
import time
import json
import http.client
import threading
import _thread
from urllib.parse import urljoin

import xmltodict
from bottle import request, Bottle, HTTPResponse

from hanip.itxpt import module_inventory_service
from hanip.itxpt import DNS_SD

from hanip.itxpt import avms_data_consumer

class ITxPTAVMS(object):
    def __init__(self, config_dict, hw_dict, conf_dir, data_dir):
        self.config_dict = config_dict
        self.hw_dict = hw_dict
        self.conf_dir = conf_dir
        self.data_dir = data_dir

        self.basic_mode = self.config_dict.get("AVMS_basic_mode", True)     #Basic mode is only when we want to obtain dest code
        self.self_consumption = self.config_dict.get("AVMS_self_consumption", True)
        self.service_discovery_running = False  #This is so that we do rerun the service discovery thread
        self.avms_server_found = False

        self.subscribe = self.config_dict.get("AVMS_subscribe", True)
        self.http_error = None
        self.server_port = self.config_dict.get("AVMS_reply_port", 9000)        #This is the Onions own HTTP server port

        self.service_ip = None  # IP address of the service provider
        self.service_port = None  # Port of the service provider
        self.service_root_path = None  # Request HTTP path

        self.avms_operation_details = {
            "runmonitoring": {
                "subscribe_path": "/avms/runmonitoring",
                "reply_path": "/avms/runmonitoringreply",
                "subscribed": False,
                "new_data": False,
                "raw_xml_data": None,
                "timeout": self.config_dict.get("AVMS_runmonitoring_timeout", 0),
                "timer": 0,
            },
            "plannedpattern": {
                "subscribe_path": "/avms/plannedpattern",
                "reply_path": "/avms/plannedpatternreply",
                "subscribed": False,
                "new_data": False,
                "raw_xml_data": None,
                "timeout": self.config_dict.get("AVMS_plannedpattern_timeout", 60),
                "timer": 0,
            },
            "vehiclemonitoring": {
                "subscribe_path": "/avms/vehiclemonitoring",
                "reply_path": "/avms/vehiclemonitoringreply",
                "subscribed": False,
                "new_data": False,
                "raw_xml_data": None,
                "timeout": self.config_dict.get("AVMS_vehiclemonitoring_timeout", 60),
                "timer": 0,
            },
            "journeymonitoring": {
                "subscribe_path": "/avms/journeymonitoring",
                "reply_path": "/avms/journeymonitoringreply",
                "subscribed": False,
                "new_data": False,
                "raw_xml_data": None,
                "timeout": self.config_dict.get("AVMS_journeymonitoring_timeout", 60),
                "timer": 0,
            },
            # "generalmessage": {
            #     "subscribe_path": "/avms/generalmessage",
            #     "reply_path": "/avms/generalmessagereply",
            #     "subscribed": False,
            #     "new_data": False,
            #     "raw_xml_data": None,
            #     "timeout": 60,
            #     "timer": 0,
            # },
            # "connectionmonitoring": {
            #     "subscribe_path": "/avms/connectionmonitoring",
            #     "reply_path": "/avms/connectionmonitoringreply",
            #     "subscribed": False,
            #     "new_data": False,
            #     "raw_xml_data": None,
            #     "timeout": 60,
            #     "timer": 0,
            # },
        }

        self.service_discover = DNS_SD.DNSSD_Discover("_itxpt_http._tcp.local.", "avms")

        if self.subscribe:
            self.setup_http_server()

        self.data_consumer = avms_data_consumer.AVMSDataConsumer()

    def setup_http_server(self):
        """
        Sets up an HTTP server that listens on all interfaces with the port number defined in the config file.  This HTTP
        server is run as a thread.
        """
        self.http_server = Bottle()

        self.http_server.route("/", method="POST", callback=self.handle_data_post)
        self.http_server.route("/avms/plannedpatternreply", method="POST", callback=self.handle_plannedpattern_post)
        self.http_server.route("/avms/runmonitoringreply", method="POST", callback=self.handle_runmonitoring_post)
        self.http_server.route("/avms/vehiclemonitoringreply", method="POST", callback=self.handle_vehiclemonitoring_post)
        self.http_server.route("/avms/journeymonitoringreply", method="POST", callback=self.handle_journeymonitoring_post)

        self.http_thread = threading.Thread(target=self.http_server.run, kwargs=dict(host="0.0.0.0",
                                                                                port=self.server_port,
                                                                                debug=False
                                                                                ))

        self.http_thread.daemon = True
        self.http_thread.start()

    def determine_data_paths(self, use_config=False):
        """
        This is for generating the paths for each AVMS operation that we are interested in, generally speaking though
        the names of the operations should be as they are in the spec so the only important bit of data to obtain is
        the root path
        :return: HTTP path for request, HTTP path for subscribe
        """
        if use_config:
            try:
                self.service_ip = self.config_dict["AVMS_service_address"]
            except KeyError:
                self.service_ip = None

            self.service_port = self.config_dict.get("AVMS_service_port", 9000)

        #Obtain the AVMS root path
        if self.config_dict["AVMS_discover"] and not use_config:
            try:
                root_path = self.service_discover.properties[b"path"].decode("utf-8")
            except KeyError:
                root_path = self.config_dict.get("AVMS_root_path", "/avms/")
        else:
            root_path = self.config_dict.get("AVMS_root_path", "/avms/")

        # Now create the operation paths
        for operation in self.avms_operation_details:
            operation_sub_path = urljoin(root_path, operation)

            if operation_sub_path[0] != "/":
                operation_sub_path = "/" + operation_sub_path

            self.avms_operation_details[operation]["subscribe_path"] = operation_sub_path
            print("AVMS: %s path %s" % (operation, operation_sub_path))

    def look_for_service(self):
        """
        If enabled, this will wait forever until a service is found, unless a timeout is configured.  Otherwise it will
        use values defined in the config file as a timeout.  Once a service has been discovered it will set the
        server_found flag to True and then terminate
        """
        print("AVMS: Looking for service")
        self.avms_server_found = False
        service_timeout = time.time()
        if not self.service_discovery_running:
            self.service_discover.run()
            self.service_discovery_running = True

        while 1:
            # As the service discovery runs in its own thread this checks if it has found an IP yet.
            if self.service_discover.serviceIP == "":
                print("AVMS: Looking for service  ...still")

                try:
                    timeout = int(self.config_dict.get("AVMS_timeout", 30)) # Default to no timeout if incorrect parameter
                except ValueError:
                    timeout = 0

                if timeout == 0:
                    time.sleep(1)
                    continue
                else:
                    if (time.time() - service_timeout) > int(self.config_dict["AVMS_timeout"]):
                        print("AVMS: Timeout reached")
                        return 1

                time.sleep(1)

            else:
                print("AVMS: Service found!")
                self.service_ip = self.service_discover.serviceIP
                self.service_port = self.service_discover.servicePort
                print(self.service_ip, self.service_port)

                self.avms_server_found = True
                return 0

    """
    ##################################################################################################################
    AVMS Subscribe
    """
    def subscribe_to_operations(self):
        """
        Previously when there was only one operation to subscribe to so only needed to worry about one but now with
        several, they each need to be subscribed to separately.

        I think the services should be subscribed in the order they are currently set to in self.avms_operation_details
        :return:
        """
        if self.basic_mode:
            operations = ["plannedpattern"]
        else:
            operations = self.avms_operation_details

        for operation in operations:
            print("AVMS: attempting %s subscription" % operation)
            self.send_subscription_request(operation)

    def check_operation_status(self):
        """
        The point of this routine is to make sure that the services are still being sent data and to resubscribe if necessary
        :return:
        """
        if self.basic_mode:
            operations = ["plannedpattern"]
        else:
            operations = self.avms_operation_details

        for operation in operations:
            if self.avms_operation_details[operation]["subscribed"]:
                timeout = self.avms_operation_details[operation]["timeout"]
                if timeout != 0:
                    if (time.time() - self.avms_operation_details[operation]["timer"]) > timeout:
                        print("AVMS: %s Timeout expired" % operation)
                        self.avms_operation_details[operation]["subscribed"] = False

                        #TODO Do we resub here?
                        self.send_subscription_request(operation)
            else:
                self.send_subscription_request(operation)


    def send_subscription_request(self, operation):
        """
        This creates the AVMS subscription message, it appears all the subscription requests are identical no matter
        which operation is being subscribed,  the only differences are the reply path, and the path where this is POSTed
        :return:
        """
        subscription_request = {
            "SubscribeRequest": {
                "Client-IP-Address": self.hw_dict["unit_IP"],
                "ReplyPort": self.server_port,
                "ReplyPath": self.avms_operation_details[operation]["reply_path"]        #TODO make this dynamic
            }
        }

        subscribe_xml = xmltodict.unparse(subscription_request, pretty=True)

        if self.post_subscription_message(subscribe_xml, self.avms_operation_details[operation]["subscribe_path"]):
            print("AVMS: %s subscription success" % operation)
            self.avms_operation_details[operation]["timer"] = time.time()
            self.avms_operation_details[operation]["subscribed"] = True

    def send_unsubscription_request(self, operation):
        """
        This creates the AVMS unsubscription message, it appears all the subscription requests are identical no matter
        which operation is being subscribed,  the only differences are the reply path, and the path where this is POSTed

        Although I cannot think of a situation where we would unsub but there for completeness sake.
        :return:
        """
        unsubscription_request = {
            "UnsubscribeRequest": {
                "Client-IP-Address": self.hw_dict["unit_IP"],
                "ReplyPort": "self.server_port",
                "ReplyPath": self.avms_operation_details[operation]["reply_path"]       #TODO make this dynamic
            }
        }

        unsubscribe_xml = xmltodict.unparse(unsubscription_request, pretty=True)

        if self.post_subscription_message(unsubscribe_xml, self.avms_operation_details[operation]["subscribe_path"]):
            self.avms_operation_details[operation]["subscribed"] = False

    def post_subscription_message(self, data, path):
        """
        This routine deals with the HTTP POST

        The header is for completeness sake, not entirely necessary but for this packet to show up in wireshark as HTTP
        then it is needed.
        It would also seem that the parameter name is case sensitive in INEOs implementation, official HTTP spec seems
        to indicate otherwise.
        :param data:
        :param path:
        :return:
        """
        print("Sending request to %s:%s%s" % (self.service_ip, self.service_port, path))

        header = {
            "Content-Type": "text/xml"
        }

        # Try 5 times to connect to a service before going back
        self.http_error = "AttemptingConnection"

        for tries in range(5):
            self.http_error = None
            try:
                conn = http.client.HTTPConnection(self.service_ip, self.service_port, timeout=5)
                conn.request("POST", path, data, header)
                reply = conn.getresponse()
            except ConnectionRefusedError as e:
                print("ConnectionRefusedError", e)
                self.http_error = "ConnectionRefusedError"
                return None
            except TimeoutError as e:
                print("TimeoutError", e)
                self.http_error = "TimeoutError"
                return None
            except OSError as e:
                print("OSError", e)
                self.http_error = "OSError"
                return None
            except http.client.HTTPException as e:
                print("HTTPError", e)
                self.http_error = "HTTPError"
                return None

            self.http_error = "None"
            status = reply.status
            body = reply.read().decode("utf-8")

            print("HTTP reply:")
            print(status, body, "\n")

            if reply.status == 200:  # 200 is HTTP for OK
                if "true" in body.lower():  # Quick hack, is there any need to properly parse the response?
                    return 1
                else:
                    time.sleep(3)
            else:
                time.sleep(3)

        return 0

    """
    ##################################################################################################################
    POST Handlers
    """
    def set_operation_xml_data(self, operation, raw_xml):
        """
        As a lot of the processes are identical for each operation this routine is called to fill in the dictionary
        and set the appropriate keys.
        :return:
        """
        print("AVMS: Setting new data for %s" % operation)
        self.avms_operation_details[operation]["raw_xml_data"] = raw_xml
        self.avms_operation_details[operation]["new_data"] = True

        if self.avms_operation_details[operation]["timeout"] != 0:
            self.avms_operation_details[operation]["timer"] = time.time()

    def handle_data_post(self):
        """

        :return:
        """
        xmldata = request.body.read().decode("utf-8")
        print(xmldata)

        return HTTPResponse(status=200)

    def handle_plannedpattern_post(self):
        """

        :return:
        """
        xmldata = request.body.read().decode("utf-8")
        if "Subscribe" in xmldata:
            pass
        else:
            self.set_operation_xml_data("plannedpattern", xmldata)

        return HTTPResponse(status=200)

    def handle_runmonitoring_post(self):
        """

        :return:
        """
        xmldata = request.body.read().decode("utf-8")
        if "Subscribe" in xmldata:
            pass
        else:
            self.set_operation_xml_data("runmonitoring", xmldata)

        return HTTPResponse(status=200)

    def handle_vehiclemonitoring_post(self):
        """

        :return:
        """
        xmldata = request.body.read().decode("utf-8")
        if "Subscribe" in xmldata:
            pass
        else:
            self.set_operation_xml_data("vehiclemonitoring", xmldata)

        return HTTPResponse(status=200)

    def handle_journeymonitoring_post(self):
        """

        :return:
        """
        xmldata = request.body.read().decode("utf-8")
        if "Subscribe" in xmldata:
            pass
        else:
            self.set_operation_xml_data("journeymonitoring", xmldata)

        return HTTPResponse(status=200)


    """
    ##################################################################################################################
    Main
    """
    def save_data_to_tmp(self, filename, avms_data_dict):
        """
        This saves whatever obtained payloads to /tmp so that other things can consume the data instead
        """
        if self.config_dict.get("AVMS_save_raw_payload", False):
            save_path = os.path.join("/tmp", filename + ".tmp")
            ready_path = os.path.join("/tmp", filename + ".json")

            with open(save_path, "w") as output_json_file:
                output_json_file.write(json.dumps(avms_data_dict))

            # Rename the file
            os.rename(save_path, ready_path)


    def consume_data(self):
        """
        This is where the raw data is processed.  If there is new data to deal with, it is first parsed.  If configured
        to do so then it will also dump the parsed contents into /tmp.

        Once parsed it is then possible to extract the data required.
        :return:
        """
        print("AVMS: Checking Data...")
        if self.avms_operation_details["runmonitoring"]["new_data"]:
            print("AVMS: \tRunMonitoring new data")
            run_monitoring_dict = self.data_consumer.parse_run_monitoring(self.avms_operation_details["runmonitoring"]["raw_xml_data"])
            self.save_data_to_tmp("runmonitoring", run_monitoring_dict)
            self.avms_operation_details["runmonitoring"]["new_data"] = False

            self.data_consumer.extract_journey_references()

        if self.avms_operation_details["plannedpattern"]["new_data"]:
            print("AVMS: \tPlannedPattern new data")
            planned_pattern_dict = self.data_consumer.parse_planned_pattern(self.avms_operation_details["plannedpattern"]["raw_xml_data"])
            self.save_data_to_tmp("plannedpattern", planned_pattern_dict)
            self.avms_operation_details["plannedpattern"]["new_data"] = False

            self.data_consumer.extract_destination_code()
            self.data_consumer.extract_journey_details()

            if not self.basic_mode:
                self.data_consumer.extract_list_of_stops()

        if self.avms_operation_details["journeymonitoring"]["new_data"]:
            print("AVMS: \tJourneyMonitoring new data")
            journey_monitoring_dict = self.data_consumer.parse_journey_monitoring(self.avms_operation_details["journeymonitoring"]["raw_xml_data"])
            self.save_data_to_tmp("journeymonitoring", journey_monitoring_dict)
            self.avms_operation_details["journeymonitoring"]["new_data"] = False

            self.data_consumer.extract_current_stop()

        if self.avms_operation_details["vehiclemonitoring"]["new_data"]:
            print("AVMS: \tVehileMonitoring new data")
            vehicle_monitoring_dict = self.data_consumer.parse_vehicle_monitoring(self.avms_operation_details["vehiclemonitoring"]["raw_xml_data"])
            self.save_data_to_tmp("vehiclemonitoring", vehicle_monitoring_dict)
            self.avms_operation_details["vehiclemonitoring"]["new_data"] = False

    def run(self):
        """
        The main loop that does all the important things I think...
        :return:
        """
        #This bit deals with obtaining the details of the service but also subscribing and will repeat the process
        #until it successfully connects to something
        while 1:
            use_config_for_path = True  # Assume that one cannot be found...
            if self.config_dict.get("AVMS_discover", True):
                if self.look_for_service():
                    print("AVMS: DNS Discovery returned nothing, using config")
                else:
                    use_config_for_path = False

            if self.service_ip == None or self.service_port == None:
                continue

            self.determine_data_paths(use_config_for_path)

            self.subscribe_to_operations()

            if self.basic_mode:
                if self.avms_operation_details["plannedpattern"]["subscribed"]:
                    break
            else:
                break

        operation_status_timer = time.time()

        while 1:
            if (time.time() - operation_status_timer) > 10:
                print("AVMS: Checking operation status")
                self.check_operation_status()
                operation_status_timer = time.time()

            if self.self_consumption:
                self.consume_data()
            time.sleep(1)

class Sign_AVMS(ITxPTAVMS):
    def __init__(self, config_dict, hw_dict, conf_dir, data_dir):
        super().__init__(config_dict, hw_dict, conf_dir, data_dir)

        self.xml_line_number_element = self.config_dict.get("AVMS_xml_line_number_element", "LineName")
        self.xml_dest_text_element = self.config_dict.get("AVMS_xml_dest_text_element", "DestinationLongName")

        self.mis = module_inventory_service.ModuleInventoryService(self.config_dict, self.hw_dict)

    def setup_sign_task(self):
        from hanip.onionip.sign import sign_task
        self.st = sign_task.SignTask(self.config_dict, self.hw_dict, self.conf_dir, self.data_dir)

    def setup_mis(self):
        self.mis.run()

    def setup_config_updater(self):
        from hanip.onionip import config_updater
        self.cu = config_updater.ConfigUpdater(self.config_dict, self.hw_dict, self.conf_dir)
        self.cu.setup_webserver()

    def generate_display_dict(self, avms_data_dict):
        """
        Generates the display dictionary from data obtained via IBISIP
        """
        delimiter = self.config_dict.get("TEMPLATES_delimiter", r"/")

        destination_text = avms_data_dict.get(self.xml_dest_text_element, " ")
        routenumber = avms_data_dict.get(self.xml_line_number_element, "")

        if destination_text is None:
            destination_text = " "
        else:
            destination_text = destination_text.replace("\n", delimiter)

        if routenumber is None:
            routenumber = " "

        display_data = {
            "$bcol": "0,0,0",
            "$fcol": self.config_dict.get("TEMPLATES_monocolour", "255,255,255"),
            "$rn": routenumber,
            "$dest": [destination_text]
        }

        self.st.update_data_dict(display_data)

    def check_avms(self):
        """
        Checks whether there is a new AVMS payload and deals with it accordingly.
        """
        if self.avms_operation_details["plannedpattern"]["new_data"]:
            print("AVMS_main: \tPlannedPattern new data")
            parsed_data = self.data_consumer.parse_planned_pattern(self.avms_operation_details["plannedpattern"]["raw_xml_data"])
            self.avms_operation_details["plannedpattern"]["new_data"] = False

            if parsed_data is not None:
                print(parsed_data)
                self.generate_display_dict(parsed_data)

    def run_sign(self):
        """
        Main loop
        """
        self.setup_sign_task()
        self.st.init_serial()
        self.setup_config_updater()
        self.setup_mis()
        self.st.display_sign_graphic("TRI-UP")

        _thread.start_new_thread(self.st.run, ())
        _thread.start_new_thread(self.run, ())

        try:
            while 1:
                self.check_avms()
                time.sleep(1)

        except KeyboardInterrupt:
            self.st.stop = True


if __name__ == "__main__":
    config_dict = {
        "AVMS_discover": True,
        "AVMS_timeout": 10,
        "AVMS_service_type": "_itxpt_http._tcp.local.",
        "AVMS_primary_hostname": "avms",
        "AVMS_subscribe": True,
        "AVMS_reply_port": 9000,
        "AVMS_service_address": "192.198.0.10",
        "AVMS_service_port": 8000,
        "AVMS_service_path": "/avms/plannedpattern"
    }

    hw_dict = {
        "unit_IP": "127.0.0.1"
    }

    avms = ITxPTAVMS(config_dict, hw_dict, None, None)
    _thread.start_new_thread(avms.run, ())

    while 1:
        if avms.avms_operation_details["plannedpattern"]["new_data"]:
            print(avms.avms_operation_details["plannedpattern"]["raw_xml_data"])
            avms.avms_operation_details["plannedpattern"]["new_data"] = False

            time.sleep(1)
