#  Copyright (c) 2021 Food-X Technologies
#
#  This file is part of foodx_devops_tools.
#
#  You should have received a copy of the MIT License along with
#  foodx_devops_tools. If not, see <https://opensource.org/licenses/MIT>.

"""Context sensitive views of pipeline configuration data."""

import copy
import dataclasses
import logging
import pathlib
import typing

from foodx_devops_tools._to import StructuredTo
from foodx_devops_tools.azure.cloud import AzureCredentials
from foodx_devops_tools.patterns import SubscriptionData

from ..deployment import DeploymentTuple
from ._exceptions import PipelineViewError
from .pipeline import PipelineConfiguration, PuffMap

log = logging.getLogger(__name__)

Z = typing.TypeVar("Z", bound="IterationContext")


class IterationContext(typing.List[str]):
    """Record the iteration path."""

    def __str__(self: Z) -> str:
        """Convert the iteration path to a structured string."""
        return ".".join(self)


Y = typing.TypeVar("Y", bound="DeploymentContext")


class DeploymentContext:
    """Deployment context data expected to be applied to resource tags."""

    commit_sha: str
    pipeline_id: str
    release_id: str
    release_state: str

    # these data must be provided at deployment time because they are more
    # deeply context sensitive.
    __application_name: typing.Optional[str] = None
    __azure_subscription_name: typing.Optional[str] = None
    __azure_tenant_name: typing.Optional[str] = None
    __client: typing.Optional[str] = None
    __frame_name: typing.Optional[str] = None
    __system: typing.Optional[str] = None

    def __init__(
        self: Y,
        commit_sha: str,
        pipeline_id: str,
        release_id: str,
        release_state: str,
    ) -> None:
        """Construct ``DeploymentContext`` object."""
        self.commit_sha = commit_sha
        self.pipeline_id = pipeline_id
        self.release_id = release_id
        self.release_state = release_state

    @property
    def application_name(self: Y) -> str:
        """Get application name property."""
        if self.__application_name:
            return self.__application_name
        else:
            raise PipelineViewError(
                "application name not specified in deployment data"
            )

    @application_name.setter
    def application_name(self: Y, v: str) -> None:
        """Set application name property."""
        self.__application_name = v

    @property
    def azure_subscription_name(self: Y) -> str:
        """Get azure subscription name property."""
        if self.__azure_subscription_name:
            return self.__azure_subscription_name
        else:
            raise PipelineViewError(
                "azure subscription name not specified in deployment data"
            )

    @azure_subscription_name.setter
    def azure_subscription_name(self: Y, v: str) -> None:
        """Set azure subscription name property."""
        self.__azure_subscription_name = v

    @property
    def azure_tenant_name(self: Y) -> str:
        """Get azure tenant name property."""
        if self.__azure_tenant_name:
            return self.__azure_tenant_name
        else:
            raise PipelineViewError(
                "azure tenant name not specified in deployment data"
            )

    @azure_tenant_name.setter
    def azure_tenant_name(self: Y, v: str) -> None:
        """Set azure tenant name property."""
        self.__azure_tenant_name = v

    @property
    def client(self: Y) -> str:
        """Get client name property."""
        if self.__client:
            return self.__client
        else:
            raise PipelineViewError("client not specified in deployment data")

    @client.setter
    def client(self: Y, v: str) -> None:
        """Set client name property."""
        self.__client = v

    @property
    def frame_name(self: Y) -> str:
        """Get frame name property."""
        if self.__frame_name:
            return self.__frame_name
        else:
            raise PipelineViewError(
                "frame name not specified in deployment data"
            )

    @frame_name.setter
    def frame_name(self: Y, v: str) -> None:
        """Set frame name property."""
        self.__frame_name = v

    @property
    def resource_suffix(self: Y) -> str:
        """Get resource suffix property."""
        try:
            subscription_data = SubscriptionData.from_subscription_name(
                self.azure_subscription_name
            )

            return subscription_data.resource_suffix
        except PipelineViewError:
            # azure_subscription_name is undefined so just return an
            # identifiable response here.
            return "UNSPECIFIED"

    @property
    def system(self: Y) -> str:
        """Get system name property."""
        if self.__system:
            return self.__system
        else:
            raise PipelineViewError("system not specified in deployment data")

    @system.setter
    def system(self: Y, v: str) -> None:
        """Set system name property."""
        self.__system = v

    def as_dict(self: Y) -> dict:
        """
        Generate ``dict`` object representation of context data.

        The representation is intended to be used for tags data in ARM
        template parameter files. Note that "microsoft", "azure" and
        "windows" are reserved prefixes in Azure tags so any fields using
        these prefixes must be renamed for the dictionary conversion.
        """
        return {
            "application_name": self.__application_name,
            "client": self.__client,
            "commit_sha": self.commit_sha,
            "frame_name": self.__frame_name,
            "pipeline_id": self.pipeline_id,
            "release_id": self.release_id,
            "release_state": self.release_state,
            "resource_suffix": self.resource_suffix,
            "subscription_name": self.__azure_subscription_name,
            "system": self.__system,
            "tenant_name": self.__azure_tenant_name,
        }

    def __str__(self: Y) -> str:
        """Convert object to str for logging purposes."""
        return str(self.as_dict())


