import re
import textwrap
import logging
import sys

from dataclasses import dataclass
from pprint import pprint

import jwt

from datetime import datetime, timedelta
from enum import Flag, auto, Enum
from functools import partial
from typing import Type, Any, Self, cast, Dict

from oldaplib.src.datamodel import DataModel
from oldaplib.src.enums.action import Action
from oldaplib.src.enums.datapermissions import DataPermission
from oldaplib.src.enums.haspropertyattr import HasPropertyAttr
from oldaplib.src.enums.adminpermissions import AdminPermission
from oldaplib.src.enums.propertyclassattr import PropClassAttr
from oldaplib.src.enums.xsd_datatypes import XsdDatatypes
from oldaplib.src.hasproperty import HasProperty
from oldaplib.src.helpers.attributechange import AttributeChange
from oldaplib.src.helpers.context import Context
from oldaplib.src.helpers.convert2datatype import convert2datatype
from oldaplib.src.helpers.langstring import LangString
from oldaplib.src.helpers.observable_dict import ObservableDict
from oldaplib.src.helpers.observable_set import ObservableSet
from oldaplib.src.helpers.oldaperror import OldapErrorNotFound, OldapErrorValue, OldapErrorInconsistency, \
    OldapErrorNoPermission, OldapError, OldapErrorUpdateFailed, OldapErrorInUse, OldapErrorAlreadyExists, OldapErrorType
from oldaplib.src.helpers.query_processor import QueryProcessor
from oldaplib.src.iconnection import IConnection
from oldaplib.src.project import Project
from oldaplib.src.propertyclass import PropertyClass
from oldaplib.src.resourceclass import ResourceClass
from oldaplib.src.xsd.iri import Iri
from oldaplib.src.xsd.xsd import Xsd
from oldaplib.src.xsd.xsd_datetimestamp import Xsd_dateTimeStamp
from oldaplib.src.xsd.xsd_integer import Xsd_integer
from oldaplib.src.xsd.xsd_ncname import Xsd_NCName
from oldaplib.src.xsd.xsd_qname import Xsd_QName
from oldaplib.src.xsd.xsd_string import Xsd_string

logger = logging.getLogger(__name__)

ValueType = LangString | ObservableSet | Xsd | Dict[Xsd_QName, DataPermission] | ObservableDict

class SortDir(str, Enum):
    asc = "asc"
    desc = "desc"

@dataclass(frozen=True)
class SortBy:
    property: Xsd_QName
    dir: SortDir = SortDir.asc


def creator_or_max_perm_block(
    *,
    graph_data: str,      # e.g. f"{graph}:data"
    resource_iri: str,    # e.g. iri.toRdf
    user_iri: str,        # e.g. con.userIri.toRdf
    min_perm: str,        # e.g. DataPermission.DATA_VIEW.numeric.toRdf
    alias: str = "acc",   # avoid var collisions when used multiple times
    include_creator: bool = True
) -> str:
    """
    Helper class to determine the permissions and act accordingly
    """
    r = f"?{alias}_role"
    dp = f"?{alias}_dataperm"
    pv = f"?{alias}_permval"
    pv_sub = f"?{alias}_pv"
    maxp = f"?{alias}_maxPerm"

    creator_branch = textwrap.dedent(f"""
    {{
        GRAPH {graph_data} {{
            {resource_iri} oldap:createdBy {user_iri} .
        }}
    }}
    """).strip()

    perm_branch = textwrap.dedent(f"""
    {{
        {{
            SELECT (MAX(xsd:integer({pv_sub})) AS {maxp})
            WHERE {{
                GRAPH oldap:admin {{
                    {user_iri} oldap:hasRole {r} .
                }}
                GRAPH {graph_data} {{
                    {resource_iri} oldap:attachedToRole {r} .
                    <<{resource_iri} oldap:attachedToRole {r}>> oldap:hasDataPermission {dp} .
                }}
                GRAPH oldap:admin {{
                    {dp} oldap:permissionValue {pv_sub} .
                    FILTER({pv_sub} >= {min_perm})
                }}
            }}
        }}
        GRAPH oldap:admin {{
            {user_iri} oldap:hasRole {r} .
            {dp} oldap:permissionValue {pv} .
        }}
        GRAPH {graph_data} {{
            {resource_iri} oldap:attachedToRole {r} .
            <<{resource_iri} oldap:attachedToRole {r}>> oldap:hasDataPermission {dp} .
        }}
        FILTER({pv} >= {min_perm} && xsd:integer({pv}) = {maxp})
    }}
    """).strip()

    if include_creator:
        return f"{{\n{creator_branch}\n}}\nUNION\n{{\n{perm_branch}\n}}"
    return perm_branch


