"""
name: apc_service_subscriber
title: Automatic Passenger Counter subscriber
author: cooper
date: 06/01/2026

desc:
This is the module that handles obtaining data from the PassengerCountingService, be it via Request or Subscription.
Before data can be obtained, it is required to know the location and where in the location it is (HTTP Path)

The location can be obtained via DNS-SD, but this cannot be said for the the HTTP path.  There is a bit of a grey area
with regards to this.  HTTP path appears to be optional in the TXT record.

Example of DNS-SD reply of the  IRMA-6:

  Name: PassengerCountingService_1234_i6_0600003019._ibisip_http._tcp.local.
  Type: _ibisip_http._tcp.local.
  Address: 192.168.1.181:5211
  Weight: 0, priority: 0
  Server: i6-00-24-ea-04-03-88.local.
  TXT Records:
    b'ver': b'2.1'
  Name: DoorStateService_i6_0600003019._ibisip_http._tcp.local.
  Type: _ibisip_http._tcp.local.
  Address: 192.168.1.181:5213
  Weight: 0, priority: 0
  Server: i6-00-24-ea-04-03-88.local.
  TXT Records:
    b'ver': b'2.1'

Now this module is written specifically for the IRMA-6, the payloads should be identical but the paths may not be, so
if some other apc comes along then maybe this needs adjusting.

This also handles multiple APCs!

An example XML file can be found here:  http://git/Onion/HanIP/-/blob/ibisip-ret-tled-support.cc/test_scripts/ibis-ip/pc_data
"""
import queue
import time
import threading
import logging
import json

import xmltodict
from queue import Queue
from copy import deepcopy

from bottle import request, Bottle
from hanip.ibis_ip import ibisip_service_discovery
from hanip.ibis_ip import ibisip_service_subscriber
from hanip.ibis_ip import ibisip_data_parser
from hanip.itxpt import mqtt_client

