from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any

from nicegui import ui

from src._core.domain.protocols.admin_service_protocol import AdminCrudServiceProtocol
from src._core.domain.value_objects.query_filter import QueryFilter
from src._core.infrastructure.admin import components as c
from src._core.infrastructure.admin.audit import AdminAction, AuditResult
from src._core.infrastructure.admin.audit.logger import get_audit_logger
from src._core.infrastructure.admin.error_handler import AdminErrorHandler
from src._core.infrastructure.admin.theme import EMPTY_DISPLAY, AdminMetrics

if TYPE_CHECKING:
    from src._core.application.dtos.base_response import PaginationInfo


@dataclass
class ColumnConfig:
    """Column configuration for admin CRUD table."""

    field_name: str
    header_name: str
    sortable: bool = True
    searchable: bool = False
    hidden: bool = False
    masked: bool = False
    width: int | None = None


@dataclass
class BaseAdminPage:
    """Base admin page with Template Method rendering.

    Domains create an instance for standard CRUD pages.
    For custom rendering, subclass and override hook methods.
    """

    domain_name: str
    display_name: str
    icon: str = "list"
    columns: list[ColumnConfig] = field(default_factory=list)
    searchable_fields: list[str] = field(default_factory=list)
    sortable_fields: list[str] = field(default_factory=list)
    default_sort_field: str = "id"
    default_sort_order: str = "desc"
    page_size: int = 20
    readonly: bool = True
    # Per-domain opt-in for VIEW_LIST / VIEW_DETAIL audit events (#206 Phase 2).
    # Default off — read-event volume is high relative to forensic value and
    # should be a deliberate choice. Compliance-heavy domains can flip this.
    log_reads: bool = False
    # Declare extra services by alias → container attr name.
    # Bootstrap resolves each by attr name from the domain container and stores
    # the callable in ``_extra_services``. Use ``_get_extra_service(alias)`` to
    # retrieve them in page handlers.
    extra_services_config: dict[str, str] = field(default_factory=dict, repr=False)
    _service_provider: Callable[[], AdminCrudServiceProtocol] | None = field(
        default=None, repr=False
    )
    _extra_services: dict[str, Callable[[], Any]] = field(
        default_factory=dict, repr=False
    )

    # ── Config helpers ──

    def get_visible_columns(self) -> list[ColumnConfig]:
        return [c for c in self.columns if not c.hidden]

    def get_masked_field_names(self) -> set[str]:
        return {c.field_name for c in self.columns if c.masked}

    def _get_service(self) -> AdminCrudServiceProtocol:
        """Resolve service from the provider injected by bootstrap."""
        if self._service_provider is None:
            raise RuntimeError(
                f"service_provider not set for '{self.domain_name}' admin page. "
                "Was bootstrap_admin() called?"
            )
        return self._service_provider()

    def _get_extra_service(self, alias: str) -> Any:
        """Resolve an extra service by alias (declared in extra_services_config)."""
        provider = self._extra_services.get(alias)
        if provider is None:
            raise RuntimeError(
                f"Extra service '{alias}' not wired for '{self.domain_name}' admin page. "
                "Declare it in extra_services_config and ensure bootstrap ran."
            )
        return provider()

    # ── Template Methods ──

    async def render_list(self, page: int = 1, search: str = "") -> None:
        """Template method: render paginated list view.

        Override individual hook methods to customize rendering.
        """
        skeleton = self._render_list_skeleton()
        try:
            # Flush the skeleton to the client before the (slow) fetch so it is
            # actually visible during loading — not just built then replaced.
            await ui.context.client.connected()
            dtos, pagination = await self._fetch_list_data(page, search)
        except Exception as exc:  # noqa: BLE001 - delegated to AdminErrorHandler
            await AdminErrorHandler.handle(exc, context=f"{self.domain_name}_list")
            return
        finally:
            skeleton.delete()  # finally: also covers cancellation / disconnect

        if self.log_reads:
            await get_audit_logger().log(
                action=AdminAction.VIEW_LIST,
                domain=self.domain_name,
                result=AuditResult.SUCCESS,
            )

        self.render_list_header()
        self.render_search_bar(search)
        if not dtos:
            self.render_empty_state(search)
            return
        self.render_list_summary(pagination)
        self.render_grid(dtos)
        self.render_pagination(pagination, search)

    async def render_detail(self, record_id: int) -> None:
        """Template method: render single-record detail view.

        Override individual hook methods to customize rendering.
        """
        skeleton = self._render_detail_skeleton()
        try:
            # Flush the skeleton to the client before the (slow) fetch so it is
            # actually visible during loading — not just built then replaced.
            await ui.context.client.connected()
            dto = await self._fetch_detail_data(record_id)
        except Exception as exc:  # noqa: BLE001 - delegated to AdminErrorHandler
            await AdminErrorHandler.handle(exc, context=f"{self.domain_name}_detail")
            self._render_back_button()
            return
        finally:
            skeleton.delete()  # finally: also covers cancellation / disconnect

        if self.log_reads:
            await get_audit_logger().log(
                action=AdminAction.VIEW_DETAIL,
                domain=self.domain_name,
                result=AuditResult.SUCCESS,
                record_id=str(record_id),
            )

        self.render_detail_header(record_id)
        self.render_detail_card(dto)

    # ── Loading skeletons ──

    def _list_skeleton_rows(self) -> int:
        """Placeholder row count for the list skeleton (default 8; less if the
        page size is smaller)."""
        return min(self.page_size, 8)

    def _render_list_skeleton(self) -> ui.element:
        """Render a list-shaped skeleton (heading + search + summary + rows)
        shown while ``_fetch_list_data`` is pending. Caller deletes it."""
        with ui.column().classes("w-full q-gutter-sm") as container:
            ui.skeleton(type="text", width="45%", height="32px")
            ui.skeleton(type="rect", width="280px", height="40px")
            ui.skeleton(type="text", width="25%", height="16px")
            for _ in range(self._list_skeleton_rows()):
                ui.skeleton(type="rect", width="100%", height="44px")
        return container

    def _detail_skeleton_rows(self) -> int:
        """Placeholder field-row count for the detail skeleton (mirrors visible
        columns, minimum 4)."""
        return max(len(self.get_visible_columns()), 4)

    def _render_detail_skeleton(self) -> ui.element:
        """Render a detail-shaped skeleton (back-button header + field rows)
        shown while ``_fetch_detail_data`` is pending. Caller deletes it."""
        with ui.column().classes("w-full q-gutter-sm") as container:
            with ui.row().classes("items-center q-gutter-sm"):
                ui.skeleton(type="QBtn", width="36px", height="36px")
                ui.skeleton(type="text", width="200px", height="32px")
            with ui.card().classes("w-full"):
                for _ in range(self._detail_skeleton_rows()):
                    with ui.row().classes("items-center q-py-xs"):
                        ui.skeleton(type="text", width="160px", height="18px")
                        ui.skeleton(type="text", width="300px", height="18px")
        return container

    # ── Data fetching (overridable) ──

    async def _fetch_list_data(self, page: int, search: str):
        """Fetch paginated list data. Override for custom filtering/aggregation."""
        service = self._get_service()
        query_filter = QueryFilter(
            sort_field=self.default_sort_field,
            sort_order=self.default_sort_order,
            search_query=search or None,
            search_fields=self.searchable_fields if search else None,
        )
        return await service.get_datas(
            page=page, page_size=self.page_size, query_filter=query_filter
        )

    async def _fetch_detail_data(self, record_id: int):
        """Fetch single record. Override for custom data source."""
        service = self._get_service()
        return await service.get_data_by_data_id(data_id=record_id)

    # ── List page hooks ──

    def render_list_header(self) -> None:
        """Hook: render list page heading."""
        c.page_header(f"{self.display_name} Management")

    def render_search_bar(self, search: str) -> None:
        """Hook: render search input if searchable_fields configured."""
        if not self.searchable_fields:
            return

        field_labels = ", ".join(self.searchable_fields)

        def _on_search(e) -> None:
            query = e.value.strip() if e.value else ""
            params = f"?search={query}" if query else ""
            ui.navigate.to(f"/admin/{self.domain_name}{params}")

        c.text_field(
            "Search",
            value=search,
            placeholder=f"Search by {field_labels}...",
            clearable=True,
        ).on("keydown.enter", _on_search).classes("w-80 q-mb-sm")

    def render_empty_state(self, search: str = "") -> None:
        """Hook: render a friendly placeholder when the list has no records."""
        message = (
            f'No results for "{search}"'
            if search
            else f"No {self.display_name.lower()} records yet"
        )
        with c.empty_state():
            ui.label(message).classes("text-subtitle1")

    def render_list_summary(self, pagination: PaginationInfo) -> None:
        """Hook: render total/page summary above grid."""
        with ui.row().classes("items-center q-mb-sm"):
            ui.label(
                f"Total: {pagination.total_items} | "
                f"Page {pagination.current_page} / {pagination.total_pages}"
            ).classes("text-caption")

    def render_grid(self, dtos: list) -> None:
        """Hook: render AG Grid table (delegates to the data_grid builder)."""
        masked_fields = self.get_masked_field_names()
        c.data_grid(
            self.build_column_defs(),
            self.build_row_data(dtos, masked_fields),
            row_click_to=lambda row: f"/admin/{self.domain_name}/{row['id']}",
        )

    def render_pagination(self, pagination: PaginationInfo, search: str) -> None:
        """Hook: render prev/next pagination (delegates to the pagination builder)."""

        def _build_page_url(target_page: int) -> str:
            params = f"page={target_page}"
            if search:
                params += f"&search={search}"
            return f"/admin/{self.domain_name}?{params}"

        c.pagination(
            current=pagination.current_page,
            total_pages=pagination.total_pages,
            on_prev=lambda: ui.navigate.to(_build_page_url(pagination.previous_page)),
            on_next=lambda: ui.navigate.to(_build_page_url(pagination.next_page)),
        )

    # ── Detail page hooks ──

    def render_detail_header(self, record_id: int) -> None:
        """Hook: render detail page heading with back button."""
        c.page_header(
            f"{self.display_name} #{record_id}",
            back_to=f"/admin/{self.domain_name}",
        )

    def render_detail_card(self, dto) -> None:
        """Hook: render detail card with all fields."""
        masked_fields = self.get_masked_field_names()
        data = dto.model_dump()

        with c.card(classes="w-full"):
            for col in self.columns:
                value = data.get(col.field_name, "")
                is_empty = value is None or value == ""
                if col.field_name in masked_fields:
                    display_value = "****" if value else EMPTY_DISPLAY
                elif is_empty:
                    display_value = EMPTY_DISPLAY
                elif hasattr(value, "isoformat"):
                    display_value = value.isoformat()
                else:
                    display_value = str(value)

                c.field_row(col.header_name, display_value, is_empty=is_empty)

    # ── Data transformation helpers ──

    def build_column_defs(self) -> list[dict]:
        """Build AG Grid column definitions. Override to customize grid columns."""
        column_defs = []
        for col in self.get_visible_columns():
            col_def: dict = {
                "headerName": col.header_name,
                "field": col.field_name,
                "sortable": col.sortable,
            }
            if col.width:
                # Honor the explicit width exactly — no minWidth floor that would
                # silently widen a deliberately narrow column (e.g. an 80px ID).
                col_def["width"] = col.width
            else:
                # Width-less columns flex to fill, with a sane minimum.
                col_def["flex"] = 1
                col_def["minWidth"] = AdminMetrics.GRID_MIN_COL_WIDTH
            if col.masked:
                col_def["valueFormatter"] = "value ? '****' : ''"
            column_defs.append(col_def)
        return column_defs

    def build_row_data(self, dtos: list, masked_fields: set[str]) -> list[dict]:
        """Build row data from DTOs. Override to customize row transformation."""
        rows = []
        for dto in dtos:
            row = dto.model_dump()
            for key, value in row.items():
                if key in masked_fields:
                    row[key] = "****" if value else ""
                elif hasattr(value, "isoformat"):
                    row[key] = value.isoformat()
            rows.append(row)
        return rows

    # ── Private utilities ──

    def _render_back_button(self) -> None:
        """Render back-to-list button (used in error paths)."""
        ui.button(
            "Back to list",
            on_click=lambda: ui.navigate.to(f"/admin/{self.domain_name}"),
        ).props("flat")
