#
# 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.
#
from jsonschema import ValidationError
from jupyter_server.base.handlers import APIHandler
from jupyter_server.utils import url_path_join
from jupyter_server.utils import url_unescape
from tornado import web

from elyra.metadata.error import MetadataExistsError
from elyra.metadata.error import MetadataNotFoundError
from elyra.metadata.error import SchemaNotFoundError
from elyra.metadata.manager import MetadataManager
from elyra.metadata.metadata import Metadata
from elyra.metadata.schema import SchemaManager
from elyra.util.http import HttpErrorMixin


class MetadataHandler(HttpErrorMixin, APIHandler):
    """Handler for metadata configurations collection."""

    @web.authenticated
    async def get(self, schemaspace):
        schemaspace = url_unescape(schemaspace)
        parent = self.settings.get("elyra")
        try:
            metadata_manager = MetadataManager(schemaspace=schemaspace, parent=parent)
            metadata = metadata_manager.get_all()
        except (ValidationError, ValueError) as err:
            raise web.HTTPError(400, str(err)) from err
        except MetadataNotFoundError as err:
            raise web.HTTPError(404, str(err)) from err
        except Exception as err:
            raise web.HTTPError(500, repr(err)) from err

        metadata_model = {schemaspace: [r.to_dict(trim=True) for r in metadata]}
        self.set_header("Content-Type", "application/json")
        self.finish(metadata_model)

    @web.authenticated
    async def post(self, schemaspace):

        schemaspace = url_unescape(schemaspace)
        parent = self.settings.get("elyra")
        try:
            instance = self._validate_body(schemaspace)
            self.log.debug(
                f"MetadataHandler: Creating metadata instance '{instance.name}' in schemaspace '{schemaspace}'..."
            )
            metadata_manager = MetadataManager(schemaspace=schemaspace, parent=parent)
            metadata = metadata_manager.create(instance.name, instance)
        except (ValidationError, ValueError, SyntaxError) as err:
            raise web.HTTPError(400, str(err)) from err
        except (MetadataNotFoundError, SchemaNotFoundError) as err:
            raise web.HTTPError(404, str(err)) from err
        except MetadataExistsError as err:
            raise web.HTTPError(409, str(err)) from err
        except Exception as err:
            raise web.HTTPError(500, repr(err)) from err

        self.set_status(201)
        self.set_header("Content-Type", "application/json")
        location = url_path_join(self.base_url, "elyra", "metadata", schemaspace, metadata.name)
        self.set_header("Location", location)
        self.finish(metadata.to_dict(trim=True))

    def _validate_body(self, schemaspace: str):
        """Validates the body issued for creates."""
        body = self.get_json_body()

        # Ensure schema_name and metadata fields exist.
        required_fields = ["schema_name", "metadata"]
        for field in required_fields:
            if field not in body:
                raise SyntaxError(f"Insufficient information - '{field}' is missing from request body.")

        # Ensure there is at least one of name or a display_name
        one_of_fields = ["name", "display_name"]
        if set(body).isdisjoint(one_of_fields):
            raise SyntaxError(
                f"Insufficient information - request body requires one of the following: {one_of_fields}."
            )

        instance = Metadata.from_dict(schemaspace, {**body})
        return instance


