import json
from pathlib import Path

import jsonschema
from django import forms
from django.utils.translation import gettext_lazy as _
from django_jsonform.widgets import JSONFormWidget

from servala.core.crd.forms import DEFAULT_FIELD_CONFIGS, MANDATORY_FIELDS
from servala.core.models import ControlPlane, ServiceDefinition

CONTROL_PLANE_USER_INFO_SCHEMA = {
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "title": {
                "type": "string",
                "title": "Title",
            },
            "content": {
                "type": "string",
                "title": "Content",
            },
            "help_text": {
                "type": "string",
                "title": "Help Text (optional)",
            },
        },
        "required": ["title", "content"],
    },
}


class ControlPlaneAdminForm(forms.ModelForm):
    certificate_authority_data = forms.CharField(
        widget=forms.Textarea,
        required=False,
        help_text=_("Certificate authority data for the Kubernetes API"),
    )
    server = forms.URLField(
        required=False,
        help_text=_("Server URL for the Kubernetes API"),
    )
    token = forms.CharField(
        widget=forms.Textarea,
        required=False,
        help_text=_("Authentication token for the Kubernetes API"),
    )

    class Meta:
        model = ControlPlane
        fields = "__all__"
        widgets = {"user_info": JSONFormWidget(schema=CONTROL_PLANE_USER_INFO_SCHEMA)}

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

        # If we have existing api_credentials, populate the individual fields
        if self.instance.pk and self.instance.api_credentials:
            creds = self.instance.api_credentials
            self.fields["certificate_authority_data"].initial = creds.get(
                "certificate-authority-data", ""
            )
            self.fields["server"].initial = creds.get("server", "")
            self.fields["token"].initial = creds.get("token", "")

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

        ca_data = cleaned_data.get("certificate_authority_data")
        server = cleaned_data.get("server")
        token = cleaned_data.get("token")

        if ca_data and server and token:
            cleaned_data["api_credentials"] = {
                "certificate-authority-data": ca_data,
                "server": server,
                "token": token,
            }
        else:
            if not (ca_data or server or token):
                cleaned_data["api_credentials"] = {}
            else:
                # Some fields are filled but not all - validation will fail at model level,
                # as model field validators are also called by the API.
                # We still create the JSON with whatever we have so the model validator can run.
                credentials = {}
                if ca_data:
                    credentials["certificate-authority-data"] = ca_data
                if server:
                    credentials["server"] = server
                if token:
                    credentials["token"] = token
                cleaned_data["api_credentials"] = credentials

        return cleaned_data

    def save(self, *args, **kwargs):
        self.instance.api_credentials = self.cleaned_data["api_credentials"]
        return super().save(*args, **kwargs)


def fields_empty(fields):
    if not fields:
        return True
    return all(not field.get("controlplane_field_mapping") for field in fields)


