<?php
namespace Components\Resources;

use Components\Support\Palette;
use Components\Ui\EmptyState;

/**
 * Generic, reusable list/table presentation for simple CRUD collections.
 * All inline HTML is captured with output buffering and returned as a string.
 *
 * Options:
 * - title: string heading (optional)
 * - items: array of associative arrays (each item at least has id/name; configurable via keys below)
 * - id_key: string key for id in items (default 'id')
 * - label_key: string key for label/title in items (fallback to name_key then 'name')
 * - name_key: string deprecated, use label_key instead
 * - sublabel_key: string key for sublabel (optional, fallback to #id · Active)
 * - status_key: string key for status (optional, used for default sublabel)
 * - outer_attrs: string raw attributes to inject on the outermost container (e.g., id="..." hx-swap-oob="...")
 * - empty_text: string to show when items is empty
 * - panel_type: 'modal' | 'drawer' (default 'modal') — controls default targets for actions
 * - panel_id: element id of the modal/drawer to target (default 'config-modal' for modal, 'config-drawer' for drawer)
 * - panel_target_id: id of the content container inside the panel (default: panel_id + '-body')
 * - new_action: array with:
 *     - label: string button label
 *     - url: string href/hx-get URL (can contain {id} placeholder; unused for new)
 *     - attrs: array of extra attributes [key => value]
 *     - variant: 'primary'|'secondary' (affects classes)
 * - row_actions: array of actions like new_action but {id} in url will be replaced
 *     - For modal/drawer actions, attributes typically include [ 'command' => 'show-modal', 'commandfor' => 'config-modal'|'config-drawer', 'hx-target' => '#config-*-body', 'hx-swap' => 'innerHTML', 'hx-get' => 'URL' ]
 *     - You may also pass 'hx_get' key as shorthand for setting hx-get
 * - menu_model_class: string FQCN of a model that can provide CRUD menu items (optional)
 * - menu_self_route: string router name to include in menu item context (optional)
 * - menu_label: string label for the per-row dropdown trigger (default: UI_ACTIONS/Actions)
 * - show_header: bool whether to show table column headers (default: true if columns present)
 * - name_label: string custom label for the first column (default: Name)
 * - columns: array of associative arrays for additional columns:
 *     - key: string key to read from item
 *     - label: string header label
 *     - class: string extra classes for cell and header (e.g. responsive hidden)
 *     - header_class: string extra classes for header only
 *     - render: callable function($val, $item) to customize output
 *     - type: 'text' | 'identity' (default 'text')
 *
 * - hide_name_column: bool whether to hide the default first identity column (default: false)
 *
 * Optional icon per row (before the name):
 * - icon (array, defaults for all rows) with keys:
 *     - svg: string raw SVG markup (required to show icon)
 *     - shape: 'round'|'square' (default 'round')
 *     - color: palette token (blue|indigo|orange|yellow|green|gray|purple|red|pink|brand) or Tailwind utilities
 *     - variant: 'tint'|'solid' (default 'tint') — tint uses bg-100 + text-600; solid uses bg-500 + text-white
 *     - size: 'sm'|'md' (default 'md')
 * - Alternatively, per-item overrides:
 *     - item['icon_svg'] or item['icon']['svg']
 *     - item['icon_shape'] or item['icon']['shape']
 *     - item['icon_color'] or item['icon']['color']
 *     - item['icon_variant'] or item['icon']['variant']
 *     - item['icon_size'] or item['icon']['size']
 */