class MetadataResourceHandler(HttpErrorMixin, APIHandler):
    """Handler for metadata configuration specific resource (e.g. a runtime element)."""

    @web.authenticated
    async def get(self, schemaspace, resource):
        schemaspace = url_unescape(schemaspace)
        resource = url_unescape(resource)
        parent = self.settings.get("elyra")

        try:
            metadata_manager = MetadataManager(schemaspace=schemaspace, parent=parent)
            metadata = metadata_manager.get(resource)
        except (ValidationError, ValueError, NotImplementedError) as err:
            raise web.HTTPError(400, str(err)) from err
        except MetadataNotFoundError as err:
            raise web.HTTPError(404, str(err)) from err
        except Exception as err:
            raise web.HTTPError(500, repr(err)) from err

        self.set_header("Content-Type", "application/json")
        self.finish(metadata.to_dict(trim=True))

    @web.authenticated
    async def put(self, schemaspace, resource):
        schemaspace = url_unescape(schemaspace)
        resource = url_unescape(resource)
        parent = self.settings.get("elyra")

        try:
            payload = self.get_json_body()
            # Get the current resource to ensure its pre-existence
            metadata_manager = MetadataManager(schemaspace=schemaspace, parent=parent)
            metadata_manager.get(resource)
            # Check if name is in the payload and varies from resource, if so, raise 400
            if "name" in payload and payload["name"] != resource:
                raise NotImplementedError(
                    f"The attempt to rename instance '{resource}' to '{payload['name']}' is not supported."
                )
            instance = Metadata.from_dict(schemaspace, {**payload})
            self.log.debug(
                f"MetadataHandler: Updating metadata instance '{resource}' in schemaspace '{schemaspace}'..."
            )
            metadata = metadata_manager.update(resource, instance)
        except (ValidationError, ValueError, NotImplementedError) as err:
            raise web.HTTPError(400, str(err)) from err
        except MetadataNotFoundError as err:
            raise web.HTTPError(404, str(err)) from err
        except Exception as err:
            raise web.HTTPError(500, repr(err)) from err

        self.set_status(200)
        self.set_header("Content-Type", "application/json")
        self.finish(metadata.to_dict(trim=True))

    @web.authenticated
    async def delete(self, schemaspace, resource):
        schemaspace = url_unescape(schemaspace)
        resource = url_unescape(resource)
        parent = self.settings.get("elyra")

        try:
            self.log.debug(
                f"MetadataHandler: Deleting metadata instance '{resource}' in schemaspace '{schemaspace}'..."
            )
            metadata_manager = MetadataManager(schemaspace=schemaspace, parent=parent)
            metadata_manager.remove(resource)
        except (ValidationError, ValueError) as err:
            raise web.HTTPError(400, str(err)) from err
        except PermissionError as err:
            raise web.HTTPError(403, str(err)) from err
        except MetadataNotFoundError as err:
            raise web.HTTPError(404, str(err)) from err
        except Exception as err:
            raise web.HTTPError(500, repr(err)) from err

        self.set_status(204)
        self.finish()


class SchemaHandler(HttpErrorMixin, APIHandler):
    """Handler for schemaspace schemas."""

    @web.authenticated
    async def get(self, schemaspace):
        schemaspace = url_unescape(schemaspace)

        try:
            schema_manager = SchemaManager.instance()
            schemas = schema_manager.get_schemaspace_schemas(schemaspace)
        except (ValidationError, ValueError, SchemaNotFoundError) as err:
            raise web.HTTPError(404, str(err)) from err
        except Exception as err:
            raise web.HTTPError(500, repr(err)) from err

        schemas_model = {schemaspace: list(schemas.values())}
        self.set_header("Content-Type", "application/json")
        self.finish(schemas_model)


class SchemaResourceHandler(HttpErrorMixin, APIHandler):
    """Handler for a specific schema (resource) for a given schemaspace."""

    @web.authenticated
    async def get(self, schemaspace, resource):
        schemaspace = url_unescape(schemaspace)
        resource = url_unescape(resource)

        try:
            schema_manager = SchemaManager.instance()
            schema = schema_manager.get_schema(schemaspace, resource)
        except (ValidationError, ValueError, SchemaNotFoundError) as err:
            raise web.HTTPError(404, str(err)) from err
        except Exception as err:
            raise web.HTTPError(500, repr(err)) from err

        self.set_header("Content-Type", "application/json")
        self.finish(schema)


class SchemaspaceHandler(HttpErrorMixin, APIHandler):
    """Handler for retrieving schemaspace names."""

    @web.authenticated
    async def get(self):

        try:
            schema_manager = SchemaManager.instance()
            schemaspaces = schema_manager.get_schemaspace_names()
        except (ValidationError, ValueError) as err:
            raise web.HTTPError(404, str(err)) from err
        except Exception as err:
            raise web.HTTPError(500, repr(err)) from err

        schemaspace_model = {"schemaspaces": schemaspaces}

        self.set_header("Content-Type", "application/json")
        self.finish(schemaspace_model)


class SchemaspaceResourceHandler(HttpErrorMixin, APIHandler):
    """Handler for retrieving schemaspace JSON info (id, display name and descripton) for a given schemaspace."""

    @web.authenticated
    async def get(self, schemaspace):

        try:
            schema_manager = SchemaManager.instance()
            schemaspace = schema_manager.get_schemaspace(schemaspace)

        except (ValidationError, ValueError) as err:
            raise web.HTTPError(404, str(err)) from err
        except Exception as err:
            raise web.HTTPError(500, repr(err)) from err

        schemaspace_info_model = {
            "name": schemaspace.name,
            "id": schemaspace.id,
            "display_name": schemaspace.display_name,
            "description": schemaspace.description,
        }

        self.set_header("Content-Type", "application/json")
        self.finish(schemaspace_info_model)
