"""Entry panels, cards, forms, edit/add."""
from __future__ import annotations

from markupsafe import escape

from shared.sx.helpers import sx_call
from shared.sx.parser import SxExpr

from .utils import (
    _entry_state_badge_html, _ticket_state_badge_html,
    _list_container, _view_toggle_html,
)


# ---------------------------------------------------------------------------
# All events / page summary entry cards
# ---------------------------------------------------------------------------

def _entry_card_html(entry, page_info: dict, pending_tickets: dict,
                     ticket_url: str, events_url_fn, *, is_page_scoped: bool = False,
                     post: dict | None = None) -> str:
    """Render a list card for one event entry."""
    from .tickets import _ticket_widget_html
    pi = page_info.get(getattr(entry, "calendar_container_id", 0), {})
    if is_page_scoped and post:
        page_slug = pi.get("slug", post.get("slug", ""))
    else:
        page_slug = pi.get("slug", "")
    page_title = pi.get("title")

    day_href = ""
    if page_slug and entry.start_at:
        day_href = events_url_fn(f"/{page_slug}/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/")
    entry_href = f"{day_href}entries/{entry.id}/" if day_href else ""

    # Title (linked or plain)
    if entry_href:
        title_html = sx_call("events-entry-title-linked",
                            href=entry_href, name=entry.name)
    else:
        title_html = sx_call("events-entry-title-plain", name=entry.name)

    # Badges
    badges_html = ""
    if page_title and (not is_page_scoped or page_title != (post or {}).get("title")):
        page_href = events_url_fn(f"/{page_slug}/")
        badges_html += sx_call("events-entry-page-badge",
                              href=page_href, title=page_title)
    cal_name = getattr(entry, "calendar_name", "")
    if cal_name:
        badges_html += sx_call("events-entry-cal-badge", name=cal_name)

    # Time line
    time_parts = ""
    if day_href and not is_page_scoped:
        time_parts += sx_call("events-entry-time-linked",
                             href=day_href,
                             date_str=entry.start_at.strftime("%a %-d %b"))
    elif not is_page_scoped:
        time_parts += sx_call("events-entry-time-plain",
                             date_str=entry.start_at.strftime("%a %-d %b"))
    time_parts += entry.start_at.strftime("%H:%M")
    if entry.end_at:
        time_parts += f' \u2013 {entry.end_at.strftime("%H:%M")}'

    cost = getattr(entry, "cost", None)
    cost_html = sx_call("events-entry-cost",
                       cost=f"\u00a3{cost:.2f}") if cost else ""

    # Ticket widget
    tp = getattr(entry, "ticket_price", None)
    widget_html = ""
    if tp is not None:
        qty = pending_tickets.get(entry.id, 0)
        widget_html = sx_call("events-entry-widget-wrapper",
                             widget=_ticket_widget_html(entry, qty, ticket_url, ctx={}))

    return sx_call("events-entry-card",
                  title=title_html, badges=SxExpr(badges_html),
                  time_parts=SxExpr(time_parts), cost=SxExpr(cost_html),
                  widget=SxExpr(widget_html))


def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict,
                          ticket_url: str, events_url_fn, *, is_page_scoped: bool = False,
                          post: dict | None = None) -> str:
    """Render a tile card for one event entry."""
    from .tickets import _ticket_widget_html
    pi = page_info.get(getattr(entry, "calendar_container_id", 0), {})
    if is_page_scoped and post:
        page_slug = pi.get("slug", post.get("slug", ""))
    else:
        page_slug = pi.get("slug", "")
    page_title = pi.get("title")

    day_href = ""
    if page_slug and entry.start_at:
        day_href = events_url_fn(f"/{page_slug}/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/")
    entry_href = f"{day_href}entries/{entry.id}/" if day_href else ""

    # Title
    if entry_href:
        title_html = sx_call("events-entry-title-tile-linked",
                            href=entry_href, name=entry.name)
    else:
        title_html = sx_call("events-entry-title-tile-plain", name=entry.name)

    # Badges
    badges_html = ""
    if page_title and (not is_page_scoped or page_title != (post or {}).get("title")):
        page_href = events_url_fn(f"/{page_slug}/")
        badges_html += sx_call("events-entry-page-badge",
                              href=page_href, title=page_title)
    cal_name = getattr(entry, "calendar_name", "")
    if cal_name:
        badges_html += sx_call("events-entry-cal-badge", name=cal_name)

    # Time
    time_html = ""
    if day_href:
        time_html += (sx_call("events-entry-time-linked",
                            href=day_href,
                            date_str=entry.start_at.strftime("%a %-d %b"))).replace(" &middot; ", "")
    else:
        time_html += entry.start_at.strftime("%a %-d %b")
    time_html += f' \u00b7 {entry.start_at.strftime("%H:%M")}'
    if entry.end_at:
        time_html += f' \u2013 {entry.end_at.strftime("%H:%M")}'

    cost = getattr(entry, "cost", None)
    cost_html = sx_call("events-entry-cost",
                       cost=f"\u00a3{cost:.2f}") if cost else ""

    # Ticket widget
    tp = getattr(entry, "ticket_price", None)
    widget_html = ""
    if tp is not None:
        qty = pending_tickets.get(entry.id, 0)
        widget_html = sx_call("events-entry-tile-widget-wrapper",
                             widget=_ticket_widget_html(entry, qty, ticket_url, ctx={}))

    return sx_call("events-entry-card-tile",
                  title=title_html, badges=SxExpr(badges_html),
                  time=SxExpr(time_html), cost=SxExpr(cost_html),
                  widget=SxExpr(widget_html))


