#######################################################################################
# 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
import base64
import logging
from juju.application import Application
from juju.controller import Controller
from n2vc.config import EnvironConfig
from n2vc.temporal_libjuju import ConnectionInfo, Libjuju
from osm_common.dataclasses.temporal_dataclasses import (
    CharmInfo,
    CheckCharmStatusInput,
    ModelInfo,
    TestVimConnectivityInput,
    VduInstantiateInput,
)
from osm_common.temporal_constants import (
    ACTIVITY_TEST_VIM_CONNECTIVITY,
    ACTIVITY_CREATE_MODEL,
    ACTIVITY_DEPLOY_CHARM,
    ACTIVITY_CHECK_CHARM_STATUS,
)
from osm_lcm.data_utils.database.database import Database
from temporalio import activity


class JujuPaasConnector:
    """Handles Juju Controller operations.

    Args:
        db  (Database):       Data Access Object
    """

    def __init__(self, db: Database):
        self.db: Database = db
        self.logger = logging.getLogger(f"lcm.act.{self.__class__.__name__}")
        self.config = EnvironConfig()

    def _decrypt_password(self, vim_content: dict) -> str:
        """Decrypt a password.
            vim_content     (dict):     VIM details as a dictionary

        Returns:
            plain text password (str)
        """
        return self.db.decrypt(
            vim_content["vim_password"],
            schema_version=vim_content["schema_version"],
            salt=vim_content["_id"],
        )

    def _get_connection_info(self, vim_id: str) -> ConnectionInfo:
        """Get VIM details from database using vim_id and returns
         the Connection Info to connect Juju Controller.

        Args:
            vim_id  (str):      VIM ID

        Returns:
            ConnectionInfo  (object)
        """
        vim_content = self.db.get_one("vim_accounts", {"_id": vim_id})
        endpoint = vim_content["vim_url"]
        username = vim_content["vim_user"]
        vim_config = vim_content["config"]
        cacert = vim_config["ca_cert_content"]
        cloud_name = vim_config["cloud"]
        cloud_credentials = vim_config["cloud_credentials"]
        password = self._decrypt_password(vim_content)
        return ConnectionInfo(
            endpoint, username, password, cacert, cloud_name, cloud_credentials
        )

    def _get_libjuju(self, vim_uuid):
        connection_info = self._get_connection_info(vim_uuid)
        return Libjuju(connection_info)

    async def _get_controller(self, vim_uuid) -> Controller:
        connection_info = self._get_connection_info(vim_uuid)
        controller = Controller()
        await controller.connect(
            endpoint=connection_info.endpoint,
            username=connection_info.user,
            password=connection_info.password,
            cacert=connection_info.cacert,
        )
        return controller

    @activity.defn(name=ACTIVITY_TEST_VIM_CONNECTIVITY)
    async def test_vim_connectivity(
        self, test_connectivity_input: TestVimConnectivityInput
    ) -> None:
        """Validates the credentials by attempting to connect to the given Juju Controller.

        Collaborators:
            DB Read:            vim_accounts
            Juju Controller:    Connect only

        Raises  (Retryable):
            ApplicationError    If any of password, cacert, cloud_credentials is invalid
                                or Juju controller is not reachable

        Activity Lifecycle:
            This activity should complete relatively quickly (in a few seconds).
            However, it would be reasonable to wait more than 72 seconds (network timeout)
            incase there are network issues.

            This activity will not report a heartbeat due to its
            short-running nature.

            It is recommended, although not necessary to implement a
            back-off strategy for this activity, as it will naturally block
            and wait on each connection attempt.
        """
        vim_id = test_connectivity_input.vim_uuid
        controller = None
        try:
            libjuju = self._get_libjuju(vim_id)
            controller = await libjuju.get_controller()
            message = f"Connection to juju controller succeeded for {vim_id}"
            self.logger.info(message)
        finally:
            await libjuju.disconnect_controller(controller)

    @activity.defn(name=ACTIVITY_CREATE_MODEL)
    async def create_model(self, create_model_input: ModelInfo) -> None:
        """Connects to Juju Controller. Creates a new model.

        Collaborators:
            DB Read:            vim_accounts
            Juju Controller:    Connect and create model.

        Raises  (Retryable):
            ApplicationError    If Juju controller is not reachable.
                                If the model already exists.

        Activity Lifecycle:
            This activity should complete relatively quickly (in a few seconds).
            However, it would be reasonable to wait more than 72 seconds (network timeout)
            incase there are network issues.

            This activity will not report a heartbeat due to its
            short-running nature.

            It is recommended, although not necessary to implement a
            back-off strategy for this activity, as it will naturally block
            and wait on each connection attempt.
        """
        controller = await self._get_controller(create_model_input.vim_uuid)
        if create_model_input.model_name in await controller.list_models():
            self.logger.debug(f"Model {create_model_input.model_name} already created")
            return

        vim_content = self.db.get_one(
            "vim_accounts", {"_id": create_model_input.vim_uuid}
        )
        vim_config = vim_content["config"]

        config = {
            "endpoints": ",".join(await controller.api_endpoints),
            "user": vim_content["vim_user"],
            "secret": self._decrypt_password(vim_content),
            "cacert": base64.b64encode(
                vim_config["ca_cert_content"].encode("utf-8")
            ).decode("utf-8"),
            "authorized-keys": vim_config["authorized_keys"],
        }

        self.logger.debug(f"Creating model {create_model_input.model_name}")
        await controller.add_model(
            create_model_input.model_name,
            config=config,
            cloud_name=vim_config["cloud"],
            credential_name=vim_config["cloud_credentials"],
        )
        self.logger.debug(f"Model {create_model_input.model_name} created")

    @activity.defn(name=ACTIVITY_DEPLOY_CHARM)
    async def deploy_charm(self, deploy_charm_input: VduInstantiateInput) -> None:
        """Deploys a charm.

        Collaborators:
            DB Read:            vim_accounts
            Juju Controller:    Connect and deploy charm

        Raises  (Retryable):
            ApplicationError    If Juju controller is not reachable
                                If application already exists

        Activity Lifecycle:
            This activity should complete relatively quickly (in a few seconds).
            However, it would be reasonable to wait more than 72 seconds (network timeout)
            incase there are network issues.

            This activity will not report a heartbeat due to its
            short-running nature.

            It is recommended, although not necessary to implement a
            back-off strategy for this activity, as it will naturally block
            and wait on each connection attempt.
        """
        model_name = deploy_charm_input.model_name
        charm_info = deploy_charm_input.charm_info
        application_name = charm_info.app_name
        controller = await self._get_controller(deploy_charm_input.vim_uuid)
        model = await controller.get_model(model_name)
        if application_name in model.applications:
            raise Exception("Application {} already exists".format(application_name))
        await model.deploy(
            entity_url=charm_info.entity_url,
            application_name=application_name,
            channel=charm_info.channel,
        )

    @activity.defn(name=ACTIVITY_CHECK_CHARM_STATUS)
    async def check_charm_status(
        self, check_charm_status: CheckCharmStatusInput
    ) -> None:
        """Checks the ready status of the charm.  This activity will block until the status of
        the application is either "active" or "blocked".  Additionally, it also blocks until
        the workload status of each of its units is also either "active" or "blocked".

        Collaborators:
            DB Read:            vim_accounts
            Juju Controller:    Connect to controller and check charm status.

        Raises  (Retryable):
            ApplicationError    If any of password, cacert, cloud_credentials is invalid
                                or Juju controller is not reachable

        Activity Lifecycle:
            This activity will continue indefinitely until the specified charm deployment
            has reached a ready state.  Heartbeats are performed to ensure this activity
            does not timeout.

            A start-to-close of something reasonable (such as 5 minutes) should be implemented
            at the workflow level and such a timeout shall trigger workflow failure logic.
        """

        controller = await self._get_controller(check_charm_status.vim_uuid)
        model = await controller.get_model(check_charm_status.model_name)
        application = model.applications[check_charm_status.application_name]

        ready = False
        last_status = None
        application_status = None
        last_unit_status = {}

        while not ready:
            activity.heartbeat()
            await asyncio.sleep(check_charm_status.poll_interval)
            # Perform the fetch of the status only once and keep it locally
            application_status = application.status
            if application_status != last_status:
                last_status = application_status
                self.logger.debug(
                    f"Application `{check_charm_status.application_name}` is {application_status}"
                )

            if application_status in ["active", "blocked"]:
                # Check each unit to see if they are also ready
                if not self._check_units_ready(
                    application=application, last_unit_status=last_unit_status
                ):
                    continue
            else:
                continue
            ready = True

    def _check_units_ready(
        self, application: Application, last_unit_status: dict
    ) -> bool:
        for unit in application.units:
            unit_workload_status = unit.workload_status
            if unit_workload_status != last_unit_status.get(unit, None):
                last_unit_status[unit] = unit_workload_status
                self.logger.debug(
                    f"Application `{application.name}` Unit `{unit}` is {unit_workload_status}"
                )
            if unit_workload_status not in ["active", "blocked"]:
                return False
        return True


