# Copyright 2016-2018, Pulumi Corporation.
#
# 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 base64
import pickle
from typing import (
    TYPE_CHECKING,
    Any,
    ClassVar,
    Dict,
    List,
    Optional,
    cast,
    no_type_check,
)

import dill

import pulumi

from .. import CustomResource, ResourceOptions
from ..runtime._serialization import _serialize
from .config import Config

if TYPE_CHECKING:
    from ..output import Inputs, Output

PROVIDER_KEY = "__provider"


class ConfigureRequest:
    """
    ConfigureRequest is the shape of the configuration data passed to a
    provider's configure method.
    """

    config: Config
    """
    The stack's configuration.
    """

    def __init__(self, config: Config):
        self.config = config


class CheckResult:
    """
    CheckResult represents the results of a call to `ResourceProvider.check`.
    """

    inputs: Dict[str, Any]
    """
    The inputs to use, if any.
    """

    failures: List["CheckFailure"]
    """
    Any validation failures that occurred.
    """

    def __init__(self, inputs: Dict[str, Any], failures: List["CheckFailure"]) -> None:
        self.inputs = inputs
        self.failures = failures


class CheckFailure:
    """
    CheckFailure represents a single failure in the results of a call to `ResourceProvider.check`
    """

    property: str
    """
    The property that failed validation.
    """

    reason: str
    """
    The reason that the property failed validation.
    """

    def __init__(self, property_: str, reason: str) -> None:
        self.property = property_
        self.reason = reason


class DiffResult:
    """
    DiffResult represents the results of a call to `ResourceProvider.diff`.
    """

    changes: Optional[bool]
    """
    If true, this diff detected changes and suggests an update.
    """

    replaces: Optional[List[str]]
    """
    If this update requires a replacement, the set of properties triggering it.
    """

    stables: Optional[List[str]]
    """
    An optional list of properties that will not ever change.
    """

    delete_before_replace: Optional[bool]
    """
    If true, and a replacement occurs, the resource will first be deleted before being recreated.
    This is to void potential side-by-side issues with the default create before delete behavior.
    """

    def __init__(
        self,
        changes: Optional[bool] = None,
        replaces: Optional[List[str]] = None,
        stables: Optional[List[str]] = None,
        delete_before_replace: Optional[bool] = None,
    ) -> None:
        self.changes = changes
        self.replaces = replaces
        self.stables = stables
        self.delete_before_replace = delete_before_replace


class CreateResult:
    """
    CreateResult represents the results of a call to `ResourceProvider.create`.
    """

    id: str
    """
    The ID of the created resource.
    """

    outs: Optional[Dict[str, Any]]
    """
    Any properties that were computed during creation.
    """

    def __init__(self, id_: str, outs: Optional[Dict[str, Any]] = None) -> None:
        self.id = id_
        self.outs = outs


class ReadResult:
    """
    The ID of the resource ready back (or blank if missing).
    """

    id: Optional[str]
    """
    The ID of the resource ready back (or blank if missing).
    """

    outs: Optional[Dict[str, Any]]
    """
    The current property state read from the live environment.
    """

    def __init__(
        self,
        id_: Optional[str] = None,
        outs: Optional[Dict[str, Any]] = None,
    ) -> None:
        self.id = id_
        self.outs = outs


class UpdateResult:
    """
    UpdateResult represents the results of a call to `ResourceProvider.update`.
    """

    outs: Optional[Dict[str, Any]]
    """
    Any properties that were computed during updating.
    """

    def __init__(self, outs: Optional[Dict[str, Any]] = None) -> None:
        self.outs = outs