#@strict
class ResourceInstance:
    """
    Represents an instance of a resource in a system based on certain defined ontology.

    Provides functionality to initialize a resource, set its values, validate those values
    against defined properties, and handle changesets related to the resource instance.
    It integrates the resource's superclass properties into the instance and ensures the
    conformance of its data to defined constraints such as minimum/maximum counts, lengths,
    and allowable values or patterns.

    Basically it's used be the ResourceInstanceFactory to create new instances of a resource.
    The basic usage is as follows:

    ```python
        project = Project.read(con=self._connection, projectIri_SName='test')
        factory = ResourceInstanceFactory(con=self._connection, project=project)
        Book = factory.createObjectInstance('Book')
    ```

    **IMPORTANT NOTE: This class is not intended to be used directly, but rather through the
    `ResourceInstanceFactory` class!**

    In a first step the factory is instantiated with the project. Then using the `createObjectInstance` method
    creates a new Python class which will represent instances of the given resource class.

    :ivar iri: The unique identifier of the resource instance.
    :type iri: Iri
    :ivar values: A mapping of property IRIs to their corresponding values, which could
                  include `LangString` objects or `ObservableSet` of values.
    :type values: dict[Iri, LangString | ObservableSet]
    :ivar graph: The name of the graph attribute in which the instance will reside,
                 represented by a short name of the project.
    :type graph: Xsd_NCName
    :ivar changeset: A mapping to track all attributes changed within the instance,
                     where the key is the property IRI and the value defines the
                     change details.
    :type changeset: dict[Iri, AttributeChange]
    """
    _iri: Iri
    _values: dict[Xsd_QName, LangString | ObservableSet | DataPermission]
    _graph: Xsd_NCName
    _changeset: dict[Xsd_QName, AttributeChange]
    _attached_roles: ObservableDict

    __slots__ = ['_iri', '_values', '_graph', '_changeset', '_superclass_objs', '_con', '_attached_roles',
                 'project', 'name', 'factory', 'properties', 'superclass', 'user_default_roles']


    def __init__(self, *,
                 iri: Iri | None = None,
                 **kwargs):
        """
        Initializes an instance of the resource class, setting the optional IRI, processing
        superclasses, and validating property constraints. This initializer interprets
        the properties and superclasses associated with the instance and ensures
        conformance to rules such as `MIN_COUNT` and `MAX_COUNT` for properties.

        :param iri: An optional parameter specifying the IRI of the instance.
        :type iri: Iri | None
        :param kwargs: A dictionary of additional property values with property IRIs
            or fragments as keys. In case of unique fragments, the dictionary may be pass
            as named method arguments
        :type kwargs: dict or named method arguments
        """
        if iri and isinstance(iri, str):
            iri = Iri(Xsd_QName(self.project.projectShortName, iri))
        self._iri = Iri(iri, validate=True) if iri else Iri()
        self._values: dict[Xsd_QName, ValueType] = {}
        self._graph = self.project.projectShortName
        self._superclass_objs = {}
        self._changeset: dict[Xsd_QName, AttributeChange] = {}
        self._attached_roles = ObservableDict(on_change=self.__attachedToRole_cb)

        def set_values(propclass: dict[Xsd_QName, HasProperty]):
            for prop_iri, hasprop in propclass.items():
                if str(prop_iri) == 'oldap:attachedToRole':
                    if kwargs.get(str(prop_iri)) or kwargs.get(prop_iri.fragment):
                        #
                        # we have an attachedToRole property given in the constructor...
                        #
                        value = kwargs[str(prop_iri)] if kwargs.get(str(prop_iri)) else kwargs[prop_iri.fragment]
                        value = {Xsd_QName(role): dperm if isinstance(dperm, DataPermission) else DataPermission.from_string(dperm) for role, dperm in value.items()}
                        if not isinstance(value, dict):
                            raise OldapErrorValue(f'{self.name}: Property {prop_iri} with attachedToRole must be a dict')
                        self._attached_roles = ObservableDict(value, on_change=self.__attachedToRole_cb)
                        self._values[prop_iri] = ObservableSet({Xsd_QName(x, validate=True) for x in value.keys()},
                                                               notifier=self.notifier, notify_data=prop_iri)
                    else:
                        #
                        # we take the user default values...
                        #
                        self._values[prop_iri] = ObservableSet({Xsd_QName(x) for x in self.user_default_roles.keys()},
                                                               notifier=self.notifier, notify_data=prop_iri)
                        self._attached_roles = ObservableDict(self.user_default_roles, on_change=self.__attachedToRole_cb)

                    continue
                if kwargs.get(str(prop_iri)) or kwargs.get(prop_iri.fragment):
                    value = kwargs[str(prop_iri)] if kwargs.get(str(prop_iri)) else kwargs[prop_iri.fragment]
                    if isinstance(value, (list, tuple, set, LangString)):  # we may have multiple values...
                        if hasprop.prop.datatype == XsdDatatypes.langString:
                            self._values[prop_iri] = LangString(value,
                                                                notifier=self.notifier, notify_data=prop_iri)
                        else:
                            self._values[prop_iri] = ObservableSet({convert2datatype(x, hasprop.prop.datatype) for x in value},
                                                                   notifier=self.notifier, notify_data=prop_iri)
                    else:
                        try:
                            self._values[prop_iri] = ObservableSet({convert2datatype(value, hasprop.prop.datatype)})
                        except TypeError as err:
                            self._values[prop_iri] = convert2datatype(value, hasprop.prop.datatype)

            for prop_iri, hasprop in propclass.items():
                #
                # Validate cardinalities
                #
                if hasprop.get(HasPropertyAttr.MIN_COUNT):  # testing for MIN_COUNT conformance
                    if hasprop[HasPropertyAttr.MIN_COUNT] > 0 and not self._values.get(prop_iri):
                        raise OldapErrorValue(f'{self.name}: Property {prop_iri} with MIN_COUNT={hasprop[HasPropertyAttr.MIN_COUNT]} is missing')
                    elif isinstance(self._values[prop_iri], ObservableSet) and len(self._values[prop_iri]) < 1:
                        raise OldapErrorValue(f'{self.name}: Property {prop_iri} with MIN_COUNT={hasprop[HasPropertyAttr.MIN_COUNT]} is missing')
                if hasprop.get(HasPropertyAttr.MAX_COUNT) and hasprop[HasPropertyAttr.MAX_COUNT] > 0:  # testing for MAX_COUNT conformance
                    if isinstance(self._values.get(prop_iri), ObservableSet) and len(self._values[prop_iri]) > hasprop[HasPropertyAttr.MAX_COUNT]:
                        raise OldapErrorValue(f'{self.name}: Property {prop_iri} with MAX_COUNT={hasprop[HasPropertyAttr.MIN_COUNT]} has to many values (n={len(self._values[prop_iri])})')
                if self._values.get(prop_iri):
                    if isinstance(self._values[prop_iri], LangString):
                        self.validate_value(self._values[prop_iri], hasprop.prop)
                    else:
                        if isinstance(self._values[prop_iri], ObservableSet):
                            for val in self._values[prop_iri]:
                                self.validate_value(val, hasprop.prop)
                        else:
                            self.validate_value(self._values[prop_iri], hasprop.prop)

        def process_superclasses(superclass: dict[Xsd_QName, ResourceClass]):
            for sc_iri, sc in superclass.items():
                if not sc:
                    continue
                if sc.superclass:
                    process_superclasses(sc.superclass)
                if sc.owl_class_iri == Xsd_QName("oldap:Thing", validate=False):
                    timestamp = Xsd_dateTimeStamp()
                    if not self._values.get(Xsd_QName('oldap:createdBy', validate=False)):
                        self._values[Xsd_QName('oldap:createdBy', validate=False)] = ObservableSet({self._con.userIri})
                    if not self._values.get(Xsd_QName('oldap:creationDate', validate=False)):
                        self._values[Xsd_QName('oldap:creationDate', validate=False)] = ObservableSet({timestamp})
                    if not self._values.get(Xsd_QName('oldap:lastModifiedBy', validate=False)):
                        self._values[Xsd_QName('oldap:lastModifiedBy', validate=False)] = ObservableSet({self._con.userIri})
                    if not self._values.get(Xsd_QName('oldap:lastModificationDate', validate=False)):
                        self._values[Xsd_QName('oldap:lastModificationDate', validate=False)] = ObservableSet({timestamp})
                set_values(sc.properties)
                for iri, prop in sc.properties.items():
                    self.properties[iri] = prop

        if self.superclass:
            process_superclasses(self.superclass)

        set_values(self.properties)

        for propname in self.properties.keys():
            setattr(ResourceInstance, propname.fragment, property(
                partial(ResourceInstance.__get_value, attr=propname),
                partial(ResourceInstance.__set_value, attr=propname),
                partial(ResourceInstance.__del_value, attr=propname)))
        self.clear_changeset()

    def __setitem__(self, key: Xsd_QName, value: ValueType | Xsd | None):
        self.__set_value(value, key)

    def __getitem__(self, key: Xsd_QName) -> ValueType | Xsd | None:
        if self._values.get(key):
            return self.__get_value(key)
        else:
            raise OldapErrorValue(f'{self.name}: __getitem__: Property {key} does not exist.')

    def __delitem__(self, key: Xsd_QName):
        if self._values.get(key):
            self.__del_value(key)

    def get(self, key: Xsd_QName) -> ValueType | Xsd | None:
        if self._values.get(key):
            return self.__get_value(key)
        else:
            return None

    def validate_value(self, values: ValueType, property: PropertyClass):
        """
        Validates a set of values against a given property and its constraints. This function ensures
        that the input values conform to specific constraints defined by the property. The function
        verifies conformance to constraints such as language requirements, predefined inclusions,
        length limits, pattern matching, exclusivity, inclusivity, and relational comparisons
        (less than or less than or equals).

        :param values: A set of values to be validated.
        :param property: The property against which the values are being validated, containing
            the constraints that the values must meet.
        :return: None if the values successfully pass all validations.
        :raises OldapErrorInconsistency: Raised when the values do not conform to the defined property
            constraints due to inconsistencies or invalid data types.
        :raises OldapErrorValue: Raised when the values explicitly violate specific constraints
            such as inclusion constraints or invalid language values.
        """
        if property.get(PropClassAttr.LANGUAGE_IN):  # testing for LANGUAGE_IN conformance
            if not isinstance(values, LangString):
                raise OldapErrorInconsistency(f'Property {property.property_class_iri} with LANGUAGE_IN requires datatype rdf:langstring, got {type(values).__name__}.')
            for lang, dummy in values.items():
                if not lang in property[PropClassAttr.LANGUAGE_IN]:
                    raise OldapErrorValue(f'Property {property.property_class_iri} with LANGUAGE_IN={property[PropClassAttr.LANGUAGE_IN]} has invalid language "{lang.value}"')
        if property.get(PropClassAttr.UNIQUE_LANG):
            return  # TODO: LangString does not yet allow multiple entries of the same language...
        if property.get(PropClassAttr.IN):
            #for val in values:
            if property.datatype is None:  # no defined datatype, e.h.sh:IRI
                tmpinset = {str(x) for x in property[PropClassAttr.IN]}
                if isinstance(values, (list, tuple, set, ObservableSet)):
                    for val in values:
                        if not str(val) in tmpinset:
                            raise OldapErrorValue(
                                f'Property {property} with IN={property[PropClassAttr.IN]} has invalid value "{val}"')
                else:
                    if not str(values) in tmpinset:
                        raise OldapErrorValue(f'Property {property.property_class_iri} with IN={property[PropClassAttr.IN]} has invalid value "{values}"')
            else:
                if isinstance(values, (list, tuple, set, ObservableSet)):
                    for val in values:
                        if not val in property[PropClassAttr.IN]:
                            raise OldapErrorValue(
                                f'Property {property} with IN={property[PropClassAttr.IN]} has invalid value "{val}"')
                else:
                    if not values in property[PropClassAttr.IN]:
                        raise OldapErrorValue(f'Property {property.property_class_iri} with IN={property[PropClassAttr.IN]} has invalid value "{values}"')
        if property.get(PropClassAttr.MIN_LENGTH):
            for val in values:
                l = 0
                try:
                    l = len(val)
                except TypeError:
                    raise OldapErrorInconsistency(f'Property {property} with MIN_LENGTH={property[PropClassAttr.MIN_LENGTH]} has no length.')
                if l < property[PropClassAttr.MIN_LENGTH]:
                    raise OldapErrorInconsistency(f'Property {property} with MIN_LENGTH={property[PropClassAttr.MIN_LENGTH]} has length "{len(val)}".')
        if property.get(PropClassAttr.MAX_LENGTH):
            for val in values:
                l = 0
                try:
                    l = len(val)
                except TypeError:
                    raise OldapErrorInconsistency(f'Property {property} with MAX_LENGTH={property[PropClassAttr.MAX_LENGTH]} has no length.')
                if l > property[PropClassAttr.MAX_LENGTH]:
                    raise OldapErrorInconsistency(f'Property {property} with MIN_LENGTH={property[PropClassAttr.MAX_LENGTH]} has length "{len(val)}".')
        if property.get(PropClassAttr.PATTERN):
            for val in values:
                if not re.fullmatch(str(property[PropClassAttr.PATTERN]), str(val)):
                    raise OldapErrorInconsistency(f'Property {property} with PATTERN={property[PropClassAttr.PATTERN]} does not conform ({val}).')
        if property.get(PropClassAttr.MIN_EXCLUSIVE):
            for val in values:
                v: bool | None = None
                try:
                    v = val > property[PropClassAttr.MIN_EXCLUSIVE]
                except TypeError:
                    raise OldapErrorInconsistency(f'Property {property} with MIN_EXCLUSIVE={property[PropClassAttr.MIN_EXCLUSIVE]} cannot be compared to "{val}".')
                if not v:
                    raise OldapErrorInconsistency(f'Property {property} with MIN_EXCLUSIVE={property[PropClassAttr.MIN_EXCLUSIVE]} has invalid "{val}".')
        if property.get(PropClassAttr.MIN_INCLUSIVE):
            for val in values:
                v: bool | None = None
                try:
                    v = val >= property[PropClassAttr.MIN_INCLUSIVE]
                except TypeError:
                    raise OldapErrorInconsistency(f'Property {property} with MIN_EXCLUSIVE={property[PropClassAttr.MIN_INCLUSIVE]} cannot be compared to "{val}".')
                if not v:
                    raise OldapErrorInconsistency(f'Property {property} with MIN_EXCLUSIVE={property[PropClassAttr.MIN_INCLUSIVE]} has invalid "{val}".')
        if property.get(PropClassAttr.MAX_EXCLUSIVE):
            for val in values:
                v: bool | None = None
                try:
                    v = val < property[PropClassAttr.MAX_EXCLUSIVE]
                except TypeError:
                    raise OldapErrorInconsistency(f'Property {property} with MAX_EXCLUSIVE={property[PropClassAttr.MAX_EXCLUSIVE]} cannot be compared to "{val}".')
                if not v:
                    raise OldapErrorInconsistency(f'Property {property} with MAX_EXCLUSIVE={property[PropClassAttr.MAX_EXCLUSIVE]} has invalid "{val}".')
        if property.get(PropClassAttr.MAX_INCLUSIVE):
            for val in values:
                v: bool | None = None
                try:
                    v = val <= property[PropClassAttr.MAX_INCLUSIVE]
                except TypeError:
                    raise OldapErrorInconsistency(f'Property {property} with MAX_INCLUSIVE={property[PropClassAttr.MAX_INCLUSIVE]} cannot be compared to "{val}".')
                if not v:
                    raise OldapErrorInconsistency(f'Property {property} with MAX_INCLUSIVE={property[PropClassAttr.MAX_INCLUSIVE]} has invalid "{val}".')
        if property.get(PropClassAttr.LESS_THAN):
            other_values = self._values.get(property[PropClassAttr.LESS_THAN])
            if other_values is not None:
                b: bool | None = None
                try:
                    min_other_value = min(other_values)
                    max_value = max(values)
                    b = max_value < min_other_value
                except TypeError:
                    raise OldapErrorInconsistency(
                        f'Property {property} with LESS_THAN={property[PropClassAttr.LESS_THAN]} cannot be compared to "{values}".')
                if not b:
                    raise OldapErrorInconsistency(
                        f'Property {property} with LESS_THAN={property[PropClassAttr.LESS_THAN]} has invalid value: "{max_value}" NOT LESS_THAN "{min_other_value}".')
        if property.get(PropClassAttr.LESS_THAN_OR_EQUALS):
            other_values = self._values.get(property[PropClassAttr.LESS_THAN])
            if other_values is not None:
                b: bool | None = None
                try:
                    min_other_value = min(other_values)
                    max_value = max(values)
                    b = max_value <= min_other_value
                except TypeError:
                    raise OldapErrorInconsistency(
                        f'Property {property} with LESS_THAN={property[PropClassAttr.LESS_THAN_OR_EQUALS]} cannot be compared "{values}".')
                if not b:
                    raise OldapErrorInconsistency(
                        f'Property {property} with LESS_THAN={property[PropClassAttr.LESS_THAN_OR_EQUALS]} has invalid value: "{max_value}" NOT LESS_THAN "{min_other_value}".')

    def notifier(self, prop_iri: Xsd_QName):
        hasprop = self.properties[prop_iri]
        self.validate_value(self._values[prop_iri], hasprop)

        if hasprop.get(HasPropertyAttr.MIN_COUNT):  # testing for MIN_COUNT conformance
            n = len(self._values[prop_iri])
            if n < hasprop[HasPropertyAttr.MIN_COUNT]:
                self._values[prop_iri].undo()
                raise OldapErrorValue(
                    f'{self.name}: Property {prop_iri} with MIN_COUNT={hasprop[HasPropertyAttr.MIN_COUNT]} has not enough values (n={n}).')

        if hasprop.get(HasPropertyAttr.MAX_COUNT) and hasprop[HasPropertyAttr.MAX_COUNT] > 0:  # testing for MAX_COUNT conformance
            n = len(self._values[prop_iri])
            if n > hasprop[HasPropertyAttr.MAX_COUNT]:
                self._values[prop_iri].undo()
                raise OldapErrorValue(
                    f'{self.name}: Property {prop_iri} with MAX_COUNT={hasprop[HasPropertyAttr.MAX_COUNT]} has to many values (n={n}).')

        self._changeset[prop_iri] = AttributeChange(None, Action.MODIFY)

    def __attachedToRole_cb(self, old_value: ObservableDict):
        if self._changeset.get(Xsd_QName('oldap:attachedToRole')) is None:
            self._changeset[Xsd_QName('oldap:attachedToRole')] = AttributeChange(old_value, Action.MODIFY)


    def check_for_permissions(self, permission: AdminPermission) -> tuple[bool, str]:
        #
        # First we check if the logged-in user ("actor") has the permission to create a user for
        # the given project!
        #
        actor = self._con.userdata
        sysperms = actor.inProject.get(Iri('oldap:SystemProject', validate=False))
        if sysperms and AdminPermission.ADMIN_OLDAP in sysperms:
            #
            # user has root privileges!
            #
            return True, "OK – IS ROOT"
        else:
            perms = actor.inProject.get(self.project.projectIri)
            if permission in perms:
                return True, "OK",
            else:
                return False, f'Actor does not have {permission} in project "{self.project.projectShortName}".'

    def __get_value(self: Self, attr: Xsd_QName | str) -> Xsd | ValueType | None:
        #attr = Xsd_QName(prefix, fragment, validate=False)
        if not isinstance(attr, Xsd_QName):
            attr = Xsd_QName(attr)
        if attr == Xsd_QName('oldap:attachedToRole'):
            return self._attached_roles
        tmp = self._values.get(attr, None)
        if tmp is not None and str(attr) in {'oldap:createdBy', 'oldap:creationDate', 'oldap:lastModifiedBy', 'oldap:lastModificationDate'}:
            if len(tmp) != 1:
                raise OldapErrorValue(f'{self.name}: Property {attr} should have exactly one value, got {len(tmp)} values.')
            return next(iter(tmp))
        return tmp

    def __set_value(self: Self, value: ValueType | Xsd | None, attr: Xsd_QName | str) -> None:
        if not isinstance(attr, Xsd_QName):
            attr = Xsd_QName(attr)

        if attr == Xsd_QName('oldap:attachedToRole'):
            if value is None:
                self._changeset[attr] = AttributeChange(self._values.get(attr), Action.DELETE)
                del self._values[attr]
                self._attached_roles = ObservableDict(notifier=self.__attachedToRole_cb)
                return
            if not isinstance(value, dict):
                raise OldapErrorValue(f'{self.name}: Property {attr} requires a dict, got {type(value).__name__}.')
            for role_qname in value.keys():
                if not isinstance(role_qname, Xsd_QName):
                    raise OldapErrorValue(f'{self.name}: Property {attr} requires keys to be Xsd_QName, got {type(role_qname).__name__}.')
                if not isinstance(value[role_qname], DataPermission):
                    raise OldapErrorValue(f'{self.name}: Property {attr} requires values to be DataPermission, got {type(value[role_qname]).__name__}.')
            self._changeset[attr] = AttributeChange(self._values.get(attr), Action.REPLACE)
            self._values[attr] = ObservableSet({value.keys()}, notifier=self.notifier, notify_data=attr)
            self._attached_roles = ObservableDict(value, notifier=self.__attachedToRole_cb)
            return
        hasprop = self.properties.get(attr)

        #
        # Validate
        #
        if hasprop.get(HasPropertyAttr.MIN_COUNT):  # testing for MIN_COUNT conformance
            if hasprop[HasPropertyAttr.MIN_COUNT] > 0 and not value:
                raise OldapErrorValue(
                    f'{self.name}: Property {attr} with MIN_COUNT={hasprop[HasPropertyAttr.MIN_COUNT]} is missing')
            elif isinstance(value, (list, tuple, set, ObservableSet)) and len(value) < hasprop[HasPropertyAttr.MIN_COUNT]:
                raise OldapErrorValue(
                    f'{self.name}: Property {attr} with MIN_COUNT={hasprop[HasPropertyAttr.MIN_COUNT]} has not enough values')
        if hasprop.get(HasPropertyAttr.MAX_COUNT) and hasprop[HasPropertyAttr.MAX_COUNT] > 0:  # testing for MAX_COUNT conformance
            if isinstance(value, (list, tuple, set, ObservableSet)) and len(value) > hasprop[HasPropertyAttr.MAX_COUNT]:
                raise OldapErrorValue(
                    f'{self.name}: Property {attr} with MAX_COUNT={hasprop[HasPropertyAttr.MIN_COUNT]} has to many values (n={len(value)})')
        if value:
            if isinstance(value, LangString):
                self.validate_value(value, hasprop.prop)
            else:
                if isinstance(value, (list, tuple, set, ObservableSet)):
                    for val in value:
                        self.validate_value(val, hasprop.prop)
                else:
                    self.validate_value(value, hasprop.prop)

        if not value:
            self._changeset[attr] = AttributeChange(self._values.get(attr), Action.DELETE)
            del self._values[attr]
        elif isinstance(value, (list, tuple, set, LangString)):  # we may have multiple values...
            if self._values.get(attr):
                self._changeset[attr] = AttributeChange(self._values.get(attr), Action.REPLACE)
            else:
                self._changeset[attr] = AttributeChange(None, Action.CREATE)
            if hasprop.prop.datatype == XsdDatatypes.langString:
                self._values[attr] = LangString(value, notifier=self.notifier, notify_data=attr)
            else:
                self._values[attr] = ObservableSet({
                    convert2datatype(x, hasprop.prop.datatype) for x in value
                }, notifier=self.notifier, notify_data=attr)
        else:
            if self._values.get(attr):
                self._changeset[attr] = AttributeChange(self._values.get(attr), Action.REPLACE)
            else:
                self._changeset[attr] = AttributeChange(None, Action.CREATE)
            try:
                self._values[attr] = ObservableSet({convert2datatype(value, hasprop.prop.datatype)})
            except TypeError:
                self._values[attr] = convert2datatype(value, hasprop.prop.datatype)

    def __del_value(self: Self, attr: Xsd_QName | str) -> None:
        if not isinstance(attr, Xsd_QName):
            attr = Xsd_QName(attr)

        if attr == Xsd_QName('oldap:attachedToRole'):
            self._changeset[attr] = AttributeChange(self._values.get(attr), Action.DELETE)
            del self._values[attr]
            self._attached_roles = ObservableDict(notifier=self.__attachedToRole_cb)
            return
        hasprop = self.properties.get(attr)

        if hasprop.get(HasPropertyAttr.MIN_COUNT):  # testing for MIN_COUNT conformance
            if hasprop[HasPropertyAttr.MIN_COUNT] > 0:
                raise OldapErrorValue(f'{self.name}: Property {attr} with MIN_COUNT={hasprop[HasPropertyAttr.MIN_COUNT]} cannot be deleted.')

        self._changeset[attr] = AttributeChange(self._values.get(attr), Action.DELETE)
        del self._values[attr]

    @property
    def iri(self) -> Iri:
        return self._iri

    @property
    def changeset(self) -> dict[Xsd_QName, AttributeChange]:
        return self._changeset

    @property
    def attachedToRoleAnnotation(self) -> ObservableDict:
        return self._attached_roles

    def clear_changeset(self) -> None:
        for item in self._values:
            if hasattr(self._values[item], 'clear_changeset'):
                self._values[item].clear_changeset()
        self._changeset = {}


    def get_data_permission(self, permission: DataPermission) -> bool:
        context = Context(name=self._con.context_name)
        permission_query = context.sparql_context
        permission_query += textwrap.dedent(f'''
        ASK {{
            {{
                GRAPH {self._graph}:data {{
                    {self._iri.toRdf} oldap:createdBy {self._con.userIri.toRdf} .
                }}
            }}
            UNION
            {{
                GRAPH oldap:admin {{
                    {self._con.userIri.toRdf} oldap:hasRole ?role .
                    ?dataperm oldap:permissionValue ?permval .
                    FILTER(?permval >= {permission.numeric.toRdf})
                }}
                GRAPH {self._graph}:data {{
                    {self._iri.toRdf} oldap:attachedToRole ?role .
                    <<{self._iri.toRdf} oldap:attachedToRole ?role>> oldap:hasDataPermission ?dataperm .
                }}
            }}
        }}''')
        if self._con.in_transaction():
            result = self._con.transaction_query(permission_query)
        else:
            result = self._con.query(permission_query)
        return result['boolean']

    def create(self, indent: int = 0, indent_inc: int = 4) -> str:
        """
        Generates an RDF/SPARQL INSERT DATA query for creating a resource with associated
        metadata and verifies permissions before execution.

        This method first checks for the necessary permissions to create a resource.
        It assigns metadata such as the creation and modification information if
        the resource name matches specific criteria. It ensures that no resource
        with the same IRI exists before proceeding with the resource creation.
        If a conflict is detected, an error is raised. The method finalizes the
        creation of the resource by executing a SPARQL query in a transactional context.

        :param indent: The current level of indentation (default is 0).
        :type indent: int
        :param indent_inc: The incremental spaces per indentation level (default is 4).
        :type indent_inc: int
        :return: A string representing the RDF/SPARQL query for creation.
        :rtype: str
        :raises OldapErrorNoPermission: If the user does not have the required permissions to create the resource.
        :raises OldapErrorAlreadyExists: If a resource with the same IRI already exists.
        :raises OldapError: For errors encountered during the SPARQL transaction execution.
        """
        result, message = self.check_for_permissions(AdminPermission.ADMIN_CREATE)
        if not result:
            raise OldapErrorNoPermission(message)

        timestamp = Xsd_dateTimeStamp()
        if self.name in ("Thing", "oldap:Thing"):
            self._values[Xsd_QName('oldap:createdBy', validate=False)] = ObservableSet({self._con.userIri})
            self._values[Xsd_QName('oldap:creationDate', validate=False)] = ObservableSet({timestamp})
            self._values[Xsd_QName('oldap:lastModifiedBy', validate=False)] = ObservableSet({self._con.userIri})
            self._values[Xsd_QName('oldap:lastModificationDate', validate=False)] = ObservableSet({timestamp})

        indent: int = 0
        indent_inc: int = 4

        blank = ''
        context = Context(name=self._con.context_name)

        #
        # Test if a resource with the same IRI already exists!
        #
        sparql0 = context.sparql_context
        sparql0 += f'ASK {{ GRAPH {self._graph}:data {{ {self._iri.toRdf} ?p ?o }} }}'

        sparql = context.sparql_context
        sparql += f'{blank:{indent * indent_inc}}INSERT DATA {{'
        sparql += f'\n{blank:{(indent + 1) * indent_inc}}GRAPH {self._graph}:data {{'

        sparql += f'\n{blank:{(indent + 2) * indent_inc}}{self._iri.toRdf} a {self.name}'
        for prop_iri, values in self._values.items():
            if self.properties.get(prop_iri) and self.properties[prop_iri].prop.datatype == XsdDatatypes.QName:
                qnames = {f'"{x}"^^xsd:QName' for x in values}
                qnames_rdf = ', '.join(qnames)
                sparql += f' ;\n{blank:{(indent + 3) * indent_inc}}{prop_iri.toRdf} {qnames_rdf}'
            else:
                sparql += f' ;\n{blank:{(indent + 3) * indent_inc}}{prop_iri.toRdf} {values.toRdf}'
        for role, dperm in self._attached_roles.items():
            sparql += f' .\n{blank:{(indent + 2) * indent_inc}}<<{self._iri.toRdf} oldap:attachedToRole {role.toRdf}>> oldap:hasDataPermission {dperm.toRdf}'

        sparql += f' .\n{blank:{(indent + 1) * indent_inc}}}}\n'
        sparql += f'{blank:{indent * indent_inc}}}}\n'
        self._con.transaction_start()
        try:
            result = self._con.transaction_query(sparql0)
            if result['boolean']:
                self._con.transaction_abort()
                raise OldapErrorAlreadyExists(f'Resource with IRI {self._iri} already exists.')
            self._con.transaction_update(sparql)
        except OldapError as e:
            print(sparql)
            self._con.transaction_abort()
            raise
        self._con.transaction_commit()


    @classmethod
    def read(cls,
             con: IConnection,
             iri: Iri) -> Self:
        """
        Reads an object of the specific class type from the RDF data source using the provided connection
        and IRI. Validates that the retrieved RDF data matches the expected class type and ensures proper
        permissions have been granted to access the resource.

        :param con: The connection object to the data source (IConnection).
        :param iri: The IRI of the resource to read (Iri).
        :return: An object of the invoking class loaded with data from the RDF resource.

        :raises OldapErrorInconsistency: If an expected QName is not found or if the retrieved object type
                                          does not match the expected class type.
        :raises OldapErrorNotFound: If the resource associated with the provided IRI is not found.
        """
        graph = cls.project.projectShortName
        context = Context(name=con.context_name)
        access_block = creator_or_max_perm_block(
            graph_data=f"{graph}:data",
            resource_iri=iri.toRdf,
            user_iri=con.userIri.toRdf,
            min_perm=DataPermission.DATA_VIEW.numeric.toRdf,
            alias="read1",
            include_creator=True
        )
        sparql = context.sparql_context
        sparql += textwrap.dedent(f'''
        SELECT DISTINCT ?predicate ?value
        WHERE {{
            {access_block}
            GRAPH {graph}:data {{
                {iri.toRdf} ?predicate ?value .
            }}
        }}
        ''')

        jsonres = con.query(sparql)
        res = QueryProcessor(context, jsonres)
        objtype = None
        kwargs: dict[str, Any] = {}
        for r in res:
            if r['predicate'] == 'rdf:type':
                if r['value'].is_qname:
                    #objtype = r['value'].as_qname.fragment
                    objtype = r['value'].as_qname
                else:
                    logger.error(f"Expected QName as value, got {r['value']}")
                    raise OldapErrorInconsistency(f"Expected QName as value, got {r['value']}")
            else:
                if r['predicate'].is_qname:
                    if kwargs.get(r['predicate'].as_qname.fragment):
                        if isinstance(kwargs[r['predicate'].as_qname.fragment], set):
                            kwargs[r['predicate'].as_qname.fragment].add(r['value'])
                        else:
                            kwargs[r['predicate'].as_qname.fragment] = r['value']
                    else:
                        try:
                            kwargs[r['predicate'].as_qname.fragment] = {r['value']}
                        except TypeError:
                            kwargs[r['predicate'].as_qname.fragment] = r['value']
                else:
                    logger.error(f"Expected QName as predicate, got {r['predicate']}")
                    raise OldapErrorInconsistency(f"Expected QName as predicate, got {r['predicate']}")

        if objtype is None:
            raise OldapErrorNotFound(f'Resource with iri <{iri}> not found.')
        sparql = context.sparql_context
        sparql += textwrap.dedent(f'''
        SELECT DISTINCT ?role ?dataperm
        WHERE {{
            GRAPH {graph}:data {{
                << {iri.toRdf} oldap:attachedToRole ?role >> oldap:hasDataPermission ?dataperm .
            }}
        }}
        ''')
        try:
            jsonres = con.query(sparql)
        except Exception as e:
            logger.error(f'Failed to query data permissions for resource {iri}: {e}')
            raise
        res = QueryProcessor(context, jsonres)
        roles = {}
        for r in res:
            roles[r['role']] = DataPermission.from_qname(r['dataperm'])
        kwargs['attachedToRole'] = roles

        if cls.__name__ != objtype:
            raise OldapErrorInconsistency(f'Expected class {cls.__name__}, got {objtype} instead.')
        return cls(iri=iri, **kwargs)

    def update(self, indent: int = 0, indent_inc: int = 4) -> None:
        """
        Updates the current resource by creating and executing SPARQL queries to modify, insert, or
        delete data based on the changeset. This method also verifies user permissions and ensures
        data consistency using timestamps. It commits modifications within a transaction while
        rolling back changes upon encountering errors.

        The `update` method processes changes in resource attributes specified in a changeset. It
        constructs appropriate SPARQL queries for CREATE, MODIFY, and DELETE actions for each
        field. Additionally, it updates the last modification timestamp for the resource and records
        the user performing the update. If permissions are insufficient or an update fails, the
        transaction is aborted.

        :param indent: The initial level of indentation for the generated SPARQL queries.
        :type indent: int
        :param indent_inc: The incremental level of indentation for nested sections in SPARQL
                           queries.
        :type indent_inc: int
        :return: None
        :rtype: None
        """
        admin_resources, message = self.check_for_permissions(AdminPermission.ADMIN_RESOURCES)

        context = Context(name=self._con.context_name)
        blank = ''
        timestamp = Xsd_dateTimeStamp()

        sparql_list = []
        required_permission = DataPermission.DATA_EXTEND
        for field, change in self._changeset.items():
            if field == 'oldap:attachedToRole':
                required_permission = DataPermission.DATA_PERMISSIONS
                continue
            if change.action == Action.MODIFY:
                continue  # will be processed below!
            if change.action != Action.CREATE:
                if required_permission < DataPermission.DATA_UPDATE:
                    required_permission = DataPermission.DATA_UPDATE
            sparql = f'# Processing field "{field}"\n'
            sparql += f'{blank:{indent * indent_inc}}WITH {self._graph}:data\n'
            if change.action != Action.CREATE:
                sparql += f'{blank:{indent * indent_inc}}DELETE {{\n'
                if isinstance(change.old_value, (ObservableSet, LangString)):
                    for val in change.old_value:
                        sparql += f'{blank:{(indent + 1) * indent_inc}}?res_iri {field.toRdf} {val.toRdf} .\n'
                else:
                    sparql += f'{blank:{(indent + 1) * indent_inc}}?res_iri {field.toRdf} {change.old_value.toRdf} .\n'
                sparql += f'{blank:{indent * indent_inc}}}}\n'
            if change.action != Action.DELETE:
                sparql += f'{blank:{indent * indent_inc}}INSERT {{\n'
                if isinstance(change.old_value, (ObservableSet, LangString)):
                    for val in self._values[field]:
                        sparql += f'{blank:{(indent + 1) * indent_inc}}?res_iri {field.toRdf} {val.toRdf} .\n'
                else:
                    sparql += f'{blank:{(indent + 1) * indent_inc}}?res_iri {field.toRdf} {self._values[field].toRdf} .\n'
                sparql += f'{blank:{indent * indent_inc}}}}\n'
            sparql += f'{blank:{indent * indent_inc}}WHERE {{\n'
            sparql += f'{blank:{(indent + 1) * indent_inc}}BIND({self._iri.toRdf} as ?res_iri)\n'
            if change.action != Action.CREATE:
                if isinstance(change.old_value, (ObservableSet, LangString)):
                    for val in change.old_value:
                        sparql += f'{blank:{(indent + 1) * indent_inc}}?res_iri {field.toRdf} {val.toRdf} .\n'
                else:
                    sparql += f'{blank:{(indent + 1) * indent_inc}}?res_iri {field.toRdf} {change.old_value.toRdf} .\n'
            sparql += f'{blank:{indent * indent_inc}}}}'
            sparql_list.append(sparql)

        for field, change in self._changeset.items():
            if field == 'oldap:attachedToRole':
                sparql = f'# Processing field "{field}"\n'
                add_sparql = False
                if (change.action == Action.DELETE or change.action == Action.REPLACE) and len(change.old_value) > 0:
                    sparql += f'{blank:{indent * indent_inc}}DELETE DATA {{\n'
                    sparql += f'{blank:{(indent + 1) * indent_inc}}GRAPH {self._graph}:data {{\n'
                    for role, dperm in change.old_value.items():
                        sparql += f'{blank:{(indent + 2) * indent_inc}}{self._iri.toRdf} {field.toRdf} {role.toRdf} .\n'
                        sparql += f'{blank:{(indent + 2) * indent_inc}}<<{self._iri.toRdf} {field.toRdf} {role.toRdf}>> oldap:hasDataPermission  {dperm.toRdf} .\n'
                    sparql += f'{blank:{(indent + 1) * indent_inc}}}}\n'
                    sparql += f'{blank:{indent * indent_inc}}}}\n'
                    add_sparql = True
                elif (change.action == Action.CREATE or change.action == Action.REPLACE) and len(self._attached_roles) > 0:
                    sparql += f'{blank:{indent * indent_inc}}INSERT DATA {{\n'
                    sparql += f'{blank:{(indent + 1) * indent_inc}}GRAPH {self._graph}:data {{\n'
                    for role, dperm in self._attached_roles.items():
                        sparql += f'{blank:{(indent + 2) * indent_inc}}{self._iri.toRdf} {field.toRdf} {role.toRdf} .\n'
                        sparql += f'{blank:{(indent + 2) * indent_inc}}<<{self._iri.toRdf} {field.toRdf} {role.toRdf}>> oldap:hasDataPermission  {dperm.toRdf} .\n'
                    sparql += f'{blank:{(indent + 1) * indent_inc}}}}\n'
                    sparql += f'{blank:{indent * indent_inc}}}}\n'
                    add_sparql = True
                elif change.action == Action.MODIFY:
                    added = {}
                    removed = {}
                    changed = {}
                    if change.old_value:
                        added = {key: self._attached_roles[key] for key in self._attached_roles.keys() - change.old_value.keys()}
                    else:
                        added = self._attached_roles
                    if change.old_value:
                        removed = {key: change.old_value[key] for key in
                                   change.old_value.keys() - self._attached_roles.keys()}
                    else:
                        removed = {}
                    if change.old_value:
                        changed = {key: {'old': change.old_value.get(key),
                                         'new': self._attached_roles.get(key)}
                                   for key in change.old_value.keys() & self._attached_roles.keys()
                                   if self._attached_roles[key] != change.old_value[key]}
                    else:
                        changed = {}
                    if len(removed) > 0 or len(changed) > 0:
                        sparql += f'{blank:{indent * indent_inc}}DELETE DATA {{\n'
                        sparql += f'{blank:{(indent + 1) * indent_inc}}GRAPH {self._graph}:data {{\n'
                        for role, dperm in removed.items():
                            sparql += f'{blank:{(indent + 2) * indent_inc}}{self._iri.toRdf} {field.toRdf} {role.toRdf} .\n'
                            sparql += f'{blank:{(indent + 2) * indent_inc}}<<{self._iri.toRdf} {field.toRdf} {role.toRdf}>> oldap:hasDataPermission  {dperm.toRdf} .\n'
                        for role, data in changed.items():
                            sparql += f'{blank:{(indent + 2) * indent_inc}}{self._iri.toRdf} {field.toRdf} {role.toRdf} .\n'
                            sparql += f'{blank:{(indent + 2) * indent_inc}}<<{self._iri.toRdf} {field.toRdf} {role.toRdf}>> oldap:hasDataPermission  {data['old'].toRdf} .\n'
                        sparql += f'{blank:{(indent + 1) * indent_inc}}}}\n'
                        sparql += f'{blank:{indent * indent_inc}}}}\n'
                        add_sparql = True
                    if len(added) > 0 or len(changed) > 0:
                        if add_sparql:
                            sparql += f'{blank:{indent * indent_inc}}\n;\n'
                        sparql += f'{blank:{indent * indent_inc}}INSERT DATA {{\n'
                        sparql += f'{blank:{(indent + 1) * indent_inc}}GRAPH {self._graph}:data {{\n'
                        for role, dperm in added.items():
                            sparql += f'{blank:{(indent + 2) * indent_inc}}{self._iri.toRdf} {field.toRdf} {role.toRdf} .\n'
                            sparql += f'{blank:{(indent + 2) * indent_inc}}<<{self._iri.toRdf} {field.toRdf} {role.toRdf}>> oldap:hasDataPermission  {dperm.toRdf} .\n'
                        for role, data in changed.items():
                            sparql += f'{blank:{(indent + 2) * indent_inc}}{self._iri.toRdf} {field.toRdf} {role.toRdf} .\n'
                            sparql += f'{blank:{(indent + 2) * indent_inc}}<<{self._iri.toRdf} {field.toRdf} {role.toRdf}>> oldap:hasDataPermission  {data['old'].toRdf} .\n'
                        sparql += f'{blank:{(indent + 1) * indent_inc}}}}\n'
                        sparql += f'{blank:{indent * indent_inc}}}}\n'
                        add_sparql = True
                if add_sparql:
                    sparql_list.append(sparql)
                continue

            if change.action != Action.MODIFY:
                continue  # has been processed above
            if self.properties[field].prop.datatype == XsdDatatypes.langString:
                sparqls = self._values[field].update(graph=Xsd_QName(self._graph, 'data'),
                                                     subject=self._iri,
                                                     field=field.as_qname)
                for lang, lchange in self._values[field].changeset.items():
                    if lchange.action != Action.CREATE:
                        if required_permission < DataPermission.DATA_UPDATE:
                            required_permission = DataPermission.DATA_UPDATE
                sparql_list.extend(sparqls)
            else:
                #
                # first we rectify the datatype of all "new" values added to the set
                #
                newset = {convert2datatype(x, self.properties[field].prop.datatype) for x in self._values[field]}
                self._values[field] = ObservableSet(newset, old_value=self._values[field].old_value, notifier=self.notifier, notify_data=field)
                sparqls = self._values[field].update_sparql(graph=Iri(f'{self._graph}:data'),
                                                            subject=self._iri,
                                                            field=field)
                sparql_list.extend(sparqls)

        sparql = context.sparql_context
        sparql += f'# Updating resource "{self._iri}"\n'
        sparql += " ;\n".join(sparql_list)

        modtime_update = context.sparql_context
        modtime_update += f'''
        WITH {self._graph}:data
        DELETE {{
            ?res oldap:lastModificationDate {self.lastModificationDate.toRdf} .
            ?res oldap:lastModifiedBy ?contributor .
        }}
        INSERT {{
            ?res oldap:lastModificationDate {timestamp.toRdf} .
            ?res oldap:lastModifiedBy {self._con.userIri.toRdf} .
        }}
        WHERE {{
            BIND({self._iri.toRdf} as ?res)
            ?res oldap:lastModificationDate {self.lastModificationDate.toRdf} .
            ?res oldap:lastModifiedBy ?contributor .
        }}
        '''

        context = Context(name=self._con.context_name)
        modtime_get = context.sparql_context
        modtime_get += f"""
        SELECT ?modified
        FROM {self._graph}:data
        WHERE {{
            {self._iri.toRdf} oldap:lastModificationDate ?modified
        }}
        """

        self._con.transaction_start()
        #
        # Test permission for Action.REPLACE
        #
        if not admin_resources:
            if not self.get_data_permission(required_permission):
                self._con.transaction_abort()
                raise OldapErrorNoPermission(f'No permission to update resource "{self._iri}"')
        try:
            self._con.transaction_update(sparql)
            self._con.transaction_update(modtime_update)
            jsonobj = self._con.transaction_query(modtime_get)
            res = QueryProcessor(context, jsonobj)
            modtime = res[0]['modified']
            if timestamp != modtime:
                raise OldapErrorUpdateFailed(f"Update failed! Timestamp does not match (modtime={modtime}, timestamp={timestamp}).")
        except OldapError:
            logger.error(f'Failed to update resource "{self._iri}"', exc_info=True)
            self._con.transaction_abort()
            raise
        try:
            self._con.transaction_commit()
        except OldapError:
            logger.error(f'Failed to commit transaction for resource "{self._iri}"', exc_info=True)
            self._con.transaction_abort()
            raise
        self.clear_changeset()

    def delete(self) -> None:
        """
        Deletes the specified resource represented by the object's IRI from the associated graph
        if the proper permissions are granted and the resource is not in use. This method initiates
        a transactional sequence to ensure data consistency and rollback in case of an error.
        The process includes checking permissions, querying whether the resource is in use,
        and finally issuing a SPARQL DELETE query to remove the resource.

        Raises appropriate exceptions if the resource is in use, permissions are insufficient,
        or any other transaction-related error occurs.

        :raises OldapErrorNoPermission: If the user lacks the necessary permissions to
            delete the resource.
        :raises OldapErrorInUse: If the resource is currently in use and cannot be deleted.
        :raises OldapError: If any error occurs during transaction start, query, update,
            or commit phases.
        """
        admin_resources, message = self.check_for_permissions(AdminPermission.ADMIN_RESOURCES)

        context = Context(name=self._con.context_name)
        inuse = context.sparql_context
        inuse += f"""
        ASK {{
            GRAPH ?g {{
                ?res ?prop {self._iri.toRdf} .
            }}
        }}
        """

        context = Context(name=self._con.context_name)
        sparql = context.sparql_context
        sparql += f"""
        DELETE WHERE {{
            GRAPH {self._graph}:data {{
                {self._iri.toRdf} ?prop ?val .
                << {self._iri.toRdf} oldap:attachedToRole ?role >> oldap:hasDataPermission ?dataperm .
            }}
        }} 
        """

        self._con.transaction_start()
        if not admin_resources:
            if not self.get_data_permission(DataPermission.DATA_DELETE):
                self._con.transaction_abort()
                raise OldapErrorNoPermission(f'No permission to update resource "{self._iri}"')
        try:
            result = self._con.transaction_query(inuse)
            if result['boolean']:
                raise OldapErrorInUse(f'Resource "{self._iri}" is in use and cannot be deleted.')
        except OldapError:
            logger.error(f'SPARQL: Failed to check whether resource "{self._iri}" is in use', exc_info=True)
            self._con.transaction_abort()
            raise
        try:
            self._con.transaction_update(sparql)
        except OldapError:
            logger.error(f'SPARQL: Failed to delete resource "{self._iri}"', exc_info=True)
            self._con.transaction_abort()
            raise
        try:
            self._con.transaction_commit()
        except OldapError:
            logger.error(f'SPARQL: Failed to commit transaction for resource "{self._iri}"', exc_info=True)
            self._con.transaction_abort()
            raise

    @staticmethod
    def read_data(con: IConnection, projectShortName: Xsd_NCName | str, iri: Iri | str) -> dict[Xsd_QName, Any]:
        """
        Retrieves data from a resource in the specified project with permissions validation.
        NOTE: It does *NOT* return the attachedToRole data!

        This function performs a SPARQL query to fetch data associated with the given
        IRI within a specific project graph, taking into account user permissions for
        the resource. If no data is found for the given IRI, or if permission requirements
        are not met, exceptions are raised.

        :param con: A connection object used to interact with the database.
        :type con: IConnection
        :param projectShortName: The short name of the project graph. Can be an Xsd_NCName
            or string.
        :type projectShortName: Xsd_NCName | str
        :param iri: The IRI of the resource to retrieve data for. Can be an Iri instance
            or string.
        :type iri: Iri | str
        :return: A dictionary mapping predicates (of type Xsd_QName) to their corresponding
            values.
        :rtype: dict[Xsd_QName, Any]
        :raises OldapErrorInconsistency: If a predicate is not a valid QName.
        :raises OldapErrorNotFound: If the resource with the specified IRI cannot be found.
        """
        if not isinstance(iri, Iri):
            iri = Iri(iri, validate=True)
        if not isinstance(projectShortName, Xsd_NCName):
            graph = Xsd_NCName(projectShortName)
        else:
            graph = projectShortName

        context = Context(name=con.context_name)
        access_block = creator_or_max_perm_block(
            graph_data=f"{graph}:data",
            resource_iri=iri.toRdf,
            user_iri=con.userIri.toRdf,
            min_perm=DataPermission.DATA_VIEW.numeric.toRdf,
            alias="read1",
            include_creator=True
        )
        sparql = context.sparql_context
        sparql += textwrap.dedent(f'''
        SELECT DISTINCT ?predicate ?value
        WHERE {{
            {access_block}
            GRAPH {graph}:data {{
                {iri.toRdf} ?predicate ?value .
            }}
        }}
        ''')

        try:
            jsonres = con.query(sparql)
        except OldapError:
            logger.error(f'SPARQL: Failed to retrieve data for resource "{iri}"', exc_info=True)
            raise
        jsonres = con.query(sparql)
        res = QueryProcessor(context, jsonres)
        data = {}
        for r in res:
            if r['predicate'].is_qname:
                if r['predicate'].as_qname == Xsd_QName('oldap:attachedToRole'):
                    continue
                if not data.get(r['predicate'].as_qname):
                    data[r['predicate'].as_qname] = []
                data[str(r['predicate'].as_qname)].append(r['value'])
            else:
                raise OldapErrorInconsistency(f"Expected QName as predicate, got {r['predicate']}")
        if not data.get('rdf:type'):
            raise OldapErrorNotFound(f'Resource with iri <{iri}> not found.')

        sparql = context.sparql_context
        sparql += textwrap.dedent(f'''
        SELECT DISTINCT ?role ?dataperm
        WHERE {{
            GRAPH {graph}:data {{
                << {iri.toRdf} oldap:attachedToRole ?role >> oldap:hasDataPermission ?dataperm .
            }}
        }}
        ''')
        try:
            jsonres = con.query(sparql)
        except Exception as e:
            logger.error(f'SPARQL: Failed to retrieve data permissions for resource "{iri}"', exc_info=True)
            raise
        res = QueryProcessor(context, jsonres)
        roles = {}
        for r in res:
            roles[r['role']] = DataPermission.from_qname(r['dataperm'])
        data[Xsd_QName('oldap:attachedToRole')] = roles
        #data[Xsd_QName('virtual:resourceIri')] = iri
        return data

    @staticmethod
    def search_fulltext(con: IConnection,
                        projectShortName: Xsd_NCName | str,
                        searchstr: str,
                        resClass: Xsd_QName | str | None = None,
                        countOnly: bool = False,
                        sortBy: list[SortBy] = [],
                        limit: int = 100,
                        offset: int = 0,
                        indent: int = 0, indent_inc: int = 4) -> int | dict[Iri, dict[str, Xsd]]:
        """
            Searches and retrieves data from the database using a full-text search query. This method allows
            users to optionally filter and sort the results based on specific criteria. It supports returning
            either the count of matching results or the results themselves in a structured format.

            :param con: The connection object used to interact with the database.
                This must implement the IConnection interface.
            :type con: IConnection

            :param projectShortName: The short name of the project for which the data
                needs to be queried.
            :type projectShortName: Xsd_NCName | str

            :param searchstr: The search string used for full-text search, with case-insensitive
                substring matching.
            :type searchstr: str

            :param resClass: An optional parameter specifying the resource class of the
                objects to be retrieved. If provided, the query will filter results for the
                specified class.
            :type resClass: Xsd_QName | str | None

            :param countOnly: A boolean flag to indicate whether to return only the count
                of matching results. If set to True, only the count is returned;
                otherwise, matching records are retrieved.
            :type countOnly: bool

            :param sortBy: A list of sorting criteria. Each criterion specifies the property
                and direction (ascending or descending) to sort the results. If not provided,
                no sorting is applied. SortBy has the following attributes:
                - property (Xsd_QName): The property to sort by. The property must be one of
                  Xsd_QName('oldap:creationDate'), Xsd_QName('oldap:lastModificationDate') or
                  Xsd_QName('oldap:propval') to sort according the property where the match has been found..
                - direction (str): The sorting direction, either 'asc' for ascending or 'desc' for descending.
            :type sortBy: list[SortBy]

            :param limit: The maximum number of results to retrieve. Defaults to 100 if not specified.
            :type limit: int

            :param offset: The number of records to skip before starting to retrieve results.
                This is useful for paginated responses.
            :type offset: int

            :param indent: The base indentation level used for formatting the generated SPARQL
                query string.
            :type indent: int

            :param indent_inc: The incremental value to add to the base indent for nested query
                components.
            :type indent_inc: int

            :return: If `countOnly` is True, an integer representing the count of matching results
                is returned. Otherwise, a dictionary is returned where the keys are IRIs of the
                matching resources, and the values are dictionaries containing their respective
                properties and values.
            :rtype: int | dict[Iri, dict[str, Xsd]]
        """
        if not isinstance(projectShortName, Xsd_NCName):
            graph = Xsd_NCName(projectShortName, validate=True)
        else:
            graph = projectShortName
        if resClass and not isinstance(resClass, Xsd_QName):
            resClass = Xsd_QName(resClass, validate=True)

        blank = ''
        context = Context(name=con.context_name)
        sparql = context.sparql_context

        if (countOnly):
            sparql += f'{blank:{indent * indent_inc}}SELECT (COUNT(DISTINCT ?s) as ?numResult)'
        else:
            sparql += f'{blank:{indent * indent_inc}}SELECT DISTINCT ?s ?t ?p ?o'
            if sortBy:
                for sortby in sortBy:
                    if sortby.property == Xsd_QName('oldap:creationDate'):
                        sparql += ' ?creationDate'
                    elif sortby.property == Xsd_QName('oldap:lastModificationDate'):
                        sparql += '?lastModificationDate'

        sparql += f'\n{blank:{indent * indent_inc}}WHERE {{'
        sparql += f'\n{blank:{(indent + 1) * indent_inc}}GRAPH oldap:admin {{'
        sparql += f'\n{blank:{(indent + 2) * indent_inc}}{con.userIri.toRdf} oldap:hasRole ?role .'
        sparql += f'\n{blank:{(indent + 2) * indent_inc}}?DataPermission oldap:permissionValue ?permval .'
        sparql += f'\n{blank:{(indent + 2) * indent_inc}}FILTER(?permval >= {DataPermission.DATA_VIEW.numeric.toRdf})'
        sparql += f'\n{blank:{(indent + 1) * indent_inc}}}}'
        sparql += f'\n{blank:{(indent + 1) * indent_inc}}GRAPH {graph}:data {{'
        if not countOnly and sortBy:
            for sortby in sortBy:
                if sortby.property == Xsd_QName('oldap:creationDate'):
                    sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s oldap:creationDate ?creationDate .'
                elif sortby.property == Xsd_QName('oldap:lastModificationDate'):
                    sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s oldap:lastModificationDate ?lastModificationDate .'
        sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s ?p ?o .'
        sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s rdf:type ?t .'
        if resClass:
            sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s rdf:type {resClass} .'
        sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s oldap:attachedToRole ?role .'
        sparql += f'\n{blank:{(indent + 2) * indent_inc}}<< ?s oldap:attachedToRole ?role >> oldap:hasDataPermission ?DataPermission .'
        sparql += f'\n{blank:{(indent + 2) * indent_inc}}FILTER(isLiteral(?o) && (datatype(?o) = xsd:string || datatype(?o) = rdf:langString || lang(?o) != ""))'
        sparql += f'\n{blank:{(indent + 2) * indent_inc}}FILTER(CONTAINS(LCASE(STR(?o)), "{searchstr}"))  # case-insensitive substring match'
        sparql += f'\n{blank:{(indent + 1) * indent_inc}}}}'
        sparql += f'\n{blank:{indent * indent_inc}}}}'

        if not countOnly and sortBy:
            for sortby in sortBy:
                if sortby.property == Xsd_QName('oldap:creationDate'):
                    if sortby.dir == SortDir.asc:
                        sparql += f'\n{blank:{indent * indent_inc}}ORDER BY ASC(?creationDate)'
                    else:
                        sparql += f'\n{blank:{indent * indent_inc}}ORDER BY DESC(?creationDate)'
                elif sortby.property == Xsd_QName('oldap:lastModificationDate'):
                    if sortby.dir == SortDir.asc:
                        sparql += f'\n{blank:{indent * indent_inc}}ORDER BY ASC(?lastModificationDate)'
                    else:
                        sparql += f'\n{blank:{indent * indent_inc}}ORDER BY DESC(?lastModificationDate)'
                elif sortby.property == Xsd_QName('oldap:propval'):
                    if sortby.dir == SortDir.asc:
                        sparql += f'\n{blank:{indent * indent_inc}}ORDER BY ASC(?o)'
                    else:
                        sparql += f'\n{blank:{indent * indent_inc}}ORDER BY DESC(?o)'

        if not countOnly:
            sparql += f'\n{blank:{indent * indent_inc}}LIMIT {limit} OFFSET {offset}'
        sparql += '\n'

        try:
            jsonres = con.query(sparql)
        except OldapError:
            logger.error(f'SPARQL: Failed to search for resources in project "{projectShortName}"', exc_info=True)
            raise
        res = QueryProcessor(context, jsonres)
        if countOnly:
            return res[0]['numResult']
        else:
            result = {}
            for r in res:
                tmp = {
                    r['p']: r['o'],
                    Xsd_QName('owl:Class'): r['t']
                }
                if sortBy:
                    for sortby in sortBy:
                        if sortby.property == Xsd_QName('oldap:creationDate'):
                            tmp['oldap:creationDate'] = r['creationDate'],
                        elif sortby.property == Xsd_QName('oldap:lastModificationDate'):
                            tmp['oldap:lastModificationDate'] = r['lastModificationDate']
                result[r['s']] = tmp
            return result

    @staticmethod
    def all_resources(con: IConnection,
                      projectShortName: Xsd_NCName | str,
                      resClass: Xsd_QName | str,
                      includeProperties: list[Xsd_QName] | None = None,
                      countOnly: bool = False,
                      sortBy: list[SortBy] = [],
                      limit: int = 100,
                      offset: int = 0,
                      indent: int = 0, indent_inc: int = 4) -> list[dict[str | Xsd_QName, list[Xsd] | LangString]] | int:
        """
        Retrieves all resources associated with a specified project and resource class, with options to include
        particular properties, count only the number of resources, and apply sorting, pagination, and indentation
        for query formatting.

        :param con: Connection object used for interfacing with the data store.
        :type con: IConnection
        :param projectShortName: Short name of the project under which resources are being queried. Can be a valid
            `Xsd_NCName` instance or a string fitting the `Xsd_NCName` criteria.
        :type projectShortName: Xsd_NCName | str
        :param resClass: The fully-qualified class name of the resource. This can either be an `Xsd_QName` object or
            a string fitting the `Xsd_QName` criteria.
        :type resClass: Xsd_QName | str
        :param includeProperties: List of properties to include in the results. These properties must follow the
            `Xsd_QName` format. If None, no specific properties are included by default.
        :type includeProperties: list[Xsd_QName] | None
        :param countOnly: When set to True, only the count of unique resources is returned. Defaults to False,
            returning the full resource set.
        :type countOnly: bool
        :param sortBy: A list of sort specifications, where each item is an instance of the `SortBy` class
            specifying property and direction for sorting. Sorting only occurs if `sortBy` is not empty.
        :type sortBy: list[SortBy]
        :param limit: The maximum number of resources to retrieve. Defaults to 100. Ignored when `countOnly`
            is True.
        :type limit: int
        :param offset: The index of the first result to retrieve, useful for pagination. Defaults to 0.
        :type offset: int
        :param indent: The initial indentation level of the SPARQL query string.
        :type indent: int
        :param indent_inc: The number of spaces to increment per indentation level for the SPARQL query formatting.
        :type indent_inc: int
        :return: Depending on the `countOnly` flag:
            - If `countOnly` is True, returns the count of unique resources as an integer.
            - If `countOnly` is False, returns a list of dictionaries representing the resources and their
              properties. Each dictionary contains the resource IRI and requested properties, with language
              strings consolidated under `LangString` if applicable.
        :rtype: list[dict[str, list[Xsd] | LangString]] | int
        """
        if not isinstance(projectShortName, Xsd_NCName):
            graph = Xsd_NCName(projectShortName, validate=True)
        else:
            graph = projectShortName
        if not isinstance(resClass, Xsd_QName):
            resClass = Xsd_QName(resClass, validate=True)

        if sortBy:
            for s in sortBy:
                if not isinstance(s, SortBy):
                    raise OldapErrorType(f'Expected SortBy, got {type(s)}')
                if not includeProperties or s.property not in includeProperties:
                    if not includeProperties:
                        includeProperties = []
                    includeProperties.append(s.property)

        blank = ''
        context = Context(name=con.context_name)
        sparql = context.sparql_context

        if (countOnly):
            sparql += f'{blank:{indent * indent_inc}}SELECT (COUNT(DISTINCT ?s) as ?numResult)'
        else:
            sparql += f'{blank:{indent * indent_inc}}SELECT DISTINCT ?s'
            if includeProperties:
                for index, item in enumerate(includeProperties):
                    sparql += f' ?o{index}'
        sparql += f'\n{blank:{indent * indent_inc}}WHERE {{'
        sparql += f'\n{blank:{(indent + 1) * indent_inc}}GRAPH oldap:admin {{'
        sparql += f'\n{blank:{(indent + 2) * indent_inc}}{con.userIri.toRdf} oldap:hasRole ?role .'
        sparql += f'\n{blank:{(indent + 2) * indent_inc}}?dataperm oldap:permissionValue ?permval .'
        sparql += f'\n{blank:{(indent + 2) * indent_inc}}FILTER(?permval >= {DataPermission.DATA_VIEW.numeric.toRdf})'
        sparql += f'\n{blank:{(indent + 1) * indent_inc}}}}'
        sparql += f'\n{blank:{(indent + 1) * indent_inc}}GRAPH {graph}:data {{'
        sparql += f'\n{blank:{(indent + 2) * indent_inc}}?s oldap:attachedToRole ?role .'
        sparql += f'\n{blank:{(indent + 2) * indent_inc}}<< ?s oldap:attachedToRole ?role >> oldap:hasDataPermission ?dataperm .'
        if includeProperties:
            for index, prop in enumerate(includeProperties):
                sparql += f'\n{blank:{(indent + 2) * indent_inc}}OPTIONAL {{ ?s {prop} ?o{index} . }}'
        sparql += f'\n{blank:{(indent + 1) * indent_inc}}}}'
        sparql += f'\n{blank:{(indent + 1) * indent_inc}}?s rdf:type {resClass} .'
        sparql += f'\n{blank:{indent * indent_inc}}}}'

        if not countOnly and sortBy:
            sparql += f'\n{blank:{indent * indent_inc}}ORDER BY'
            for s in sortBy:
                if s.dir == SortDir.asc:
                    sparql += f' ASC(?o{includeProperties.index(s.property)})'
                else:
                    sparql += f' DESC(?o{includeProperties.index(s.property)})'
            sparql += '\n'

        if not countOnly:
            sparql += f'\n{blank:{indent * indent_inc}}LIMIT {limit} OFFSET {offset}'
        sparql += '\n'

        try:
            jsonres = con.query(sparql)
        except OldapError:
            logger.error(f'SPARQL: Failed to retrieve resources for project "{projectShortName}"', exc_info=True)
            raise
        res = QueryProcessor(context, jsonres)
        if countOnly:
            return res[0]['numResult']
        else:
            result: list[dict[str, list[Xsd] | LangString]] = []

            for r in res:
                resiri = r['s']
                if not result:
                    result.append({'iri': [resiri]})
                resource = None
                for x in result: # we check if the arre already contains a resource with the given IRI
                    if resiri == x['iri'][0]:
                        resource = x  # yes, we assign it to resource
                if not resource:
                    resource = {'iri': [r['s']]}  # no, we create it (The resource IRI didn't yet occur in the result
                    result.append(resource)
                if includeProperties:
                    for index, property in enumerate(includeProperties):
                        if resource.get(property, None) is None:
                            resource[property] = []
                        raw = r.get(f'o{index}')
                        if raw is None:
                            continue
                        if raw not in resource[property]:
                            resource[property].append(raw)
            if includeProperties:
                for resource in result:
                    for index, property in enumerate(includeProperties):
                        is_langstring = False
                        for val in resource[property]:
                            if isinstance(val, Xsd_string) and val.lang is not None:
                                is_langstring = True
                                break
                        if is_langstring:
                            resource[property] = LangString(resource[property])
        return result

    @staticmethod
    def get_media_object_by_id(con: IConnection, mediaObjectId: Xsd_string | str) -> dict[str, Xsd]:
        """
        Retrieves a media object by its ID from the system. This method queries a SPARQL endpoint
        to fetch details about the media object and constructs a resulting dictionary with the
        media object attributes and its associated metadata. Additionally, a signed JWT token
        is included in the result for security purposes.

        :param con: A connection interface that provides access to the SPARQL endpoint and user context.
        :type con: IConnection
        :param mediaObjectId: The ID of the media object to be fetched, represented as an XSD string or a Python string.
        :type mediaObjectId: Xsd_string | str
        :return: A dictionary containing key-value pairs of media object attributes and metadata,
            with an additional JWT token for security. Returns None if the media object is not found.
        :rtype: dict[str, Xsd] | None
        :raises OldapErrorNotFound: Raised when the media object with the specified ID is not found.
        """
        if not isinstance(mediaObjectId, Xsd_string):
            mediaObjectId = Xsd_string(mediaObjectId, validate=True)
        blank = ''
        context = Context(name=con.context_name)
        sparql = context.sparql_context
        sparql += textwrap.dedent(f"""
        SELECT DISTINCT ?subject ?graph ?path ?prop ?val ?permval
        WHERE {{
            VALUES ?inputImageId {{ {mediaObjectId.toRdf} }}
            {{
                SELECT ?subject ?graph (MAX(xsd:integer(?pv)) AS ?maxPerm)
                WHERE {{
                    ?subject rdf:type shared:MediaObject .
                    GRAPH oldap:admin {{
                        {con.userIri.toRdf} oldap:hasRole ?role .
                    }}
                    GRAPH ?graph {{
                        ?subject oldap:attachedToRole ?role .
                        <<?subject oldap:attachedToRole ?role>> oldap:hasDataPermission ?dataperm .
                        ?subject shared:assetId ?inputImageId .
                    }}
                    GRAPH oldap:admin {{
                        ?dataperm oldap:permissionValue ?pv .
                    }}
                }}
                GROUP BY ?subject ?graph
            }}

            ?subject rdf:type shared:MediaObject .
            GRAPH oldap:admin {{
                {con.userIri.toRdf} oldap:hasRole ?role .
                ?dataperm oldap:permissionValue ?permval .
            }}
            GRAPH ?graph {{
                ?subject oldap:attachedToRole ?role .
                <<?subject oldap:attachedToRole ?role>> oldap:hasDataPermission ?dataperm .
                ?subject shared:assetId ?inputImageId .
                OPTIONAL {{ ?subject shared:path ?path . }}
                ?subject ?prop ?val .
            }}

            FILTER(xsd:integer(?permval) = ?maxPerm)
        }}
        """)
        try:
            jsonres = con.query(sparql)
        except OldapError:
            logger.error(f'SPARQL: Failed to retrieve media object with ID "{mediaObjectId}"', exc_info=True)
            raise
        res = QueryProcessor(context, jsonres)
        if len(res) == 0:
            raise OldapErrorNotFound(f'Media object with id {mediaObjectId} not found.')
        result: dict[str, Xsd] = {
            'iri': res[0].get('subject'),
            'graph': res[0].get('graph'),
            'permval': res[0].get('permval')
        }
        for r in res:
            if str(r['prop']) == 'rdf:type':
                continue
            if str(r['prop']) in {'oldap:createdBy', 'oldap:creationDate', 'oldap:lastModifiedBy', 'oldap:lastModificationDate',
                                  'dcterms:type', 'shared:assetId', 'shared:originalName', 'shared:originalMimeType',
                                  'shared:serverUrl', 'shared:path', 'shared:protocol', 'shared:derivativeName'}:
                result[str(r['prop'])] = r['val']
            else:
                if result.get(str(r['prop'])) is None:
                    result[str(r['prop'])] = []
                result[str(r['prop'])].append(r['val'])

        expiration = datetime.now().astimezone() + timedelta(minutes=2)
        payload = {
            'userIri': str(con.userIri),
            'userid': str(con.userid),
            'id': str(mediaObjectId),
            'path': str(result.get('shared:path')),
            'assetId': str(result.get('shared:assetId')),
            'derivativeName': str(result.get('shared:derivativeName')),
            'permval': int(result.get('permval')),
            "exp": expiration.timestamp(),
            "iat": int(datetime.now().astimezone().timestamp()),
            "iss": "http://oldap.org"
        }
        token = jwt.encode(
            payload=payload,
            key=con.jwtkey,
            algorithm="HS256")
        result['token'] = token

        return result

    @staticmethod
    def get_media_object_by_iri(con: IConnection, mediaObjectIri: Iri | str) -> dict[str, Xsd] | None:
        if not isinstance(mediaObjectIri, Iri):
            mediaObjectIri = Iri(mediaObjectIri, validate=True)
        blank = ''
        context = Context(name=con.context_name)
        sparql = context.sparql_context
        sparql += textwrap.dedent(f"""
        SELECT ?graph ?prop ?val ?permval
        WHERE {{
            BIND({mediaObjectIri.toRdf} AS ?obj)
            BIND({con.userIri.toRdf} AS ?user)
            # max permval per graph (remove ?graph/GROUP BY if you want global max)
            {{
                SELECT ?graph (MAX(xsd:integer(?pv)) AS ?maxPerm)
                WHERE {{
                    ?obj rdf:type shared:MediaObject .
                    GRAPH oldap:admin {{
                        ?user oldap:hasRole ?role .
                    }}
                    GRAPH ?graph {{
                        ?obj oldap:attachedToRole ?role .
                        <<?obj oldap:attachedToRole ?role>> oldap:hasDataPermission ?dataperm .
                    }}
                    GRAPH oldap:admin {{
                        ?dataperm oldap:permissionValue ?pv .
                    }}
                }}
                GROUP BY ?graph
            }}

            ?obj rdf:type shared:MediaObject .
            GRAPH oldap:admin {{
                ?user oldap:hasRole ?role .
                ?dataperm oldap:permissionValue ?permval .
            }}
            GRAPH ?graph {{
                ?obj oldap:attachedToRole ?role .
                <<?obj oldap:attachedToRole ?role>> oldap:hasDataPermission ?dataperm .
                ?obj ?prop ?val .
            }}

            FILTER(xsd:integer(?permval) = ?maxPerm)
        }}
        """)
        try:
            jsonres = con.query(sparql)
        except OldapError:
            logger.error(f'SPARQL: Failed to retrieve media object with IRI "{mediaObjectIri}"', exc_info=True)
            raise
        res = QueryProcessor(context, jsonres)
        if len(res) == 0:
            raise OldapErrorNotFound(f'Media object with iri {mediaObjectIri} not found.')
        result: dict[str, Xsd] = {
            'iri': mediaObjectIri,
            'graph': res[0].get('graph'),
            'permval': res[0].get('permval')
        }
        for r in res:
            if str(r['prop']) == 'rdf:type':
                continue
            if str(r['prop']) in {'oldap:createdBy', 'oldap:creationDate', 'oldap:lastModifiedBy', 'oldap:lastModificationDate',
                                  'dcterms:type', 'shared:assetId', 'shared:originalName', 'shared:originalMimeType',
                                  'shared:serverUrl', 'shared:path', 'shared:protocol', 'shared:derivativeName'}:
                result[str(r['prop'])] = r['val']
            else:
                if result.get(str(r['prop'])) is None:
                    result[str(r['prop'])] = []
                result[str(r['prop'])].append(r['val'])

        expiration = datetime.now().astimezone() + timedelta(minutes=2)
        payload = {
            'userIri': str(con.userIri),
            'userid': str(con.userid),
            'projectShortName': res[0].get('graph').prefix if res[0].get('graph') else None,
            'id': str(result.get('shared:assetId')),
            'path': str(result['shared:path']),
            'assetId': str(result.get('shared:assetId')),
            'derivativeName': str(result.get('shared:derivativeName')),
            'permval': int(result['permval']),
            "exp": expiration.timestamp(),
            "iat": int(datetime.now().astimezone().timestamp()),
            "iss": "http://oldap.org"
        }
        token = jwt.encode(
            payload=payload,
            key=con.jwtkey,
            algorithm="HS256")
        result['token'] = token

        return result


    def toJsonObject(self) -> dict[str, list[str] | str]:
        result = {'iri': str(self._iri)}
        for propiri, values in self._values.items():
            if str(propiri) in {'oldap:createdBy', 'oldap:creationDate', 'oldap:lastModificationDate', 'oldap:lastModifiedBy'}:
                iterator = iter(values)
                result[str(propiri)] = str(next(iterator))
                continue

            result[str(propiri)] = [str(v) for v in values]
        return result


