"""Layout registrations, page helpers, and shared hydration helpers.

All helpers return data dicts — no sx_call().
Markup composition lives entirely in .sx defpage and .sx defcomp files.
"""
from __future__ import annotations

from typing import Any

from shared.sx.parser import SxExpr


# ---------------------------------------------------------------------------
# Shared hydration helpers
# ---------------------------------------------------------------------------

def _add_to_defpage_ctx(**kwargs: Any) -> None:
    """Add data to g._defpage_ctx for the app-level context_processor."""
    from quart import g
    if not hasattr(g, '_defpage_ctx'):
        g._defpage_ctx = {}
    g._defpage_ctx.update(kwargs)


def _ensure_post_defpage_ctx() -> None:
    """Copy g.post_data["post"] into g._defpage_ctx for layout IO primitives."""
    from quart import g
    post_data = getattr(g, "post_data", None)
    if post_data and post_data.get("post"):
        _add_to_defpage_ctx(post=post_data["post"])


async def _ensure_container_nav_defpage_ctx() -> None:
    """Fetch container_nav and add to g._defpage_ctx for layout IO primitives."""
    from quart import g
    dctx = getattr(g, "_defpage_ctx", None) or {}
    if dctx.get("container_nav"):
        return
    post = dctx.get("post") or {}
    post_id = post.get("id")
    slug = post.get("slug", "")
    if not post_id:
        return
    from shared.infrastructure.fragments import fetch_fragments
    current_cal = getattr(g, "calendar_slug", "") or ""
    nav_params = {
        "container_type": "page",
        "container_id": str(post_id),
        "post_slug": slug,
        "current_calendar": current_cal,
    }
    events_nav, market_nav = await fetch_fragments([
        ("events", "container-nav", nav_params),
        ("market", "container-nav", nav_params),
    ], required=False)
    _add_to_defpage_ctx(container_nav=events_nav + market_nav)


async def _ensure_calendar(calendar_slug: str | None) -> None:
    """Load calendar into g.calendar if not already present."""
    from quart import g, abort
    if hasattr(g, 'calendar'):
        _add_to_defpage_ctx(calendar=g.calendar)
        return
    from bp.calendar.services.calendar_view import (
        get_calendar_by_post_and_slug, get_calendar_by_slug,
    )
    post_data = getattr(g, "post_data", None)
    if post_data:
        post_id = (post_data.get("post") or {}).get("id")
        cal = await get_calendar_by_post_and_slug(g.s, post_id, calendar_slug)
    else:
        cal = await get_calendar_by_slug(g.s, calendar_slug)
    if not cal:
        abort(404)
    g.calendar = cal
    g.calendar_slug = calendar_slug
    _add_to_defpage_ctx(calendar=cal)
    _ensure_post_defpage_ctx()


async def _ensure_entry(entry_id: int | None) -> None:
    """Load calendar entry into g.entry if not already present."""
    from quart import g, abort
    if hasattr(g, 'entry'):
        _add_to_defpage_ctx(entry=g.entry)
        return
    from sqlalchemy import select
    from models.calendars import CalendarEntry
    result = await g.s.execute(
        select(CalendarEntry).where(
            CalendarEntry.id == entry_id,
            CalendarEntry.deleted_at.is_(None),
        )
    )
    entry = result.scalar_one_or_none()
    if entry is None:
        abort(404)
    g.entry = entry
    _add_to_defpage_ctx(entry=entry)


