# Default Python Libraries
import base64
from typing import List, Type, TypeVar

# AristaFlow REST Libraries
import af_execution_manager
from af_execution_manager.api.activity_execution_control_api import ActivityExecutionControlApi
from af_org_model_manager.api.global_security_manager_api import GlobalSecurityManagerApi
from af_org_model_manager.models.auth_data_app_name import AuthDataAppName
from af_org_model_manager.models.auth_data_arbitrary import AuthDataArbitrary
from af_org_model_manager.models.auth_data_org_pos import AuthDataOrgPos
from af_org_model_manager.models.auth_data_password import AuthDataPassword
from af_org_model_manager.models.auth_data_user_name import AuthDataUserName
from af_org_model_manager.models.authentication_data import AuthenticationData
from af_org_model_manager.models.client_session_details import ClientSessionDetails
from af_org_model_manager.models.qualified_agent import QualifiedAgent
from af_remote_html_runtime_manager.api.runtime_manager_api import RuntimeManagerApi
from af_remote_html_runtime_manager.api.synchronous_activity_starting_api import (
    SynchronousActivityStartingApi,
)
from af_remote_html_runtime_manager.models.activity_rest_callback_data import (
    ActivityRestCallbackData,
)
from af_remote_html_runtime_manager.models.ebp_instance_reference import EbpInstanceReference
from af_remote_html_runtime_manager.models.gui_context import GuiContext
from af_worklist_manager.models.af_activity_reference import AfActivityReference
from af_worklist_manager.models.worklist_item import WorklistItem
from aristaflow.absence_service import AbsenceService
from aristaflow.delegation_service import DelegationService
from aristaflow.execution_history_service import ExecutionHistoryService
from aristaflow.html_gui_context import HtmlGuiContext
from aristaflow.org_model_service import OrgModelService
from aristaflow.process_service import ProcessService
from aristaflow.service_provider import ServiceProvider

from .configuration import Configuration
from .worklist_service import WorklistService


T = TypeVar("T")
D = TypeVar("D")


