"""Response builder functions for virtual collection navigation (Tier 1 API)."""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/routes/handlers.ipynb.

# %% auto #0
__all__ = ['build_nav_response', 'build_cursor_move_response', 'handle_navigate', 'handle_navigate_to_index',
           'handle_update_viewport', 'handle_focus_row', 'handle_activate', 'handle_sort',
           'build_items_changed_response']

# %% ../../nbs/routes/handlers.ipynb #6fde5c29
import inspect
from typing import Any, Callable, List, Optional, Tuple

from fasthtml.common import Hidden

from cjm_fasthtml_virtual_collection.core.models import (
    VirtualCollectionConfig, VirtualCollectionState, VirtualCollectionUrls,
)
from ..core.html_ids import VirtualCollectionHtmlIds
from cjm_fasthtml_virtual_collection.core.windowing import (
    navigate, navigate_cursor, clamp_window_start, compute_window,
    find_nearest_focusable,
)
from ..components.table import render_slot, render_table_rows
from ..components.footer import render_footer
from ..components.scrollbar import render_scrollbar

# %% ../../nbs/routes/handlers.ipynb #2adaa2a1
def _render_window_start_oob(
    state: VirtualCollectionState,     # Current state
    ids: VirtualCollectionHtmlIds,     # HTML IDs
) -> Hidden:  # Hidden input with OOB swap
    """Render OOB hidden input carrying the current window_start for JS thumb positioning."""
    return Hidden(
        value=str(state.window_start),
        id=ids.window_start_input,
        name="window_start",
        hx_swap_oob="outerHTML",
    )


def _render_scrollbar_nav_oob(
    state: VirtualCollectionState,     # Current state
    config: VirtualCollectionConfig,   # Collection config
    ids: VirtualCollectionHtmlIds,     # HTML IDs
) -> 'Any':  # Scrollbar element with OOB swap (or None if no scrollbar configured)
    """Render OOB scrollbar with fresh data attributes."""
    if not config.show_scrollbar:
        return None
    sb = render_scrollbar(state, config, ids)
    sb.attrs["hx-swap-oob"] = "outerHTML"
    return sb


def build_nav_response(
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state (already mutated)
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    focus_url: str = "",                    # URL for click-to-focus
) -> Tuple:  # OOB elements (slot OOBs + footer + window_start input + scrollbar)
    """Build OOB response for navigation: all visible slots + footer + window_start + scrollbar."""
    render_start, render_end = compute_window(
        state.window_start, state.visible_rows, state.total_items
    )

    slot_oobs = [
        render_slot(
            slot_index=slot_idx, item=items[item_idx], item_index=item_idx,
            config=config, state=state, ids=ids, render_cell=render_cell,
            oob=True, focus_url=focus_url,
        )
        for slot_idx, item_idx in enumerate(range(render_start, render_end))
    ]

    result = tuple(slot_oobs) + (
        render_footer(state, ids, oob=True),
        _render_window_start_oob(state, ids),
    )

    sb_oob = _render_scrollbar_nav_oob(state, config, ids)
    if sb_oob is not None:
        result = result + (sb_oob,)

    return result

# %% ../../nbs/routes/handlers.ipynb #d3rfjri5i06
def build_cursor_move_response(
    old_cursor: int,                        # Previous cursor index
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state (cursor already updated)
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    focus_url: str = "",                    # URL for click-to-focus
) -> Tuple:  # OOB elements (affected slot OOBs + footer + window_start input + scrollbar)
    """Build OOB response for cursor-only move: swap just the affected slots."""
    oob_parts = []

    # Re-render old cursor slot (remove highlight)
    if old_cursor != state.cursor_index:
        old_slot = old_cursor - state.window_start
        oob_parts.append(render_slot(
            slot_index=old_slot, item=items[old_cursor], item_index=old_cursor,
            config=config, state=state, ids=ids, render_cell=render_cell,
            oob=True, focus_url=focus_url,
        ))

    # Re-render new cursor slot (add highlight)
    new_slot = state.cursor_index - state.window_start
    oob_parts.append(render_slot(
        slot_index=new_slot, item=items[state.cursor_index], item_index=state.cursor_index,
        config=config, state=state, ids=ids, render_cell=render_cell,
        oob=True, focus_url=focus_url,
    ))

    oob_parts.append(render_footer(state, ids, oob=True))
    oob_parts.append(_render_window_start_oob(state, ids))

    sb_oob = _render_scrollbar_nav_oob(state, config, ids)
    if sb_oob is not None:
        oob_parts.append(sb_oob)

    return tuple(oob_parts)

# %% ../../nbs/routes/handlers.ipynb #321e1995
def _is_cursor_visible(
    state: VirtualCollectionState,  # Current state
) -> bool:  # Whether cursor is within the visible window
    """Check if the cursor index is within the current visible window."""
    if state.cursor_index < 0:
        return False
    return state.window_start <= state.cursor_index < state.window_start + state.visible_rows


