#######################################################################################
# Copyright ETSI Contributors and Others.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import asyncio
from datetime import timedelta
import traceback
from typing import Tuple

from osm_common.temporal.states import VnfInstantiationState, VnfState
from osm_common.temporal.activities.vnf import (
    ChangeVnfInstantiationState,
    ChangeVnfState,
    GetTaskQueue,
    GetVimCloud,
    SetVnfModel,
    SendNotificationForVnf,
    GetVnfDescriptor,
    GetVnfRecord,
)

from osm_common.temporal.workflows.vnf import (
    VnfInstantiateWorkflow,
    VnfPrepareWorkflow,
)
from osm_common.temporal.workflows.vdu import VduInstantiateWorkflow
from osm_common.temporal_task_queues.task_queues_mappings import LCM_TASK_QUEUE

from osm_lcm.temporal.juju_paas_activities import CharmInfoUtils
from osm_lcm.temporal.vnf_activities import VnfSpecifications
from temporalio import workflow
from temporalio.common import RetryPolicy
from temporalio.converter import value_to_type
from temporalio.exceptions import ActivityError, ChildWorkflowError

_SANDBOXED = False
retry_policy = RetryPolicy(maximum_attempts=3)
default_schedule_to_close_timeout = timedelta(minutes=10)


@workflow.defn(name=VnfInstantiateWorkflow.__name__, sandboxed=False)
class VnfInstantiateWorkflowImpl(VnfInstantiateWorkflow):
    @workflow.run
    async def run(self, workflow_input: VnfInstantiateWorkflow.Input) -> None:
        self.logger.info(f"Deploying VNF {workflow_input.vnfr_uuid}")
        try:
            await self.update_vnf_instantiation_state(
                ChangeVnfInstantiationState.Input(
                    vnfr_uuid=workflow_input.vnfr_uuid,
                    state=VnfInstantiationState.NOT_INSTANTIATED,
                ),
            )
            await self.send_notification_for_vnf(
                ChangeVnfInstantiationState.Input(
                    vnfr_uuid=workflow_input.vnfr_uuid,
                    state=VnfInstantiationState.NOT_INSTANTIATED,
                ),
            )
            vnf_task_queue = value_to_type(
                GetTaskQueue.Output,
                await workflow.execute_activity(
                    activity=GetTaskQueue.__name__,
                    arg=GetTaskQueue.Input(workflow_input.vnfr_uuid),
                    activity_id=f"{GetTaskQueue.__name__}-{workflow_input.vnfr_uuid}",
                    task_queue=LCM_TASK_QUEUE,
                    schedule_to_close_timeout=default_schedule_to_close_timeout,
                    retry_policy=retry_policy,
                ),
            )

            self.logger.debug(f"Dependent task queue is {vnf_task_queue.task_queue}")

            await workflow.execute_child_workflow(
                workflow=VnfPrepareWorkflow.__name__,
                arg=workflow_input,
                task_queue=vnf_task_queue.task_queue,
                id=f"{VnfPrepareWorkflow.__name__}-{workflow_input.vnfr_uuid}",
            )

            get_vnf_record = value_to_type(
                GetVnfRecord.Output,
                await workflow.execute_activity(
                    activity=GetVnfRecord.__name__,
                    arg=GetVnfRecord.Input(workflow_input.vnfr_uuid),
                    activity_id=f"{GetVnfRecord.__name__}-{workflow_input.vnfr_uuid}",
                    task_queue=vnf_task_queue.task_queue,
                    schedule_to_close_timeout=default_schedule_to_close_timeout,
                    retry_policy=retry_policy,
                ),
            )
            activities_results = await asyncio.gather(
                workflow.execute_activity(
                    activity=GetVnfDescriptor.__name__,
                    arg=GetVnfDescriptor.Input(get_vnf_record.vnfr["vnfd-id"]),
                    activity_id=f"{GetVnfDescriptor.__name__}-{get_vnf_record.vnfr['vnfd-id']}",
                    task_queue=vnf_task_queue.task_queue,
                    schedule_to_close_timeout=default_schedule_to_close_timeout,
                    retry_policy=retry_policy,
                ),
                workflow.execute_activity(
                    activity=GetVimCloud.__name__,
                    arg=GetVimCloud.Input(workflow_input.vnfr_uuid),
                    activity_id=f"{GetVimCloud.__name__}-{workflow_input.vnfr_uuid}",
                    task_queue=vnf_task_queue.task_queue,
                    schedule_to_close_timeout=default_schedule_to_close_timeout,
                    retry_policy=retry_policy,
                ),
            )
            get_vnf_descriptor, get_cloud = value_to_type(
                GetVnfDescriptor.Output, activities_results[0]
            ), value_to_type(GetVimCloud.Output, activities_results[1])

            await self.instantiate_vdus(
                vnfr=get_vnf_record.vnfr,
                vnfd=get_vnf_descriptor.vnfd,
                task_queue=vnf_task_queue.task_queue,
                cloud=get_cloud.cloud,
                vnf_instantiation_config=workflow_input.instantiation_config,
            )
            await self.update_vnf_instantiation_state(
                ChangeVnfInstantiationState.Input(
                    vnfr_uuid=workflow_input.vnfr_uuid,
                    state=VnfInstantiationState.INSTANTIATED,
                ),
            )
            await self.update_vnf_state(
                ChangeVnfState.Input(
                    vnfr_uuid=workflow_input.vnfr_uuid, state=VnfState.STARTED
                ),
            )
            await self.send_notification_for_vnf(
                ChangeVnfInstantiationState.Input(
                    vnfr_uuid=workflow_input.vnfr_uuid,
                    state=VnfInstantiationState.INSTANTIATED,
                ),
            )

        except ActivityError as e:
            err_details = str(e.cause.with_traceback(e.__traceback__))
            await self.update_vnf_instantiation_state(
                ChangeVnfInstantiationState.Input(
                    vnfr_uuid=workflow_input.vnfr_uuid,
                    state=VnfInstantiationState.INSTANTIATED,
                ),
            )
            await self.update_vnf_state(
                ChangeVnfState.Input(
                    vnfr_uuid=workflow_input.vnfr_uuid, state=VnfState.STOPPED
                ),
            )
            await self.send_notification_for_vnf(
                ChangeVnfInstantiationState.Input(
                    vnfr_uuid=workflow_input.vnfr_uuid,
                    state=VnfInstantiationState.INSTANTIATED,
                ),
            )
            self.logger.error(
                f"{VnfInstantiateWorkflow.__name__} failed with {err_details}"
            )
            raise e

        except ChildWorkflowError as e:
            err_details = str(e.cause.with_traceback(e.cause.__traceback__))
            await self.update_vnf_instantiation_state(
                ChangeVnfInstantiationState.Input(
                    vnfr_uuid=workflow_input.vnfr_uuid,
                    state=VnfInstantiationState.INSTANTIATED,
                ),
            )
            await self.update_vnf_state(
                ChangeVnfState.Input(
                    vnfr_uuid=workflow_input.vnfr_uuid, state=VnfState.STOPPED
                ),
            )
            await self.send_notification_for_vnf(
                ChangeVnfInstantiationState.Input(
                    vnfr_uuid=workflow_input.vnfr_uuid,
                    state=VnfInstantiationState.INSTANTIATED,
                ),
            )
            self.logger.error(
                f"{VnfInstantiateWorkflow.__name__} failed with {err_details}"
            )
            raise e

        except Exception as e:
            err_details = str(traceback.format_exc())
            await self.update_vnf_instantiation_state(
                ChangeVnfInstantiationState.Input(
                    vnfr_uuid=workflow_input.vnfr_uuid,
                    state=VnfInstantiationState.INSTANTIATED,
                ),
            )
            await self.update_vnf_state(
                ChangeVnfState.Input(
                    vnfr_uuid=workflow_input.vnfr_uuid, state=VnfState.STOPPED
                ),
            )
            await self.send_notification_for_vnf(
                ChangeVnfInstantiationState.Input(
                    vnfr_uuid=workflow_input.vnfr_uuid,
                    state=VnfInstantiationState.INSTANTIATED,
                ),
            )
            self.logger.error(
                f"{VnfInstantiateWorkflow.__name__} failed with {err_details}"
            )
            raise e

    @staticmethod
    async def update_vnf_state(vnf_state: ChangeVnfState.Input):
        await workflow.execute_activity(
            activity=ChangeVnfState.__name__,
            arg=vnf_state,
            activity_id=f"{ChangeVnfState.__name__}-{vnf_state.vnfr_uuid}",
            task_queue=LCM_TASK_QUEUE,
            schedule_to_close_timeout=default_schedule_to_close_timeout,
            retry_policy=retry_policy,
        )

    @staticmethod
    async def update_vnf_instantiation_state(
        vnf_instantiation_state: ChangeVnfInstantiationState.Input,
    ):
        await workflow.execute_activity(
            activity=ChangeVnfInstantiationState.__name__,
            arg=vnf_instantiation_state,
            activity_id=f"{ChangeVnfInstantiationState.__name__}-{vnf_instantiation_state.vnfr_uuid}",
            task_queue=LCM_TASK_QUEUE,
            schedule_to_close_timeout=default_schedule_to_close_timeout,
            retry_policy=retry_policy,
        )

    @staticmethod
    async def send_notification_for_vnf(
        vnf_instantiation_state: ChangeVnfInstantiationState.Input,
    ):
        await workflow.execute_activity(
            activity=SendNotificationForVnf.__name__,
            arg=vnf_instantiation_state,
            activity_id=f"{SendNotificationForVnf.__name__}-{vnf_instantiation_state.vnfr_uuid}",
            task_queue=LCM_TASK_QUEUE,
            schedule_to_close_timeout=default_schedule_to_close_timeout,
            retry_policy=retry_policy,
        )

    @staticmethod
    async def instantiate_vdus(
        vnfr: dict,
        vnfd: dict,
        task_queue: str,
        cloud: str,
        vnf_instantiation_config: dict,
    ):
        for vdu in vnfd.get("vdu"):
            (
                vdu_instantiate_input,
                vdu_instantiate_workflow_id,
            ) = VnfInstantiateWorkflowImpl._get_vdu_instantiate_input(
                vnfr=vnfr,
                vnfd=vnfd,
                vdu=vdu,
                cloud=cloud,
                vnf_instantiation_config=vnf_instantiation_config,
            )
            await workflow.execute_child_workflow(
                workflow=VduInstantiateWorkflow.__name__,
                arg=vdu_instantiate_input,
                task_queue=task_queue,
                id=vdu_instantiate_workflow_id,
            )

    @staticmethod
    def _get_vdu_instantiate_input(
        vnfr, vnfd, vdu, cloud, vnf_instantiation_config
    ) -> Tuple[VduInstantiateWorkflow.Input, str]:
        """Calculates the VDU instantiate input data without reaching Database."""
        model_name = vnfr.get("namespace")
        vim_id = vnfr.get("vim-account-id")
        sw_image_descs = vnfd.get("sw-image-desc")
        vdu_info = CharmInfoUtils.get_charm_info(vdu, sw_image_descs)
        vdu_instantiation_config = VnfSpecifications.get_vdu_instantiation_params(
            vdu["id"], vnf_instantiation_config
        )
        compute_constraints = VnfSpecifications.get_compute_constraints(vdu, vnfd)
        config = VnfSpecifications.get_application_config(vdu, vdu_instantiation_config)
        vdu_instantiate_input = VduInstantiateWorkflow.Input(
            vim_uuid=vim_id,
            model_name=model_name,
            charm_info=vdu_info,
            constraints=compute_constraints,
            cloud=cloud,
            config=config,
        )
        vdu_instantiate_workflow_id = (
            vdu_instantiate_input.model_name
            + "-"
            + vdu_instantiate_input.charm_info.app_name
        )
        return vdu_instantiate_input, vdu_instantiate_workflow_id


@workflow.defn(name=VnfPrepareWorkflow.__name__, sandboxed=False)
class VnfPrepareWorkflowImpl(VnfPrepareWorkflow):
    @workflow.run
    async def run(self, workflow_input: VnfPrepareWorkflow.Input) -> None:
        try:
            await workflow.execute_activity(
                activity=SetVnfModel.__name__,
                arg=SetVnfModel.Input(
                    workflow_input.vnfr_uuid, workflow_input.model_name
                ),
                activity_id=f"{SetVnfModel.__name__}-{workflow_input.vnfr_uuid}",
                task_queue=LCM_TASK_QUEUE,
                schedule_to_close_timeout=default_schedule_to_close_timeout,
                retry_policy=retry_policy,
            )
        except ActivityError as e:
            err_details = str(e.cause.with_traceback(e.__traceback__))
            self.logger.error(
                f"{VnfPrepareWorkflow.__name__} failed with {err_details}"
            )
            raise e

        except Exception as e:
            err_details = str(traceback.format_exc())
            self.logger.error(
                f"{VnfPrepareWorkflow.__name__} failed with {err_details}"
            )
            raise e
