"""Basic CRUD operations for the server."""

from sqlalchemy.orm import with_polymorphic
from sqlalchemy import exists
from sqlalchemy.orm.exc import NoResultFound
from hydrus.data.db_models import (Graph, BaseProperty, RDFClass, Instance,
                                   Terminal, GraphIAC, GraphIIT, GraphIII)

from hydrus.data.exceptions import (ClassNotFound, InstanceExists, PropertyNotFound,
                                    NotInstanceProperty, NotAbstractProperty,
                                    InstanceNotFound)
from sqlalchemy.orm.scoping import scoped_session
from typing import Dict, Optional, Any, List

triples = with_polymorphic(Graph, '*')
properties = with_polymorphic(BaseProperty, "*")


def get(id_: int, type_: str, api_name: str, session: scoped_session, recursive: bool = False) -> Dict[str, str]:
    """Retrieve an Instance with given ID from the database [GET]."""
    object_template = {
        "@type": "",
    } # type: Dict[str, Any]
    try:
        rdf_class = session.query(RDFClass).filter(
            RDFClass.name == type_).one()
    except NoResultFound:
        raise ClassNotFound(type_=type_)

    try:
        instance = session.query(Instance).filter(
            Instance.id == id_, Instance.type_ == rdf_class.id).one()
    except NoResultFound:
        raise InstanceNotFound(type_=rdf_class.name, id_=id_)

    data_IAC = session.query(triples).filter(triples.GraphIAC.subject == id_).all()

    data_III = session.query(triples).filter(triples.GraphIII.subject == id_).all()

    data_IIT = session.query(triples).filter(triples.GraphIIT.subject == id_).all()

    for data in data_IAC:
        prop_name = session.query(properties).filter(properties.id == data.predicate).one().name
        class_name = session.query(RDFClass).filter(RDFClass.id == data.object_).one().name
        object_template[prop_name] = class_name

    for data in data_III:
        prop_name = session.query(properties).filter(properties.id == data.predicate).one().name
        instance = session.query(Instance).filter(Instance.id == data.object_).one()
        # Get class name for instance object
        inst_class_name = session.query(RDFClass).filter(RDFClass.id == instance.type_).one().name
        # Recursive call should get the instance needed
        object_ = get(id_=instance.id, type_=inst_class_name, session=session, recursive=True, api_name=api_name)
        object_template[prop_name] = object_

    for data in data_IIT:
        prop_name = session.query(properties).filter(properties.id == data.predicate).one().name
        terminal = session.query(Terminal).filter(Terminal.id == data.object_).one()
        try:
            object_template[prop_name] = terminal.value
        except:
            # If terminal is none
            object_template[prop_name] = ""
    object_template["@type"] = rdf_class.name
    if not recursive:
        object_template["@id"] = "/"+api_name+"/"+type_+"Collection/"+str(id_)

    return object_template


def insert(object_: Dict[str, Any], session: scoped_session , id_: Optional[int] =None) -> int:
    """Insert an object to database [POST] and returns the inserted object."""
    rdf_class = None
    instance = None

    # Check for class in the begging
    try:
        rdf_class = session.query(RDFClass).filter(
            RDFClass.name == object_["@type"]).one()
    except NoResultFound:
        raise ClassNotFound(type_=object_["@type"])

    if id_ is not None:
        if session.query(exists().where(Instance.id == id_)).scalar():
            raise InstanceExists(type_=rdf_class.name, id_=id_)
        else:
            instance = Instance(id=id_, type_=rdf_class.id)
    else:
        instance = Instance(type_=rdf_class.id)
    session.add(instance)
    session.flush()

    for prop_name in object_:
        if prop_name not in ["@type", "@context"]:
            try:
                property_ = session.query(properties).filter(
                    properties.name == prop_name).one()
            except NoResultFound:
                # Adds new Property
                session.close()
                raise PropertyNotFound(type_=prop_name)

            # For insertion in III
            if type(object_[prop_name]) == dict:
                instance_id = insert(object_[prop_name], session=session)
                instance_object = session.query(Instance).filter(Instance.id == instance_id).one()

                if property_.type_ == "PROPERTY" or property_.type_ == "INSTANCE":
                    property_.type_ = "INSTANCE"
                    session.add(property_)
                    triple = GraphIII(subject=instance.id, predicate=property_.id, object_=instance_object.id)
                    session.add(triple)
                else:
                    session.close()
                    raise NotInstanceProperty(type_=prop_name)

            # For insertion in IAC
            elif session.query(exists().where(RDFClass.name == str(object_[prop_name]))).scalar():
                if property_.type_ == "PROPERTY" or property_.type_ == "ABSTRACT":
                    property_.type_ = "ABSTRACT"
                    session.add(property_)
                    class_ = session.query(RDFClass).filter(RDFClass.name == object_[prop_name]).one()
                    triple = GraphIAC(subject=instance.id, predicate=property_.id, object_=class_.id)
                    session.add(triple)
                else:
                    session.close()
                    raise NotAbstractProperty(type_=prop_name)

            # For insertion in IIT
            else:
                terminal = Terminal(value=object_[prop_name])
                session.add(terminal)
                session.flush()     # Assigns ID without committing

                if property_.type_ == "PROPERTY" or property_.type_ == "INSTANCE":
                    property_.type_ = "INSTANCE"
                    session.add(property_)
                    triple = GraphIIT(subject=instance.id, predicate=property_.id, object_=terminal.id)
                    # Add things directly to session, if anything fails whole transaction is aborted
                    session.add(triple)
                else:
                    session.close()
                    raise NotInstanceProperty(type_=prop_name)

    session.commit()
    return instance.id