def _entry_cards_html(entries, page_info, pending_tickets, ticket_url,
                      events_url_fn, view, page, has_more, next_url,
                      *, is_page_scoped=False, post=None) -> str:
    """Render entry cards (list or tile) with sentinel."""
    parts = []
    last_date = None
    for entry in entries:
        if view == "tile":
            parts.append(_entry_card_tile_html(
                entry, page_info, pending_tickets, ticket_url, events_url_fn,
                is_page_scoped=is_page_scoped, post=post,
            ))
        else:
            entry_date = entry.start_at.strftime("%A %-d %B %Y") if entry.start_at else ""
            if entry_date != last_date:
                parts.append(sx_call("events-date-separator",
                                    date_str=entry_date))
                last_date = entry_date
            parts.append(_entry_card_html(
                entry, page_info, pending_tickets, ticket_url, events_url_fn,
                is_page_scoped=is_page_scoped, post=post,
            ))

    if has_more:
        parts.append(sx_call("sentinel-simple",
                            id=f"sentinel-{page}", next_url=next_url))
    return "".join(parts)


# ---------------------------------------------------------------------------
# All events / page summary main panels
# ---------------------------------------------------------------------------

def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_info,
                             page, view, ticket_url, next_url, events_url_fn,
                             *, is_page_scoped=False, post=None) -> str:
    """Render the events main panel with view toggle + cards."""
    toggle = _view_toggle_html(ctx, view)

    if entries:
        cards = _entry_cards_html(
            entries, page_info, pending_tickets, ticket_url, events_url_fn,
            view, page, has_more, next_url,
            is_page_scoped=is_page_scoped, post=post,
        )
        grid_cls = ("max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
                    if view == "tile" else "max-w-full px-3 py-3 space-y-3")
        body = sx_call("events-grid", grid_cls=grid_cls, cards=SxExpr(cards))
    else:
        body = sx_call("empty-state", icon="fa fa-calendar-xmark",
                      message="No upcoming events",
                      cls="px-3 py-12 text-center text-stone-400")

    return sx_call("events-main-panel-body",
                  toggle=toggle, body=body)


# ---------------------------------------------------------------------------
# Entry main panel
# ---------------------------------------------------------------------------

