<?php
namespace Components\Resources;

use Components\Support\Palette;
use Components\ConfigStore;
use Components\Router;
use Components\Ui\FlowHelpers;
use Components\Form\Toggle; // align with trigger_module controls
use Components\Ui\Card;
use Components\Ui\ListLayout;
use Components\Ui\GroupSection;
use Components\Ui\FilterPills;

/**
 * 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'] ?? \mh_lng('UI_NO_ENTRIES_FOUND', 'No entries found.'));
        $emptyHtmlOpt = $options['empty_html'] ?? null; // optional raw HTML/callable override for empty state
        $emptyHtml = '';
        if ($emptyHtmlOpt !== null) {
            try {
                if (is_callable($emptyHtmlOpt)) {
                    $emptyHtml = (string)call_user_func($emptyHtmlOpt, $options);
                } else {
                    $emptyHtml = (string)$emptyHtmlOpt;
                }
            } catch (\Throwable $e) {
                $emptyHtml = '';
            }
        }
        $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;

        // Optional inline toggle control (aligned with trigger_module)
        // Usage example:
        //   'toggle' => [
        //       'value_key' => 'status',         // defaults to status_key
        //       'value' => true|false,           // overrides value_key when provided
        //       'name' => 'status',              // form field name (default: 'status')
        //       'size' => 'sm',                  // toggle size
        //       'active_color' => 'green',       // active color token
        //       'wrapper_id' => 'toggle-{id}',   // optional wrapper id pattern (supports {id} replacement)
        //       'input_attrs' => 'hx-post="..." hx-trigger="change" ...', // raw attributes for the <input> (supports {id} replacement)
        //       'disabled_key' => 'toggle_disabled', // optional per-row boolean flag to disable the control
        //   ]
        $toggleOpt = is_array($options['toggle'] ?? null) ? $options['toggle'] : 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);
        $expandPatternBgType = (string)($options['expand_pattern_type'] ?? 'dots'); // 'lines' or 'dots'
        $expandRefreshButton = (bool)($options['expand_refresh_button'] ?? false);
        $refreshType = (string)($options['refresh_type'] ?? 'newsletter_item');

        // Grouping support (e.g., for Audience lists):
        // options['group'] = [
        //   'by' => 'group_key' | callable($item):string,   // how to derive group label from each item
        //   'label' => 'source',                            // used in the header toggle label ("Group by <label>")
        //   'grouped' => bool,                              // current mode; when true → render grouped sections
        //   'filters' => bool,                               // when false, hide filter pills in ungrouped view
        //   'toggle' => [                                    // optional toggle control to switch grouped/ungrouped
        //       'url' => string,                             // hx-get url to reload the list with toggled state
        //       'target' => string,                          // container id to target with hx-get
        //       'push_url' => string|null,                   // optional hx-push-url
        //       'label' => string|null,                      // optional custom label for "Group by"
        //   ],
        // ]
        $groupOpt = is_array($options['group'] ?? null) ? $options['group'] : null;
        $grouped = false;
        $groupByFn = null; // function(array $item): string
        $groupLabel = 'group';
        $groupFilters = true;
        $containerIdOpt = (string)($options['container_id'] ?? '');
        $headerLeftHtml = '';
        $headerRightHtml = '';
        // Allow callers to inject custom header content (e.g., inline controls)
        $headerLeftHtmlOpt = isset($options['header_left']) ? (string)$options['header_left'] : '';
        $headerRightHtmlOpt = isset($options['header_right']) ? (string)$options['header_right'] : '';
        $groupsMap = [];
        $groupNames = [];
        if ($groupOpt) {
            $grouped = (bool)($groupOpt['grouped'] ?? true);
            $groupFilters = (bool)($groupOpt['filters'] ?? true);
            $groupLabel = (string)($groupOpt['label'] ?? 'group');
            // Optional mapping of raw filter values to human-readable labels
            $groupLabelsMap = is_array($groupOpt['labels_map'] ?? null) ? $groupOpt['labels_map'] : [];
            $by = $groupOpt['by'] ?? null;
            if (is_callable($by)) {
                $groupByFn = $by;
            } elseif (is_string($by) && $by !== '') {
                $key = (string)$by;
                $groupByFn = function(array $row) use ($key): string {
                    $v = $row[$key] ?? '';
                    if ($v === '' && isset($row['__model']) && is_object($row['__model']) && isset($row['__model']->{$key})) {
                        return (string)($row['__model']->{$key});
                    }
                    return (string)$v;
                };
            }

            if ($groupByFn) {
                foreach ($items as $row) {
                    try {
                        $g = trim((string)call_user_func($groupByFn, $row));
                    } catch (\Throwable $e) {
                        $g = '';
                    }
                    if ($g === '') { $g = (string)\mh_lng('UI_OTHER', 'Other'); }
                    if (!isset($groupsMap[$g])) { $groupsMap[$g] = []; }
                    $groupsMap[$g][] = $row;
                }
                $groupNames = array_keys($groupsMap);
                sort($groupNames, SORT_NATURAL | SORT_FLAG_CASE);
            }
        }

        // 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'] ?? \Components\Support\Htmx::resolvePanelId(null, $panelType === 'drawer'));
        $panelTargetId = (string)($options['panel_target_id'] ?? ($panelId . '-body'));

        // Optional: DOM id prefix for the row title element so OOB updates can target it
        $rowTitleIdPrefix = (string)($options['row_title_id_prefix'] ?? '');

        // 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); ?>,
                    // Ungrouped view filter helpers (safe to keep when unused)
                    filterActive: "ALL",
                    setFilter(v) { this.filterActive = v; },
                    isFilter(v) { return this.filterActive === v; },
                    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); },
                    // Group-scoped helpers expected by Ui\GroupSection ExpandControls
                    expandSome(keys) {
                        if (!Array.isArray(keys)) return;
                        keys.forEach(k => { if (Object.prototype.hasOwnProperty.call(this.openMap, k)) { this.openMap[k] = true; } });
                        this.$nextTick(() => this.persistAllOpen());
                    },
                    collapseSome(keys) {
                        if (!Array.isArray(keys)) return;
                        keys.forEach(k => { if (Object.prototype.hasOwnProperty.call(this.openMap, k)) { this.openMap[k] = false; } });
                        this.$nextTick(() => this.persistAllOpen());
                    },
                    allOpenSome(keys) {
                        if (!Array.isArray(keys) || keys.length === 0) return false;
                        return keys.every(k => this.openMap[k] === true);
                    },
                    allClosedSome(keys) {
                        if (!Array.isArray(keys) || keys.length === 0) return false;
                        return keys.every(k => this.openMap[k] === false);
                    }
                }'
                x-ref="root"
            <?php endif; ?>
        >
            <?php
                // Resolve list id for per-user layout; prefer explicit options, then a semantic prefix, then title slug
                // Do NOT fall back to panel id (e.g., 'config-drawer'), as it is unrelated to the logical list identity.
                if (isset($options['list_id']) && (string)$options['list_id'] !== '') {
                    $__listId = (string)$options['list_id'];
                } elseif (isset($options['open_key_prefix']) && (string)$options['open_key_prefix'] !== '') {
                    // Use the open_key_prefix as a stable semantic identifier when available (e.g., 'newsletter-flow')
                    $__listId = (string)$options['open_key_prefix'];
                } else {
                    // Fallback: slugified title
                    $__listId = strtolower(trim(preg_replace('/[^a-z0-9]+/i','-',$title)));
                }
                $__layout = ListLayout::current($__listId);
            ?>
            <?php
                // Build the section content to be wrapped by GroupSection (header + controls unified)
                ob_start();
            ?>

            <?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;
                }
                $naAttrs['data-no-global-loader'] = '1';
                $naAttrs['data-no-local-loader'] = '1';

                echo \Components\Button::render((string)($newAction['label'] ?? \mh_lng('UI_NEW', 'New')),[
                        'variant' => (string)($newAction['variant'] ?? 'primary'),
                        'size' => 'xs',
                        'attrs' => $naAttrs,
                ]);
            endif; ?>

            <?php if ($expandable && $expandControls && !empty($allKeys) && $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 if (empty($items)): ?>
                <?php if (trim($emptyHtml) !== ''): ?>
                    <?php echo $emptyHtml; ?>
                <?php else: ?>
                    <div class="text-sm text-muted"><?php echo \Components\Base::h($emptyText); ?></div>
                <?php endif; ?>
            <?php else: ?>
                <?php
                // Helper to render a list of rows (reuses existing row markup below)
                $___renderRows = function(array $___rows) use (
                    $idKey, $nameKey, $statusKey, $expandable, $openKeyField, $openKeyPrefix,
                    $eventPrefix, $rowRefreshUrlOpt, $rowRefreshEventsOpt, $panelId, $panelTargetId,
                    $toggleOpt, $expandPatternBg, $expandPatternBgType, $expandContent, $expandInclude,
                    $expandRefreshUrlOpt, $expandRefreshEventsOpt, $expandRefreshButton, $refreshType,
                    $rowTitleIdPrefix, $options, $configAction, $expandContentClass,
                    $groupOpt, $groupByFn, $grouped, $rowActions
                ) {
                    ob_start();
                    foreach ($___rows as $it):

                        $id = (int)($it[$idKey] ?? 0);

                        // Resolve placeholders (e.g., {id}, {newsletter_campaign_id}) from item data
                        $search = ['{id}', '%7Bid%7D'];
                        $replace = [(string)$id, (string)$id];
                        foreach ($it as $rowKey => $rowVal) {
                            if (is_scalar($rowVal) && (string)$rowKey !== '') {
                                $search[] = '{' . $rowKey . '}';
                                $search[] = '%7B' . $rowKey . '%7D';
                                $replace[] = (string)$rowVal;
                                $replace[] = (string)$rowVal;
                            }
                        }

                        $name = htmlspecialchars((string)($it[$nameKey] ?? ''), ENT_QUOTES, 'UTF-8');
                        $nameSuffixHtml = (string)($it['name_suffix_html'] ?? ''); // raw HTML allowed (e.g., count anchor)
                        // Optional inline HTML placed directly after the title (e.g., favorite star)
                        $titleAfterHtml = (string)($it['title_after_html'] ?? ''); // raw HTML allowed
                        $status = (string)($it[$statusKey] ?? 'on');
                        // Prefer model-aware scheduleState() when available to derive visual on/off
                        $isOn = null;
                        if (isset($it['__model']) && is_object($it['__model']) && method_exists($it['__model'], 'scheduleState')) {
                            try {
                                $st = $it['__model']->scheduleState();
                                $stateFromModel = isset($st['state']) ? strtolower(trim((string)$st['state'])) : '';
                                // Treat these schedule states as OFF
                                $offStates = ['paused', 'draft', 'ended', 'completed'];
                                $isOn = !in_array($stateFromModel, $offStates, true);
                            } catch (\Throwable $e) {
                                $isOn = null; // fall back to generic status handling
                            }
                        }
                        if ($isOn === null) {
                            // Fallback: Normalize generic status string. Treat paused/draft/ended/completed as OFF regardless.
                            $statusLower = strtolower(trim($status));
                            $isExplicitOff = in_array($statusLower, ['paused', 'draft', 'ended', 'completed'], true);
                            $isExplicitOn  = ($status === 'Y' || $status === '1' || in_array($statusLower, ['on', 'active', 'true'], true));
                            $isOn = (!$isExplicitOff) && $isExplicitOn;
                        }
                        $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'];
                        // Optional compact type label (e.g., Bulk Mail | Scheduled Window | Automation)
                        $typeLabel = isset($it['type_label']) ? trim((string)$it['type_label']) : '';
                        // 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($search, $replace, $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";
                                }
                                // Use an attribute selector to avoid any CSS escaping issues with ':' in ids
                                // Example: [id="item-newsletter-flow:19"]
                                $hxSelectEsc = '[id="' . 'item-' . $openKey . '"]';
                                $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="*"';
                            }
                        }
                        // Per-item filter attributes for ungrouped view: apply x-show on each item
                        $rowFilterAttrs = '';
                        if ($groupOpt && $groupByFn && !$grouped) {
                            try {
                                $__g = trim((string)call_user_func($groupByFn, $it));
                            } catch (\Throwable $e) { $__g = ''; }
                            if ($__g === '') { $__g = (string)\mh_lng('UI_OTHER', 'Other'); }
                            $grpEsc = \Components\Base::h($__g, false);
                            $rowFilterAttrs = ' x-show="filterActive===\'ALL\' || filterActive===$el.dataset.group" data-group="' . $grpEsc . '"';
                        }
                        ?>
                        <div  id="item-<?php echo \Components\Base::h($openKey); ?>"<?= $rowHxAttrsStr; ?><?= $rowFilterAttrs; ?>
                                class="<?php echo \Components\Base::h(Card::baseContainerClass(($expandable && $openKey !== ''))); ?>"
                             <?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 ? '' : ' opacity-80'; ?>">
                                    <div class="<?php echo htmlspecialchars($iconWrapCls, ENT_QUOTES, 'UTF-8'); ?>  <?php echo $isOn ? '' : 'grayscale-0 opacity-80'; ?>"><?php echo $iconSvgHtml; ?></div>
                                    <div class="flex flex-col justify-between min-w-0">
                                        <?php
                                        $rowTitleId = $rowTitleIdPrefix !== '' ? ($rowTitleIdPrefix . (string)$id) : '';
                                        $titleAttr = ' title="ID: #' . (int)$id . '"';
                                        $idAttr = $rowTitleId !== '' ? (' id="' . \Components\Base::h($rowTitleId) . '"') : '';
                                        ?>
                                        <div<?= $idAttr . $titleAttr; ?> class="text-sm text-primary font-medium flex items-center gap-2">
                                            <span><?php echo $name; ?></span>
                                            <?php if ($titleAfterHtml !== ''): ?>
                                                <span class="inline-flex items-center"><?php echo $titleAfterHtml; ?></span>
                                            <?php endif; ?>
                                            <?php if ($nameSuffixHtml !== ''): ?>
                                                <span class="text-xs text-secondary/70">(<?php echo $nameSuffixHtml; ?>)</span>
                                            <?php endif; ?>
                                        </div>
                                        <?php
                                            // Optional extra subline content provided by caller (raw HTML allowed)
                                            $sublineSuffixHtml = isset($it['subline_suffix_html']) ? (string)$it['subline_suffix_html'] : '';
                                            ?>
                                        <div class="text-xs text-secondary/70">
                                          <?php echo \Components\Base::h($typeLabel); ?>
                                        <?php if ($sublineSuffixHtml !== ''): ?> · <?php echo $sublineSuffixHtml; ?><?php endif; ?>
                                        </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"><?php echo \mh_lng('UI_CLICK_TO_OPEN', 'Click to open'); ?></div>
                                        </div>
                                    <?php endif; ?>
                                </div>
                                <div class="shrink-0 self-center flex items-center gap-2" id="controls-<?php echo \Components\Base::h((string)$openKey); ?>">
                                    <?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 using the shared Ui\Badge component
                                    $renderBadge = function(array $cfg) use ($panelId, $panelTargetId, $rowActions) {
                                        // Determine the accessible label (fallback to stripped HTML when only label_html is given)
                                        $label = (string)($cfg['label'] ?? '');
                                        if ($label === '' && isset($cfg['label_html']) && is_string($cfg['label_html'])) {
                                            $label = trim(strip_tags((string)$cfg['label_html']));
                                        }
                                        if ($label === '' && empty($cfg['label_html'])) {
                                            return '';
                                        }

                                        // Align with trigger_module badge sizing and baseline
                                        // Ensure badges are vertically centered with the gear icon.
                                        // We always switch inner to inline-flex + items-center to normalize
                                        // vertical alignment; add small gap only when an icon is present.
                                        $opts = [
                                            'wrapper_class' => 'shrink-0 self-center p-1.5 inline-flex',
                                        ];

                                        if (array_key_exists('label_html', $cfg) && is_string($cfg['label_html'])) {
                                            $opts['content_html'] = (string)$cfg['label_html'];
                                        }
                                        if (!empty($cfg['title'])) { $opts['title'] = (string)$cfg['title']; }
                                        if (!empty($cfg['aria'])) { $opts['aria_label'] = (string)$cfg['aria']; }
                                        if (!empty($cfg['icon'])) { $opts['icon'] = (string)$cfg['icon']; }
                                        if (!empty($cfg['icon_class'])) { $opts['icon_class'] = (string)$cfg['icon_class']; }
                                        if (!empty($cfg['icon_container_class'])) { $opts['icon_container_class'] = (string)$cfg['icon_container_class']; }

                                        // Normalize inner layout: only force inline layout when an icon is present.
                                        // This keeps text-only status_badge using the Badge default (block),
                                        // avoiding visual regressions while still aligning icon badges correctly.
                                        $hasIcon = (!empty($cfg['icon']) || !empty($cfg['icon_class']) || !empty($cfg['icon_container_class']));
                                        if ($hasIcon) {
                                            $opts['inner_class'] = trim(($opts['inner_class'] ?? '') . ' inline-flex items-center gap-1 leading-none');
                                        }

                                        // Class/color handling (prefer explicit class)
                                        if (!empty($cfg['class'])) {
                                            $cls = (string)$cfg['class'];
                                            if (stripos($cls, 'text-') === false) { $cls .= ' text-white'; }
                                            $opts['class'] = trim($cls);
                                        } elseif (!empty($cfg['color'])) {
                                            $color = (string)$cfg['color'];
                                            $opts['color'] = $color;
                                            if (preg_match('/\b(bg|text|fill|stroke|ring|border)-/i', $color) && stripos($color, 'text-') === false) {
                                                // Ensure readable contrast when using solid/bg utility classes
                                                $opts['inner_class'] .= ' text-white';
                                            }
                                        }

                                        // Modal conveniences + custom attrs
                                        if (!empty($cfg['modal_url'])) { $opts['modal_url'] = (string)$cfg['modal_url']; }
                                        if (!empty($cfg['modal_hx_get'])) { $opts['modal_hx_get'] = (string)$cfg['modal_hx_get']; }
                                        if (!empty($cfg['modal_size'])) { $opts['modal_size'] = (string)$cfg['modal_size']; }
                                        $opts['panel_id'] = isset($cfg['panel_id']) && is_string($cfg['panel_id']) && $cfg['panel_id'] !== '' ? (string)$cfg['panel_id'] : $panelId;
                                        $opts['panel_target_id'] = isset($cfg['panel_target_id']) && is_string($cfg['panel_target_id']) && $cfg['panel_target_id'] !== '' ? (string)$cfg['panel_target_id'] : $panelTargetId;
                                        if (isset($cfg['attrs']) && is_array($cfg['attrs'])) { $opts['attrs'] = $cfg['attrs']; }

                                        // Prefer <button> when clickable
                                        $clickable = (!empty($opts['attrs']) || !empty($opts['modal_url']) || !empty($opts['modal_hx_get']));
                                        if ($clickable) { $opts['tag'] = 'button'; }

                                        return \Components\Ui\Badge::render('default', $label, $opts);
                                    };

                                    // 1) status badge or schedule state fallback
                                    $badgeCfg = $it['status_badge'] ?? null;
                                    // Optional stable wrapper id for OOB updates (set by caller)
                                    $statusOobId = isset($it['status_oob_id']) && is_string($it['status_oob_id'])
                                        ? trim((string)$it['status_oob_id']) : '';
                                    if ($statusOobId !== '') {
                                        echo '<div id="' . \Components\Base::h($statusOobId) . '">';
                                    }

                                    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';
                                            // Keep a descriptive title from model state for tooltip
                                            $title = isset($st['label']) ? (string)$st['label'] : ucfirst($state);
                                            // Prefer model-provided localized short labels when available
                                            if (method_exists($it['__model'], 'scheduleStateBadgeLabel')) {
                                                $short = (string)$it['__model']::scheduleStateBadgeLabel($state);
                                            } elseif (isset($st['short_label']) && trim((string)$st['short_label']) !== '') {
                                                $short = (string)$st['short_label'];
                                            } else {
                                                // Legacy-safe fallback
                                                $short = $isOn ? (string)\mh_lng('UI_ACTIVE', 'Active') : (string)\mh_lng('UI_INACTIVE', 'Inactive');
                                            }
                                            // Map state to color classes via shared palette helper
                                            $bg = \Components\Support\Palette::newsletterStateBg($state);
                                            $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);
                                    if ($statusOobId !== '') { echo '</div>'; }
                                    ?>
                                    <?php
                                    // Build a simplified dropdown (ellipsis) combining: Config action and optional Toggle item
                                    $menuItems = [];

                                    // 1) Configuration action as a menu item (if provided)
                                    if ($configAction) {
                                        $cAttrs = $configAction['attrs'] ?? [];
                                        // Prevent row expand/collapse when clicking inside the dropdown
                                        if ($expandable && (!isset($cAttrs['@click.stop']) && !isset($cAttrs['@click']))) {
                                            $cAttrs['@click.stop'] = '';
                                        }
                                        // Resolve placeholders in config_action URL and attrs
                                        if (isset($configAction['url']) && !isset($cAttrs['hx-get'])) {
                                            $cUrl = str_replace($search, $replace, (string)$configAction['url']);
                                            $isDrawer = (substr($panelId, -6) === 'drawer');
                                            if ($isDrawer && stripos($cUrl, 'drawer=') === false) {
                                                $cUrl .= (strpos($cUrl, '?') === false ? '?' : '&') . 'drawer=1';
                                            }
                                            $cAttrs['hx-get'] = $cUrl;
                                        }
                                        foreach ($cAttrs as $k => $v) {
                                            if (is_string($v)) {
                                                $cAttrs[$k] = str_replace($search, $replace, $v);
                                            }
                                        }
                                        // Default modal + HTMX targets
                                        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'; }

                                        $menuItems[] = [
                                            'label' => (string)($configAction['label'] ?? \mh_lng('UI_CONFIGURE', 'Configure')),
                                            'icon'  => (string)($configAction['icon'] ?? \Components\Base::SVG_GEAR),
                                            'attrs' => $cAttrs,
                                        ];
                                    }

                                    // 2) Optional single Toggle item (status-aware like trigger_module)
                                    if ($toggleOpt !== null) {
                                        // Determine ON/OFF from either explicit value or row status
                                        $toggleInitialOn = false;
                                        if (array_key_exists('value', $toggleOpt)) {
                                            $toggleInitialOn = (bool)$toggleOpt['value'];
                                        } else {
                                            $valKey = (string)($toggleOpt['value_key'] ?? $statusKey);
                                            $raw = (string)($it[$valKey] ?? ($isOn ? 'on' : ''));
                                            $toggleInitialOn = ($raw === 'Y' || $raw === '1' || in_array(strtolower($raw), ['on','active','true'], true));
                                        }
                                        $tInputAttrs  = (string)($toggleOpt['input_attrs'] ?? '');
                                        // Optional per-row disabled flag
                                        $tDisabled    = false;
                                        if (isset($toggleOpt['disabled_key']) && is_string($toggleOpt['disabled_key'])) {
                                            $dk = (string)$toggleOpt['disabled_key'];
                                            $tDisabled = !empty($it[$dk]);
                                        }

                                        // Resolve {id} placeholders embedded in raw input attributes
                                        if ($tInputAttrs !== '') {
                                            $tInputAttrs = str_replace($search, $replace, $tInputAttrs);
                                        }
                                        // Parse raw attributes into an array the Dropdown understands
                                        $tAttrs = self::parseAttrsString($tInputAttrs);
                                        if ($expandable && !isset($tAttrs['@click.stop'])) { $tAttrs['@click.stop'] = ''; }
                                        if ($tDisabled) { $tAttrs['disabled'] = 'true'; $tAttrs['aria-disabled'] = 'true'; }

                                        // Ensure HTMX listens on the element itself, independent of event bubbling
                                        // (Dropdown items use onmousedown + @click.stop which can stop propagation).
                                        // Explicit hx-trigger="click" guarantees the request fires when the menu item is clicked.
                                        if (!isset($tAttrs['hx-trigger'])) { $tAttrs['hx-trigger'] = 'click'; }

                                        // Fallback wiring: if no explicit HTMX action was provided via input_attrs,
                                        // derive a sensible default for known resources (e.g., newsletter items).
                                        if (!isset($tAttrs['hx-post']) && !isset($tAttrs['hx-get'])) {
                                            // Allow callers to pass an explicit post URL in toggle options
                                            $fallbackPostUrl = '';
                                            if (!empty($toggleOpt['post_url']) && is_string($toggleOpt['post_url'])) {
                                                $fallbackPostUrl = (string)$toggleOpt['post_url'];
                                            } elseif ($eventPrefix === 'newsletter_item' && $id > 0) {
                                                // Default for NewsletterItem toggles
                                                try {
                                                    $fallbackPostUrl = \Components\Router::url('newsletter_item_toggle', ['partial' => 1, 'id' => (string)$id]);
                                                } catch (\Throwable $e) { $fallbackPostUrl = ''; }
                                            }
                                            if ($fallbackPostUrl !== '') {
                                                $tAttrs['hx-post'] = $fallbackPostUrl;
                                            }
                                        }

                                        // Align with trigger_module behavior:
                                        // - When an HTMX action is present but no explicit target/swap provided,
                                        //   default to replacing the entire row wrapper (outerHTML) so the card updates in place.
                                        if ((isset($tAttrs['hx-post']) || isset($tAttrs['hx-get'])) && !isset($tAttrs['hx-target'])) {
                                            if ($openKey !== '') {
                                                // Prefer attribute selector to avoid CSS escaping pitfalls with ':' in ids
                                                $tAttrs['hx-target'] = '[id="' . 'item-' . $openKey . '"]';
                                                $tAttrs['hx-swap'] = 'outerHTML';
                                            }
                                        }

                                        // If caller provided an hx-target via input_attrs and it uses an id selector (#...)
                                        // convert it to an attribute selector [id="..."] to ensure universal validity.
                                        if (isset($tAttrs['hx-target']) && is_string($tAttrs['hx-target'])) {
                                            $__t = trim((string)$tAttrs['hx-target']);
                                            if (strpos($__t, '#') === 0) {
                                                $tAttrs['hx-target'] = '[id="' . ltrim($__t, '#') . '"]';
                                            }
                                        }
                                        // Prefer no global loader for snappy feel; allow local loader by default (0)
                                        if (!isset($tAttrs['data-no-global-loader'])) { $tAttrs['data-no-global-loader'] = '1'; }
                                        if (!isset($tAttrs['data-no-local-loader'])) { $tAttrs['data-no-local-loader'] = '0'; }

                                        // When currently OFF, mirror trigger_module behavior and include hx-vals
                                        // so the backend receives flow_status=on to activate the item.
                                        if (!$toggleInitialOn && !isset($tAttrs['hx-vals'])) {
                                            $tAttrs['hx-vals'] = json_encode(['flow_status' => 'on']);
                                        }

                                        $menuItems[] = [
                                            'label' => $toggleInitialOn ? \mh_lng('UI_PAUSE', 'Pause') : \mh_lng('UI_ACTIVATE', 'Activate'),
                                            'icon'  => $toggleInitialOn ? \Components\Base::SVG_OUTLINE_PAUSE : \Components\Base::SVG_OUTLINE_PLAY,
                                            'attrs' => $tAttrs,
                                        ];
                                    }

                                    // 3) Row actions (e.g., Move…) appended to the dropdown with a divider above
                                    if (!empty($rowActions)) {
                                        $actionAdded = false;
                                        foreach ($rowActions as $raKey => $ra) {
                                            if (!is_array($ra)) { continue; }
                                            if ($ra['divider'] ?? false) {
                                                $menuItems[] = ['divider' => true];
                                                $actionAdded = true;
                                                continue;
                                            }
                                            $raLabel = (string)($ra['label'] ?? $raKey);
                                            $raAttrs = is_array($ra['attrs'] ?? null) ? $ra['attrs'] : [];
                                            // URL and attrs placeholders already resolved via $search/$replace built above
                                            if (isset($ra['url'])) {
                                                $raUrl = str_replace($search, $replace, (string)$ra['url']);
                                                if (!isset($raAttrs['hx-get'])) {
                                                    $raAttrs['hx-get'] = $raUrl;
                                                }
                                            }
                                            foreach ($raAttrs as $k => $v) {
                                                if (is_string($v)) {
                                                    $raAttrs[$k] = str_replace($search, $replace, $v);
                                                }
                                            }
                                            if ($expandable && !isset($raAttrs['@click.stop'])) {
                                                $raAttrs['@click.stop'] = '';
                                            }
                                            // Add divider before first row action
                                            if (!$actionAdded && !empty($menuItems)) {
                                                $menuItems[] = ['divider' => true];
                                            }
                                            $menuItems[] = [
                                                'label' => $raLabel,
                                                'icon'  => (string)($ra['icon'] ?? ''),
                                                'attrs' => $raAttrs,
                                            ];
                                            $actionAdded = true;
                                        }
                                    }

                                    // Render the dropdown if there are any items
                                    if (!empty($menuItems)) {
                                        // Build the trigger button HTML (icon-only ellipsis)
                                        $triggerBtnHtml = \Components\Button::render('', [
                                            'variant' => 'ghost',
                                            'size' => 'md',
//                                            'rounded' => 'md',
                                            'icon' => \Components\Base::SVG_ELLIPSIS_HORIZONTAL,
                                            'attrs' => [ '@click.stop' => '', 'aria-label' => \mh_lng('UI_MORE', 'More') ],
                                        ]);

                                        echo \Components\Dropdown::render([
                                            // Keep wrapper attrs minimal to avoid nested Alpine state unless caller needs it
                                            'wrapper_attrs' => [],
                                            'trigger_tag' => 'div',
                                            'trigger_html' => $triggerBtnHtml,
                                            'trigger_attrs' => [ '@click.stop' => '' ],
                                            'anchor' => 'bottom end',
                                            'items' => $menuItems,
                                            'wrapper_class' => 'inline-flex relative',
                                        ]);
                                    }
                                    ?>
                                    <?php if ($expandable && $openKey !== ''): ?>
                                        <?php
                                        // Use unified Button UI for expand/collapse indicator (matches trigger_module)
                                        echo \Components\Button::render('', [
                                                'variant' => 'ghost',
                                                'size'    => 'md',
                                                'icon'    =>
                                                        '<span x-cloak x-show="openMap[\'' . \Components\Base::h($openKey) . '\']">' . \Components\Base::SVG_CHEVRON_UP . '</span>' .
                                                        '<span x-show="!openMap[\'' . \Components\Base::h($openKey) . '\']">' . \Components\Base::SVG_CHEVRON_DOWN . '</span>',
                                                'attrs'   => [
                                                'aria-label' => 'Toggle details',
                                                    'data-no-global-loader' => '1',
                                                    'data-no-local-loader' => '1',
                                                ],
                                        ]);
                                        ?>
                                    <?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($search, $replace, $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.
                                        // Use attribute selector form to avoid issues with ':' in ids
                                        $hxTargetEsc = '[id="' . 'expandable-content-' . $openKey . '"]';
                                        $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="<?= \Components\Base::h(FlowHelpers::alpineData(), false); ?>"
                                     x-init="update(); $nextTick(() => update()); setTimeout(() => update(), 60)"
                                     @resize.window.debounce.120="update()"
                                     @mouseOver.debounce.120="update()"
                                     class="<?= Card::flowClasses() ?> <?php echo $isOn ? '' : ''; ?>">
                                    <div x-show="refreshing" x-cloak class="pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-white/50 dark:bg-gray-900/50 transition-opacity duration-100 ease-in-out">
                                        <div class="flex items-center gap-2 text-secondary">
                                            <?= \Components\Base::SVG_SPINNER; ?>
                                        </div>
                                    </div>
                                    <?php if ($expandPatternBg && $expandPatternBgType === 'lines'): ?>
                                        <!-- Fixed background pattern inside expandable area -->
                                        <?php $patternId = 'pattern-' . preg_replace('/[^a-zA-Z0-9_-]/','_', $openKey); ?>
                                        <svg class="absolute inset-0 rounded-2xl size-full stroke-divider/90 dark:stroke-muted/10 pointer-events-none z-0" fill="none" aria-hidden="true">
                                            <defs>
                                                <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; ?>
                                    <?= FlowHelpers::renderScrollHints(); ?>
                                    <?php if ($expandRefreshButton) { echo FlowHelpers::renderRefreshButton($refreshType, (int)$id, $eventPrefix); } ?>
                                    <!-- 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
                                            $rendered = false;
                                            // Advanced rendering policy for NewsletterItem flows:
                                            // - Pre-render only when the item is likely to be inspected (draft/active/scheduled)
                                            // - For other states, lazy-load when the expanded area becomes visible (intersect)
                                            if (isset($it['__model'])) {
                                                $model = $it['__model'];
                                                // Prefer model hint when available
                                                $shouldEager = false;
                                                if (is_object($model) && method_exists($model, 'shouldPreRenderFlow')) {
                                                    try { $shouldEager = (bool)$model->shouldPreRenderFlow(); } catch (\Throwable $e) { $shouldEager = false; }
                                                }
                                                if (is_object($model) && method_exists($model, 'renderFlow')) {
                                                    try {
                                                        if ($shouldEager) {
                                                            // Use cache when present; allow inline budget to decide lazy placeholders for overflow
                                                            echo $model->renderFlow(true, true);
                                                            $rendered = true;
                                                        } else {
                                                            // Lightweight lazy placeholder: fetch flow HTML only when visible inside expanded panel
                                                            try {
                                                                if (is_object($model) && method_exists($model, 'lazyFlowUrl')) {
                                                                    $lazyUrl = (string)$model->lazyFlowUrl((int)$id);
                                                                } else {
                                                                    $lazyUrl = \Components\Router::url('newsletter_item_flow', ['partial' => 1, 'id' => (int)$id]);
                                                                }
                                                            } catch (\Throwable $e) { $lazyUrl = ''; }
                                                            if ($lazyUrl !== '') {
                                                                $lazyUrlEsc = htmlspecialchars($lazyUrl, ENT_QUOTES, 'UTF-8');
                                                                echo '<div hx-get="' . $lazyUrlEsc . '"'
                                                                    . ' hx-trigger="intersect once delay:300ms"'
                                                                    . ' hx-swap="outerHTML" hx-disinherit="*" data-no-global-loader="1">'
                                                                    . self::flowLoader()
                                                                    . '</div>';
                                                                $rendered = true;
                                                            }
                                                        }
                                                    } catch (\Throwable $e) { /* ignore */ }
                                                }
                                            }

                                            if (!$rendered) {
                                                // 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">' . \mh_lng('UI_NO_CONTENT_HERE', 'No content here') . '</div>';
                                                }
                                            }
                                            ?>
                                        </div>
                                    </div>
                                </div>
                            <?php endif; ?>
                        </div>
                    <?php endforeach; return ob_get_clean(); };

                    // If grouping is enabled and active, render per-group sections
                    if ($groupOpt && $groupByFn && $grouped && !empty($groupsMap)) {
                        foreach ($groupsMap as $__grpName => $__grpItems) {
                            // Render rows for this group
                            $__groupContent = $___renderRows($__grpItems);
                            // Compute group-scoped keys and initial states
                            $__gKeys = [];
                            if ($expandable) {
                                foreach ($__grpItems as $__it) {
                                    $rk = (string)($__it[$openKeyField] ?? '');
                                    if ($rk !== '') { $__gKeys[] = $openKeyPrefix . ':' . $rk; }
                                }
                            }
                            $__gKeys = array_values(array_unique($__gKeys));
                            $__groupOpenMap = array_intersect_key($openMap, array_flip($__gKeys));
                            $__grpAllOpenInit = !empty($__groupOpenMap) && !in_array(false, $__groupOpenMap, true);
                            $__grpAllClosedInit = !empty($__groupOpenMap) && !in_array(true, $__groupOpenMap, true);

                            echo GroupSection::render((string)$__grpName, $__groupContent, [
                                'section_class' => 'mb-4 mt-2',
                                'layout_controls' => false,
                                'expand_controls' => ($expandable && $expandControls && !empty($__gKeys)),
                                'expand_group_keys' => $__gKeys,
                                'expand_all_open_init' => $__grpAllOpenInit,
                                'expand_all_closed_init' => $__grpAllClosedInit,
                            ]);
                        }
                    } else {
                        // Ungrouped view: optionally provide filter pills in the header and render all rows
                        if ($groupOpt && $groupByFn && !$grouped && $groupFilters && !empty($groupNames)) {
                            // Pass optional labels map so filter pills show friendly labels
                            $headerLeftHtml = FilterPills::render($groupNames, ['labels_map' => $groupLabelsMap]);
                        }

                        // Render rows flat with optional per-group visibility wrappers
                        if ($groupOpt && $groupByFn && !$grouped && !empty($groupsMap)) {
                            foreach ($groupsMap as $__grpName => $__grpItems) {
                                // Render items directly; each item has x-show + data-group
                                echo $___renderRows($__grpItems);
                            }
                        } else {
                            // No grouping configured → render all items normally
                            echo $___renderRows($items);
                        }
                    }
                ?>
            <?php endif; ?>
            <?php
                $__sectionContent = ob_get_clean();
                // Optional header right toggle button when grouping is available
                if ($groupOpt && isset($groupOpt['toggle']) && is_array($groupOpt['toggle'])) {
                    $tg = $groupOpt['toggle'];
                    $tgUrl = isset($tg['url']) ? (string)$tg['url'] : '';
                    $tgTarget = isset($tg['target']) ? (string)$tg['target'] : $containerIdOpt;
                    $tgPush = isset($tg['push_url']) ? (string)$tg['push_url'] : '';
                    if ($tgUrl !== '' && $tgTarget !== '') {
                        $lbl = $grouped
                            ? (string)\mh_lng('UI_UNGROUP', 'Ungroup')
                            : (string)($tg['label'] ?? \mh_lng('UI_GROUP_BY_SOURCE', 'Group by source'));
                        $attrs = [
                            'hx-get' => $tgUrl,
                            'hx-target' => '#' . $tgTarget,
                            'hx-swap' => 'outerHTML',
                            'data-no-global-loader' => '1',
                        ];
                        if ($tgPush !== '') { $attrs['hx-push-url'] = $tgPush; }
                        $headerRightHtml = \Components\Button::render($lbl, [
                            'size' => 'sm',
                            'variant' => 'ghost',
                            'attrs' => $attrs,
                        ]);
                    }
                }

                // Favor caller-provided header slots when present; append generated helpers after/before
                if ($headerLeftHtmlOpt !== '') { $headerLeftHtml = $headerLeftHtmlOpt . $headerLeftHtml; }
                if ($headerRightHtmlOpt !== '') { $headerRightHtml = $headerRightHtml . $headerRightHtmlOpt; }

                echo GroupSection::render(
                    (string)$title,
                    $__sectionContent,
                    [
                        'list_id' => $__listId,
                        'layout_controls' => true,
                        // Only show outer expand controls when not grouped; grouped mode uses per-section controls
                        'expand_controls' => ($expandable && $expandControls && !empty($allKeys) && (!$groupOpt || !$grouped)),
                        'expand_group_keys' => $allKeys,
                        'expand_all_open_init' => $allOpenInit,
                        'expand_all_closed_init' => $allClosedInit,
                        // Header extras
                        'header_left' => $headerLeftHtml,
                        'header_right' => $headerRightHtml,
                        'title_html' => $options['title_html'] ?? null,
                    ]
                );
            ?>
        </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;
    }

    /**
     * Parse a raw HTML attributes string (e.g. from Toggle input_attrs) into an associative array.
     * Supports both double and single quoted values. Best-effort; ignores malformed tokens.
     *
     * @param string $attrStr
     * @return array<string,string>
     */
    private static function parseAttrsString(string $attrStr): array
    {
        $attrStr = trim($attrStr);
        if ($attrStr === '') { return []; }
        $attrs = [];
        // quoted key/value pairs (supports both double and single quotes)
        $re = '/([A-Za-z0-9_:@.\-\[\]]+)\s*=\s*(["\'])(.*?)\2/';
        if (preg_match_all($re, $attrStr, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $m) { $attrs[$m[1]] = $m[3]; }
        }
        return $attrs;
    }

    /**
     * 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);

        // Apply size to child <svg> via wrapper classes; do not mutate the SVG markup itself
        $iconWrapCls = trim($iconWrapCls . ' ' . '[&_svg]:' . $svgSz);

        $iconSvgHtml = $iconSvg;

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

    public 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-4 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-4 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();
    }

}