def delete(id_:int, type_:str, session:scoped_session) -> None:
    """Delete an Instance and all its relations from DB given id [DELETE]."""
    try:
        rdf_class = session.query(RDFClass).filter(RDFClass.name == type_).one()
    except NoResultFound:
        print(type_)
        raise ClassNotFound(type_=type_)
    try:
        instance = session.query(Instance).filter(
            Instance.id == id_ and type_ == rdf_class.id).one()
    except NoResultFound:
        raise InstanceNotFound(type_=rdf_class.name, id_=id_)

    data_IIT = session.query(triples).filter(triples.GraphIIT.subject == id_).all()
    data_IAC = session.query(triples).filter(triples.GraphIAC.subject == id_).all()
    data_III = session.query(triples).filter(triples.GraphIII.subject == id_).all()

    data = data_III + data_IIT + data_IAC
    for item in data:
        session.delete(item)

    for data in data_IIT:
        terminal = session.query(Terminal).filter(Terminal.id == data.object_).one()
        session.delete(terminal)

    for data in data_III:
        III_instance = session.query(Instance).filter(Instance.id == data.object_).one()
        III_instance_type = session.query(RDFClass).filter(RDFClass.id == III_instance.type_).one()
        # Get the III object type_
        delete(III_instance.id, III_instance_type.name, session=session)

    session.delete(instance)
    session.commit()


def update(id_: int, type_: str, object_: Dict[str,str], session: scoped_session, api_name:str) -> int:
    """Update an object properties based on the given object [PUT]."""
    # Keep the object as fail safe
    instance = get(id_=id_, type_=type_, session=session, api_name=api_name)
    instance.pop("@id")

    # Delete the old object
    delete(id_=id_, type_=type_, session=session)
    # Try inserting new object
    try:
        insert(object_=object_, id_=id_, session=session)
    except Exception as e:
        # Put old object back
        insert(object_=instance, id_=id_, session=session)
        raise e

    get(id_=id_, type_=type_, session=session, api_name=api_name)
    return id_


def get_collection(API_NAME: str, type_: str, session: scoped_session) -> Dict[str, Any]:
    """Retrieve a type of collection from the database."""
    collection_template = {
        "@id": "/"+API_NAME+"/" + type_ + "Collection/",
        "@context": None,
        "@type": type_ + "Collection",
        "members": list()
    } # type: Dict[str, Any]
    try:
        rdf_class = session.query(RDFClass).filter(
            RDFClass.name == type_).one()
    except NoResultFound:
        raise ClassNotFound(type_=type_)

    try:
        instances = session.query(Instance).filter(Instance.type_ == rdf_class.id).all()
    except NoResultFound:
        instances = list()

    for instance_ in instances:
        object_template = {
            "@id": "/"+API_NAME+"/" + type_ + "Collection/" + str(instance_.id),
            "@type": type_
        }
        collection_template["members"].append(object_template)
    return collection_template


def get_single(type_: str, api_name: str, session: scoped_session) -> Dict[str, Any]:
    """Get instance of classes with single objects."""
    try:
        rdf_class = session.query(RDFClass).filter(RDFClass.name == type_).one()
    except NoResultFound:
        raise ClassNotFound(type_=type_)

    try:
        instance = session.query(Instance).filter(Instance.type_ == rdf_class.id).all()[-1]
    except (NoResultFound, IndexError, ValueError):
        raise InstanceNotFound(type_=rdf_class.name)
    object_ = get(instance.id, rdf_class.name, session=session, api_name=api_name)

    object_["@id"] = "/"+api_name+"/"+type_

    return object_