class ResourceInstanceFactory:
    """
    Represents a factory for creating instances of resources in a specific project.

    The `ResourceInstanceFactory` class is used for creating resource instances
    associated with a data model and project. It provides an interface to dynamically
    generate classes for resource objects with properties and inheritance derived
    from the project's data model.

    :ivar _con: The connection interface used to interact with the backend.
    :type _con: IConnection
    :ivar _project: Represents the project associated with the factory.
    :type _project: Project
    :ivar _datamodel: Represents the data model associated with the project.
    :type _datamodel: DataModel
    """
    _con: IConnection
    _project: Project
    _sharedProject: Project
    _datamodel: DataModel
    _sharedModel: DataModel
    _user_default_roles: Dict[Xsd_QName, DataPermission] = {}

    def __init__(self,
                 con: IConnection,
                 project: Project | Iri | Xsd_NCName | str):
        self._con = con
        if isinstance(project, Project):
            self._project = project
        else:
            self._project = Project.read(self._con, project)
        self._sharedProject = Project.read(self._con, "oldap:SharedProject")
        if self._con._userdata.hasRole:
            self._user_default_roles = {r: DataPermission.from_qname(p) for r, p in self._con._userdata.hasRole.items()}

        self._datamodel = DataModel.read(con=self._con, project=self._project)
        self._sharedModel = DataModel.read(con=self._con, project=self._sharedProject)

    def createObjectInstance(self, name: Xsd_NCName | Xsd_QName | str) -> Type:  ## ToDo: Get name automatically from IRI
        if isinstance(name, Xsd_QName):
            classiri = name
        else:
            try:
                classiri = Xsd_QName(name, validate=True)
            except (ValueError, OldapErrorValue):
                if not isinstance(name, Xsd_NCName):
                    name = Xsd_NCName(name, validate=True)
                classiri = Xsd_QName(self._project.projectShortName, name)
        resclass = self._datamodel.get(classiri)
        if not resclass:
            resclass = self._sharedModel.get(classiri)
        if resclass is None:
            raise OldapErrorNotFound(f'Given Resource Class "{classiri}" not found.')
        return type(str(name), (ResourceInstance,), {
            '_con': self._con,
            'project': self._project,
            'name': resclass.owl_class_iri,
            'factory': self,
            'properties': resclass.properties,
            'superclass': resclass.superclass,
            'user_default_roles': self._user_default_roles,
        })


    def read(self, iri: Iri | str) -> ResourceInstance:
        if not isinstance(iri, Iri):
            iri = Iri(iri, validate=True)
        graph = self._project.projectShortName
        context = Context(name=self._con.context_name)
        sparql = context.sparql_context
        sparql += textwrap.dedent(f'''
        SELECT DISTINCT ?predicate ?value
        WHERE {{
            # 1) compute the max permval for this user + this resource
            {{
                SELECT (MAX(?pv) AS ?permval)
                WHERE {{
                    GRAPH oldap:admin {{
                        {self._con.userIri.toRdf} oldap:hasRole ?role .
                    }}
                    GRAPH {graph}:data {{
                        {iri.toRdf} oldap:attachedToRole ?role .
                        << {iri.toRdf} oldap:attachedToRole ?role >> oldap:hasDataPermission ?dataperm .
                    }}
                    GRAPH oldap:admin {{
                        ?dataperm oldap:permissionValue ?pv .
                    }}
                }}
            }}

            # 2) return the resource triples, but only if there exists a role with that max permval
            GRAPH {graph}:data {{
                {iri.toRdf} ?predicate ?value .
            }}
            FILTER EXISTS {{
                GRAPH oldap:admin {{
                    {self._con.userIri.toRdf} oldap:hasRole ?role .
                    ?dataperm oldap:permissionValue ?permval .
                }}
                GRAPH {graph}:data {{
                    {iri.toRdf} oldap:attachedToRole ?role .
                    << {iri.toRdf} oldap:attachedToRole ?role >> oldap:hasDataPermission ?dataperm .
                }}
            }}
        }}''')
        jsonres = self._con.query(sparql)
        try:
            res = QueryProcessor(context, jsonres)
        except OldapError:
            logger.error(f'SPARQL: Failed to retrieve resource "{iri}"', exc_info=True)
            raise
        res = QueryProcessor(context, jsonres)
        objtype = None
        kwargs: dict[str, Any] = {}
        for r in res:
            if r['predicate'] == 'rdf:type':
                if r['value'].is_qname:
                    objtype = r['value'].as_qname
                else:
                    raise OldapErrorInconsistency(f"Expected QName as value, got {r['value']}")
            else:
                if r['predicate'].is_qname:
                    if kwargs.get(r['predicate'].as_qname.fragment):
                        if isinstance(kwargs[r['predicate'].as_qname.fragment], set):
                            kwargs[r['predicate'].as_qname.fragment].add(r['value'])
                        else:
                            kwargs[r['predicate'].as_qname.fragment] = r['value']
                    else:
                        try:
                            kwargs[r['predicate'].as_qname.fragment] = {r['value']}
                        except TypeError:
                            kwargs[r['predicate'].as_qname.fragment] = r['value']
                else:
                    raise OldapErrorInconsistency(f"Expected QName as predicate, got {r['predicate']}")
        if objtype is None:
            raise OldapErrorNotFound(f'Resource with iri <{iri}> not found.')
        sparql = context.sparql_context
        sparql += textwrap.dedent(f'''
        SELECT DISTINCT ?role ?dataperm
        WHERE {{
            GRAPH {graph}:data {{
                << {iri.toRdf} oldap:attachedToRole ?role >> oldap:hasDataPermission ?dataperm .
            }}
        }}
        ''')
        try:
            jsonres = self._con.query(sparql)
        except Exception as e:
            logger.error(f'SPARQL: Failed to retrieve data permissions for resource "{iri}"', exc_info=True)
            raise
        res = QueryProcessor(context, jsonres)
        roles = {}
        for r in res:
            roles[r['role']] = DataPermission.from_qname(r['dataperm'])
        kwargs['attachedToRole'] = roles
        Instance = self.createObjectInstance(objtype)
        return Instance(iri=iri, **kwargs)