X = typing.TypeVar("X", bound="DeployDataView")


class DeployDataView:
    """Data critical to a resource deployment."""

    azure_credentials: AzureCredentials
    deployment_tuple: str
    location_primary: str
    release_state: str
    static_secrets: dict

    frame_folder: typing.Optional[pathlib.Path] = None

    __location_secondary: typing.Optional[str] = None

    iteration_context: IterationContext
    to: StructuredTo
    __puff_map: typing.Optional[PuffMap] = None

    def __init__(
        self: X,
        azure_credentials: AzureCredentials,
        deployment_tuple: str,
        location_primary: str,
        release_state: str,
        static_secrets: dict,
        location_secondary: typing.Optional[str] = None,
    ) -> None:
        """Construct ``DeployDataView`` object."""
        self.azure_credentials = azure_credentials
        self.deployment_tuple = deployment_tuple
        self.location_primary = location_primary
        self.release_state = release_state
        self.static_secrets = static_secrets
        self.__location_secondary = location_secondary

        self.iteration_context = IterationContext()
        self.to = StructuredTo()

    @property
    def location_secondary(self: X) -> typing.Optional[str]:
        """Get location secondary property."""
        return self.__location_secondary

    @location_secondary.setter
    def location_secondary(self: X, v: str) -> None:
        """Set location secondary property."""
        self.__location_secondary = v

    @property
    def puff_map(self: X) -> PuffMap:
        """Get puff map property."""
        if self.__puff_map:
            return self.__puff_map
        else:
            raise PipelineViewError("puff map not specified in deployment data")

    @puff_map.setter
    def puff_map(self: X, value: PuffMap) -> None:
        """Set puff map property."""
        self.__puff_map = value

    def __str__(self: X) -> str:
        """Convert object to str for logging purposes."""
        return str(
            {
                "azure_credentials": self.azure_credentials,
                "deployment_tuple": self.deployment_tuple,
                "location_primary": self.location_primary,
                "release_state": self.release_state,
                "location_secondary": self.__location_secondary,
                "iteration_context": self.iteration_context,
            }
        )


W = typing.TypeVar("W", bound="FlattenedDeployment")


@dataclasses.dataclass
class FlattenedDeployment:
    """Flattened deployment data for deployment concurrency."""

    context: DeploymentContext
    data: DeployDataView

    def copy_add_frame(self: W, frame_name: str) -> W:
        """
        Deep copy this object and add frame name data.

        Args:
            frame_name: Frame name data to add.

        Returns:
            Copied and updated object.
        """
        x = copy.deepcopy(self)
        x.context.frame_name = frame_name
        x.data.iteration_context.append(frame_name)
        return x

    def copy_add_application(self: W, application_name: str) -> W:
        """
        Deep copy this object and add application name data.

        Args:
            application_name: Application name data to add.

        Returns:
            Copied and updated object.
        """
        x = copy.deepcopy(self)
        x.context.application_name = application_name
        x.data.iteration_context.append(application_name)
        return x


V = typing.TypeVar("V", bound="SubscriptionView")
U = typing.TypeVar("U", bound="DeploymentView")
T = typing.TypeVar("T", bound="ReleaseView")


class SubscriptionView:
    """View of pipeline configuration conditioned by a subscription name."""

    deployment_view: "DeploymentView"
    subscription_name: str

    def __init__(self: V, deployment_view: U, subscription_name: str) -> None:
        """Construct ``SubscriptionView`` object."""
        self.deployment_view = deployment_view
        self.subscription_name = subscription_name

        self._validate_subscription()

    @property
    def deploy_data(self: V) -> typing.List[DeployDataView]:
        """Provide deployment data for each location in this subscription."""
        base_data = (
            self.deployment_view.release_view.configuration.subscriptions[
                self.subscription_name
            ]
        )
        result: typing.List[DeployDataView] = list()
        this_deployment = (
            self.deployment_view.release_view.configuration.deployments[
                str(self.deployment_view.deployment_tuple)
            ]
        )
        this_service_principals = (
            self.deployment_view.release_view.configuration.service_principals  # noqa E501
        )
        if not this_service_principals:
            raise PipelineViewError("Missing service principal credentials")

        this_tenants = self.deployment_view.release_view.configuration.tenants
        for this_locations in this_deployment.subscriptions[
            self.subscription_name
        ].locations:
            this_credentials = AzureCredentials(
                userid=this_service_principals[self.subscription_name].id,
                secret=this_service_principals[self.subscription_name].secret,
                name=this_service_principals[self.subscription_name].name,
                subscription=base_data.azure_id,
                tenant=this_tenants[base_data.tenant].azure_id,
            )
            static_secrets = dict()
            if self.deployment_view.release_view.configuration.static_secrets:
                static_secrets = self.deployment_view.release_view.configuration.static_secrets[  # noqa E501
                    self.subscription_name
                ]

            this_data: typing.Dict[str, typing.Any] = {
                "azure_credentials": this_credentials,
                "deployment_tuple": str(self.deployment_view.deployment_tuple),
                "location_primary": this_locations.primary,
                "location_secondary": None,
                "release_state": self.deployment_view.release_view.deployment_context.release_state,  # noqa E501
                "static_secrets": static_secrets,
            }
            if this_locations.secondary:
                this_data["location_secondary"] = this_locations.secondary

            result.append(DeployDataView(**this_data))

        return result

    def _validate_subscription(self: V) -> None:
        if (
            self.subscription_name
            not in self.deployment_view.release_view.configuration.subscriptions
        ):
            raise PipelineViewError(
                "Bad subscription, {0}".format(str(self.subscription_name))
            )


