"""
@package   mqtt-broker-configurator
@file      mqtt_broker_configurator.py
@brief     Configures the MQTT broker for forwarding MQTT topics to the cloud

@author    andy wright
@date      12/06/2024
@copyright Copyright 2024 Hanover Displays Limited.
@license   This program is the confidential and proprietary product of
           Hanover Displays Limited. Any unauthorised use, reproduction or
           transfer of this program is strictly prohibited. (Subject to
           limited distribution and restricted disclosure only.) All
           rights reserved.
"""

import os
import argparse
import subprocess
import signal


DEFAULT_BRIDGE_OPTIONS = (
    ('restart_timeout', '30 300 300'),
    ('notifications', 'false'),
)


class mqttBrokerConfigurator(object):
    def __init__(self, inputPath, outputPath):
        self.inputPath  = inputPath
        self.outputPath = outputPath
        os.makedirs(self.outputPath, exist_ok=True)
        self.__addIncludeDirToMosquittoConfig(self.outputPath)

    def processBrokerConfig(self, serverAddress, serverPort, userName, password, customerID, VIN, deviceName, machineName, serialNumber):
        replacements = {
            '<CustomerID>': customerID,
            '<VIN>': VIN,
            '<deviceName>': deviceName,
            '<serverAddress>': serverAddress,
            '<serverPort>': serverPort,
            '<machineName>': machineName,
            '<serialNumber>': serialNumber,
        }

        if userName[0] == '!':
            buf = []
            self.__unscramble_text(buf, 62, userName[1:])
            userName = "".join(buf)
        replacements['<userName>'] = userName

        if password[0] == '!':
            buf = []
            self.__unscramble_text(buf, 62, password[1:])
            password = "".join(buf)
        replacements['<password>'] = password

        self.__process_files(self.inputPath, self.outputPath, replacements)

        self.__signalMosquittoToReloadConfig()

    def __process_files(self, input_dir, output_dir, replacements):
        try:
            files = [os.path.join(input_dir, f) for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]

            for file_path in files:
                base_name = os.path.basename(file_path)
                name_without_ext = os.path.splitext(base_name)[0]

                with open(file_path, 'r', encoding='utf-8') as src_file:
                    content = src_file.read()

                processed_content = self.__apply_replacements(content, replacements, name_without_ext)

                os.makedirs(output_dir, exist_ok=True)
                with open(os.path.join(output_dir, base_name), 'w', encoding='utf-8') as dst_file:
                    dst_file.write(processed_content)

        except FileNotFoundError:
            print(f"Directory '{input_dir}' not found.")
        except Exception as e:
            print(f"An error occurred: {e}")

    def __apply_replacements(self, content, replacements, base_name):
        processed_content = content.replace('<baseName>', base_name.replace('-', '_'))
        for old_word, new_word in replacements.items():
            processed_content = processed_content.replace(old_word, new_word)
        processed_content = self.__apply_bridge_defaults(processed_content)
        return processed_content

    def __apply_bridge_defaults(self, content):
        directives = set()
        for line in content.splitlines():
            stripped = line.strip()
            if not stripped or stripped.startswith('#'):
                continue
            directives.add(stripped.split(None, 1)[0])

        if 'connection' not in directives:
            return content

        missing_options = []
        for directive, value in DEFAULT_BRIDGE_OPTIONS:
            if directive not in directives:
                missing_options.append(f'{directive} {value}')

        if not missing_options:
            return content

        if content and not content.endswith('\n'):
            content += '\n'
        if content and not content.endswith('\n\n'):
            content += '\n'
        return content + '\n'.join(missing_options) + '\n'

    def __signalMosquittoToReloadConfig(self):
        process_name = "mosquitto"
        try:
            # Run the ps command without any options
            ps_output = subprocess.check_output(["ps"]).decode()

            # Find the line that contains the process name
            for line in ps_output.splitlines():
                if process_name in line:
                    # Extract the PID (it's the second item in the line, after splitting by whitespace)
                    pid = int(line.split()[0])

                    # Send the HUP signal to the process using os.kill
                    os.kill(pid, signal.SIGHUP)
                    print(f"HUP signal sent to process {process_name} with PID {pid}.")
                    break
            else:
                print(f"Process {process_name} not found.")
        except Exception as e:
            print(f"An error occurred: {e}")

    def __addIncludeDirToMosquittoConfig(self, includeDir):
        try:
            # Read the file content
            with open('/etc/mosquitto/mosquitto.conf', 'r', encoding='utf-8') as file:
                content = file.read()

            if f"include_dir {includeDir}" in content:
                print(f"include_dir {includeDir} already exists in the file.")
                return

            # Add the include_dir directive
            modified_content = content + f"\ninclude_dir {includeDir}\n"

            # Write the modified content back to the file
            with open('/etc/mosquitto/mosquitto.conf', 'w', encoding='utf-8') as file:
                file.write(modified_content)

        except FileNotFoundError:
            print(f"File '/etc/mosquitto/mosquitto.conf' not found.")
        except Exception as e:
            print(f"An error occurred: {e}")

    startCh = '!'
    outputChars = ord('~') - ord(' ')

    def __decode(self, c, reset):
        global off
        if reset:
            off = 3
        if ord(c) < ord('!') or ord(c) > ord('~'):
            return c
        c_val = ord(c) - ord(self.startCh)
        x = c_val - off
        if x < 0:
            x += self.outputChars
        off += 7
        if off >= self.outputChars:
            off = 3
        return chr(x + ord(self.startCh))

    def __unscramble_text(self, buf, size, s):
        global off
        off = 3  # Initialize static variable
        i = 0
        for char in s:
            if i >= size:
                return 1
            buf.append(self.__decode(char, i == 0))
            i += 1
        return 0

if __name__ == "__main__":
    # Instantiate the parser
    parser = argparse.ArgumentParser(description='Configures the MQTT broker for forwarding MQTT topics to the cloud')

    # Parse arguments
    parser.add_argument('inputPath')
    parser.add_argument('outputPath')
    parser.add_argument('--customerID', type=str, help='Configured Customer ID')
    parser.add_argument('--VIN', type=str, help='Configured VIN')
    parser.add_argument('--deviceName', type=str, help='Configured device name')
    parser.add_argument('--machineName', type=str, help='Machine name')
    parser.add_argument('--serialNumber', type=str, help='Serial number')
    parser.add_argument('--serverAddress', type=str, help='Configured server address')
    parser.add_argument('--serverPort', type=str, help='Configured server port')
    parser.add_argument('--userName', type=str, help='Configured user name')
    parser.add_argument('--password', type=str, help='Configured password')
    args = parser.parse_args()

    brokerConfig = mqttBrokerConfigurator(args.inputPath, args.outputPath)
    brokerConfig.processBrokerConfig(args.serverAddress,
                                     args.serverPort,
                                     args.userName,
                                     args.password,
                                     args.customerID,
                                     args.VIN,
                                     args.deviceName,
                                     args.machineName,
                                     args.serialNumber)