def _append_cursor_change(
    result: Tuple,                          # Base response tuple
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state
    on_cursor_change: Optional[Callable],   # Callback: (item, cursor_index, state) -> Tuple
) -> Tuple:  # Response with appended cursor change OOB elements
    """Append on_cursor_change callback results to a response tuple."""
    if on_cursor_change is None:
        return result
    cursor = state.cursor_index
    if cursor < 0 or cursor >= len(items):
        return result
    extra = on_cursor_change(items[cursor], cursor, state)
    if extra:
        return result + tuple(extra) if not isinstance(extra, tuple) else result + extra
    return result


def _scroll_to_cursor(
    state: VirtualCollectionState,  # Current state (mutated in place)
) -> None:
    """Scroll window to bring an off-screen cursor into view."""
    if state.cursor_index < 0:
        return
    window_end = state.window_start + state.visible_rows
    if state.cursor_index >= window_end:
        state.window_start = state.cursor_index - state.visible_rows + 1
    elif state.cursor_index < state.window_start:
        state.window_start = state.cursor_index
    state.window_start = clamp_window_start(
        state.window_start, state.visible_rows, state.total_items
    )


def handle_navigate(
    direction: str,                         # 'up', 'down', 'page_up', 'page_down', 'first', 'last'
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state (mutated in place)
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    focus_url: str = "",                    # URL for click-to-focus
    is_skippable: Optional[Callable[[Any], bool]] = None,  # Predicate: item -> skip?
    on_cursor_change: Optional[Callable] = None,  # Callback: (item, cursor_index, state) -> Tuple
) -> Tuple:  # OOB elements
    """Navigate in a direction. Mutates state in place."""
    if state.total_items == 0:
        return ()

    # Build index-based skip predicate from item-based one
    _skip_idx = (lambda idx: is_skippable(items[idx])) if is_skippable else None

    old_cursor = state.cursor_index

    if direction in ('up', 'down'):
        # If cursor is off-screen, scroll window to bring it into view
        # then proceed with normal navigation from the now-visible cursor
        if not _is_cursor_visible(state):
            _scroll_to_cursor(state)
            result = build_nav_response(items, state, config, ids, render_cell, focus_url)
            return result

        new_cursor, new_window_start, window_changed = navigate_cursor(
            state.cursor_index, direction, state.window_start,
            state.visible_rows, state.total_items, is_skippable=_skip_idx,
        )
        state.cursor_index = new_cursor
        state.window_start = new_window_start

        if window_changed:
            result = build_nav_response(items, state, config, ids, render_cell, focus_url)
        else:
            result = build_cursor_move_response(old_cursor, items, state, config, ids, render_cell, focus_url)
    else:
        # Page/Home/End navigation
        state.window_start = navigate(
            state.window_start, direction, state.visible_rows, state.total_items
        )
        # Move cursor to nearest focusable in the new window
        if direction in ('first', 'page_up'):
            target = state.window_start
            search_dir = 1
        else:
            target = min(state.window_start + state.visible_rows - 1, state.total_items - 1)
            search_dir = -1
        state.cursor_index = find_nearest_focusable(
            target, state.total_items, _skip_idx, direction=search_dir,
        )
        result = build_nav_response(items, state, config, ids, render_cell, focus_url)

    if state.cursor_index != old_cursor:
        result = _append_cursor_change(result, items, state, on_cursor_change)
    return result

# %% ../../nbs/routes/handlers.ipynb #yf0uovgd0kc
def _call_action_callback(
    callback: Callable,  # Consumer callback to invoke
    item: Any,  # Item at the cursor position
    row_index: int,  # Row index
    state: VirtualCollectionState,  # Current VC state
    request: Any = None,  # FastHTML request (passed if callback accepts it)
) -> Any:  # Callback result
    """Call an action callback, passing request if the callback signature accepts it."""
    sig = inspect.signature(callback)
    if 'request' in sig.parameters and request is not None:
        return callback(item, row_index, state, request=request)
    return callback(item, row_index, state)

# %% ../../nbs/routes/handlers.ipynb #bb86598d
def handle_navigate_to_index(
    target_index: int,                      # Target window_start
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state (mutated in place)
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    focus_url: str = "",                    # URL for click-to-focus
) -> Tuple:  # OOB elements
    """Navigate to a specific index. Mutates state.window_start in place."""
    if state.total_items == 0:
        return ()
    state.window_start = clamp_window_start(
        target_index, state.visible_rows, state.total_items
    )
    return build_nav_response(items, state, config, ids, render_cell, focus_url)

