"""
Name: device_management_service
Title: DeviceManagementService
Author: Cooper
Date: 20/04/2020

Desc:  This service is similar to the ModuleInventoryService done for ITxPT, key differences being of course the SRV
and TXT record and the files served up via HTTP.  This was initially writen for IBIS-IP version 1, but it seems that
version 2 serves up the same XMLs.

Example:
<_Service._Proto.Name TTL Class SRV Priority Weight Port Target>.

So for us it would be:
<_DeviceManagementService._ibisip_http.local. 3600 IN SRV 10 0 80 HanoverSign_1.local.>

The TXT record would contain only
- ver: The IBIS-IP version
- path: The path of the xml file

The hostname can be set via the address switch e.g. HanoverSign_0 would be sign zero, for a controller this can always
be 0 (might need looking at in more detail if there are more than one controllers)

Path of the HTTP service(s):
<HanoverSign_1:80/DeviceManagementService/GetDeviceConfiguration>
<HanoverSign_1:80/DeviceManagementService/GetDeviceStatus>
<HanoverSign_1:80/DeviceManagementService/GetDeviceInformation>

HTTP is no longer provided by the module that worked for ITxPT, now using library called Bottle, which is based on Flask
At some point it would be worth using the same lib for ITxPT.

23/03/2020:  At the moment everything is statically coded for testing purposes once the rest of the application has
been built we can make the data dynamic.

25/10/2023:  This series of changes is for SR2635.  We now need to make this dynamic so that it can be used on G5 and EG4
hw_dict looks something like this so whatever imports it should make something similar:
        self.hw_dict = {
            "serial_number": "",            #Mandatory
            "software_version": "NA",       #Optional, unused
            "hardware_version": "NA",       #Optional, unused
            "superx_version": "",           #Optional, unused
            "model": "NA",                  #Mandatory
            "address": "",                  #Mandatory
            "sign_size": "",                #Optional, unused
            "colour_resolution": None,      #Optional, unused
            "colour_alignment": None,       #Optional, unused
            "hw_status": "1",               #Optional, unused
            "hw_type": "NA",                #Mandatory
            "unit_IP": "",                  #Mandatory
            "unit_MAC": "",                 #Optional, unused
            "onion_ver": "",                #Optional, unused
        }


"""
import os
import time
import _thread
import threading
import xmltodict
import logging

from xml.dom.minidom import parseString

from bottle import route, run, template, Bottle
from bottle import static_file as bottle_file       #For serving up files, but may not be needed

from hanip.itxpt import DNS_SD


