<?php
namespace Components\Resources;

use Components\Support\Palette;
use Components\ConfigStore;
use Components\Router;

/**
 * ItemList: like ResourceList but styled to resemble trigger_module cards.
 * All inline HTML is captured with output buffering and returned as a string.
 *
 * API mirrors ResourceList::render($options) for drop-in usage:
 * - title, items, id_key, name_key, status_key, outer_attrs, empty_text
 * - panel_type, panel_id, panel_target_id
 * - new_action, row_actions
 * - config_action: optional gear-style button (like trigger_module) opening a panel
 * - icon: global default icon config (see ResourceList docs)
 */
class ItemList
{
    public static function render(array $options): string
    {
        $title = (string)($options['title'] ?? '');
        $items = is_array($options['items'] ?? null) ? $options['items'] : [];
        $idKey = (string)($options['id_key'] ?? 'id');
        $nameKey = (string)($options['name_key'] ?? 'name');
        $statusKey = (string)($options['status_key'] ?? 'status');
        $outerAttrs = trim((string)($options['outer_attrs'] ?? ''));
        $emptyText = (string)($options['empty_text'] ?? 'No entries found.');
        $newAction = is_array($options['new_action'] ?? null) ? $options['new_action'] : null;
        $rowActions = is_array($options['row_actions'] ?? null) ? $options['row_actions'] : [];
        // Optional config button (gear) similar to pages/components/trigger_module.php
        $configAction = is_array($options['config_action'] ?? null) ? $options['config_action'] : null;

        // Expandable content options
        $expandContent = $options['expand_content'] ?? null; // callable returning HTML or string HTML
        $expandInclude = isset($options['expand_include']) ? (string)$options['expand_include'] : '';
        $expandable = (bool)($options['expandable'] ?? false) || is_callable($expandContent) || ($expandInclude !== '');
        $expandControls = (bool)($options['expand_controls'] ?? $expandable);
        // Match trigger_module.php expand area defaults (outer wrapper fixed, inner scroller handles x-scroll & padding)
        $expandContentClass = (string)($options['expand_content_class'] ?? 'relative overflow-hidden bg-divider/0 border-t border-divider/50 dark:border-muted/10');
        $openKeyField = (string)($options['open_key_field'] ?? $idKey);
        $openKeyPrefix = (string)($options['open_key_prefix'] ?? 'itemlist'); // caller should set a model-specific prefix
        $persistOpen = (bool)($options['persist_open'] ?? true);
        $expandPatternBg = (bool)($options['expand_pattern_bg'] ?? true);
        // Lazy load options for expandable content
        $expandLazyUrlOpt = $options['expand_lazy_url'] ?? null; // string|callable($item,$id)
        $expandLazySwap = (string)($options['expand_lazy_swap'] ?? 'innerHTML');
        $expandLazyAttrs = is_array($options['expand_lazy_attrs'] ?? null) ? $options['expand_lazy_attrs'] : [];

        // Pattern A (HX-Trigger + client-side refresh) support
        // Optional: endpoints and events enabling self-refresh of #item-* and #expandable-* blocks
        $eventPrefix = (string)($options['event_prefix'] ?? ''); // e.g. 'item' or 'flow'
        // Row (outer card) self-refresh
        $rowRefreshUrlOpt = $options['row_refresh_url'] ?? null; // string|callable($item,$id)
        $rowRefreshEventsOpt = $options['row_refresh_events'] ?? null; // string|array
        // Expandable area self-refresh
        $expandRefreshUrlOpt = $options['expand_refresh_url'] ?? null; // string|callable($item,$id)
        $expandRefreshEventsOpt = $options['expand_refresh_events'] ?? null; // string|array

        // Panel configuration (modal or drawer)
        $panelType = (string)($options['panel_type'] ?? 'modal');
        $panelId = (string)($options['panel_id'] ?? ($panelType === 'drawer' ? 'config-drawer' : 'config-modal'));
        $panelTargetId = (string)($options['panel_target_id'] ?? ($panelId . '-body'));

        // helpers moved to methods: btnClasses(), renderAttrs(), buildIcon()

        // Build openMap for Alpine (expand/collapse state) if expandable
        $allKeys = [];
        if ($expandable && !empty($items)) {
            foreach ($items as $it) {
                $rawKey = (string)($it[$openKeyField] ?? '');
                if ($rawKey === '') { continue; }
                $allKeys[] = $openKeyPrefix . ':' . $rawKey;
            }
        }
        $openMap = [];
        $allOpenInit = false;
        $allClosedInit = false;
        $listId = (string)($options['list_id'] ?? ('itemlist-' . substr(md5(($title ?: 'list') . '|' . json_encode($allKeys)), 0, 8)));
        $bulkJsonId = $listId . '-open-json';
        $bulkSaverId = $listId . '-open-saver';
        if (!empty($allKeys)) {
            $openMap = ConfigStore::getUiOpenMap();
            foreach ($allKeys as $k) {
                if (!isset($openMap[$k])) { $openMap[$k] = false; }
            }
            // Limit to keys present on this list
            $openMap = array_intersect_key($openMap, array_flip($allKeys));
            $allOpenInit = !empty($openMap) && !in_array(false, $openMap, true);
            $allClosedInit = !empty($openMap) && !in_array(true, $openMap, true);
        }

        ob_start();
        ?>
        <div <?= $outerAttrs; ?> class="space-y-2"
            <?php if ($expandable): ?>
                x-data='{
                    openMap: <?php echo json_encode($openMap, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?>,
                    toggle(id) { this.openMap[id] = !this.openMap[id]; <?php echo $persistOpen ? 'this.$nextTick(() => this.persistAllOpen());' : ''; ?> },
                    persistAllOpen() {
                        const saver = (this.$refs && this.$refs.bulkSaver) ? this.$refs.bulkSaver : document.getElementById("<?php echo \Components\Base::h($bulkSaverId); ?>");
                        if (saver) { saver.dispatchEvent(new CustomEvent("save-open-bulk", { bubbles: true })); }
                    },
                    expandAll() { Object.keys(this.openMap).forEach(k => this.openMap[k] = true); this.$nextTick(() => this.persistAllOpen()); },
                    collapseAll() { Object.keys(this.openMap).forEach(k => this.openMap[k] = false); this.$nextTick(() => this.persistAllOpen()); },
                    allOpen() { const v = Object.values(this.openMap); return v.length>0 && v.every(x => x === true); },
                    allClosed() { const v = Object.values(this.openMap); return v.length>0 && v.every(x => x === false); }
                }'
                x-ref="root"
            <?php endif; ?>
        >
            <div class="flex items-center justify-between mb-2">
                <div class="text-sm font-semibold text-gray-900 dark:text-gray-100"><?php echo \Components\Base::h($title); ?></div>
                <?php if ($newAction):
                    $naAttrs = $newAction['attrs'] ?? [];
                    if (!isset($naAttrs['command'])) { $naAttrs['command'] = 'show-modal'; }
                    if (!isset($naAttrs['commandfor'])) { $naAttrs['commandfor'] = $panelId; }
                    if (!isset($naAttrs['hx-target'])) { $naAttrs['hx-target'] = '#' . $panelTargetId; }
                    if (!isset($naAttrs['hx-swap'])) { $naAttrs['hx-swap'] = 'innerHTML'; }
                    if (isset($newAction['url'])) {
                        $url = (string)$newAction['url'];
                        $isDrawer = (substr($panelId, -6) === 'drawer');
                        if ($isDrawer && stripos($url, 'drawer=') === false) {
                            $url .= (strpos($url, '?') === false ? '?' : '&') . 'drawer=1';
                        }
                        $naAttrs['hx-get'] = $url;
                    }
                    ?>
                    <button type="button" class="inline-flex items-center rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-white hover:bg-primary/90"<?= self::renderAttrs($naAttrs); ?>>
                        <?php echo \Components\Base::h((string)($newAction['label'] ?? 'New')); ?>
                    </button>
                <?php endif; ?>
            </div>
            <?php if ($expandable && $expandControls && !empty($allKeys)): ?>
                <div class="flex items-center justify-end gap-2 mb-3 ">
                    <?php
                    echo \Components\Button::render('Expand all', [
                        'size' => 'sm',
                        'variant' => 'ghost',
                        'attrs' => [
                            '@click' => 'expandAll()',
                            'data-no-global-loader' => '1',
                            'x-show' => '!allOpen()',
                            'x-cloak' => true,
                            'style' => ($allOpenInit ? 'display:none' : null),
                        ],
                    ]);
                    echo \Components\Button::render('Collapse all', [
                        'size' => 'sm',
                        'variant' => 'ghost',
                        'attrs' => [
                            '@click' => 'collapseAll()',
                            'data-no-global-loader' => '1',
                            'x-show' => '!allClosed()',
                            'x-cloak' => true,
                            'style' => ($allClosedInit ? 'display:none' : null),
                        ],
                    ]);
                    ?>
                </div>
                <?php if ($persistOpen): ?>
                    <input type="hidden" id="<?php echo \Components\Base::h($bulkJsonId); ?>" name="open_map" :value="JSON.stringify(openMap)" />
                    <div id="<?php echo \Components\Base::h($bulkSaverId); ?>" x-ref="bulkSaver" class="hidden" data-no-global-loader="1"
                         hx-post="<?= Router::url('save_ui_open_bulk', ['partial' => 1]); ?>"
                         hx-trigger="save-open-bulk"
                         hx-include="#<?php echo \Components\Base::h($bulkJsonId); ?>"
                         hx-swap="none"></div>
                <?php endif; ?>
            <?php endif; ?>
            <?php if (empty($items)): ?>
                <div class="text-sm text-muted"><?php echo \Components\Base::h($emptyText); ?></div>
            <?php else: ?>
                <div class="grid grid-cols-1 gap-2">
                    <?php foreach ($items as $it):
                        $id = (int)($it[$idKey] ?? 0);
                        $name = htmlspecialchars((string)($it[$nameKey] ?? ''), ENT_QUOTES, 'UTF-8');
                        $nameSuffixHtml = (string)($it['name_suffix_html'] ?? ''); // raw HTML allowed (e.g., count anchor)
                        $status = (string)($it[$statusKey] ?? 'on');
                        $isOn = ($status === 'Y' || $status === '1' || strtolower($status) === 'on' || strtolower($status) === 'active' || strtolower($status) === 'true');
                        $openKey = '';
                        if ($expandable) {
                            $rk = (string)($it[$openKeyField] ?? '');
                            if ($rk !== '') { $openKey = $openKeyPrefix . ':' . $rk; }
                        }
                        // Icon config (moved to helper)
                        $__icon = self::buildIcon($it, $options['icon'] ?? null);
                        $iconWrapCls = $__icon['wrapClass'];
                        $iconSvgHtml = $__icon['svgHtml'];
                        // Build optional Pattern A self-refresh attributes for the row wrapper
                        $rowHxAttrsStr = '';
                        $rowRefreshUrl = '';
                        if ($rowRefreshUrlOpt !== null) {
                            if (is_callable($rowRefreshUrlOpt)) {
                                try { $rowRefreshUrl = (string)call_user_func($rowRefreshUrlOpt, $it, $id); } catch (\Throwable $e) { $rowRefreshUrl = ''; }
                            } else {
                                $rowRefreshUrl = (string)$rowRefreshUrlOpt;
                            }
                            if ($rowRefreshUrl !== '') {
                                $rowRefreshUrl = str_replace(['{id}','%7Bid%7D'], (string)$id, $rowRefreshUrl);
                                // Determine events to listen to
                                $rowEvents = [];
                                if ($rowRefreshEventsOpt !== null) {
                                    $rowEvents = is_array($rowRefreshEventsOpt) ? $rowRefreshEventsOpt : array_map('trim', explode(',', (string)$rowRefreshEventsOpt));
                                } elseif ($eventPrefix !== '') {
                                    $rowEvents = [ $eventPrefix . ':updated' ];
                                }
                                // Build hx-trigger string with per-id filter and from:body
                                $trigs = [];
                                foreach ($rowEvents as $ev) {
                                    $ev = trim((string)$ev);
                                    if ($ev === '') { continue; }
                                    $trigs[] = $ev . "[detail.id == '" . \Components\Base::h((string)$id) . "'] from:body";
                                }
                                // Build hx-select CSS selector and escape ':' which has special meaning in CSS
                                $hxSelectId = '#item-' . $openKey;
                                $hxSelectEsc = str_replace(':', '\\:', $hxSelectId);
                                $rowHxAttrsStr = ' hx-get="' . htmlspecialchars($rowRefreshUrl, ENT_QUOTES, 'UTF-8') . '"'
                                    . ' hx-swap="outerHTML"'
                                    . ' hx-select="' . htmlspecialchars($hxSelectEsc, ENT_QUOTES, 'UTF-8') . '"'
                                    . ($trigs ? ' hx-trigger="' . htmlspecialchars(implode(', ', $trigs), ENT_QUOTES, 'UTF-8') . '"' : '')
                                    . ' data-no-global-loader="1"'
                                    . ' hx-disinherit="*"';
                            }
                        }
                        ?>
                        <div  id="item-<?php echo \Components\Base::h($openKey); ?>"<?= $rowHxAttrsStr; ?>
                                class="relative hover:-translate-y-0 shadow-xs hover:ring-2 ring-4 ring-inset ring-white transition-all bg-elevated hover:bg-canvas rounded-xl overflow-hidden border-0 border-divider dark:bg-zinc-800 <?php echo ($expandable && $openKey !== '') ? 'cursor-pointer select-none' : ''; ?>"
                             <?php if ($expandable && $openKey !== ''): ?>
                                 @click="toggle('<?php echo \Components\Base::h($openKey); ?>')"
                                 data-no-global-loader="1" data-no-local-loader="1"
                             <?php endif; ?>
                        >
                            <div class="flex items-start justify-between p-3">
                                <div class="group/clickhint truncate relative flex flex-grow items-center space-x-4 leading-none <?php echo $isOn ? '' : 'grayscale opacity-50'; ?>">
                                    <div class="<?php echo htmlspecialchars($iconWrapCls, ENT_QUOTES, 'UTF-8'); ?>"><?php echo $iconSvgHtml; ?></div>
                                    <div class="flex flex-col justify-between min-w-0">
                                        <div class="text-sm text-primary font-medium">
                                            <?php echo $name; ?>
                                            <?php if ($nameSuffixHtml !== ''): ?>
                                                <span class="ml-2 text-xs text-secondary/70">(<?php echo $nameSuffixHtml; ?>)</span>
                                            <?php endif; ?>
                                        </div>
                                        <div class="text-xs text-secondary/70">#<?php echo (int)$id; ?><?php echo $isOn ? ' · Active' : ''; ?></div>
                                    </div>
                                    <?php if ($expandable && $openKey !== ''): ?>
                                        <!-- Hover overlay shown on card hover, only over the left content area (not over controls) -->
                                        <div x-cloak x-show="!openMap['<?php echo \Components\Base::h($openKey); ?>']" class="pointer-events-none absolute inset-0 flex items-center justify-end px-6 opacity-0 group-hover/clickhint:opacity-100 transition-opacity delay-500 duration-100">
                                            <div class="px-2 py-1 text-xs font-medium rounded-md bg-primary/30 text-white shadow-sm ring-1 ring-white/20">Click to open</div>
                                        </div>
                                    <?php endif; ?>
                                </div>
                                <div class="shrink-0 self-center flex items-center gap-3">
                                    <?php
                                    // Support multiple badges. Backwards compatible with single 'status_badge'.
                                    // Final order:
                                    // 1) explicit per-item 'status_badge' or auto scheduleState() (if provided)
                                    // 2) additional 'badges' provided by the item (array of strings/configs)
                                    $badgeHtmlList = [];

                                    // Helper to render a badge config array
                                    $renderBadge = function(array $cfg) {
                                        $bLabel = (string)($cfg['label'] ?? '');
                                        $bLabelHtml = array_key_exists('label_html', $cfg) ? (string)$cfg['label_html'] : null; // raw
                                        $bTitle = (string)($cfg['title'] ?? ($bLabel !== '' ? $bLabel : ''));
                                        // Support either explicit utility classes via 'class' or Palette token via 'color'
                                        $bClassRaw = (string)($cfg['class'] ?? '');
                                        $bColor = (string)($cfg['color'] ?? '');
                                        $bAria = (string)($cfg['aria'] ?? ($bLabel !== '' ? $bLabel : (is_string($bLabelHtml) ? strip_tags($bLabelHtml) : '')));
                                        $bIcon = (string)($cfg['icon'] ?? ''); // optional raw svg to prefix
                                        $bIconClass = trim((string)($cfg['icon_class'] ?? 'size-3 mr-1 -ml-0.5'));

                                        // If neither label nor label_html present, skip
                                        if ($bLabel === '' && !is_string($bLabelHtml)) { return ''; }

                                        // Resolve classes
                                        if ($bClassRaw !== '') {
                                            $bClass = $bClassRaw; // caller supplied full utility classes
                                        } elseif ($bColor !== '') {
                                            // Use Palette classes for the provided token or utilities string
                                            $bClass = Palette::classes($bColor, 'gray');
                                        } else {
                                            // Default filled style
                                            $bClass = 'bg-slate-600 text-white';
                                        }

                                        // Ensure readable text when caller only supplied bg-* utilities
                                        if ($bClass !== '' && stripos($bClass, 'text-') === false) {
                                            $bClass .= ' text-white';
                                        }

                                        $titleEsc = \Components\Base::h($bTitle);
                                        $ariaEsc = \Components\Base::h($bAria);
                                        $classEsc = \Components\Base::h($bClass);

                                        // Prepare inner HTML (optional icon prefix)
                                        $inner = is_string($bLabelHtml) ? $bLabelHtml : \Components\Base::h($bLabel);
                                        if ($bIcon !== '' && strpos($bIcon, '<svg') !== false) {
                                            $iconHtml = $bIcon;
                                            // ensure a small size class is present on the svg
                                            if (strpos($iconHtml, 'class=') !== false) {
                                                $iconHtml = preg_replace('/class=\"([^\"]*)\"/i', 'class="$1 ' . \Components\Base::h($bIconClass) . '"', $iconHtml, 1);
                                            } else {
                                                $iconHtml = preg_replace('/<svg\b/i', '<svg class="' . \Components\Base::h($bIconClass) . '"', $iconHtml, 1);
                                            }
                                            $inner = $iconHtml . ($inner !== '' ? ' ' . $inner : '');
                                        }

                                        return <<<HTML
<div class="shrink-0" @click.stop role="status" title="$titleEsc" aria-label="$ariaEsc">
  <div class="inline-flex items-center transform rounded-full px-1.5 text-xs ring-2 ring-white dark:ring-gray-900 $classEsc">$inner</div>
</div>
HTML;
                                    };

                                    // 1) status badge or schedule state fallback
                                    $badgeCfg = $it['status_badge'] ?? null;
                                    if (is_string($badgeCfg)) {
                                        $badgeHtmlList[] = $badgeCfg; // raw HTML provided by caller
                                    } elseif (is_array($badgeCfg)) {
                                        $html = $renderBadge($badgeCfg);
                                        if ($html !== '') { $badgeHtmlList[] = $html; }
                                    } elseif (isset($it['__model']) && is_object($it['__model']) && method_exists($it['__model'], 'scheduleState')) {
                                        try {
                                            $st = $it['__model']->scheduleState();
                                            $state = isset($st['state']) ? (string)$st['state'] : 'active';
                                            $title = isset($st['label']) ? (string)$st['label'] : ucfirst($state);
                                            $short = ucfirst($state);
                                            // Map state to color classes (Tailwind utilities included in safelist)
                                            $bg = ($state === 'active') ? 'bg-green-600' : (($state === 'pending') ? 'bg-orange-600' : 'bg-slate-600');
                                            $badgeHtmlList[] = $renderBadge([
                                                'label' => $short,
                                                'title' => $title,
                                                'class' => $bg,
                                            ]);
                                        } catch (\Throwable $e) {
                                            // no-op
                                        }
                                    }

                                    // 2) Additional badges
                                    $moreBadges = $it['badges'] ?? [];
                                    if (is_array($moreBadges)) {
                                        foreach ($moreBadges as $b) {
                                            if (is_string($b)) {
                                                $badgeHtmlList[] = $b;
                                            } elseif (is_array($b)) {
                                                $html = $renderBadge($b);
                                                if ($html !== '') { $badgeHtmlList[] = $html; }
                                            }
                                        }
                                    }

                                    echo implode("\n", $badgeHtmlList);
                                    ?>
                                    <?php
                                    // Render a consolidated "more" dropdown using the same pattern as StepCard
                                    if (!empty($rowActions)) {
                                        $dropdownItems = [];
                                        foreach ($rowActions as $action) {
                                            if (!is_array($action)) { continue; }
                                            $aAttrs = $action['attrs'] ?? [];
                                            // Prevent toggling when clicking inside the dropdown
                                            if ($expandable && (!isset($aAttrs['@click.stop']) && !isset($aAttrs['@click']))) {
                                                $aAttrs['@click.stop'] = '';
                                            }
                                            // Support URL shortcut with {id} replacement
                                            if (isset($action['url']) && !isset($aAttrs['hx-get'])) {
                                                $url = (string)$action['url'];
                                                $repl = (string)$id;
                                                $url = str_replace('{id}', $repl, $url);
                                                $url = str_ireplace('%7Bid%7D', $repl, $url);
                                                $isDrawer = (substr($panelId, -6) === 'drawer');
                                                if ($isDrawer && stripos($url, 'drawer=') === false) {
                                                    $url .= (strpos($url, '?') === false ? '?' : '&') . 'drawer=1';
                                                }
                                                $aAttrs['hx-get'] = $url;
                                            }
                                            // Default modal + htmx targets
                                            if (!isset($aAttrs['command'])) { $aAttrs['command'] = 'show-modal'; }
                                            if (!isset($aAttrs['commandfor'])) { $aAttrs['commandfor'] = $panelId; }
                                            if (!isset($aAttrs['hx-target'])) { $aAttrs['hx-target'] = '#' . $panelTargetId; }
                                            if (!isset($aAttrs['hx-swap'])) { $aAttrs['hx-swap'] = 'innerHTML'; }

                                            // Optional href passthrough with {id} replacement
                                            $href = null;
                                            if (isset($action['href'])) {
                                                $href = (string)$action['href'];
                                                $href = str_replace('{id}', (string)$id, $href);
                                                $href = str_ireplace('%7Bid%7D', (string)$id, $href);
                                            }

                                            $dropdownItems[] = [
                                                'label' => (string)($action['label'] ?? 'Action'),
                                                'icon' => (string)($action['icon'] ?? ''),
                                                'attrs' => $aAttrs,
                                                'href' => $href,
                                                'divider' => (bool)($action['divider'] ?? false),
                                            ];
                                        }

                                        if (!empty($dropdownItems)) {
                                            $triggerAttrs = [
                                                'type' => 'button',
                                                'class' => 'inline-flex items-center font-medium text-center rounded-lg text-sm px-3 py-2 bg-transparent hover:bg-gray-100 text-secondary!',
                                                'aria-label' => 'More',
                                            ];
                                            if ($expandable) { $triggerAttrs['@click.stop'] = ''; }
                                            echo \Components\Dropdown::render([
                                                'trigger_tag' => 'button',
                                                'trigger_attrs' => $triggerAttrs,
                                                'trigger_html' => \Components\Base::SVG_ELLIPSIS_HORIZONTAL,
                                                'items' => $dropdownItems,
                                                'anchor' => 'bottom end',
                                                'popover' => 'manual',
                                                'wrapper_class' => 'inline-flex relative',
                                            ]);
                                        }
                                    }
                                    ?>
                                    <?php if ($configAction):
                                        $cAttrs = $configAction['attrs'] ?? [];
                                        // Prevent toggling when clicking the config button
                                        if ($expandable && (!isset($cAttrs['@click.stop']) && !isset($cAttrs['@click']))) {
                                            $cAttrs['@click.stop'] = '';
                                        }
                                        // Support URL shortcut with {id} replacement
                                        if (isset($configAction['url']) && !isset($cAttrs['hx-get'])) {
                                            $cUrl = (string)$configAction['url'];
                                            $repl = (string)$id;
                                            $cUrl = str_replace('{id}', $repl, $cUrl);
                                            $cUrl = str_ireplace('%7Bid%7D', $repl, $cUrl);
                                            $isDrawer = (substr($panelId, -6) === 'drawer');
                                            if ($isDrawer && stripos($cUrl, 'drawer=') === false) {
                                                $cUrl .= (strpos($cUrl, '?') === false ? '?' : '&') . 'drawer=1';
                                            }
                                            $cAttrs['hx-get'] = $cUrl;
                                        }
                                        if (!isset($cAttrs['command'])) { $cAttrs['command'] = 'show-modal'; }
                                        if (!isset($cAttrs['commandfor'])) { $cAttrs['commandfor'] = $panelId; }
                                        if (!isset($cAttrs['hx-target'])) { $cAttrs['hx-target'] = '#' . $panelTargetId; }
                                        if (!isset($cAttrs['hx-swap'])) { $cAttrs['hx-swap'] = 'innerHTML'; }
                                        $cVariant = (string)($configAction['variant'] ?? 'ghost');
                                        $cSize = (string)($configAction['size'] ?? 'sm');
                                        $cIcon = (string)($configAction['icon'] ?? \Components\Base::SVG_GEAR);
                                        $cClass = (string)($configAction['class'] ?? 'text-secondary! ');
                                        echo \Components\Button::render('', [
                                            'size' => $cSize,
                                            'variant' => $cVariant,
                                            'icon' => $cIcon,
                                            'class' => $cClass,
                                            'attrs' => $cAttrs,
                                        ]);
                                    endif; ?>
                                    <?php if ($expandable && $openKey !== ''): ?>
                                        <div class="flex items-center text-secondary! w-6">
                                            <span x-cloak x-show="openMap['<?php echo \Components\Base::h($openKey); ?>']">
                                                <?= \Components\Base::SVG_CHEVRON_UP; ?>
                                            </span>
                                            <span x-show="!openMap['<?php echo \Components\Base::h($openKey); ?>']">
                                                <?= \Components\Base::SVG_CHEVRON_DOWN; ?>
                                            </span>
                                        </div>
                                    <?php endif; ?>
                                </div>
                            </div>
                            <?php if ($expandable && $openKey !== ''): ?>
                                <?php
                                // Optional Pattern A self-refresh attributes for expandable wrapper
                                $expandHxAttrsStr = '';
                                $expandRefreshUrl = '';
                                if ($expandRefreshUrlOpt !== null) {
                                    if (is_callable($expandRefreshUrlOpt)) {
                                        try { $expandRefreshUrl = (string)call_user_func($expandRefreshUrlOpt, $it, $id); } catch (\Throwable $e) { $expandRefreshUrl = ''; }
                                    } else {
                                        $expandRefreshUrl = (string)$expandRefreshUrlOpt;
                                    }
                                    if ($expandRefreshUrl !== '') {
                                        $expandRefreshUrl = str_replace(['{id}','%7Bid%7D'], (string)$id, $expandRefreshUrl);
                                        // Determine events
                                        $expEvents = [];
                                        if ($expandRefreshEventsOpt !== null) {
                                            $expEvents = is_array($expandRefreshEventsOpt) ? $expandRefreshEventsOpt : array_map('trim', explode(',', (string)$expandRefreshEventsOpt));
                                        } elseif ($eventPrefix !== '') {
                                            // Important: do NOT listen to ":updated" by default for expandable areas.
                                            // The surrounding row typically refreshes on ":updated" and re-renders this
                                            // expandable wrapper including its lazy loader, which would then immediately
                                            // fetch the expandable content again, causing a duplicate request. To avoid
                                            // double-fetching (observed for newsletter_item_flow), we only listen to a
                                            // dedicated "...:expandable:refresh" event here. If a caller truly needs to
                                            // refresh the expandable area directly on update, it can pass
                                            // expand_refresh_events explicitly in options.
                                            $expEvents = [ $eventPrefix . ':expandable:refresh' ];
                                        }
                                        $trigs2 = [];
                                        foreach ($expEvents as $ev2) {
                                            $ev2 = trim((string)$ev2);
                                            if ($ev2 === '') { continue; }
                                            $trigs2[] = $ev2 . "[detail.id == '" . \Components\Base::h((string)$id) . "'] from:body";
                                        }
                                        // Target the inner content container so the structural wrapper
                                        // (pattern bg, scroller, paddings) remains intact when refreshing.
                                        // Replacing the wrapper's innerHTML directly breaks layout.
                                        $hxTargetId = '#expandable-content-' . $openKey;
                                        $hxTargetEsc = str_replace(':', '\\:', $hxTargetId);
                                        $expandHxAttrsStr = ' hx-get="' . htmlspecialchars($expandRefreshUrl, ENT_QUOTES, 'UTF-8') . '"'
                                            . ' hx-target="' . htmlspecialchars($hxTargetEsc, ENT_QUOTES, 'UTF-8') . '" hx-swap="innerHTML"'
                                            . ($trigs2 ? ' hx-trigger="' . htmlspecialchars(implode(', ', $trigs2), ENT_QUOTES, 'UTF-8') . '"' : '')
                                            . ' data-no-global-loader="1"'
                                            . ' hx-disinherit="*"';
                                    }
                                }
                                ?>
                                <div id="expandable-<?php echo \Components\Base::h($openKey); ?>"<?= $expandHxAttrsStr; ?>
                                     x-cloak
                                     x-show="openMap['<?php echo \Components\Base::h($openKey); ?>']"
                                     x-collapse.duration.100ms
                                     @click.stop
                                     x-data="{
                                        canScrollX: false,
                                        atStart: true,
                                        atEnd: false,
                                        update() {
                                            const el = this.$refs.scroller || this.$el;
                                            const max = Math.max(0, el.scrollWidth - el.clientWidth);
                                            this.canScrollX = el.scrollWidth > (el.clientWidth + 2);
                                            this.atStart = el.scrollLeft <= 1;
                                            this.atEnd = el.scrollLeft >= (max - 1);
                                        },
                                        scrollBy(dx) {
                                            const el = this.$refs.scroller || this.$el;
                                            el.scrollBy({ left: dx, behavior: 'smooth' });
                                        }
                                     }"
                                     x-init="update(); $nextTick(() => update()); setTimeout(() => update(), 60)"
                                     @resize.window.debounce.120="update()"
                                     @mouseOver.debounce.120="update()"
                                     class="<?php echo \Components\Base::h($expandContentClass); ?> <?php echo $isOn ? '' : 'grayscale opacity-50'; ?>">
                                    <?php if ($expandPatternBg): ?>
                                        <!-- Fixed background pattern inside expandable area -->
                                        <svg class="absolute inset-0 rounded-xl size-full stroke-divider/90 dark:stroke-muted/10 pointer-events-none z-0" fill="none" aria-hidden="true">
                                            <defs>
                                                <?php $patternId = 'pattern-' . preg_replace('/[^a-zA-Z0-9_-]/','_', $openKey); ?>
                                                <pattern id="<?php echo \Components\Base::h($patternId); ?>" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
                                                    <path d="M-1 5L5 -1M3 9L8.5 3.5" stroke-width="0.5"></path>
                                                </pattern>
                                            </defs>
                                            <rect stroke="none" fill="url(#<?php echo \Components\Base::h($patternId); ?>)" width="100%" height="100%"></rect>
                                        </svg>
                                    <?php endif; ?>
                                    <!-- Horizontal scroll hint overlays (fixed within the expand area) -->
                                    <div x-cloak x-show="canScrollX && !atStart" x-transition.opacity
                                         class="pointer-events-none absolute inset-y-0 left-0 flex items-center z-20">
                                        <button type="button" @click.stop="scrollBy(-280)"
                                                class="pointer-events-auto inline-flex items-center justify-center w-6 h-full bg-secondary/20 text-white shadow-sm ring-1 ring-white/20 hover:bg-secondary/30 focus:outline-none">
                                            <!-- left chevron -->
                                            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
                                                <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
                                            </svg>
                                        </button>
                                    </div>
                                    <div x-cloak x-show="canScrollX && !atEnd" x-transition.opacity
                                         class="pointer-events-none absolute inset-y-0 right-0 flex items-center z-20">
                                        <button type="button" @click.stop="scrollBy(280)"
                                                class="pointer-events-auto inline-flex items-center justify-center w-6 h-full bg-secondary/20 text-white shadow-sm ring-1 ring-white/20 hover:bg-secondary/30 focus:outline-none">
                                            <!-- right chevron -->
                                            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
                                                <path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
                                            </svg>
                                        </button>
                                    </div>
                                    <!-- Inner horizontal scroller: content scrolls; scrollbar stays here -->
                                    <div x-ref="scroller" @scroll.passive="update()" class="relative overflow-x-auto overflow-y-hidden">
                                        <div id="expandable-content-<?php echo \Components\Base::h($openKey); ?>" class="inline-block relative z-10 p-8">
                                            <?php
                                            // Lazy-loading support
                                            $lazyUrl = '';
                                            if ($expandLazyUrlOpt !== null) {
                                                if (is_callable($expandLazyUrlOpt)) {
                                                    try { $lazyUrl = (string)call_user_func($expandLazyUrlOpt, $it, $id); } catch (\Throwable $e) { $lazyUrl = ''; }
                                                } else {
                                                    $lazyUrl = (string)$expandLazyUrlOpt;
                                                }
                                                if ($lazyUrl !== '') {
                                                    $lazyUrl = str_replace(['{id}','%7Bid%7D'], (string)$id, $lazyUrl);
                                                    $hxAttrs = array_merge([
                                                        'hx-get' => $lazyUrl,
                                                        'hx-trigger' => 'revealed once',
                                                        'hx-swap' => $expandLazySwap,
                                                        'hx-target' => 'this',
                                                        'data-no-global-loader' => '1',
                                                    ], $expandLazyAttrs);
                                                    echo '<div class="min-h-10" ' . self::renderAttrs($hxAttrs) . '>' . self::flowLoader() .'</div>';
                                                }
                                            }
                                            if ($lazyUrl === '') {
                                                // Render immediate body
                                                if (is_callable($expandContent)) {
                                                    try {
                                                        echo (string)call_user_func($expandContent, $it, $id);
                                                    } catch (\Throwable $e) {
                                                        echo '<div class="text-xs text-red-600">' . \Components\Base::h($e->getMessage()) . '</div>';
                                                    }
                                                } elseif ($expandInclude !== '') {
                                                    $item = $it; // provide $item in include scope
                                                    try { include $expandInclude; } catch (\Throwable $e) {
                                                        echo '<div class="text-xs text-red-600">' . \Components\Base::h($e->getMessage()) . '</div>';
                                                    }
                                                } else {
                                                    echo '<div class="text-sm text-secondary">No content here</div>';
                                                }
                                            }
                                            ?>
                                        </div>
                                    </div>
                                </div>
                            <?php endif; ?>
                        </div>
                    <?php endforeach; ?>
                </div>
            <?php endif; ?>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
     * Render HTML attributes from an associative array.
     * Supports hx_get => hx-get shorthand.
     */
    private static function renderAttrs(array $attrs): string
    {
        $out = '';
        foreach ($attrs as $k => $v) {
            if ($v === null || $v === false) { continue; }
            if ($k === 'hx_get') { // allow shorthand
                $k = 'hx-get';
            }
            $out .= ' ' . htmlspecialchars((string)$k, ENT_QUOTES, 'UTF-8') . '="' . htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8') . '"';
        }
        return $out;
    }

