"""Imports :
    flask.json.jsonify : Turns the JSON output into a Response object with the
    application/json mimetype Ref- http://flask.pocoo.org/docs/0.12/api
    flask.request : The request object used by default in Flask.
    Remembers the matched endpoint and view arguments.
    Ref - http://flask.pocoo.org/docs/0.12/api
    flask.abort : Raises an HTTPException for the given status code or WSGI
    application: Ref - http://flask.pocoo.org/docs/0.12/api
    flask_restful.Resource : Represents an abstract RESTful resource.
    Ref - http://flask-restful.readthedocs.io/en/latest/api.html
    hydrus.data.crud : Function/Class to perform basic CRUD operations for the server
    hydrus.data.user.check_authorization : Funcion checks if the request object has the
    correct authorization
    hydrus.utils.get_session : Gets the database session for the server
    hydrus.utils.get_doc : Function which gets the server API documentation
    hydrus.utils.get_api_name : Function which gets the server API name
    hydrus.utils.get_hydrus_server_url : Function the gets the server URL
    hydrus.utils.get_authentication : Function that checks whether API needs to be
    authenticated or not
"""  # nopep8

import json
from typing import Dict, Any, Union
from flask import Response, jsonify, request, abort
from flask_restful import Resource
from hydra_python_core.doc_writer import HydraStatus, HydraError

from hydrus.auth import check_authentication_response
from hydrus.data import crud
from hydrus.data.exceptions import (
    ClassNotFound,
    InstanceExists,
    PropertyNotFound,
    InstanceNotFound,
    PageNotFound,
    InvalidSearchParameter,
    OffsetOutOfRange)
from hydrus.helpers import (
    set_response_headers,
    checkClassOp,
    getType,
    validObject,
    checkEndpoint,
    validObjectList,
    type_match,
    hydrafy,
    check_writeable_props,
    check_required_props,
    add_iri_template,
    finalize_response,
    send_sync_update,
    get_link_props,
    get_link_props_for_multiple_objects)
from hydrus.utils import (
    get_session,
    get_doc,
    get_api_name,
    get_hydrus_server_url,
    get_page_size,
    get_pagination)
from hydrus.socketio_factory import socketio


class Index(Resource):
    """Class for the EntryPoint."""

    def get(self) -> Response:
        """Return main entrypoint for the api."""
        return set_response_headers(jsonify(get_doc().entrypoint.get()))


class Vocab(Resource):
    """Vocabulary for Hydra."""

    def get(self) -> Response:
        """Return the main hydra vocab."""
        return set_response_headers(jsonify(get_doc().generate()))


class Entrypoint(Resource):
    """Hydra EntryPoint."""

    def get(self) -> Response:
        """Return application main Entrypoint."""
        response = {"@context": get_doc().entrypoint.context.generate()}
        return set_response_headers(jsonify(response))


