import re

from django import forms
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
from django.db import models
from django.forms.models import ModelForm, ModelFormMetaclass
from django.utils.translation import gettext_lazy as _

from servala.core.models import ServiceInstance


class CRDModel(models.Model):
    """Base class for all virtual CRD models"""

    def __init__(self, **kwargs):
        if spec := kwargs.pop("spec", None):
            kwargs.update(unnest_data({"spec": spec}))
        super().__init__(**kwargs)

    class Meta:
        abstract = True


def duplicate_field(field_name, model):
    # Get the field from the model
    field = model._meta.get_field(field_name)

    # Create a new field with the same attributes
    new_field = type(field).__new__(type(field))
    new_field.__dict__.update(field.__dict__)

    # Ensure the field is not linked to the original model
    new_field.model = None
    new_field.auto_created = False

    return new_field


def generate_django_model(schema, group, version, kind):
    """
    Generates a virtual Django model from a Kubernetes CRD's OpenAPI v3 schema.
    """
    # We always need these three fields to know our own name and our full namespace
    model_fields = {"__module__": "crd_models"}
    for field_name in ("name", "organization", "context"):
        model_fields[field_name] = duplicate_field(field_name, ServiceInstance)

    # All other fields are generated from the schema, except for the
    # resourceRef object
    spec = schema["properties"].get("spec") or {}
    spec["properties"].pop("resourceRef", None)
    model_fields.update(build_object_fields(spec, "spec", parent_required=False))

    # Store the original schema on the model class
    model_fields["SCHEMA"] = schema

    meta_class = type("Meta", (), {"app_label": "crd_models"})
    model_fields["Meta"] = meta_class

    # create the model class
    model_name = kind
    model_class = type(model_name, (CRDModel,), model_fields)
    return model_class


def build_object_fields(schema, name, verbose_name_prefix=None, parent_required=False):
    required_fields = schema.get("required") or []
    properties = schema.get("properties") or {}
    fields = {}

    for field_name, field_schema in properties.items():
        is_required = field_name in required_fields or parent_required
        full_name = f"{name}.{field_name}"
        result = get_django_field(
            field_schema,
            is_required,
            field_name,
            full_name,
            verbose_name_prefix=verbose_name_prefix,
        )
        if isinstance(result, dict):
            fields.update(result)
        else:
            fields[full_name] = result
    return fields


def deslugify(title):
    if "_" in title:
        title.replace("_", " ")
        return title.title()
    return re.sub(r"(?<!^)(?=[A-Z])", " ", title).capitalize()


def get_django_field(
    field_schema, is_required, field_name, full_name, verbose_name_prefix=None
):
    field_type = field_schema.get("type") or "string"
    format = field_schema.get("format")
    verbose_name_prefix = verbose_name_prefix or ""
    verbose_name = f"{verbose_name_prefix} {deslugify(field_name)}".strip()

    # Pass down the requirement status from parent to child fields
    kwargs = {
        "blank": not is_required,  # All fields are optional by default
        "null": not is_required,
        "help_text": field_schema.get("description"),
        "validators": [],
        "verbose_name": verbose_name,
        "default": field_schema.get("default"),
    }

    if minimum := field_schema.get("minimum"):
        kwargs["validators"].append(MinValueValidator(minimum))
    if maximum := field_schema.get("maximum"):
        kwargs["validators"].append(MaxValueValidator(maximum))

    if field_type == "string":
        if format == "date-time":
            return models.DateTimeField(**kwargs)
        elif format == "date":
            return models.DateField(**kwargs)
        else:
            max_length = field_schema.get("max_length") or 255
            if pattern := field_schema.get("pattern"):
                kwargs["validators"].append(RegexValidator(regex=pattern))
            if choices := field_schema.get("enum"):
                kwargs["choices"] = ((choice, choice) for choice in choices)
            return models.CharField(max_length=max_length, **kwargs)
    elif field_type == "integer":
        return models.IntegerField(**kwargs)
    elif field_type == "number":
        return models.FloatField(**kwargs)
    elif field_type == "boolean":
        return models.BooleanField(**kwargs)
    elif field_type == "object":
        # Here we pass down the requirement status to nested objects
        return build_object_fields(
            field_schema,
            full_name,
            verbose_name_prefix=f"{verbose_name}:",
            parent_required=is_required,
        )
    elif field_type == "array":
        kwargs["help_text"] = field_schema.get("description") or _("List of values")
        from servala.frontend.forms.widgets import DynamicArrayField

        field = models.JSONField(**kwargs)
        formfield_kwargs = {
            "label": field.verbose_name,
            "required": not field.blank,
        }

        array_validation = {}
        if min_items := field_schema.get("min_items"):
            array_validation["min_items"] = min_items
        if max_items := field_schema.get("max_items"):
            array_validation["max_items"] = max_items
        if unique_items := field_schema.get("unique_items"):
            array_validation["unique_items"] = unique_items
        if items_schema := field_schema.get("items"):
            array_validation["items_schema"] = items_schema
        if array_validation:
            formfield_kwargs["array_validation"] = array_validation

        field.formfield = lambda: DynamicArrayField(**formfield_kwargs)

        return field
    return models.CharField(max_length=255, **kwargs)


