"""CRUD auto-generation from Pydantic models.

Generates list/create/detail/edit/delete routes and UI from a single
Pydantic BaseModel + Storage backend.
"""

from __future__ import annotations

import json
import math
import urllib.parse
import uuid
from abc import ABC, abstractmethod
from collections.abc import Awaitable
from typing import Any, Callable, Generic, TypeVar, get_origin

from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, ValidationError

from kokage_ui.components import Alert
from kokage_ui.elements import A, Button, Component, Div, H1
from kokage_ui.features.i18n import t
from kokage_ui.htmx import ConfirmDelete, SearchFilter
from kokage_ui.models import ModelDetail, ModelForm, ModelTable, _extract_datetime_field, _extract_media_field, _resolve_annotation

FileHandler = Callable[[str, Any], Awaitable[str]]

T = TypeVar("T", bound=BaseModel)


# ========================================
# Storage ABC
# ========================================


class Storage(ABC, Generic[T]):
    """Abstract async storage interface for CRUD operations."""

    @abstractmethod
    async def list(
        self, *, skip: int = 0, limit: int = 20, search: str | None = None
    ) -> tuple[list[T], int]:
        """Return (items, total_count)."""

    @abstractmethod
    async def get(self, id: str) -> T | None:
        """Get a single item by ID."""

    @abstractmethod
    async def create(self, data: T) -> T:
        """Create a new item. Returns the created item (with ID assigned)."""

    @abstractmethod
    async def update(self, id: str, data: T) -> T | None:
        """Update an existing item. Returns updated item or None if not found."""

    @abstractmethod
    async def delete(self, id: str) -> bool:
        """Delete an item. Returns True if deleted, False if not found."""

    async def reorder(self, ids: list[str]) -> None:
        """Reorder items by the given ID list. Default is a no-op."""


# ========================================
# InMemoryStorage
# ========================================


class InMemoryStorage(Storage[T]):
    """In-memory dict-based storage with UUID auto-assignment.

    Args:
        model: Pydantic model class.
        initial: Optional list of initial items.
        id_field: Name of the ID field (default: "id").
    """

    def __init__(
        self,
        model: type[T],
        *,
        initial: list[T] | None = None,
        id_field: str = "id",
    ) -> None:
        self._model = model
        self._id_field = id_field
        self._data: dict[str, T] = {}

        if initial:
            for item in initial:
                item_id = getattr(item, id_field, "") or ""
                if not item_id:
                    item_id = str(uuid.uuid4())[:8]
                    item = item.model_copy(update={id_field: item_id})
                self._data[item_id] = item

    async def list(
        self, *, skip: int = 0, limit: int = 20, search: str | None = None
    ) -> tuple[list[T], int]:
        items = list(self._data.values())
        if search:
            search_lower = search.lower()
            items = [
                item
                for item in items
                if any(
                    search_lower in str(getattr(item, f, "")).lower()
                    for f in self._model.model_fields
                )
            ]
        total = len(items)
        return items[skip : skip + limit], total

    async def get(self, id: str) -> T | None:
        return self._data.get(id)

    async def create(self, data: T) -> T:
        item_id = getattr(data, self._id_field, "") or ""
        if not item_id:
            item_id = str(uuid.uuid4())[:8]
            data = data.model_copy(update={self._id_field: item_id})
        self._data[item_id] = data
        return data

    async def update(self, id: str, data: T) -> T | None:
        if id not in self._data:
            return None
        data = data.model_copy(update={self._id_field: id})
        self._data[id] = data
        return data

    async def delete(self, id: str) -> bool:
        if id in self._data:
            del self._data[id]
            return True
        return False

    async def reorder(self, ids: list[str]) -> None:
        new_data: dict[str, T] = {}
        for id_ in ids:
            if id_ in self._data:
                new_data[id_] = self._data[id_]
        for id_, item in self._data.items():
            if id_ not in new_data:
                new_data[id_] = item
        self._data = new_data


# ========================================
# Pagination Component
# ========================================


