import logging

from cloud_controller import API_ENDPOINT_IP, API_ENDPOINT_PORT
from cloud_controller.knowledge.knowledge import Knowledge
from cloud_controller.knowledge.model import Component
from cloud_controller.knowledge.instance import CompinPhase, ManagedCompin
from cloud_controller.middleware.middleware_pb2 import InstanceConfig, ProbeType, DependencyAddress, MongoParameters
from cloud_controller.middleware.middleware_pb2_grpc import MiddlewareAgentStub
from cloud_controller.task_executor.execution_context import KubernetesExecutionContext, ExecutionContext
from cloud_controller.tasks.preconditions import compin_exists, check_phase
from cloud_controller.tasks.task import Task


class FinalizeInstanceTask(Task):
    """
    Sends a FinalizeExecution command to an instance.
    """

    def __init__(self, component: Component, instance_id: str):
        self._component: Component = component
        self._instance_id: str = instance_id
        super(FinalizeInstanceTask, self).__init__(
            task_id=self.generate_id()
        )
        self.add_precondition(check_phase, (self._component.application.name, self._component.name, self._instance_id,
                                            CompinPhase.INIT))

    def execute(self, context: ExecutionContext) -> bool:
        stub = context.get_instance_agent(self._component.application.name, self._component.name, self._instance_id)
        stub.FinalizeExecution(DependencyAddress())
        logging.info(f"Finalize call was sent to the instance {self._instance_id} of {self._component.name}")
        return True

    def update_model(self, knowledge: Knowledge) -> None:
        compin = knowledge.actual_state.get_compin(
            self._component.application.name,
            self._component.name,
            self._component.id
        )
        if compin is not None and compin.phase < CompinPhase.FINALIZING:
            compin.phase = CompinPhase.FINALIZING

    def generate_id(self) -> str:
        return f"{self.__class__.__name__}_{self._component.application.name}_{self._component.name}_" \
               f"{self._instance_id}"


class SetMongoParametersTask(Task):
    """
    Tries to set mongo parameters for a Managed compin, if a compin have already started. Mongo parameters include
    IP address of a mongos instance, database and collection to use, and a personal shard key.
    """

    def __init__(self, component: Component, instance_id: str, key: int, mongos_ip: str):
        self._component: Component = component
        self._instance_id: str = instance_id
        self._key = key
        self._mongos_ip = mongos_ip
        super(SetMongoParametersTask, self).__init__(
            task_id=self.generate_id()
        )
        self.add_precondition(check_phase, (self._component.application.name, self._component.name, self._instance_id,
                                            CompinPhase.INIT))

    def execute(self, context: ExecutionContext) -> bool:
        parameters = MongoParameters(
            db=self._component.application.name,
            collection=self._component.name,
            shardKey=self._key,
            mongosIp=self._mongos_ip
        )
        stub = context.get_instance_agent(self._component.application.name, self._component.name, self._instance_id)
        stub.SetMongoParameters(parameters)
        logging.info(f"New mongo parameters were sent to {self._component.name}:"
                     f"{self._component.application.name}")
        return True

    def update_model(self, knowledge: Knowledge) -> None:
        compin = knowledge.actual_state.get_compin(
            self._component.application.name,
            self._component.name,
            self._component.id
        )
        if compin is not None:
            compin.mongo_init_completed = True

    def generate_id(self) -> str:
        return f"{self.__class__.__name__}_{self._component.application.name}_{self._component.name}_" \
               f"{self._instance_id}_{self._key}_{self._mongos_ip}"