def _entry_main_panel_html(ctx: dict) -> str:
    """Render the entry detail panel (name, slot, time, state, cost, tickets,
    buy form, date, posts, options + edit button)."""
    from quart import url_for
    from .tickets import render_buy_form

    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 = ctx.get("styles") or {}
    list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
    pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")

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

    def _field(label, content_html):
        return sx_call("events-entry-field", label=label, content=SxExpr(content_html))

    # Name
    name_html = _field("Name", sx_call("events-entry-name-field", name=entry.name))

    # Slot
    slot = getattr(entry, "slot", None)
    if slot:
        flex_label = "(flexible)" if getattr(slot, "flexible", False) else "(fixed)"
        slot_inner = sx_call("events-entry-slot-assigned",
                            slot_name=slot.name, flex_label=flex_label)
    else:
        slot_inner = sx_call("events-entry-slot-none")
    slot_html = _field("Slot", slot_inner)

    # Time Period
    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"
    time_html = _field("Time Period", sx_call("events-entry-time-field",
                                              time_str=start_str + end_str))

    # State
    state_html = _field("State", sx_call("events-entry-state-field",
                                         entry_id=str(eid),
                                         badge=_entry_state_badge_html(state)))

    # Cost
    cost = getattr(entry, "cost", None)
    cost_str = f"{cost:.2f}" if cost is not None else "0.00"
    cost_html = _field("Cost", sx_call("events-entry-cost-field",
                                       cost=f"\u00a3{cost_str}"))

    # Ticket Configuration (admin)
    tickets_html = _field("Tickets", sx_call("events-entry-tickets-field",
                                             entry_id=str(eid),
                                             tickets_config=SxExpr(render_entry_tickets_config(entry, calendar, day, month, year))))

    # Buy Tickets (public-facing)
    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 {}
    buy_html = render_buy_form(
        entry, ticket_remaining, ticket_sold_count,
        user_ticket_count, user_ticket_counts_by_type,
    )

    # Date
    date_str = entry.start_at.strftime("%A, %B %d, %Y") if entry.start_at else ""
    date_html = _field("Date", sx_call("events-entry-date-field", date_str=date_str))

    # Associated Posts
    entry_posts = ctx.get("entry_posts") or []
    posts_html = _field("Associated Posts", sx_call("events-entry-posts-field",
                                                    entry_id=str(eid),
                                                    posts_panel=render_entry_posts_panel(entry_posts, entry, calendar, day, month, year)))

    # Options and Edit Button
    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,
    )

    return sx_call("events-entry-panel",
                  entry_id=str(eid), list_container=list_container,
                  name=name_html, slot=slot_html,
                  time=time_html, state=state_html,
                  cost=cost_html, tickets=tickets_html,
                  buy=SxExpr(buy_html), date=date_html,
                  posts=posts_html,
                  options=_entry_options_html(entry, calendar, day, month, year),
                  pre_action=pre_action, edit_url=edit_url)


# ---------------------------------------------------------------------------
# Entry header row
# ---------------------------------------------------------------------------

def _entry_header_html(ctx: dict, *, oob: bool = False) -> str:
    """Build entry detail header row."""
    from quart import url_for

    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    entry = ctx.get("entry")
    if not entry:
        return ""
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")

    link_href = url_for(
        "defpage_entry_detail",
        calendar_slug=cal_slug,
        year=year, month=month, day=day,
        entry_id=entry.id,
    )
    label_html = sx_call("events-entry-label",
                        entry_id=str(entry.id),
                        title=_entry_title_html(entry),
                        times=SxExpr(_entry_times_html(entry)))

    nav_html = _entry_nav_html(ctx)

    return sx_call("menu-row-sx", id="entry-row", level=5,
                  link_href=link_href, link_label_content=label_html,
                  nav=SxExpr(nav_html) if nav_html else None, child_id="entry-header-child", oob=oob)


def _entry_times_html(entry) -> str:
    """Render entry times label."""
    start = entry.start_at
    end = entry.end_at
    if not start:
        return ""
    start_str = start.strftime("%H:%M")
    end_str = f" \u2192 {end.strftime('%H:%M')}" if end else ""
    return sx_call("events-entry-times", time_str=start_str + end_str)


# ---------------------------------------------------------------------------
# Entry nav (desktop + admin link)
# ---------------------------------------------------------------------------

def _entry_nav_html(ctx: dict) -> str:
    """Entry desktop nav: associated posts scrolling menu + admin link."""
    from quart import url_for

    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    entry = ctx.get("entry")
    if not entry:
        return ""
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")
    entry_posts = ctx.get("entry_posts") or []
    rights = ctx.get("rights") or {}
    is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)

    blog_url_fn = ctx.get("blog_url")

    parts = []

    # Associated Posts scrolling menu
    if entry_posts:
        post_links = ""
        for ep in entry_posts:
            slug = getattr(ep, "slug", "")
            title = getattr(ep, "title", "")
            feat = getattr(ep, "feature_image", None)
            href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/"
            if feat:
                img_html = sx_call("events-post-img", src=feat, alt=title)
            else:
                img_html = sx_call("events-post-img-placeholder")
            post_links += sx_call("events-entry-nav-post-link",
                                 href=href, img=img_html, title=title)
        parts.append((sx_call("events-entry-posts-nav-oob",
                            items=SxExpr(post_links))).replace(' :hx-swap-oob "true"', ''))

    # Admin link
    if is_admin:
        admin_url = url_for(
            "defpage_entry_admin",
            calendar_slug=cal_slug,
            day=day, month=month, year=year,
            entry_id=entry.id,
        )
        parts.append(sx_call("events-entry-admin-link", href=admin_url))

    return "".join(parts)


# ---------------------------------------------------------------------------
# Entry optioned (confirm/decline/provisional response)
# ---------------------------------------------------------------------------

