<?php
// components/Form/RichSelect.php
namespace Components\Form;

class RichSelect extends BaseField {
    /**
     * @param array $options map of value => [label=>..., description=>...]
     * @param array $opts    supports: errors, help, input_attrs, label_badge_html, icon (SVG string shown before selected label), variant (ghost), trigger_class
     */
    public static function render(string $name, string $label, array $options = [], $value = '', array $opts = []): string {
        $errors = $opts['errors'] ?? [];
        $help = $opts['help'] ?? '';
        $extra = $opts['input_attrs'] ?? '';
        $hasError = !empty($errors);
        $icon = $opts['icon'] ?? '';

        $nameEsc = self::h($name);
        $labelEsc = self::h($label);

        // Build options normalized structure
        $normalized = [];
        foreach ($options as $val => $def) {
            // Subheader/separator entry
            if (is_array($def) && isset($def['subheader'])) {
                $normalized[] = [
                    'subheader'   => true,
                    'value'       => (string)$val,
                    'label'       => (string)$def['subheader'],
                    'description' => '',
                    'label_html'  => null,
                ];
                continue;
            }
            if (is_array($def)) {
                $text = (string)($def['label'] ?? $def['text'] ?? $val);
                $desc = (string)($def['description'] ?? $def['desc'] ?? '');
                $labelHtml = isset($def['label_html']) ? (string)$def['label_html'] : null;
                $disabled = !empty($def['disabled']);
            } else {
                $text = (string)$def;
                $desc = '';
                $labelHtml = null;
                $disabled = false;
            }
            $normalized[] = [
                'subheader'   => false,
                'value'       => (string)$val,
                'label'       => $text,
                'description' => $desc,
                'label_html'  => $labelHtml,
                'disabled'    => $disabled,
            ];
        }

        // Determine selected label (and optional HTML) for trigger
        $selectedLabel = '';
        $selectedLabelHtml = null;
        foreach ($normalized as $opt) {
            if (!empty($opt['subheader'])) { continue; }
            if ((string)$opt['value'] === (string)$value) {
                $selectedLabel = $opt['label'];
                $selectedLabelHtml = $opt['label_html'] ?? null;
                break;
            }
        }
        if ($selectedLabel === '') {
            // Find first non-subheader entry
            $first = null;
            foreach ($normalized as $opt) {
                if (empty($opt['subheader'])) { $first = $opt; break; }
            }
            $selectedLabel     = $first['label']     ?? 'Select';
            $selectedLabelHtml = $first['label_html'] ?? null;
        }

        $helpHtml  = self::helpHtml($help);
        $errorHtml = self::errorsHtml($errors);
        $clientErr = self::clientErrorHtml($name);
        $valueEsc  = self::h((string)$value);
        $badge     = $opts['label_badge_html'] ?? '';
        $size      = $opts['size'] ?? 'md';

        $sizeClasses = [
            'xs' => 'py-0.5 px-2',
            'sm' => 'py-1 px-2.5',
            'md' => 'py-1.5 px-3',
            'lg' => 'py-2 px-3.5',
        ];
        $sizeClass = $sizeClasses[$size] ?? $sizeClasses['md'];

        if (isset($opts['trigger_class'])) {
            $triggerClass = $opts['trigger_class'];
        } elseif (($opts['variant'] ?? '') === 'ghost') {
            $triggerClass = 'flex items-center justify-between gap-x-2 rounded-md ' . $sizeClass . ' text-base sm:text-sm/6 hover:bg-gray-100 focus:outline-none dark:hover:bg-white/10';
        } else {
            $triggerClass = self::inputClass($hasError);
            $triggerClass = str_replace('block', 'flex items-center justify-between gap-x-2', $triggerClass);
            
            // Adjust padding if not md (since inputClass defaults to py-1.5)
            if ($size === 'xs') {
                $triggerClass = str_replace('py-1.5', 'py-0.5', $triggerClass);
            } elseif ($size === 'sm') {
                $triggerClass = str_replace('py-1.5', 'py-1', $triggerClass);
            } elseif ($size === 'lg') {
                $triggerClass = str_replace('py-1.5', 'py-2', $triggerClass);
            }

            $triggerClass .= ' px-3 hover:bg-gray-50 focus-visible:outline-2 focus-visible:outline-brand-500 dark:hover:bg-white/10';
        }

        $containerClass = (string)($opts['container_class'] ?? '');
        $labelHtml = '';
        if (!($opts['no_label'] ?? false)) {
            $labelHtml = self::renderLabel('', $label, $badge, $opts);
        }

        $inputWrapperClass = $opts['no_label'] ?? false ? '' : 'mt-2';

        // Enable search UI by default for longer lists; can be forced on/off via opts.
        $searchableCount = 0;
        foreach ($normalized as $opt) {
            if (empty($opt['subheader'])) { $searchableCount++; }
        }
        $enableSearch = array_key_exists('search', $opts)
            ? (bool)$opts['search']
            : ($searchableCount > 8);

        $searchPlaceholder = (string)($opts['search_placeholder'] ?? 'Search');

        // Build <el-option> list
        $optsHtml = '';
        foreach ($normalized as $opt) {
            // Render subheader/separator
            if (!empty($opt['subheader'])) {
                $subheaderLabel = self::h($opt['label']);
                $subVal = self::h((string)($opt['value'] ?? '__subheader__'));
                $optsHtml .= <<<HTML
    <el-option value="{$subVal}" disabled aria-disabled="true" role="presentation" tabindex="-1" data-subheader="1" class="block cursor-default px-3 pt-3 pb-1 text-xs font-semibold text-gray-400 uppercase tracking-wide dark:text-gray-500 border-t border-gray-100 dark:border-white/10 mt-1 select-none">
      <span class="block">{$subheaderLabel}</span>
    </el-option>
HTML;
                continue;
            }
            $v = self::h($opt['value']);
            $t = self::h($opt['label']);
            $searchHay = strtolower(trim((string)$opt['label'] . ' ' . (string)$opt['description'] . ' ' . (string)$opt['value']));
            $searchHayEsc = self::h($searchHay);
            $optLabelHtml = $opt['label_html'] ?? null;
            $displayHtml = ($optLabelHtml !== null && $optLabelHtml !== '')
                ? $optLabelHtml
                : '<p class="font-normal group-aria-selected/option:font-semibold in-[el-selectedcontent]:font-semibold">' . $t . '</p>';
            if ($opt['description'] !== '') {
                $descEsc = self::h($opt['description']);
                $d = <<<HTML
<p class="mt-2 text-gray-500 group-focus/option:text-brand-200 in-[el-selectedcontent]:hidden dark:text-gray-400 dark:group-focus/option:text-brand-100">{$descEsc}</p>
HTML;
            } else {
                $d = '';
            }

            $dis = $opt['disabled'] ? ' disabled aria-disabled="true"' : '';
            $disClass = $opt['disabled'] ? ' opacity-50 cursor-not-allowed pointer-events-none' : '';

            $optsHtml .= <<<HTML
    <el-option value="{$v}"{$dis} data-search="{$searchHayEsc}" tabindex="-1"
      @keydown.enter.stop.prevent="\$el.click()"
      @keydown.space.stop.prevent="\$el.click()"
      @keydown.tab.stop="(() => {
        if (\$event.shiftKey) {
          let prev = \$el.previousElementSibling;
          while (prev && (prev.hidden || prev.hasAttribute('disabled') || prev.getAttribute('aria-disabled') === 'true' || !prev.hasAttribute('data-search'))) {
            prev = prev.previousElementSibling;
          }
          if (prev) {
            \$event.preventDefault();
            prev.focus();
            if (typeof prev.scrollIntoView === 'function') { prev.scrollIntoView({ block: 'nearest' }); }
          } else if (\$refs.search) {
            \$event.preventDefault();
            \$refs.search.focus();
          }
        } else {
          let next = \$el.nextElementSibling;
          while (next && (next.hidden || next.hasAttribute('disabled') || next.getAttribute('aria-disabled') === 'true' || !next.hasAttribute('data-search'))) {
            next = next.nextElementSibling;
          }
          if (next) {
            \$event.preventDefault();
            next.focus();
            if (typeof next.scrollIntoView === 'function') { next.scrollIntoView({ block: 'nearest' }); }
          }
        }
      })()"
      @keydown.arrow-down.stop.prevent="(() => {
        let next = \$el.nextElementSibling;
        while (next && (next.hidden || next.hasAttribute('disabled') || next.getAttribute('aria-disabled') === 'true' || !next.hasAttribute('data-search'))) {
          next = next.nextElementSibling;
        }
        if (next) {
          next.focus();
          if (typeof next.scrollIntoView === 'function') { next.scrollIntoView({ block: 'nearest' }); }
        }
      })()"
      @keydown.arrow-up.stop.prevent="(() => {
        let prev = \$el.previousElementSibling;
        while (prev && (prev.hidden || prev.hasAttribute('disabled') || prev.getAttribute('aria-disabled') === 'true' || !prev.hasAttribute('data-search'))) {
          prev = prev.previousElementSibling;
        }
        if (prev) {
          prev.focus();
          if (typeof prev.scrollIntoView === 'function') { prev.scrollIntoView({ block: 'nearest' }); }
        } else if (\$refs.search) {
          \$refs.search.focus();
        }
      })()"
      class="group/option relative block cursor-default py-2 pr-9 pl-4 text-sm text-gray-700 select-none focus:bg-gray-100 focus:gray-900 focus:outline-hidden dark:text-white dark:focus:bg-brand-600{$disClass}">
      {$displayHtml}
      {$d}
      <span class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-700 group-not-aria-selected/option:hidden group-focus/option:text-gray-900 in-[el-selectedcontent]:hidden dark:text-brand-400">
        <svg viewBox="0 0 20 20" fill="currentColor" data-slot="icon" aria-hidden="true" class="size-4">
          <path d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z" clip-rule="evenodd" fill-rule="evenodd" />
        </svg>
      </span>
    </el-option>