async def _ensure_entry_context(entry_id: int | None) -> None:
    """Load full entry context (ticket data, posts) into g.* and _defpage_ctx."""
    from quart import g
    from sqlalchemy import select
    from sqlalchemy.orm import selectinload
    from models.calendars import CalendarEntry
    from bp.tickets.services.tickets import (
        get_available_ticket_count,
        get_sold_ticket_count,
        get_user_reserved_count,
    )
    from shared.infrastructure.cart_identity import current_cart_identity
    from bp.calendar_entry.services.post_associations import get_entry_posts

    await _ensure_entry(entry_id)

    # Reload with ticket_types eagerly loaded
    stmt = (
        select(CalendarEntry)
        .where(CalendarEntry.id == entry_id, CalendarEntry.deleted_at.is_(None))
        .options(selectinload(CalendarEntry.ticket_types))
    )
    result = await g.s.execute(stmt)
    calendar_entry = result.scalar_one_or_none()

    if calendar_entry and getattr(g, "calendar", None):
        if calendar_entry.calendar_id != g.calendar.id:
            calendar_entry = None

    if calendar_entry:
        await g.s.refresh(calendar_entry, ['slot'])
        g.entry = calendar_entry
        entry_posts = await get_entry_posts(g.s, calendar_entry.id)
        ticket_remaining = await get_available_ticket_count(g.s, calendar_entry.id)
        ticket_sold_count = await get_sold_ticket_count(g.s, calendar_entry.id)
        ident = current_cart_identity()
        user_ticket_count = await get_user_reserved_count(
            g.s, calendar_entry.id,
            user_id=ident["user_id"],
            session_id=ident["session_id"],
        )
        user_ticket_counts_by_type = {}
        if calendar_entry.ticket_types:
            for tt in calendar_entry.ticket_types:
                if tt.deleted_at is None:
                    user_ticket_counts_by_type[tt.id] = await get_user_reserved_count(
                        g.s, calendar_entry.id,
                        user_id=ident["user_id"],
                        session_id=ident["session_id"],
                        ticket_type_id=tt.id,
                    )
        _add_to_defpage_ctx(
            entry=calendar_entry,
            entry_posts=entry_posts,
            ticket_remaining=ticket_remaining,
            ticket_sold_count=ticket_sold_count,
            user_ticket_count=user_ticket_count,
            user_ticket_counts_by_type=user_ticket_counts_by_type,
        )


async def _ensure_day_data(year: int, month: int, day: int) -> None:
    """Load day-specific data for layout header functions."""
    from quart import g, session as qsession
    if hasattr(g, 'day_date'):
        return
    from datetime import date as date_cls, datetime, timezone, timedelta
    from sqlalchemy import select
    from bp.calendar.services import get_visible_entries_for_period
    from models.calendars import CalendarSlot

    calendar = getattr(g, "calendar", None)
    if not calendar:
        return

    try:
        day_date = date_cls(year, month, day)
    except (ValueError, TypeError):
        return

    period_start = datetime(year, month, day, tzinfo=timezone.utc)
    period_end = period_start + timedelta(days=1)

    user = getattr(g, "user", None)
    session_id = qsession.get("calendar_sid")

    visible = await get_visible_entries_for_period(
        sess=g.s,
        calendar_id=calendar.id,
        period_start=period_start,
        period_end=period_end,
        user=user,
        session_id=session_id,
    )

    weekday_attr = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"][day_date.weekday()]
    stmt = (
        select(CalendarSlot)
        .where(
            CalendarSlot.calendar_id == calendar.id,
            getattr(CalendarSlot, weekday_attr) == True,  # noqa: E712
            CalendarSlot.deleted_at.is_(None),
        )
        .order_by(CalendarSlot.time_start.asc(), CalendarSlot.id.asc())
    )
    result = await g.s.execute(stmt)
    day_slots = list(result.scalars())

    g.day_date = day_date
    _add_to_defpage_ctx(
        qsession=qsession,
        day_date=day_date,
        day=day,
        year=year,
        month=month,
        day_entries=visible.merged_entries,
        user_entries=visible.user_entries,
        confirmed_entries=visible.confirmed_entries,
        day_slots=day_slots,
    )


# ---------------------------------------------------------------------------
# Layouts — all layouts delegate to .sx defcomps via register_sx_layout
# ---------------------------------------------------------------------------

def _register_events_layouts() -> None:
    from shared.sx.layouts import register_sx_layout
    register_sx_layout("events-calendar-admin",
                       "events-cal-admin-layout-full", "events-cal-admin-layout-oob")
    register_sx_layout("events-slots",
                       "events-slots-layout-full", "events-slots-layout-oob")
    register_sx_layout("events-slot",
                       "events-slot-layout-full", "events-slot-layout-oob")
    register_sx_layout("events-day-admin",
                       "events-day-admin-layout-full", "events-day-admin-layout-oob")
    register_sx_layout("events-entry",
                       "events-entry-layout-full", "events-entry-layout-oob")
    register_sx_layout("events-entry-admin",
                       "events-entry-admin-layout-full", "events-entry-admin-layout-oob")
    register_sx_layout("events-ticket-types",
                       "events-ticket-types-layout-full", "events-ticket-types-layout-oob")
    register_sx_layout("events-ticket-type",
                       "events-ticket-type-layout-full", "events-ticket-type-layout-oob")
    register_sx_layout("events-markets",
                       "events-markets-layout-full", "events-markets-layout-oob")