class Pagination(Component):
    """DaisyUI join button group for pagination with htmx support.

    Args:
        current_page: Current page number (1-indexed).
        total_pages: Total number of pages.
        base_url: URL template for page links.
        search: Optional search query to preserve in URLs.
    """

    tag = "div"

    def __init__(
        self,
        *,
        current_page: int,
        total_pages: int,
        base_url: str,
        target: str = "#table-container",
        search: str | None = None,
        **attrs: Any,
    ) -> None:
        if total_pages <= 1:
            super().__init__(**attrs)
            return

        # Show at most 7 page buttons: first, last, current, and neighbors
        max_visible = 7
        if total_pages <= max_visible:
            visible = list(range(1, total_pages + 1))
        else:
            visible_set = {1, total_pages}
            half = (max_visible - 2) // 2
            start = max(2, current_page - half)
            end = min(total_pages - 1, current_page + half)
            # Adjust if near edges
            if end - start < max_visible - 3:
                if start == 2:
                    end = min(total_pages - 1, start + max_visible - 3)
                else:
                    start = max(2, end - (max_visible - 3))
            visible_set.update(range(start, end + 1))
            visible = sorted(visible_set)

        buttons: list[Any] = []
        for page_num in visible:
            url = f"{base_url}?page={page_num}"
            if search:
                url += f"&q={search}"
            btn_cls = "join-item btn btn-sm"
            if page_num == current_page:
                btn_cls += " btn-active"
            buttons.append(
                Button(
                    str(page_num),
                    cls=btn_cls,
                    hx_get=url,
                    hx_target=target,
                )
            )

        attrs["cls"] = "join mt-4"
        super().__init__(*buttons, **attrs)


# ========================================
# CRUDRouter
# ========================================


_to_html_string_fn: Callable | None = None


def _to_html_string_lazy(result: Any) -> str:
    """Convert to HTML string with lazy import to avoid circular imports."""
    global _to_html_string_fn
    if _to_html_string_fn is None:
        from kokage_ui.core import _to_html_string

        _to_html_string_fn = _to_html_string
    return _to_html_string_fn(result)


def _process_list_fields(
    model: type[BaseModel],
    raw_data: dict[str, Any],
    form_data: Any,
    exclude: list[str],
) -> None:
    """Handle list fields: collect multiple form values via getlist()."""
    for field_name, field_info in model.model_fields.items():
        if field_name in exclude:
            continue
        base_type, _ = _resolve_annotation(field_info.annotation)
        if get_origin(base_type) is list:
            raw_data[field_name] = form_data.getlist(field_name)


def _process_bool_fields(
    model: type[BaseModel],
    raw_data: dict[str, Any],
    form_data: Any,
    exclude: list[str],
) -> None:
    """Handle bool fields: unchecked checkboxes are absent from form data."""
    for field_name, field_info in model.model_fields.items():
        if field_name in exclude:
            continue
        base_type, _ = _resolve_annotation(field_info.annotation)
        if base_type is bool:
            raw_data[field_name] = field_name in form_data


async def _process_media_fields(
    model: type[BaseModel],
    raw_data: dict[str, Any],
    form_data: Any,
    exclude: list[str],
    file_handler: FileHandler | None,
    existing_instance: BaseModel | None = None,
) -> None:
    """Process media fields: call file_handler for uploaded files."""
    for field_name, field_info in model.model_fields.items():
        if field_name in exclude:
            continue
        media = _extract_media_field(field_info)
        if media is None:
            continue

        file_value = form_data.get(field_name)

        # Check if a file was actually uploaded
        if file_value and hasattr(file_value, "filename") and file_value.filename:
            if file_handler:
                url = await file_handler(field_name, file_value)
                raw_data[field_name] = url
            else:
                raw_data[field_name] = ""
        else:
            # No new file uploaded → keep existing value (edit mode)
            if existing_instance:
                raw_data[field_name] = getattr(existing_instance, field_name, "")
            else:
                raw_data[field_name] = raw_data.get(field_name, "")