# %% ../../nbs/routes/handlers.ipynb #72c4a7a1
def _render_scrollbar_oob(
    state: VirtualCollectionState,     # Current state
    config: VirtualCollectionConfig,   # Collection config
    ids: VirtualCollectionHtmlIds,     # HTML IDs
) -> Any:  # Scrollbar element with OOB swap (or None if no scrollbar configured)
    """Render OOB scrollbar with fresh data attributes."""
    if not config.show_scrollbar:
        return None
    sb = render_scrollbar(state, config, ids)
    sb.attrs["hx-swap-oob"] = "outerHTML"
    return sb


def _build_container_response(
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    focus_url: str = "",                    # URL for click-to-focus
) -> Tuple:  # OOB elements (container + footer + window_start input [+ scrollbar])
    """Build OOB response that replaces the entire rows container with new slots."""
    rows_oob = render_table_rows(items, config, state, ids, render_cell, focus_url=focus_url)
    rows_oob.attrs["hx-swap-oob"] = "outerHTML"
    result = (
        rows_oob,
        render_footer(state, ids, oob=True),
        _render_window_start_oob(state, ids),
    )
    sb_oob = _render_scrollbar_oob(state, config, ids)
    if sb_oob is not None:
        result = result + (sb_oob,)
    return result


def handle_update_viewport(
    visible_rows: int,                      # New visible row count
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state (mutated in place)
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    is_auto: bool = True,                   # Whether from auto-fit
    focus_url: str = "",                    # URL for click-to-focus
) -> Tuple:  # OOB elements
    """Update viewport with new row count. Mutates state in place."""
    visible_rows = max(1, visible_rows)
    state.visible_rows = visible_rows
    state.is_auto_mode = is_auto
    state.window_start = clamp_window_start(
        state.window_start, state.visible_rows, state.total_items
    )
    # Keep cursor visible after resize
    _scroll_to_cursor(state)
    # Replace entire container — slot count changed
    return _build_container_response(items, state, config, ids, render_cell, focus_url)

# %% ../../nbs/routes/handlers.ipynb #yz8pye66p
def handle_focus_row(
    row_index: int,                         # Row index to focus
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state (mutated in place)
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    focus_url: str = "",                    # URL for click-to-focus
    on_refocus: Optional[Callable] = None,  # Callback when clicking already-focused row: (item, row_index, state) -> Tuple
    is_skippable: Optional[Callable[[Any], bool]] = None,  # Predicate: item -> skip?
    on_cursor_change: Optional[Callable] = None,  # Callback: (item, cursor_index, state) -> Tuple
    request: Any = None,  # FastHTML request (passed to on_refocus if it accepts it)
) -> Tuple:  # OOB elements (affected slot OOBs + footer + window_start input)
    """Move cursor to a specific row via click/tap/scrollbar.

    If the clicked row is skippable, finds the nearest focusable row (searching
    forward first, then backward) and focuses that instead. This ensures scrollbar
    drag to a skippable region still moves cursor to the closest data row.
    If `on_refocus` is provided and the target row is already the cursor,
    delegates to `on_refocus` instead of the normal cursor-move logic.
    When the target row is off-screen (e.g. scrollbar drag to distant row),
    scrolls the viewport and rebuilds all visible slots.
    """
    if state.total_items == 0:
        return ()
    original_target = max(0, min(row_index, state.total_items - 1))
    clamped = original_target

    # If target is skippable, find nearest focusable row
    if is_skippable is not None and is_skippable(items[clamped]):
        _skip_idx = lambda idx: is_skippable(items[idx])
        clamped = find_nearest_focusable(clamped, state.total_items, _skip_idx, direction=1)
        if clamped < 0:
            return ()  # All items skippable

    # Refocus: target row is already the cursor
    if on_refocus is not None and clamped == state.cursor_index:
        return _call_action_callback(on_refocus, items[clamped], clamped, state, request)

    old_cursor = state.cursor_index
    old_window = state.window_start
    state.cursor_index = clamped

    # Ensure cursor is visible
    if not _is_cursor_visible(state):
        _scroll_to_cursor(state)

    # Show context: if original target is above the viewport (e.g. scrollbar
    # dragged to a skippable header that got snapped forward), adjust window
    # to include the original target so skippable rows are visible.
    if original_target < state.window_start:
        state.window_start = clamp_window_start(
            original_target, state.visible_rows, state.total_items
        )

    # Full viewport rebuild if window moved; lightweight swap if only cursor moved
    if state.window_start != old_window:
        result = build_nav_response(items, state, config, ids, render_cell, focus_url)
    else:
        result = build_cursor_move_response(old_cursor, items, state, config, ids, render_cell, focus_url)

    if state.cursor_index != old_cursor:
        result = _append_cursor_change(result, items, state, on_cursor_change)
    return result

