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

:PROJECT: LARAsuite

*generate YAML Schema *

:details: generate_yaml_template - a YAML Schema generator for LARA-django.
          This is a LARA-django-base management command to generate a YAML import template for a
          given app. The generated schema can be used to import data into LARA via the gRPC interface.

          usage:
            lara-django-dev generate_yaml_template <lara_app>

          once the template is generated, it can be used to import data into LARA via the gRPC interface:

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

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

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)

SCHEMA_FILE_HEADER = """#_____________________________________________________________________
#  :PROJECT: LARAsuite

#  {lara_app} YAML data import template

# :details: {lara_app} YAML template for import of model instances into LARA via the gRPC interface.
#            generated with 
#            lara-django-dev generate_yaml_template {lara_app}
# :authors: {lara_author} <{lara_author_email}>
#________________________________________________________________________

"""


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


# safe_string = lambda s: s.replace('"', '\\"').replace("\n", "\\n")
# safe_string = lambda s: f'"{s}"' if s else ""


class Command(LabelCommand):
    help = """Generate a YAML template 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 YAML template 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 = []

        YAMLApp(app, model_res, options)


class YAMLApp:
    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.force_overwrite = options["force"]
        self.author_name = options["author_name"]
        self.author_email = options["author_email"]

        self.schema_str = ""
        self.model_dict = {}
        self.internal_model_name = set()
        self.external_model_name = set()

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

        self.generate_schema()

    def generate_schema(self):
        self.generate_schema_header()
        self.generate_classes()

        # saving schema to file
        schema_filename = f"{self.app_name}_data_import_template.yaml"

        print(f"writing {schema_filename} to {os.getcwd()}")
        # print(self.schema_str)

        if os.path.isfile(schema_filename):
            if self.force_overwrite:
                with open(schema_filename, "w") as f:
                    f.write(self.schema_str)
            else:
                append = input(
                    f"{schema_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(schema_filename, "a") as f:
                        f.write(self.schema_str)
                elif append.lower() == "o":
                    with open(schema_filename, "w") as f:
                        f.write(self.schema_str)
        else:
            with open(schema_filename, "w") as f:
                f.write(self.schema_str)

    def generate_schema_header(self):
        self.schema_str += SCHEMA_FILE_HEADER.format(
            lara_app=self.app_name,
            lara_author=self.author_name,
            lara_author_email=self.author_email,
        )
        # adding LARA prefixes
        # self.schema_str += "".join([indent(1, f"{prefix}: {uri}") for prefix, uri in settings.LARA_PREFIXES.items()])

        # adding imports
        self.schema_str += """\n"""

    def generate_classes(self):
        class_str = ""
        for model in self.app_config.get_models():
            self.internal_model_name.add(model.__name__)

            class_str += (
                indent(0, "#________  " + model.__name__ + "  ________")
                # + indent(1, "# description:")
                # + indent(1, "#  " +  model.__doc__.replace("\n"," ").strip())
                # + indent(1, "# parameters:")
                + self.parse_fields(model)
                + "\n"
            )

        # first adding external classes
        self.external_model_name = self.external_model_name - self.internal_model_name
        # print(f"external classes: {self.external_classes}"
        # self.schema_str += "".join([indent(1, f"{external_class}:\n") for external_class in self.external_model_name])

        for external_class in self.external_model_name:
            self.schema_str += (
                indent(1, external_class + ":") + indent(2, f"description: {self.model_dict[external_class]}") + "\n"
            )

        # then adding internal classes
        self.schema_str += class_str
        # ext_name = self.external_model_name.pop()
        # print(f"ext classes: {ext_name} -> {self.ext_model_dict[ext_name]}")

    def parse_fields(self, model):
        fields_str = "- "

        default_prefix = "lara"
        indent_level = 0
        cs = 8 # comment_spacing
        for field in model._meta.get_fields():
            #print(f"field: {field} --> {indent_level} \n")
            # print(f"field: {field} --> {type(field)} \n")

            if (
                isinstance(field, models.fields.related.ManyToManyField)
                or isinstance(field, models.fields.reverse_related.ManyToOneRel)
                or isinstance(field, models.fields.reverse_related.ManyToManyRel)
                or isinstance(field, models.fields.reverse_related.OneToOneRel)
                or isinstance(field, models.fields.reverse_related.ManyToManyRel)
            ):
                required = False
            else:
                required = field.blank == False

            match field:
                case models.fields.related.ManyToOneRel():
                    indent_level = 0
                case models.fields.UUIDField():
                    if field.primary_key:
                        indent_level = 0
                        fields_str += indent(indent_level, field.name + f":{' '*cs}#  UUID string (primary key, required)")
                    else:
                        fields_str += indent(indent_level, field.name + f":{' '*cs}#  UUID string ({'required' if required else 'optional'})")
                    # self.external_model_name.add("UUID")

                case models.fields.BooleanField():
                    fields_str += indent(indent_level, field.name + f": null{' '*cs}#  boolean ({'required' if required else 'optional'}) ")
                case models.fields.CharField():
                    fields_str += indent(indent_level, field.name + f": null{' '*cs}#  string ({'required' if required else 'optional'})")

                case models.fields.TextField():
                    fields_str += indent(indent_level, field.name + f": null{' '*cs}#  string ({'required' if required else 'optional'})")
                case models.fields.IntegerField():
                    fields_str += indent(indent_level, field.name + f": null{' '*cs}#  integer ({'required' if required else 'optional'})")

                case models.fields.FloatField():
                    fields_str += indent(indent_level, field.name + f": null{' '*cs}#  float ({'required' if required else 'optional'})")

                case models.fields.DecimalField():
                    fields_str += indent(indent_level, field.name + f": null{' '*cs}#  float ({'required' if required else 'optional'})")

                case models.fields.DateTimeField():
                    fields_str += indent(indent_level, field.name + f": null{' '*cs}#  datetime, e.g. 2024-03-31T00:00:00Z ({'required' if required else 'optional'})")

                case models.fields.DateField():
                    fields_str += indent(indent_level, field.name + f": null{' '*cs}#  date, e.g. 2024-03-31 ({'required' if required else 'optional'})")
 
                case models.fields.json.JSONField():
                    fields_str += indent(indent_level, field.name + f": null{' '*cs}#  json ({'required' if required else 'optional'})")

                case models.fields.files.FileField():
                    fields_str += indent(indent_level, field.name + f": null{' '*cs}#  file ({'required' if required else 'optional'})")

                case models.fields.related.ForeignKey():
                    # print(f"{field.name} --- attributes: {field.__dict__}")
                    fields_str += indent(indent_level, field.name + f": null{' '*cs}#  foreign key ({'required' if required else 'optional'})")
                    self.external_model_name.add(field.related_model.__name__)

                    self.model_dict[field.related_model.__name__] = field.related_model

                case models.OneToOneField():
                    fields_str += indent(indent_level, field.name + f": null{' '*cs}#  one2one key ({'required' if required else 'optional'})")
                    self.external_model_name.add(field.related_model.__name__)

                    self.model_dict[field.related_model.__name__] = field.related_model

                case models.fields.related.ManyToManyField():
                    # print(f"field attributes: {field.__dict__}")
                    fields_str += indent(indent_level, field.name + f": []{' '*cs}#  many2many key ({'required' if required else 'optional'})")
                    self.external_model_name.add(field.related_model.__name__)

                    self.model_dict[field.related_model.__name__] = field.related_model

            indent_level = 1

        return fields_str

    def generate_container(self):
        self.schema_str += indent(1, "Container:") + indent(2, "tree_root: true") + indent(2, "attributes:")

        for model in self.app_config.get_models():
            self.schema_str += (
                indent(3, model.__name__.lower() + "s:")
                + indent(4, "range: " + model.__name__)
                + indent(4, "multivalued: true")
                + indent(4, "inlined_as_list: true")
                + "\n"
            )
