"""
mizanFormMixin - Turn Django Forms into server functions.

This mixin transforms any Django Form into mizan server functions,
preserving full Django Form functionality (validation, widgets, ModelChoiceField, etc.)
while exposing them through the unified server function API.

Usage:
    from django import forms
    from mizan.forms import mizanFormMixin, mizanFormMeta

    class ContactForm(mizanFormMixin, forms.Form):
        mizan = mizanFormMeta(
            name="contact",
            title="Contact Us",
            submit_label="Send",
        )

        name = forms.CharField()
        email = forms.EmailField()
        message = forms.CharField(widget=forms.Textarea)

        def on_submit_success(self, request):
            send_email(self.cleaned_data)
            return {"sent": True}

Auto-registers server functions:
    - contact.schema
    - contact.validate
    - contact.submit
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, ClassVar

from django import forms
from django.http import HttpRequest
from pydantic import BaseModel, create_model

if TYPE_CHECKING:
    from .schemas import FormValidation


def _django_field_to_python_type(field: forms.Field) -> type:
    """
    Map a Django form field to a Python type for Pydantic schema generation.

    This provides TypeScript with proper field types instead of generic `any`.
    """
    # Handle common Django field types
    if isinstance(field, forms.BooleanField):
        return bool
    elif isinstance(field, forms.IntegerField):
        return int
    elif isinstance(field, forms.FloatField):
        return float
    elif isinstance(field, forms.DecimalField):
        return str  # Decimals serialize as strings for precision
    elif isinstance(field, forms.DateTimeField):
        return str  # ISO format string
    elif isinstance(field, forms.DateField):
        return str  # ISO format string
    elif isinstance(field, forms.TimeField):
        return str  # ISO format string
    elif isinstance(field, forms.JSONField):
        return dict | list | str | int | float | bool | None
    elif isinstance(field, forms.MultipleChoiceField):
        return list[str]
    elif isinstance(field, forms.FileField):
        return str  # File path/name as string
    elif isinstance(field, forms.ImageField):
        return str  # File path/name as string
    else:
        # Default to string (covers CharField, EmailField, URLField, etc.)
        return str


def _create_form_input_schema(
    form_class: type[forms.BaseForm],
    schema_name: str,
) -> type[BaseModel]:
    """
    Create a Pydantic model from Django Form fields.

    This generates a typed schema for the form's input data, giving TypeScript
    full LSP support (autocomplete, type checking) for form fields.

    Args:
        form_class: Django Form class to introspect
        schema_name: Name for the generated Pydantic model (e.g., "ContactFormData")

    Returns:
        A Pydantic BaseModel subclass with fields matching the form
    """
    # Instantiate form without data to get field definitions
    try:
        form = form_class()
    except TypeError:
        # Form requires extra args (like request) - use form_class.base_fields instead
        fields_dict = getattr(form_class, "base_fields", {})
    else:
        fields_dict = form.fields

    # Build Pydantic field definitions
    pydantic_fields: dict[str, Any] = {}

    for field_name, field in fields_dict.items():
        python_type = _django_field_to_python_type(field)

        # Optional fields (not required or has initial value)
        if not field.required:
            python_type = python_type | None
            default = None
        elif field.initial is not None:
            default = field.initial
        else:
            default = ...  # Required field

        pydantic_fields[field_name] = (python_type, default)

    # Create the model with a unique name
    model = create_model(schema_name, **pydantic_fields)

    return model


class mizanFormMeta(BaseModel):
    """
    Configuration for a mizan form.

    This Pydantic model provides type-safe configuration with full LSP support,
    and serializes to JSON for the frontend schema.

    Required:
        name: API identifier (e.g., "contact" → contact.schema, contact.validate, contact.submit)

    Display options:
        title: Display title (default: derived from class name)
        subtitle: Display subtitle
        submit_label: Submit button text (default: "Submit")

    Frontend behavior:
        live_validation: Enable live validation as user types (default: True)
        live_form_errors: Show form-level errors during live validation (default: False)
        refetch_schema_on_validate: Refetch schema on each validation - useful for
            dynamic choice fields (default: False)

    Features:
        enable_formset: Generate formset endpoints (default: False)
    """

    # Required
    name: str

    # Display
    title: str | None = None
    subtitle: str | None = None
    submit_label: str = "Submit"

    # Frontend behavior
    live_validation: bool = True
    live_form_errors: bool = False
    refetch_schema_on_validate: bool = False

    # Features
    enable_formset: bool = False


class mizanFormMixin:
    """
    Mixin that exposes a Django Form as mizan server functions.

    Add this mixin to any Django Form class along with a `mizan` configuration:

        class ContactForm(mizanFormMixin, forms.Form):
            mizan = mizanFormMeta(
                name="contact",
                title="Contact Us",
            )

            name = forms.CharField()
            email = forms.EmailField()

            def on_submit_success(self, request):
                return {"sent": True}

    This auto-registers:
        - contact.schema - Get form field definitions
        - contact.validate - Validate form data
        - contact.submit - Submit form

    Overridable methods:
        get_init_kwargs(cls, request) -> dict: Extra kwargs for form instantiation
        on_submit_success(self, request) -> dict | None: Handle successful submission
        on_submit_failure(self, request, errors) -> None: Handle failed submission
    """

    # Configuration - subclasses must define this
    mizan: ClassVar[mizanFormMeta]

    # Track registered forms to avoid duplicate registration
    _mizan_registered: ClassVar[bool] = False

    @classmethod
    def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
        """
        Override to provide extra kwargs for form instantiation.

        Common use: pass request or user to forms that need them.

        Example:
            @classmethod
            def get_init_kwargs(cls, request):
                return {"request": request, "user": request.user}
        """
        return {}

    def on_submit_success(self, request: HttpRequest) -> dict | None:
        """
        Called after successful form validation and submission.

        Override to handle the form submission logic.
        Return a dict to include data in the response.

        Example:
            def on_submit_success(self, request):
                self.save()
                return {"id": self.instance.pk}
        """
        # Default: call save() if available
        if hasattr(self, "save"):
            result = self.save()
            # If save returns something serializable, include it
            if isinstance(result, dict):
                return result
        return None

    def on_submit_failure(self, request: HttpRequest, errors: "FormValidation") -> None:
        """
        Called after form validation fails.

        Override to add custom error handling, logging, etc.
        """
        pass

    def __init_subclass__(cls, **kwargs):
        """Auto-register when a concrete form class is defined."""
        super().__init_subclass__(**kwargs)

        # Only register concrete forms with mizan config defined
        if _is_concrete_mizan_form(cls):
            _register_form_as_server_functions(cls)


def _is_concrete_mizan_form(cls: type) -> bool:
    """
    Check if a class is a concrete mizan form ready for registration.

    A form is concrete if:
    1. It has a `mizan` attribute that is a mizanFormMeta instance
    2. It inherits from Django's BaseForm
    3. It hasn't been registered yet (for this class definition)
    """
    # Must have mizan config (check cls.__dict__ to avoid inheriting)
    mizan_config = cls.__dict__.get("mizan")
    if not isinstance(mizan_config, mizanFormMeta):
        return False

    # Must be a Django form
    if not issubclass(cls, forms.BaseForm):
        return False

    # Check if already registered (handle re-imports gracefully)
    if cls.__dict__.get("_mizan_registered", False):
        return False

    return True


def _register_form_as_server_functions(form_class: type) -> None:
    """
    Register a Django Form class as mizan server functions.

    Creates and registers:
    - {name}.schema - Returns form field definitions
    - {name}.validate - Validates form data
    - {name}.submit - Validates and submits form

    Each function gets a unique typed schema for better TypeScript LSP support.
    """
    from .schemas import FormSchema, FormSubmitFail, FormSubmitPass, FormValidation
    from .schema_utils import build_form_schema
    from .validation_utils import validate_form_instance
    from mizan_core.registry import register
    from mizan_core.client.function import ServerFunction

    config: mizanFormMeta = form_class.mizan
    form_name = config.name

    # Mark as registered
    form_class._mizan_registered = True

    # Generate PascalCase name for schemas (e.g., "contact" -> "Contact")
    pascal_name = "".join(
        word.capitalize()
        for word in form_name.replace(".", "_").replace("-", "_").split("_")
    )

    # NOTE: We cannot create FormDataSchema here because form fields aren't
    # populated yet during __init_subclass__. We use lazy creation instead.
    _form_data_schema_cache: dict[str, type[BaseModel]] = {}

    def get_form_data_schema() -> type[BaseModel]:
        """Lazily create the form data schema (form fields aren't available at registration time)."""
        if "schema" not in _form_data_schema_cache:
            _form_data_schema_cache["schema"] = _create_form_input_schema(
                form_class, f"{pascal_name}FormData"
            )
        return _form_data_schema_cache["schema"]

    # -------------------------------------------------------------------------
    # Schema Function
    # -------------------------------------------------------------------------

    # Schema input wraps the form data for pre-populating dynamic fields
    FormSchemaInput = create_model(
        f"{pascal_name}SchemaInput",
        data=(dict[str, Any], {}),
    )

    class SchemaFunction(ServerFunction):
        Input = FormSchemaInput
        Output = FormSchema

        _meta: ClassVar[dict] = {
            "form": True,
            "form_name": form_name,
            "form_role": "schema",
            "form_class": form_class,  # Store reference for schema generation
        }

        def call(self, input) -> FormSchema:
            init_kwargs = form_class.get_init_kwargs(self.request)
            schema = build_form_schema(
                form_class,
                data=input.data if input else {},
                **init_kwargs,
            )
            # Override with mizanFormMeta values
            if config.title is not None:
                schema.title = config.title
            if config.subtitle is not None:
                schema.subtitle = config.subtitle
            schema.submit_label = config.submit_label
            # Behavior settings are nested in schema.meta
            schema.meta.live_validation = config.live_validation
            schema.meta.live_form_errors = config.live_form_errors
            schema.meta.refetch_schema_on_validate = config.refetch_schema_on_validate
            return schema

    SchemaFunction.__name__ = f"{form_name}_schema"
    SchemaFunction.__qualname__ = f"{form_name}_schema"
    register(SchemaFunction, f"{form_name}.schema")

    # -------------------------------------------------------------------------
    # Validate Function
    # -------------------------------------------------------------------------

    # Use generic dict input - form fields aren't available during __init_subclass__
    FormValidateInput = create_model(
        f"{pascal_name}ValidateInput",
        data=(dict[str, Any], ...),
    )

    class ValidateFunction(ServerFunction):
        Input = FormValidateInput
        Output = FormValidation

        _meta: ClassVar[dict] = {
            "form": True,
            "form_name": form_name,
            "form_role": "validate",
        }

        def call(self, input) -> FormValidation:
            init_kwargs = form_class.get_init_kwargs(self.request)
            # Input data is already a dict
            data = input.data
            _, validation = validate_form_instance(
                form_class,
                data=data,
                files=None,
                **init_kwargs,
            )
            return validation

    ValidateFunction.__name__ = f"{form_name}_validate"
    ValidateFunction.__qualname__ = f"{form_name}_validate"
    register(ValidateFunction, f"{form_name}.validate")

    # -------------------------------------------------------------------------
    # Submit Function
    # -------------------------------------------------------------------------

    class SubmitFunction(ServerFunction):
        """
        Submit function handles both JSON and multipart/form-data.

        The executor detects form functions and parses the request appropriately.
        """

        # Use dict for input - form fields unknown at registration time
        Input = None  # Signals executor to pass raw dict

        _meta: ClassVar[dict] = {
            "form": True,
            "form_name": form_name,
            "form_role": "submit",
            "multipart": True,  # Signal that this function accepts multipart
        }

        def call(self, input) -> FormSubmitPass | FormSubmitFail:
            """Execute form submission."""
            request = self.request

            # Check if we have multipart data from executor
            if hasattr(request, "_mizan_form_data"):
                data = request._mizan_form_data
                files = request._mizan_form_files
            elif input is not None:
                # JSON input - already a dict
                data = input if isinstance(input, dict) else input.model_dump()
                files = None
            else:
                data = {}
                files = None

            init_kwargs = form_class.get_init_kwargs(request)

            # Create and validate form
            form, validation = validate_form_instance(
                form_class,
                data=data,
                files=files,
                **init_kwargs,
            )

            if form.is_valid():
                # Call the form's on_submit_success
                result_data = form.on_submit_success(request)
                return FormSubmitPass(success=True, data=result_data)

            # Call the form's on_submit_failure
            form.on_submit_failure(request, validation)
            return FormSubmitFail(success=False, errors=validation)

    SubmitFunction.__name__ = f"{form_name}_submit"
    SubmitFunction.__qualname__ = f"{form_name}_submit"
    SubmitFunction.Output = FormSubmitPass  # For schema generation
    register(SubmitFunction, f"{form_name}.submit")

    # -------------------------------------------------------------------------
    # Formset Functions (if enabled)
    # -------------------------------------------------------------------------

    if config.enable_formset:
        _register_formset_functions(form_class, form_name)


def _register_formset_functions(
    form_class: type,
    form_name: str,
) -> None:
    """Register formset server functions for a form."""
    from django.forms import formset_factory

    from .schemas import (
        FormsetSchema,
        FormsetSubmitFail,
        FormsetSubmitPass,
        FormsetValidation,
    )
    from .schema_utils import build_form_schema
    from .validation_utils import build_formset_validation
    from .formset_utils import forms_to_formset_post_data
    from mizan_core.registry import register
    from mizan_core.client.function import ServerFunction

    formset_class = formset_factory(form_class)

    # Generate PascalCase name for schemas
    pascal_name = "".join(
        word.capitalize()
        for word in form_name.replace(".", "_").replace("-", "_").split("_")
    )

    # NOTE: We cannot create typed schemas here because form fields aren't
    # populated yet during __init_subclass__. We use generic dict inputs.

    # -------------------------------------------------------------------------
    # Formset Schema Function
    # -------------------------------------------------------------------------

    FormsetSchemaInput = create_model(
        f"{pascal_name}FormsetSchemaInput",
        forms=(list[dict[str, Any]], []),
    )

    class FormsetSchemaFunction(ServerFunction):
        Input = FormsetSchemaInput
        Output = FormsetSchema

        _meta: ClassVar[dict] = {
            "form": True,
            "form_name": form_name,
            "form_role": "formset_schema",
        }

        def call(self, input) -> FormsetSchema:
            init_kwargs = form_class.get_init_kwargs(self.request)
            forms_data = input.forms if input else []

            formset_data = forms_to_formset_post_data(forms_data)
            formset = formset_class(formset_data)

            return FormsetSchema(
                forms=[
                    build_form_schema(form_class, data=fd, **init_kwargs)
                    for fd in forms_data
                ],
                min_num=formset.min_num,
                max_num=formset.max_num,
                can_delete=formset.can_delete,
                can_order=formset.can_order,
            )

    FormsetSchemaFunction.__name__ = f"{form_name}_formset_schema"
    register(FormsetSchemaFunction, f"{form_name}.formset.schema")

    # -------------------------------------------------------------------------
    # Formset Validate Function
    # -------------------------------------------------------------------------

    # Generic dict input - form fields aren't available during __init_subclass__
    FormsetValidateInput = create_model(
        f"{pascal_name}FormsetValidateInput",
        forms=(list[dict[str, Any]], ...),
    )

    class FormsetValidateFunction(ServerFunction):
        Input = FormsetValidateInput
        Output = FormsetValidation

        _meta: ClassVar[dict] = {
            "form": True,
            "form_name": form_name,
            "form_role": "formset_validate",
        }

        def call(self, input) -> FormsetValidation:
            init_kwargs = form_class.get_init_kwargs(self.request)
            # Input.forms is already a list of dicts
            forms_data = input.forms

            formset_data = forms_to_formset_post_data(forms_data)
            formset = formset_class(formset_data, form_kwargs=init_kwargs)

            for form in formset:
                form.empty_permitted = False

            return build_formset_validation(formset)

    FormsetValidateFunction.__name__ = f"{form_name}_formset_validate"
    register(FormsetValidateFunction, f"{form_name}.formset.validate")

    # -------------------------------------------------------------------------
    # Formset Submit Function
    # -------------------------------------------------------------------------

    # Generic dict input - form fields aren't available during __init_subclass__
    FormsetSubmitInput = create_model(
        f"{pascal_name}FormsetSubmitInput",
        forms=(list[dict[str, Any]], ...),
    )

    class FormsetSubmitFunction(ServerFunction):
        Input = FormsetSubmitInput

        _meta: ClassVar[dict] = {
            "form": True,
            "form_name": form_name,
            "form_role": "formset_submit",
            "multipart": True,
        }

        def call(self, input) -> FormsetSubmitPass | FormsetSubmitFail:
            request = self.request
            init_kwargs = form_class.get_init_kwargs(request)

            # Handle multipart vs JSON
            if hasattr(request, "_mizan_form_data"):
                post_data = request._mizan_form_data
                files = request._mizan_form_files
            elif input and hasattr(input, "forms"):
                # Input.forms is already a list of dicts
                forms_data = input.forms
                post_data = forms_to_formset_post_data(forms_data)
                files = None
            else:
                post_data = {}
                files = None

            formset = formset_class(post_data, files=files, form_kwargs=init_kwargs)

            if formset.is_valid():
                for form in formset.forms:
                    if form.cleaned_data:
                        form.on_submit_success(request)
                return FormsetSubmitPass(success=True)

            validation = build_formset_validation(formset)
            # Call failure handler on each form
            for form in formset.forms:
                if hasattr(form, "on_submit_failure"):
                    form.on_submit_failure(request, validation)

            return FormsetSubmitFail(success=False, errors=validation)

    FormsetSubmitFunction.__name__ = f"{form_name}_formset_submit"
    FormsetSubmitFunction.Output = FormsetSubmitPass
    register(FormsetSubmitFunction, f"{form_name}.formset.submit")


# ─── Public helpers ─────────────────────────────────────────────────────────


def register_form(
    form_class: type,
    name: str,
    submit_handler: Any = None,
) -> None:
    """
    Register a Django Form class as Mizan server functions.

    Creates and registers `{name}.schema`, `{name}.validate`, and
    `{name}.submit` (if a submit_handler is provided).
    """
    from mizan_core.client.function import create_form_functions
    from mizan_core.registry import register

    schema_fn, validate_fn, submit_fn = create_form_functions(
        form_class, name, submit_handler
    )
    register(schema_fn, f"{name}.schema")
    register(validate_fn, f"{name}.validate")
    if submit_fn:
        register(submit_fn, f"{name}.submit")


def get_forms() -> dict[str, list]:
    """
    Group registered form-related functions by their form name.

    Returns a mapping like:
        {"contact": [ContactSchema, ContactValidate, ContactSubmit], ...}
    """
    from mizan_core.registry import get_all_functions

    forms: dict[str, list] = {}
    for name, cls in get_all_functions().items():
        meta = getattr(cls, "_meta", {})
        if not meta.get("form"):
            continue
        form_name = meta.get("form_name")
        forms.setdefault(form_name, []).append(cls)
    return forms