# %% ../../nbs/routes/handlers.ipynb #twfrtie2aw
def handle_activate(
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    on_activate: Callable,                  # Consumer callback: (item, row_index, state[, request]) -> Tuple of OOB elements
    focus_url: str = "",                    # URL for click-to-focus
    request: Any = None,  # FastHTML request (passed to on_activate if it accepts it)
) -> Tuple:  # OOB elements from consumer callback
    """Activate the focused row via Space/Enter. Delegates to consumer callback."""
    cursor = state.cursor_index
    if cursor < 0 or cursor >= state.total_items:
        return ()
    if not _is_cursor_visible(state):
        return ()
    return _call_action_callback(on_activate, items[cursor], cursor, state, request)

# %% ../../nbs/routes/handlers.ipynb #qswv325tauk
def handle_sort(
    column_key: str,                        # Column key to sort by
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state (mutated in place)
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    sort_callback: Callable,                # Consumer: (items, column_key, ascending) -> sorted items
    sort_url: str = "",                     # Sort URL for header re-render
    focus_url: str = "",                    # URL for click-to-focus
    is_skippable: Optional[Callable[[Any], bool]] = None,  # Predicate: item -> skip?
    on_cursor_change: Optional[Callable] = None,  # Callback: (item, cursor_index, state) -> Tuple
) -> Tuple:  # OOB elements (header + rows + footer + window_start)
    """Sort by column. Toggles direction if same column, resets window to start."""
    old_cursor = state.cursor_index

    if state.sort_column == column_key:
        state.sort_ascending = not state.sort_ascending
    else:
        state.sort_column = column_key
        state.sort_ascending = True

    # Consumer sorts items in place
    sort_callback(items, column_key, state.sort_ascending)

    # Reset to top and find nearest focusable cursor position
    state.window_start = 0
    _skip_idx = (lambda idx: is_skippable(items[idx])) if is_skippable else None
    state.cursor_index = find_nearest_focusable(0, state.total_items, _skip_idx, direction=1)

    # Re-render header with updated sort indicators
    from cjm_fasthtml_virtual_collection.components.table import render_header_row
    header_oob = render_header_row(config, ids, state=state, sort_url=sort_url)
    header_oob.attrs["hx-swap-oob"] = "outerHTML"

    # Re-render visible rows
    nav_response = build_nav_response(items, state, config, ids, render_cell, focus_url)

    result = (header_oob,) + nav_response
    if state.cursor_index != old_cursor:
        result = _append_cursor_change(result, items, state, on_cursor_change)
    return result

# %% ../../nbs/routes/handlers.ipynb #b4t4pmhyka
def build_items_changed_response(
    items:list,  # Full item list (already mutated by consumer)
    state:VirtualCollectionState,  # Current state (mutated in place)
    config:VirtualCollectionConfig,  # Collection config
    ids:VirtualCollectionHtmlIds,  # HTML IDs
    render_cell:Callable,  # Consumer cell render callback
    focus_url:str="",  # URL for click-to-focus
    is_skippable:Optional[Callable[[Any], bool]]=None,  # Predicate: item -> skip?
    refit_callback:str="",  # JS auto-fit call expression (from auto_fit_callback_name)
) -> Tuple:  # OOB elements (container + scrollbar + footer + window_start [+ refit trigger])
    """Rebuild the VC after the consumer modifies the item list externally.

    Updates total_items, clamps window_start and cursor_index, then replaces
    the entire rows container via OOB. Use this after deleting, adding, or
    filtering items — any operation that changes the item count.

    Pass `refit_callback` (from `auto_fit_callback_name(config)`) to trigger
    auto-fit re-evaluation after the update — needed when items are added and
    the viewport may have room for more rows than currently rendered.
    """
    state.total_items = len(items)

    if state.total_items == 0:
        state.window_start = 0
        state.cursor_index = -1
    else:
        state.window_start = clamp_window_start(
            state.window_start, state.visible_rows, state.total_items,
        )
        # Clamp cursor to valid range, then find nearest focusable
        _skip_idx = (lambda idx: is_skippable(items[idx])) if is_skippable else None
        if state.cursor_index >= state.total_items:
            state.cursor_index = state.total_items - 1
        if state.cursor_index < 0:
            state.cursor_index = 0
        state.cursor_index = find_nearest_focusable(
            state.cursor_index, state.total_items, _skip_idx, direction=-1,
        )

    result = _build_container_response(items, state, config, ids, render_cell, focus_url)

    if refit_callback:
        from fasthtml.common import Div, Script

        # OOB-swap the refit trigger div with a script that calls auto-fit.
        # HTMX executes scripts in OOB-swapped content.
        trigger = Div(
            Script(f"setTimeout(() => {{ {refit_callback}; }}, 0);"),
            id=ids.refit_trigger,
            style="display:none",
        )
        trigger.attrs['hx-swap-oob'] = 'innerHTML'
        result = result + (trigger,)

    return result