class DeviceManagementService(object):
    def __init__(self, config_dict: dict, hw_dict: dict, debug: bool = False):
        self.hw_dict = hw_dict
        self.config_dict = config_dict
        self.xml_path = "/tmp"      #Place all generated XML files in the temp dir
        self.debug = debug

        self.http_port = 8181
        self.http_server = Bottle()

        self.ibis_ip_version = "1.1"
        self.device_id = None
        self.ibisip_device_mapping()

    def ibisip_device_mapping(self):
        """
        This is used to map the device address to a given IBIS-IP device ID.  Because this isn't defined in the specs
        it will have to be set in the configuration.  Now this would offer two ways to set the ID, either you can
        explicity set an ID, or use some form of mapping.

        If all means to obtain one via the config fail then just use the address.
        """
        explicit_id = self.config_dict.get("IBISIP_explicit_id", "")

        if explicit_id == "":
            if self.hw_dict["hw_type"] != "con":
                mapped_id = self.config_dict.get("IBISIP_mapped_id_%s" % self.hw_dict["address"], "")
            else:
                mapped_id = self.config_dict.get("IBISIP_mapped_id_con", "10")        #CON isnt provided an address by hwdetermine

            if mapped_id == "":
                self.device_id = self.hw_dict["address"]
            else:
                self.device_id = mapped_id
        else:
            self.device_id = explicit_id

        return self.device_id

    """
    ###################################################################################################################
    HTTP Server related functions
    """

    def setup_http_server(self) -> None:
        """
        This sets up the HTTP paths required for DMS and then starts the webserver.
        :return:
        """
        logging.info("Initiating DMS Webserver port %s" % self.http_port)
        self.http_server.route('/test', method="GET", callback=self.test_page)
        self.http_server.route('/DeviceManagementService/GetDeviceConfiguration', method="GET", callback=self.device_configuration)
        self.http_server.route('/DeviceManagementService/GetDeviceErrorMessages', method="GET", callback=self.device_errors)
        self.http_server.route('/DeviceManagementService/GetDeviceInformation', method="GET", callback=self.device_information)
        self.http_server.route('/DeviceManagementService/GetDeviceStatus', method="GET", callback=self.device_status)
        self.http_server.route('/DeviceManagementService/GetServiceStatus', method="GET", callback=self.service_status)
        self.http_server.route('/DeviceManagementService/GetServiceInformation', method="GET", callback=self.service_information)
        self.http_server.route('/info', method="GET", callback=self.hardware_dictionary)

        self.http_thread = threading.Thread(target=self.http_server.run, kwargs=dict(host="0.0.0.0",
                                                                                port=self.http_port,
                                                                                debug=self.debug,
                                                                                quiet=not self.debug
                                                                                ))

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

    def test_page(self) -> str:
        """
        Dumpy function to test HTTP stuff
        :return:
        """
        return "Hello World"

    """
    ###################################################################################################################
    DNS-SD
    """

    def setup_dnssd(self) -> None:
        """
        This starts the DNS-SD service to advertise the presence of this device on the network
        :return:
        """
        logging.info("Initiating DNS-SD")

        ibisiptxt = {
            "version": self.ibis_ip_version,
            "path": self.config_dict.get("IBISIP_dms_root_path", "")        #This is for whether a slash is needed or not
        }

        self.dms_dnssd = DNS_SD.IBISIP_DNSSD(self.hw_dict["unit_IP"], "%s-%s" % (self.hw_dict["hw_type"], self.hw_dict["serial_number"]))
        self.dms_dnssd.device_management_service(self.http_port, ibisiptxt)

    def start_service(self) -> None:
        """
        This is the entry point into this module and needs to be run as a thread for it to keep running.
        :return:
        """
        logging.info("Starting DeviceManagementService...")
        self.setup_dnssd()
        self.setup_http_server()

        while 1:
            logging.info("DMS: Service running")
            time.sleep(10)


    """
    ###################################################################################################################
    XML Related Functions
    """

    def create_xml_dirs(self) -> None:
        """
        If we decide to generate files then we would store them here?
        :return:
        """
        try:
            os.mkdir(os.path.join(self.xml_path, "ibisip_xml"))
        except OSError as e:
            logging.info("Cannot create XML dir: %s" % e)
        else:
            logging.info("IBISIP XML dirs created")

    def get_time_stamp(self) -> str:
        """
        Generates a timestamp int he following format:
        YYYY-MM-DDTHH:MM:SSZ
        :return:
        """
        timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ")
        return timestamp

    def xml_pretty_printt(self, xml: str) -> str:
        """
        Makes the XML all pretty, aaww.
        :param xml:
        :return:
        """
        dom = parseString(xml)
        return dom.toprettyxml()

    """
    ###################################################################################################################
    IBIS-IP Calls
    """

    def device_configuration(self) -> str:
        """
        Frequency: One shot
        Path: /DeviceManagementService/GetDeviceConfiguration
        Contains timestamp and the device plug in position (device ID)
        MUST IMPLEMENT
        :return:
        """
        device_configuration_dict = {
            "DeviceManagementService.GetDeviceConfigurationResponse": {
                "DeviceManagementService.GetDeviceConfigurationResponseData": {
                    "TimeStamp": {
                        "Value": self.get_time_stamp()
                    },
                    "DeviceID": {
                        "Value": self.device_id
                    }
                }
            }
        }

        device_configuration_xml = xmltodict.unparse(device_configuration_dict, pretty=True)
        # logging.info(device_configuration_xml)
        return(device_configuration_xml)

    def device_errors(self) -> str:
        """
        Frequency: Cyclic updating
        Path: /DeviceManagementService/GetDeviceErrorMessages
        This contains a listing of all the errors that have occurred since the system started
        :return:
        """
        return("Device Error")

    def device_information(self) -> str:
        """
        Frequency: One shot
        Path: /DeviceManagementService/GetDeviceInformation
        Contains manufacture information and device class (including error message?)
        MUST IMPLEMENT
        :return:
        """
        #Obtain the device class, it seems that if a sign is not a front, then it is a side.
        if self.hw_dict["hw_type"] == "con":
            device_class = "MMI"
        else:
            if self.hw_dict["address"] == "0":
                device_class = "FrontDisplay"
            else:
                device_class = "SideDisplay"

        device_information_dict = {
            "DeviceManagementService.GetDeviceInformationResponse": {
                "DeviceManagementService.GetDeviceInformationResponseData":
                    {
                        "TimeStamp": {
                            "Value": self.get_time_stamp()
                        },
                        "DeviceInformation": {
                            "DeviceName": {
                                "Value": self.hw_dict["model"]
                            },
                            "Manufacturer": {
                                "Value": "Hanover Displays"
                            },
                            "SerialNumber": {
                                "Value": self.hw_dict["serial_number"]
                            },
                            "DeviceClass": device_class
                        }
                    }
            }
        }

        device_information_xml = xmltodict.unparse(device_information_dict, pretty=True)
        # logging.info(device_information_xml)
        return(device_information_xml)


    def device_status_information(self) -> str:
        """
        Frequency: Cyclic updating
        Path:
        Seems to present the most recent error, but isnt in the specification but XML files exist for it?
        NOT IMPLEMENTING
        :return:
        """
        pass

    def device_status(self) -> str:
        """
        Frequency: Cyclic updating
        Seems to present the most recent status: defective/notavailable/running
        How does an unavailable device report itself as such?
        MUST IMPLEMENT
        :return:
        """
        # return bottle_file("GetDeviceStatus.xml", "../www/", "application/xml", headers=None)

        device_status_dict = {
            "DeviceManagementService.GetDeviceStatusResponseData": {
                "DeviceManagementService.GetDeviceStatusResponseData": {
                    "TimeStamp": {
                        "Value": self.get_time_stamp(),
                    },
                    "DeviceState": "running"
                }
            }
        }

        device_status_xml = xmltodict.unparse(device_status_dict, pretty=True)
        return device_status_xml

    def service_information(self) -> str:
        """
        Frequency: One shot
        Provides information about the service, servicename, version
        MUST IMPLEMENT
        :return:
        """

        service_information_dict = {
            "DeviceManagementService.GetServiceInformationResponse": {
                "DeviceManagementService.GetServiceInformationResponseData":
                    {
                        "TimeStamp": {
                            "Value": self.get_time_stamp(),
                        },
                        "ServiceInformationList": {
                            "ServiceInformation": {
                                "Service": {
                                    "ServiceName": "DeviceManagementService",
                                    "IBIS-IP-Version": {
                                        "Value": self.ibis_ip_version
                                    }
                                },
                                "Autostart": {
                                    "Value": "true"
                                }
                            }
                        }
                    }
            }
        }

        service_information_xml = xmltodict.unparse(service_information_dict, pretty=True)
        # logging.info(service_information_xml)
        return (service_information_xml)

    def service_status(self) -> str:
        """
        No idea, seems useless
        NOT IMPLEMENTING
        :return:
        """
        return ("Service Status")

    def restart_device(self) -> None:
        """
        I guess this just tells the onion to reboot?  Or at least restart the service
        :return:
        """
        pass

    """
    HANOVER URLS
    """

    def hardware_dictionary(self) -> str:
        """
        This is not part of the IBIS-IP spec but I have added it so that we can get more information about the device
        via the same mechanisms
        :return:
        """
        hardware_dictionary = {
            "Hanover.GetHardwareDictionary": self.hw_dict
        }

        hardware_dictionary_xml = xmltodict.unparse(hardware_dictionary, pretty=True)

        return hardware_dictionary_xml


if __name__ == "__main__":
    import re

    ip_regex = r"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$"

    config_dict = {
        "test": "icles"
    }

    hw_dict = {
        "unit_IP": "192.168.10.30",
        "serial_number": "123456",
        "model": "CoopTest",
        # "hw_type": "con",
        "hw_type": "ext",
        "address": "21"
    }

    while 1:
        entered_ip = input("Enter device IP: ").rstrip()
        if re.match(ip_regex, entered_ip):
            hw_dict["unit_IP"] = entered_ip
            break
        else:
            print("Invalid IP, try again")

    dms = DeviceManagementService(config_dict, hw_dict, debug=True)
    _thread.start_new_thread(dms.start_service,())

    while 1:
        # print("test")
        time.sleep(5)