class PassengerCountingSubscriber(object):
    def __init__(self, config_dict, hw_dict):
        self.config_dict = config_dict
        self.hw_dict = hw_dict

        self.ibisip_service_name = "PassengerCountingService"
        self.preferred_apc_version = self.config_dict.get("IBISIP-PC_pref_ver", "2.1")

        self.apcs_found = {}        # This contains a dict of found APCs with all their attributes where the key is the IP
        self.service_path_request = None    #Request HTTP path
        self.service_path_subscribe = None  #Subscribe HTTP path
        self.poll_interval = 5  # When module is in request mode, the frequency to request data

        self.subscribe = self.config_dict.get("IBISIP-PC_subscribe", True)      #I can't think of when this would be false
        self.subscription_timeout = self.config_dict.get("IBISIP-PC_subscription_timeout", 0)
        self.local_server_port = self.config_dict.get("IBISIP-PC_server_port", 5211)    #Local http port

        self.new_data = False
        self.apc_data_queue = Queue(maxsize=20)     # Raw(?) data from the APC is queued here for processing separately

        self.ibisip_service_subber = ibisip_service_subscriber.IBISIPServiceSubscriber(self.config_dict, self.ibisip_service_name)
        self.ibisip_pc_parser = ibisip_data_parser.IRMA_APC_Parser(self.config_dict)
        self.mqtt_client = mqtt_client.MQTT_Client()

        try:
            self.init_g4()
        except ImportError as e:
            self.eg4_mode = False
            # print("EG4 inactive")
            print(e)
        else:
            self.eg4_mode = True
            # print("EG4 active")

        if self.subscribe:
            #In subscribe mode, the module needs to initiate a HTTP server to receive POST messages
            self.setup_http_server()

    def init_g4(self):
        # Attempt to connect to the g4 console application.
        from __main__ import G4_CONSOLE
        # logging.info("G4: Initialising")
        self.g4hook = G4_CONSOLE()

    """
    ###################################################################################################################
    HTTP Server setup
    """

    def setup_http_server(self):
        """
        Sets up a 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()

        logging.info("%s Initiating %s Webserver port %s" % (self.ibisip_service_name, self.ibisip_service_name, self.local_server_port))

        self.http_server.route('/', method="GET", callback=self.handle_post)
        self.http_server.route('/PassengerCounterSubscriber/AllData', method="POST", callback=self.handle_all_data_post)
        #self.http_server.route('/PassengerCounterSubscriber/CountingState', method="POST", callback=self.handle_counting_state_post)

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

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

        logging.info("%s: HTTP Server ready" % self.ibisip_service_name)

    """
    ###################################################################################################################
    Service Discovery
    """
    def find_service(self):
        """
        Calls IBISIP Service discovery, any found services will look like this:
        {'name': 'PassengerCountingService_2_i6_0600003035._ibisip_http._tcp.local.', 'type': '_ibisip_http._tcp.local.', 'address': '192.168.1.173', 'port': 5211, 'weight': 0, 'priority': 0, 'server': 'i6-00-24-ea-04-03-80.local.', 'txt_records': {b'ver': b'2.1'}}

        One can assume that the APC will always advertise its presense on the network, at the moment there is no provision
        for defining these in the config

        """
        ibisipsd = ibisip_service_discovery.IBISIPServiceDiscovery(self.config_dict)
        found_apcs = ibisipsd.discover_relevant_services("PassengerCountingService", self.subscription_timeout)

        #Currently follows IRMA APC paths, but if that should change then it can be overriden in a config
        subscribe_path = self.config_dict.get("IBISIP-PC_alldata_path_subscribe",
                                              "$VER/PassengerCountingService/SubscribeAllData")

        get_path = self.config_dict.get("IBISIP-PC_alldata_path_get",
                                              "$VER/PassengerCountingService/GetAllData")

        if len(found_apcs) > 0:
            for apc in found_apcs:
                logging.info("%s APC found at %s" % (self.ibisip_service_name, apc["address"]))

                _apc_dict = self.apc_blank_dict()
                _apc_dict["name"] = apc["name"]
                _apc_dict["ip"] = apc["address"]
                _apc_dict["port"] = apc["port"]

                try:
                    version = apc["txt_records"][b"ver"].decode("utf-8")
                except KeyError:
                    version = "1.0"

                _apc_dict["version"] = version
                _apc_dict["subscribe_path"] = subscribe_path.replace("$VER", "/%s" % version)
                _apc_dict["get_path"] = get_path.replace("$VER", "/%s" % version)

                self.apcs_found[apc["address"]] =_apc_dict

            # print(self.apcs_found)

    def apc_blank_dict(self):
        """
        The dictionary format for the storing info on the APCs.  Just need to decide if the dict key should be the IP
        of the APC or not...
        """
        apc_dict = {
            "name": "",
            "version": "",
            "ip": "",
            "port": "",
            "get_path": "",
            "subscribe_path": "",
            "subscribed": False,
            "timer": 0,
            "doorid": "0"
        }

        return apc_dict

    """
    ###################################################################################################################
    Service Subscription
    """
    def subscribe_to_all_apcs(self):
        """
        This subscribes to all found apcs one at a time
        """
        for apc in self.apcs_found.keys():
            ip = self.apcs_found[apc]["ip"]
            port = self.apcs_found[apc]["port"]
            path = self.apcs_found[apc]["subscribe_path"]

            success, timer = self.send_subscription(ip, port, path)

            self.apcs_found[apc]["subscribed"] = success
            self.apcs_found[apc]["timer"] = timer

    def send_subscription(self, service_ip, service_port, service_path):
        """
        This sends the subscription xml to tell the APC where to post the counts to
        """
        subscription_dict = {
            "SubscribeRequest": {
                "Client-IP-Address": {
                    "Value": self.hw_dict["unit_IP"]
                },
                "ReplyPort": {
                    "Value": self.local_server_port
                },
                "ReplyPath": {
                    "Value": "/PassengerCounterSubscriber/AllData"
                }
            }
        }

        for x in range(5):
            sub_state, sub_time = self.ibisip_service_subber.send_subscription_request(service_ip,
                                                                                       service_port,
                                                                                       service_path,
                                                                                       subscription_dict,
                                                                                       blocking=False)

            if sub_state:
                logging.info("%s Subscription successful" % self.ibisip_service_name)
                return sub_state, sub_time

        return False, 0

    def send_unsubscription(self, service_ip, service_port, service_path):
        """
        Not jet implemented

        Needs completing and finding out what the format of this message is...
        """
        unsubscription_dict = {
            "UnsubscribeRequest": {
                "Client-IP-Address": {
                    "Value": self.hw_dict["unit_IP"]
                },
                "ReplyPort": {
                    "Value": self.config_dict["IBISIP_server_port"]
                },
                "ReplyPath": {
                    "Value": self.config_dict["IBISIP_server_path"]
                }
            }
        }

    """
    ###################################################################################################################
    Service data
    """
    def get_door_ids(self):
        """
        For IRMA APCs the door ID can be configured, although I am not sure what purpose this serves as they do not
        talk to each other.

        The needs a http get to /PassengerCountingService/GetAllData
        """
        for apc in self.apcs_found.keys():
            ip = self.apcs_found[apc]["ip"]
            port = self.apcs_found[apc]["port"]
            path = self.apcs_found[apc]["get_path"]

            all_data_raw_xml = self.ibisip_service_subber.get_data(ip, port, path)

            door_id = self.ibisip_pc_parser.extract_door_id(all_data_raw_xml)

            if door_id is not None:
                logging.info("%s APC@%s has doorid %s" % (self.ibisip_service_name, apc, door_id))
                self.apcs_found[apc]["door_id"] = door_id
            else:
                self.apcs_found[apc]["door_id"] = "0"


    """
    ###################################################################################################################
    APC Triggers
    """
    def connect_to_local_broker(self):
        """
        Connects to local (EG4) broker and subscribes to the digital input topics to obtain door signal
        """
        broker_ip = "127.0.0.1"
        di_topic = "door_states/state"

        logging.info("%s Connecting to local mqtt broker" % self.ibisip_service_name)
        self.mqtt_client.set_broker_address(broker_ip, False)
        self.mqtt_client.connect_client()
        self.mqtt_client.subscribe_to_topics(di_topic)

        self.mqtt_thread = threading.Thread(target=self.mqtt_client.run_client)

        self.mqtt_thread.daemon = True
        self.mqtt_thread.start()
        logging.info("%s Connected to local mqtt broker!" % self.ibisip_service_name)

    def poll_door_state(self):
        """
        Polls the mqtt client to see if there is data to process.

        Door state payload looks like this:

        {
            "atDateTime": "2026-03-02T10:08:19+00:00",
            "changedAt": "2026-03-02T10:08:19+00:00",
            "state": "CLOSED"
        }

        """
        if self.mqtt_client.newMsg:
            self.mqtt_client.newMsg = False

            door_state = self.mqtt_client.payload

            try:
                door_state_dict = json.loads(door_state)
            except json.JSONDecodeError as e:
                logging.error(e)
                return False

            try:
                door_state = door_state_dict["state"]
            except KeyError:
                logging.warning("%s No door state" % self.ibisip_service_name)
                return False
            else:
                logging.info("%s Door open? %s" % (self.ibisip_service_name, door_state))
                if door_state == "OPENED":
                    return True
                else:
                    return False

    def trigger_counting(self, trigger=True):
        """
        This tells the APCs to start (default behaviour) or stop counting.

        For this command to work, Installation > Door > Use door contact must be disabled
        Door number/ID can be set in the same place but changing it has no bearing on the doorID.
        """

        if trigger:
            logging.info("%s Starting count" % self.ibisip_service_name)
            xml_root = "PassengerCountingService.StartCountingRequest"
            path = "/PassengerCountingService/StartCounting"
        else:
            logging.info("%s Stopping count" % self.ibisip_service_name)
            xml_root = "PassengerCountingService.StopCountingRequest"
            path = "/PassengerCountingService/StopCounting"

        for apc in self.apcs_found.keys():
            ip = self.apcs_found[apc]["ip"]
            port = self.apcs_found[apc]["port"]
            ver = self.apcs_found[apc]["version"]
            door_id = self.apcs_found[apc]["door_id"]

            _path = ("/%s" % ver) + path

            trigger_count_dict = {
                xml_root: {
                    "DoorIdList": {
                        "DoorID": {
                            "Value": door_id
                        }
                    }
                }
            }

            trigger_count_xml = xmltodict.unparse(trigger_count_dict)

            status, body = self.ibisip_service_subber.post_data(ip, port, _path, trigger_count_xml)

            if status != 200:
                logging.error("Cannot post count trigger")
            else:
                logging.info("%s Count trigger state: %s" % (self.ibisip_service_name, trigger))


    """
    ###################################################################################################################
    Data Handling
    """
    def handle_post(self):
        """
        Dummy callback, not used for anything but to prove the server is up and running
        """
        client_ip = request.environ.get("REMOTE_ADDR")
        print(client_ip)

    def handle_all_data_post(self):
        """
        As there are potentially multiple APCs this should go into a queue for processing separately
        """
        client_ip = request.environ.get("REMOTE_ADDR")
        xmldata = request.body.read().decode("utf-8")

        logging.info("POST from %s" % client_ip)
        #This needs to be put into a queue!!
        try:
            self.apc_data_queue.put([client_ip, xmldata], block=False)
        except queue.Full:
            logging.error("IBISIP-PC: Queue full")
        else:
            self.new_data = True

    def handle_counting_state_post(self):
        """
        Not used currently, maybe expanded if requirements need it.
        """
        client_ip = request.environ.get("REMOTE_ADDR")
        xmldata = request.body.read().decode("utf-8")
        print(xmldata)

    def add_all_totals(self):
        """
        Adds the totals of all the APCs together
        """
        #Take a copy of the values as it stands
        apc_data = deepcopy(self.apcs_found)

        sum = 0

        for apc in apc_data.keys():
            try:
                _counts = apc_data[apc]["counts"]
            except KeyError:
                logging.error("%s No count data for APC at %s" % (self.ibisip_service_name, apc))
                continue

            if isinstance(_counts, dict):
                for object in _counts.keys():
                    _totals = _counts[object]["total"]

                    sum += _totals

        logging.info("%s Total count: %s" % (self.ibisip_service_name, sum))
        return sum

    def convert_to_eg4_payload(self, total_count):
        """
        This creates the EG4 xml and then funnels it along to the eg4 hook.
        """
        eg4_apc_payload_dict = {
            "PassengerCountingService.GetAllDataResponse": {
                "AllData": {
                    "TimeStamp": {
                        "Value": "1970-01-01T02:50:17"
                    },
                    "CountingData": {
                        "TotalPassengerCount": {
                            "Value": total_count
                        },
                        "DoorID": {
                            "Value": "1"
                        },
                        "Count": [
                            {
                                "ObjectClass": "Adult",
                                "In": {
                                    "Value": "1"
                                },
                                "Out": {
                                    "Value": "1"
                                },
                                "CountQuality": "Regular"
                            },
                            {
                                "ObjectClass": "Child",
                                "In": {
                                    "Value": "1"
                                },
                                "Out": {
                                    "Value": "1"
                                },
                                "CountQuality": "Regular"
                            }
                        ],
                        "State": {
                            "OpenState": {
                                "Value": "OpenState"
                            }
                        }
                    }
                }
            }
        }

        if self.eg4_mode:
            logging.info("%s feeding EG4" % self.ibisip_service_name)
            eg4_apc_xml = xmltodict.unparse(eg4_apc_payload_dict)
            print(eg4_apc_xml)
            self.g4hook.process_ibis_ip_xml(eg4_apc_xml)

    """
    ###################################################################################################################
    Main
    """
    def main(self):
        """
        Main loop
        """
        self.connect_to_local_broker()

        self.find_service()
        self.get_door_ids()
        self.subscribe_to_all_apcs()

        counting_active = False

        while 1:
            door_poll = self.poll_door_state()

            if door_poll is not None:
                if door_poll:
                    if not counting_active:
                        counting_active = True
                        self.trigger_counting(True)
                else:
                    if counting_active:
                        self.trigger_counting(False)
                        counting_active = False

            try:
                queue_item = self.apc_data_queue.get(block=False)
            except queue.Empty:
                #Process totals when queue is empty
                if self.new_data:
                    total_count = self.add_all_totals()
                    self.convert_to_eg4_payload(total_count)
                    self.new_data = False

                time.sleep(1)
                continue

            source_ip = queue_item[0]
            raw_alldata_xml = queue_item[1]

            apc_values = self.ibisip_pc_parser.extract_pc_data(raw_alldata_xml)
            self.apcs_found[source_ip]["counts"] = apc_values

            time.sleep(0.1)


if __name__ == "__main__":
    import sys
    logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)

    configs = {
        "IBISIP-PC_discover": True,
        "IBISIP-PC_ver": "2.1",
        "IBISIP-PC_timeout": 0,
        "IBISIP-PC_service_ip": "192.168.1.181",
        "IBISIP-PC_service_port": 5211,
        "IBISIP-PC_subscribe": True,
        "IBISIP-PC_server_port": 8080,
        "IBISIP-PC_preamble": "PassengerCountingService.GetAllDataResponse",
        "IBISIP-PC_time_stamp": "AllData;TimeStamp;Value",
        "IBISIP-PC_door_id": "AllData;CountingData;DoorID;Value",
        "IBISIP-PC_count": "AllData;CountingData;Count",        # This points to a list
    }

    hw = {
        "unit_IP": "192.168.1.177",
        "hw_type": "con"
    }

    fake_apcs = {
        "192.168.1.181": {
            "ip": "192.168.1.181",
            "port": 5211,
            "version": "2.1"
        }
    }

    pc = PassengerCountingSubscriber(configs, hw)
    pc.main()

    # pc.apcs_found = fake_apcs
    # pc.trigger_counting()
    # time.sleep(10)
    # pc.trigger_counting(False)