# ---------------------------------------------------------------------------
# Badge data helpers
# ---------------------------------------------------------------------------

_ENTRY_STATE_CLASSES = {
    "confirmed": "bg-emerald-100 text-emerald-800",
    "provisional": "bg-amber-100 text-amber-800",
    "ordered": "bg-sky-100 text-sky-800",
    "pending": "bg-stone-100 text-stone-700",
    "declined": "bg-red-100 text-red-800",
}

_TICKET_STATE_CLASSES = {
    "confirmed": "bg-emerald-100 text-emerald-800",
    "checked_in": "bg-blue-100 text-blue-800",
    "reserved": "bg-amber-100 text-amber-800",
    "cancelled": "bg-red-100 text-red-800",
}


def _entry_badge_data(state: str) -> dict:
    cls = _ENTRY_STATE_CLASSES.get(state, "bg-stone-100 text-stone-700")
    label = state.replace("_", " ").capitalize()
    return {"cls": cls, "label": label}


def _ticket_badge_data(state: str) -> dict:
    cls = _TICKET_STATE_CLASSES.get(state, "bg-stone-100 text-stone-700")
    label = (state or "").replace("_", " ").capitalize()
    return {"cls": cls, "label": label}


# ---------------------------------------------------------------------------
# Styles helper
# ---------------------------------------------------------------------------

def _styles_data() -> dict:
    """Extract common style classes from g.styles."""
    from quart import g
    styles = getattr(g, "styles", None) or {}

    def _gs(attr):
        return getattr(styles, attr, "") if hasattr(styles, attr) else styles.get(attr, "")

    return {
        "list-container": _gs("list_container"),
        "pre-action": _gs("pre_action_button"),
        "action-btn": _gs("action_button"),
        "tr-cls": _gs("tr"),
        "pill-cls": _gs("pill"),
        "nav-btn": _gs("nav_button"),
    }


# ---------------------------------------------------------------------------
# Page helpers
# ---------------------------------------------------------------------------

def _register_events_helpers() -> None:
    from shared.sx.pages import register_page_helpers

    register_page_helpers("events", {
        "calendar-admin-data": _h_calendar_admin_data,
        "day-admin-data": _h_day_admin_data,
        "slots-data": _h_slots_data,
        "slot-data": _h_slot_data,
        "entry-data": _h_entry_data,
        "entry-admin-data": _h_entry_admin_data,
        "ticket-types-data": _h_ticket_types_data,
        "ticket-type-data": _h_ticket_type_data,
        "tickets-data": _h_tickets_data,
        "ticket-detail-data": _h_ticket_detail_data,
        "ticket-admin-data": _h_ticket_admin_data,
        "markets-data": _h_markets_data,
    })


# ---------------------------------------------------------------------------
# Calendar admin
# ---------------------------------------------------------------------------

async def _h_calendar_admin_data(calendar_slug=None, **kw) -> dict:
    from quart import url_for
    from shared.browser.app.csrf import generate_csrf_token

    await _ensure_calendar(calendar_slug)
    await _ensure_container_nav_defpage_ctx()

    from quart import g
    calendar = getattr(g, "calendar", None)
    if not calendar:
        return {}

    csrf = generate_csrf_token()
    cal_slug = getattr(calendar, "slug", "")
    desc = getattr(calendar, "description", "") or ""
    desc_edit_url = url_for("calendar.admin.calendar_description_edit",
                            calendar_slug=cal_slug)

    return {
        "cal-description": desc,
        "csrf": csrf,
        "desc-edit-url": desc_edit_url,
    }


# ---------------------------------------------------------------------------
# Day admin
# ---------------------------------------------------------------------------

async def _h_day_admin_data(calendar_slug=None, year=None, month=None,
                            day=None, **kw) -> dict:
    await _ensure_calendar(calendar_slug)
    await _ensure_container_nav_defpage_ctx()
    if year is not None:
        await _ensure_day_data(int(year), int(month), int(day))
    return {}