class ResourceList
{
    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');
        $labelKey = (string)($options['label_key'] ?? $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'] : [];
        $columns = is_array($options['columns'] ?? null) ? $options['columns'] : [];
        $pagination = is_array($options['pagination'] ?? null) ? $options['pagination'] : null;
        $search = is_array($options['search'] ?? null) ? $options['search'] : null;
        $filters = is_array($options['filters'] ?? null) ? $options['filters'] : [];
        $showHeader = (bool)($options['show_header'] ?? (!empty($columns) || !empty($options['name_label'])));

        // Optional per-row menu provider (model-based)
        $menuModelClass = (string)($options['menu_model_class'] ?? ($options['model_class'] ?? ''));
        $menuSelfRoute = (string)($options['menu_self_route'] ?? ($options['self_route'] ?? ''));
        $menuLabel = (string)($options['menu_label'] ?? \mh_lng('UI_ACTIONS','Actions'));
        $rowActionsAsMenu = (bool)($options['row_actions_as_menu'] ?? false);
        $menuTrigger = (string)($options['menu_trigger'] ?? 'default');

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

        ob_start();
        ?>
        <div <?= $outerAttrs; ?> class="rounded-2xl border border-divider bg-white p-3 dark:border-white/10 dark:bg-slate-900">
            <?= self::renderHeader($title, $newAction, $panelId, $panelTargetId, $search, $filters); ?>
            <?php if (empty($items)): ?>
                <?php
                $emptyState = is_array($options['empty_state'] ?? null) ? $options['empty_state'] : [];
                // Prefer explicit empty_title, else provided in empty_state, else fallback
                $emptyTitle = (string)($options['empty_title'] ?? ($emptyState['title'] ?? \mh_lng('UI_NO_RESULTS','No result found')));
                if (!isset($emptyState['title'])) { $emptyState['title'] = $emptyTitle; }
                if (!isset($emptyState['text'])) { $emptyState['text'] = $emptyText; }
                if (!isset($emptyState['icon']) && ($search || !empty($filters))) { $emptyState['icon'] = \Components\Base::SVG_SEARCH; }

                $hasSearchValue = $search && !empty($search['value']);
                $hasFilterValue = false;
                foreach ($filters as $f) {
                    if (!empty($f['value'])) { $hasFilterValue = true; break; }
                }

                if (!isset($emptyState['action']) && ($hasSearchValue || $hasFilterValue)) {
                    $resetUrl = $search['url'] ?? null;
                    $resetTarget = $search['target'] ?? null;
                    if (!$resetUrl || !$resetTarget) {
                        foreach ($filters as $f) {
                            if (isset($f['url'])) { $resetUrl = $f['url']; }
                            if (isset($f['target'])) { $resetTarget = $f['target']; }
                            if ($resetUrl && $resetTarget) break;
                        }
                    }

                    if ($resetUrl && $resetTarget) {
                        $resetParams = [];
                        $clearKeys = [];
                        if ($search) {
                            // Search is always cleared to an empty string.
                            $resetParams['search'] = '';
                            $clearKeys[] = 'search';
                        }
                        foreach ($filters as $fKey => $f) {
                            $fValue = $f['value'] ?? '';
                            // For numeric filters (like IDs), reset to '0' to avoid 'int' validation errors in MailBeez.
                            // For others, use an empty string.
                            $resetParams[(string)$fKey] = (is_numeric($fValue) && $fValue !== '') ? '0' : '';
                            $clearKeys[] = (string)$fKey;
                        }
                        if ($pagination) {
                            $resetParams['page'] = '1';
                        }
                        if (!empty($clearKeys)) {
                            $resetParams['_clear_filters'] = implode(',', $clearKeys);
                        }

                        $emptyState['action'] = [
                            'label' => \mh_lng('UI_CLEAR_FILTERS', 'Clear search and filters'),
                            'variant' => 'secondary',
                            'attrs' => [
                                'hx-get' => $resetUrl,
                                'hx-target' => $resetTarget,
                                'hx-swap' => 'outerHTML',
                                'hx-vals' => json_encode($resetParams),
                            ]
                        ];
                    }
                }

                echo EmptyState::render($emptyState);
                ?>
            <?php else: ?>
                <div class="mt-4 flow-root">
                    <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
                        <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
                            <table class="relative min-w-full divide-y divide-divider-strong dark:divide-white/10">
                                <?= self::renderColumnsHeader($options, $columns, !empty($options['icon']) || !empty($items[0]['icon']), $showHeader); ?>
                                <tbody class="divide-y divide-divider dark:divide-white/10">
                                    <?php foreach ($items as $it): ?>
                                        <?= self::renderItemRow(
                                            $it,
                                            $options,
                                            $idKey,
                                            $labelKey,
                                            $statusKey,
                                            $panelId,
                                            $panelTargetId,
                                            $menuModelClass,
                                            $menuSelfRoute,
                                            $menuLabel,
                                            $rowActions,
                                            $columns,
                                            $rowActionsAsMenu,
                                            $menuTrigger
                                        ); ?>
                                    <?php endforeach; ?>
                                </tbody>
                            </table>
                        </div>
                    </div>
                </div>
                <?= self::renderPagination($pagination); ?>
            <?php endif; ?>
        </div>
        <?php
        return ob_get_clean();
    }

    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';
        }
        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';
    }

    private static function renderAttrs(array $attrs): string
    {
        $out = '';
        foreach ($attrs as $k => $v) {
            if ($v === null || $v === false) { continue; }
            if ($k === 'hx_get') { $k = 'hx-get'; }
            $out .= ' ' . htmlspecialchars((string)$k, ENT_QUOTES, 'UTF-8') . '="' . htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8') . '"';
        }
        return $out;
    }

    private static function addDrawerParam(string $url, string $panelId): string
    {
        // Append drawer=1 when targeting a drawer panel and param is absent
        $isDrawer = (substr($panelId, -6) === 'drawer');
        if (!$isDrawer) { return $url; }
        if ($url === '' || stripos($url, 'drawer=') !== false) { return $url; }
        $sep = (strpos($url, '?') === false) ? '?' : '&';
        return $url . $sep . 'drawer=1';
    }

    private static function normalizePanelAttrs(array $attrs, string $panelId, string $panelTargetId): array
    {
        if (!isset($attrs['command'])) { $attrs['command'] = 'show-modal'; }
        if (!isset($attrs['commandfor'])) { $attrs['commandfor'] = $panelId; }
        if (!isset($attrs['hx-target'])) { $attrs['hx-target'] = '#' . $panelTargetId; }
        if (!isset($attrs['hx-swap'])) { $attrs['hx-swap'] = 'innerHTML'; }
        // If hx-get already present and panel is a drawer, ensure drawer=1 is included
        if (isset($attrs['hx-get'])) {
            $attrs['hx-get'] = self::addDrawerParam((string)$attrs['hx-get'], $panelId);
        }
        return $attrs;
    }

    private static function renderHeader(string $title, ?array $newAction, string $panelId, string $panelTargetId, ?array $search = null, array $filters = []): string
    {
        ob_start();
        ?>
        <div class="flex items-center justify-between mb-3 gap-4">
            <div class="flex items-center gap-4 flex-1 min-w-0">
<!--                <div class="text-sm font-semibold text-gray-900 dark:text-gray-100 shrink-0">--><?php //echo \Components\Base::h($title); ?><!--</div>-->
                <div class="flex items-center gap-2 flex-1">
                    <?php if ($search):
                        $searchValue = (string)($search['value'] ?? '');
                        $searchPlaceholder = (string)($search['placeholder'] ?? \mh_lng('UI_SEARCH', 'Search...'));
                        $searchAttrs = is_array($search['attrs'] ?? null) ? $search['attrs'] : [];
                        if (!isset($searchAttrs['hx-trigger'])) { $searchAttrs['hx-trigger'] = 'keyup changed delay:500ms, search'; }
                        if (!isset($searchAttrs['hx-target']) && isset($search['target'])) { $searchAttrs['hx-target'] = $search['target']; }
                        if (!isset($searchAttrs['hx-get']) && isset($search['url'])) { $searchAttrs['hx-get'] = $search['url']; }
                        if (!isset($searchAttrs['hx-include'])) { $searchAttrs['hx-include'] = 'closest div'; }
                        // Help HTMX focus preservation with a stable ID if one is provided or can be derived
                        if (!isset($searchAttrs['id'])) {
                            $searchId = (string)($search['id'] ?? '');
                            if ($searchId === '' && isset($search['target'])) {
                                $targetId = ltrim((string)$search['target'], '#');
                                if ($targetId !== '') { $searchId = $targetId . '-search'; }
                            }
                            if ($searchId !== '') { $searchAttrs['id'] = $searchId; }
                        }
                        echo \Components\Form\Input::render('search', 'Search', $searchValue, [
                            'no_label' => true,
                            'container_class' => 'flex-1 max-w-xs min-w-[120px]',
                            'prefix_icon_html' => \Components\Base::SVG_SEARCH,
                            'show_reset' => true,
                            'input_type' => 'search',
                            'input_attrs' => 'placeholder="' . \Components\Base::h($searchPlaceholder) . '" ' . self::renderAttrs($searchAttrs),
                        ]);
                        ?>
                    <?php endif; ?>

                    <?php if (!empty($filters)): ?>
                        <div class="flex items-center gap-1 ml-auto shrink-0">
                            <?php foreach ($filters as $fKey => $f):
                                $fValue = (string)($f['value'] ?? '');
                                $fOptions = is_array($f['options'] ?? null) ? $f['options'] : [];
                                $fAttrs = is_array($f['attrs'] ?? null) ? $f['attrs'] : [];
                                if (!isset($fAttrs['hx-trigger'])) { $fAttrs['hx-trigger'] = 'change delay:50ms'; }
                                if (!isset($fAttrs['hx-target']) && isset($f['target'])) { $fAttrs['hx-target'] = $f['target']; }
                                if (!isset($fAttrs['hx-get']) && isset($f['url'])) { $fAttrs['hx-get'] = $f['url']; }
                                if (!isset($fAttrs['hx-include'])) { $fAttrs['hx-include'] = 'closest div'; }

                                $component = (string)($f['component'] ?? ($f['rich'] ?? false ? 'rich_select' : ''));

                                if ($component === 'rich_select') {
                                    // Enrich options with CollectionIdentity when identity/dot_class/icon is provided
                                    $hasIdentity = false;
                                    foreach ($fOptions as $opt) {
                                        if (is_array($opt) && (isset($opt['identity']) || isset($opt['dot_class']) || isset($opt['icon']))) { $hasIdentity = true; break; }
                                    }
                                    $rich = [];
                                    foreach ($fOptions as $val => $opt) {
                                        if (is_array($opt) && array_key_exists('subheader', $opt)) {
                                            $rich[$val] = ['subheader' => (string)$opt['subheader']];
                                            continue;
                                        }
                                        $rawLabel = is_array($opt) ? (string)($opt['label'] ?? $opt['text'] ?? $val) : (string)$opt;
                                        $isEmpty = ($val === '' || $val === '0' || $val === 0);

                                        // Requirement: if no value just show a filter name e.g. "Templates", no dot
                                        if ($isEmpty && isset($f['label'])) {
                                            $rawLabel = $f['label'];
                                        }

                                        if ($hasIdentity && is_array($opt) && !$isEmpty && (isset($opt['identity']) || isset($opt['dot_class']) || isset($opt['icon']))) {
                                            $identity = $opt['identity'] ?? (string)($opt['dot_class'] ?? 'bg-gray-400');
                                            $identityOpts = [];
                                            if (isset($opt['icon'])) { $identityOpts['icon'] = $opt['icon']; }
                                            if (isset($opt['icon_class'])) { $identityOpts['icon_class'] = $opt['icon_class']; }
                                            if (isset($opt['size'])) { $identityOpts['size'] = $opt['size']; }
                                            if (isset($opt['dot_size_class'])) { $identityOpts['dot_size_class'] = $opt['dot_size_class']; }
                                            $labelHtml = \Components\Ui\CollectionIdentity::render(\Components\Base::h($rawLabel), $identity, $identityOpts);
                                            $rich[$val] = [ 'label' => $rawLabel, 'label_html' => $labelHtml, 'description' => (string)($opt['description'] ?? $opt['desc'] ?? '') ];
                                        } else {
                                            $rich[$val] = ['label' => $rawLabel];
                                            if (is_array($opt) && isset($opt['description'])) { $rich[$val]['description'] = $opt['description']; }
                                        }
                                    }
                                    $fOptions = $rich;

                                    echo \Components\Form\RichSelect::render($fKey, $fKey, $fOptions, $fValue, [
                                        'no_label' => true,
                                        'variant' => 'ghost',
                                        'container_class' => 'shrink-0',
                                        'input_attrs' => self::renderAttrs($fAttrs),
                                    ]);
                                } else {
                                    // Handle empty label override for standard Select
                                    if (isset($f['label'])) {
                                        foreach ($fOptions as $val => $opt) {
                                            $isEmpty = ($val === '' || $val === '0' || $val === 0);
                                            if ($isEmpty) {
                                                if (is_array($fOptions[$val])) {
                                                    $fOptions[$val]['label'] = $f['label'];
                                                } else {
                                                    $fOptions[$val] = $f['label'];
                                                }
                                            }
                                        }
                                    }
                                    echo \Components\Form\Select::render($fKey, $fKey, $fOptions, $fValue, [
                                        'no_label' => true,
                                        'variant' => 'ghost',
                                        'container_class' => 'shrink-0',
                                        'include_empty' => false,
                                        'input_attrs' => self::renderAttrs($fAttrs),
                                    ]);
                                }
                                ?>
                            <?php endforeach; ?>
                        </div>
                    <?php endif; ?>
                </div>
            </div>
            <?php if ($newAction):
                $naAttrs = $newAction['attrs'] ?? [];
                $naAttrs = self::normalizePanelAttrs($naAttrs, $panelId, $panelTargetId);
                if (isset($newAction['url'])) { $naAttrs['hx-get'] = self::addDrawerParam((string)$newAction['url'], $panelId); }
                $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' => 'md',
                    'attrs' => $naAttrs,
                ]);
            endif; ?>
        </div>
        <?php
        return ob_get_clean();
    }

    public static function computeIconHtml(array $options, array $it): string
    {
        // Icon config (global defaults + per-item overrides)
        $iconOpt = $options['icon'] ?? null;
        $iconDefaults = is_array($iconOpt) ? $iconOpt : (is_string($iconOpt) ? ['svg' => $iconOpt] : []);
        $itIconOpt = $it['icon'] ?? null;
        $itIcon = is_array($itIconOpt) ? $itIconOpt : (is_string($itIconOpt) ? ['svg' => $itIconOpt] : []);
        $iconSvg = (string)($it['icon_svg'] ?? ($itIcon['svg'] ?? ($iconDefaults['svg'] ?? '')));
        $iconShape = strtolower((string)($it['icon_shape'] ?? ($itIcon['shape'] ?? ($iconDefaults['shape'] ?? 'round'))));
        $iconVariant = strtolower((string)($it['icon_variant'] ?? ($itIcon['variant'] ?? ($iconDefaults['variant'] ?? 'tint'))));
        $iconColor = (string)($it['icon_color'] ?? ($itIcon['color'] ?? ($iconDefaults['color'] ?? '')));
        $iconSize = strtolower((string)($it['icon_size'] ?? ($itIcon['size'] ?? ($iconDefaults['size'] ?? 'md'))));
        // Compute size classes
        $wrapSz = ($iconSize === 'sm') ? 'size-8 p-1.5' : 'size-10 p-2';
        $svgSz = ($iconSize === 'sm') ? 'size-6' : 'size-6';
        $shapeCls = ($iconShape === 'square') ? 'rounded-lg' : 'rounded-full';
        $baseWrapCls = (($iconShape === 'round') ? 'relative ' : '') . 'flex items-center justify-center shrink-0 ' . $shapeCls . ' ' . $wrapSz;
        if ($iconShape === 'square') { $baseWrapCls .= ' shadow-sm'; }
        // Resolve color classes
        $iconColor = trim($iconColor);
        $isUtility = $iconColor !== '' && (preg_match('/\b(bg|text|from|to|via|fill|stroke|ring|border)-/i', $iconColor) || strpos($iconColor, ' ') !== false);
        if ($isUtility) {
            $colorCls = $iconColor;
        } else {
            if ($iconVariant === 'solid') {
                $bgDot = Palette::dotBg($iconColor !== '' ? $iconColor : 'gray', 'gray');
                $colorCls = trim($bgDot . ' text-white');
            } else {
                $colorCls = Palette::classes($iconColor !== '' ? $iconColor : 'gray', 'gray');
            }
        }
        $iconWrapCls = trim($baseWrapCls . ' ' . $colorCls);
        // Inject a size class into the SVG if possible
        $iconSvgHtml = $iconSvg;
        if ($iconSvgHtml !== '') {
            if (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);
                }
            }
        }
        if ($iconSvgHtml === '') { return ''; }
        return '<div class="' . htmlspecialchars($iconWrapCls, ENT_QUOTES, 'UTF-8') . '">' . $iconSvgHtml . '</div>';
    }

    private static function resolveMenuItems(string $menuModelClass, string $menuSelfRoute, int $id, array $it, string $panelId, string $panelTargetId): array
    {
        $menuItems = [];
        if ($menuModelClass !== '') {
            try {
                $ctx = [
                    'mode'      => 'list',
                    'id'        => $id,
                    'values'    => $it,
                    'selfRoute' => $menuSelfRoute,
                ];
                if (class_exists($menuModelClass)) {
                    if (method_exists($menuModelClass, 'getCrudMenuItems')) {
                        /** @phpstan-ignore-next-line */
                        $menu = $menuModelClass::getCrudMenuItems($ctx);
                        if (is_array($menu)) { $menuItems = $menu; }
                    } elseif (method_exists($menuModelClass, 'crudMenuItems')) {
                        /** @phpstan-ignore-next-line */
                        $menu = $menuModelClass::crudMenuItems($ctx);
                        if (is_array($menu)) { $menuItems = $menu; }
                    } else {
                        // Try instance method fallback if the model supports it
                        try {
                            $inst = new $menuModelClass($it);
                            if (method_exists($inst, 'getCrudMenuItems')) {
                                $menu = $inst->getCrudMenuItems($ctx);
                                if (is_array($menu)) { $menuItems = $menu; }
                            } else {
                                // Static property fallback
                                $rc = new \ReflectionClass($menuModelClass);
                                $defaults = $rc->getDefaultProperties();
                                $prop = $defaults['crudMenu'] ?? ($defaults['menu'] ?? null);
                                if (is_array($prop)) { $menuItems = $prop; }
                            }
                        } catch (\Throwable $e) { /* ignore */ }
                    }
                }
            } catch (\Throwable $e) { /* ignore */ }
        }

        if (!empty($menuItems)) {
            foreach ($menuItems as $miIdx => $mi) {
                if (!is_array($mi)) continue;
                $hrefI = isset($mi['href']) ? (string)$mi['href'] : '';
                $attrsI = is_array($mi['attrs'] ?? null) ? $mi['attrs'] : [];
                if ($hrefI === '') {
                    $attrsI = self::normalizePanelAttrs($attrsI, $panelId, $panelTargetId);
                }
                if (isset($attrsI['hx_get']) && !isset($attrsI['hx-get'])) {
                    $attrsI['hx-get'] = $attrsI['hx_get'];
                    unset($attrsI['hx_get']);
                }
                $menuItems[$miIdx]['attrs'] = $attrsI;
            }
        }
        return $menuItems;
    }

    private static function renderRowActions(array $rowActions, int $id, string $panelId, string $panelTargetId): string
    {
        ob_start();
        foreach ($rowActions as $action):
            $aAttrs = $action['attrs'] ?? [];
            if (isset($action['url'])) {
                $url = (string)$action['url'];
                $repl = (string)$id;
                $url = str_replace('{id}', $repl, $url);
                $url = str_ireplace('%7Bid%7D', $repl, $url);
                $aAttrs['hx-get'] = self::addDrawerParam($url, $panelId);
            }
            $aAttrs = self::normalizePanelAttrs($aAttrs, $panelId, $panelTargetId);
            $variant = (string)($action['variant'] ?? 'secondary');
            ?>
            <button type="button" class="<?php echo self::btnClasses($variant); ?>"<?= self::renderAttrs($aAttrs); ?>>
                <?php echo \Components\Base::h((string)($action['label'] ?? 'Action')); ?>
            </button>
            <?php
        endforeach;
        return ob_get_clean();
    }

    private static function renderColumnsHeader(array $options, array $columns, bool $hasIcon, bool $showHeader = true): string
    {
        if (!$showHeader) { return ''; }
        ob_start();
        $nameLabel = (string)($options['name_label'] ?? \mh_lng('UI_NAME', 'Name'));
        $hideNameColumn = (bool)($options['hide_name_column'] ?? false);
        ?>
        <thead>
            <tr>
                <?php if (!$hideNameColumn): ?>
                    <th scope="col" class="sticky top-0 z-10 bg-white/75 py-3.5 pr-3 pl-4 text-left text-sm font-semibold text-gray-900 backdrop-blur-sm backdrop-filter dark:bg-slate-900/75 dark:text-gray-100 sm:pl-0">
                        <?php echo \Components\Base::h($nameLabel); ?>
                    </th>
                <?php endif; ?>
                <?php foreach ($columns as $idx => $col):
                    $colClass = (string)($col['header_class'] ?? $col['class'] ?? '');
                    $isFirst = $hideNameColumn && $idx === 0;
                    $thClass = $isFirst
                        ? "sticky top-0 z-10 bg-white/75 py-3.5 pr-3 pl-4 text-left text-sm font-semibold text-gray-900 backdrop-blur-sm backdrop-filter dark:bg-slate-900/75 dark:text-gray-100 sm:pl-0"
                        : "sticky top-0 z-10 bg-white/75 px-3 py-3.5 text-left text-sm font-semibold text-gray-900 backdrop-blur-sm backdrop-filter dark:bg-slate-900/75 dark:text-gray-100";
                ?>
                    <th scope="col" class="<?php echo $thClass; ?> <?php echo $colClass; ?>">
                        <?php echo \Components\Base::h($col['label'] ?? ''); ?>
                    </th>
                <?php endforeach; ?>
                <th scope="col" class="sticky top-0 z-10 bg-white/75 py-3.5 pr-4 pl-3 backdrop-blur-sm backdrop-filter dark:bg-slate-900/75 sm:pr-0">
                    <span class="sr-only"><?php echo \mh_lng('UI_ACTIONS', 'Actions'); ?></span>
                </th>
            </tr>
        </thead>
        <?php
        return ob_get_clean();
    }

    private static function renderPagination(?array $pagination): string
    {
        if (!$pagination) { return ''; }
        $total = (int)($pagination['total'] ?? 0);
        $perPage = (int)($pagination['per_page'] ?? 10);
        $currentPage = (int)($pagination['current_page'] ?? 1);
        $totalPages = (int)ceil($total / $perPage);
        if ($totalPages <= 1) { return ''; }

        $baseUrl = (string)($pagination['url'] ?? '');
        $target = (string)($pagination['target'] ?? '');
        $attrs = is_array($pagination['attrs'] ?? null) ? $pagination['attrs'] : [];
        if ($target !== '' && !isset($attrs['hx-target'])) { $attrs['hx-target'] = $target; }

        $renderLink = function($page, $label, $active = false, $disabled = false) use ($baseUrl, $attrs) {
            $pageUrl = $baseUrl;
            if (strpos($pageUrl, '{page}') !== false) {
                $pageUrl = str_replace('{page}', (string)$page, $pageUrl);
            } else {
                $sep = (strpos($pageUrl, '?') === false) ? '?' : '&';
                $pageUrl .= $sep . 'page=' . $page;
            }

            $cls = 'relative inline-flex items-center px-3 py-2 text-sm font-semibold ring-1 ring-inset ring-gray-300 focus:z-20 focus:outline-offset-0 dark:ring-white/10 ';
            if ($active) {
                $cls .= 'z-10 bg-primary text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary';
            } else {
                $cls .= 'text-gray-900 hover:bg-gray-50 focus:z-20 dark:text-gray-200 dark:hover:bg-white/5';
            }

            if ($disabled) {
                return '<span class="' . $cls . ' opacity-50 cursor-not-allowed">' . $label . '</span>';
            }

            $linkAttrs = $attrs;
            $linkAttrs['hx-get'] = $pageUrl;
            return '<button type="button" class="' . $cls . '"' . self::renderAttrs($linkAttrs) . '>' . $label . '</button>';
        };

        ob_start();
        ?>
        <div class="mt-4 flex items-center justify-between border-t border-gray-200 pt-4 dark:border-white/10">
            <div class="flex flex-1 justify-between sm:hidden">
                <?= $renderLink($currentPage - 1, \mh_lng('UI_PREVIOUS', 'Previous'), false, $currentPage <= 1); ?>
                <?= $renderLink($currentPage + 1, \mh_lng('UI_NEXT', 'Next'), false, $currentPage >= $totalPages); ?>
            </div>
            <div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
                <div>
                    <p class="text-sm text-gray-700 dark:text-gray-400">
                        <?php echo sprintf(\mh_lng('UI_PAGINATION_SHOWING_X_TO_Y_OF_Z', 'Showing <span class="font-medium">%d</span> to <span class="font-medium">%d</span> of <span class="font-medium">%d</span> results'),
                            ($currentPage - 1) * $perPage + 1,
                            min($currentPage * $perPage, $total),
                            $total
                        ); ?>
                    </p>
                </div>
                <div>
                    <nav class="isolate inline-flex -space-x-px rounded-md shadow-xs" aria-label="Pagination">
                        <?= $renderLink($currentPage - 1, '
                            <span class="sr-only">' . \mh_lng('UI_PREVIOUS', 'Previous') . '</span>
                            <svg class="size-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" /></svg>
                        ', false, $currentPage <= 1); ?>

                        <?php
                        $range = 2;
                        $dotsBefore = false;
                        $dotsAfter = false;
                        for ($i = 1; $i <= $totalPages; $i++):
                            if ($i == 1 || $i == $totalPages || ($i >= $currentPage - $range && $i <= $currentPage + $range)):
                                echo $renderLink($i, (string)$i, $i == $currentPage);
                            elseif ($i < $currentPage - $range && !$dotsBefore):
                                echo '<span class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-300 focus:outline-offset-0 dark:text-gray-400 dark:ring-white/10">...</span>';
                                $dotsBefore = true;
                            elseif ($i > $currentPage + $range && !$dotsAfter):
                                echo '<span class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-300 focus:outline-offset-0 dark:text-gray-400 dark:ring-white/10">...</span>';
                                $dotsAfter = true;
                            endif;
                        endfor;
                        ?>

                        <?= $renderLink($currentPage + 1, '
                            <span class="sr-only">' . \mh_lng('UI_NEXT', 'Next') . '</span>
                            <svg class="size-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>
                        ', false, $currentPage >= $totalPages); ?>
                    </nav>
                </div>
            </div>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
     * Renders the identity block (icon + label + sublabel).
     *
     * @param array $it Item data
     * @param array $options ResourceList options (for id_key, label_key, sublabel_key, status_key, icon)
     * @return string
     */
    public static function renderIdentity(array $it, array $options): string
    {
        // Resolve label
        $labelKey = $options['label_key'] ?? $options['name_key'] ?? 'name';
        $label = \Components\Base::h((string)($it[$labelKey] ?? ''), false);

        // Resolve sublabel
        $sublabelKey = $options['sublabel_key'] ?? null;
        if ($sublabelKey) {
            $sublabel = \Components\Base::h((string)($it[$sublabelKey] ?? ''), false);
        }

        $iconHtml = self::computeIconHtml($options, $it);

        ob_start();
        ?>
        <div class="flex items-center gap-3">
            <?php echo $iconHtml; ?>
            <div class="min-w-0 flex-1">
                <div class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate"><?php echo $label; ?></div>
                <div class="text-xs text-gray-500 dark:text-gray-400"><?php echo $sublabel; ?></div>
            </div>
        </div>
        <?php
        return ob_get_clean();
    }

    private static function renderItemRow(
        array $it,
        array $options,
        string $idKey,
        string $labelKey,
        string $statusKey,
        string $panelId,
        string $panelTargetId,
        string $menuModelClass,
        string $menuSelfRoute,
        string $menuLabel,
        array $rowActions,
        array $columns = [],
        bool $rowActionsAsMenu = false,
        string $menuTrigger = 'default'
    ): string {
        $id = (int)($it[$idKey] ?? 0);

        // Resolve model-defined CRUD menu items for this row
        $menuItems = self::resolveMenuItems($menuModelClass, $menuSelfRoute, $id, $it, $panelId, $panelTargetId);

        if ($rowActionsAsMenu && !empty($rowActions)) {
            if (!empty($menuItems)) {
                $menuItems[] = ['divider' => true];
            }
            foreach ($rowActions as $action) {
                $aAttrs = $action['attrs'] ?? [];
                if (isset($action['url'])) {
                    $url = (string)$action['url'];
                    $repl = (string)$id;
                    $url = str_replace('{id}', $repl, $url);
                    $url = str_ireplace('%7Bid%7D', $repl, $url);
                    $aAttrs['hx-get'] = self::addDrawerParam($url, $panelId);
                }
                $aAttrs = self::normalizePanelAttrs($aAttrs, $panelId, $panelTargetId);

                $menuItems[] = [
                    'label' => (string)($action['label'] ?? 'Action'),
                    'attrs' => $aAttrs,
                    'icon' => $action['icon'] ?? null,
                ];
            }
        }

        ob_start();
        $hideNameColumn = (bool)($options['hide_name_column'] ?? false);
        ?>
        <tr>
            <?php if (!$hideNameColumn): ?>
                <td class="py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 dark:text-gray-100 sm:pl-0">
                    <?php echo self::renderIdentity($it, $options); ?>
                </td>
            <?php endif; ?>

            <?php foreach ($columns as $idx => $col):
                $type = $col['type'] ?? 'text';
                $colClass = (string)($col['class'] ?? '');
                $isFirst = $hideNameColumn && $idx === 0;

                if ($type === 'identity') {
                    $val = self::renderIdentity($it, $options);
                } else {
                    $val = (string)($it[$col['key'] ?? ''] ?? '');
                    $render = $col['render'] ?? null;
                    if (is_callable($render)) {
                        $val = (string)call_user_func($render, $val, $it);
                    } else {
                        $val = \Components\Base::h($val);
                    }
                }

                $tdClass = $isFirst
                    ? "py-4 pr-3 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 dark:text-gray-100 sm:pl-0"
                    : "px-3 py-4 text-sm whitespace-nowrap " . ($type === 'identity' ? 'text-gray-900 dark:text-gray-100 font-medium' : 'text-gray-500 dark:text-gray-400');
                ?>
                <td class="<?php echo trim($tdClass . ' ' . $colClass); ?>">
                    <?php echo $val; ?>
                </td>
            <?php endforeach; ?>

            <td class="py-4 pr-4 pl-3 text-right text-sm font-medium whitespace-nowrap sm:pr-0">
                <div class="flex items-center justify-end gap-2">
                    <?php if (!empty($menuItems)) {
                        $dropdownOpts = [
                            'label' => $menuLabel,
                            'items' => $menuItems,
                            'trigger_attrs' => [
                                'class' => 'inline-flex w-full justify-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20'
                            ],
                        ];
                        if ($menuTrigger === 'ellipsis') {
                            $dropdownOpts['trigger_html'] = \Components\Base::SVG_ELLIPSIS_HORIZONTAL;
                            $dropdownOpts['trigger_attrs']['class'] = 'flex items-center rounded-full p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300';
                        }
                        echo \Components\Dropdown::render($dropdownOpts);
                    } ?>
                    <?php if (!$rowActionsAsMenu) {
                        echo self::renderRowActions($rowActions, $id, $panelId, $panelTargetId);
                    } ?>
                </div>
            </td>
        </tr>
        <?php
        return ob_get_clean();
    }
}