class ResourceProvider:
    """
    ResourceProvider is a Dynamic Resource Provider which allows defining new kinds of resources
    whose CRUD operations are implemented inside your Python program.
    """

    serialize_as_secret_always: bool = True
    """
    Controls whether the serialized provider is a secret.
    By default it is True which makes the serialized provider a secret always.
    Set to False in a subclass to make the serialized provider a secret only
    if any secret Outputs were captured during serialization of the provider.
    """

    def check(self, _olds: Dict[str, Any], news: Dict[str, Any]) -> CheckResult:
        """
        Check validates that the given property bag is valid for a resource of the given type.
        """
        return CheckResult(news, [])

    def diff(
        self,
        _id: str,
        _olds: Dict[str, Any],
        _news: Dict[str, Any],
    ) -> DiffResult:
        """
        Diff checks what impacts a hypothetical update will have on the resource's properties.
        """
        return DiffResult()

    def create(self, props: Dict[str, Any]) -> CreateResult:
        """
        Create allocates a new instance of the provided resource and returns its unique ID
        afterwards. If this call fails, the resource must not have been created (i.e., it is
        "transactional").
        """
        raise Exception("Subclass of ResourceProvider must implement 'create'")

    def read(self, id_: str, props: Dict[str, Any]) -> ReadResult:
        """
        Reads the current live state associated with a resource.  Enough state must be included in
        the inputs to uniquely identify the resource; this is typically just the resource ID, but it
        may also include some properties.
        """
        return ReadResult(id_, props)

    def update(
        self,
        _id: str,
        _olds: Dict[str, Any],
        _news: Dict[str, Any],
    ) -> UpdateResult:
        """
        Update updates an existing resource with new values.
        """
        return UpdateResult()

    def delete(self, _id: str, _props: Dict[str, Any]) -> None:
        """
        Delete tears down an existing resource with the given ID.  If it fails, the resource is
        assumed to still exist.
        """

    def configure(self, req: ConfigureRequest) -> None:
        """
        Configure sets up the resource provider. Use this to initialize the
        provider and store configuration.
        """

    def __init__(self) -> None:
        pass


# TODO[python/mypy#1102]: mypy doesn't currently support multiline comments
# multiple errors related to the type assignment we're doing in this method eg 'Picker = _Pickler'
@no_type_check
def serialize_provider(provider: ResourceProvider) -> str:
    # We need to customize our Pickler to ensure we sort dictionaries before serializing to try to
    # ensure we get a deterministic result.  Without this we would see changes to our serialized
    # provider even when there are no actual changes.
    old_pickler = pickle.Pickler
    pickle.Pickler = pickle._Pickler  # pylint: disable=protected-access

    # See: https://github.com/uqfoundation/dill/issues/481#issuecomment-1133789848
    unsorted_batch_setitems = pickle.Pickler._batch_setitems

    def batch_set_items_sorted(self, items):
        unsorted_batch_setitems(self, sorted(items))

    pickle.Pickler._batch_setitems = batch_set_items_sorted

    def save_dict_sorted(self, obj):
        if self.bin:
            self.write(pickle.EMPTY_DICT)
        else:  # proto 0 -- can't use EMPTY_DICT
            self.write(pickle.MARK + pickle.DICT)

        self.memoize(obj)
        self._batch_setitems(obj.items())  # pylint: disable=protected-access

    pickle.Pickler.save_dict = save_dict_sorted

    # Use dill to recursively pickle the provider and store base64 encoded form
    try:
        byts = dill.dumps(provider, protocol=pickle.DEFAULT_PROTOCOL, recurse=True)
        return base64.b64encode(byts).decode("utf-8")
    finally:
        # Restore the original pickler
        pickle.Pickler = old_pickler


class Resource(CustomResource):
    """
    Resource represents a Pulumi Resource that incorporates an inline implementation of the Resource's CRUD operations.
    """

    _resource_type_name: ClassVar[str]

    def __init_subclass__(cls, module: str = "", name: str = "Resource"):
        if module:
            module = f"/{module}"
        cls._resource_type_name = f"dynamic{module}:{name}"

    def __init__(
        self,
        provider: ResourceProvider,
        name: str,
        props: "Inputs",
        opts: Optional[ResourceOptions] = None,
    ) -> None:
        """
        :param str provider: The implementation of the resource's CRUD operations.
        :param str name: The name of this resource.
        :param Optional[dict] props: The arguments to use to populate the new resource. Must not define the reserved
                property "__provider".
        :param Optional[ResourceOptions] opts: A bag of options that control this resource's behavior.
        """

        if PROVIDER_KEY in props:
            raise Exception("A dynamic resource must not define the __provider key")

        props = cast(dict, props)

        # Serialize the provider, allowing secret Outputs to be captured.
        serialized_provider, contains_secrets = _serialize(
            True, serialize_provider, provider
        )

        # serialize_as_secret_always is True by default, which makes the serialized provider
        # a secret always, which is the existing behavior as of v3.75.0.
        # When serialize_as_secret_always is set to False, the serialized provider is made a
        # secret only if any secret Outputs were captured during serialization of the
        # provider.
        serialize_as_secret_always: bool = getattr(
            provider, "serialize_as_secret_always", True
        )
        if serialize_as_secret_always or contains_secrets:
            serialized_provider = pulumi.Output.secret(serialized_provider)

        props[PROVIDER_KEY] = serialized_provider

        super().__init__(f"pulumi-python:{self._resource_type_name}", name, props, opts)