# ---------------------------------------------------------------------------
# Slots listing
# ---------------------------------------------------------------------------

async def _h_slots_data(calendar_slug=None, **kw) -> dict:
    from quart import g, url_for
    from shared.browser.app.csrf import generate_csrf_token
    from bp.slots.services.slots import list_slots as svc_list_slots

    await _ensure_calendar(calendar_slug)
    await _ensure_container_nav_defpage_ctx()

    calendar = getattr(g, "calendar", None)
    slots = await svc_list_slots(g.s, calendar.id) if calendar else []
    _add_to_defpage_ctx(slots=slots)

    styles = _styles_data()
    csrf = generate_csrf_token()
    cal_slug = getattr(calendar, "slug", "")
    hx_select = getattr(g, "hx_select_search", "#main-panel")
    csrf_hdr = {"X-CSRFToken": csrf}
    add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug)

    slots_list = []
    for s in slots:
        slot_href = url_for("defpage_slot_detail",
                            calendar_slug=cal_slug, slot_id=s.id)
        del_url = url_for("calendar.slots.slot.slot_delete",
                          calendar_slug=cal_slug, slot_id=s.id)
        desc = getattr(s, "description", "") or ""
        days_display = getattr(s, "days_display", "\u2014")
        day_list = days_display.split(", ")
        has_days = bool(day_list and day_list[0] != "\u2014")
        time_start = s.time_start.strftime("%H:%M") if s.time_start else ""
        time_end = s.time_end.strftime("%H:%M") if s.time_end else ""
        cost = getattr(s, "cost", None)
        cost_str = f"{cost:.2f}" if cost is not None else ""

        slots_list.append({
            "name": s.name,
            "description": desc,
            "day-list": day_list if has_days else [],
            "has-days": has_days,
            "flexible": "yes" if s.flexible else "no",
            "time-str": f"{time_start} - {time_end}",
            "cost-str": cost_str,
            "slot-href": slot_href,
            "del-url": del_url,
        })

    return {
        "has-slots": bool(slots),
        "slots-list": slots_list,
        "add-url": add_url,
        "csrf-hdr": csrf_hdr,
        "hx-select": hx_select,
        **styles,
    }


# ---------------------------------------------------------------------------
# Slot detail
# ---------------------------------------------------------------------------

async def _h_slot_data(calendar_slug=None, slot_id=None, **kw) -> dict:
    from quart import g, abort, url_for

    await _ensure_calendar(calendar_slug)
    await _ensure_container_nav_defpage_ctx()

    from bp.slot.services.slot import get_slot as svc_get_slot
    slot = await svc_get_slot(g.s, slot_id) if slot_id else None
    if not slot:
        abort(404)
    g.slot = slot
    _add_to_defpage_ctx(slot=slot)

    calendar = getattr(g, "calendar", None)
    styles = _styles_data()
    cal_slug = getattr(calendar, "slug", "")

    days_display = getattr(slot, "days_display", "\u2014")
    day_list = days_display.split(", ")
    has_days = bool(day_list and day_list[0] != "\u2014")
    time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
    time_end = slot.time_end.strftime("%H:%M") if slot.time_end else ""
    cost = getattr(slot, "cost", None)
    cost_str = f"{cost:.2f}" if cost is not None else ""
    edit_url = url_for("calendar.slots.slot.get_edit",
                       slot_id=slot.id, calendar_slug=cal_slug)

    return {
        "slot-id-str": str(slot.id),
        "day-list": day_list if has_days else [],
        "has-days": has_days,
        "flexible": "yes" if getattr(slot, "flexible", False) else "no",
        "time-str": f"{time_start} \u2014 {time_end}",
        "cost-str": cost_str,
        "edit-url": edit_url,
        **styles,
    }


# ---------------------------------------------------------------------------
# Entry detail (complex — sub-panels returned as SxExpr)
# ---------------------------------------------------------------------------