def insert_single(object_: Dict[str, Any], session: scoped_session) -> Any:
    """Insert instance of classes with single objects."""
    try:
        rdf_class = session.query(RDFClass).filter(RDFClass.name == object_["@type"]).one()
    except NoResultFound:
        raise ClassNotFound(type_=object_["@type"])

    try:
        session.query(Instance).filter(Instance.type_ == rdf_class.id).all()[-1]
    except (NoResultFound, IndexError, ValueError):
        return insert(object_, session=session)

    raise InstanceExists(type_=rdf_class.name)


def update_single(object_: Dict[str, Any], session: scoped_session, api_name: str) -> int:
    """Update instance of classes with single objects."""
    try:
        rdf_class = session.query(RDFClass).filter(RDFClass.name == object_["@type"]).one()
    except NoResultFound:
        print("Class not found in update_single")
        raise ClassNotFound(type_=object_["@type"])

    try:
        instance = session.query(Instance).filter(Instance.type_ == rdf_class.id).all()[-1]
    except (NoResultFound, IndexError, ValueError):
        raise InstanceNotFound(type_=rdf_class.name)

    return update(id_=instance.id, type_=object_["@type"], object_=object_, session=session, api_name=api_name)


def delete_single(type_: str, session: scoped_session) -> None:
    """Delete instance of classes with single objects."""
    try:
        rdf_class = session.query(RDFClass).filter(RDFClass.name == type_).one()
    except NoResultFound:
        raise ClassNotFound(type_=type_)

    try:
        instance = session.query(Instance).filter(Instance.type_ == rdf_class.id).all()[-1]
    except (NoResultFound, IndexError, ValueError):
        raise InstanceNotFound(type_=rdf_class.name)

    return delete(instance.id, type_, session=session)
"""Basic CRUD operations for the server."""

from sqlalchemy.orm import with_polymorphic
from sqlalchemy import exists
from sqlalchemy.orm.exc import NoResultFound
from hydrus.data.db_models import (Graph, BaseProperty, RDFClass, Instance,
                                   Terminal, GraphIAC, GraphIIT, GraphIII)

from hydrus.data.exceptions import (ClassNotFound, InstanceExists, PropertyNotFound,
                                    NotInstanceProperty, NotAbstractProperty,
                                    InstanceNotFound)
from sqlalchemy.orm.scoping import scoped_session
from typing import Dict, Optional, Any, List

triples = with_polymorphic(Graph, '*')
properties = with_polymorphic(BaseProperty, "*")


def get(id_: int, type_: str, api_name: str, session: scoped_session, recursive: bool = False) -> Dict[str, str]:
    """Retrieve an Instance with given ID from the database [GET]."""
    object_template = {
        "@type": "",
    } # type: Dict[str, Any]
    try:
        rdf_class = session.query(RDFClass).filter(
            RDFClass.name == type_).one()
    except NoResultFound:
        raise ClassNotFound(type_=type_)

    try:
        instance = session.query(Instance).filter(
            Instance.id == id_, Instance.type_ == rdf_class.id).one()
    except NoResultFound:
        raise InstanceNotFound(type_=rdf_class.name, id_=id_)

    data_IAC = session.query(triples).filter(triples.GraphIAC.subject == id_).all()

    data_III = session.query(triples).filter(triples.GraphIII.subject == id_).all()

    data_IIT = session.query(triples).filter(triples.GraphIIT.subject == id_).all()

    for data in data_IAC:
        prop_name = session.query(properties).filter(properties.id == data.predicate).one().name
        class_name = session.query(RDFClass).filter(RDFClass.id == data.object_).one().name
        object_template[prop_name] = class_name

    for data in data_III:
        prop_name = session.query(properties).filter(properties.id == data.predicate).one().name
        instance = session.query(Instance).filter(Instance.id == data.object_).one()
        # Get class name for instance object
        inst_class_name = session.query(RDFClass).filter(RDFClass.id == instance.type_).one().name
        # Recursive call should get the instance needed
        object_ = get(id_=instance.id, type_=inst_class_name, session=session, recursive=True, api_name=api_name)
        object_template[prop_name] = object_

    for data in data_IIT:
        prop_name = session.query(properties).filter(properties.id == data.predicate).one().name
        terminal = session.query(Terminal).filter(Terminal.id == data.object_).one()
        try:
            object_template[prop_name] = terminal.value
        except:
            # If terminal is none
            object_template[prop_name] = ""
    object_template["@type"] = rdf_class.name
    if not recursive:
        object_template["@id"] = "/"+api_name+"/"+type_+"Collection/"+str(id_)

    return object_template