def render_entry_optioned(entry, calendar, day, month, year) -> str:
    """Render entry options buttons + OOB title & state swaps."""
    options = _entry_options_html(entry, calendar, day, month, year)
    title = _entry_title_html(entry)
    state = _entry_state_badge_html(getattr(entry, "state", "pending") or "pending")

    return options + sx_call("events-entry-optioned-oob",
                            entry_id=str(entry.id),
                            title=title, state=state)


def _entry_title_html(entry) -> str:
    """Render entry title (icon + name + state badge)."""
    state = getattr(entry, "state", "pending") or "pending"
    return sx_call("events-entry-title",
                  name=entry.name,
                  badge=_entry_state_badge_html(state))


def _entry_options_html(entry, calendar, day, month, year) -> str:
    """Render confirm/decline/provisional buttons based on entry state."""
    from quart import url_for, g
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    styles = getattr(g, "styles", None) or {}
    action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "")

    cal_slug = getattr(calendar, "slug", "")
    eid = entry.id
    state = getattr(entry, "state", "pending") or "pending"
    target = f"#calendar_entry_options_{eid}"

    def _make_button(action_name, label, confirm_title, confirm_text, *, trigger_type="submit"):
        url = url_for(
            f"calendar.day.calendar_entries.calendar_entry.{action_name}",
            calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid,
        )
        btn_type = "button" if trigger_type == "button" else "submit"
        return sx_call("events-entry-option-button",
                      url=url, target=target, csrf=csrf, btn_type=btn_type,
                      action_btn=action_btn, confirm_title=confirm_title,
                      confirm_text=confirm_text, label=label,
                      is_btn=trigger_type == "button")

    buttons_html = ""
    if state == "provisional":
        buttons_html += _make_button(
            "confirm_entry", "confirm",
            "Confirm entry?", "Are you sure you want to confirm this entry?",
        )
        buttons_html += _make_button(
            "decline_entry", "decline",
            "Decline entry?", "Are you sure you want to decline this entry?",
        )
    elif state == "confirmed":
        buttons_html += _make_button(
            "provisional_entry", "provisional",
            "Provisional entry?", "Are you sure you want to provisional this entry?",
            trigger_type="button",
        )

    return sx_call("events-entry-options",
                  entry_id=str(eid), buttons=SxExpr(buttons_html))


# ---------------------------------------------------------------------------
# Entry tickets config (display + form)
# ---------------------------------------------------------------------------

def render_entry_tickets_config(entry, calendar, day, month, year) -> str:
    """Render ticket config display + edit form for admin entry view."""
    from quart import url_for
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    cal_slug = getattr(calendar, "slug", "")
    eid = entry.id
    tp = getattr(entry, "ticket_price", None)
    tc = getattr(entry, "ticket_count", None)
    eid_s = str(eid)
    show_js = f"document.getElementById('ticket-form-{eid}').classList.remove('hidden'); this.classList.add('hidden');"
    hide_js = (f"document.getElementById('ticket-form-{eid}').classList.add('hidden'); "
               f"document.getElementById('entry-tickets-{eid}').querySelectorAll('button:not([type=submit])').forEach(btn => btn.classList.remove('hidden'));")

    if tp is not None:
        tc_str = f"{tc} tickets" if tc is not None else "Unlimited"
        display_html = sx_call("events-ticket-config-display",
                              price_str=f"\u00a3{tp:.2f}",
                              count_str=tc_str, show_js=show_js)
    else:
        display_html = sx_call("events-ticket-config-none", show_js=show_js)

    update_url = url_for(
        "calendar.day.calendar_entries.calendar_entry.update_tickets",
        entry_id=eid, calendar_slug=cal_slug, day=day, month=month, year=year,
    )
    hidden_cls = "" if tp is None else "hidden"
    tp_val = f"{tp:.2f}" if tp is not None else ""
    tc_val = str(tc) if tc is not None else ""

    form_html = sx_call("events-ticket-config-form",
                       entry_id=eid_s, hidden_cls=hidden_cls,
                       update_url=update_url, csrf=csrf,
                       price_val=tp_val, count_val=tc_val, hide_js=hide_js)
    return display_html + form_html


# ---------------------------------------------------------------------------
# Entry posts panel
# ---------------------------------------------------------------------------