def unnest_data(data):
    result = {}

    def _flatten_dict(d, parent_key=""):
        for key, value in d.items():
            new_key = f"{parent_key}.{key}" if parent_key else key
            if isinstance(value, dict):
                _flatten_dict(value, new_key)
            else:
                result[new_key] = value

    _flatten_dict(data)
    return result


class CrdModelFormMixin:
    HIDDEN_FIELDS = [
        "spec.compositeDeletePolicy",
        "spec.compositionRef",
        "spec.compositionRevisionRef",
        "spec.compositionRevisionSelector",
        "spec.compositionSelector",
        "spec.compositionUpdatePolicy",
        "spec.parameters.monitoring.alertmanagerConfigRef",
        "spec.parameters.monitoring.alertmanagerConfigSecretRef",
        "spec.parameters.network.serviceType",
        "spec.parameters.scheduling",
        "spec.parameters.security",
        "spec.parameters.size.cpu",
        "spec.parameters.size.memory",
        "spec.parameters.size.requests.cpu",
        "spec.parameters.size.requests.memory",
        "spec.publishConnectionDetailsTo",
        "spec.resourceRef",
        "spec.writeConnectionSecretToRef",
    ]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.schema = self._meta.model.SCHEMA

        for field in ("organization", "context"):
            self.fields[field].widget = forms.HiddenInput()

        for name, field in self.fields.items():
            if name in self.HIDDEN_FIELDS or any(
                name.startswith(f) for f in self.HIDDEN_FIELDS
            ):
                field.widget = forms.HiddenInput()
                field.required = False

        if self.instance and self.instance.pk:
            self.fields["name"].disabled = True
            self.fields["name"].help_text = _("Name cannot be changed after creation.")
            self.fields["name"].widget = forms.HiddenInput()

    def strip_title(self, field_name, label):
        field = self.fields[field_name]
        if field and field.label and (position := field.label.find(label)) != -1:
            field.label = field.label[position + len(label) :]

    def has_mandatory_fields(self, field_list):
        for field_name in field_list:
            if field_name in self.fields and self.fields[field_name].required:
                return True
        return False

    def get_fieldsets(self):
        fieldsets = []

        # General fieldset for non-spec fields
        general_fields = [
            field_name
            for field_name in self.fields.keys()
            if not field_name.startswith("spec.")
        ]
        if general_fields:
            fieldset = {
                "title": "General",
                "fields": general_fields,
                "fieldsets": [],
                "has_mandatory": self.has_mandatory_fields(general_fields),
            }
            if all(
                [
                    isinstance(self.fields[field].widget, forms.HiddenInput)
                    for field in general_fields
                ]
            ):
                fieldset["hidden"] = True
            fieldsets.append(fieldset)

        # Process spec fields
        others = []
        top_level_fieldsets = {}
        hidden_spec_fields = []

        for field_name in self.fields:
            if field_name.startswith("spec."):
                if isinstance(self.fields[field_name].widget, forms.HiddenInput):
                    hidden_spec_fields.append(field_name)
                    continue

                parts = field_name.split(".")
                if len(parts) == 2:
                    # Top-level spec field
                    others.append(field_name)
                elif len(parts) == 3:
                    # Second-level field - promote to top-level fieldset
                    fieldset_key = f"{parts[1]}.{parts[2]}"
                    if not top_level_fieldsets.get(fieldset_key):
                        top_level_fieldsets[fieldset_key] = {
                            "fields": [],
                            "fieldsets": {},
                            "title": f"{deslugify(parts[2])}",
                        }
                    top_level_fieldsets[fieldset_key]["fields"].append(field_name)
                else:
                    # Third-level and deeper - create nested fieldsets
                    fieldset_key = f"{parts[1]}.{parts[2]}"
                    if not top_level_fieldsets.get(fieldset_key):
                        top_level_fieldsets[fieldset_key] = {
                            "fields": [],
                            "fieldsets": {},
                            "title": f"{deslugify(parts[2])}",
                        }

                    sub_key = parts[3]
                    if not top_level_fieldsets[fieldset_key]["fieldsets"].get(sub_key):
                        top_level_fieldsets[fieldset_key]["fieldsets"][sub_key] = {
                            "title": deslugify(sub_key),
                            "fields": [],
                        }
                    top_level_fieldsets[fieldset_key]["fieldsets"][sub_key][
                        "fields"
                    ].append(field_name)

        for fieldset in top_level_fieldsets.values():
            nested_fieldsets_list = []
            for sub_fieldset in fieldset["fieldsets"].values():
                if len(sub_fieldset["fields"]) == 1:
                    # If nested fieldset has only one field, move it to parent
                    fieldset["fields"].append(sub_fieldset["fields"][0])
                else:
                    # Keep as nested fieldset with proper title stripping
                    title = f"{fieldset['title']}: {sub_fieldset['title']}: "
                    for field in sub_fieldset["fields"]:
                        self.strip_title(field, title)
                    nested_fieldsets_list.append(sub_fieldset)

            fieldset["fieldsets"] = nested_fieldsets_list
            total_fields = len(fieldset["fields"]) + len(nested_fieldsets_list)
            if total_fields == 1 and len(fieldset["fields"]) == 1:
                others.append(fieldset["fields"][0])
            else:
                title = f"{fieldset['title']}: "
                for field in fieldset["fields"]:
                    self.strip_title(field, title)

                all_fields = fieldset["fields"][:]
                for sub_fieldset in nested_fieldsets_list:
                    all_fields.extend(sub_fieldset["fields"])
                fieldset["has_mandatory"] = self.has_mandatory_fields(all_fields)

                fieldsets.append(fieldset)

        # Add 'others' tab if there are any fields
        if others:
            fieldsets.append(
                {
                    "title": "Others",
                    "fields": others,
                    "fieldsets": [],
                    "has_mandatory": self.has_mandatory_fields(others),
                }
            )

        if hidden_spec_fields:
            fieldsets.append(
                {
                    "title": "Advanced",
                    "fields": hidden_spec_fields,
                    "fieldsets": [],
                    "hidden": True,
                    "has_mandatory": self.has_mandatory_fields(hidden_spec_fields),
                }
            )

        fieldsets.sort(key=lambda f: f.get("hidden", False))

        return fieldsets

    def get_nested_data(self):
        """
        Builds the original nested JSON structure from flat form data.
        Form fields are named with dot notation (e.g., 'spec.replicas')
        """
        result = {}

        for field_name, value in self.cleaned_data.items():
            if value is None or value == "":
                continue

            parts = field_name.split(".")
            current = result

            # Navigate through the nested structure
            for i, part in enumerate(parts):
                if i == len(parts) - 1:
                    # Last part, set the value
                    current[part] = value
                else:
                    # Create nested dict if it doesn't exist
                    if part not in current:
                        current[part] = {}
                    current = current[part]
        return result

    def clean(self):
        cleaned_data = super().clean()
        self.validate_nested_data(
            self.get_nested_data().get("spec", {}), self.schema["properties"]["spec"]
        )
        return cleaned_data

    def validate_nested_data(self, data, schema):
        """Validate data against the provided OpenAPI v3 schema"""
        # TODO: actually validate the nested data.
        # TODO: get jsonschema to give us a path to the failing field rather than just an error message,
        # then add the validation error to that field (self.add_error())
        # try:
        #     validate(instance=data, schema=schema)
        # except Exception as e:
        #     raise forms.ValidationError(f"Validation error: {e.message}")
        pass


def generate_model_form_class(model):
    meta_attrs = {
        "model": model,
        "fields": "__all__",
    }
    fields = {
        "Meta": type("Meta", (object,), meta_attrs),
        "__module__": "crd_models",
    }
    class_name = f"{model.__name__}ModelForm"
    return ModelFormMetaclass(class_name, (CrdModelFormMixin, ModelForm), fields)
