"""_____________________________________________________________________

:PROJECT: LARAsuite

*generate High Level Interface *

:details: generate_high_level_interface - a High Level Interface generator for LARA-django.
          This is a LARA-django-base management command to generate a High Level Interface for a
          given app. The generated interface can be used to generate various validators.
          s. https://linkml.io/linkml/index.html

          usage:
            lara-django-dev generate_high_level_interface <lara_app>

          once the interface is generated, it can be used to generate a pydantic model (from the LinkML package) with:
            gen-pydantic <lara_app>_interface.yaml

:authors: mark doerr <mark.doerr@uni-greifswald.de>

.. note:: -
.. todo:: - async
          - delete: return no list.
________________________________________________________________________
"""

import logging
import os

from django.apps import apps
from django.conf import settings
from django.core.management.base import LabelCommand
from django.db import models

# Configurable constants
MAX_LINE_WIDTH = getattr(settings, "MAX_LINE_WIDTH", 120)
INDENT_WIDTH = getattr(settings, "INDENT_WIDTH", 2)

INTERFACE_FILE_HEADER = """\"\"\"_____________________________________________________________________

:PROJECT: LARAsuite

*{lara_app} high_level_interface *

:details: {lara_app} high_level_interface.
         - generated with 'lara-django-dev generate_high_level_interface {lara_app}
:authors: {lara_author} <{lara_author_email}>

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


def indent(level=1, text="", end="\n"):
    return "\n".join([(level * INDENT_WIDTH * " ") + line for line in text.split("\n")]) + end


def lower_strip(s):
    return s.lower().replace(" ", "").strip()


class Command(LabelCommand):
    help = """Generate a High Level Interface for a given LARA app (models)"""
    args = "[app_name, author_name, author_email]"
    can_import_settings = True

    def add_arguments(self, parser):
        parser.add_argument("app_name", help="Name of the LARA app to generate the LinML Schema for")

        parser.add_argument("--author-name", help="Name of the author", default="lara")
        parser.add_argument("--author-email", help="Email of the author", default="lara@lara.org")
        parser.add_argument(
            "--force",
            action="store_true",
            help="overwrite existing files",
            default=False,
        )
        # parser.add_argument('model_name', nargs='*')

    # @signalcommand
    def handle(self, *args, **options):
        self.app_name = options["app_name"]

        logging.warning("!! only a scaffold is generated, please check/add content to the generated files !!!")

        try:
            app = apps.get_app_config(self.app_name)
        except LookupError:
            self.stderr.write("This command requires an existing app name as argument")
            self.stderr.write("Available apps:")
            app_labels = [app.label for app in apps.get_app_configs()]
            for label in sorted(app_labels):
                self.stderr.write("    %s" % label)
            return

        model_res = []
        # for arg in options['model_name']:
        #     model_res.append(re.compile(arg, re.IGNORECASE))

        HigLevelInterfaceApp(app, model_res, options)


class HigLevelInterfaceApp:
    def __init__(self, app_config, model_res, options):
        self.app_config = app_config
        self.model_res = model_res

        self.app_name = options["app_name"]
        self.base_app_name = self.app_name[len("lara_django_") :]  # self.app_name.split("_")[2]
        self.force_overwrite = options["force"]
        self.author_name = options["author_name"]
        self.author_email = options["author_email"]

        self.interface_str = ""
        self.model_names = [model.__name__ for model in self.app_config.get_models()]

        self.generate_interface()

    def generate_interface(self):
        self.generate_interface_header()
        self.generate_interface_imports()
        self.generate_classes()

        # saving interface to file
        interface_filename = f"{self.app_name}_interface.py"

        print(f"writing {interface_filename} to {os.getcwd()}")
        # print(self.interface_str)

        if os.path.isfile(interface_filename):
            if self.force_overwrite:
                with open(interface_filename, "w") as f:
                    f.write(self.interface_str)
            else:
                append = input(
                    f"{interface_filename} already exists,\n press 'a' to append,\n press 'o' to overwrite,\n or 'n' to do nothing: "  # noqa: E501
                )
                if append.lower() == "a":
                    with open(interface_filename, "a") as f:
                        f.write(self.interface_str)
                elif append.lower() == "o":
                    with open(interface_filename, "w") as f:
                        f.write(self.interface_str)
        else:
            with open(interface_filename, "w") as f:
                f.write(self.interface_str)

    def generate_interface_header(self):
        self.interface_str += INTERFACE_FILE_HEADER.format(
            lara_app=self.app_name,
            lara_author=self.author_name,
            lara_author_email=self.author_email,
        )

    def generate_interface_imports(self):
        self.interface_str += f"""