class CRUDRouter(Generic[T]):
    """Auto-register all CRUD routes for a Pydantic model.

    Args:
        app: FastAPI application.
        prefix: URL prefix (e.g., "/users").
        model: Pydantic BaseModel class.
        storage: Storage backend.
        id_field: Name of the ID field.
        per_page: Items per page.
        title: Display title (defaults to model name + "s").
        exclude_fields: Fields to exclude from all views.
        table_exclude: Fields to exclude from table view.
        form_exclude: Fields to exclude from forms.
        page_wrapper: Optional callable(content, title) → Page for custom layout.
        theme: DaisyUI theme name.
        realtime_validation: Enable per-field htmx validation.
        file_handler: Async callback (field_name, UploadFile) → URL string.
        sortable: Enable drag & drop reordering with SortableJS.
    """

    def __init__(
        self,
        app: FastAPI,
        prefix: str,
        model: type[T],
        storage: Storage[T],
        *,
        id_field: str = "id",
        per_page: int = 20,
        title: str | None = None,
        exclude_fields: list[str] | None = None,
        table_exclude: list[str] | None = None,
        form_exclude: list[str] | None = None,
        page_wrapper: Callable[..., Any] | None = None,
        theme: str = "light",
        realtime_validation: bool = False,
        file_handler: FileHandler | None = None,
        sortable: bool = False,
    ) -> None:
        self.app = app
        self.prefix = prefix.rstrip("/")
        self.model = model
        self.storage = storage
        self.id_field = id_field
        self.per_page = per_page
        self.title = title or f"{model.__name__}s"
        self.page_wrapper = page_wrapper
        self.theme = theme
        self.realtime_validation = realtime_validation
        self.file_handler = file_handler
        self.sortable = sortable

        # Cache computed exclude lists
        _exclude = exclude_fields or []
        self._table_exclude = list(set(_exclude + (table_exclude or [])))
        self._form_exclude = list(set(_exclude + (form_exclude or []) + [id_field]))

        self._register_routes()
        if self.realtime_validation:
            self._register_validation_routes()

    def _get_table_exclude(self) -> list[str]:
        return self._table_exclude

    def _get_form_exclude(self) -> list[str]:
        return self._form_exclude

    def _has_media_fields(self) -> bool:
        """Check if the model has any MediaField annotated fields."""
        for fi in self.model.model_fields.values():
            if _extract_media_field(fi) is not None:
                return True
        return False

    def _has_datetime_fields(self) -> bool:
        """Check if the model has any DateField/TimeField/DateTimeField annotated fields."""
        import datetime as dt_mod

        for fi in self.model.model_fields.values():
            if _extract_datetime_field(fi) is not None:
                return True
            base_type, _ = _resolve_annotation(fi.annotation)
            if isinstance(base_type, type) and issubclass(base_type, (dt_mod.date, dt_mod.time)):
                return True
        return False

    def _build_form(self, **kwargs: Any) -> Any:
        """Build a ModelForm or ValidatedModelForm depending on config."""
        if self._has_media_fields():
            kwargs.setdefault("hx_encoding", "multipart/form-data")
        if self.realtime_validation:
            from kokage_ui.models import ValidatedModelForm

            return ValidatedModelForm(
                self.model,
                validate_url=f"{self.prefix}/_validate",
                **kwargs,
            )
        return ModelForm(self.model, **kwargs)

    def _register_validation_routes(self) -> None:
        """Register per-field validation endpoints."""
        from pydantic import ValidationError as PydanticValidationError

        from kokage_ui.models import _SENTINEL, _field_to_component, _filter_fields

        fields = _filter_fields(self.model, include=None, exclude=self._get_form_exclude())
        prefix = self.prefix

        for field_name, field_info in fields:

            def _make_handler(name: str, fi: Any) -> Any:
                async def handler(request: Request) -> HTMLResponse:
                    form_data = await request.form()
                    raw_data = dict(form_data)

                    error_msg = None
                    try:
                        self.model.model_validate(raw_data)
                    except PydanticValidationError as e:
                        for err in e.errors():
                            loc = err.get("loc", ())
                            if loc and str(loc[0]) == name:
                                error_msg = err.get("msg", "Invalid value")
                                break

                    component = _field_to_component(
                        name,
                        fi,
                        value=raw_data.get(name, _SENTINEL),
                        error_message=error_msg,
                        field_id=f"field-{name}",
                        extra_input_attrs={
                            "hx_post": f"{prefix}/_validate/{name}",
                            "hx_trigger": "change delay:500ms",
                            "hx_target": f"#field-{name}",
                            "hx_swap": "outerHTML",
                        },
                    )
                    return HTMLResponse(content=component.render())

                return handler

            self.app.add_api_route(
                f"{prefix}/_validate/{field_name}",
                _make_handler(field_name, field_info),
                methods=["POST"],
                response_class=HTMLResponse,
            )

    def _wrap_page(self, content: Any, page_title: str) -> Any:
        if self.page_wrapper:
            return self.page_wrapper(content, page_title)
        from kokage_ui.page import Page

        return Page(
            content,
            title=page_title,
            theme=self.theme,
            include_toast=True,
            include_sortablejs=self.sortable,
            include_flatpickr=self._has_datetime_fields(),
        )

    @staticmethod
    def _toast_url(base_url: str, message: str, toast_type: str = "success") -> str:
        """Append toast query parameters to a URL."""
        sep = "&" if "?" in base_url else "?"
        return f"{base_url}{sep}_toast={urllib.parse.quote(message)}&_toast_type={toast_type}"

    def _render_actions(self, row: T) -> Component:
        """Render Edit + Delete action buttons for a table row."""
        row_id = getattr(row, self.id_field)
        return Div(
            A(t("crud.edit"), href=f"{self.prefix}/{row_id}/edit", cls="btn btn-warning btn-xs"),
            ConfirmDelete(
                t("crud.delete"),
                url=f"{self.prefix}/{row_id}",
                confirm_message=t("crud.confirm_delete", name=self.model.__name__),
                target="body",
                cls="btn btn-error btn-outline btn-xs ml-2",
            ),
            cls="flex gap-1",
        )

    def _build_list_table(
        self,
        items: list[T],
        total: int,
        page: int,
        search: str | None,
    ) -> Component:
        """Build the table + pagination fragment."""
        if self.sortable:
            from kokage_ui.fields.sortable import SortableList

            list_items = []
            for row in items:
                row_id = str(getattr(row, self.id_field))
                label = row_id
                for fname in self.model.model_fields:
                    if fname == self.id_field:
                        continue
                    val = getattr(row, fname, None)
                    if isinstance(val, str) and val:
                        label = val
                        break
                list_items.append({"id": row_id, "label": label})
            return Div(
                SortableList(items=list_items, url=f"{self.prefix}/_reorder"),
                id="table-container",
            )

        total_pages = max(1, math.ceil(total / self.per_page))
        table_exclude = self._get_table_exclude()

        # Build cell renderers: make ID field a link to detail page
        cell_renderers: dict[str, Callable[[Any], Any]] = {
            self.id_field: lambda v: A(str(v), href=f"{self.prefix}/{v}"),
        }

        table = ModelTable(
            self.model,
            rows=items,
            exclude=table_exclude,
            zebra=True,
            cell_renderers=cell_renderers,
            extra_columns={t("common.actions"): self._render_actions},
        )

        pagination = Pagination(
            current_page=page,
            total_pages=total_pages,
            base_url=f"{self.prefix}/_list",
            target="#table-container",
            search=search,
        )

        return Div(table, pagination, id="table-container")

    def _register_routes(self) -> None:
        prefix = self.prefix
        app = self.app
        router = self

        # --- GET {prefix} — List page ---
        async def list_page(request: Request, page: int = 1, q: str = "") -> HTMLResponse:
            skip = (page - 1) * router.per_page
            items, total = await router.storage.list(
                skip=skip, limit=router.per_page, search=q or None
            )
            table_fragment = router._build_list_table(items, total, page, q or None)

            search_input = SearchFilter(
                url=f"{prefix}/_list",
                target="#table-container",
                placeholder=t("crud.search_placeholder", title=router.title.lower()),
                cls="input input-bordered w-full mb-4",
            )

            content = Div(
                H1(router.title, cls="text-3xl font-bold mb-6"),
                Div(
                    A(t("crud.new"), href=f"{prefix}/new", cls="btn btn-primary btn-sm"),
                    cls="mb-4",
                ),
                search_input,
                table_fragment,
                cls="container mx-auto p-4",
            )

            page_obj = router._wrap_page(content, router.title)
            return HTMLResponse(content=_to_html_string_lazy(page_obj))

        app.add_api_route(
            prefix,
            list_page,
            methods=["GET"],
            response_class=HTMLResponse,
        )

        # --- GET {prefix}/_list — Table fragment (htmx) ---
        async def list_fragment(
            request: Request, page: int = 1, q: str = ""
        ) -> HTMLResponse:
            skip = (page - 1) * router.per_page
            items, total = await router.storage.list(
                skip=skip, limit=router.per_page, search=q or None
            )
            table_fragment = router._build_list_table(items, total, page, q or None)
            return HTMLResponse(content=_to_html_string_lazy(table_fragment))

        app.add_api_route(
            f"{prefix}/_list",
            list_fragment,
            methods=["GET"],
            response_class=HTMLResponse,
        )

        # --- GET {prefix}/new — Create form page ---
        async def create_page(request: Request) -> HTMLResponse:
            form = router._build_form(
                action=f"{prefix}/new",
                method="post",
                submit_text=t("crud.create"),
                submit_color="primary",
                exclude=router._get_form_exclude(),
                hx_post=f"{prefix}/new",
                hx_target="#form-container",
            )

            new_title = t("crud.new_title", name=router.model.__name__)
            content = Div(
                H1(new_title, cls="text-3xl font-bold mb-6"),
                Div(form, id="form-container", cls="max-w-lg"),
                cls="container mx-auto p-4",
            )

            page_obj = router._wrap_page(content, new_title)
            return HTMLResponse(content=_to_html_string_lazy(page_obj))

        # --- POST {prefix}/new — Create handler ---
        async def create_handler(request: Request) -> Response:
            form_data = await request.form()
            raw_data = dict(form_data)
            _process_bool_fields(router.model, raw_data, form_data, router._get_form_exclude())
            _process_list_fields(router.model, raw_data, form_data, router._get_form_exclude())
            await _process_media_fields(router.model, raw_data, form_data, router._get_form_exclude(), router.file_handler)

            try:
                instance = router.model.model_validate(raw_data)
                created = await router.storage.create(instance)
                created_id = getattr(created, router.id_field)
                redirect_url = router._toast_url(
                    f"{prefix}/{created_id}", t("crud.created")
                )

                # htmx request → HX-Redirect header
                if request.headers.get("hx-request"):
                    return Response(
                        status_code=200,
                        headers={"HX-Redirect": redirect_url},
                    )
                # Normal form → redirect
                return Response(
                    status_code=303,
                    headers={"Location": redirect_url},
                )
            except ValidationError as e:
                error_list = e.errors()
                form = router._build_form(
                    action=f"{prefix}/new",
                    method="post",
                    submit_text=t("crud.create"),
                    submit_color="primary",
                    exclude=router._get_form_exclude(),
                    values=raw_data,
                    errors=error_list,
                    hx_post=f"{prefix}/new",
                    hx_target="#form-container",
                )
                html = _to_html_string_lazy(form)

                # htmx request → return form fragment
                if request.headers.get("hx-request"):
                    return HTMLResponse(content=html, status_code=422)

                # Normal request → full page with error form
                new_title = t("crud.new_title", name=router.model.__name__)
                content = Div(
                    H1(new_title, cls="text-3xl font-bold mb-6"),
                    Div(form, id="form-container", cls="max-w-lg"),
                    cls="container mx-auto p-4",
                )
                page_obj = router._wrap_page(content, new_title)
                return HTMLResponse(
                    content=_to_html_string_lazy(page_obj), status_code=422
                )

        # Register /new BEFORE /{id} to avoid path conflicts
        app.add_api_route(
            f"{prefix}/new",
            create_page,
            methods=["GET"],
            response_class=HTMLResponse,
        )
        app.add_api_route(
            f"{prefix}/new",
            create_handler,
            methods=["POST"],
        )

        # --- POST {prefix}/_reorder — Reorder handler (sortable) ---
        if router.sortable:

            async def reorder_handler(request: Request) -> Response:
                form_data = await request.form()
                ids_raw = form_data.get("ids", "[]")
                try:
                    ids = json.loads(ids_raw)
                except (json.JSONDecodeError, ValueError):
                    return Response(status_code=400, content="Invalid ids")
                await router.storage.reorder([str(i) for i in ids])
                return Response(status_code=204)

            app.add_api_route(
                f"{prefix}/_reorder",
                reorder_handler,
                methods=["POST"],
            )

        # --- GET {prefix}/{id} — Detail page ---
        async def detail_page(request: Request, id: str) -> HTMLResponse:
            item = await router.storage.get(id)
            if item is None:
                content = Div(
                    Alert(t("crud.not_found"), variant="error"),
                    cls="container mx-auto p-4",
                )
                page_obj = router._wrap_page(content, t("crud.not_found_title"))
                return HTMLResponse(
                    content=_to_html_string_lazy(page_obj), status_code=404
                )

            detail = ModelDetail(item, title=str(getattr(item, router.id_field)))

            actions = Div(
                A(t("crud.edit"), href=f"{prefix}/{id}/edit", cls="btn btn-warning btn-sm"),
                ConfirmDelete(
                    t("crud.delete"),
                    url=f"{prefix}/{id}",
                    confirm_message=t("crud.confirm_delete", name=router.model.__name__),
                    target="body",
                    cls="btn btn-error btn-outline btn-sm ml-2",
                ),
                A(t("crud.back_to_list"), href=prefix, cls="btn btn-ghost btn-sm ml-2"),
                cls="mt-4",
            )

            detail_title = t("crud.detail_title", name=router.model.__name__)
            content = Div(
                H1(detail_title, cls="text-3xl font-bold mb-6"),
                detail,
                actions,
                cls="container mx-auto p-4",
            )

            page_obj = router._wrap_page(content, detail_title)
            return HTMLResponse(content=_to_html_string_lazy(page_obj))

        app.add_api_route(
            f"{prefix}/{{id}}",
            detail_page,
            methods=["GET"],
            response_class=HTMLResponse,
        )

        # --- GET {prefix}/{id}/edit — Edit form page ---
        async def edit_page(request: Request, id: str) -> HTMLResponse:
            item = await router.storage.get(id)
            if item is None:
                content = Div(
                    Alert(t("crud.not_found"), variant="error"),
                    cls="container mx-auto p-4",
                )
                page_obj = router._wrap_page(content, t("crud.not_found_title"))
                return HTMLResponse(
                    content=_to_html_string_lazy(page_obj), status_code=404
                )

            form = router._build_form(
                action=f"{prefix}/{id}/edit",
                method="post",
                submit_text=t("crud.update"),
                submit_color="warning",
                exclude=router._get_form_exclude(),
                instance=item,
                hx_post=f"{prefix}/{id}/edit",
                hx_target="#form-container",
            )

            edit_title = t("crud.edit_title", name=router.model.__name__)
            content = Div(
                H1(edit_title, cls="text-3xl font-bold mb-6"),
                Div(form, id="form-container", cls="max-w-lg"),
                cls="container mx-auto p-4",
            )

            page_obj = router._wrap_page(content, edit_title)
            return HTMLResponse(content=_to_html_string_lazy(page_obj))

        # --- POST {prefix}/{id}/edit — Update handler ---
        async def edit_handler(request: Request, id: str) -> Response:
            item = await router.storage.get(id)
            if item is None:
                return HTMLResponse(content=t("crud.not_found"), status_code=404)

            form_data = await request.form()
            raw_data = dict(form_data)
            _process_bool_fields(router.model, raw_data, form_data, router._get_form_exclude())
            _process_list_fields(router.model, raw_data, form_data, router._get_form_exclude())
            await _process_media_fields(router.model, raw_data, form_data, router._get_form_exclude(), router.file_handler, existing_instance=item)

            # Preserve ID
            raw_data[router.id_field] = id

            try:
                instance = router.model.model_validate(raw_data)
                await router.storage.update(id, instance)
                redirect_url = router._toast_url(
                    f"{prefix}/{id}", t("crud.updated")
                )

                if request.headers.get("hx-request"):
                    return Response(
                        status_code=200,
                        headers={"HX-Redirect": redirect_url},
                    )
                return Response(
                    status_code=303,
                    headers={"Location": redirect_url},
                )
            except ValidationError as e:
                error_list = e.errors()
                form = router._build_form(
                    action=f"{prefix}/{id}/edit",
                    method="post",
                    submit_text=t("crud.update"),
                    submit_color="warning",
                    exclude=router._get_form_exclude(),
                    values=raw_data,
                    errors=error_list,
                    hx_post=f"{prefix}/{id}/edit",
                    hx_target="#form-container",
                )
                html = _to_html_string_lazy(form)

                if request.headers.get("hx-request"):
                    return HTMLResponse(content=html, status_code=422)

                edit_title = t("crud.edit_title", name=router.model.__name__)
                content = Div(
                    H1(edit_title, cls="text-3xl font-bold mb-6"),
                    Div(form, id="form-container", cls="max-w-lg"),
                    cls="container mx-auto p-4",
                )
                page_obj = router._wrap_page(content, edit_title)
                return HTMLResponse(
                    content=_to_html_string_lazy(page_obj), status_code=422
                )

        app.add_api_route(
            f"{prefix}/{{id}}/edit",
            edit_page,
            methods=["GET"],
            response_class=HTMLResponse,
        )
        app.add_api_route(
            f"{prefix}/{{id}}/edit",
            edit_handler,
            methods=["POST"],
        )

        # --- DELETE {prefix}/{id} — Delete handler ---
        async def delete_handler(request: Request, id: str) -> Response:
            deleted = await router.storage.delete(id)
            if not deleted:
                return HTMLResponse(content=t("crud.not_found"), status_code=404)

            redirect_url = router._toast_url(prefix, t("crud.deleted"))
            if request.headers.get("hx-request"):
                return Response(
                    status_code=200,
                    headers={"HX-Redirect": redirect_url},
                )
            return Response(
                status_code=303,
                headers={"Location": redirect_url},
            )

        app.add_api_route(
            f"{prefix}/{{id}}",
            delete_handler,
            methods=["DELETE"],
        )