def insert(object_: Dict[str, Any], session: scoped_session , id_: Optional[int] =None) -> int:
    """Insert an object to database [POST] and returns the inserted object."""
    rdf_class = None
    instance = None

    # Check for class in the begging
    try:
        rdf_class = session.query(RDFClass).filter(
            RDFClass.name == object_["@type"]).one()
    except NoResultFound:
        raise ClassNotFound(type_=object_["@type"])

    if id_ is not None:
        if session.query(exists().where(Instance.id == id_)).scalar():
            raise InstanceExists(type_=rdf_class.name, id_=id_)
        else:
            instance = Instance(id=id_, type_=rdf_class.id)
    else:
        instance = Instance(type_=rdf_class.id)
    session.add(instance)
    session.flush()

    for prop_name in object_:
        if prop_name not in ["@type", "@context"]:
            try:
                property_ = session.query(properties).filter(
                    properties.name == prop_name).one()
            except NoResultFound:
                # Adds new Property
                session.close()
                raise PropertyNotFound(type_=prop_name)

            # For insertion in III
            if type(object_[prop_name]) == dict:
                instance_id = insert(object_[prop_name], session=session)
                instance_object = session.query(Instance).filter(Instance.id == instance_id).one()

                if property_.type_ == "PROPERTY" or property_.type_ == "INSTANCE":
                    property_.type_ = "INSTANCE"
                    session.add(property_)
                    triple = GraphIII(subject=instance.id, predicate=property_.id, object_=instance_object.id)
                    session.add(triple)
                else:
                    session.close()
                    raise NotInstanceProperty(type_=prop_name)

            # For insertion in IAC
            elif session.query(exists().where(RDFClass.name == str(object_[prop_name]))).scalar():
                if property_.type_ == "PROPERTY" or property_.type_ == "ABSTRACT":
                    property_.type_ = "ABSTRACT"
                    session.add(property_)
                    class_ = session.query(RDFClass).filter(RDFClass.name == object_[prop_name]).one()
                    triple = GraphIAC(subject=instance.id, predicate=property_.id, object_=class_.id)
                    session.add(triple)
                else:
                    session.close()
                    raise NotAbstractProperty(type_=prop_name)

            # For insertion in IIT
            else:
                terminal = Terminal(value=object_[prop_name])
                session.add(terminal)
                session.flush()     # Assigns ID without committing

                if property_.type_ == "PROPERTY" or property_.type_ == "INSTANCE":
                    property_.type_ = "INSTANCE"
                    session.add(property_)
                    triple = GraphIIT(subject=instance.id, predicate=property_.id, object_=terminal.id)
                    # Add things directly to session, if anything fails whole transaction is aborted
                    session.add(triple)
                else:
                    session.close()
                    raise NotInstanceProperty(type_=prop_name)

    session.commit()
    return instance.id


def delete(id_:int, type_:str, session:scoped_session) -> None:
    """Delete an Instance and all its relations from DB given id [DELETE]."""
    try:
        rdf_class = session.query(RDFClass).filter(RDFClass.name == type_).one()
    except NoResultFound:
        print(type_)
        raise ClassNotFound(type_=type_)
    try:
        instance = session.query(Instance).filter(
            Instance.id == id_ and type_ == rdf_class.id).one()
    except NoResultFound:
        raise InstanceNotFound(type_=rdf_class.name, id_=id_)

    data_IIT = session.query(triples).filter(triples.GraphIIT.subject == id_).all()
    data_IAC = session.query(triples).filter(triples.GraphIAC.subject == id_).all()
    data_III = session.query(triples).filter(triples.GraphIII.subject == id_).all()

    data = data_III + data_IIT + data_IAC
    for item in data:
        session.delete(item)

    for data in data_IIT:
        terminal = session.query(Terminal).filter(Terminal.id == data.object_).one()
        session.delete(terminal)

    for data in data_III:
        III_instance = session.query(Instance).filter(Instance.id == data.object_).one()
        III_instance_type = session.query(RDFClass).filter(RDFClass.id == III_instance.type_).one()
        # Get the III object type_
        delete(III_instance.id, III_instance_type.name, session=session)

    session.delete(instance)
    session.commit()


