# author Aaron Moser

#---------------------------------------------------------------------------------------------------------------

import constants

from typing import List
from datetime import datetime, timedelta

from heater_instance_interface_manager import HeaterInstanceInterfaceManager
from global_outcome import GlobalOutcome
from simulation import Simulation
from heater.host import Host

#---------------------------------------------------------------------------------------------------------------

class LoadBalancer:
    """Implementation of load balancer, allows to add instances and start the simulation.
    """

#---------------------------------------------------------------------------------------------------------------

    def __init__(
            self,
            global_outcome,
            db_interface,
            simulation_mode: Simulation.Mode,
            start_date: datetime,
            end_date: datetime
        ):
        """Instantiates a LoadBalancer object.

        Args:
            self (LoadBalancer): this current object, which is instantiated.
            db_interface (DatabaseInterface): interface class to database.
            simulation_mode (Simulation.Mode): current mode of simulation.
        """
        self.state: constants.State = constants.State.Stopped
        self.global_outcome: GlobalOutcome = global_outcome
        self.db_interface = db_interface
        self.simulation = Simulation()
        self.simulation.set_mode(simulation_mode)
        self.running = False
        self.start_date = start_date
        self.end_date = end_date
        self.heater_instance_interface_manager: HeaterInstanceInterfaceManager = HeaterInstanceInterfaceManager(global_outcome, simulation_mode, start_date, end_date, db_interface)

#---------------------------------------------------------------------------------------------------------------

    def run(self) -> None:
        """Loop of asking user for input and processing entered command.

        Args:
            self (LoadBalancer): this current object, on which method is called.

        Returns:
            None
        """
        self.print_start_message()
        self.running = True
        while self.running:
            self._process_first_level_command(self._get_command_args())

#---------------------------------------------------------------------------------------------------------------

    def print_start_message(self) -> None:
        """Prints message, load balancer started

        Args:
            self (LoadBalancer): this current object, on which method is called.

        Returns:
            None
        """
        print(constants.START_MESSAGE)

#---------------------------------------------------------------------------------------------------------------

    def _get_command_args(self) -> List[str]:
        """Prompts user to enter command and returns command string array splitted by spaces

        Args:
            self (LoadBalancer): this current object, on which method is called.

        Returns:
            List[str]: Partstrings which are created by splitting the command entered by user at spaces.
        """
        return input("-------------------------------\nPlease enter command (Enter 'help' to see all commands)\nCommand: ").lower().split(" ")
        
#---------------------------------------------------------------------------------------------------------------

    def _process_first_level_command(self, command_args) -> None:
        """Processes the first level (command_args[0]) command.

        Args:
            self (LoadBalancer): this current object, on which method is called.
            command_args (List[str]): partstrings of command string split at spaces.

        Returns:
            None
        """

        match command_args[0]:

            #
            case constants.START_SIMULATION_COMMAND:
                if self.heater_instance_interface_manager.get_heater_instances_count() >= constants.MINIMUM_NUMBER_CLIENTS:
                    self.start_simulation(command_args)
                else:
                    print("Please add more instances, before starting the simulation.")

            case constants.STOP_SIMULATION_COMMAND:
                self.stop_simulation()

            #     
            case constants.SET_SIMULATION_MODE_COMMAND:
                if self.heater_instance_interface_manager.get_heater_instances_count() >= constants.MINIMUM_NUMBER_CLIENTS:
                    self.set_simulation_mode(command_args)
                else:
                    print("Please add more instances, before re-setting the simulation mode.")

            #
            case constants.ADD_INSTANCE_COMMAND:
                if self.heater_instance_interface_manager.get_heater_instances_count() < constants.MAXIMUM_NUMBER_CLIENTS:
                    self.add_instance(command_args)
                else:
                    print("Maximum number of clients reached, you have to remove an instance before you can add another.")

            #
            case constants.REMOVE_INSTANCE_COMMAND:
                self.remove_instance(command_args)

            #
            case constants.SHOW_INSTANCES_COMMAND:
                self.show_instances()

            case constants.DISPLAY_OUTCOME_COMMAND:
                self.display_outcome()

            #
            case constants.HELP_COMMAND:
                self.print_help()

            # 
            case constants.EXIT_COMMAND:
                self.exit_application()

            # If user enters command, which doesn't match any case above.
            case _:
                print("Unknown command: " + command_args[0])

