#######################################################################################
# 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 logging
import traceback
from abc import ABC, abstractmethod
from datetime import timedelta

from osm_common.dataclasses.temporal_dataclasses import (
    LcmOperationState,
    NsLcmOperationInput,
    UpdateLcmOperationStateInput,
)
from osm_common.temporal_constants import (
    ACTIVITY_NSLCM_NO_OP,
    ACTIVITY_UPDATE_LCM_OPERATION_STATE,
    WORKFLOW_NSLCM_NO_OP,
)
from temporalio import workflow
from temporalio.common import RetryPolicy
from temporalio.exceptions import ActivityError


class LcmOperationWorkflow(ABC):
    """
    An abstract base class representing a Lifecycle Management Operation.  Any
    workflows that need LCM OP control should extend this class and implement
    the workflow method.

    Methods
    -------

    @abstractmethod workflow(input: NsLcmOperationInput)
        Method for subclasses to implement the actual workflow that is being
        wrapped in this operation.

    @workflow.run wrap_nslcmop(input: NsLcmOperationInput)
        Must be implemented in every subclass exactly as follows:
        @workflow.run
        async def wrap_nslcmop(self, input: NsLcmOperationInput) -> None:
            await super().wrap_nslcmop(input=input)
    """

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

    def __init__(self):
        self.logger = logging.getLogger(f"lcm.wfl.{self.__class__.__name__}")
        self.op_id = None
        self.stage = ""

    @abstractmethod
    async def workflow(self, input: NsLcmOperationInput):
        pass

    async def wrap_nslcmop(self, input: NsLcmOperationInput):
        self.op_id = input.nslcmop["_id"]
        await self.update_operation_state(LcmOperationState.PROCESSING)
        try:
            await self.workflow(input=input)
        except ActivityError as e:
            # TODO: Deteremine the best content for Activity Errors OSM-994
            self.logger.exception(e)
            await self.update_operation_state(
                LcmOperationState.FAILED,
                error_message=str(e.cause.message),
                detailed_status=str(e.cause.with_traceback(e.__traceback__)),
            )
            raise e
        except Exception as e:
            self.logger.exception(e)
            self.update_operation_state(
                LcmOperationState.FAILED,
                error_message=str(e),
                detailed_status=traceback.format_exc(),
            )
            raise e
        await self.update_operation_state(LcmOperationState.COMPLETED)

    async def update_operation_state(
        self,
        op_state: LcmOperationState,
        stage: str = None,
        error_message: str = "",
        detailed_status: str = "",
    ) -> None:
        if stage is not None:
            self.stage = stage
        input = UpdateLcmOperationStateInput(
            op_id=self.op_id,
            op_state=op_state,
            stage=self.stage,
            error_message=error_message,
            detailed_status=detailed_status,
        )
        await workflow.execute_activity(
            activity=ACTIVITY_UPDATE_LCM_OPERATION_STATE,
            arg=input,
            activity_id=f"{ACTIVITY_UPDATE_LCM_OPERATION_STATE}-{self.op_id}",
            schedule_to_close_timeout=LcmOperationWorkflow.default_schedule_to_close_timeout,
            retry_policy=LcmOperationWorkflow.retry_policy,
        )


@workflow.defn(name=WORKFLOW_NSLCM_NO_OP, sandboxed=LcmOperationWorkflow._SANDBOXED)
class NsNoOpWorkflow(LcmOperationWorkflow):
    """
    This is a simple No Operation workflow that simply calls a No Operation
    activity.  It can be used as a placeholder when developing workflows.
    """

    @workflow.run
    async def wrap_nslcmop(self, input: NsLcmOperationInput) -> None:
        await super().wrap_nslcmop(input=input)

    async def workflow(self, input: NsLcmOperationInput) -> None:
        self.logger.debug(f"Called with: {input.nslcmop}")

        await workflow.execute_activity(
            activity=ACTIVITY_NSLCM_NO_OP,
            arg=input,
            activity_id=f"{ACTIVITY_NSLCM_NO_OP}-{self.op_id}",
            schedule_to_close_timeout=LcmOperationWorkflow.default_schedule_to_close_timeout,
            retry_policy=LcmOperationWorkflow.retry_policy,
        )