class DeploymentView:
    """View of pipeline configuration conditioned by deployment state."""

    release_view: "ReleaseView"
    deployment_tuple: DeploymentTuple

    def __init__(
        self: U, release_view: T, deployment_state: DeploymentTuple
    ) -> None:
        """Construct ``DeploymentView`` object."""
        self.release_view = release_view
        self.deployment_tuple = deployment_state

        self._validate_deployment_tuple()

    @property
    def subscriptions(self: U) -> typing.List[SubscriptionView]:
        """Provide the subscriptions in this deployment."""
        result: typing.List[SubscriptionView] = list()
        if (
            str(self.deployment_tuple)
            in self.release_view.configuration.deployments
        ):
            for (
                this_subscription
            ) in self.release_view.configuration.deployments[
                str(self.deployment_tuple)
            ].subscriptions.keys():
                result.append(SubscriptionView(self, this_subscription))

        return result

    def _validate_deployment_tuple(self: U) -> None:
        if (
            self.deployment_tuple.release_state
            not in self.release_view.configuration.release_states
        ):
            raise PipelineViewError(
                "Bad release state, {0}".format(str(self.deployment_tuple))
            )

        if (
            self.deployment_tuple.client
            not in self.release_view.configuration.clients.keys()
        ):
            raise PipelineViewError(
                "Bad client, {0}".format(str(self.deployment_tuple))
            )

        if (
            self.deployment_tuple.system
            not in self.release_view.configuration.systems
        ):
            raise PipelineViewError(
                "Bad system, {0}".format(str(self.deployment_tuple))
            )


class ReleaseView:
    """View of pipeline configuration conditioned by release state."""

    configuration: PipelineConfiguration
    deployment_context: DeploymentContext

    def __init__(
        self: T,
        configuration: PipelineConfiguration,
        deployment_context: DeploymentContext,
    ) -> None:
        """Construct ``ReleaseView`` object."""
        self.configuration = configuration
        self.deployment_context = deployment_context

        self._validate_release_state()

    @property
    def deployments(self: T) -> typing.List[DeploymentView]:
        """Provide the deployments in this release."""
        result: typing.List[DeploymentView] = list()
        for this_client in self.configuration.clients.keys():
            for this_system in self.configuration.systems:
                this_state = DeploymentTuple(
                    client=this_client,
                    release_state=self.deployment_context.release_state,
                    system=this_system,
                )
                if str(this_state) in self.configuration.deployments.keys():
                    result.append(DeploymentView(self, this_state))

        return result

    def flatten(
        self: T,
        to: StructuredTo,
    ) -> typing.List[FlattenedDeployment]:
        """Flatten the nested hierarchy of views into a simple list."""
        result: typing.List[FlattenedDeployment] = list()
        for this_deployment in self.deployments:
            for this_subscription in this_deployment.subscriptions:
                for this_deploy_data in this_subscription.deploy_data:
                    updated_context = copy.deepcopy(self.deployment_context)
                    updated_context.azure_subscription_name = (
                        this_subscription.subscription_name
                    )
                    updated_context.azure_tenant_name = (
                        self.configuration.subscriptions[
                            this_subscription.subscription_name
                        ].tenant
                    )
                    updated_context.client = (
                        this_deployment.deployment_tuple.client
                    )
                    updated_context.system = (
                        this_deployment.deployment_tuple.system
                    )

                    updated_data = copy.deepcopy(this_deploy_data)
                    updated_data.to = to

                    this_value = FlattenedDeployment(
                        context=updated_context,
                        data=updated_data,
                    )
                    result.append(this_value)
        return result

    def _validate_release_state(self: T) -> None:
        if (
            self.deployment_context.release_state
            not in self.configuration.release_states
        ):
            message = "Bad release state, {0}, {1}".format(
                self.deployment_context.release_state,
                str(self.configuration.release_states),
            )
            raise PipelineViewError(message)