class Item(Resource):
    """Handles all operations(GET, POST, PATCH, DELETE) on Items
    (item can be anything depending upon the vocabulary)."""

    def get(self, id_: str, path: str) -> Response:
        """
        GET object with id = id_ from the database.
        :param id_ : Item ID
        :param path : Path for Item ( Specified in APIDoc @id)
        """
        id_ = str(id_)
        auth_response = check_authentication_response()
        if isinstance(auth_response, Response):
            return auth_response

        class_type = get_doc().collections[path]["collection"].class_.title
        # Get path of the collection-class
        class_path = get_doc().collections[path]["collection"].class_.path

        if checkClassOp(class_path, "GET"):
            # Check if class_type supports GET operation
            try:
                # Try getting the Item based on ID and Class type
                response = crud.get(
                    id_,
                    class_type,
                    api_name=get_api_name(),
                    session=get_session())

                response = finalize_response(class_path, response)
                return set_response_headers(
                    jsonify(hydrafy(response, path=path)))

            except (ClassNotFound, InstanceNotFound) as e:
                error = e.get_HTTP()
                return set_response_headers(jsonify(error.generate()), status_code=error.code)
        abort(405)

    def post(self, id_: str, path: str) -> Response:
        """Update object of type<path> at ID<id_> with new object_ using HTTP POST.
        :param id_ - ID of Item to be updated
        :param path - Path for Item type( Specified in APIDoc @id)
        """
        id_ = str(id_)
        auth_response = check_authentication_response()
        if isinstance(auth_response, Response):
            return auth_response

        class_type = get_doc().collections[path]["collection"].class_.title
        # Get path of the collection-class
        class_path = get_doc().collections[path]["collection"].class_.path
        object_ = json.loads(request.data.decode('utf-8'))
        if checkClassOp(class_path, "POST") and check_writeable_props(class_path, object_):
            # Check if class_type supports POST operation
            obj_type = getType(class_path, "POST")
            link_props, link_type_check = get_link_props(class_path, object_)
            # Load new object and type
            if validObject(object_) and object_["@type"] == obj_type and check_required_props(
                    class_path, object_) and link_type_check:
                try:
                    # Update the right ID if the object is valid and matches
                    # type of Item
                    object_id = crud.update(
                        object_=object_,
                        id_=id_,
                        link_props=link_props,
                        type_=object_["@type"],
                        session=get_session(),
                        api_name=get_api_name())
                    method = "POST"
                    resource_url = "{}{}/{}/{}".format(
                            get_hydrus_server_url(), get_api_name(), path, object_id)
                    last_job_id = crud.get_last_modification_job_id(session=get_session())
                    new_job_id = crud.insert_modification_record(method, resource_url,
                                                                 session=get_session())
                    send_sync_update(socketio=socketio, new_job_id=new_job_id,
                                     last_job_id=last_job_id, method=method,
                                     resource_url=resource_url)
                    headers_ = [{"Location": resource_url}]
                    status_description = "Object with ID {} successfully updated".format(object_id)
                    status = HydraStatus(code=200, title="Object updated", desc=status_description)
                    return set_response_headers(jsonify(status.generate()), headers=headers_)

                except (ClassNotFound, InstanceNotFound, InstanceExists, PropertyNotFound) as e:
                    error = e.get_HTTP()
                    return set_response_headers(jsonify(error.generate()), status_code=error.code)
            else:
                error = HydraError(code=400, title="Data is not valid")
                return set_response_headers(jsonify(error.generate()), status_code=error.code)
        else:
            abort(405)

    def put(self, id_: str, path: str) -> Response:
        """Add new object_ optional <id_> parameter using HTTP PUT.
        :param id_ - ID of Item to be updated
        :param path - Path for Item type( Specified in APIDoc @id) to be updated
        """
        id_ = str(id_)
        auth_response = check_authentication_response()
        if isinstance(auth_response, Response):
            return auth_response

        class_type = get_doc().collections[path]["collection"].class_.title
        # Get path of the collection-class
        class_path = get_doc().collections[path]["collection"].class_.path
        if checkClassOp(class_path, "PUT"):
            # Check if class_type supports PUT operation
            object_ = json.loads(request.data.decode('utf-8'))
            obj_type = getType(class_path, "PUT")
            link_props, link_type_check = get_link_props(class_path, object_)
            # Load new object and type
            if validObject(object_) and object_["@type"] == obj_type and check_required_props(
                    class_path, object_) and link_type_check:
                try:
                    # Add the object with given ID
                    object_id = crud.insert(object_=object_, id_=id_,
                                            link_props=link_props, session=get_session())
                    headers_ = [{"Location": "{}{}/{}/{}".format(
                        get_hydrus_server_url(), get_api_name(), path, object_id)}]
                    status_description = "Object with ID {} successfully added".format(object_id)
                    status = HydraStatus(code=201, title="Object successfully added.",
                                         desc=status_description)
                    return set_response_headers(
                        jsonify(status.generate()), headers=headers_, status_code=status.code)
                except (ClassNotFound, InstanceExists, PropertyNotFound) as e:
                    error = e.get_HTTP()
                    return set_response_headers(jsonify(error.generate()), status_code=error.code)
            else:
                error = HydraError(code=400, title="Data is not valid")
                return set_response_headers(jsonify(error.generate()), status_code=error.code)
        else:
            abort(405)

    def delete(self, id_: str, path: str) -> Response:
        """Delete object with id=id_ from database."""
        id_ = str(id_)
        auth_response = check_authentication_response()
        if isinstance(auth_response, Response):
            return auth_response

        class_type = get_doc().collections[path]["collection"].class_.title
        # Get path of the collection-class
        class_path = get_doc().collections[path]["collection"].class_.path

        if checkClassOp(class_path, "DELETE"):
            # Check if class_type supports PUT operation
            try:
                # Delete the Item with ID == id_
                crud.delete(id_, class_type, session=get_session())
                method = "DELETE"
                resource_url = "{}{}/{}/{}".format(
                    get_hydrus_server_url(), get_api_name(), path, id_)
                last_job_id = crud.get_last_modification_job_id(session=get_session())
                new_job_id = crud.insert_modification_record(method, resource_url,
                                                             session=get_session())
                send_sync_update(socketio=socketio, new_job_id=new_job_id,
                                 last_job_id=last_job_id, method=method,
                                 resource_url=resource_url)
                status_description = "Object with ID {} successfully deleted".format(id_)
                status = HydraStatus(code=200, title="Object successfully deleted.",
                                     desc=status_description)
                return set_response_headers(jsonify(status.generate()))

            except (ClassNotFound, InstanceNotFound) as e:
                error = e.get_HTTP()
                return set_response_headers(jsonify(error.generate()), status_code=error.code)

        abort(405)