async def _h_entry_data(calendar_slug=None, entry_id=None, **kw) -> dict:
    from quart import url_for, g
    from .entries import (
        _entry_nav_html,
        _entry_options_html,
        render_entry_tickets_config,
        render_entry_posts_panel,
    )
    from .tickets import render_buy_form

    await _ensure_calendar(calendar_slug)
    await _ensure_entry_context(entry_id)

    from shared.sx.page import get_template_context
    ctx = await get_template_context()

    entry = ctx.get("entry")
    if not entry:
        return {}

    calendar = ctx.get("calendar")
    cal_slug = getattr(calendar, "slug", "") if calendar else ""
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")

    styles = _styles_data()
    eid = entry.id
    state = getattr(entry, "state", "pending") or "pending"

    # Simple field data
    slot = getattr(entry, "slot", None)
    has_slot = slot is not None
    slot_name = slot.name if slot else ""
    flex_label = "(flexible)" if slot and getattr(slot, "flexible", False) else "(fixed)"
    start_str = entry.start_at.strftime("%H:%M") if entry.start_at else ""
    end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else " \u2013 open-ended"
    cost = getattr(entry, "cost", None)
    cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
    date_str = entry.start_at.strftime("%A, %B %d, %Y") if entry.start_at else ""
    badge = _entry_badge_data(state)

    edit_url = url_for(
        "calendar.day.calendar_entries.calendar_entry.get_edit",
        entry_id=eid, calendar_slug=cal_slug,
        day=day, month=month, year=year,
    )

    # Complex sub-panels (pre-composed as SxExpr)
    ticket_remaining = ctx.get("ticket_remaining")
    ticket_sold_count = ctx.get("ticket_sold_count", 0)
    user_ticket_count = ctx.get("user_ticket_count", 0)
    user_ticket_counts_by_type = ctx.get("user_ticket_counts_by_type") or {}
    entry_posts = ctx.get("entry_posts") or []

    tickets_config = render_entry_tickets_config(entry, calendar, day, month, year)
    buy_form = render_buy_form(
        entry, ticket_remaining, ticket_sold_count,
        user_ticket_count, user_ticket_counts_by_type,
    )
    posts_panel = render_entry_posts_panel(
        entry_posts, entry, calendar, day, month, year,
    )
    options_html = _entry_options_html(entry, calendar, day, month, year)

    # Entry menu (pre-composed for :menu slot)
    entry_menu = _entry_nav_html(ctx)

    return {
        "entry-id-str": str(eid),
        "entry-name": entry.name,
        "has-slot": has_slot,
        "slot-name": slot_name,
        "flex-label": flex_label,
        "time-str": start_str + end_str,
        "state-badge-cls": badge["cls"],
        "state-badge-label": badge["label"],
        "cost-str": cost_str,
        "date-str": date_str,
        "edit-url": edit_url,
        "tickets-config": SxExpr(tickets_config),
        "buy-form": SxExpr(buy_form) if buy_form else None,
        "posts-panel": SxExpr(posts_panel),
        "options-html": SxExpr(options_html),
        "entry-menu": SxExpr(entry_menu) if entry_menu else None,
        **styles,
    }


# ---------------------------------------------------------------------------
# Entry admin
# ---------------------------------------------------------------------------

async def _h_entry_admin_data(calendar_slug=None, entry_id=None,
                              year=None, month=None, day=None, **kw) -> dict:
    from quart import url_for, g

    await _ensure_calendar(calendar_slug)
    await _ensure_container_nav_defpage_ctx()
    await _ensure_entry_context(entry_id)

    calendar = getattr(g, "calendar", None)
    entry = getattr(g, "entry", None)
    if not calendar or not entry:
        return {}

    cal_slug = getattr(calendar, "slug", "")
    styles = _styles_data()

    from shared.sx.page import get_template_context
    ctx = await get_template_context()
    select_colours = ctx.get("select_colours", "")

    ticket_types_href = url_for(
        "calendar.day.calendar_entries.calendar_entry.ticket_types.get",
        calendar_slug=cal_slug, entry_id=entry.id,
        year=year, month=month, day=day,
    )

    return {
        "ticket-types-href": ticket_types_href,
        "select-colours": select_colours,
        **styles,
    }


# ---------------------------------------------------------------------------
# Ticket types listing
# ---------------------------------------------------------------------------

