"""_____________________________________________________________________

:PROJECT: LARAsuite

*lara_django_data high_level_interface *

:details: lara_django_data high_level_interface.
         - generated with 'lara-django-dev generate_high_level_interface lara_django_data
:authors: mark doerr <mark.doerr@uni-greifswald.de>

.. note:: -
.. todo:: - 
________________________________________________________________________
"""

import json
import asyncio
import logging
import grpc
from typing import Union

from google.protobuf.struct_pb2 import Struct
from google.protobuf.json_format import MessageToDict

import lara_django_base_grpc.v1.lara_django_base_pb2 as lara_django_base_pb2
import lara_django_base_grpc.v1.lara_django_base_pb2_grpc as lara_django_base_pb2_grpc

import lara_django_data_grpc.v1.lara_django_data_pb2 as lara_django_data_pb2
import lara_django_data_grpc.v1.lara_django_data_pb2_grpc as lara_django_data_pb2_grpc

import  lara_django_base_grpc.lara_django_base_data_model as base_dm
import  lara_django_base_grpc.lara_django_base_interface as base_if
from  lara_django_base_grpc.abstract_interface import LARAInterface, GRPCChannelSingleton, create_file,  export_file, import_file, id_cache, update_image, update_file

import  lara_django_data_grpc.lara_django_data_data_model as data_dm 



#_________________  ExtraData  ______________________