class ItemCollection(Resource):
    """Handle operation related to ItemCollection (a collection of items)."""

    def get(self, path: str) -> Response:
        """
        Retrieve a collection of items from the database.
        """
        search_params = request.args.to_dict()
        auth_response = check_authentication_response()
        if isinstance(auth_response, Response):
            return auth_response
        endpoint_ = checkEndpoint("GET", path)
        if not endpoint_['method']:
            # If endpoint and Get method not supported in the API
            abort(endpoint_['status'])
        elif path in get_doc().collections:
            # If endpoint and GET method is supported in the API
            # and collection name in document's collections
            collection = get_doc().collections[path]["collection"]
            # get path of the collection class
            class_path = collection.class_.path
            try:
                # Get collection details from the database
                if get_pagination():
                    # Get paginated response
                    response = crud.get_collection(
                        get_api_name(), collection.class_.title, session=get_session(),
                        paginate=True, path=path, page_size=get_page_size(),
                        search_params=search_params)
                else:
                    # Get whole collection
                    response = crud.get_collection(
                        get_api_name(), collection.class_.title, session=get_session(),
                        paginate=False, path=path, search_params=search_params)

                response["search"] = add_iri_template(path=class_path,
                                                      API_NAME=get_api_name())

                return set_response_headers(jsonify(hydrafy(response, path=path)))

            except (ClassNotFound, PageNotFound, InvalidSearchParameter, OffsetOutOfRange) as e:
                error = e.get_HTTP()
                return set_response_headers(jsonify(error.generate()), status_code=error.code)

        # If endpoint and GET method is supported in the API and class is supported
        elif path in get_doc().parsed_classes and "{}Collection".format(
                path) not in get_doc().collections:
            try:
                class_type = get_doc().parsed_classes[path]['class'].title
                response = crud.get_single(
                    class_type,
                    api_name=get_api_name(),
                    session=get_session(),
                    path=path)
                response = finalize_response(path, response)
                return set_response_headers(jsonify(hydrafy(response, path=path)))

            except (ClassNotFound, InstanceNotFound) as e:
                error = e.get_HTTP()
                return set_response_headers(jsonify(error.generate()), status_code=error.code)

    def put(self, path: str) -> Response:
        """
        Method executed for PUT requests.
        Used to add an item to a collection
        :param path - Path for Item type ( Specified in APIDoc @id)
        """
        auth_response = check_authentication_response()
        if isinstance(auth_response, Response):
            return auth_response

        endpoint_ = checkEndpoint("PUT", path)
        if endpoint_['method']:
            # If endpoint and PUT method is supported in the API
            object_ = json.loads(request.data.decode('utf-8'))

            if path in get_doc().collections:
                # If collection name in document's collections
                collection = get_doc().collections[path]["collection"]

                # title of HydraClass object corresponding to collection
                obj_type = collection.class_.title
                # get path of the collection class
                class_path = collection.class_.path

                if validObject(object_) and object_["@type"] == obj_type and check_required_props(
                        class_path, object_):
                    # If Item in request's JSON is a valid object ie. @type is a key in object_
                    # and the right Item type is being added to the collection
                    try:
                        # Insert object and return location in Header
                        object_id = crud.insert(object_=object_, session=get_session())
                        headers_ = [
                            {"Location": "{}{}/{}/{}".format(
                                get_hydrus_server_url(), get_api_name(), path, object_id)}]
                        status_description = "Object with ID {} successfully added".format(
                            object_id)
                        status = HydraStatus(code=201, title="Object successfully added",
                                             desc=status_description)
                        return set_response_headers(
                            jsonify(status.generate()), headers=headers_, status_code=status.code)
                    except (ClassNotFound, InstanceExists, PropertyNotFound) as e:
                        error = e.get_HTTP()
                        return set_response_headers(jsonify(error.generate()),
                                                    status_code=error.code)

                else:
                    error = HydraError(code=400, title="Data is not valid")
                    return set_response_headers(jsonify(error.generate()), status_code=error.code)

            elif path in get_doc().parsed_classes and "{}Collection".format(path) not in get_doc(
            ).collections:
                # If path is in parsed_classes but is not a collection
                obj_type = getType(path, "PUT")
                link_props, link_type_check = get_link_props(path, object_)
                if object_["@type"] == obj_type and validObject(object_) and check_required_props(
                        path, object_) and link_type_check:
                    try:
                        object_id = crud.insert(object_=object_, link_props=link_props,
                                                session=get_session())
                        headers_ = [{"Location": "{}{}/{}/".format(
                                get_hydrus_server_url(), get_api_name(), path)}]
                        status = HydraStatus(code=201, title="Object successfully added")
                        return set_response_headers(
                            jsonify(status.generate()), headers=headers_, status_code=status.code)
                    except (ClassNotFound, InstanceExists, PropertyNotFound) as e:
                        error = e.get_HTTP()
                        return set_response_headers(jsonify(error.generate()),
                                                    status_code=error.code)

                else:
                    error = HydraError(code=400, title="Data is not valid")
                    return set_response_headers(jsonify(error.generate()), status_code=error.code)

        abort(endpoint_['status'])

    def post(self, path: str) -> Response:
        """
        Method executed for POST requests.
        Used to update a non-collection class.
        :param path - Path for Item type ( Specified in APIDoc @id)
        """
        auth_response = check_authentication_response()
        if isinstance(auth_response, Response):
            return auth_response

        endpoint_ = checkEndpoint("POST", path)
        if endpoint_['method']:
            object_ = json.loads(request.data.decode('utf-8'))
            if path in get_doc().parsed_classes and "{}Collection".format(path) not in get_doc(
            ).collections:
                obj_type = getType(path, "POST")
                link_props, link_type_check = get_link_props(path, object_)
                if check_writeable_props(path, object_):
                    if object_["@type"] == obj_type and check_required_props(
                            path, object_) and validObject(object_) and link_type_check:
                        try:
                            crud.update_single(
                                object_=object_,
                                session=get_session(),
                                api_name=get_api_name(),
                                link_props=link_props,
                                path=path)
                            method = "POST"
                            resource_url = "{}{}/{}".format(
                                get_hydrus_server_url(), get_api_name(), path)
                            last_job_id = crud.get_last_modification_job_id(session=get_session())
                            new_job_id = crud.insert_modification_record(method, resource_url,
                                                                         session=get_session())
                            send_sync_update(socketio=socketio, new_job_id=new_job_id,
                                             last_job_id=last_job_id, method=method,
                                             resource_url=resource_url)
                            headers_ = [
                                {"Location": "{}/{}/".format(
                                    get_hydrus_server_url(), get_api_name(), path)}]
                            status = HydraStatus(code=200, title="Object successfully added")
                            return set_response_headers(
                                jsonify(status.generate()), headers=headers_)
                        except (ClassNotFound, InstanceNotFound,
                                InstanceExists, PropertyNotFound) as e:
                            error = e.get_HTTP()
                            return set_response_headers(
                                jsonify(error.generate()), status_code=error.code)

                    error = HydraError(code=400, title="Data is not valid")
                    return set_response_headers(jsonify(error.generate()), status_code=error.code)
                else:
                    abort(405)

        abort(endpoint_['status'])

    def delete(self, path: str) -> Response:
        """
        Method executed for DELETE requests.
        Used to delete a non-collection class.
        :param path - Path for Item ( Specified in APIDoc @id)
        """
        auth_response = check_authentication_response()
        if isinstance(auth_response, Response):
            return auth_response

        endpoint_ = checkEndpoint("DELETE", path)
        if not endpoint_['method']:
            abort(endpoint_['status'])
        elif path in get_doc().parsed_classes and "{}Collection".format(
                path) not in get_doc().collections:
            # No Delete Operation for collections
            try:
                class_type = get_doc().parsed_classes[path]['class'].title
                crud.delete_single(class_type, session=get_session())
                method = "DELETE"
                resource_url = "{}{}/{}".format(
                    get_hydrus_server_url(), get_api_name(), path)
                last_job_id = crud.get_last_modification_job_id(session=get_session())
                new_job_id = crud.insert_modification_record(method, resource_url,
                                                             session=get_session())
                send_sync_update(socketio=socketio, new_job_id=new_job_id,
                                 last_job_id=last_job_id, method=method,
                                 resource_url=resource_url)
                status = HydraStatus(code=200, title="Object successfully added")
                return set_response_headers(jsonify(status.generate()))
            except (ClassNotFound, InstanceNotFound) as e:
                error = e.get_HTTP()
                return set_response_headers(
                    jsonify(error.generate()), status_code=error.code)