HTML;
        }

        // Selected trigger content: use rich HTML when available, else plain escaped label
        $selectedContent = ($selectedLabelHtml !== null && $selectedLabelHtml !== '')
            ? $selectedLabelHtml
            : self::h($selectedLabel);

        // Optional icon before selected content
        $iconHtml = ($icon !== '') ? '<span class="shrink-0 text-gray-400 dark:text-gray-500">' . $icon . '</span>' : '';

        $chevronHtml = <<<HTML
<svg viewBox="0 0 20 20" fill="currentColor" data-slot="icon" aria-hidden="true" class="size-5 shrink-0 currentColor">
  <path d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" fill-rule="evenodd" />
</svg>
HTML;

        $triggerContent = <<<HTML
{$iconHtml}<el-selectedcontent class="truncate">{$selectedContent}</el-selectedcontent>
HTML;

        $triggerHtml = self::renderValueBox($triggerContent, [
            'tag'           => 'button',
            'attrs'         => 'type="button" aria-label="Change selection"',
            'right_content' => $chevronHtml,
            'trigger_class' => $triggerClass,
        ]);

        $searchPlaceholderEsc = self::h($searchPlaceholder);

        // Alpine-powered client-side search. We keep it markup-only (no extra JS bundle changes).
        $alpineWrapperAttrs = '';
        $searchUiHtml = '';
        if ($enableSearch) {
            $alpineWrapperAttrs = 'x-data="{ q: \'\' }" '
                . 'x-effect="(() => { '
                . 'const needle = (q || \'\').trim().toLowerCase(); '
                . 'const root = $refs.opts; if (!root) return; '
                . 'let visible = 0; '
                . 'root.querySelectorAll(\'el-option[data-search]\').forEach((el) => { '
                . 'const hay = (el.getAttribute(\'data-search\') || \'\'); '
                . 'const show = needle === \'\' || hay.indexOf(needle) !== -1; '
                . 'el.hidden = !show; if (show) visible++; '
                . '}); '
                . 'root.querySelectorAll(\'el-option[data-subheader]\').forEach((el) => { el.hidden = needle !== \'\'; }); '
                . 'if ($refs.nores) { $refs.nores.hidden = !(needle !== \'\' && visible === 0); } '
                . '})()"';

            $searchUiHtml = <<<HTML
      <div class="sticky top-0 z-10 border-b border-gray-100 bg-white/95 px-2 py-2 backdrop-blur dark:border-white/10 dark:bg-gray-800/95">
        <input type="search" placeholder="{$searchPlaceholderEsc}" autocomplete="off" name=""
          x-ref="search"
          x-model.debounce.150ms="q"
          @input.stop
          @change.stop
          @keydown="(() => { if (!['Escape', 'Tab', 'ArrowDown', 'ArrowUp', 'Enter'].includes(\$event.key)) { \$event.stopPropagation(); if (typeof \$event.stopImmediatePropagation === 'function') { \$event.stopImmediatePropagation(); } } })()"
          @keydown.tab="(() => { if (!\$event.shiftKey) { \$event.preventDefault(); \$event.stopPropagation(); if (typeof \$event.stopImmediatePropagation === 'function') { \$event.stopImmediatePropagation(); } const root = \$refs.opts; if (!root) return; const first = Array.from(root.querySelectorAll('el-option[data-search]')).find((el) => !el.hidden && !el.hasAttribute('disabled') && (el.getAttribute('aria-disabled') !== 'true')); if (first && typeof first.focus === 'function') { \$nextTick(() => { first.focus(); if (typeof first.scrollIntoView === 'function') { first.scrollIntoView({ block: 'nearest' }); } }); } } })()"
          @keydown.arrow-down.stop.prevent="(() => { 
            const root = \$refs.opts; if (!root) return; 
            const first = Array.from(root.querySelectorAll('el-option[data-search]')).find((el) => !el.hidden && !el.hasAttribute('disabled') && (el.getAttribute('aria-disabled') !== 'true')); 
            if (first && typeof first.focus === 'function') { \$nextTick(() => { first.focus(); if (typeof first.scrollIntoView === 'function') { first.scrollIntoView({ block: 'nearest' }); } }); } 
          })()"
          @keydown.enter.stop.prevent="(() => {
            const root = \$refs.opts;
            if (!root) return;
            const first = Array.from(root.querySelectorAll('el-option[data-search]')).find((el) => !el.hidden && !el.hasAttribute('disabled') && (el.getAttribute('aria-disabled') !== 'true'));
            if (first) {
              first.click();
            }
          })()"
          class="block w-full rounded-md bg-white px-2.5 py-1.25 text-base sm:text-sm/6 text-gray-900 ring-1 ring-inset ring-gray-200 placeholder:text-gray-400 focus:outline-hidden focus:ring-2 focus:ring-brand-500 dark:bg-white/5 dark:text-white dark:ring-white/10 dark:placeholder:text-gray-500" />
      </div>
      <div x-ref="nores" hidden class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">No results</div>
