import inspect
import typing as t
import warnings
from collections.abc import Callable

from wtforms import Form
from wtforms.fields import HiddenField
from wtforms.fields.core import UnboundField
from wtforms.validators import InputRequired

from flask_admin._compat import iteritems
from flask_admin.form import BaseForm
from flask_admin.form import rules

from .._types import T_MODEL_VIEW
from .._types import T_ORM_MODEL
from .._types import T_SQLALCHEMY_COLUMN
from .widgets import XEditableWidget


def converts(*args: t.Any) -> Callable[[t.Any], t.Any]:
    def _inner(func: t.Callable[[t.Any], t.Any]) -> t.Callable[[t.Any], t.Any]:
        func._converter_for = frozenset(args)  # type: ignore[attr-defined]
        return func

    return _inner


class BaseListForm(BaseForm):
    list_form_pk = HiddenField(validators=[InputRequired()])


def create_editable_list_form(
    form_base_class: type[Form],
    form_class: type[Form],
    widget: t.Callable[..., t.Any] | None = None,
) -> type[BaseListForm]:
    """
    Create a form class with all the fields wrapped in a FieldList.

    Wrapping each field in FieldList allows submitting POST requests
    in this format: ('<field_name>-<primary_key>', '<value>')

    Used in the editable list view.

    :param form_base_class:
        WTForms form class, by default `form_base_class` from base.
    :param form_class:
        WTForms form class generated by `form.get_form`.
    :param widget:
        WTForms widget class. Defaults to `XEditableWidget`.
    """
    if widget is None:
        widget = XEditableWidget()

    class ListForm(BaseListForm, form_base_class):  # type: ignore[misc, valid-type]
        pass

    # iterate FormMeta to get unbound fields, replace widget, copy to ListForm
    for name, obj in form_class.__dict__.items():
        if isinstance(obj, UnboundField):
            obj.kwargs["widget"] = widget
            setattr(ListForm, name, obj)

            if name == "list_form_pk":
                raise Exception("Form already has a list_form_pk column.")

    return ListForm


class InlineBaseFormAdmin:
    """
    Settings for inline form administration.

    You can use this class to customize displayed form.
    For example::

        class MyUserInfoForm(InlineBaseFormAdmin):
            form_columns = ('name', 'email')
    """

    _defaults = [
        "form_base_class",
        "form_columns",
        "form_excluded_columns",
        "form_args",
        "form_extra_fields",
    ]

    def __init__(self, **kwargs: t.Any) -> None:
        """
        Constructor

        :param kwargs:
            Additional options
        """
        for k in self._defaults:
            if not hasattr(self, k):
                setattr(self, k, None)

        for k, v in iteritems(kwargs):
            setattr(self, k, v)

        # Convert form rules
        form_rules = getattr(self, "form_rules", None)
        self._form_rules: rules.RuleSet | None
        if form_rules:
            self._form_rules = rules.RuleSet(self, form_rules)
        else:
            self._form_rules = None

    def get_form(self) -> t.Union[T_MODEL_VIEW, None]:
        """
        If you want to use completely custom form for inline field, you can override
        Flask-Admin form generation logic by overriding this method and returning your
        form.
        """
        return None

    def postprocess_form(self, form_class: BaseForm) -> BaseForm:
        """
        Post process form. Use this to contribute fields.

        For example::

            class MyInlineForm(InlineFormAdmin):
                def postprocess_form(self, form):
                    form.value = StringField('value')
                    return form

            class MyAdmin(ModelView):
                inline_models = (MyInlineForm(ValueModel),)
        """
        return form_class

    def on_model_change(
        self, form: Form, model: T_MODEL_VIEW, is_created: bool
    ) -> None:
        """
        Called when inline model is about to be saved.

        :param form:
            Inline form
        :param model:
            Model
        :param is_created:
            Will be set to True if the model is being created, False if edited
        """
        pass

    def _on_model_change(
        self, form: Form, model: T_MODEL_VIEW, is_created: bool
    ) -> None:
        """
        Compatibility helper.
        """
        try:
            self.on_model_change(form, model, is_created)
        except TypeError:
            msg = (
                "%s.on_model_change() now accepts third "
                + "parameter is_created. Please update your code"
            ) % self.model  # type: ignore[attr-defined]
            warnings.warn(msg, stacklevel=1)

            self.on_model_change(form, model)  # type: ignore[call-arg]


class InlineFormAdmin(InlineBaseFormAdmin):
    """
    Settings for inline form administration. Used by relational backends (SQLAlchemy,
    Peewee), where model
    class can not be inherited from the parent model definition.
    """

    def __init__(
        self, model: t.Union[T_ORM_MODEL, "InlineBaseFormAdmin"], **kwargs: t.Any
    ) -> None:
        """
        Constructor

        :param model:
            Model class
        """
        self.model = model

        super().__init__(**kwargs)


class ModelConverterBase:
    def __init__(
        self, converters: dict[t.Any, t.Any] | None = None, use_mro: bool = True
    ) -> None:
        self.use_mro = use_mro

        if not converters:
            converters = {}

        for name in dir(self):
            obj = getattr(self, name)
            if hasattr(obj, "_converter_for"):
                for classname in obj._converter_for:
                    converters[classname] = obj

        self.converters = converters

    def get_converter(
        self, column: T_SQLALCHEMY_COLUMN
    ) -> t.Callable[..., t.Any] | None:
        types: list[type] | tuple[type, ...]
        if self.use_mro:
            types = inspect.getmro(type(column.type))
        else:
            types = [type(column.type)]

        # Search by module + name
        for col_type in types:
            type_string = f"{col_type.__module__}.{col_type.__name__}"

            if type_string in self.converters:
                return self.converters[type_string]

        # Search by name
        for col_type in types:
            if col_type.__name__ in self.converters:
                return self.converters[col_type.__name__]

        return None

    def get_form(
        self,
        model: T_MODEL_VIEW,
        base_class: type[BaseForm] = BaseForm,
        only: t.Any = None,
        exclude: t.Any = None,
        field_args: t.Any = None,
    ) -> t.Any:
        raise NotImplementedError()


class InlineModelConverterBase:
    form_admin_class = InlineFormAdmin

    def __init__(self, view: T_MODEL_VIEW) -> None:
        """
        Base constructor

        :param view:
            View class
        """
        self.view = view

    def get_label(self, info: T_MODEL_VIEW, name: str) -> str | None:
        """
        Get inline model field label

        :param info:
            Inline model info
        :param name:
            Field name
        """
        form_name = getattr(info, "form_label", None)
        if form_name:
            return form_name

        column_labels = getattr(self.view, "column_labels", None)

        if column_labels and name in column_labels:
            return column_labels[name]

        return None

    def get_info(
        self,
        p: tuple[T_ORM_MODEL, dict[str, t.Any]] | InlineFormAdmin | type[T_ORM_MODEL],
    ) -> InlineFormAdmin | None:
        """
        Figure out InlineFormAdmin information.

        :param p:
            Inline model. Can be one of:

             - ``tuple``, first value is related model instance,
             second is dictionary with options
             - ``InlineFormAdmin`` instance
             - Model class
        """
        if isinstance(p, tuple):
            return self.form_admin_class(p[0], **p[1])
        elif isinstance(p, self.form_admin_class):
            return p

        return None


class FieldPlaceholder:
    """
    Field placeholder for model convertors.
    """

    def __init__(self, field: "UnboundField[t.Any]") -> None:
        self.field = field