class ServiceDefinitionAdminForm(forms.ModelForm):
    api_group = forms.CharField(
        required=False,
        help_text=_("API Group"),
    )
    api_version = forms.CharField(
        required=False,
        help_text=_("API Version"),
    )
    api_kind = forms.CharField(
        required=False,
        help_text=_("API Kind"),
    )

    class Meta:
        model = ServiceDefinition
        fields = "__all__"

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

        # If we have existing api_definition, populate the individual fields
        if self.instance.pk and self.instance.api_definition:
            api_def = self.instance.api_definition
            self.fields["api_group"].initial = api_def.get("group", "")
            self.fields["api_version"].initial = api_def.get("version", "")
            self.fields["api_kind"].initial = api_def.get("kind", "")

        schema_path = Path(__file__).parent / "schemas" / "form_config_schema.json"
        with open(schema_path) as f:
            self.form_config_schema = json.load(f)

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

        api_group = cleaned_data.get("api_group")
        api_version = cleaned_data.get("api_version")
        api_kind = cleaned_data.get("api_kind")

        if api_group and api_version and api_kind:
            cleaned_data["api_definition"] = {
                "group": api_group,
                "version": api_version,
                "kind": api_kind,
            }
        else:
            if not (api_group or api_version or api_kind):
                cleaned_data["api_definition"] = {}
            else:
                # Some fields are filled but not all – validation will fail at model level.
                api_def = {}
                if api_group:
                    api_def["group"] = api_group
                if api_version:
                    api_def["version"] = api_version
                if api_kind:
                    api_def["kind"] = api_kind
                cleaned_data["api_definition"] = api_def

        form_config = cleaned_data.get("form_config")

        # Convert empty form_config to None (no custom form)
        if form_config:
            if not form_config.get("fieldsets") or all(
                fields_empty(fieldset.get("fields"))
                for fieldset in form_config.get("fieldsets")
            ):
                form_config = None
                cleaned_data["form_config"] = None

        if form_config:
            form_config = self._normalize_form_config_types(form_config)
            cleaned_data["form_config"] = form_config

            try:
                jsonschema.validate(
                    instance=form_config, schema=self.form_config_schema
                )
            except jsonschema.ValidationError as e:
                raise forms.ValidationError(
                    {
                        "form_config": _("Invalid form configuration: {}").format(
                            e.message
                        )
                    }
                )
            except jsonschema.SchemaError as e:
                raise forms.ValidationError(
                    {"form_config": _("Schema error: {}").format(e.message)}
                )

            self._validate_field_mappings(form_config, cleaned_data)

        return cleaned_data

    def _normalize_form_config_types(self, form_config):
        """
        Normalize form_config by converting string representations of numbers
        to actual integers/floats. The JSON form widget sends all values
        as strings, but the schema expects proper types.
        """
        if not isinstance(form_config, dict):
            return form_config

        integer_fields = ["max_length", "rows", "min_values", "max_values"]
        number_fields = ["min_value", "max_value"]

        for fieldset in form_config.get("fieldsets", []):
            for field in fieldset.get("fields", []):
                for field_name in integer_fields:
                    if field_name in field and field[field_name] is not None:
                        value = field[field_name]
                        if isinstance(value, str):
                            try:
                                field[field_name] = int(value) if value else None
                            except (ValueError, TypeError):
                                pass

                for field_name in number_fields:
                    if field_name in field and field[field_name] is not None:
                        value = field[field_name]
                        if isinstance(value, str):
                            try:
                                field[field_name] = (
                                    int(value) if "." not in value else float(value)
                                )
                            except (ValueError, TypeError):
                                pass

        return form_config

    def _validate_field_mappings(self, form_config, cleaned_data):
        if not self.instance.pk:
            return
        crd = self.instance.offering_control_planes.all().first()
        if not crd:
            return

        schema = None
        try:
            schema = crd.resource_schema
        except Exception:
            pass

        if not schema or not (spec_schema := schema.get("properties", {}).get("spec")):
            return

        valid_paths = self._extract_field_paths(spec_schema, "spec") | {"display_name"}
        included_mappings = set()
        errors = []
        for fieldset in form_config.get("fieldsets", []):
            for field in fieldset.get("fields", []):
                mapping = field.get("controlplane_field_mapping")
                included_mappings.add(mapping)

                # Validate that fields without defaults have required properties
                if mapping not in DEFAULT_FIELD_CONFIGS:
                    if not field.get("label"):
                        errors.append(
                            _(
                                "Field with mapping '{}' must have a 'label' property "
                                "(or use a mapping with default config)"
                            ).format(mapping)
                        )
                    if not field.get("type"):
                        errors.append(
                            _(
                                "Field with mapping '{}' must have a 'type' property "
                                "(or use a mapping with default config)"
                            ).format(mapping)
                        )

                if mapping and mapping not in valid_paths:
                    field_name = field.get("label", field.get("name", mapping))
                    errors.append(
                        _(
                            "Field '{}' has invalid mapping '{}'. Valid paths are: {}"
                        ).format(
                            field_name,
                            mapping,
                            ", ".join(sorted(valid_paths)[:10])
                            + ("..." if len(valid_paths) > 10 else ""),
                        )
                    )

                if field.get("type") == "choice" and field.get("choices"):
                    self._validate_choice_field(
                        field, mapping, spec_schema, "spec", errors
                    )

        for mandatory_field in MANDATORY_FIELDS:
            if mandatory_field not in included_mappings:
                errors.append(
                    _(
                        "Required field '{}' must be included in the form configuration"
                    ).format(mandatory_field)
                )

        if errors:
            raise forms.ValidationError({"form_config": errors})

    def _validate_choice_field(self, field, mapping, spec_schema, prefix, errors):
        if not mapping:
            return

        field_name = field.get("label", mapping)
        custom_choices = field.get("choices", [])

        # Single-element choices [value] are transformed to [value, value]
        for i, choice in enumerate(custom_choices):
            if not isinstance(choice, (list, tuple)):
                errors.append(
                    _(
                        "Field '{}': Choice at index {} must be a list or tuple, "
                        "but got: {}"
                    ).format(field_name, i, repr(choice))
                )
                return

            choice_len = len(choice)
            if choice_len == 1:
                custom_choices[i] = [choice[0], choice[0]]
            elif choice_len == 0 or choice_len > 2:
                errors.append(
                    _(
                        "Field '{}': Choice at index {} must have 1 or 2 elements "
                        "(got {}): {}"
                    ).format(field_name, i, choice_len, repr(choice))
                )
                return

        field_schema = self._get_field_schema(spec_schema, mapping, prefix)
        if not field_schema:
            return

        control_plane_choices = field_schema.get("enum", [])
        if not control_plane_choices:
            return

        custom_choice_values = [choice[0] for choice in custom_choices]

        invalid_choices = [
            value
            for value in custom_choice_values
            if value not in control_plane_choices
        ]

        if invalid_choices:
            errors.append(
                _(
                    "Field '{}' has invalid choice values: {}. "
                    "Valid choices from control plane are: {}"
                ).format(
                    field_name,
                    ", ".join(f"'{c}'" for c in invalid_choices),
                    ", ".join(f"'{c}'" for c in control_plane_choices),
                )
            )

    def _get_field_schema(self, schema, field_path, prefix):
        if not field_path or not schema:
            return None

        if field_path.startswith(prefix + "."):
            field_path = field_path[len(prefix) + 1 :]

        parts = field_path.split(".")
        current_schema = schema

        for part in parts:
            if not isinstance(current_schema, dict):
                return None

            properties = current_schema.get("properties", {})
            if part not in properties:
                return None

            current_schema = properties[part]

        return current_schema

    def _extract_field_paths(self, schema, prefix=""):
        paths = set()

        if not isinstance(schema, dict):
            return paths

        if "type" in schema and schema["type"] != "object":
            if prefix:
                paths.add(prefix)

        if schema.get("properties"):
            for prop_name, prop_schema in schema["properties"].items():
                new_prefix = f"{prefix}.{prop_name}" if prefix else prop_name
                paths.add(new_prefix)
                paths.update(self._extract_field_paths(prop_schema, new_prefix))

        if schema.get("type") == "array" and "items" in schema:
            if prefix:
                paths.add(prefix)

        return paths

    def save(self, *args, **kwargs):
        self.instance.api_definition = self.cleaned_data["api_definition"]
        return super().save(*args, **kwargs)