#---------------------------------------------------------------------------------------------------------------

    def start_simulation(self, command_args) -> None:
        """Starts simulation.

        Args:
            self (LoadBalancer): this current object, on which method is called.
            command_args (List[str]): partstrings of command string split at spaces.

        Returns:
            None
        """
        if len(command_args) > 1:
            match command_args[1]:
                case constants.SET_SIMULATION_MODE_COMMAND_REALTIME:
                    if len(command_args) == 4:
                        start_date = command_args[2]
                        end_date = command_args[3]
                        date_format = "%d.%m.%Y"
                        self.start_date = datetime.strptime(start_date, date_format)
                        self.end_date = datetime.strptime(end_date, date_format)
                        self.start_simulation_in_mode(Simulation.Mode.REALTIME)

                    elif len(command_args) == 3:
                        start_date = command_args[2]
                        date_format = "%d.%m.%Y"
                        self.start_date = datetime.strptime(start_date, date_format)
                        one_day_difference = timedelta(days=1)
                        self.end_date = start_date + one_day_difference
                        self.start_simulation_in_mode(Simulation.Mode.REALTIME)
                        
                    else:
                        self.start_simulation_in_mode(Simulation.Mode.REALTIME)
                
                case constants.SET_SIMULATION_MODE_COMMAND_DAILY:
                    if len(command_args) == 4:
                        start_date = command_args[2]
                        end_date = command_args[3]
                        date_format = "%d.%m.%Y"
                        self.start_date = datetime.strptime(start_date, date_format)
                        self.end_date = datetime.strptime(end_date, date_format)
                        self.start_simulation_in_mode(Simulation.Mode.DAILY)

                    elif len(command_args) == 3:
                        start_date = command_args[2]
                        date_format = "%d.%m.%Y"
                        self.start_date = datetime.strptime(start_date, date_format)
                        one_day_difference = timedelta(days=1)
                        self.end_date = start_date + one_day_difference
                        self.start_simulation_in_mode(Simulation.Mode.DAILY)
                        
                    else:
                        self.start_simulation_in_mode(Simulation.Mode.DAILY)
                    
                case _:
                    print("Unknown simulation command.")
        else:
            self.heater_instance_interface_manager.change_simulation_mode(self.simulation.get_mode())

    def start_simulation_in_mode(self, simulation_mode: Simulation.Mode):
        self.heater_instance_interface_manager.set_start_and_end_date(self.start_date, self.end_date)
        self.heater_instance_interface_manager.change_simulation_mode(simulation_mode)
        self.simulation.set_mode(simulation_mode)
        self.state: constants.State = constants.State.Running
        match simulation_mode:
            case Simulation.Mode.DAILY:
                print("Simulation started in daily mode. With start date: " + str(self.start_date) + " and end date: " + str(self.end_date))
            case Simulation.Mode.REALTIME:
                print("Simulation started in realtime mode. With start date: " + str(self.start_date) + " and end date: " + str(self.end_date))

#---------------------------------------------------------------------------------------------------------------

    def stop_simulation(self) -> None:
        if self.state == constants.State.Running:
            self.heater_instance_interface_manager.stop_threads()
            self.state = constants.State.Stopped
            print("Stopped simulation. To restart enter StartSimulation [mode] [start date] [end date].")
        else:
            print("Error: Simulation is not running. Can't stop if not running.")

#---------------------------------------------------------------------------------------------------------------

    def set_simulation_mode(self, command_args) -> None:
        """If simulation is running, a user has to use stop simulation and start again or just SetSimulationMode. 
        Will stop currently running threads and restart them with new mode.

        Args:
            self (LoadBalancer): this current object, on which method is called.

        Returns:
            None
        """
        if len(command_args) > 1:
            match command_args[1]:
                case constants.SET_SIMULATION_MODE_COMMAND_REALTIME:
                    if self.get_approval():
                        self.heater_instance_interface_manager.change_simulation_mode(Simulation.Mode.REALTIME)
                        self.simulation.set_mode(Simulation.Mode.REALTIME)
                    else:    
                        print("Continuing at current mode.")
                        
                case constants.SET_SIMULATION_MODE_COMMAND_DAILY:
                    if self.get_approval():
                        self.heater_instance_interface_manager.change_simulation_mode(Simulation.Mode.DAILY)
                        self.simulation.set_mode(Simulation.Mode.DAILY)
                    else:    
                        print("Continuing at current mode.")
        else:
            print("Error, simulation mode missing, please re-enter command.")