def update(id_: int, type_: str, object_: Dict[str,str], session: scoped_session, api_name:str) -> int:
    """Update an object properties based on the given object [PUT]."""
    # Keep the object as fail safe
    instance = get(id_=id_, type_=type_, session=session, api_name=api_name)
    instance.pop("@id")

    # Delete the old object
    delete(id_=id_, type_=type_, session=session)
    # Try inserting new object
    try:
        insert(object_=object_, id_=id_, session=session)
    except Exception as e:
        # Put old object back
        insert(object_=instance, id_=id_, session=session)
        raise e

    get(id_=id_, type_=type_, session=session, api_name=api_name)
    return id_


def get_collection(API_NAME: str, type_: str, session: scoped_session) -> Dict[str, Any]:
    """Retrieve a type of collection from the database."""
    collection_template = {
        "@id": "/"+API_NAME+"/" + type_ + "Collection/",
        "@context": None,
        "@type": type_ + "Collection",
        "members": list()
    } # type: Dict[str, Any]
    try:
        rdf_class = session.query(RDFClass).filter(
            RDFClass.name == type_).one()
    except NoResultFound:
        raise ClassNotFound(type_=type_)

    try:
        instances = session.query(Instance).filter(Instance.type_ == rdf_class.id).all()
    except NoResultFound:
        instances = list()

    for instance_ in instances:
        object_template = {
            "@id": "/"+API_NAME+"/" + type_ + "Collection/" + str(instance_.id),
            "@type": type_
        }
        collection_template["members"].append(object_template)
    return collection_template


def get_single(type_: str, api_name: str, session: scoped_session) -> Dict[str, Any]:
    """Get instance of classes with single objects."""
    try:
        rdf_class = session.query(RDFClass).filter(RDFClass.name == type_).one()
    except NoResultFound:
        raise ClassNotFound(type_=type_)

    try:
        instance = session.query(Instance).filter(Instance.type_ == rdf_class.id).all()[-1]
    except (NoResultFound, IndexError, ValueError):
        raise InstanceNotFound(type_=rdf_class.name)
    object_ = get(instance.id, rdf_class.name, session=session, api_name=api_name)

    object_["@id"] = "/"+api_name+"/"+type_

    return object_


def insert_single(object_: Dict[str, Any], session: scoped_session) -> Any:
    """Insert instance of classes with single objects."""
    try:
        rdf_class = session.query(RDFClass).filter(RDFClass.name == object_["@type"]).one()
    except NoResultFound:
        raise ClassNotFound(type_=object_["@type"])

    try:
        session.query(Instance).filter(Instance.type_ == rdf_class.id).all()[-1]
    except (NoResultFound, IndexError, ValueError):
        return insert(object_, session=session)

    raise InstanceExists(type_=rdf_class.name)


def update_single(object_: Dict[str, Any], session: scoped_session, api_name: str) -> int:
    """Update instance of classes with single objects."""
    try:
        rdf_class = session.query(RDFClass).filter(RDFClass.name == object_["@type"]).one()
    except NoResultFound:
        print("Class not found in update_single")
        raise ClassNotFound(type_=object_["@type"])

    try:
        instance = session.query(Instance).filter(Instance.type_ == rdf_class.id).all()[-1]
    except (NoResultFound, IndexError, ValueError):
        raise InstanceNotFound(type_=rdf_class.name)

    return update(id_=instance.id, type_=object_["@type"], object_=object_, session=session, api_name=api_name)


def delete_single(type_: str, session: scoped_session) -> None:
    """Delete instance of classes with single objects."""
    try:
        rdf_class = session.query(RDFClass).filter(RDFClass.name == type_).one()
    except NoResultFound:
        raise ClassNotFound(type_=type_)

    try:
        instance = session.query(Instance).filter(Instance.type_ == rdf_class.id).all()[-1]
    except (NoResultFound, IndexError, ValueError):
        raise InstanceNotFound(type_=rdf_class.name)

    return delete(instance.id, type_, session=session)
Basic CRUD operations for the server. Retrieve an Instance with given ID from the database [GET]. # type: Dict[str, Any] # Get class name for instance object # Recursive call should get the instance needed # If terminal is none Insert an object to database [POST] and returns the inserted object. # Check for class in the begging # Adds new Property # For insertion in III # For insertion in IAC # For insertion in IIT # Assigns ID without committing # Add things directly to session, if anything fails whole transaction is aborted Delete an Instance and all its relations from DB given id [DELETE]. # Get the III object type_ Update an object properties based on the given object [PUT]. # Keep the object as fail safe # Delete the old object # Try inserting new object # Put old object back Retrieve a type of collection from the database. # type: Dict[str, Any] Get instance of classes with single objects. Insert instance of classes with single objects. Update instance of classes with single objects. Delete instance of classes with single objects.