import re
from functools import partial
from uuid import uuid4

from nicegui import ElementFilter, app, events, run, ui
from nicegui.binding import BindableProperty
from pydantic import AnyUrl, ValidationError
from sqlalchemy import select
from xplan_tools.model import model_factory
from xplan_tools.model.base import BaseFeature
from xplan_tools.model.orm import Feature, Refs
from xplan_tools.util.style import add_style_properties_to_feature

from xmas_app.settings import settings
from xmas_app.util.codelist import get_codelist_options


class ModelForm:
    """A dynamic form builder for XPlan features using NiceGUI.

    This class provides methods to render and manage forms for editing and previewing
    XPlan/XTrasse features, including nested data types and associations.
    """

    editable = BindableProperty()

    def __init__(
        self,
        appschema: str,
        version: str,
        parent: ui.element | None = None,
        header: ui.header | None = None,
    ):
        """Initialize the ModelForm.

        Args:
            appschema (str): The application schema name.
            version (str): The schema version.
            parent (ui.element | None): The parent UI element for rendering.
            header (ui.header | None): Optional header UI element.
        """
        self.appschema = appschema
        self.version = version
        self.featuretype = None
        self.sub_models = {}
        self.editable = True
        self.parent = parent
        self.header = header
        self.rendered = False

    async def _filter_properties(self):
        """Filter property rows in the UI based on the selected filter option."""
        self.row_filter.set_value("")
        match self.radio_filter.value:
            case "all":
                ElementFilter(marker="property_row").classes(remove="hidden")
            case "mandatory":
                ElementFilter(marker="property_row").classes(remove="hidden")
                ElementFilter(marker="nullable").classes(add="hidden")
            case "set":
                for property_row in ElementFilter(marker="property_row"):
                    property_row.classes(remove="hidden")
                for elem in (
                    ElementFilter(kind=ui.expansion)
                    .within(marker="property_row")
                    .not_within(marker="sub_form")
                ):
                    if not getattr(self.feature, elem.text, None):
                        ElementFilter(marker=elem.text).not_within(
                            marker="sub_form"
                        ).classes(add="hidden")

    @staticmethod
    def validate_uri(value: str | None):
        if value is None:
            return True
        try:
            _ = AnyUrl(value)
        except ValidationError:
            return False
        else:
            return True

    async def _preview_association(self, assoc_id: str):
        """Show a dialog previewing the associated object.

        Args:
            assoc_id (str): The ID of the associated object to preview.
        """
        with self.parent:
            with (
                ui.dialog() as dialog,
                ui.card().style("width: auto; max-width: none") as card,
            ):
                await self._render_object(assoc_id, card, preview=True)
        await dialog
        dialog.delete()

    def _propagate_data(self):
        if not self.editable:
            return
        if model_instance := self.model_instance:
            feature_data = {
                "properties": model_instance.model_dump(
                    mode="json",
                    exclude_none=True,
                    exclude={"id", self.model.get_geom_field()},
                ),
                "featuretype": self.model.get_name(),
                "id": model_instance.id,
            }
        else:
            feature_data = {
                "properties": None,
                "featuretype": None,
                "id": None,
            }
            # self.radio_filter.set_value("mandatory")
        ui.run_javascript(f"""new QWebChannel(qt.webChannelTransport, function (channel) {{
                                    channel.objects.handler.receive_feature({feature_data})
                            }});""")

    def _get_bindable_object(self, model: type[BaseFeature]) -> type:
        """Construct a class with bindable properties from a BaseFeature.

        Takes a BaseFeature class as input and returns a new class with the BaseFeature's attributes as BindableProperties.
        The new bindable class can be instantiated with keyword arguments corresponding to the attributes,
        e.g. with an unpacked dict generated by the model_dump() method.

        Nested models will be assigned to corresponding bindable classes recursively.

        Args:
            model (type[BaseFeature]): The Pydantic BaseFeature to build the bindable class from.

        Returns:
            type: The bindable class with properties corresponding to the model fields.
        """

        def constructor(bindable_object, **kwargs) -> None:
            """The init method of the bindable class.

            Args:
                bindable_object (object): The class instance, i.e. self.
                kwargs: The feature's attributes as keyword arguments.
            """
            for key in model.model_fields.keys():
                value = kwargs.get(key, None)
                prop_info = model.get_property_info(key)
                if prop_info["stereotype"] == "DataType" and value:
                    if isinstance(prop_info["typename"], str):
                        sub_obj = self._get_bindable_object(
                            model_factory(
                                prop_info["typename"], self.version, self.appschema
                            )
                        )
                        value = (
                            [sub_obj(**item) for item in value]
                            if prop_info["list"]
                            else sub_obj(**value)
                        )
                    else:
                        value = (
                            [
                                self._get_bindable_object(
                                    model_factory(
                                        item["datatype"], self.version, self.appschema
                                    )
                                )(**item)
                                for item in value
                            ]
                            if prop_info["list"]
                            else self._get_bindable_object(
                                model_factory(
                                    value["datatype"], self.version, self.appschema
                                )
                            )(**value)
                        )
                setattr(bindable_object, key, value)

        attributes = {"__init__": constructor}
        for field in model.model_fields.keys():
            attributes[field] = BindableProperty(
                on_change=lambda *_: self._propagate_data()
            )
        return type(model.get_name(), (), attributes)

    @property
    def model_instance(self) -> BaseFeature | None:
        """Get the validated model instance from the current feature.

        Returns:
            BaseFeature | None: The validated model instance, or None if validation fails.
        """
        feature = self.feature
        vars(feature).pop("sub_models", None)
        if (geom := vars(self.feature).pop("geometry", None)) and (
            geom_field := self.model.get_geom_field()
        ):
            setattr(feature, geom_field, geom)
        try:
            return self.model.model_validate(feature)
        except ValidationError as e:
            message = f"Fehlende Attribute: {', '.join(set([str(error['loc'][0]) for error in e.errors()]))}"
            if getattr(self, "current_validation_error", None) != message:
                self.current_validation_error = message
                ui.notify(
                    message,
                    type="negative",
                    position="bottom",
                )

    async def _set_stylesheetId_and_schriftinhalt(self) -> None:
        """Set the stylesheetId and schriftinhalt attributes based on the referenced feature."""
        ref_id = getattr(self.feature, "dientZurDarstellungVon", None)
        if not ref_id or not getattr(self.feature, "art", None):
            return
        obj = self.model_instance
        if not obj:
            return
        new_obj = add_style_properties_to_feature(obj, settings.repo.get(ref_id[0]))
        stylesheet_id = getattr(new_obj, "stylesheetId", None)
        setattr(
            self.feature, "stylesheetId", str(stylesheet_id) if stylesheet_id else None
        )
        if schriftinhalt := getattr(new_obj, "schriftinhalt", None):
            setattr(self.feature, "schriftinhalt", schriftinhalt)

    def _get_association_targets(
        self,
        property: str | list,
        typenames: str | list,
        excluded_ids: list[str] | str = [],
        plan_id: str | None = None,
    ):
        """Get possible association targets for a property.

        Args:
            property (str | list): The property name or list of names.
            typenames (str | list): The type name(s) to filter by.
            excluded_ids (list[str] | str, optional): IDs to exclude. Defaults to [].
            plan_id (str | None, optional): The ID of the associated plan. Defaults to None.

        Returns:
            dict: Mapping of feature IDs to feature type names.
        """
        if plan_id and isinstance(property, str) and property.startswith("gehoertZu"):
            plan = settings.repo.get(plan_id)
            if property.endswith("Bereich"):
                bereiche = [settings.repo.get(bereich) for bereich in plan.bereich]
                return {
                    bereich.id: f"{bereich.get_name()} {bereich.nummer}"
                    for bereich in bereiche
                }
            else:
                return {plan_id: f"{plan.get_name()} {plan.name}"}
        with settings.repo.Session() as session:
            stmt = (
                select(Feature)
                .where(
                    Feature.featuretype.in_(
                        [typenames] if isinstance(typenames, str) else typenames
                    )
                )
                .where(
                    Feature.id.not_in(
                        [excluded_ids]
                        if isinstance(excluded_ids, str)
                        else excluded_ids
                    )
                )
                .where(Feature.id != self.feature.id)
            )
            if plan_id:
                # limit features to the ones belonging to the plan
                stmt = stmt.where(
                    Feature.refs_inv.any(
                        Refs.feature.has(Feature.refs_inv.any(Refs.base_id == plan_id)),
                    )
                )
            result = session.execute(stmt)

            return {
                str(feature.id): f"{feature.featuretype} ({str(feature.id)[:8]})"
                for feature in result.unique().scalars().all()
            }

    def _get_association_info(
        self,
        ids: list[str] | str,
    ) -> dict:
        """Get information about associated features.

        Args:
            ids (list[str] | str): List of feature IDs or a single ID.

        Returns:
            dict: Mapping of feature IDs to feature type names.
        """
        with settings.repo.Session() as session:
            result = session.execute(
                select(Feature).where(
                    Feature.id.in_(ids if isinstance(ids, list) else [ids])
                )
            )
            return {
                str(feature.id): feature.featuretype
                for feature in result.unique().scalars().all()
            } | {
                sub_model_id: sub_model.get_name()
                for sub_model_id, sub_model in self.sub_models.items()
            }

    def _is_geometry_required(self, typename: str) -> bool:
        """Check if a geometry is required for the given type.

        Args:
            typename (str): The type name.

        Returns:
            bool: True if geometry is required, False otherwise.
        """
        model = model_factory(typename, self.version, self.appschema)
        if geom := model.get_geom_field():
            return not model.get_property_info(geom)["nullable"]
        return False

    def _get_art_options(self) -> None:
        """Populate the 'art' select input with options derived from referenced feature attributes."""

        def derive_art_from_attributes():
            """Derive art options from the referenced feature's attributes.

            Returns:
                dict: Mapping of attribute paths to display values.
            """

            def set_option_value(path, value):
                art[path] = f"{path} ({str(value)})"

            art = {}
            for name, value in ref_feature.model_dump(
                mode="json",
                exclude_none=True,
                exclude={"id", ref_feature.get_geom_field()},
            ).items():
                # TODO refactor recursive parsing
                prop_info = ref_feature.get_property_info(name)
                if prop_info["stereotype"] == "Association":
                    continue
                elif prop_info["list"]:
                    for index, item in enumerate(value, 0):
                        path = f"{name}[{index + 1}]"
                        if prop_info["stereotype"] == "BasicType":
                            set_option_value(path, item)
                        elif prop_info["stereotype"] == "Enumeration":
                            set_option_value(path, prop_info["enum_info"][item]["name"])
                        elif prop_info["stereotype"] == "DataType":
                            sub_model = getattr(ref_feature, name)[index]
                            for sub_name, sub_value in item.items():
                                sub_prop_info = sub_model.get_property_info(sub_name)
                                sub_path = "/".join(
                                    [path, sub_model.get_name(), sub_name]
                                )
                                set_option_value(
                                    sub_path,
                                    sub_prop_info["enum_info"][sub_value]["name"]
                                    if sub_prop_info["stereotype"] == "Enumeration"
                                    else sub_value,
                                )
                elif prop_info["stereotype"] == "DataType":
                    sub_model = model_factory(
                        prop_info["typename"],
                        self.version,
                        self.appschema,
                    )
                    for sub_name, sub_value in value.items():
                        sub_prop_info = sub_model.get_property_info(sub_name)
                        path = "/".join([name, prop_info["typename"], sub_name])
                        set_option_value(
                            path,
                            sub_prop_info["enum_info"][sub_value]["name"]
                            if sub_prop_info["stereotype"] == "Enumeration"
                            else sub_value,
                        )
                else:
                    set_option_value(
                        name,
                        prop_info["enum_info"][value]["name"]
                        if prop_info["stereotype"] == "Enumeration"
                        else value,
                    )
            return art

        if ref := getattr(self.feature, "dientZurDarstellungVon", None):
            ref_feature = settings.repo.get(ref[0])
            self.feature.gehoertZuBereich = str(ref_feature.gehoertZuBereich)
            art_options = derive_art_from_attributes()
            self.art_input.set_options(self.art_input.options | art_options)

    # @ui.refreshable_method
    async def _build_sub_form(
        self, key: str, bind_obj: type, prop_info: dict, preview: bool = False
    ) -> None:
        """Construct a nested form for data types.

        Args:
            key (str): The name of the attribute where the data type is the value.
            bind_obj (type): The bindable parent class the sub-form is bound to.
            prop_info (dict): The property info object for the given attribute.
            preview (bool, optional): Whether the form should be rendered in preview mode. Defaults to False.
        """

        async def add_form(type_name: str | None = None):
            """Add a new sub-form instance for the data type.

            Args:
                type_name (str | None): The type name to instantiate, if it's a union type, otherwise the typename is taken from the property info.
            """
            sub_obj = self._get_bindable_object(
                model_factory(
                    type_name or prop_info["typename"], self.version, self.appschema
                )
            )()
            if not getattr(bind_obj, key, None):
                setattr(bind_obj, key, [sub_obj] if prop_info["list"] else sub_obj)
            elif prop_info["list"]:
                getattr(bind_obj, key).append(sub_obj)
            else:
                setattr(bind_obj, key, sub_obj)

            item = await build_item(
                None,
                sub_obj,
                getattr(bind_obj, key).index(sub_obj) if prop_info["list"] else None,
            )
            item.move(item_list)
            # self._build_sub_form.refresh()

        def delete_form(element: ui.element, item: type):
            """Delete a sub-form instance.

            Args:
                index (int | None): The index to delete, if the property is a list.
            """
            if isinstance(getattr(bind_obj, key), list):
                getattr(bind_obj, key).remove(item)
                if not len(getattr(bind_obj, key)):
                    setattr(bind_obj, key, None)
            else:
                setattr(bind_obj, key, None)
            element.delete()
            # self._build_sub_form.refresh()

        async def build_item(
            sub_model: BaseFeature | None,
            item: type,
            index: int | None = None,
        ) -> ui.item:
            """Build a UI item for a sub-form instance.

            This corresponds to an instance of the data type.

            Args:
                sub_model (BaseFeature | None): The sub-model class.
                item (type): The bindable object instance.
                index (int | None): The index in the list, if applicable.
            """
            if not sub_model:
                sub_model = model_factory(
                    item.__class__.__name__,
                    self.version,
                    self.appschema,
                )

            with ui.item().props("dense") as list_item:
                with ui.item_section():
                    if isinstance(prop_info["typename"], list):
                        with ui.item_label():
                            with ui.expansion(sub_model.get_name()).props(
                                "expand-icon=o_info dense dense-toggle"
                            ):
                                ui.label(sub_model.__doc__ or "").style(
                                    "max-width: 70ch;"
                                )
                    await self._build_model_form(
                        sub_model,
                        getattr(bind_obj, key)[index]
                        if prop_info["list"]
                        else getattr(bind_obj, key),
                        preview=preview,
                    )
                if not preview:
                    with ui.item_section().props("side"):
                        delete_button = (
                            ui.button(
                                icon="delete",
                                on_click=partial(delete_form, list_item, item),
                            )
                            .props("flat round")
                            .bind_enabled_from(
                                bind_obj,
                                key,
                                backward=lambda x: len(x) > 1
                                if not prop_info["nullable"] and isinstance(x, list)
                                else x is not None
                                if prop_info["nullable"]
                                else x,
                            )
                        )
                        if not self.editable:
                            delete_button.disable()
            return list_item

        if not prop_info["nullable"] and not getattr(bind_obj, key, None):
            if isinstance(prop_info["typename"], str):
                sub_obj = self._get_bindable_object(
                    model_factory(prop_info["typename"], self.version, self.appschema)
                )()
                setattr(bind_obj, key, [sub_obj] if prop_info["list"] else sub_obj)

        value = getattr(bind_obj, key, None)
        with (
            ui.list()
            .classes("w-full")
            .props("bordered separator dense")
            .mark("sub_form")
        ) as item_list:
            sub_model = None
            if isinstance(prop_info["typename"], str):
                sub_model = model_factory(
                    prop_info["typename"],
                    self.version,
                    self.appschema,
                )
                with ui.item().props("dense"):
                    with ui.expansion(sub_model.get_name()).props(
                        "expand-icon=o_info dense dense-toggle"
                    ):
                        ui.label(sub_model.__doc__ or "").style("max-width: 70ch;")
                ui.separator()
            if value is not None:
                if isinstance(value, list):
                    for index, item in enumerate(value):
                        await build_item(sub_model, item, index)
                else:
                    await build_item(sub_model, value)
            if not preview:
                if prop_info["list"] or (prop_info["nullable"] and not value):
                    with ui.item().classes("justify-center"):
                        if isinstance(prop_info["typename"], str):
                            ui.button(
                                icon="add",
                                on_click=add_form,
                            ).bind_enabled_from(self, "editable").props("flat round")
                        else:
                            with (
                                ui.dropdown_button(
                                    text="Datentyp auswählen",
                                    icon="add",
                                    auto_close=True,
                                )
                                .bind_enabled_from(self, "editable")
                                .props("flat rounded no-caps")
                            ):
                                for type_name in prop_info["typename"]:
                                    ui.item(
                                        type_name,
                                        on_click=lambda x: add_form(
                                            next(x.sender.descendants()).text
                                        ),
                                    )

    async def _build_model_form(
        self,
        model: BaseFeature,
        bind_obj: type,
        main_form: bool = False,
        preview: bool = False,
    ) -> None:
        """Construct a form for a given feature.

        This method is also called recursively for nested data types.

        Args:
            model (BaseFeature): The BaseFeature class.
            bind_obj (type): The bindable class to bind input to.
            main_form (bool, optional): Whether it's the initial/parent form or a sub-form. Defaults to False.
            preview (bool, optional): Whether the form should be rendered in preview mode. Defaults to False.
        """
        if main_form:
            with ui.row(align_items="center").classes("justify-between") as class_info:
                with ui.expansion(model.get_name()).props(
                    f"expand-icon=o_info dense dense-toggle header-class=text-h6{' expand-icon-class=text-white' if not preview else ''}"
                ):
                    ui.label(model.__doc__ or "")
            if not preview:
                with class_info:
                    with ui.row(align_items="center"):
                        row_filter = (
                            ui.input(
                                "Attributnamen und -definition filtern",
                                on_change=lambda x: radio_filter.set_value("all")
                                if x.value
                                else radio_filter.set_value(radio_filter.value),
                            )
                            .classes("on-right")
                            .style("width: 380px;")
                            .props("clearable square filled dense bg-color=white")
                        )
                        self.row_filter = row_filter
                        with row_filter.add_slot("prepend"):
                            ui.icon("o_filter_alt")
                        radio_filter = (
                            ui.radio(
                                {
                                    "all": "Alle Attribute",
                                    "mandatory": "Pflichattribute",
                                    "set": "Belegte Attribute",
                                },
                                value="all",
                                on_change=self._filter_properties,
                            )
                            .props("dense inline dark color=white")
                            .classes("on-right")
                        )
                        self.radio_filter = radio_filter
            class_info.move(self.header if not preview else self.parent, 0)
        with ui.grid(columns="310px auto"):
            for key, field_info in model.model_fields.items():
                if key == model.get_geom_field():
                    continue
                prop_info = model.get_property_info(key)
                if preview and (
                    prop_info["stereotype"] == "Association"
                    or getattr(bind_obj, key, None) is None
                ):
                    continue
                default = None
                if isinstance(
                    field_default := getattr(field_info, "default", None),
                    (float, int, str),
                ):
                    default = field_default
                with ui.row() as property_row:
                    if main_form and not preview:
                        property_row.bind_visibility_from(
                            row_filter,
                            "value",
                            backward=lambda filter_value,
                            key=key,
                            docs=field_info.description: re.search(
                                filter_value, key + str(docs), re.I
                            )
                            if filter_value
                            else True,
                        )
                        marker = ["property_row", key]
                        if prop_info["nullable"]:
                            marker.append("nullable")
                        property_row.mark(" ".join(marker))
                    with ui.expansion(key).props(
                        "expand-icon=o_info dense dense-toggle header-class=text-bold"
                    ):
                        ui.label(
                            "Identifikator des Features."
                            if key == "id"
                            else field_info.description or ""
                        )
                with ui.row() as property_row:
                    if main_form and not preview:
                        property_row.bind_visibility_from(
                            row_filter,
                            "value",
                            backward=lambda filter_value,
                            key=key,
                            docs=field_info.description: re.search(
                                filter_value, key + str(docs), re.I
                            )
                            if filter_value
                            else True,
                        )
                        marker = ["property_row", key]
                        if prop_info["nullable"]:
                            marker.append("nullable")
                        property_row.mark(" ".join(marker))
                    if key == "id":
                        input = (
                            ui.label()
                            .bind_text_from(
                                bind_obj,
                                key,
                            )
                            .classes("w-full")
                        )
                        continue
                    if key == "stylesheetId":
                        input = (
                            ui.label()
                            .bind_text_from(
                                bind_obj,
                                key,
                                backward=lambda x: x.split("/")[-1]
                                if x and "/" in x
                                else "keine passende ID gefunden",
                            )
                            .classes("w-full")
                        )
                        continue
                    match prop_info["stereotype"]:
                        case "BasicType":
                            match prop_info["typename"]:
                                case "CharacterString":
                                    if key == "art":
                                        options = {}
                                        if existing := getattr(bind_obj, key, None):
                                            options.update(
                                                {
                                                    item: f"{item} (?)"
                                                    for item in existing
                                                }
                                            )
                                        input = (
                                            ui.select(
                                                options,
                                                label=prop_info["typename"],
                                                on_change=self._set_stylesheetId_and_schriftinhalt
                                                if self.editable
                                                else None,
                                                multiple=prop_info["list"],
                                                clearable=prop_info["nullable"],
                                                validation=None
                                                if prop_info["nullable"]
                                                else {"Pflichtfeld": lambda x: x},
                                            )
                                            .bind_value(
                                                bind_obj,
                                                key,
                                                forward=lambda x: None if not x else x,
                                            )
                                            .props(
                                                f"stack-label square filled dense{' readonly' if not self.editable else ''}"
                                            )
                                            .classes("w-full")
                                            .mark("art")
                                        )
                                        self.art_input = input
                                        self._get_art_options()
                                        input.validate()
                                    elif prop_info["list"]:
                                        ui.label("TODO")
                                    else:
                                        input = (
                                            ui.textarea(
                                                prop_info["typename"],
                                                validation=None
                                                if prop_info["nullable"]
                                                else {"Pflichtfeld": lambda x: x},
                                            )
                                            .bind_value(
                                                bind_obj,
                                                key,
                                                forward=lambda x: None if not x else x,
                                            )
                                            # .bind_enabled_from(self, "editable")
                                            .props(
                                                f"stack-label clearable autogrow square filled dense{' readonly' if not self.editable else ''}"
                                            )
                                            .classes("w-full")
                                        )
                                        input.validate()
                                case "Integer":
                                    if prop_info["list"]:
                                        ui.label("TODO")
                                    else:
                                        input = (
                                            ui.number(
                                                prop_info["typename"],
                                                value=default,
                                                precision=0,
                                                format="%d",
                                                validation=None
                                                if prop_info["nullable"]
                                                else {
                                                    "Pflichtfeld": lambda x: x
                                                    is not None
                                                },
                                            )
                                            .bind_value(
                                                bind_obj,
                                                key,
                                                forward=lambda x: int(x)
                                                if x is not None
                                                else None,
                                                # backward=lambda x: x,
                                            )
                                            # .bind_enabled_from(self, "editable")
                                            .props(
                                                f"stack-label clearable square filled dense{' readonly' if not self.editable else ''}"
                                            )
                                            .classes("w-full")
                                        )
                                        input.validate()
                                case "Decimal":
                                    if prop_info["list"]:
                                        ui.label("TODO")
                                    else:
                                        input = (
                                            ui.number(
                                                prop_info["typename"],
                                                # value=default,
                                                validation=None
                                                if prop_info["nullable"]
                                                else {
                                                    "Pflichtfeld": lambda x: x
                                                    is not None
                                                },
                                            )
                                            .bind_value(
                                                bind_obj,
                                                key,
                                                # backward=lambda x: None if not x else x,
                                            )
                                            # .bind_enabled_from(self, "editable")
                                            .props(
                                                f"stack-label clearable square filled dense{' readonly' if not self.editable else ''}"
                                            )
                                            .classes("w-full")
                                        )
                                        input.validate()
                                case "Boolean":
                                    ui.switch(value=default).props(
                                        "toggle-indeterminate"
                                    ).bind_value(bind_obj, key).bind_enabled_from(
                                        self, "editable"
                                    )
                                case "URI":
                                    if prop_info["list"]:
                                        ui.label("TODO")
                                    else:
                                        input = (
                                            ui.input(
                                                prop_info["typename"],
                                                validation={
                                                    "Keine URI": self.validate_uri
                                                }
                                                if prop_info["nullable"]
                                                else {
                                                    "Pflichtfeld": lambda x: x,
                                                    "Keine URI": self.validate_uri,
                                                },
                                            )
                                            .bind_value(
                                                bind_obj,
                                                key,
                                                forward=lambda x: None if not x else x,
                                            )
                                            # .bind_enabled_from(self, "editable")
                                            .classes("w-full")
                                            .props(
                                                f"stack-label square filled dense{' readonly' if not self.editable else ''}"
                                            )
                                        )
                                        input.validate()
                                case _:
                                    ui.label("BasicType")
                        case "Enumeration":
                            input = (
                                ui.select(
                                    {
                                        k: f"{v['name']} ({k})"
                                        for k, v in prop_info["enum_info"].items()
                                    },
                                    label=prop_info["typename"],
                                    multiple=prop_info["list"],
                                    clearable=prop_info["nullable"],
                                    validation=None
                                    if prop_info["nullable"]
                                    else {"Pflichtfeld": lambda x: x},
                                )
                                .classes("w-full")
                                .props(
                                    f"stack-label square filled dense{' readonly' if not self.editable else ''}"
                                )
                                .bind_value(
                                    bind_obj,
                                    key,
                                    backward=lambda x: None if not x else x,
                                )
                                # .bind_enabled_from(self, "editable")
                            )
                            if prop_info["list"]:
                                input.props("use-chips")
                            input.validate()
                        case "Codelist":
                            uri_validator = (
                                lambda value: self.validate_uri(value)
                                if isinstance(value, str)
                                else all([self.validate_uri(item) for item in value])
                                if value
                                else True
                            )
                            input = (
                                ui.select(
                                    await get_codelist_options(
                                        values=getattr(bind_obj, key, []),
                                        codelist=prop_info["typename"],
                                    ),
                                    label=prop_info["typename"],
                                    multiple=prop_info["list"],
                                    clearable=prop_info["nullable"],
                                    with_input=True,
                                    # new_value_mode="add-unique",
                                    # key_generator=lambda value,
                                    # codelist=prop_info[
                                    #     "typename"
                                    # ]: f"urn:{self.appschema}:codelist:{codelist}:{value}",
                                    validation={"Keine URI": uri_validator}
                                    if prop_info["nullable"]
                                    else {
                                        "Pflichtfeld": lambda x: x,
                                        "Keine URI": uri_validator,
                                    },
                                )
                                .bind_value(
                                    bind_obj,
                                    key,
                                    forward=lambda x: None if not x else x,
                                )
                                # .bind_enabled_from(self, "editable")
                                .classes("w-full")
                                .props(
                                    f"stack-label square filled dense{' readonly' if not self.editable else ''}"
                                )
                            )
                            if prop_info["list"]:
                                input.props("use-chips")
                            input.validate()
                            with input.add_slot("after"):
                                ui.button(
                                    icon="o_open_in_new",
                                    on_click=lambda input=input: ui.navigate.to(
                                        input.value
                                    ),
                                ).bind_enabled_from(
                                    input,
                                    "value",
                                    backward=lambda value: "registry.gdi-de.org"
                                    in value
                                    if value
                                    else False,
                                ).props("square unelevated")
                        case "Temporal":
                            match prop_info["typename"]:
                                case "Date":
                                    with (
                                        ui.input(
                                            prop_info["typename"],
                                            validation=None
                                            if prop_info["nullable"]
                                            else {"Pflichtfeld": lambda x: x},
                                        )
                                        .bind_value(
                                            bind_obj,
                                            key,
                                            backward=lambda x: None if not x else x,
                                        )
                                        # .bind_enabled_from(self, "editable")
                                        .classes("w-full")
                                        .props(
                                            f"stack-label square filled dense{' readonly' if not self.editable else ''}"
                                        ) as date
                                    ):
                                        with date.add_slot("append"):
                                            ui.icon("edit_calendar").on(
                                                "click", lambda: menu.open()
                                            ).classes("cursor-pointer")
                                        with ui.menu() as menu:
                                            ui.date().bind_value(date).props(
                                                f"{' readonly' if not self.editable else ''}"
                                            )
                                        date.validate()
                                case "TM_Duration":
                                    input = (
                                        ui.number(
                                            prop_info["typename"],
                                            value=default,
                                            suffix="Tage",
                                            step=1,
                                            precision=0,
                                            format="%d",
                                            validation=None
                                            if prop_info["nullable"]
                                            else {
                                                "Pflichtfeld": lambda x: x is not None
                                            },
                                        )
                                        # .bind_enabled_from(self, "editable")
                                        .bind_value(
                                            bind_obj,
                                            key,
                                            forward=lambda x: x * 86400 if x else None,
                                            backward=lambda x: x / 86400 if x else None,
                                        )
                                        .props(
                                            f"stack-label clearable square filled dense{' readonly' if not self.editable else ''}"
                                        )
                                        .classes("w-full")
                                    )
                                    input.validate()
                                case _:
                                    ui.label("TODO Temporal")
                        case "DataType":
                            await self._build_sub_form(
                                key, bind_obj, prop_info, preview
                            )
                        case "Association":
                            if prop_info["list"]:
                                with (
                                    ui.button(
                                        icon="link",
                                        on_click=lambda e,
                                        bind_obj=bind_obj,
                                        key=key,
                                        prop_info=prop_info: self._render_assoc_dialog(
                                            e.sender, bind_obj, key, prop_info
                                        ),
                                    )
                                    .bind_enabled_from(self, "editable")
                                    .props("square unelevated")
                                ):
                                    ui.badge(text=0, color="secondary").props(
                                        "floating"
                                    ).bind_text_from(
                                        bind_obj,
                                        key,
                                        backward=lambda x: len(x)
                                        if isinstance(x, list)
                                        else 1
                                        if x
                                        else 0,
                                    )
                            else:
                                # with ui.row(align_items="center"):
                                targets = self._get_association_targets(
                                    key,
                                    prop_info["typename"],
                                    plan_id=app.storage.client.get("plan_id", None),
                                )
                                input = (
                                    ui.select(
                                        targets,
                                        # value=keys[0]
                                        # if len((keys := list(targets.keys()))) == 1
                                        # else None,
                                        multiple=prop_info["list"],
                                        label="Keine passenden Features vorhanden"
                                        if not targets
                                        else None,
                                        clearable=prop_info["nullable"],
                                        validation=None
                                        if prop_info["nullable"]
                                        else {"Pflichtfeld": lambda x: x},
                                    )
                                    .bind_value(
                                        bind_obj,
                                        key,
                                        backward=lambda x: None if not x else x,
                                    )
                                    # .bind_enabled_from(self, "editable")
                                    # .props(
                                    #     "use-chips counter options-dense options-cover"
                                    # )
                                    .classes("w-full")
                                    .props(
                                        f"square filled dense{' readonly' if not (targets and self.editable) else ''}"
                                    )
                                )
                                if parent_id := app.storage.client.get(
                                    "parent_id", None
                                ):
                                    input.set_value(parent_id)
                                elif len((keys := list(targets.keys()))) == 1:
                                    input.set_value(keys[0])
                                input.validate()
                                with input.add_slot("after"):
                                    ui.button(
                                        icon="o_open_in_new",
                                        on_click=lambda input=input: ui.navigate.to(
                                            f"/feature/{input.value}"
                                        ),
                                    ).bind_enabled_from(input, "value").props(
                                        "square unelevated"
                                    )
                                    ui.button(
                                        icon="o_preview",
                                        on_click=lambda assoc_id=input.value: self._preview_association(
                                            assoc_id
                                        ),
                                    ).bind_enabled_from(input, "value").props(
                                        "square unelevated"
                                    )
                                # with input.add_slot("no-option"):
                                #     ui.item(
                                #         f"Keine passenden Features vom Typ {prop_info['typename'] if isinstance(prop_info['typename'], str) else '/'.join(prop_info['typename'])} vorhanden"
                                #     )
                                # if (
                                #     base_form
                                #     and isinstance(prop_info["typename"], str)
                                #     and not self._is_geometry_required(
                                #         prop_info["typename"]
                                #     )
                                # ):
                                #     with input.add_slot("after"):
                                #         ui.button(
                                #             icon="add",
                                #             on_click=partial(
                                #                 self._render_dialog,
                                #                 prop_info["typename"],
                                #                 input,
                                #             ),
                                #         ).props("flat round")
                        case "Measure":
                            uom = str(prop_info["uom"])
                            input = (
                                ui.number(
                                    f"{prop_info['typename']} [{prop_info['uom']}]",
                                    suffix=prop_info["uom"],
                                    min=-360.0 if uom == "grad" else None,
                                    max=360.0 if uom == "grad" else None,
                                    validation=None
                                    if prop_info["nullable"]
                                    else {"Pflichtfeld": lambda x: x},
                                )
                                .bind_value(
                                    bind_obj,
                                    key,
                                    forward=lambda x, uom=uom: None
                                    if not x
                                    else {"value": x, "uom": uom},
                                    backward=lambda x: None if not x else x["value"],
                                )
                                # .bind_enabled_from(self, "editable")
                                .classes("w-full")
                                .props(
                                    f"stack-label square filled dense{' readonly' if not self.editable else ''}"
                                )
                            )
                            input.validate()
                        case _:
                            ui.label("TODO")
        self.rendered = True

    async def _render_dialog(
        self, typename: str, linked: ui.list, model_parent: ui.item
    ):
        """Render a dialog for creating a new sub-model object.

        Args:
            typename (str): The type name of the sub-model.
            linked (ui.list): The UI list to add the new object to.
            model_parent (ui.item): The parent UI item for the model.
        """

        def handle_submit():
            if sub_model_instance := sub_model_form.model_instance:
                dialog.submit(sub_model_instance)

        with linked:
            with ui.dialog().props("persistent") as dialog, ui.card():
                with ui.card_section():
                    ui.label(f"Neues {typename}-Objekt").classes("text-h6")
                with ui.card_section().style(
                    "width: auto; max-width: none"
                ) as model_content:
                    sub_model_form = ModelForm(
                        self.appschema, self.version, model_content
                    )
                    await sub_model_form.render_form(
                        True, typename, {"id": str(uuid4())}
                    )
                with ui.card_actions():
                    ui.button(
                        text="Speichern",
                        icon="save",
                        on_click=handle_submit,
                    ).props("unelevated rounded no-caps")
                    ui.button(
                        text="Abbrechen",
                        icon="cancel",
                        on_click=lambda: dialog.submit(None),
                    ).props("unelevated rounded no-caps")
        sub_model: BaseFeature = await dialog
        if sub_model:
            self.sub_models[sub_model.id] = sub_model
            with linked:
                with (
                    ui.item(
                        on_click=lambda e: self._render_object(
                            sub_model.id, model_parent, True
                        )
                    )
                    .props("clickable")
                    .mark(sub_model.id) as parent
                ):
                    with ui.item_section().props("side"):
                        ui.button(
                            icon="link_off",
                            on_click=lambda parent=parent,
                            model_parent=model_parent,
                            id=sub_model.id: (
                                parent.delete(),
                                model_parent.clear(),
                                self.sub_models.pop(id, None),
                            ),
                        ).props("flat round")
                    with ui.item_section():
                        ui.item_label(sub_model.get_name())
                        ui.item_label(sub_model.id).props("caption")
                    with ui.item_section().props("side"):
                        ui.icon("data_object").props("flat").tooltip("neues Objekt")
        dialog.delete()

    async def _render_object(
        self,
        id: str,
        parent: ui.element,
        new_feature: bool = False,
        preview: bool = False,
    ):
        """Render an object in the UI for preview or editing.

        Args:
            id (str): The ID of the object to render.
            parent (ui.element): The parent UI element.
            new_feature (bool, optional): Whether this is a new feature. Defaults to False.
            preview (bool, optional): Whether to render in preview mode. Defaults to False.
        """
        parent.clear()
        with parent:
            try:
                linked_feature = (
                    self.sub_models[id]
                    if new_feature or id in self.sub_models.keys()
                    else settings.repo.get(id)
                )
                await ModelForm(
                    self.appschema,
                    self.version,
                    parent,
                ).render_form(
                    False,
                    linked_feature.get_name(),
                    linked_feature.model_dump(
                        mode="json", context={"datatype": True, "file_uri": True}
                    ),
                    True,
                    preview=True,
                )
            except ValidationError:
                ui.notify(
                    "Fehler beim Anzeigen der Daten",
                    type="negative",
                    position="top",
                )

    async def _render_assoc_dialog(
        self, parent: ui.element, bind_obj: type, key: str, prop_info: dict
    ):
        """Render a dialog for editing associations.

        Args:
            parent (ui.element): The parent UI element.
            bind_obj (type): The bindable object containing the association.
            key (str): The property key for the association.
            prop_info (dict): The property info for the association.
        """

        def handle_link_button(e: events.ClickEventArguments):
            item = next(filter(lambda element: element._markers, e.sender.ancestors()))
            if (item_id := item._markers[0]) in self.sub_models.keys():
                item.delete()
                self.sub_models.pop(item_id)
                if isinstance(getattr(bind_obj, key), list):
                    getattr(bind_obj, key).remove(item_id)
                else:
                    setattr(bind_obj, key, None)
                return
            target = (
                linked
                if any(
                    filter(lambda element: element == candidates, e.sender.ancestors())
                )
                else candidates
            )
            if target == linked and not prop_info["list"] and any(linked.descendants()):
                return ui.notify(
                    f"mehrfache Belegung von {key} nicht möglich",
                    type="negative",
                    position="top",
                )
            item.move(target)
            e.sender.props("icon=add_link") if e.sender.props[
                "icon"
            ] == "link_off" else e.sender.props("icon=link_off")

        with parent:
            with (
                ui.dialog() as dialog,
                ui.card().style("width: auto; max-width: none"),
            ):
                with ui.card_section():
                    ui.label("Relationseditor").classes("text-h4")
                with ui.card_section():
                    with ui.grid(columns="auto 75vh auto").classes("w-full"):
                        with ui.column():
                            ui.label("Bestehende Relationen").classes("text-h6")
                            with (
                                ui.list()
                                .classes("w-full")
                                .props("bordered separator dense") as linked
                            ):
                                associations = await run.io_bound(
                                    self._get_association_info,
                                    getattr(bind_obj, key, []),
                                )
                                for id, featuretype in associations.items():
                                    with (
                                        ui.item(
                                            on_click=lambda id=id: self._render_object(
                                                id, center
                                            )
                                        )
                                        .props("clickable")
                                        .mark(id) as parent
                                    ):
                                        with ui.item_section().props("side"):
                                            ui.button(
                                                icon="link_off",
                                                on_click=handle_link_button,
                                            ).props("flat round")
                                        with ui.item_section():
                                            ui.item_label(featuretype)
                                            ui.item_label(id).props("caption")
                                        with ui.item_section().props("side"):
                                            ui.icon("data_object").props(
                                                "flat"
                                            ).tooltip(
                                                "neues Objekt"
                                            ) if id in self.sub_models.keys() else ui.icon(
                                                "dataset_linked"
                                            ).props("flat").tooltip(
                                                "bestehende Relation"
                                            )
                        with ui.column():
                            with ui.row().classes("flex-center w-full"):
                                ui.label("Objektattribute").classes("text-h6")
                            with (
                                ui.card()
                                .props("flat square bordered")
                                .classes("w-full") as center
                            ):
                                ui.label("Objekt auswählen").classes("text-italic")
                        with ui.column():
                            ui.label("Mögliche Relationen").classes("text-h6")
                            with (
                                ui.list()
                                .classes("w-full")
                                .props("bordered separator dense") as candidates
                            ):
                                associations = await run.io_bound(
                                    self._get_association_targets,
                                    key,
                                    prop_info["typename"],
                                    getattr(bind_obj, key, None) or [],
                                    app.storage.client.get("plan_id", None),
                                )
                                for id, featuretype in associations.items():
                                    with (
                                        ui.item(
                                            on_click=lambda id=id: self._render_object(
                                                id, center
                                            )
                                        )
                                        .props("clickable")
                                        .mark(id) as parent
                                    ):
                                        with ui.item_section().props("side"):
                                            ui.button(
                                                icon="add_link",
                                                on_click=handle_link_button,
                                            ).props("flat round")
                                        with ui.item_section():
                                            ui.item_label(featuretype)
                                            ui.item_label(id).props("caption")
                                        with ui.item_section().props("side"):
                                            ui.icon("dataset").props("flat").tooltip(
                                                "neue Relation"
                                            )
                with ui.card_actions().classes("w-full"):
                    ui.button(
                        text="Speichern",
                        icon="save",
                        on_click=lambda: dialog.submit(
                            {
                                "type": "save",
                                "linked": [
                                    item._markers[0]
                                    for item in linked.descendants()
                                    if item._markers
                                ],
                            }
                        ),
                    ).props("unelevated rounded no-caps")
                    ui.button(
                        text="Abbrechen",
                        icon="cancel",
                        on_click=lambda: dialog.submit(
                            {
                                "type": "cancel",
                                "linked": [
                                    item._markers[0]
                                    for item in linked.descendants()
                                    if item._markers
                                ],
                            }
                        ),
                    ).props("unelevated rounded no-caps")
                    if isinstance(
                        prop_info["typename"], str
                    ) and not self._is_geometry_required(prop_info["typename"]):
                        ui.space()
                        ui.button(
                            text="Objekt hinzufügen",
                            icon="add",
                            on_click=partial(
                                self._render_dialog,
                                prop_info["typename"],
                                linked,
                                center,
                            ),
                        ).props("unelevated rounded no-caps")

        action = await dialog
        if action and action["type"] == "save":
            setattr(
                bind_obj,
                key,
                action["linked"]
                if prop_info["list"]
                else action["linked"][0]
                if action["linked"]
                else None,
            )
        else:
            # delete orphaned sub-models on cancelation
            for id in list(self.sub_models.keys()):
                if id in action["linked"]:
                    self.sub_models.pop(id)

        dialog.delete()

    async def render_form(
        self,
        editable: bool,
        feature_type: str,
        feature: dict,
        main_form: bool = True,
        preview: bool = False,
    ):
        """Render the form for a given feature.

        Args:
            editable (bool): Whether the form is editable.
            feature_type (str): The type of the feature.
            feature (dict): The feature data as a dictionary.
            main_form (bool, optional): Whether this is the main form. Defaults to True.
            preview (bool, optional): Whether to render in preview mode. Defaults to False.
        """
        self.parent.clear()
        self.editable = editable
        self.featuretype = feature_type
        self.model = model_factory(feature_type, self.version, self.appschema)
        if main_form:
            if (geom := feature.pop("geometry", None)) and (
                model_geom := self.model.get_geom_field()
            ):
                feature[model_geom] = geom
        self.feature = self._get_bindable_object(self.model)(**feature)
        await self._build_model_form(self.model, self.feature, main_form, preview)