def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) -> str:
    """Render associated posts list with remove buttons and search input."""
    from quart import url_for
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    cal_slug = getattr(calendar, "slug", "")
    eid = entry.id
    eid_s = str(eid)

    posts_html = ""
    if entry_posts:
        items = ""
        for ep in entry_posts:
            ep_title = getattr(ep, "title", "")
            ep_id = getattr(ep, "id", 0)
            feat = getattr(ep, "feature_image", None)
            img_html = (sx_call("events-post-img", src=feat, alt=ep_title)
                        if feat else sx_call("events-post-img-placeholder"))

            del_url = url_for(
                "calendar.day.calendar_entries.calendar_entry.remove_post",
                calendar_slug=cal_slug, day=day, month=month, year=year,
                entry_id=eid, post_id=ep_id,
            )
            items += sx_call("events-entry-post-item",
                            img=img_html, title=ep_title,
                            del_url=del_url, entry_id=eid_s,
                            csrf_hdr={"X-CSRFToken": csrf})
        posts_html = sx_call("events-entry-posts-list", items=SxExpr(items))
    else:
        posts_html = sx_call("events-entry-posts-none")

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

    return sx_call("events-entry-posts-panel",
                  posts=posts_html, search_url=search_url,
                  entry_id=eid_s)


# ---------------------------------------------------------------------------
# Entry posts nav OOB
# ---------------------------------------------------------------------------

def render_entry_posts_nav_oob(entry_posts) -> str:
    """Render OOB nav for entry posts (scrolling menu)."""
    from quart import g
    styles = getattr(g, "styles", None) or {}
    nav_btn = getattr(styles, "nav_button", "") if hasattr(styles, "nav_button") else styles.get("nav_button", "")
    blog_url_fn = getattr(g, "blog_url", None)

    if not entry_posts:
        return sx_call("events-entry-posts-nav-oob-empty")

    items = ""
    for ep in entry_posts:
        slug = getattr(ep, "slug", "")
        title = getattr(ep, "title", "")
        feat = getattr(ep, "feature_image", None)
        href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/"
        img_html = (sx_call("events-post-img", src=feat, alt=title)
                    if feat else sx_call("events-post-img-placeholder"))
        items += sx_call("events-entry-nav-post",
                        href=href, nav_btn=nav_btn,
                        img=img_html, title=title)

    return sx_call("events-entry-posts-nav-oob", items=SxExpr(items))


# ---------------------------------------------------------------------------
# Day entries nav OOB
# ---------------------------------------------------------------------------

def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str:
    """Render OOB nav for confirmed entries in a day."""
    from quart import url_for, g

    styles = getattr(g, "styles", None) or {}
    nav_btn = getattr(styles, "nav_button", "") if hasattr(styles, "nav_button") else styles.get("nav_button", "")
    cal_slug = getattr(calendar, "slug", "")

    if not confirmed_entries:
        return sx_call("events-day-entries-nav-oob-empty")

    items = ""
    for entry in confirmed_entries:
        href = url_for(
            "defpage_entry_detail",
            calendar_slug=cal_slug,
            year=day_date.year, month=day_date.month, day=day_date.day,
            entry_id=entry.id,
        )
        start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
        end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
        items += sx_call("events-day-nav-entry",
                        href=href, nav_btn=nav_btn,
                        name=entry.name, time_str=start + end)

    return sx_call("events-day-entries-nav-oob", items=SxExpr(items))


# ---------------------------------------------------------------------------
# Post nav entries OOB
# ---------------------------------------------------------------------------

def render_post_nav_entries_oob(associated_entries, calendars, post) -> str:
    """Render OOB nav for associated entries and calendars of a post."""
    from quart import g
    from shared.infrastructure.urls import events_url

    styles = getattr(g, "styles", None) or {}
    nav_btn = getattr(styles, "nav_button_less_pad", "") if hasattr(styles, "nav_button_less_pad") else styles.get("nav_button_less_pad", "")

    has_entries = associated_entries and getattr(associated_entries, "entries", None)
    has_items = has_entries or calendars

    if not has_items:
        return sx_call("events-post-nav-oob-empty")

    slug = post.get("slug", "") if isinstance(post, dict) else getattr(post, "slug", "")

    items = ""
    if has_entries:
        for entry in associated_entries.entries:
            entry_path = (
                f"/{slug}/{entry.calendar_slug}/"
                f"{entry.start_at.year}/{entry.start_at.month}/{entry.start_at.day}/"
                f"entries/{entry.id}/"
            )
            href = events_url(entry_path)
            time_str = entry.start_at.strftime("%b %d, %Y at %H:%M")
            end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
            items += sx_call("events-post-nav-entry",
                            href=href, nav_btn=nav_btn,
                            name=entry.name, time_str=time_str + end_str)

    if calendars:
        for cal in calendars:
            cs = getattr(cal, "slug", "")
            local_href = events_url(f"/{slug}/{cs}/")
            items += sx_call("events-post-nav-calendar",
                            href=local_href, nav_btn=nav_btn, name=cal.name)

    hs = ("on load or scroll "
          "if window.innerWidth >= 640 and my.scrollWidth > my.clientWidth "
          "remove .hidden from .entries-nav-arrow add .flex to .entries-nav-arrow "
          "else add .hidden to .entries-nav-arrow remove .flex from .entries-nav-arrow end")

    return sx_call("events-post-nav-wrapper",
                  items=SxExpr(items), hyperscript=hs)