HTML;
        }

        // When the search UI is present, remove the top padding on <el-options>.
        // Otherwise there is a small empty strip above the sticky search header where options can peek through.
        $optionsPaddingClass = $enableSearch ? 'pt-0 pb-1' : 'py-1';

        $inputInner = <<<HTML
<el-select name="{$nameEsc}" value="{$valueEsc}" {$extra}>
  {$triggerHtml}

  <el-options x-ref="opts" anchor="bottom start" popover
    @toggle="(() => { if (\$event && \$event.newState === 'open') { q = ''; if (\$refs.search && typeof \$refs.search.focus === 'function') { \$nextTick(() => \$refs.search.focus()); } } })()"
    class="min-w-[var(--button-width)] w-max max-w-96 max-h-60 origin-top overflow-auto rounded-2xl bg-white {$optionsPaddingClass} shadow-lg outline-1 outline-black/5 [--anchor-gap:--spacing(1)] data-leave:transition data-leave:transition-discrete data-leave:duration-100 data-leave:ease-in data-closed:data-leave:opacity-0 dark:bg-gray-800 dark:shadow-none dark:-outline-offset-1 dark:outline-white/10">
    {$searchUiHtml}
    {$optsHtml}
  </el-options>
</el-select>
HTML;

        // Use renderContainer but wrap it with the Alpine data for search
        $opts['container_attrs'] = $alpineWrapperAttrs;
        return self::renderContainer($labelHtml, $inputInner, $helpHtml, $errorHtml, $clientErr, $opts);
    }
}