async def _h_ticket_types_data(calendar_slug=None, entry_id=None,
                                year=None, month=None, day=None, **kw) -> dict:
    from quart import g, url_for
    from shared.browser.app.csrf import generate_csrf_token

    await _ensure_calendar(calendar_slug)
    await _ensure_entry(entry_id)

    entry = getattr(g, "entry", None)
    calendar = getattr(g, "calendar", None)

    from bp.ticket_types.services.tickets import list_ticket_types as svc_list_ticket_types
    ticket_types = await svc_list_ticket_types(g.s, entry.id) if entry else []
    _add_to_defpage_ctx(ticket_types=ticket_types)

    styles = _styles_data()
    csrf = generate_csrf_token()
    cal_slug = getattr(calendar, "slug", "")
    hx_select = getattr(g, "hx_select_search", "#main-panel")
    eid = entry.id if entry else 0
    csrf_hdr = {"X-CSRFToken": csrf}

    types_list = []
    for tt in (ticket_types or []):
        tt_href = url_for(
            "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get",
            calendar_slug=cal_slug, year=year, month=month, day=day,
            entry_id=eid, ticket_type_id=tt.id,
        )
        del_url = url_for(
            "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete",
            calendar_slug=cal_slug, year=year, month=month, day=day,
            entry_id=eid, ticket_type_id=tt.id,
        )
        cost = getattr(tt, "cost", None)
        cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"

        types_list.append({
            "tt-href": tt_href,
            "tt-name": tt.name,
            "cost-str": cost_str,
            "count": str(tt.count),
            "del-url": del_url,
        })

    add_url = url_for(
        "calendar.day.calendar_entries.calendar_entry.ticket_types.add_form",
        calendar_slug=cal_slug, entry_id=eid, year=year, month=month, day=day,
    )

    return {
        "has-types": bool(ticket_types),
        "types-list": types_list,
        "add-url": add_url,
        "csrf-hdr": csrf_hdr,
        "hx-select": hx_select,
        **styles,
    }


# ---------------------------------------------------------------------------
# Ticket type detail
# ---------------------------------------------------------------------------

async def _h_ticket_type_data(calendar_slug=None, entry_id=None,
                               ticket_type_id=None,
                               year=None, month=None, day=None, **kw) -> dict:
    from quart import g, abort, url_for

    await _ensure_calendar(calendar_slug)
    await _ensure_entry(entry_id)

    from bp.ticket_type.services.ticket import get_ticket_type as svc_get_ticket_type
    ticket_type = await svc_get_ticket_type(g.s, ticket_type_id) if ticket_type_id else None
    if not ticket_type:
        abort(404)
    g.ticket_type = ticket_type
    _add_to_defpage_ctx(ticket_type=ticket_type)

    entry = getattr(g, "entry", None)
    calendar = getattr(g, "calendar", None)
    styles = _styles_data()
    cal_slug = getattr(calendar, "slug", "")
    cost = getattr(ticket_type, "cost", None)
    cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
    count = getattr(ticket_type, "count", 0)

    edit_url = url_for(
        "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_edit",
        ticket_type_id=ticket_type.id, calendar_slug=cal_slug,
        year=year, month=month, day=day,
        entry_id=entry.id if entry else 0,
    )

    return {
        "ticket-id": str(ticket_type.id),
        "tt-name": ticket_type.name,
        "cost-str": cost_str,
        "count-str": str(count),
        "edit-url": edit_url,
        **styles,
    }


# ---------------------------------------------------------------------------
# My tickets
# ---------------------------------------------------------------------------

async def _h_tickets_data(**kw) -> dict:
    from quart import g, url_for
    from shared.infrastructure.cart_identity import current_cart_identity
    from bp.tickets.services.tickets import get_user_tickets

    ident = current_cart_identity()
    tickets = await get_user_tickets(
        g.s,
        user_id=ident["user_id"],
        session_id=ident["session_id"],
    )

    from shared.sx.page import get_template_context
    ctx = await get_template_context()
    styles = ctx.get("styles") or {}
    list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")

    tickets_list = []
    for ticket in (tickets or []):
        href = url_for("defpage_ticket_detail", code=ticket.code)
        entry = getattr(ticket, "entry", None)
        entry_name = entry.name if entry else "Unknown event"
        tt = getattr(ticket, "ticket_type", None)
        state = getattr(ticket, "state", "")
        cal = getattr(entry, "calendar", None) if entry else None

        time_str = ""
        if entry and entry.start_at:
            time_str = entry.start_at.strftime("%A, %B %d, %Y at %H:%M")
            if entry.end_at:
                time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}"

        badge = _ticket_badge_data(state)
        tickets_list.append({
            "href": href,
            "entry-name": entry_name,
            "type-name": tt.name if tt else None,
            "time-str": time_str or None,
            "cal-name": cal.name if cal else None,
            "badge-cls": badge["cls"],
            "badge-label": badge["label"],
            "code-prefix": ticket.code[:8],
        })

    return {
        "has-tickets": bool(tickets),
        "tickets-list": tickets_list,
        "list-container": list_container,
    }