# ---------------------------------------------------------------------------
# Calendar description display + edit form
# ---------------------------------------------------------------------------

def render_calendar_description(calendar, *, oob: bool = False) -> str:
    """Render calendar description display with edit button, optionally with OOB title."""
    from quart import url_for
    from .calendar import _calendar_description_display_html

    cal_slug = getattr(calendar, "slug", "")
    edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug)
    html = _calendar_description_display_html(calendar, edit_url)

    if oob:
        desc = getattr(calendar, "description", "") or ""
        html += sx_call("events-calendar-description-title-oob",
                       description=desc)
    return html


def render_calendar_description_edit(calendar) -> str:
    """Render calendar description edit form."""
    from quart import url_for
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()
    cal_slug = getattr(calendar, "slug", "")
    desc = getattr(calendar, "description", "") or ""

    save_url = url_for("calendar.admin.calendar_description_save", calendar_slug=cal_slug)
    cancel_url = url_for("calendar.admin.calendar_description_view", calendar_slug=cal_slug)

    return sx_call("events-calendar-description-edit-form",
                  save_url=save_url, cancel_url=cancel_url,
                  csrf=csrf, description=desc)


# ---------------------------------------------------------------------------
# Entry admin page / OOB
# ---------------------------------------------------------------------------

def _entry_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
    """Build the entry admin header row."""
    from quart import url_for

    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    entry = ctx.get("entry")
    if not entry:
        return ""
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")

    link_href = url_for(
        "defpage_entry_admin",
        calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id,
    )
    # Nav: ticket_types link
    nav_html = _entry_admin_nav_html(ctx)

    return sx_call("menu-row-sx", id="entry-admin-row", level=6,
                  link_href=link_href, link_label="admin", icon="fa fa-cog",
                  nav=nav_html or None, child_id="entry-admin-header-child", oob=oob)


def _entry_admin_nav_html(ctx: dict) -> str:
    """Entry admin nav: ticket_types link."""
    from quart import url_for

    calendar = ctx.get("calendar")
    if not calendar:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    entry = ctx.get("entry")
    if not entry:
        return ""
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")
    select_colours = ctx.get("select_colours", "")

    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 sx_call("nav-link", href=href, label="ticket_types",
                  select_colours=select_colours)


def _entry_admin_main_panel_html(ctx: dict) -> str:
    """Entry admin main panel: just a ticket_types link."""
    from quart import url_for

    calendar = ctx.get("calendar")
    entry = ctx.get("entry")
    if not calendar or not entry:
        return ""
    cal_slug = getattr(calendar, "slug", "")
    day = ctx.get("day")
    month = ctx.get("month")
    year = ctx.get("year")
    styles = ctx.get("styles") or {}
    nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "")
    hx_select = ctx.get("hx_select_search", "#main-panel")
    select_colours = ctx.get("select_colours", "")

    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 sx_call("nav-link", href=href, label="ticket_types",
                  select_colours=select_colours, aclass=nav_btn,
                  is_selected=False)


# ---------------------------------------------------------------------------
# Post search results
# ---------------------------------------------------------------------------

def render_post_search_results(search_posts, search_query, page, total_pages,
                                entry, calendar, day, month, year) -> str:
    """Render post search results (replaces _types/entry/_post_search_results.html)."""
    from quart import url_for
    from shared.browser.app.csrf import generate_csrf_token
    csrf = generate_csrf_token()

    cal_slug = getattr(calendar, "slug", "")
    eid = entry.id

    parts = []
    for sp in search_posts:
        post_url = url_for("calendar.day.calendar_entries.calendar_entry.add_post",
                            calendar_slug=cal_slug, day=day, month=month, year=year,
                            entry_id=eid)
        feat = getattr(sp, "feature_image", None)
        title = getattr(sp, "title", "")
        if feat:
            img_html = sx_call("events-post-img", src=feat, alt=title)
        else:
            img_html = sx_call("events-post-img-placeholder")

        parts.append(sx_call("events-post-search-item",
                            post_url=post_url, entry_id=str(eid), csrf=csrf,
                            post_id=str(sp.id), img=img_html, title=title))

    result = "".join(parts)

    if page < int(total_pages):
        next_url = url_for("calendar.day.calendar_entries.calendar_entry.search_posts",
                            calendar_slug=cal_slug, day=day, month=month, year=year,
                            entry_id=eid, q=search_query, page=page + 1)
        result += sx_call("events-post-search-sentinel",
                         page=str(page), next_url=next_url)
    elif search_posts:
        result += sx_call("events-post-search-end")

    return result


