"""State module for managing Buckets."""
import copy
from typing import Any
from typing import Dict
from typing import List

from idem_gcp.tool.gcp.utils import get_project_from_account

__contracts__ = ["resource"]


# TODO:
# TREQ = {
#     "present": {
#
#     }
# }


async def present(
    hub,
    ctx,
    name: str,
    resource_id: str = None,
    predefined_acl: str = None,
    predefined_default_object_acl: str = None,
    project: str = None,
    # TODO: Remove user_project because it cannot be changed as it is set in the credentials
    user_project: str = None,
    acl: List = None,
    billing: Dict = None,
    cors: List = None,
    custom_placement_config: Dict = None,
    default_event_based_hold: bool = None,
    default_object_acl: List = None,
    encryption: Dict = None,
    iam_configuration: Dict = None,
    labels: Dict = None,
    lifecycle: Dict = None,
    location: str = None,
    logging: Dict = None,
    rpo: str = None,
    storage_class: str = None,
    versioning: Dict = None,
    website: Dict = None,
):
    """Ensure a bucket exists pursuant to the requested state.

    :param hub: The redistributed pop central hub.
    :param ctx: A dict with the keys/values for the execution of the Idem run
    located in `hub.idem.RUNS[ctx['run_name']]`.
    :param name: The name of the state (e.g., "assure bucket present").
    :param project: Name of the project in which to insert/update the bucket.
    :param userProject: Project id of the user to bill.
    :param body: A `Bucket`_ dictionary passed as the body to the GCP APIs
    as they require.

    Example SLS file usage:
    .. code-block::
        "Assure Bucket Present":
            gcp.storage.buckets.present:
                - project: MyProject
                - userProject: None
                - body:
                    name: MyBucket
                    cors:
                    - maxAgeSeconds: 42
                      method:
                        - "GET"
                        - "OPTIONS"
                        - "POST"
                        - "PATCH"
                        - "DELETE"
                      origin": [ '*' ]
                      responseHeader:
                        - "Cache-Control"
                        - "Content-Language"
                        - "Content-Length"
                        - "Content-Type"
                        - "Expires"
                        - "Last-Modified"
                        - "Pragma"

    .. _Bucket: https://googleapis.github.io/google-api-python-client/docs/dyn/storage_v1.buckets.html#insert
    """
    result = {
        "result": True,
        "old_state": None,
        "new_state": None,
        "name": name,
        "comment": [],
    }

    project = get_project_from_account(ctx, project)

    # Get the resource_id from ESM
    if not resource_id:
        resource_id = (ctx.old_state or {}).get("resource_id")

    old = None

    if resource_id:
        old_get_ret = await hub.exec.gcp.storage.buckets.get(
            ctx,
            name=name,
        )

        if not old_get_ret["result"] or not old_get_ret["ret"]:
            result["result"] = False
            result["comment"] += old_get_ret["comment"]
            # TODO: Handle the case when resource does not exist
            return result

        # copy.copy(old_get_ret['ret']) is needed to convert any objects of type NamespaceDict to dict
        # in old_get_ret['ret']. This is done so that comparisons with the plan_state which has
        # objects of type dict works properly
        old = copy.deepcopy(copy.copy(old_get_ret["ret"]))
        result["old_state"] = old

    # TODO: Check if body contains the same parameters as old
    # to be autogenerated by pop-create based on insert/update props in properties.yaml
    resource_body = {
        "acl": acl,
        "billing": billing,
        "cors": cors,
        "custom_placement_config": custom_placement_config,
        "default_event_based_hold": default_event_based_hold,
        "default_object_acl": default_object_acl,
        "encryption": encryption,
        "iam_configuration": iam_configuration,
        "labels": labels,
        "lifecycle": lifecycle,
        "location": location,
        "logging": logging,
        "name": name,
        "rpo": rpo,
        "storage_class": storage_class,
        "versioning": versioning,
        "website": website,
    }

    # TODO: How to handle query params which are not returned in the response body of get
    plan_state = {
        # "project": project,
        # "predefined_acl": predefined_acl,
        # "predefined_default_object_acl": predefined_default_object_acl,
        "resource_id": resource_id,
        **resource_body,
    }

    plan_state = {k: v for (k, v) in plan_state.items() if v is not None}

    if old:
        changes = hub.tool.gcp.utils.compare_states(old, plan_state)

        if not changes:
            result["result"] = True
            result["comment"].append(
                hub.tool.gcp.comment_utils.already_exists_comment(
                    "gcp.storage.bucket", name
                )
            )
            result["new_state"] = plan_state

            return result

        non_updatable_properties = (
            hub.tool.gcp.resource_prop_utils.get_non_updatable_properties(
                "storage.buckets"
            )
        )

        for key, value in plan_state.items():
            if value == old.get(key):
                non_updatable_properties.discard(key)

        if non_updatable_properties:
            result["result"] = False
            result["comment"].append(
                hub.tool.gcp.comment_utils.non_updatable_properties_comment(
                    "gcp.storage.bucket", name, non_updatable_properties
                )
            )
            result["new_state"] = copy.deepcopy(old)
            return result

        if ctx["test"]:
            result["comment"].append(
                hub.tool.gcp.comment_utils.would_update("gcp.storage.bucket", name)
            )
            result["new_state"] = plan_state
            return result

        # Perform update
        update_ret = await hub.exec.gcp_api.client.storage.buckets.update(
            hub,
            ctx,
            name=name,
            resource_id=resource_id,
            body=resource_body,
            userProject=user_project,
        )
        if not update_ret["result"]:
            result["result"] = False
            result["comment"] += update_ret["comment"]
            return result
        result["comment"].append(
            hub.tool.gcp.comment_utils.update_comment("gcp.storage.bucket", name)
        )
        result["new_state"] = update_ret["ret"]

    else:
        if ctx["test"]:
            result["comment"].append(
                hub.tool.gcp.comment_utils.would_create_comment(
                    "gcp.storage.bucket", name
                )
            )
            result["new_state"] = plan_state
            return result

        # Create
        create_ret = await hub.exec.gcp_api.client.storage.buckets.insert(
            ctx,
            name=name,
            project=project,
            body=resource_body,
            userProject=user_project,
        )
        if not create_ret["result"]:
            result["result"] = False
            result["comment"] += create_ret["comment"]
            return result
        result["comment"].append(
            hub.tool.gcp.comment_utils.create_comment("gcp.storage.bucket", name)
        )
        result["old_state"] = {}
        result["new_state"] = create_ret["ret"]

    return result