class CharmInfoUtils:
    @staticmethod
    def get_charm_info(vdu: dict, sw_image_descs: list) -> CharmInfo:
        """Extract the charm info of a VDU.
        Args:
            vdu (dict):    contains the charm information.
            sw_image_descs (list): list of images in the VNF.

        Returns:
            CharmInfo  (object)
        """
        app_name = vdu.get("id")
        channel = CharmInfoUtils._get_property_value(
            "channel", vdu.get("configurable-properties")
        )
        entity_url = CharmInfoUtils._get_entity_url(
            vdu.get("sw-image-desc"), sw_image_descs
        )
        return CharmInfo(app_name, channel, entity_url)

    @staticmethod
    def _get_property_value(wanted_property: str, configurable_properties: list) -> str:
        """Extract the value corresponding to a given key-value pairs in
            vdu.configurable-properties

        Args:
            wanted_property (str): property used as key.
            configurable_properties (list): list of configurable-properties in VDU.

        Returns:
            value of the wanted property  (str)
        """
        filtered_property = next(
            filter(
                lambda property: property.get("key") == wanted_property,
                configurable_properties,
            ),
            None,
        )
        return filtered_property.get("value") if filtered_property else None

    @staticmethod
    def _get_entity_url(sw_image_desc_id: str, sw_image_descs: list) -> str:
        """Extract the image field for a given image_id
        Args:
            sw_image_desc_id (str): ID of the image used by a VDU.
            sw_image_descs (list): information of available images in the VNF.

        Returns:
            image of the sw_image_desc_id (str)
        """
        filtered_image = next(
            filter(lambda image: image.get("id") == sw_image_desc_id, sw_image_descs),
            None,
        )
        return filtered_image.get("image") if filtered_image else None