class Items(Resource):

    def put(self, path, int_list="") -> Response:
        """
        To insert multiple objects into the database
        :param path: endpoint
        :param int_list: Optional String containing ',' separated ID's
        :return:
        """
        auth_response = check_authentication_response()
        if isinstance(auth_response, Response):
            return auth_response

        endpoint_ = checkEndpoint("PUT", path)
        if endpoint_['method']:
            # If endpoint and PUT method is supported in the API
            object_ = json.loads(request.data.decode('utf-8'))
            object_ = object_["data"]
            if path in get_doc().collections:
                # If collection name in document's collections
                collection = get_doc().collections[path]["collection"]
                # title of HydraClass object corresponding to collection
                obj_type = collection.class_.title
                # get path of the collection class
                class_path = collection.class_.path
                incomplete_objects = list()
                for obj in object_:
                    if not check_required_props(class_path, obj):
                        incomplete_objects.append(obj)
                        object_.remove(obj)
                link_props_list, link_type_check = get_link_props_for_multiple_objects(class_path,
                                                                                       object_)
                if validObjectList(object_) and link_type_check:
                    type_result = type_match(object_, obj_type)
                    # If Item in request's JSON is a valid object
                    # ie. @type is one of the keys in object_
                    if type_result:
                        # If the right Item type is being added to the
                        # collection
                        try:
                            # Insert object and return location in Header
                            object_id = crud.insert_multiple(
                                objects_=object_, session=get_session(), id_=int_list,
                                link_props_list=link_props_list)
                            headers_ = [{"Location": "{}{}/{}/{}".format(
                                    get_hydrus_server_url(), get_api_name(), path, object_id)}]
                            if len(incomplete_objects) > 0:
                                status = HydraStatus(code=202,
                                                     title="Object(s) missing required property")
                                response = status.generate()
                                response["objects"] = incomplete_objects
                                return set_response_headers(
                                    jsonify(response), headers=headers_, status_code=status.code)
                            else:
                                status_description = "Objects with ID {} successfully added".format(
                                    object_id)
                                status = HydraStatus(code=201, title="Objects successfully added",
                                                     desc=status_description)
                                return set_response_headers(
                                    jsonify(status.generate()), headers=headers_,
                                    status_code=status.code)
                        except (ClassNotFound, InstanceExists, PropertyNotFound) as e:
                            error = e.get_HTTP()
                            return set_response_headers(jsonify(error.generate()),
                                                        status_code=error.code)

                error = HydraError(code=400, title="Data is not valid")
                return set_response_headers(jsonify(error.generate()), status_code=error.code)

        abort(endpoint_['status'])

    def delete(self, path, int_list):
        """
        To delete multiple objects
        :param path: endpoints
        :param int_list: Optional String containing ',' separated ID's
        :return:
        """
        auth_response = check_authentication_response()
        if isinstance(auth_response, Response):
            return auth_response
        class_type = get_doc().collections[path]["collection"].class_.title

        if checkClassOp(class_type, "DELETE"):
            # Check if class_type supports PUT operation
            try:
                # Delete the Item with ID == id_
                crud.delete_multiple(int_list, class_type, session=get_session())
                method = "DELETE"
                path_url = "{}{}/{}".format(
                    get_hydrus_server_url(), get_api_name(), path)
                last_job_id = crud.get_last_modification_job_id(session=get_session())
                id_list = int_list.split(',')
                for item in id_list:
                    resource_url = path_url + item
                    new_job_id = crud.insert_modification_record(method, resource_url,
                                                                 session=get_session())
                    send_sync_update(socketio=socketio, new_job_id=new_job_id,
                                     last_job_id=last_job_id, method=method,
                                     resource_url=resource_url)
                    last_job_id = new_job_id
                status_description = "Objects with ID {} successfully deleted".format(
                    id_list)
                status = HydraStatus(code=200, title="Objects successfully deleted",
                                     desc=status_description)
                return set_response_headers(jsonify(status.generate()))

            except (ClassNotFound, InstanceNotFound) as e:
                error = e.get_HTTP()
                return set_response_headers(jsonify(error.generate()), status_code=error.code)

        abort(405)


class Contexts(Resource):
    """Dynamically genereated contexts."""

    def get(self, category: str) -> Response:
        """Return the context for the specified class."""
        # Check for collection
        if category in get_doc().collections:
            # type: Union[Dict[str,Any],Dict[int,str]]
            response = {"@context": get_doc().collections[category]["context"].generate()}
            return set_response_headers(jsonify(response))
        # Check for non collection class
        elif category in get_doc().parsed_classes:
            response = {"@context": get_doc().parsed_classes[category]["context"].generate()}
            return set_response_headers(jsonify(response))
        else:
            error = HydraError(code=404, title="NOT FOUND", desc="Context not found")
            return set_response_headers(jsonify(error.generate()), status_code=error.code)