import json
import asyncio
import logging
import grpc

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 {self.app_name}_grpc.v1.{self.app_name}_pb2 as {self.app_name}_pb2
import {self.app_name}_grpc.v1.{self.app_name}_pb2_grpc as {self.app_name}_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, import_model_instance_from_file, export_model_instance_to_file, upload_file, download_file, update_file, upload_image, update_image, download_image, id_cache

import  {self.app_name}_grpc.{self.app_name}_data_model as {self.base_app_name}_dm

logger = logging.getLogger(__name__)

\n\n"""

    def generate_classes(self):
        for model in self.app_config.get_models():
            has_namespace = False
            has_image = False
            has_file = False

            field_names = [field.name for field in model._meta.get_fields()]

            for field in model._meta.get_fields():
                match field:
                    case models.fields.files.FileField():
                        # ImageField is a subclass of FileField, determine, if it is an ImageField
                        if field.__class__.__name__ == "ImageField":
                            has_image = True
                        else:
                            has_file = True

                if field.name == "namespace":
                    has_namespace = True

            self.interface_str += (
                self.generate_class_init(model, has_namespace=has_namespace, has_file=has_file, has_image=has_image)
                + self.generate_class_create(model, has_namespace=has_namespace, has_file=has_file, has_image=has_image)
                + self.generate_class_retrieve(
                    model, has_namespace=has_namespace, has_file=has_file, has_image=has_image
                )
                + self.generate_class_list(model, has_namespace=has_namespace, has_file=has_file, has_image=has_image)
                + self.generate_class_update(model, has_namespace=has_namespace, has_file=has_file, has_image=has_image)
                + self.generate_class_delete(model, has_namespace=has_namespace, has_file=has_file, has_image=has_image)
            )

    def generate_class_init(self, model, has_namespace=False, has_file=False, has_image=False):
        model_name = model.__name__
        model_name_low = model.__name__.lower()

        file_image_id_stub = f"""
        self.item_id = None
        self.update_item_id = "{model_name_low}_id"
        self.stub = self.{model_name_low}_client
"""

        file_chunk = f"""
        self.file_download_info = {self.app_name}_pb2.FileDownloadInfo
        self.file_upload_chunk = {self.app_name}_pb2.FileUploadChunk
"""

        image_chunk = f"""
        self.image_upload_chunk = {self.app_name}_pb2.ImageUploadChunk
"""

        if has_namespace:
            return f"""#_________________  {model_name}  ______________________\n\n