    /**
     * Return button classes by variant name.
     */
    private static function btnClasses(string $variant): string
    {
        if ($variant === 'primary') {
            return 'inline-flex items-center rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-white hover:bg-primary/90';
        }
        // secondary (default)
        return 'inline-flex items-center rounded-md border border-gray-200 bg-white px-2.5 py-1 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-slate-800 dark:text-gray-200';
    }

    /**
     * Build icon wrapper classes and adjusted SVG HTML.
     * Accepts item-level overrides and optional global defaults (string svg or array config).
     *
     * @param array $item
     * @param mixed $globalIconOpt
     * @return array{wrapClass:string, svgHtml:string}
     */
    private static function buildIcon(array $item, $globalIconOpt): array
    {
        // Global defaults and per-item overrides
        $iconDefaults = is_array($globalIconOpt) ? $globalIconOpt : (is_string($globalIconOpt) ? ['svg' => $globalIconOpt] : []);
        $itIconOpt = $item['icon'] ?? null;
        $itIcon = is_array($itIconOpt) ? $itIconOpt : (is_string($itIconOpt) ? ['svg' => $itIconOpt] : []);

        $iconSvg = (string)($item['icon_svg'] ?? ($itIcon['svg'] ?? ($iconDefaults['svg'] ?? '')));
        $iconShape = strtolower((string)($item['icon_shape'] ?? ($itIcon['shape'] ?? ($iconDefaults['shape'] ?? 'square'))));
        $iconVariant = strtolower((string)($item['icon_variant'] ?? ($itIcon['variant'] ?? ($iconDefaults['variant'] ?? 'solid'))));
        $iconColor = (string)($item['icon_color'] ?? ($itIcon['color'] ?? ($iconDefaults['color'] ?? 'bg-indigo-500 text-white')));
        $iconSize = strtolower((string)($item['icon_size'] ?? ($itIcon['size'] ?? ($iconDefaults['size'] ?? 'md'))));

        // Compute size classes
        $wrapSz = ($iconSize === 'sm') ? 'size-8 p-1.5' : 'size-10 p-2';
        $svgSz = 'size-6';
        $shapeCls = ($iconShape === 'square') ? 'rounded-lg' : 'rounded-full';
        $baseWrapCls = 'flex items-center justify-center shrink-0 ' . $shapeCls . ' ' . $wrapSz . ' shadow-sm';

        // Resolve color classes (accept utility strings directly)
        $iconColorTrim = trim($iconColor);
        $isUtility = $iconColorTrim !== '' && (preg_match('/\b(bg|text|from|to|via|fill|stroke|ring|border)-/i', $iconColorTrim) || strpos($iconColorTrim, ' ') !== false);
        if ($isUtility) {
            $colorCls = $iconColorTrim;
        } else {
            if ($iconVariant === 'solid') {
                $bgDot = Palette::dotBg($iconColorTrim !== '' ? $iconColorTrim : 'indigo', 'indigo');
                $colorCls = trim($bgDot . ' text-white');
            } else {
                $colorCls = Palette::classes($iconColorTrim !== '' ? $iconColorTrim : 'indigo', 'indigo');
            }
        }
        $iconWrapCls = trim($baseWrapCls . ' ' . $colorCls);

        $iconSvgHtml = $iconSvg;
        if ($iconSvgHtml !== '' && strpos($iconSvgHtml, '<svg') !== false) {
            if (strpos($iconSvgHtml, 'class=') !== false) {
                $iconSvgHtml = preg_replace('/class=\"([^\"]*)\"/i', 'class="$1 ' . htmlspecialchars($svgSz, ENT_QUOTES, 'UTF-8') . '"', $iconSvgHtml, 1);
            } else {
                $iconSvgHtml = preg_replace('/<svg\b/i', '<svg class="' . htmlspecialchars($svgSz, ENT_QUOTES, 'UTF-8') . '"', $iconSvgHtml, 1);
            }
        }

        return [
            'wrapClass' => $iconWrapCls,
            'svgHtml' => $iconSvgHtml,
        ];
    }

