# -*- coding: utf-8 -*-
"""_____________________________________________________________________

: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 re
import os
import logging

from django.apps import apps
from django.conf import settings
from django.core.management.base import LabelCommand, CommandError
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: "
                )
                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 typing import Union

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
            model_field_names = [field.name for field in model._meta.get_fields()]
            
            if "namespace" in model_field_names:
                has_namespace = True

            self.interface_str += f"""#_________________  {model.__name__}  ______________________\n\n
class {model.__name__}Interface:
    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.{model.__name__.lower()}_class_client = (
        #     {self.app_name}_pb2_grpc.{model.__name__}classByIRIControllerStub(channel)
        # )
        
        self.{model.__name__.lower()}_client = {self.app_name}_pb2_grpc.{model.__name__}ControllerStub(channel)

        # this is required by the upload_file decorator, please activate if needed for file upload
        # self.stub = self.{model.__name__.lower()}_client
        # self.file_chunk = {self.app_name}_pb2.FileChunk
        # self.update_item_id = "{model.__name__.lower()}_id"

        # self.{model.__name__.lower()}_full_client = {self.app_name}_pb2_grpc.{model.__name__}FullControllerStub(
        #     channel
        # )

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

    @default_namespace_uri.setter
    def default_namespace_uri(self, namespace_uri: str = None):
        if namespace_uri is not None:
            self._default_namespace_uri = namespace_uri

    @import_model_instance_from_file(relation_resolver=lambda x: x)
    #@upload_file  # activate if needed for file upload
    #@upload_image  # activate if needed for image upload
    async def create(self,  {lower_strip( model._meta.verbose_name_plural)}:  list[{self.base_app_name}_dm.{model.__name__}] = None,  ids_only: bool = False, namespace_uri: str = None) -> Union[list[str], list[{self.base_app_name}_dm.{model.__name__}]]:
        # TODO: only retrieve default_namespace_id if not already retrieved and namespace_uri is None
        self.namespace_id = None
        # {model.__name__.lower()}class_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:
            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__.lower()}_class_iri is None:
        #     raise ValueError("No sample class IRI is given.")
        # try:
        #     res = await self.{model.__name__.lower()}_class_client.Retrieve(
        #         {self.app_name}_pb2.{model.__name__}classRetrieveRequest(iri={model.__name__.lower()}_class_iri)
        #     )
        #     {model.__name__.lower()}class_id = res.{model.__name__.lower()}class_id 
        # except Exception as e:
        #     logger.error(f"Error retrieving {model.__name__.lower()}class_id: {{e}}")
        # for {model.__name__.lower()} in {model.__name__.lower()}s:
        #     {model.__name__.lower()}.namespace = self.namespace_id
        #     {model.__name__.lower()}.{model.__name__.lower()}_class = {model.__name__.lower()}class_id

        try:
            create_coroutines = ( self.{model.__name__.lower()}_client.Create({self.app_name}_pb2.{model.__name__}Request(**{model.__name__.lower()}.model_dump())) for {model.__name__.lower()} in {lower_strip( model._meta.verbose_name_plural)} )
            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [item.{model.__name__.lower()}_id for item in res]
            else:
                return res

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

    @export_model_instance_to_file
    async def retrieve(
        self, 
        {model.__name__.lower()}_id: str = None,
        {model.__name__.lower()}_name: str = None,
        #{model.__name__.lower()}_name_full: str = None,
        filter_dict: dict = None,
        raw : bool = False
        ) -> list[{self.base_app_name}_dm.{model.__name__}]:
        \"\"\" retrieve a list of {model.__name__.lower()} instances by {model.__name__.lower()}_id, {model.__name__.lower()}_name or filter_dict, 
               returns a list of {model.__name__.lower()} data model objects,
               if raw is True, the raw protobuf objects are returned 
        \"\"\"  
        filter_list = []
        results_list = []
        if  {model.__name__.lower()}_id is not None:
            try:
                res =  await self.{model.__name__.lower()}_client.Retrieve(
                    {self.app_name}_pb2.{model.__name__}RetrieveRequest({model.__name__.lower()}_id={model.__name__.lower()}_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__.lower()}_id: {{e}}")

        if {model.__name__.lower()}_name is not None:
            filter_list.append({{"name": {model.__name__.lower()}_name}})
        # if {model.__name__.lower()}_name_full is not None:
        #     filter_list.append({{"name_full": {model.__name__.lower()}_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.{model.__name__.lower()}_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__.lower()}_name: {{e}}")

        return results_list

    #@id_cache
    async def retrieve_id(self, 
                          {model.__name__.lower()}_name: str = None,
                          {model.__name__.lower()}_name_full: str = None,
                          iri: str = None,) -> str:
        \"\"\" retrieve the {model.__name__.lower()}_id for a given {model.__name__.lower()}_name, 
               {model.__name__.lower()}_name_full or iri 
        \"\"\"
        if {model.__name__.lower()}_name is not None:
            filter_dict = {{"name": {model.__name__.lower()}_name}}
        elif {model.__name__.lower()}_name_full is not None:
            filter_dict = {{"name_full": {model.__name__.lower()}_name_full}}
        elif iri is not None:
            filter_dict = {{"iri": iri}}
        metadata = (("filters", (json.dumps(filter_dict))),)
        try:
            res = await self.{model.__name__.lower()}_client.List(
                {self.app_name}_pb2.{model.__name__}ListRequest(), metadata=metadata
            )
            results = res.results
        except Exception as e:
            logger.error(f"Error retrieving {model.__name__.lower()}_name: {{e}}")
        if len(results) == 1 :
            return results[0].{model.__name__.lower()}_id
        else:
            raise ValueError(f"Error retrieving {model.__name__.lower()}_id - no or too many results found.")

    #@download_file
    #@download_image
    async def get_by_id(self, {model.__name__.lower()}_id: str = None) -> {self.base_app_name}_dm.{model.__name__}:
        \"\"\" retrieve a {model.__name__.lower()} data model by {model.__name__.lower()}_id \"\"\"
        try:
            res = await self.{model.__name__.lower()}_client.Retrieve(
                {self.app_name}_pb2.{model.__name__}RetrieveRequest({model.__name__.lower()}_id={model.__name__.lower()}_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__.lower()}_id: {{e}}")

    #@download_file
    #@download_image
    async def get_by_name(self, {model.__name__.lower()}_name: str = None,
                                namespace_uri : str = None) -> {self.base_app_name}_dm.{model.__name__}:
        \"\"\" retrieve a {model.__name__.lower()} data model by {model.__name__.lower()}_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:
            namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
            res = await self.{model.__name__.lower()}_client.Retrieve(
                {self.app_name}_pb2.{model.__name__}RetrieveRequest(name={model.__name__.lower()}_name, namespace=namespace_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__.lower()}_name: {{e}}")
            
        
    async def list_items(self, filter_dict: dict = None, ids_only: bool = False) -> list[str]:
        \"\"\" list all {model.__name__.lower()}s \"\"\"
        if filter_dict is None:
            res = await self.{model.__name__.lower()}_client.List({self.app_name}_pb2.{model.__name__}ListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.{model.__name__.lower()}_client.List(
                {self.app_name}_pb2.{model.__name__}ListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.{model.__name__.lower()}_id for item in res.results]
        else:
            return res.results
            

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

    @import_model_instance_from_file(relation_resolver=lambda x: x)
    # @update_file
    # @update_image
    async def update(self, {model.__name__.lower()} : {self.base_app_name}_dm.{model.__name__} = None) -> None:
        \"\"\" update a {model.__name__.lower()} model instance \"\"\"
        try:
            await self.{model.__name__.lower()}_client.Update(
                {self.app_name}_pb2.{model.__name__}Request(**{model.__name__.lower()}.model_dump()
            ))
        except Exception as e:
            logger.error(f"Error updating {model.__name__.lower()}_id: {{e}}")

    async def delete(self, id_list: list[str] = None, filter_dict: dict = None) -> None:
        \"\"\" delete a {model.__name__.lower()} by {model.__name__.lower()}_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__.lower()}_client.Destroy({self.app_name}_pb2.{model.__name__}DestroyRequest({model.__name__.lower()}_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)
            
        except Exception as e:
            logger.error(f"Error deleting {model.__name__.lower()}_id: {{e}}")
\n"""
  


            