#
# Copyright 2018-2022 Elyra Authors
#
# 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 json
from typing import Any
from typing import Type
from typing import TypeVar

from traitlets.utils.importstring import import_item

from elyra.metadata.schema import SchemaManager

# Setup forward reference for type hint on return from class factory method.  See
# https://stackoverflow.com/questions/39205527/can-you-annotate-return-type-when-value-is-instance-of-cls/39205612#39205612
M = TypeVar("M", bound="Metadata")


class Metadata(object):
    name = None
    resource = None
    display_name = None
    schema_name = None
    metadata = {}
    reason = None

    def __init__(self, **kwargs: Any) -> None:
        self.name = kwargs.get("name")
        self.display_name = kwargs.get("display_name")
        self.schema_name = kwargs.get("schema_name")
        self.metadata = kwargs.get("metadata", {})
        self.resource = kwargs.get("resource")
        self.reason = kwargs.get("reason")

    def on_load(self, **kwargs: Any) -> None:
        """Called by MetadataManager after fetching the instance and prior to validation.

        :param kwargs: additional arguments
        """
        pass

    def pre_save(self, **kwargs: Any) -> None:
        """Called by MetadataManager prior to saving the instance.

        :param kwargs: additional arguments
        Keyword Args:
            for_update (bool): indicates if this save operation if for update (True) or create (False)
        """
        pass

    def post_save(self, **kwargs: Any) -> None:
        """Called by MetadataManager following the save of the instance.

        :param kwargs: additional arguments
        Keyword Args:
            for_update (bool): indicates if this save operation if for update (True) or create (False)

        Note: Since exceptions thrown from this method can adversely affect the operation,
        implementations are advised to trap all exceptions unless the operation's
        rollback is warranted.
        """
        pass

    def pre_delete(self, **kwargs: Any) -> None:
        """Called by MetadataManager prior to deleting the instance."""
        pass

    def post_delete(self, **kwargs: Any) -> None:
        """Called by MetadataManager following the deletion of the instance.

        Note: Since exceptions thrown from this method can adversely affect the operation,
        implementations are advised to trap all exceptions unless the operation's
        rollback is warranted.
        """
        pass

    @classmethod
    def from_dict(cls: Type[M], schemaspace: str, metadata_dict: dict) -> M:
        """Creates an appropriate instance of Metadata from a dictionary instance"""

        # Get the schema and look for metadata_class entry and use that, else Metadata.
        metadata_class_name = "elyra.metadata.metadata.Metadata"
        schema_name = metadata_dict.get("schema_name")
        if schema_name:
            schema = SchemaManager.instance().get_schema(schemaspace, schema_name)
            metadata_class_name = schema.get("metadata_class_name", metadata_class_name)
        metadata_class = import_item(metadata_class_name)
        try:
            instance = metadata_class(**metadata_dict)
            if not isinstance(instance, Metadata):
                raise ValueError(
                    f"The metadata_class_name ('{metadata_class_name}') for "
                    f"schema '{schema_name}' must be a subclass of '{cls.__name__}'!"
                )
        except TypeError as te:
            raise ValueError(
                f"The metadata_class_name ('{metadata_class_name}') for "
                f"schema '{schema_name}' must be a subclass of '{cls.__name__}'!"
            ) from te
        return instance

    def to_dict(self, trim: bool = False) -> dict:
        # Exclude resource, and reason only if trim is True since we don't want to persist that information.
        #  Method prepare_write will be used to remove name prior to writes.
        d = dict(name=self.name, display_name=self.display_name, metadata=self.metadata, schema_name=self.schema_name)
        if not trim:
            if self.resource:
                d["resource"] = self.resource
            if self.reason:
                d["reason"] = self.reason

        return d

    def to_json(self, trim: bool = False) -> str:
        return json.dumps(self.to_dict(trim=trim), indent=2)

    def prepare_write(self) -> dict:
        """Prepares this instance for storage, stripping name, reason, and resource and converting to a dict"""
        prepared = self.to_dict(trim=True)  # we should also trim 'name' when storing
        prepared.pop("name", None)
        return prepared

    def __repr__(self):
        return self.to_json()