    private static function flowLoader() {
        // Show the loader by default, but allow callers to explicitly suppress it
        // by passing a URL flag on refresh renders (e.g. row refresh via _row endpoint).
        // This avoids relying on HX-Request, which may be present on both initial and
        // refresh requests in this app's architecture.
        if (!empty($_REQUEST['no_loader'])) {
            return '';
        }
        ob_start();
        ?>
        <!-- placeholder -->
        <div id="placeholder" class="flex items-center shrink max-w-full py-2">
            <div class="relative bg-white ring ring-muted shrink-0 text-left rounded-full p-1.5 pr-3 space-x-2 leading-none">
                <div class="relative animate-pulse  flex items-center space-x-2">
                    <div class="relative flex items-center justify-center rounded-full size-8 p-1.5 bg-muted"></div>
                    <div class="flex flex-col justify-between min-w-0 h-8 pt-0.5  ">
                        <div class="block bg-muted h-3 w-24 rounded-2xl" ></div>
                        <div class="block bg-muted h-3 w-36 rounded-2xl" ></div>
                    </div>
                </div>
            </div>
            <div class="flex flex-row items-center justify-center flex-1">
                <div class="h-px bg-muted flex-1 min-w-[24px]"></div>
            </div>
            <div class="relative flex items-center bg-white ring ring-muted shrink-0 text-left rounded-lg p-1.5 pr-3 space-x-2 leading-none">
                <div class="relative animate-pulse  flex items-center space-x-2">

                <div class="relative flex items-center justify-center rounded-md size-8 p-1.5 bg-muted"></div>
                <div class="flex flex-col justify-between min-w-0 h-8 pt-0.5 ">
                    <div class="block bg-muted h-3 w-12 rounded-2xl" ></div>
                    <div class="block bg-muted h-3 w-18 rounded-2xl" ></div>
                </div>
                </div>

            </div>
        </div>
        <!-- /placeholder -->
    <?php
        return ob_get_clean();
    }

}