# ---------------------------------------------------------------------------
# Entry edit form
# ---------------------------------------------------------------------------

def render_entry_edit_form(entry, calendar, day, month, year, day_slots) -> str:
    """Render entry edit form (replaces _types/entry/_edit.html)."""
    from quart import url_for, g
    from shared.browser.app.csrf import generate_csrf_token
    from .slots import _slot_options_html, _SLOT_PICKER_JS
    csrf = generate_csrf_token()

    styles = getattr(g, "styles", None) or {}
    list_container = styles.get("list_container", "") if isinstance(styles, dict) else getattr(styles, "list_container", "")
    action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
    cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
    cal_slug = getattr(calendar, "slug", "")
    eid = entry.id

    put_url = url_for("calendar.day.calendar_entries.calendar_entry.put",
                       calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid)
    cancel_url = url_for("defpage_entry_detail",
                          calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid)

    # Slot picker
    if day_slots:
        options_html = _slot_options_html(day_slots, selected_slot_id=getattr(entry, "slot_id", None))
        slot_picker_html = sx_call("events-slot-picker",
                                  id=f"entry-slot-{eid}", options=SxExpr(options_html))
    else:
        slot_picker_html = sx_call("events-no-slots")

    # Values
    start_val = entry.start_at.strftime("%H:%M") if entry.start_at else ""
    end_val = entry.end_at.strftime("%H:%M") if entry.end_at else ""
    cost = getattr(entry, "cost", None)
    cost_display = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
    tp = getattr(entry, "ticket_price", None)
    tc = getattr(entry, "ticket_count", None)
    tp_val = f"{tp:.2f}" if tp is not None else ""
    tc_val = str(tc) if tc is not None else ""

    html = sx_call("events-entry-edit-form",
                  entry_id=str(eid), list_container=list_container,
                  put_url=put_url, cancel_url=cancel_url, csrf=csrf,
                  name_val=entry.name or "", slot_picker=slot_picker_html,
                  start_val=start_val, end_val=end_val, cost_display=cost_display,
                  ticket_price_val=tp_val, ticket_count_val=tc_val,
                  action_btn=action_btn, cancel_btn=cancel_btn)
    return html + _SLOT_PICKER_JS


# ---------------------------------------------------------------------------
# Entry add form / button
# ---------------------------------------------------------------------------

def render_entry_add_form(calendar, day, month, year, day_slots) -> str:
    """Render entry add form (replaces _types/day/_add.html)."""
    from quart import url_for, g
    from shared.browser.app.csrf import generate_csrf_token
    from .slots import _slot_options_html, _SLOT_PICKER_JS
    csrf = generate_csrf_token()

    styles = getattr(g, "styles", None) or {}
    action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
    cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
    cal_slug = getattr(calendar, "slug", "")

    post_url = url_for("calendar.day.calendar_entries.add_entry",
                        calendar_slug=cal_slug, day=day, month=month, year=year)
    cancel_url = url_for("calendar.day.calendar_entries.add_button",
                          calendar_slug=cal_slug, day=day, month=month, year=year)

    # Slot picker
    if day_slots:
        options_html = _slot_options_html(day_slots)
        slot_picker_html = sx_call("events-slot-picker",
                                  id="entry-slot-new", options=SxExpr(options_html))
    else:
        slot_picker_html = sx_call("events-no-slots")

    html = sx_call("events-entry-add-form",
                  post_url=post_url, csrf=csrf,
                  slot_picker=slot_picker_html,
                  action_btn=action_btn, cancel_btn=cancel_btn,
                  cancel_url=cancel_url)
    return html + _SLOT_PICKER_JS


