import paramiko
import weakref
import math
import json

from heater.host import Host


class HeaterInstance:
    def __init__(self, host: Host, iter=1000):
        """Creates a heater instance

        Args:
            host (Host): host to connect to
            iter (int, optional): Number of iterations to compute in one go. Defaults to 1000.

        Raises:
            IOError: if there was an error reading the file
            paramiko.ssh_exception.PasswordRequiredException: if the private key file is encrypted, and password is None
            paramiko.ssh_exception.SSHException: if the key file is invalid
        """
        self.host = host
        self.iter = iter
        self.__ssh = paramiko.SSHClient()
        self.__ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        if host.passphrase:
            self.__key = paramiko.RSAKey.from_private_key_file(host.ssh_key_filename, host.passphrase)
        else:
            self.__key = paramiko.RSAKey.from_private_key_file(host.ssh_key_filename)
        self.__finalizer = weakref.finalize(self, self.close)

    def run(self, percent: float, milliseconds: int) -> tuple[int, int, float, float]:
        """Runs heater for a time dependend on given parameters.
        Returns number of tries, number of hits, difference between wanted runtime and executed runtime, difference as a factor.
        Differnce is negative when was shorter than expected or positive when longer than expected.
        Difference factor is the percent of the expected time the program ran.

        Args:
            percent (float): cpu usage percentage
            milliseconds (int): time to run

        Returns:
            tuple[int, int, float, float]: tries, count, difference, difference as factor
        """
        runtime = percent * milliseconds
        cmd = f'./monteCarloPi -d {math.floor(runtime)} -i {self.iter}'
        duration = 0.
        tries = 0
        count = 0

        try:
            _, stdout, stderr = self.__ssh.exec_command(cmd)
            if stderr.readlines() == []:
                try:
                    result_json = json.loads(stdout.read().decode().strip("\n"))
                    tries = result_json["tries"]
                    count = result_json["count"]
                    duration = result_json["durationMilli"]
                except json.JSONDecodeError:
                    pass
        except paramiko.SSHException:
            pass
        diff = (duration - runtime) * (1 / percent)
        diffFactor = duration / runtime
        return tries, count, diff, diffFactor

    def connect(self):
        """Connect to heater per ssh.

        Raises:
            paramiko.ssh_exception.BadHostKeyException: if the server's host key could not be verified.
            paramiko.ssh_exception.AuthenticationException: if authentication failed.
            paramiko.ssh_exception.UnableToAuthenticate: if authentication failed (when auth_strategy is non-None; and note that this is a subclass of AuthenticationException).
            socket.error: if a socket error (other than connection-refused or host-unreachable) occurred while connecting.
            paramiko.ssh_exception.NoValidConnectionsError: if all valid connection targets for the requested hostname (eg IPv4 and IPv6) yielded connection-refused or host-unreachable socket errors.
            paramiko.ssh_exception.SSHException: if there was any other error connecting or establishing an SSH session.
        """
        self.__ssh.connect(hostname=self.host.hostname, username=self.host.username, pkey=self.__key)

    @property
    def open(self) -> bool:
        """Returns if ssh connection is open.

        Returns:
            bool: is ssh connection open
        """
        if self.__ssh.get_transport() is not None:
            return self.__ssh.get_transport().is_active()
        return False

    def close(self):
        """Closes ssh connection.
        """
        if self.open:
            self.__ssh.close()

    def cleanup(self):
        """cleans heater instance up.
        Only call once.
        Don't use instance after cleanup.
        Is called automatically by garbage collection.
        """
        self.__finalizer()

    @property
    def cleandup(self) -> bool:
        """Returns if object was alreade cleand up.

        Returns:
            bool: was object cleand up
        """
        return not self.__finalizer.alive