#---------------------------------------------------------------------------------------------------------------

    def get_approval(self) -> bool:
        """Prompts user if s/he really wants to stop the simulation.

        Args:
            self (LoadBalancer): this current object, on which method is called.

        Returns:
            bool: True if user enters 'Y|y', False otherwise.
        """
        return input("Warning, current simulation will stop. Do you really want to stop? (Y)es (N)o\nCommand: ").lower() == "y"

#---------------------------------------------------------------------------------------------------------------

    def add_instance(self, command_args) -> None:
        """Add instance to instance interface manager and start worker thread.

        Args:
            self (LoadBalancer): this current object, on which method is called.
            command_args (List[str]): partstrings of command string split at spaces.

        Returns:
            None
        """
        hostname = command_args[1]
        username = command_args[2]
        ssh_key_filename = command_args[3]
        passphrase = ""
        if len(command_args) >= 5:
            passphrase = command_args[4]
        match self.heater_instance_interface_manager.add_heater_instance_and_start_thread_if_running(Host(hostname, username, ssh_key_filename, passphrase), self.state):
            case HeaterInstanceInterfaceManager.HeaterInstanceInterfaceManagerErrorCode.HEATER_INSTANCE_ALREADY_EXISTS:
                print("Instance already exists.")
            case HeaterInstanceInterfaceManager.HeaterInstanceInterfaceManagerErrorCode.HEATER_INSTANCE_ADDED:
                print("Instance added.")
            case HeaterInstanceInterfaceManager.HeaterInstanceInterfaceManagerErrorCode.HEATER_INSTANCE_ADDED_AND_STARTED:
                print("Instance added and thread started.")
            case _:
                print("Unkown error code.")

#---------------------------------------------------------------------------------------------------------------

    def remove_instance(self, command_args) -> None:
        """Removes instance indicated by host- and username given in command_args and stops worker thread.

        Args:
            self (LoadBalancer): this current object, on which method is called.
            command_args (List[str]): partstrings of command string split at spaces.
            
        Returns:
            None.
        """
        hostname = command_args[1]
        username = command_args[2]
        match self.heater_instance_interface_manager.remove_heater_instance(Host(hostname, username, None, None)):
            case HeaterInstanceInterfaceManager.HeaterInstanceInterfaceManagerErrorCode.HEATER_INSTANCE_REMOVED:
                print("Instance removed successfully.")
            case _:
                print("Unknown error code at remove_instance received.")

#---------------------------------------------------------------------------------------------------------------

    def show_instances(self) -> None:
        """Shows instances by printing them to console.

        Args:
            self (LoadBalancer): this current object, on which method is called.
            
        Returns:
            None.
        """
        self.heater_instance_interface_manager.show_heater_instances()

#---------------------------------------------------------------------------------------------------------------

    def display_outcome(self) -> None:
        """Displays global outcome stats by printing them to console.

        Args:
            self (LoadBalancer): this current object, on which method is called.
            
        Returns:
            None.
        """
        tries, count = self.global_outcome.get_tries_count()
        pi:float = 0.0
        if tries != 0:
            pi = float(count) / float(tries) * float(4)
        print("Count: " + str(count) + ", Tries: " + str(tries) + ", PI: " + str(pi))

#---------------------------------------------------------------------------------------------------------------

    def print_help(self) -> None:
        """Prints list of commands with description from constants.

        Args:
            self (LoadBalancer): this current object, on which method is called.
            
        Returns:
            None.
        """
        for command_description in constants.COMMANDS_HELP:
            print(command_description)

#---------------------------------------------------------------------------------------------------------------

    def exit_application(self) -> None:
        """Exits application by stopping all threads and sets own state to stopped, 
        then stops while loop and terminates.

        Args:
            self (LoadBalancer): this current object, on which method is called.
            
        Returns:
            None.
        """
        self.heater_instance_interface_manager.stop_threads()
        self.simulation.set_state(Simulation.State.STOPPED)
        self.running = False

#---------------------------------------------------------------------------------------------------------------