# ---------------------------------------------------------------------------
# Ticket detail
# ---------------------------------------------------------------------------

async def _h_ticket_detail_data(code=None, **kw) -> dict:
    from quart import g, abort, url_for
    from shared.infrastructure.cart_identity import current_cart_identity
    from bp.tickets.services.tickets import get_ticket_by_code

    ticket = await get_ticket_by_code(g.s, code) if code else None
    if not ticket:
        abort(404)
    # Verify ownership
    ident = current_cart_identity()
    if ident["user_id"] is not None:
        if ticket.user_id != ident["user_id"]:
            abort(404)
    elif ident["session_id"] is not None:
        if ticket.session_id != ident["session_id"]:
            abort(404)
    else:
        abort(404)

    from shared.sx.page import get_template_context
    ctx = await get_template_context()
    styles = ctx.get("styles") or {}
    list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")

    entry = getattr(ticket, "entry", None)
    tt = getattr(ticket, "ticket_type", None)
    state = getattr(ticket, "state", "")
    ticket_code = ticket.code
    cal = getattr(entry, "calendar", None) if entry else None
    checked_in_at = getattr(ticket, "checked_in_at", None)

    bg_map = {"confirmed": "bg-emerald-50", "checked_in": "bg-blue-50",
              "reserved": "bg-amber-50"}
    header_bg = bg_map.get(state, "bg-stone-50")
    entry_name = entry.name if entry else "Ticket"
    back_href = url_for("defpage_my_tickets")

    badge = _ticket_badge_data(state)

    time_date = entry.start_at.strftime("%A, %B %d, %Y") if entry and entry.start_at else None
    time_range = entry.start_at.strftime("%H:%M") if entry and entry.start_at else None
    if time_range and entry.end_at:
        time_range += f" \u2013 {entry.end_at.strftime('%H:%M')}"

    tt_desc = f"{tt.name} \u2014 \u00a3{tt.cost:.2f}" if tt and getattr(tt, "cost", None) else None
    checkin_str = checked_in_at.strftime("Checked in: %B %d, %Y at %H:%M") if checked_in_at else None

    qr_script = (
        f"(function(){{var c=document.getElementById('ticket-qr-{ticket_code}');"
        "if(c&&typeof QRCode!=='undefined'){"
        "var cv=document.createElement('canvas');"
        f"QRCode.toCanvas(cv,'{ticket_code}',{{width:200,margin:2,color:{{dark:'#1c1917',light:'#ffffff'}}}},function(e){{if(!e)c.appendChild(cv)}});"
        "}})()"
    )

    return {
        "list-container": list_container,
        "back-href": back_href,
        "header-bg": header_bg,
        "entry-name": entry_name,
        "badge-cls": badge["cls"],
        "badge-label": badge["label"],
        "type-name": tt.name if tt else None,
        "ticket-code": ticket_code,
        "time-date": time_date,
        "time-range": time_range,
        "cal-name": cal.name if cal else None,
        "type-desc": tt_desc,
        "checkin-str": checkin_str,
        "qr-script": qr_script,
    }


# ---------------------------------------------------------------------------
# Ticket admin dashboard
# ---------------------------------------------------------------------------