class ExtraDataInterface:
    def __init__(self, channel: grpc.Channel = None, default_namespace_uri : str = None) -> None:
        self.default_namespace_uri = None
        self.default_namespace_id = None

        if default_namespace_uri is not None:
            self.default_namespace_uri = default_namespace_uri

        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel
        
        self.namespace_if = base_if.NamespaceInterface(channel)

        # self.extradata_class_client = (
        #     lara_django_data_pb2_grpc.ExtraDataclassByIRIControllerStub(channel)
        # )
        
        self.extradata_client = lara_django_data_pb2_grpc.ExtraDataControllerStub(channel)

        # this is required by the import_file decorator, please activate if needed for file upload
        self.stub = self.extradata_client
        self.file_chunk = lara_django_data_pb2.FileChunk
        self.update_item_id = "extradata_id"

        # self.extradata_full_client = lara_django_data_pb2_grpc.ExtraDataFullControllerStub(
        #     channel
        # )

    @import_file(relation_resolver=lambda x: x)
    #@create_file  # activate if needed for file upload
    #@create_image  # activate if needed for image upload
    async def create(self,  extradata:  list[data_dm.ExtraData] = None,  ids_only: bool = False, namespace_uri: str = None) -> Union[list[str], list[data_dm.ExtraData]]:
        # TODO: only retrieve default_namespace_id if not already retrieved and namespace_uri is None
        self.namespace_id = None
        # extradataclass_id = None
        if namespace_uri is None and self.default_namespace_uri is not None:
            namespace_uri = self.default_namespace_uri
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        try:
            self.namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
        except Exception as e:
            logging.error(f"Error retrieving namespace_id: {e}")
        
        # some sample code for retrieving a class IRI and adding namespace and class to the model
        # if extradata_class_iri is None:
        #     raise ValueError("No sample class IRI is given.")
        # try:
        #     res = await self.extradata_class_client.Retrieve(
        #         lara_django_data_pb2.ExtraDataclassRetrieveRequest(iri=extradata_class_iri)
        #     )
        #     extradataclass_id = res.extradataclass_id 
        # except Exception as e:
        #     logging.error(f"Error retrieving extradataclass_id: {e}")
        # for extradata in extradatas:
        #     extradata.namespace = self.namespace_id
        #     extradata.extradata_class = extradataclass_id

        try:
            create_coroutines = ( self.extradata_client.Create(lara_django_data_pb2.ExtraDataRequest(**extradata.model_dump())) for extradata in extradata )
            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [item.extradata_id for item in res]
            else:
                return res

        except Exception as e:
            logging.error(f"Error creating extradata: {e}")

    @export_file
    async def retrieve(
        self, 
        extradata_id: str = None,
        extradata_name: str = None,
        #extradata_name_full: str = None,
        filter_dict: dict = None,
        raw : bool = False
        ) -> list[data_dm.ExtraData]:
        filter_list = []
        results_list = []
        if  extradata_id is not None:
            try:
                res =  await self.extradata_client.Retrieve(
                    lara_django_data_pb2.ExtraDataRetrieveRequest(extradata_id=extradata_id) )
                if raw:
                    results_list.append(res)
                else:
                    results_list.append( data_dm.ExtraData(**MessageToDict(res, preserving_proto_field_name=True)) )
            except Exception as e:
                logging.error(f"Error retrieving extradata_id: {e}")

        if extradata_name is not None:
            filter_list.append({"name": extradata_name})
        # if extradata_name_full is not None:
        #     filter_list.append({"name_full": extradata_name_full})
        if filter_dict is not None:
            filter_list.append(filter_dict)

        for filter_dict in filter_list:
            metadata = (("filters", (json.dumps(filter_dict))),)
            try:
                res = await self.extradata_client.List(
                        lara_django_data_pb2.ExtraDataListRequest(), metadata=metadata
                      )
                if raw:
                      results_list += res.results
                else:
                    # a queue could be used here to speed up the retrieval
                    results = res.results
                    results_list += [ data_dm.ExtraData(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
            except Exception as e:
                logging.error(f"Error retrieving extradata_name: {e}")

        return results_list

    #@id_cache
    async def retrieve_id(self, 
                          extradata_name: str = None,
                          extradata_name_full: str = None,
                          iri: str = None,) -> str:
        if extradata_name is not None:
            filter_dict = {"name": extradata_name}
        elif extradata_name_full is not None:
            filter_dict = {"name_full": extradata_name_full}
        elif iri is not None:
            filter_dict = {"iri": iri}
            metadata = (("filters", (json.dumps(filter_dict))),)
            try:
                res = await self.extradata_client.List(
                    lara_django_data_pb2.ExtraDataListRequest(), metadata=metadata
                )
                results = res.results
            except Exception as e:
                logging.error(f"Error retrieving extradata_name: {e}")
            if len(results) > 0 and len(results) < 2:
                return results[0].extradata_id
            else:
                raise ValueError(f"Error retrieving extradata_id - no or too many results found.")
            
        
    async def list_items(self, filter_dict: dict = None, ids_only: bool = False) -> list[str]:
        if filter_dict is None:
            res = await self.extradata_client.List(lara_django_data_pb2.ExtraDataListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.extradata_client.List(
                lara_django_data_pb2.ExtraDataListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.extradata_id for item in res.results]
        else:
            return res.results
            

    async def list_full(self, filter_dict: dict = None) :
        if self.extradata_full_client is None:
            raise ValueError("No full client available.")
        if filter_dict is None:
            return await self.extradata_full_client.List(
                lara_django_data_pb2.ExtraDataFullListRequest()
            )
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            return await self.extradata_full_client.List(
                lara_django_data_pb2.ExtraDataFullListRequest(), metadata=metadata
            )

    @import_file(relation_resolver=lambda x: x)
    # @update_image
    # @update_file
    async def update(self, extradata : data_dm.ExtraData = None) -> None:
        try:
            await self.extradata_client.Update(
                lara_django_data_pb2.ExtraDataRequest(**extradata.model_dump()
            ))
        except Exception as e:
            logging.error(f"Error updating extradata_id: {e}")

    async def delete(self, id_list: list[str] = None, filter_dict: dict = None) -> None:
        if filter_dict is not None:
            id_list = await self.list_items(filter_dict=filter_dict, ids_only=True)
    
        try:
            delete_coroutines = ( self.extradata_client.Destroy(lara_django_data_pb2.ExtraDataDestroyRequest(extradata_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)
            
        except Exception as e:
            logging.error(f"Error deleting extradata_id: {e}")

#_________________  BlockchainHashes  ______________________


class BlockchainHashesInterface:
    def __init__(self, channel: grpc.Channel = None, default_namespace_uri : str = None) -> None:
        self.default_namespace_uri = None
        self.default_namespace_id = None

        if default_namespace_uri is not None:
            self.default_namespace_uri = default_namespace_uri

        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel
        
        self.namespace_if = base_if.NamespaceInterface(channel)

        # self.blockchainhashes_class_client = (
        #     lara_django_data_pb2_grpc.BlockchainHashesclassByIRIControllerStub(channel)
        # )
        
        self.blockchainhashes_client = lara_django_data_pb2_grpc.BlockchainHashesControllerStub(channel)

        # this is required by the import_file decorator, please activate if needed for file upload
        # self.stub = self.blockchainhashes_client
        # self.file_chunk = lara_django_data_pb2.FileChunk
        # self.update_item_id = "blockchainhashes_id"

        # self.blockchainhashes_full_client = lara_django_data_pb2_grpc.BlockchainHashesFullControllerStub(
        #     channel
        # )

    @import_file(relation_resolver=lambda x: x)
    #@create_file  # activate if needed for file upload
    #@create_image  # activate if needed for image upload
    async def create(self,  blockchainhashes:  list[data_dm.BlockchainHashes] = None,  ids_only: bool = False, namespace_uri: str = None) -> Union[list[str], list[data_dm.BlockchainHashes]]:
        # TODO: only retrieve default_namespace_id if not already retrieved and namespace_uri is None
        self.namespace_id = None
        # blockchainhashesclass_id = None
        if namespace_uri is None and self.default_namespace_uri is not None:
            namespace_uri = self.default_namespace_uri
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        try:
            self.namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
        except Exception as e:
            logging.error(f"Error retrieving namespace_id: {e}")
        
        # some sample code for retrieving a class IRI and adding namespace and class to the model
        # if blockchainhashes_class_iri is None:
        #     raise ValueError("No sample class IRI is given.")
        # try:
        #     res = await self.blockchainhashes_class_client.Retrieve(
        #         lara_django_data_pb2.BlockchainHashesclassRetrieveRequest(iri=blockchainhashes_class_iri)
        #     )
        #     blockchainhashesclass_id = res.blockchainhashesclass_id 
        # except Exception as e:
        #     logging.error(f"Error retrieving blockchainhashesclass_id: {e}")
        # for blockchainhashes in blockchainhashess:
        #     blockchainhashes.namespace = self.namespace_id
        #     blockchainhashes.blockchainhashes_class = blockchainhashesclass_id

        try:
            create_coroutines = ( self.blockchainhashes_client.Create(lara_django_data_pb2.BlockchainHashesRequest(**blockchainhashes.model_dump())) for blockchainhashes in blockchainhashes )
            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [item.blockchainhashes_id for item in res]
            else:
                return res

        except Exception as e:
            logging.error(f"Error creating blockchainhashes: {e}")

    @export_file
    async def retrieve(
        self, 
        blockchainhashes_id: str = None,
        blockchainhashes_name: str = None,
        #blockchainhashes_name_full: str = None,
        filter_dict: dict = None,
        raw : bool = False
        ) -> list[data_dm.BlockchainHashes]:
        filter_list = []
        results_list = []
        if  blockchainhashes_id is not None:
            try:
                res =  await self.blockchainhashes_client.Retrieve(
                    lara_django_data_pb2.BlockchainHashesRetrieveRequest(blockchainhashes_id=blockchainhashes_id) )
                if raw:
                    results_list.append(res)
                else:
                    results_list.append( data_dm.BlockchainHashes(**MessageToDict(res, preserving_proto_field_name=True)) )
            except Exception as e:
                logging.error(f"Error retrieving blockchainhashes_id: {e}")

        if blockchainhashes_name is not None:
            filter_list.append({"name": blockchainhashes_name})
        # if blockchainhashes_name_full is not None:
        #     filter_list.append({"name_full": blockchainhashes_name_full})
        if filter_dict is not None:
            filter_list.append(filter_dict)

        for filter_dict in filter_list:
            metadata = (("filters", (json.dumps(filter_dict))),)
            try:
                res = await self.blockchainhashes_client.List(
                        lara_django_data_pb2.BlockchainHashesListRequest(), metadata=metadata
                      )
                if raw:
                      results_list += res.results
                else:
                    # a queue could be used here to speed up the retrieval
                    results = res.results
                    results_list += [ data_dm.BlockchainHashes(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
            except Exception as e:
                logging.error(f"Error retrieving blockchainhashes_name: {e}")

        return results_list

    #@id_cache
    async def retrieve_id(self, 
                          blockchainhashes_name: str = None,
                          blockchainhashes_name_full: str = None,
                          iri: str = None,) -> str:
        if blockchainhashes_name is not None:
            filter_dict = {"name": blockchainhashes_name}
        elif blockchainhashes_name_full is not None:
            filter_dict = {"name_full": blockchainhashes_name_full}
        elif iri is not None:
            filter_dict = {"iri": iri}
            metadata = (("filters", (json.dumps(filter_dict))),)
            try:
                res = await self.blockchainhashes_client.List(
                    lara_django_data_pb2.BlockchainHashesListRequest(), metadata=metadata
                )
                results = res.results
            except Exception as e:
                logging.error(f"Error retrieving blockchainhashes_name: {e}")
            if len(results) > 0 and len(results) < 2:
                return results[0].blockchainhashes_id
            else:
                raise ValueError(f"Error retrieving blockchainhashes_id - no or too many results found.")
            
        
    async def list_items(self, filter_dict: dict = None, ids_only: bool = False) -> list[str]:
        if filter_dict is None:
            res = await self.blockchainhashes_client.List(lara_django_data_pb2.BlockchainHashesListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.blockchainhashes_client.List(
                lara_django_data_pb2.BlockchainHashesListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.blockchainhashes_id for item in res.results]
        else:
            return res.results
            

    async def list_full(self, filter_dict: dict = None) :
        if self.blockchainhashes_full_client is None:
            raise ValueError("No full client available.")
        if filter_dict is None:
            return await self.blockchainhashes_full_client.List(
                lara_django_data_pb2.BlockchainHashesFullListRequest()
            )
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            return await self.blockchainhashes_full_client.List(
                lara_django_data_pb2.BlockchainHashesFullListRequest(), metadata=metadata
            )

    @import_file(relation_resolver=lambda x: x)
    # @update_image
    # @update_file
    async def update(self, blockchainhashes : data_dm.BlockchainHashes = None) -> None:
        try:
            await self.blockchainhashes_client.Update(
                lara_django_data_pb2.BlockchainHashesRequest(**blockchainhashes.model_dump()
            ))
        except Exception as e:
            logging.error(f"Error updating blockchainhashes_id: {e}")

    async def delete(self, id_list: list[str] = None, filter_dict: dict = None) -> None:
        if filter_dict is not None:
            id_list = await self.list_items(filter_dict=filter_dict, ids_only=True)
    
        try:
            delete_coroutines = ( self.blockchainhashes_client.Destroy(lara_django_data_pb2.BlockchainHashesDestroyRequest(blockchainhashes_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)
            
        except Exception as e:
            logging.error(f"Error deleting blockchainhashes_id: {e}")

#_________________  Visualisation  ______________________


class VisualisationInterface:
    def __init__(self, channel: grpc.Channel = None, default_namespace_uri : str = None) -> None:
        self.default_namespace_uri = None
        self.default_namespace_id = None

        if default_namespace_uri is not None:
            self.default_namespace_uri = default_namespace_uri

        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel
        
        self.namespace_if = base_if.NamespaceInterface(channel)

        # self.visualisation_class_client = (
        #     lara_django_data_pb2_grpc.VisualisationclassByIRIControllerStub(channel)
        # )
        
        self.visualisation_client = lara_django_data_pb2_grpc.VisualisationControllerStub(channel)

        # this is required by the import_file decorator, please activate if needed for file upload
        # self.stub = self.visualisation_client
        # self.file_chunk = lara_django_data_pb2.FileChunk
        # self.update_item_id = "visualisation_id"

        # self.visualisation_full_client = lara_django_data_pb2_grpc.VisualisationFullControllerStub(
        #     channel
        # )

    @import_file(relation_resolver=lambda x: x)
    #@create_file  # activate if needed for file upload
    #@create_image  # activate if needed for image upload
    async def create(self,  visualisations:  list[data_dm.Visualisation] = None,  ids_only: bool = False, namespace_uri: str = None) -> Union[list[str], list[data_dm.Visualisation]]:
        # TODO: only retrieve default_namespace_id if not already retrieved and namespace_uri is None
        self.namespace_id = None
        # visualisationclass_id = None
        if namespace_uri is None and self.default_namespace_uri is not None:
            namespace_uri = self.default_namespace_uri
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        try:
            self.namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
        except Exception as e:
            logging.error(f"Error retrieving namespace_id: {e}")
        
        # some sample code for retrieving a class IRI and adding namespace and class to the model
        # if visualisation_class_iri is None:
        #     raise ValueError("No sample class IRI is given.")
        # try:
        #     res = await self.visualisation_class_client.Retrieve(
        #         lara_django_data_pb2.VisualisationclassRetrieveRequest(iri=visualisation_class_iri)
        #     )
        #     visualisationclass_id = res.visualisationclass_id 
        # except Exception as e:
        #     logging.error(f"Error retrieving visualisationclass_id: {e}")
        # for visualisation in visualisations:
        #     visualisation.namespace = self.namespace_id
        #     visualisation.visualisation_class = visualisationclass_id

        try:
            create_coroutines = ( self.visualisation_client.Create(lara_django_data_pb2.VisualisationRequest(**visualisation.model_dump())) for visualisation in visualisations )
            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [item.visualisation_id for item in res]
            else:
                return res

        except Exception as e:
            logging.error(f"Error creating visualisation: {e}")

    @export_file
    async def retrieve(
        self, 
        visualisation_id: str = None,
        visualisation_name: str = None,
        #visualisation_name_full: str = None,
        filter_dict: dict = None,
        raw : bool = False
        ) -> list[data_dm.Visualisation]:
        filter_list = []
        results_list = []
        if  visualisation_id is not None:
            try:
                res =  await self.visualisation_client.Retrieve(
                    lara_django_data_pb2.VisualisationRetrieveRequest(visualisation_id=visualisation_id) )
                if raw:
                    results_list.append(res)
                else:
                    results_list.append( data_dm.Visualisation(**MessageToDict(res, preserving_proto_field_name=True)) )
            except Exception as e:
                logging.error(f"Error retrieving visualisation_id: {e}")

        if visualisation_name is not None:
            filter_list.append({"name": visualisation_name})
        # if visualisation_name_full is not None:
        #     filter_list.append({"name_full": visualisation_name_full})
        if filter_dict is not None:
            filter_list.append(filter_dict)

        for filter_dict in filter_list:
            metadata = (("filters", (json.dumps(filter_dict))),)
            try:
                res = await self.visualisation_client.List(
                        lara_django_data_pb2.VisualisationListRequest(), metadata=metadata
                      )
                if raw:
                      results_list += res.results
                else:
                    # a queue could be used here to speed up the retrieval
                    results = res.results
                    results_list += [ data_dm.Visualisation(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
            except Exception as e:
                logging.error(f"Error retrieving visualisation_name: {e}")

        return results_list

    #@id_cache
    async def retrieve_id(self, 
                          visualisation_name: str = None,
                          visualisation_name_full: str = None,
                          iri: str = None,) -> str:
        if visualisation_name is not None:
            filter_dict = {"name": visualisation_name}
        elif visualisation_name_full is not None:
            filter_dict = {"name_full": visualisation_name_full}
        elif iri is not None:
            filter_dict = {"iri": iri}
            metadata = (("filters", (json.dumps(filter_dict))),)
            try:
                res = await self.visualisation_client.List(
                    lara_django_data_pb2.VisualisationListRequest(), metadata=metadata
                )
                results = res.results
            except Exception as e:
                logging.error(f"Error retrieving visualisation_name: {e}")
            if len(results) > 0 and len(results) < 2:
                return results[0].visualisation_id
            else:
                raise ValueError(f"Error retrieving visualisation_id - no or too many results found.")
            
        
    async def list_items(self, filter_dict: dict = None, ids_only: bool = False) -> list[str]:
        if filter_dict is None:
            res = await self.visualisation_client.List(lara_django_data_pb2.VisualisationListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.visualisation_client.List(
                lara_django_data_pb2.VisualisationListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.visualisation_id for item in res.results]
        else:
            return res.results
            

    async def list_full(self, filter_dict: dict = None) :
        if self.visualisation_full_client is None:
            raise ValueError("No full client available.")
        if filter_dict is None:
            return await self.visualisation_full_client.List(
                lara_django_data_pb2.VisualisationFullListRequest()
            )
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            return await self.visualisation_full_client.List(
                lara_django_data_pb2.VisualisationFullListRequest(), metadata=metadata
            )

    @import_file(relation_resolver=lambda x: x)
    # @update_image
    # @update_file
    async def update(self, visualisation : data_dm.Visualisation = None) -> None:
        try:
            await self.visualisation_client.Update(
                lara_django_data_pb2.VisualisationRequest(**visualisation.model_dump()
            ))
        except Exception as e:
            logging.error(f"Error updating visualisation_id: {e}")

    async def delete(self, id_list: list[str] = None, filter_dict: dict = None) -> None:
        if filter_dict is not None:
            id_list = await self.list_items(filter_dict=filter_dict, ids_only=True)
    
        try:
            delete_coroutines = ( self.visualisation_client.Destroy(lara_django_data_pb2.VisualisationDestroyRequest(visualisation_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)
            
        except Exception as e:
            logging.error(f"Error deleting visualisation_id: {e}")

#_________________  Data  ______________________


class DataInterface:
    def __init__(self, channel: grpc.Channel = None, default_namespace_uri : str = None) -> None:
        self.default_namespace_uri = None
        self.default_namespace_id = None

        if default_namespace_uri is not None:
            self.default_namespace_uri = default_namespace_uri

        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel
        
        self.namespace_if = base_if.NamespaceInterface(channel)

        # self.data_class_client = (
        #     lara_django_data_pb2_grpc.DataclassByIRIControllerStub(channel)
        # )
        
        self.data_client = lara_django_data_pb2_grpc.DataControllerStub(channel)

        # this is required by the import_file decorator, please activate if needed for file upload
        self.stub = self.data_client
        self.file_chunk = lara_django_data_pb2.FileChunk
        self.update_item_id = "data_id"

        # self.data_full_client = lara_django_data_pb2_grpc.DataFullControllerStub(
        #     channel
        # )

    @import_file(relation_resolver=lambda x: x)
    @create_file  # activate if needed for file upload
    #@create_image  # activate if needed for image upload
    async def create(self,  data:  list[data_dm.Data] = None,  ids_only: bool = False, namespace_uri: str = None) -> Union[list[str], list[data_dm.Data]]:
        # TODO: only retrieve default_namespace_id if not already retrieved and namespace_uri is None
        self.namespace_id = None
        # dataclass_id = None
        if namespace_uri is None and self.default_namespace_uri is not None:
            namespace_uri = self.default_namespace_uri
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        try:
            self.namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
        except Exception as e:
            logging.error(f"Error retrieving namespace_id: {e}")
        
        # some sample code for retrieving a class IRI and adding namespace and class to the model
        # if data_class_iri is None:
        #     raise ValueError("No sample class IRI is given.")
        # try:
        #     res = await self.data_class_client.Retrieve(
        #         lara_django_data_pb2.DataclassRetrieveRequest(iri=data_class_iri)
        #     )
        #     dataclass_id = res.dataclass_id 
        # except Exception as e:
        #     logging.error(f"Error retrieving dataclass_id: {e}")

        try:
            coroutines = []
            for datum in data:
                datum.namespace = self.namespace_id
                #     datum.data_class = dataclass_id
                datum_dict = datum.model_dump()

                try:
                    data_struct = Struct()

                    # string to json
                    data_json = json.loads(datum_dict.get("data_json"))
                    data_struct.update(data_json)
                    datum_dict["data_json"] = data_struct
                except Exception as e:
                    logging.error(f"Error converting data_json to Struct: {e}")
                
                # convert metadata to struct

                try:
                    metadata_struct = Struct()
                    metadata_json = json.loads(datum_dict.get("metadata"))
                    metadata_struct.update(metadata_json)
                    datum_dict["metadata"] = metadata_struct

                except Exception as e:
                    logging.error(f"Error converting metadata to Struct: {e}")

                #create_coroutines = ( self.data_client.Create(lara_django_data_pb2.DataRequest(**data.model_dump())) for data in data )

                coroutines.append(self.data_client.Create(lara_django_data_pb2.DataRequest(**datum_dict)))

            res = await asyncio.gather(*coroutines)
            if ids_only:
                return [item.data_id for item in res]
            else:
                return res

        except Exception as e:
            logging.error(f"Error creating data: {e}")

    @export_file
    async def retrieve(
        self, 
        data_id: str = None,
        data_name: str = None,
        #data_name_full: str = None,
        filter_dict: dict = None,
        raw : bool = False
        ) -> list[data_dm.Data]:
        filter_list = []
        results_list = []
        if  data_id is not None:
            try:
                res =  await self.data_client.Retrieve(
                    lara_django_data_pb2.DataRetrieveRequest(data_id=data_id) )
                if raw:
                    results_list.append(res)
                else:
                    results_list.append( data_dm.Data(**MessageToDict(res, preserving_proto_field_name=True)) )
            except Exception as e:
                logging.error(f"Error retrieving data_id: {e}")

        if data_name is not None:
            filter_list.append({"name": data_name})
        # if data_name_full is not None:
        #     filter_list.append({"name_full": data_name_full})
        if filter_dict is not None:
            filter_list.append(filter_dict)

        for filter_dict in filter_list:
            metadata = (("filters", (json.dumps(filter_dict))),)
            try:
                res = await self.data_client.List(
                        lara_django_data_pb2.DataListRequest(), metadata=metadata
                      )
                if raw:
                      results_list += res.results
                else:
                    # a queue could be used here to speed up the retrieval
                    results = res.results
                    results_list += [ data_dm.Data(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
            except Exception as e:
                logging.error(f"Error retrieving data_name: {e}")

        return results_list

    #@id_cache
    async def retrieve_id(self, 
                          data_name: str = None,
                          data_name_full: str = None,
                          iri: str = None,) -> str:
        if data_name is not None:
            filter_dict = {"name": data_name}
        elif data_name_full is not None:
            filter_dict = {"name_full": data_name_full}
        elif iri is not None:
            filter_dict = {"iri": iri}
            metadata = (("filters", (json.dumps(filter_dict))),)
            try:
                res = await self.data_client.List(
                    lara_django_data_pb2.DataListRequest(), metadata=metadata
                )
                results = res.results
            except Exception as e:
                logging.error(f"Error retrieving data_name: {e}")
            if len(results) > 0 and len(results) < 2:
                return results[0].data_id
            else:
                raise ValueError(f"Error retrieving data_id - no or too many results found.")
            
        
    async def list_items(self, filter_dict: dict = None, ids_only: bool = False) -> list[str]:
        if filter_dict is None:
            res = await self.data_client.List(lara_django_data_pb2.DataListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.data_client.List(
                lara_django_data_pb2.DataListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.data_id for item in res.results]
        else:
            return res.results
            

    async def list_full(self, filter_dict: dict = None) :
        if self.data_full_client is None:
            raise ValueError("No full client available.")
        if filter_dict is None:
            return await self.data_full_client.List(
                lara_django_data_pb2.DataFullListRequest()
            )
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            return await self.data_full_client.List(
                lara_django_data_pb2.DataFullListRequest(), metadata=metadata
            )

    @import_file(relation_resolver=lambda x: x)
    # @update_image
    @update_file
    async def update(self, data : data_dm.Data = None) -> None:
        try:
            await self.data_client.Update(
                lara_django_data_pb2.DataRequest(**data.model_dump()
            ))
        except Exception as e:
            logging.error(f"Error updating data_id: {e}")

    async def delete(self, id_list: list[str] = None, filter_dict: dict = None) -> None:
        if filter_dict is not None:
            id_list = await self.list_items(filter_dict=filter_dict, ids_only=True)
    
        try:
            delete_coroutines = ( self.data_client.Destroy(lara_django_data_pb2.DataDestroyRequest(data_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)
            
        except Exception as e:
            logging.error(f"Error deleting data_id: {e}")

#_________________  DataSubset  ______________________


class DataSubsetInterface:
    def __init__(self, channel: grpc.Channel = None, default_namespace_uri : str = None) -> None:
        self.default_namespace_uri = None
        self.default_namespace_id = None

        if default_namespace_uri is not None:
            self.default_namespace_uri = default_namespace_uri

        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel
        
        self.namespace_if = base_if.NamespaceInterface(channel)

        # self.datasubset_class_client = (
        #     lara_django_data_pb2_grpc.DataSubsetclassByIRIControllerStub(channel)
        # )
        
        self.datasubset_client = lara_django_data_pb2_grpc.DataSubsetControllerStub(channel)

        # this is required by the import_file decorator, please activate if needed for file upload
        # self.stub = self.datasubset_client
        # self.file_chunk = lara_django_data_pb2.FileChunk
        # self.update_item_id = "datasubset_id"

        # self.datasubset_full_client = lara_django_data_pb2_grpc.DataSubsetFullControllerStub(
        #     channel
        # )

    @import_file(relation_resolver=lambda x: x)
    #@create_file  # activate if needed for file upload
    #@create_image  # activate if needed for image upload
    async def create(self,  datasubsets:  list[data_dm.DataSubset] = None,  ids_only: bool = False, namespace_uri: str = None) -> Union[list[str], list[data_dm.DataSubset]]:
        # TODO: only retrieve default_namespace_id if not already retrieved and namespace_uri is None
        self.namespace_id = None
        # datasubsetclass_id = None
        if namespace_uri is None and self.default_namespace_uri is not None:
            namespace_uri = self.default_namespace_uri
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        try:
            self.namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
        except Exception as e:
            logging.error(f"Error retrieving namespace_id: {e}")
        
        # some sample code for retrieving a class IRI and adding namespace and class to the model
        # if datasubset_class_iri is None:
        #     raise ValueError("No sample class IRI is given.")
        # try:
        #     res = await self.datasubset_class_client.Retrieve(
        #         lara_django_data_pb2.DataSubsetclassRetrieveRequest(iri=datasubset_class_iri)
        #     )
        #     datasubsetclass_id = res.datasubsetclass_id 
        # except Exception as e:
        #     logging.error(f"Error retrieving datasubsetclass_id: {e}")
        # for datasubset in datasubsets:
        #     datasubset.namespace = self.namespace_id
        #     datasubset.datasubset_class = datasubsetclass_id

        try:
            create_coroutines = ( self.datasubset_client.Create(lara_django_data_pb2.DataSubsetRequest(**datasubset.model_dump())) for datasubset in datasubsets )
            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [item.datasubset_id for item in res]
            else:
                return res

        except Exception as e:
            logging.error(f"Error creating datasubset: {e}")

    @export_file
    async def retrieve(
        self, 
        datasubset_id: str = None,
        datasubset_name: str = None,
        #datasubset_name_full: str = None,
        filter_dict: dict = None,
        raw : bool = False
        ) -> list[data_dm.DataSubset]:
        filter_list = []
        results_list = []
        if  datasubset_id is not None:
            try:
                res =  await self.datasubset_client.Retrieve(
                    lara_django_data_pb2.DataSubsetRetrieveRequest(datasubset_id=datasubset_id) )
                if raw:
                    results_list.append(res)
                else:
                    results_list.append( data_dm.DataSubset(**MessageToDict(res, preserving_proto_field_name=True)) )
            except Exception as e:
                logging.error(f"Error retrieving datasubset_id: {e}")

        if datasubset_name is not None:
            filter_list.append({"name": datasubset_name})
        # if datasubset_name_full is not None:
        #     filter_list.append({"name_full": datasubset_name_full})
        if filter_dict is not None:
            filter_list.append(filter_dict)

        for filter_dict in filter_list:
            metadata = (("filters", (json.dumps(filter_dict))),)
            try:
                res = await self.datasubset_client.List(
                        lara_django_data_pb2.DataSubsetListRequest(), metadata=metadata
                      )
                if raw:
                      results_list += res.results
                else:
                    # a queue could be used here to speed up the retrieval
                    results = res.results
                    results_list += [ data_dm.DataSubset(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
            except Exception as e:
                logging.error(f"Error retrieving datasubset_name: {e}")

        return results_list

    #@id_cache
    async def retrieve_id(self, 
                          datasubset_name: str = None,
                          datasubset_name_full: str = None,
                          iri: str = None,) -> str:
        if datasubset_name is not None:
            filter_dict = {"name": datasubset_name}
        elif datasubset_name_full is not None:
            filter_dict = {"name_full": datasubset_name_full}
        elif iri is not None:
            filter_dict = {"iri": iri}
            metadata = (("filters", (json.dumps(filter_dict))),)
            try:
                res = await self.datasubset_client.List(
                    lara_django_data_pb2.DataSubsetListRequest(), metadata=metadata
                )
                results = res.results
            except Exception as e:
                logging.error(f"Error retrieving datasubset_name: {e}")
            if len(results) > 0 and len(results) < 2:
                return results[0].datasubset_id
            else:
                raise ValueError(f"Error retrieving datasubset_id - no or too many results found.")
            
        
    async def list_items(self, filter_dict: dict = None, ids_only: bool = False) -> list[str]:
        if filter_dict is None:
            res = await self.datasubset_client.List(lara_django_data_pb2.DataSubsetListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.datasubset_client.List(
                lara_django_data_pb2.DataSubsetListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.datasubset_id for item in res.results]
        else:
            return res.results
            

    async def list_full(self, filter_dict: dict = None) :
        if self.datasubset_full_client is None:
            raise ValueError("No full client available.")
        if filter_dict is None:
            return await self.datasubset_full_client.List(
                lara_django_data_pb2.DataSubsetFullListRequest()
            )
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            return await self.datasubset_full_client.List(
                lara_django_data_pb2.DataSubsetFullListRequest(), metadata=metadata
            )

    @import_file(relation_resolver=lambda x: x)
    # @update_image
    # @update_file
    async def update(self, datasubset : data_dm.DataSubset = None) -> None:
        try:
            await self.datasubset_client.Update(
                lara_django_data_pb2.DataSubsetRequest(**datasubset.model_dump()
            ))
        except Exception as e:
            logging.error(f"Error updating datasubset_id: {e}")

    async def delete(self, id_list: list[str] = None, filter_dict: dict = None) -> None:
        if filter_dict is not None:
            id_list = await self.list_items(filter_dict=filter_dict, ids_only=True)
    
        try:
            delete_coroutines = ( self.datasubset_client.Destroy(lara_django_data_pb2.DataSubsetDestroyRequest(datasubset_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)
            
        except Exception as e:
            logging.error(f"Error deleting datasubset_id: {e}")

#_________________  Evaluation  ______________________


class EvaluationInterface:
    def __init__(self, channel: grpc.Channel = None, default_namespace_uri : str = None) -> None:
        self.default_namespace_uri = None
        self.default_namespace_id = None

        if default_namespace_uri is not None:
            self.default_namespace_uri = default_namespace_uri

        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel
        
        self.namespace_if = base_if.NamespaceInterface(channel)

        # self.evaluation_class_client = (
        #     lara_django_data_pb2_grpc.EvaluationclassByIRIControllerStub(channel)
        # )
        
        self.evaluation_client = lara_django_data_pb2_grpc.EvaluationControllerStub(channel)

        # this is required by the import_file decorator, please activate if needed for file upload
        # self.stub = self.evaluation_client
        # self.file_chunk = lara_django_data_pb2.FileChunk
        # self.update_item_id = "evaluation_id"

        # self.evaluation_full_client = lara_django_data_pb2_grpc.EvaluationFullControllerStub(
        #     channel
        # )

    @import_file(relation_resolver=lambda x: x)
    #@create_file  # activate if needed for file upload
    #@create_image  # activate if needed for image upload
    async def create(self,  evaluations:  list[data_dm.Evaluation] = None,  ids_only: bool = False, namespace_uri: str = None) -> Union[list[str], list[data_dm.Evaluation]]:
        # TODO: only retrieve default_namespace_id if not already retrieved and namespace_uri is None
        self.namespace_id = None
        # evaluationclass_id = None
        if namespace_uri is None and self.default_namespace_uri is not None:
            namespace_uri = self.default_namespace_uri
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        try:
            self.namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
        except Exception as e:
            logging.error(f"Error retrieving namespace_id: {e}")
        
        # some sample code for retrieving a class IRI and adding namespace and class to the model
        # if evaluation_class_iri is None:
        #     raise ValueError("No sample class IRI is given.")
        # try:
        #     res = await self.evaluation_class_client.Retrieve(
        #         lara_django_data_pb2.EvaluationclassRetrieveRequest(iri=evaluation_class_iri)
        #     )
        #     evaluationclass_id = res.evaluationclass_id 
        # except Exception as e:
        #     logging.error(f"Error retrieving evaluationclass_id: {e}")
        # for evaluation in evaluations:
        #     evaluation.namespace = self.namespace_id
        #     evaluation.evaluation_class = evaluationclass_id

        try:
            create_coroutines = ( self.evaluation_client.Create(lara_django_data_pb2.EvaluationRequest(**evaluation.model_dump())) for evaluation in evaluations )
            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [item.evaluation_id for item in res]
            else:
                return res

        except Exception as e:
            logging.error(f"Error creating evaluation: {e}")

    @export_file
    async def retrieve(
        self, 
        evaluation_id: str = None,
        evaluation_name: str = None,
        #evaluation_name_full: str = None,
        filter_dict: dict = None,
        raw : bool = False
        ) -> list[data_dm.Evaluation]:
        filter_list = []
        results_list = []
        if  evaluation_id is not None:
            try:
                res =  await self.evaluation_client.Retrieve(
                    lara_django_data_pb2.EvaluationRetrieveRequest(evaluation_id=evaluation_id) )
                if raw:
                    results_list.append(res)
                else:
                    results_list.append( data_dm.Evaluation(**MessageToDict(res, preserving_proto_field_name=True)) )
            except Exception as e:
                logging.error(f"Error retrieving evaluation_id: {e}")

        if evaluation_name is not None:
            filter_list.append({"name": evaluation_name})
        # if evaluation_name_full is not None:
        #     filter_list.append({"name_full": evaluation_name_full})
        if filter_dict is not None:
            filter_list.append(filter_dict)

        for filter_dict in filter_list:
            metadata = (("filters", (json.dumps(filter_dict))),)
            try:
                res = await self.evaluation_client.List(
                        lara_django_data_pb2.EvaluationListRequest(), metadata=metadata
                      )
                if raw:
                      results_list += res.results
                else:
                    # a queue could be used here to speed up the retrieval
                    results = res.results
                    results_list += [ data_dm.Evaluation(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
            except Exception as e:
                logging.error(f"Error retrieving evaluation_name: {e}")

        return results_list

    #@id_cache
    async def retrieve_id(self, 
                          evaluation_name: str = None,
                          evaluation_name_full: str = None,
                          iri: str = None,) -> str:
        if evaluation_name is not None:
            filter_dict = {"name": evaluation_name}
        elif evaluation_name_full is not None:
            filter_dict = {"name_full": evaluation_name_full}
        elif iri is not None:
            filter_dict = {"iri": iri}
            metadata = (("filters", (json.dumps(filter_dict))),)
            try:
                res = await self.evaluation_client.List(
                    lara_django_data_pb2.EvaluationListRequest(), metadata=metadata
                )
                results = res.results
            except Exception as e:
                logging.error(f"Error retrieving evaluation_name: {e}")
            if len(results) > 0 and len(results) < 2:
                return results[0].evaluation_id
            else:
                raise ValueError(f"Error retrieving evaluation_id - no or too many results found.")
            
        
    async def list_items(self, filter_dict: dict = None, ids_only: bool = False) -> list[str]:
        if filter_dict is None:
            res = await self.evaluation_client.List(lara_django_data_pb2.EvaluationListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.evaluation_client.List(
                lara_django_data_pb2.EvaluationListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.evaluation_id for item in res.results]
        else:
            return res.results
            

    async def list_full(self, filter_dict: dict = None) :
        if self.evaluation_full_client is None:
            raise ValueError("No full client available.")
        if filter_dict is None:
            return await self.evaluation_full_client.List(
                lara_django_data_pb2.EvaluationFullListRequest()
            )
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            return await self.evaluation_full_client.List(
                lara_django_data_pb2.EvaluationFullListRequest(), metadata=metadata
            )

    @import_file(relation_resolver=lambda x: x)
    # @update_image
    # @update_file
    async def update(self, evaluation : data_dm.Evaluation = None) -> None:
        try:
            await self.evaluation_client.Update(
                lara_django_data_pb2.EvaluationRequest(**evaluation.model_dump()
            ))
        except Exception as e:
            logging.error(f"Error updating evaluation_id: {e}")

    async def delete(self, id_list: list[str] = None, filter_dict: dict = None) -> None:
        if filter_dict is not None:
            id_list = await self.list_items(filter_dict=filter_dict, ids_only=True)
    
        try:
            delete_coroutines = ( self.evaluation_client.Destroy(lara_django_data_pb2.EvaluationDestroyRequest(evaluation_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)
            
        except Exception as e:
            logging.error(f"Error deleting evaluation_id: {e}")