async def absent(
    hub,
    ctx,
    name: str,
    resource_id: str = None,
    project: str = None,
    user_project: str = None,
):
    """Ensure a bucket does not exist.

    :param hub: The redistributed pop central hub.
    :param ctx: A dict with the keys/values for the execution of the Idem run
    located in `hub.idem.RUNS[ctx['run_name']]`.
    :param name: The name of the state (e.g., "assure subnet absent").
    :param project: The project in which the bucket should not exist.

    Example usage:
        .. code-block: yaml
            gcp.storage.buckets.absent:
                - project: {{ project }}
                - bucket: {{ bucket_name }}
                - user_project: {{ billto_project_name }}
    """
    result = {
        "result": True,
        "old_state": None,
        "new_state": None,
        "name": name,
        "comment": [],
    }

    if not resource_id:
        resource_id = (ctx.old_state or {}).get("resource_id")

    if not resource_id:
        result["comment"].append(
            hub.tool.aws.comment_utils.already_absent_comment(
                "gcp.storage.buckets", name
            )
        )
        return result

    get_ret = await hub.exec.gcp.storage.buckets.get(ctx, name=name)
    if not get_ret["result"] or not get_ret["ret"]:
        result["result"] = get_ret["result"]
        result["comment"] += get_ret["comment"]
        # TODO: Should we get the old state from the context?
        return result

    if ctx["test"]:
        result["comment"].append(
            hub.tool.gcp.comment_utils.would_delete_comment("gcp.storage.bucket", name)
        )
        result["old_state"] = get_ret["ret"]
        return result
    else:
        # Delete the existing bucket (async call).
        del_ret = await hub.exec.gcp.storage.buckets.delete(
            ctx, name=name, resource_id=resource_id, userProject=user_project
        )
        if not del_ret["result"]:
            result["result"] = False
            result["comment"] += del_ret["comment"]
            return result

        result["old_state"] = get_ret["ret"]
        result["comment"].append(
            hub.tool.gcp.comment_utils.delete_comment("gcp.storage.bucket", name)
        )

    return result


async def describe(hub, ctx) -> Dict[str, Dict[str, Any]]:
    r"""Describe the resource in a way that can be recreated/managed with the corresponding "present" function.

    Retrieves a list of buckets.

    Returns:
        Dict[str, Any]

    Examples:
        .. code-block:: bash

            $ idem describe gcp.storage.buckets
    """
    result = {}

    # TODO: Pagination
    describe_ret = await hub.exec.gcp.storage.buckets.list(
        ctx, project=ctx.acct.project_id
    )

    if not describe_ret["result"]:
        hub.log.debug(f"Could not describe buckets {describe_ret['comment']}")
        return {}

    for resource in describe_ret["ret"]["items"]:
        resource_id = resource.get("resource_id")
        result[resource_id] = {
            "gcp.storage.buckets.present": [
                {parameter_key: parameter_value}
                for parameter_key, parameter_value in resource.items()
            ]
        }
    return result


def is_pending(hub, ret: dict, state: str = None, **pending_kwargs) -> bool:
    return hub.tool.gcp.utils.is_pending(ret=ret, state=state, **pending_kwargs)