async def _h_ticket_admin_data(**kw) -> dict:
    from quart import g, url_for
    from sqlalchemy import select, func
    from sqlalchemy.orm import selectinload
    from models.calendars import CalendarEntry, Ticket
    from shared.browser.app.csrf import generate_csrf_token

    result = await g.s.execute(
        select(Ticket)
        .options(
            selectinload(Ticket.entry).selectinload(CalendarEntry.calendar),
            selectinload(Ticket.ticket_type),
        )
        .order_by(Ticket.created_at.desc())
        .limit(50)
    )
    tickets = result.scalars().all()

    total = await g.s.scalar(select(func.count(Ticket.id)))
    confirmed = await g.s.scalar(
        select(func.count(Ticket.id)).where(Ticket.state == "confirmed")
    )
    checked_in = await g.s.scalar(
        select(func.count(Ticket.id)).where(Ticket.state == "checked_in")
    )
    reserved = await g.s.scalar(
        select(func.count(Ticket.id)).where(Ticket.state == "reserved")
    )

    csrf = generate_csrf_token()
    lookup_url = url_for("ticket_admin.lookup")

    from shared.sx.page import get_template_context
    ctx = await get_template_context()
    styles = ctx.get("styles") or {}
    list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")

    # Stats cards data
    admin_stats = []
    for label, key, border, bg, text_cls in [
        ("Total", "total", "border-stone-200", "", "text-stone-900"),
        ("Confirmed", "confirmed", "border-emerald-200", "bg-emerald-50", "text-emerald-700"),
        ("Checked In", "checked_in", "border-blue-200", "bg-blue-50", "text-blue-700"),
        ("Reserved", "reserved", "border-amber-200", "bg-amber-50", "text-amber-700"),
    ]:
        val_map = {"total": total, "confirmed": confirmed,
                   "checked_in": checked_in, "reserved": reserved}
        val = val_map.get(key, 0) or 0
        lbl_cls = text_cls.replace("700", "600").replace("900", "500") if "stone" not in text_cls else "text-stone-500"
        admin_stats.append({
            "border": border, "bg": bg, "text-cls": text_cls,
            "label-cls": lbl_cls, "value": str(val), "label": label,
        })

    # Ticket rows data
    admin_tickets = []
    for ticket in tickets:
        entry = getattr(ticket, "entry", None)
        tt = getattr(ticket, "ticket_type", None)
        state = getattr(ticket, "state", "")
        tcode = ticket.code
        checked_in_at = getattr(ticket, "checked_in_at", None)

        date_str = None
        if entry and entry.start_at:
            date_str = entry.start_at.strftime("%d %b %Y, %H:%M")

        badge = _ticket_badge_data(state)
        can_checkin = state in ("confirmed", "reserved")
        is_checked_in = state == "checked_in"
        checkin_url = url_for("ticket_admin.do_checkin", code=tcode) if can_checkin else None
        checkin_time = checked_in_at.strftime("%H:%M") if checked_in_at else ""

        admin_tickets.append({
            "code": tcode,
            "code-short": tcode[:12] + "...",
            "entry-name": entry.name if entry else "\u2014",
            "date-str": date_str,
            "type-name": tt.name if tt else "\u2014",
            "badge-cls": badge["cls"],
            "badge-label": badge["label"],
            "can-checkin": can_checkin,
            "is-checked-in": is_checked_in,
            "checkin-url": checkin_url,
            "checkin-time": checkin_time,
        })

    return {
        "admin-stats": admin_stats,
        "admin-tickets": admin_tickets,
        "list-container": list_container,
        "lookup-url": lookup_url,
        "csrf": csrf,
        "has-tickets": bool(tickets),
    }


# ---------------------------------------------------------------------------
# Markets
# ---------------------------------------------------------------------------

async def _h_markets_data(**kw) -> dict:
    from quart import url_for
    from shared.browser.app.csrf import generate_csrf_token
    from shared.sx.helpers import call_url

    _ensure_post_defpage_ctx()

    from shared.sx.page import get_template_context
    ctx = await get_template_context()

    rights = ctx.get("rights") or {}
    is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
    has_access = ctx.get("has_access")
    can_create = has_access("markets.create_market") if callable(has_access) else is_admin
    csrf = generate_csrf_token()
    markets_raw = ctx.get("markets") or []

    post = ctx.get("post") or {}
    slug = post.get("slug", "")
    csrf_hdr = {"X-CSRFToken": csrf}

    markets_list = []
    for m in markets_raw:
        m_slug = getattr(m, "slug", "") if hasattr(m, "slug") else m.get("slug", "")
        m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "")
        market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/")
        del_url = url_for("markets.delete_market", market_slug=m_slug)

        markets_list.append({
            "href": market_href,
            "name": m_name,
            "slug": m_slug,
            "del-url": del_url,
            "csrf-hdr": csrf_hdr,
        })

    return {
        "can-create": can_create,
        "create-url": url_for("markets.create_market") if can_create else None,
        "csrf": csrf,
        "markets-list": markets_list,
    }