class AristaFlowClientService(object):
    """Client Services for accessing the AristaFlow BPM platform"""

    # the user session id this client service belongs to
    __user_session: str = None
    # authentication
    __csd: ClientSessionDetails = None
    # the AristaFlow configuration
    __af_conf: Configuration = None
    __service_provider: ServiceProvider = None
    __worklist_service: WorklistService = None
    __process_service: ProcessService = None
    __delegation_service: DelegationService = None
    __absence_service: AbsenceService = None
    __execution_history_service: ExecutionHistoryService = None
    __org_model_service: OrgModelService = None

    def __init__(
        self, configuration: Configuration, user_session: str, service_provider: ServiceProvider
    ):
        self.__af_conf = configuration
        self.__user_session = user_session
        self.__service_provider = service_provider

    def get_service(self, service_type: Type[T]) -> T:
        """
        Returns a service instance for the given service type, e.g.
        get_service(InstanceControlApi)
        @param service_type The class of the requested service.
        """
        return self.__service_provider.get_service(service_type)

    @property
    def client_session_details(self) -> ClientSessionDetails:
        """Returns the client session details of this service
        :return: The client session details of this service
        """
        return self.__csd

    @property
    def is_authenticated(self) -> bool:
        """Returns true, if this client service is already authenticated"""
        return self.__csd is not None

    def authenticate(self, username: str, password: str = None, org_pos_id: int = None):
        if self.__csd is not None:
            raise Exception("Already authenticated")

        auth_data: List[AuthenticationData] = [
            AuthDataUserName(username, sub_class="AuthDataUserName")
        ]
        if org_pos_id is not None:
            auth_data.append(AuthDataOrgPos(org_pos_id, sub_class="AuthDataOrgPos"))
        psk = self.__af_conf.pre_shared_key
        method: str
        # if a password was provided, use it
        if password:
            auth_data.append(AuthDataPassword(password=password, sub_class="AuthDataPassword"))
            method = "UTF-8_PASSWORD"
        # if PSK is configured, use that
        elif psk:
            # get the utf-8 bytes, encode them using base 64 and decode the resulting bytes using ASCII
            psk_encoded = base64.b64encode(bytes(psk, "UTF-8")).decode("ascii")
            auth_data.append(AuthDataArbitrary(data=psk_encoded, sub_class="AuthDataArbitrary"))
            method = "SHARED_UTF-8_KEY"
        else:
            raise Exception("No authentication method left")

        gsm: GlobalSecurityManagerApi = self.get_service(GlobalSecurityManagerApi)

        # use a provided application name
        if self.__af_conf.application_name:
            if org_pos_id is None:
                # if an application name is provided, an org position ID must be used as well
                # get the org positions
                agents: List[QualifiedAgent] = gsm.pre_authenticate_method(method, body=auth_data)
                agent: QualifiedAgent
                # pick the single org position
                if len(agents) == 1:
                    agent = agents[0]
                # none: can't login
                elif len(agents) == 0:
                    raise Exception(
                        f"User does not have an org position {username} (supplied org position id: {org_pos_id})"
                    )
                else:
                    # use the first org position, except there is a agent_name/username match
                    agent = agents[0]
                    for a in agents:
                        if a.agent_name == username:
                            agent = a
                            break
                # set the org position for the actual authentication
                auth_data.append(AuthDataOrgPos(agent.org_pos_id, sub_class="AuthDataOrgPos"))
            # use the application name
            auth_data.append(
                AuthDataAppName(
                    app_name=self.__af_conf.application_name, sub_class="AuthDataAppName"
                )
            )

        csds: List[ClientSessionDetails] = gsm.authenticate_all_method(
            method, self.__af_conf.caller_uri, body=auth_data
        )

        csd: ClientSessionDetails
        if len(csds) == 1:
            csd = csds[0]
        elif len(csds) == 0:
            raise Exception(
                f"User does not have an org position {username} (supplied org position id: {org_pos_id})"
            )
        else:
            # pick the first as default
            csd = csds[0]
            # pick the one where username and org position name are the same
            for entry in csds:
                if entry.agent.agent.org_pos_name == entry.agent.agent.agent_name:
                    csd = entry
                    break

        self.__service_provider.authenticated(csd)
        self.__csd = csd

    def is_html_activity(self, item: WorklistItem) -> bool:
        return (
            item.act_ref.gui_context_id == "HTMLContext"
            or item.act_ref.executable_component_name == "de.aristaflow.form.Form"
            or item.act_ref.executable_component_name == "de.aristaflow.form.GeneratedForm"
        )

    @property
    def worklist_service(self):
        if self.__worklist_service is None:
            self.__worklist_service = WorklistService(self.__service_provider, self.__af_conf)
        return self.__worklist_service

    @property
    def process_service(self):
        if self.__process_service is None:
            self.__process_service = ProcessService(self.__service_provider, self.__af_conf)
        return self.__process_service

    @property
    def delegation_service(self):
        if self.__delegation_service is None:
            self.__delegation_service = DelegationService(self.__service_provider)
        return self.__delegation_service

    @property
    def absence_service(self):
        if self.__absence_service is None:
            self.__absence_service = AbsenceService(self.__service_provider)
        return self.__absence_service

    @property
    def execution_history_service(self):
        if self.__execution_history_service is None:
            self.__execution_history_service = ExecutionHistoryService(self.__service_provider)
        return self.__execution_history_service

    @property
    def org_model_service(self):
        if self.__org_model_service is None:
            self.__org_model_service = OrgModelService(self.__service_provider)
        return self.__org_model_service

    def start_html_activity(self, item: WorklistItem, callback_url: str):
        """
        Starts the given HTML GUI worklist item using the Remote HTML Runtime Manager
        """
        if item is None:
            raise Exception("No worklist item provided")
        # accept user form and HTMLContext based activities
        if not (self.is_html_activity(item)):
            raise Exception(f"Not an HTML activity: {item.act_ref.gui_context_id}")
        if item.state == "STARTED":
            raise Exception("Item is already started")
        sas = self.get_html_activity_starting()
        gc: GuiContext
        ar: AfActivityReference = item.act_ref
        # print('Starting activity...')
        ebp_ir: EbpInstanceReference = EbpInstanceReference(
            ar.type,
            ar.instance_id,
            ar.instance_log_id,
            ar.base_template_id,
            ar.node_id,
            ar.node_iteration,
            ar.execution_manager_uris,
            ar.runtime_manager_uris,
        )
        # "AVAILABLE", "ASSIGNED", "STARTED", "SUSPENDED", "ENQUIRED"
        if item.state == "AVAILABLE" or item.state == "ASSIGNED":
            if callback_url is not None:
                cb_data = ActivityRestCallbackData(
                    sub_class="ActivityRestCallbackData",
                    notification_callback=callback_url,
                    activity=ebp_ir,
                )
                gc = sas.start_activity_callback(body=cb_data)
            else:
                gc = sas.start_activity(body=ebp_ir)
        else:
            if callback_url is not None:
                cb_data = ActivityRestCallbackData(
                    sub_class="ActivityRestCallbackData",
                    notification_callback=callback_url,
                    activity=ebp_ir,
                )
                gc = sas.resume_activity_callback(body=cb_data)
            else:
                gc = sas.resume_activity(body=ebp_ir)
        return HtmlGuiContext(gc)

    def get_html_activity_starting(self) -> SynchronousActivityStartingApi:
        """
        Returns the Remote HTML Runtime Manager Synchronous Activity Starting, ensuring logon to the Runtime Manager
        """
        sas: SynchronousActivityStartingApi = self.get_service(SynchronousActivityStartingApi)
        rm: RuntimeManagerApi = self.get_service(RuntimeManagerApi)
        # always logon again, since the server might have been restarted in the meantime
        rm.logon(body=self.__csd)
        return sas

    def reset_activity(self, item: WorklistItem):
        """Resets the given worklist item."""
        if item.state != "STARTED":
            # nothing to do
            return
        # TODO check for a "local" activity for a soft reset
        ar: AfActivityReference = item.act_ref
        ebp_ir = af_execution_manager.EbpInstanceReference(
            ar.type,
            ar.instance_id,
            ar.instance_log_id,
            ar.base_template_id,
            ar.node_id,
            ar.node_iteration,
            ar.execution_manager_uris,
            ar.runtime_manager_uris,
        )
        aec: ActivityExecutionControlApi = self.get_service(ActivityExecutionControlApi)
        aec.reset_to_prev_savepoint(body=ebp_ir, force=True)

    def deserialize(self, data, klass: Type[D]) -> D:
        """Deserialize data using the given class of the generated OpenAPI models."""
        return self.__service_provider.deserialize(data, klass)

    def serialize(self, obj) -> str:
        """Serialize REST model object"""
        return self.__service_provider.serialize(obj)

    @property
    def autostart_timeout_seconds(self) -> int:
        """Wait time in seconds for auto start signals"""
        return self.__af_conf.autostart_timeout_seconds
