"""
The formal interface a database must implement to enable
"""
from typing import Optional, List, Tuple
from laborchestrator.structures import ContainerInfo, ProcessStep
from abc import ABC, abstractmethod


class StatusDBInterface(ABC):
    #def __init__(self):
    #    pass

    @classmethod
    def __subclasshook__(cls, subclass):
        # todo: can't i do this with an iteration?
        return (hasattr(subclass, 'get_all_positions')
                and callable(subclass.get_all_positions)
                and hasattr(subclass, 'add_process_to_db')
                and callable(subclass.add_process_to_db)
                and hasattr(subclass, 'get_available_processes')
                and callable(subclass.get_available_processes)
                and hasattr(subclass, 'get_process')
                and callable(subclass.get_process)
                and hasattr(subclass, 'create_experiment')
                and callable(subclass.create_experiment)
                and hasattr(subclass, 'moved_container')
                and callable(subclass.moved_container)
                and hasattr(subclass, 'unlidded_container')
                and callable(subclass.unlidded_container)
                and hasattr(subclass, 'lidded_container')
                and callable(subclass.lidded_container)
                and hasattr(subclass, 'get_cont_info_by_barcode')
                and callable(subclass.get_cont_info_by_barcode)
                and hasattr(subclass, 'add_container')
                and callable(subclass.add_container)
                and hasattr(subclass, 'remove_container')
                and callable(subclass.remove_container)
                and hasattr(subclass, 'set_barcode_at_position')
                and callable(subclass.set_barcode_at_position)
                and hasattr(subclass, 'set_barcode')
                and callable(subclass.set_barcode)
                and hasattr(subclass, 'update_lid_position')
                and callable(subclass.update_lid_position)
                and hasattr(subclass, 'get_estimated_duration')
                and callable(subclass.get_estimated_duration)
                and hasattr(subclass, 'set_barcode_at_position')
                and callable(subclass.safe_step_to_db)
                or NotImplemented)

    @abstractmethod
    def get_all_positions(self, device: str) -> List[int]:
        """
        Provides the indices of all positions in the specified device (independent of emptiness).
        :param device:
        :return:
        """

    def add_process_to_db(self, name: str, src: str) -> str:
        """

        :param name:
        :param src:
        :return: the uuid of the created process
        """

    def get_available_processes(self) -> List[Tuple[str, str]]:
        """

        :return: A List of tuples [process name, process uuid] for all processes in the db
        """

    def get_process(self, process_uuid: str) -> str:
        """

        :param process_uuid: The unique identifier of the process in the db
        :return: the source code defining the process
        """

    def create_experiment(self, process_id: str) -> str:
        """

        :param process_id: the unique id of the process running in this experiment
        :return: the uuid of the created experiment
        """

    @abstractmethod
    def get_container_at_position(self, device: str, pos: int) -> Optional[ContainerInfo]:
        """
        Checks if there is a container at a specified position.
        :param device:
        :param pos:
        :return: None if there is no container at the specified position. All available information otherwise.
        """

    @abstractmethod
    def position_empty(self, device: str, pos: int) -> bool:
        """
        Checks whether the specified position is empty
        :param device: Name of the device
        :param pos: Index of the slot
        :return:
        """

    @abstractmethod
    def moved_container(self, source_device: str, source_pos: int, target_device: str, target_pos: int,
                        barcode: Optional[str] = None):
        """
        Saves a movement of a container (specified by its prior position) to the database
        :param source_device:
        :param source_pos:
        :param target_device:
        :param target_pos:
        :param barcode:
        :return:
        """

    @abstractmethod
    def unlidded_container(self, cont_info: ContainerInfo, lid_device: str, lid_pos: int):
        """
        Saves the removal of the lid and its new position to the database
        :param cont:
        :param lid_device:
        :param lid_pos:
        :return:
        """
        pass

    @abstractmethod
    def lidded_container(self, cont_info: ContainerInfo, lid_device: Optional[str], lid_pos: Optional[int]):
        """
        Saved to the database, that a containers lid was put back on. The position of the used lid can be specified,
        so the database will/can check that it was the correct one.
        :param cont:
        :param lid_device:
        :param lid_pos:
        :return:
        """
        pass

    @abstractmethod
    def get_cont_info_by_barcode(self, barcode: str) -> Optional[ContainerInfo]:
        """
        Retrieves all available information for the container with the given barcode
        :param barcode:
        :return:
        """

    @abstractmethod
    def add_container(self, cont: ContainerInfo):
        """
        Adds a container to the database with its starting position as the current position.
        :param cont: The container to add
        :return: nothin
        """

    @abstractmethod
    def remove_container(self, cont: ContainerInfo):
        """
        Marks the given container as removed from the platform. The information is still kept in the database
        :param cont: The container to mark removed
        :return: nothing
        """

    @abstractmethod
    def set_barcode(self, cont: ContainerInfo):
        """
        Sets the barcode of an existing container. Assumes the barcode is already saved in the ContainerInfo.
        :param cont:
        :return:
        """

    @abstractmethod
    def update_lid_position(self, cont: ContainerInfo):
        """
        Sets the barcode of an existing container. Assumes the barcode is already saved in the ContainerInfo.
        :param cont:
        :return:
        """

    @abstractmethod
    def get_estimated_duration(self, step: ProcessStep, confidence=.95) -> Optional[float]:
        """
        Checks the database for similar jobs to estimate the duration of a job.
        :param step: the job of which the duration shall be estimated
        :param confidence: chance, that the actual duration is less or equal the estimated duration.
        :return: duration in seconds or None if no information was found in the database
        """

    @abstractmethod
    def get_estimated_durations(self, steps: List[ProcessStep], confidence=.95) -> List[Optional[float]]:
        """
        Same as get_estimated_duration(), but with many steps at once to save time
        :return: The list will match the length and order of the given steps
        """

    @abstractmethod
    def safe_step_to_db(self, step: ProcessStep, container_info: ContainerInfo, experiment_uuid: str):
        """
        Saves a finished job to the database. It automatically recognizes move jobs.
        :param experiment_uuid: The uuid of the experiment in the database, this step belongs to
        :param container_info: information about the processed container
        :param step: the structures.ProcessStep to save
        :return: nothing
        """

    @abstractmethod
    def write_server_certificate(self, device_name, cert: str) -> None:
        """

        :param device_name:
        :param cert:
        :return:
        """

    @abstractmethod
    def get_server_certificate(self, device_name) -> str:
        """

        :param device_name:
        :return:
        """