class SetMiddlewareAddressTask(Task):
    """
    Sends the IP address of a dependency to the MiddlewareAgent of the compin. The task will be performed only if
    the dependent compin had already reached the INIT state, and the providing compin had reached READY state.
    :preconditions return: True if successful, False if one of the compins is not in a required state.
    """

    def __init__(self, providing_component: Component, providing_instance_id: str,
                 dependent_component: Component, dependent_instance_id: str):
        self._app_name: str = providing_component.application.name
        self._providing_component: Component = providing_component
        self._providing_instance_id: str = providing_instance_id
        self._dependent_component: Component = dependent_component
        self._dependent_instance_id: str = dependent_instance_id
        super(SetMiddlewareAddressTask, self).__init__(
            task_id=self.generate_id()
        )
        self.add_precondition(check_phase, (self._app_name, self._providing_component.name,
                                            self._providing_instance_id, CompinPhase.READY))
        self.add_precondition(check_phase, (self._app_name, self._dependent_component.name,
                                            self._dependent_instance_id, CompinPhase.INIT))

    def execute(self, context: ExecutionContext) -> bool:
        ip = context.get_instance_ip(
            self._providing_component.application.name,
            self._providing_component.name,
            self._providing_instance_id
        )
        address = DependencyAddress(name=self._providing_component.name, ip=ip)
        stub = context.get_instance_agent(self._app_name, self._dependent_component.name, self._dependent_instance_id)
        stub.SetDependencyAddress(address)
        logging.info(f"Dependency address for instance at {ip} set to {ip}.")
        return True

    def update_model(self, knowledge: Knowledge) -> None:
        # Finally, change the record in the actual state model
        knowledge.actual_state.set_dependency(
            self._app_name,
            self._dependent_component.name,
            self._dependent_instance_id,
            self._providing_component.name,
            self._providing_instance_id
        )

    def generate_id(self) -> str:
        return f"{self.__class__.__name__}_{self._app_name}_{self._dependent_component.name}_" \
               f"{self._dependent_instance_id}_{self._providing_component.name}_{self._providing_instance_id}"


class InitializeInstanceTask(Task):

    def __init__(self, component: Component, instance_id: str, run_count: int, access_token: str, production: bool):
        self._component: Component = component
        self._app_name: str = self._component.application.name
        self._component_name: str = self._component.name
        self._instance_id: str = instance_id
        self._run_count: int = run_count
        self._access_token: str = access_token
        self._production: bool = production
        super(InitializeInstanceTask, self).__init__(
            task_id=self.generate_id()
        )
        self.add_precondition(compin_exists, (self._app_name, self._component_name, self._instance_id))
        self.add_precondition(check_phase, (self._app_name, self._component_name, self._instance_id, CompinPhase.INIT))

    def execute(self, context: ExecutionContext) -> bool:
        init_data = InstanceConfig(
            instance_id=self._instance_id,
            api_endpoint_ip=API_ENDPOINT_IP,
            api_endpoint_port=API_ENDPOINT_PORT,
            access_token=self._access_token,
            production=self._production
        )
        for probe in self._component.probes:
            probe_pb = init_data.probes.add()
            probe_pb.name = probe.name
            probe_pb.signal_set = probe.signal_set
            probe_pb.execution_time_signal = probe.execution_time_signal
            probe_pb.run_count_signal = probe.run_count_signal
            probe_pb.run_count = self._run_count
            if probe.code != "":
                probe_pb.type = ProbeType.Value('CODE')
                probe_pb.code = probe.code
                probe_pb.config = probe.config
                probe_pb.args = probe.args
            else:
                probe_pb.type = ProbeType.Value('PROCEDURE')
        stub: MiddlewareAgentStub = context.get_instance_agent(self._app_name, self._component_name, self._instance_id)
        stub.InitializeInstance(init_data)
        logging.info(f"Instance {self._instance_id} was successfully initialized")
        return True

    def update_model(self, knowledge: Knowledge) -> None:
        instance = knowledge.actual_state.get_compin(self._app_name, self._component_name, self._instance_id)
        if instance is not None:
            assert isinstance(instance, ManagedCompin)
            instance.init_completed = True
            KubernetesExecutionContext.update_instance_phase(instance)

    def generate_id(self) -> str:
        return f"{self.__class__.__name__}_{self._app_name}_{self._component.name}_{self._instance_id}"