class {model_name}Interface:
    \"\"\"Initialize the {model_name}Interface.\"\"\"

    def __init__(self, channel: grpc.Channel = None, 
                       default_namespace_uri : str = None,
                       default_namespace_id : str = None,
                       namespace_if = None ) -> None:
        self._default_namespace_uri = default_namespace_uri
        self._default_namespace_id = default_namespace_id

        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel

        self.namespace_if = namespace_if if namespace_if is not None else base_if.NamespaceInterface(channel)

        # self.{model_name_low}_class_client = (
        #     {self.app_name}_pb2_grpc.{model_name}classByIRIControllerStub(channel)
        # )

        self.{model_name_low}_client = {self.app_name}_pb2_grpc.{model_name}ControllerStub(channel)
        {file_image_id_stub if has_file or has_image else ""}{file_chunk if has_file else ""}{image_chunk if has_image else ""}

        # self.{model_name_low}_full_client = {self.app_name}_pb2_grpc.{model_name}FullControllerStub(
        #     channel
        # )

    @property
    def default_namespace_uri(self):
        return self._default_namespace_uri

    @property
    def default_namespace_id(self):
        return self._default_namespace_id

    @classmethod
    async def build_if(cls, channel: grpc.Channel = None, default_namespace_uri: str = None):

        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel

        namespace_if = base_if.NamespaceInterface(channel)
        try:
            default_namespace_id = await namespace_if.retrieve_id(namespace_uri=default_namespace_uri)
        except Exception as e:
            logger.error(f"Error retrieving namespace_id: {{e}}")

        return cls(channel=channel, default_namespace_uri=default_namespace_uri,  
                   default_namespace_id=default_namespace_id, namespace_if=namespace_if)

    async def set_default_namespace_uri(self, namespace_uri: str = None):
        if namespace_uri is not None:
            self._default_namespace_uri = namespace_uri.strip()
        try:
            self._default_namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
        except Exception as e:
            logger.error(f"Error retrieving namespace_id: {{e}}")
    """
        ###  no namespace
        return f"""#_________________  {model_name}  ______________________\n\n
