from contextlib import suppress

from django import forms
from django.core.validators import MaxValueValidator, MinValueValidator
from django.forms.models import ModelForm, ModelFormMetaclass

from servala.core.crd.utils import deslugify
from servala.core.models import ControlPlaneCRD
from servala.frontend.forms.widgets import DynamicArrayWidget, NumberInputWithAddon

# Fields that must be present in every form
MANDATORY_FIELDS = ["display_name"]

# Default field configurations - fields that can be included with just a mapping
# to avoid administrators having to duplicate common information
DEFAULT_FIELD_CONFIGS = {
    "display_name": {
        "type": "text",
        "label": "Instance Name",
        "help_text": "",
        "required": True,
        "max_length": 100,
    },
    "spec.parameters.service.fqdn": {
        "type": "array",
        "label": "FQDNs",
        "help_text": "Domain names for accessing this service",
        "required": False,
    },
    "spec.parameters.size.disk": {
        "type": "number",
        "label": "Disk size",
        "addon_text": "Gi",
    },
}


class FormGeneratorMixin:
    """Shared base class for ModelForm classes based on our generated CRD models.
    There are two relevant child classes:
        - CrdModelFormMixin: For fully auto-generated forms from the spec
        - CustomFormMixin: For forms built from form_config settings.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        if "context" in self.fields:
            self.fields["context"].widget = forms.HiddenInput()
            if crd := self.initial.get("context"):
                crd = getattr(crd, "pk", crd)  # can be int or object
                self.fields["context"].queryset = ControlPlaneCRD.objects.filter(pk=crd)

    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


class CrdModelFormMixin(FormGeneratorMixin):
    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.publishConnectionDetailsTo",
        "spec.resourceRef",
        "spec.writeConnectionSecretToRef",
    ]

    # Fields populated from compute plan
    READONLY_FIELDS = [
        "spec.parameters.size.cpu",
        "spec.parameters.size.memory",
        "spec.parameters.size.requests.cpu",
        "spec.parameters.size.requests.memory",
    ]

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

        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
            elif name in self.READONLY_FIELDS or any(
                name.startswith(f) for f in self.READONLY_FIELDS
            ):
                field.disabled = True
                field.required = False
                field.widget.attrs["readonly"] = "readonly"
                field.widget.attrs["class"] = (
                    field.widget.attrs.get("class", "") + " form-control-plaintext"
                )

    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 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)


class CustomFormMixin(FormGeneratorMixin):
    """
    Base for custom (user-friendly) forms generated from ServiceDefinition.form_config.
    """

    IS_CUSTOM_FORM = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._apply_field_config()

    def _apply_field_config(self):
        for fieldset in self.form_config.get("fieldsets", []):
            for fc in fieldset.get("fields", []):
                field_name = fc.get("controlplane_field_mapping")

                if field_name not in self.fields:
                    continue

                field_config = fc.copy()
                # Merge with defaults if field has default config
                if field_name in DEFAULT_FIELD_CONFIGS:
                    field_config = DEFAULT_FIELD_CONFIGS[field_name].copy()
                    for key, value in fc.items():
                        if value or (value is False):
                            field_config[key] = value

                field = self.fields[field_name]
                field_type = field_config.get("type")

                field.label = field_config.get("label", field_name)
                field.help_text = field_config.get("help_text", "")
                field.required = field_config.get("required", False)

                if field_type == "textarea":
                    field.widget = forms.Textarea(
                        attrs={"rows": field_config.get("rows", 4)}
                    )
                elif field_type == "array":
                    field.widget = DynamicArrayWidget()
                elif field_type == "choice":
                    if hasattr(field, "choices") and field.choices:
                        field._controlplane_choices = list(field.choices)
                    if custom_choices := field_config.get("choices"):
                        field.choices = [tuple(choice) for choice in custom_choices]

                if field_type == "number":
                    min_val = field_config.get("min_value")
                    max_val = field_config.get("max_value")
                    unit = field_config.get("addon_text")

                    if not isinstance(field, forms.IntegerField):
                        widget = field.widget
                        self.fields[field_name] = forms.IntegerField(
                            label=field.label,
                            help_text=field.help_text,
                            required=field.required,
                            widget=widget,
                        )
                        field = self.fields[field_name]

                    if unit:
                        field.widget = NumberInputWithAddon(addon_text=unit)
                        field.addon_text = unit
                        value = self.initial.get(field_name)
                        if value and isinstance(value, str) and value.endswith(unit):
                            numeric_value = value[: -len(unit)]
                            with suppress(ValueError):
                                if "." in numeric_value:
                                    self.initial[field_name] = float(numeric_value)
                                else:
                                    self.initial[field_name] = int(numeric_value)

                    validators = []
                    if min_val is not None:
                        with suppress(ValueError):
                            validators.append(MinValueValidator(int(min_val)))
                            field.widget.attrs["min"] = min_val
                    if max_val is not None:
                        with suppress(ValueError):
                            validators.append(MaxValueValidator(int(max_val)))
                            field.widget.attrs["max"] = max_val

                    if validators:
                        field.validators.extend(validators)

                if "default_value" in field_config and field.initial is None:
                    field.initial = field_config["default_value"]

                if field_type in ("text", "textarea") and field_config.get(
                    "max_length"
                ):
                    field.max_length = field_config.get("max_length")
                    if hasattr(field.widget, "attrs"):
                        field.widget.attrs["maxlength"] = field_config.get("max_length")

                field.controlplane_field_mapping = field_name

    def get_fieldsets(self):
        fieldsets = []
        for fieldset_config in self.form_config.get("fieldsets", []):
            field_names = [
                f["controlplane_field_mapping"]
                for f in fieldset_config.get("fields", [])
            ]
            fieldset = {
                "title": fieldset_config.get("title", "General"),
                "fields": field_names,
                "fieldsets": [],
                "has_mandatory": self.has_mandatory_fields(field_names),
            }
            fieldsets.append(fieldset)

        return fieldsets

    def clean(self):
        cleaned_data = super().clean()

        for field_name, field in self.fields.items():
            if hasattr(field, "_controlplane_choices"):
                value = cleaned_data.get(field_name)
                if value:
                    valid_values = [choice[0] for choice in field._controlplane_choices]
                    if value not in valid_values:
                        self.add_error(
                            field_name,
                            forms.ValidationError(
                                f"'{value}' is not a valid choice. "
                                f"Must be one of: {valid_values.join(', ')}"
                            ),
                        )

        return cleaned_data

    def get_nested_data(self):
        nested = {}
        for field_name in self.fields.keys():
            if field_name == "context":
                value = self.cleaned_data.get(field_name)
                if value is not None:
                    nested[field_name] = value
                continue

            mapping = field_name
            value = self.cleaned_data.get(field_name)
            field = self.fields[field_name]

            if addon_text := getattr(field, "addon_text", None):
                value = f"{value}{addon_text}"

            parts = mapping.split(".")
            current = nested
            for part in parts[:-1]:
                if part not in current:
                    current[part] = {}
                current = current[part]

            current[parts[-1]] = value

        return nested


def generate_custom_form_class(form_config, model):
    """
    Generate a custom (user-friendly) form class from form_config JSON.
    """
    field_list = ["context", "display_name"]

    for fieldset in form_config.get("fieldsets", []):
        for field_config in fieldset.get("fields", []):
            field_name = field_config.get("controlplane_field_mapping")
            if field_name:
                field_list.append(field_name)

    fields = {
        "context": forms.ModelChoiceField(
            queryset=ControlPlaneCRD.objects.none(),
            required=True,
            widget=forms.HiddenInput(),
        ),
    }

    meta_attrs = {
        "model": model,
        "fields": field_list,
    }

    form_fields = {
        "Meta": type("Meta", (object,), meta_attrs),
        "__module__": "crd_models",
        "form_config": form_config,
        **fields,
    }

    class_name = f"{model.__name__}CustomForm"
    return ModelFormMetaclass(class_name, (CustomFormMixin, ModelForm), form_fields)