def render_entry_add_button(calendar, day, month, year) -> str:
    """Render entry add button (replaces _types/day/_add_button.html)."""
    from quart import url_for, g

    styles = getattr(g, "styles", None) or {}
    pre_action = styles.get("pre_action_button", "") if isinstance(styles, dict) else getattr(styles, "pre_action_button", "")
    cal_slug = getattr(calendar, "slug", "")
    add_url = url_for("calendar.day.calendar_entries.add_form",
                       calendar_slug=cal_slug, day=day, month=month, year=year)

    return sx_call("events-entry-add-button", pre_action=pre_action, add_url=add_url)


# ---------------------------------------------------------------------------
# Fragment: container cards entries
# ---------------------------------------------------------------------------

def render_fragment_container_cards(batch, post_ids, slug_map) -> str:
    """Render container cards entries (replaces fragments/container_cards_entries.html)."""
    from shared.infrastructure.urls import events_url

    parts = []
    for post_id in post_ids:
        parts.append(f"<!-- card-widget:{post_id} -->")
        widget_entries = batch.get(post_id, [])
        if widget_entries:
            cards_html = ""
            for entry in widget_entries:
                _post_slug = slug_map.get(post_id, "")
                _entry_path = (
                    f"/{_post_slug}/{entry.calendar_slug}/"
                    f"{entry.start_at.year}/{entry.start_at.month}/"
                    f"{entry.start_at.day}/entries/{entry.id}/"
                )
                time_str = entry.start_at.strftime("%H:%M")
                if entry.end_at:
                    time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}"
                cards_html += sx_call("events-frag-entry-card",
                                     href=events_url(_entry_path),
                                     name=entry.name,
                                     date_str=entry.start_at.strftime("%a, %b %d"),
                                     time_str=time_str)
            parts.append(sx_call("events-frag-entries-widget", cards=SxExpr(cards_html)))
        parts.append(f"<!-- /card-widget:{post_id} -->")

    return "\n".join(parts)


# ---------------------------------------------------------------------------
# Fragment: account page tickets
# ---------------------------------------------------------------------------

def render_fragment_account_tickets(tickets) -> str:
    """Render account page tickets (replaces fragments/account_page_tickets.html)."""
    from shared.infrastructure.urls import events_url

    if tickets:
        items_html = ""
        for ticket in tickets:
            href = events_url(f"/tickets/{ticket.code}/")
            date_str = ticket.entry_start_at.strftime("%d %b %Y, %H:%M")
            cal_name = ""
            if getattr(ticket, "calendar_name", None):
                cal_name = f'<span>&middot; {escape(ticket.calendar_name)}</span>'
            type_name = ""
            if getattr(ticket, "ticket_type_name", None):
                type_name = f'<span>&middot; {escape(ticket.ticket_type_name)}</span>'
            badge_html = sx_call("status-pill",
                                status=getattr(ticket, "state", ""))
            items_html += sx_call("events-frag-ticket-item",
                                 href=href, entry_name=ticket.entry_name,
                                 date_str=date_str, calendar_name=cal_name,
                                 type_name=type_name, badge=badge_html)
        body = sx_call("events-frag-tickets-list", items=SxExpr(items_html))
    else:
        body = sx_call("empty-state", message="No tickets yet.",
                      cls="text-sm text-stone-500")

    return sx_call("events-frag-tickets-panel", items=body)


# ---------------------------------------------------------------------------
# Fragment: account page bookings
# ---------------------------------------------------------------------------

def render_fragment_account_bookings(bookings) -> str:
    """Render account page bookings (replaces fragments/account_page_bookings.html)."""
    if bookings:
        items_html = ""
        for booking in bookings:
            date_str = booking.start_at.strftime("%d %b %Y, %H:%M")
            if getattr(booking, "end_at", None):
                date_str_extra = f'<span>&ndash; {escape(booking.end_at.strftime("%H:%M"))}</span>'
            else:
                date_str_extra = ""
            cal_name = ""
            if getattr(booking, "calendar_name", None):
                cal_name = f'<span>&middot; {escape(booking.calendar_name)}</span>'
            cost_str = ""
            if getattr(booking, "cost", None):
                cost_str = f'<span>&middot; &pound;{escape(str(booking.cost))}</span>'
            badge_html = sx_call("status-pill",
                                status=getattr(booking, "state", ""))
            items_html += sx_call("events-frag-booking-item",
                                 name=booking.name,
                                 date_str=date_str + date_str_extra,
                                 calendar_name=cal_name, cost_str=cost_str,
                                 badge=badge_html)
        body = sx_call("events-frag-bookings-list", items=SxExpr(items_html))
    else:
        body = sx_call("empty-state", message="No bookings yet.",
                      cls="text-sm text-stone-500")

    return sx_call("events-frag-bookings-panel", items=body)