class {model_name}Interface:
    \"\"\"Initialize the {model_name}Interface.\"\"\"

    def __init__(self, channel: grpc.Channel = None) -> None:
        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel

        self.{model_name_low}_client = {self.app_name}_pb2_grpc.{model_name}ControllerStub(channel)
        {file_image_id_stub if has_file or has_image else ""}{file_chunk if has_file else ""}{image_chunk if has_image else ""}
    """

    # replace variable names by shortcuts defined above
    def generate_class_create(self, model, has_namespace=False, has_file=False, has_image=False):
        model_name = model.__name__
        model_name_low = model.__name__.lower()
        # model_verbose_name = model._meta.verbose_name
        model_verbose_name_plural = lower_strip(model._meta.verbose_name_plural)

        upload_file_decorator = "\n    @upload_file  # needed for file upload"
        upload_image_decorator = "\n    @upload_image  # needed for image upload"

        model_class_id = f"{model_name_low}class_id = None"
        if has_namespace:
            return f"""
    @import_model_instance_from_file(){upload_file_decorator if has_file else ""}{upload_image_decorator if has_image else ""}
    async def create(self,  {lower_strip(model_verbose_name_plural)}:  list[{self.base_app_name}_dm.{model_name}] | None = None,
                            ids_only: bool = False,
                            namespace_uri: str = None) -> list[str] | list[{self.base_app_name}_dm.{model_name}]:
        \"\"\"
        Create a list of {model_name_low} instances.

            Returns a list of {model_name_low} data model objects or ids_only.

        :param {lower_strip(model_verbose_name_plural)}: list of {model_name_low} data model objects
        :param ids_only: return only the ids of the created instances
        :return: list of {model_name_low} data model objects or ids_only
        \"\"\"
        {model_class_id if has_namespace else ""}
        namespace_id = None
        if namespace_uri is None and self._default_namespace_uri is not None:
            namespace_uri = self._default_namespace_uri
            namespace_id = self._default_namespace_id
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        if namespace_id is None:
            try:
                namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
            except Exception as e:
                logger.error(f"Error retrieving namespace_id: {{e}}")

        # some sample code for retrieving a class IRI and adding namespace and class to the model
        # if {model_name_low}_class_iri is None:
        #     raise ValueError("No sample class IRI is given.")
        # try:
        #     res = await self.{model_name_low}_class_client.Retrieve(
        #         {self.app_name}_pb2.{model_name}classRetrieveRequest(iri={model_name_low}_class_iri)
        #     )
        #     {model_name_low}class_id = res.{model_name_low}class_id
        # except Exception as e:
        #     logger.error(f"Error retrieving {model_name_low}class_id: {{e}}")
        for {model_name_low} in {model_verbose_name_plural}:
            if {model_name_low}.namespace == "" or {model_name_low}.namespace == "default":
                    {model_name_low}.namespace = namespace_id
            # {model_name_low}.{model_name_low}_class = {model_name_low}class_id # uncomment if a model class is used

        try:
            create_coroutines = ( self.{model_name_low}_client.Create({self.app_name}_pb2.{model_name}Request(**{model_name_low}.model_dump())) for {model_name_low} in {model_verbose_name_plural} )
            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [ MessageToDict(item, preserving_proto_field_name=True).get("{model_name_low}_id") for item in res ]
            else:
                return res

        except Exception as e:
            logger.error(f"Error creating {model_name_low}: {{e}}")
        """
        ###  no namespace
        return f"""
    @import_model_instance_from_file(){upload_file_decorator if has_file else ""}{upload_image_decorator if has_image else ""}
    async def create(self,  {model_verbose_name_plural}:  list[{self.base_app_name}_dm.{model_name}] | None = None,  ids_only: bool = False) -> list[str] | list[{self.base_app_name}_dm.{model_name}]:
        \"\"\"
        Create a list of {model_name_low} instances.

            Returns a list of {model_name_low} data model objects or ids_only.
        :param {model_verbose_name_plural}: list of {model_name_low} data model objects
        :param ids_only: return only the ids of the created instances
        :return: list of {model_name_low} data model objects or ids_only
        \"\"\"
        try:
            create_coroutines = ( self.{model_name_low}_client.Create({self.app_name}_pb2.{model_name}Request(**{model_name_low}.model_dump())) for {model_name_low} in {model_verbose_name_plural} )
            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [item.{model_name_low}_id for item in res]
            else:
                return res

        except Exception as e:
            logger.error(f"Error creating {model_name_low}: {{e}}")
        """

    def generate_class_retrieve(self, model, has_namespace=False, has_file=False, has_image=False):
        model_name = model.__name__
        model_name_low = model.__name__.lower()

        download_file_decorator = "\n    @download_file  # needed for file download"
        download_image_decorator = "\n    @download_image  # needed for image download"

        get_file_method = f"""
    @download_file  # needed for file download
    async def get_file(self, {model_name_low}_id : str | None = None, id_only : bool = True ) -> bytes | None:
        \"\"\"Retrieve a file by data_id.\"\"\"
        self.item_id = {model_name_low}_id
        """

        get_file_by_name_namespace_method = f"""
    async def get_file_by_name(self, name: str | None = None,
                                namespace_uri : str | None = None,
                                filename_target : str | None = None,
                                as_byte_str : bool | None = None ) -> bytes | None:
        \"\"\"Retrieve a file by name.\"\"\"
        if namespace_uri is None and self._default_namespace_uri is not None:
            namespace_uri = self._default_namespace_uri
            # logger.warning(f"no namespace_uri provided, using default namespace: {{namespace_uri}}")
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        try:
            {model_name_low}_id = await self.get_by_name(name=name, namespace_uri=namespace_uri, id_only=True)
            return await self.get_file({model_name_low}_id={model_name_low}_id, filename_target=filename_target, as_byte_str=as_byte_str, get_file=True)
        except Exception as e:
            logger.error(f"Error retrieving {model_name_low} name: {{e}}")
        """  # noqa: E501

        get_file_by_name_method = f"""
    async def get_file_by_name(self, name: str | None = None,
                                filename_target : str | None = None,
                                as_byte_str : bool | None = None ) -> bytes | None:
        \"\"\"Retrieve a file by name.\"\"\"
        try:
            {model_name_low}_id = await self.get_by_name(name=name, id_only=True)
            return await self.get_file({model_name_low}_id={model_name_low}_id, filename_target=filename_target, as_byte_str=as_byte_str,  get_file=True)
        except Exception as e:
            logger.error(f"Error retrieving {model_name_low} name: {{e}}")
        """  # noqa: E501
        get_image_method = f"""
    @download_image  # needed for image download
    async def get_image(self, {model_name_low}_id : str = None, id_only : bool = True) -> bytes | None:
        \"\"\"Retrieve an image by data_id.\"\"\"
        self.item_id = {model_name_low}_id
        """

        if has_namespace:
            return f"""
    @export_model_instance_to_file
    async def retrieve(
        self,
        namespace_uri: str | None = None,
        {model_name_low}_id: str | None = None,
        name: str | None = None,
        # name_display: str | None = None,
        filter_dict: dict | None = None,
        raw : bool = False
        ) -> list[{self.base_app_name}_dm.{model_name}]:
        \"\"\"
        Retrieve a list of {model_name_low} instances by {model_name_low}_id, name or filter_dict.

            Returns a list of {model_name_low} data model objects.
                If raw is True, the raw protobuf objects are returned.
                If namespace_uri is "default", the default namespace_uri is used.
        \"\"\"
        if namespace_uri == "default":
            if self._default_namespace_uri is not None:
                namespace_uri = self._default_namespace_uri
            else:
                raise ValueError("No default namespace URI is given.")

        results_list = []
        if  {model_name_low}_id is not None:
            try:
                res =  await self.{model_name_low}_client.Retrieve(
                    {self.app_name}_pb2.{model_name}RetrieveRequest({model_name_low}_id={model_name_low}_id) )
                if raw:
                    results_list.append(res)
                else:
                    results_list.append( {self.base_app_name}_dm.{model_name}(**MessageToDict(res, preserving_proto_field_name=True)) )
            except Exception as e:
                logger.error(f"Error retrieving {model_name_low}_id: {{e}}")

        if filter_dict is None:
            filter_dict = {{}}

        if name is not None:
            filter_dict["name"] = name
        if namespace_uri is not None:
            filter_dict["namespace__uri"] = namespace_uri
        # if name_display is not None:
        #    filter_dict["name_display"] = name_display

        metadata = (("filters", (json.dumps(filter_dict))),)
        try:
            res = await self.{model_name_low}_client.List(
                    {self.app_name}_pb2.{model_name}ListRequest(), metadata=metadata
                    )
            if raw:
                    results_list += res.results
            else:
                # :TODO: a queue could be used here to speed up the retrieval
                results = res.results
                results_list += [ {self.base_app_name}_dm.{model_name}(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
        except Exception as e:
            logger.error(f"Error retrieving {model_name_low} name: {{e}}")

        return results_list

    #@id_cache
    async def retrieve_id(self,
                          name: str | None = None,
                          # name_display: str = None,
                          namespace_uri : str | None = None,
                          filter_dict : dict | None = None,
                          iri: str | None = None) -> str | None:
        \"\"\"Retrieve the {model_name_low}_id for a given name, name_display or iri.\"\"\"

        results_list = await self.retrieve(name=name, 
                        namespace_uri=namespace_uri, filter_dict=filter_dict, raw=True)

        if len(results_list) == 1 :
            return results_list[0].{model_name_low}_id
        else:
            raise ValueError(f"Error retrieving {model_name_low}_id - no or too many results found.")
    {download_file_decorator if has_file else ""}{download_image_decorator if has_image else ""}
    async def get_by_id(self, {model_name_low}_id: str = None, id_only: bool = False) -> {self.base_app_name}_dm.{model_name}:
        \"\"\"Retrieve a {model_name_low} data model by {model_name_low}_id.\"\"\"
        try:
            res = await self.{model_name_low}_client.Retrieve(
                {self.app_name}_pb2.{model_name}RetrieveRequest({model_name_low}_id={model_name_low}_id)
            )
            item = {self.base_app_name}_dm.{model_name}(**MessageToDict(res, preserving_proto_field_name=True))
            self.item_id = item.{model_name_low}_id
            return item
        except Exception as e:
            logger.error(f"Error retrieving {model_name_low}_id: {{e}}")
    {download_file_decorator if has_file else ""}{download_image_decorator if has_image else ""}
    async def get_by_name(self, name: str = None,
                                namespace_uri : str = None,
                                id_only: bool = False) -> {self.base_app_name}_dm.{model_name} | str:
        \"\"\"Retrieve a {model_name_low} data model by name.\"\"\"
        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:
            res = await self.{model_name_low}_client.RetrieveByNameNamespace(
                {self.app_name}_pb2.{model_name}RetrieveByNameNSRequest(name=name, namespace_uri=namespace_uri)
            )
            item = MessageToDict(res, preserving_proto_field_name=True)
            self.item_id = item["{model_name_low}_id"]
            if id_only:
                return self.item_id
            else:
                return {self.base_app_name}_dm.{model_name}(**item)
        except Exception as e:
            logger.error(f"Error retrieving {model_name_low} name: {{e}}")

    {get_file_method if has_file else ""}
    {get_file_by_name_namespace_method if has_file else ""}
    {get_image_method if has_image else ""}
    """  # noqa: E501
        ###  no namespace
        return f"""
    @export_model_instance_to_file
    async def retrieve(
        self,
        {model_name_low}_id: str | None = None,
        name: str | None = None,
        # name_display: str = None,
        filter_dict: dict | None = None,
        raw : bool = False
        ) -> list[{self.base_app_name}_dm.{model_name}]:
        \"\"\"
        Retrieve a list of {model_name_low} instances by {model_name_low}_id, name or filter_dict.

            Returns a list of {model_name_low} data model objects.
            If raw is True, the raw protobuf objects are returned.
        \"\"\"
        results_list = []
        if  {model_name_low}_id is not None:
            try:
                res =  await self.{model_name_low}_client.Retrieve(
                    {self.app_name}_pb2.{model_name}RetrieveRequest({model_name_low}_id={model_name_low}_id) )
                if raw:
                    results_list.append(res)
                else:
                    results_list.append( {self.base_app_name}_dm.{model_name}(**MessageToDict(res, preserving_proto_field_name=True)) )
            except Exception as e:
                logger.error(f"Error retrieving {model_name_low}_id: {{e}}")

        if filter_dict is None:
            filter_dict = {{}}

        if name is not None:
            filter_dict["name"] = name
        # if name_display is not None:
        #    filter_dict["name_display"] = name_display

        metadata = (("filters", (json.dumps(filter_dict))),)
        try:
            res = await self.{model_name_low}_client.List(
                    {self.app_name}_pb2.{model_name}ListRequest(), 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 += [ {self.base_app_name}_dm.{model_name}(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
        except Exception as e:
            logger.error(f"Error retrieving {model_name_low} name: {{e}}")

        return results_list

    #@id_cache
    async def retrieve_id(self,
                          name: str | None = None,
                          # name_display: str = None,
                          filter_dict : dict | None = None,
                          iri: str | None = None) -> str | None:
        \"\"\"Retrieve the {model_name_low}_id for a given name, (name_display) or iri.\"\"\"
        results_list = await self.retrieve(name=name, filter_dict=filter_dict, raw=True)
        if len(results_list) == 1 :
            return results_list[0].{model_name_low}_id
        else:
            raise ValueError(f"Error retrieving {model_name_low}_id - no or too many results found.")
    {download_file_decorator if has_file else ""}{download_image_decorator if has_image else ""}
    async def get_by_id(self, {model_name_low}_id: str = None) -> {self.base_app_name}_dm.{model_name}:
        \"\"\"Retrieve a {model_name_low} data model by {model_name_low}_id.\"\"\"
        try:
            res = await self.{model_name_low}_client.Retrieve(
                {self.app_name}_pb2.{model_name}RetrieveRequest({model_name_low}_id={model_name_low}_id)
            )
            return {self.base_app_name}_dm.{model_name}(**MessageToDict(res, preserving_proto_field_name=True))
        except Exception as e:
            logger.error(f"Error retrieving {model_name_low}_id: {{e}}")
    {download_file_decorator if has_file else ""}{download_image_decorator if has_image else ""}
    async def get_by_name(self, name: str = None, id_only: bool = False) -> {self.base_app_name}_dm.{model_name} | str:
        \"\"\"Retrieve a {model_name_low} data model by name.\"\"\"
        try:
            res = await self.{model_name_low}_client.RetrieveByNameNamespace(
                {self.app_name}_pb2.{model_name}RetrieveByNameNSRequest(name=name, namespace_uri="")
            )
            if id_only:
                return MessageToDict(res, preserving_proto_field_name=True)["{model_name_low}_id"]
            else:
                return {self.base_app_name}_dm.{model_name}(**MessageToDict(res, preserving_proto_field_name=True))
        except Exception as e:
            logger.error(f"Error retrieving {model_name_low} name: {{e}}")
    {get_file_method if has_file else ""}
    {get_file_by_name_method if has_file else ""}
    {get_image_method if has_image else ""}
    """

    def generate_class_list(self, model, has_namespace=False, has_file=False, has_image=False):
        model_name = model.__name__
        model_name_low = model.__name__.lower()

        return f"""
    async def list_items(self, filter_dict: dict | None = None, ids_only: bool = False) -> list[str]:
        \"\"\"List all {model_name_low}s.\"\"\"
        if filter_dict is None:
            res = await self.{model_name_low}_client.List({self.app_name}_pb2.{model_name}ListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.{model_name_low}_client.List(
                {self.app_name}_pb2.{model_name}ListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.{model_name_low}_id for item in res.results]
        else:
            return res.results

    async def list_full(self, filter_dict: dict | None = None) -> list:
        \"\"\"List all {model_name_low}s.\"\"\"
        if self.{model_name_low}_full_client is None:
            raise ValueError("No full client available.")
        if filter_dict is None:
            return await self.{model_name_low}_full_client.List(
                {self.app_name}_pb2.{model_name}FullListRequest()
            )
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            return await self.{model_name_low}_full_client.List(
                {self.app_name}_pb2.{model_name}FullListRequest(), metadata=metadata
            )
    """

    def generate_class_update(self, model, has_namespace=False, has_file=False, has_image=False):
        model_name = model.__name__
        model_name_low = model.__name__.lower()

        update_file_decorator = "\n    @update_file  # needed for file update"
        update_image_decorator = "\n    @update_image  # needed for image update"

        return f"""
    @import_model_instance_from_file(){update_file_decorator if has_file else ""}{update_image_decorator if has_image else ""}
    async def update(self, {model_name_low} : {self.base_app_name}_dm.{model_name} = None) -> None:
        \"\"\"Update a {model_name_low} model instance.\"\"\"
        try:
            res = await self.{model_name_low}_client.Update(
                {self.app_name}_pb2.{model_name}Request(**{model_name_low}.model_dump()
            ))
        except Exception as e:
            logger.error(f"Error updating {model_name_low}_id: {{e}}")
        else:
            return res
    """

    def generate_class_delete(self, model, has_namespace=False, has_file=False, has_image=False):
        model_name = model.__name__
        model_name_low = model.__name__.lower()

        return f"""
    async def delete(self, id_list: list[str] | None = None, filter_dict: dict | None = None) -> None:
        \"\"\"Delete a {model_name_low} by {model_name_low}_id or filter_dict.\"\"\"
        if filter_dict is not None:
            id_list = await self.list_items(filter_dict=filter_dict, ids_only=True)

        try:
            delete_coroutines = ( self.{model_name_low}_client.Destroy({self.app_name}_pb2.{model_name}DestroyRequest({model_name_low}_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)

        except Exception as e:
            logger.error(f"Error deleting {model_name_low}_id: {{e}}")
\n"""
