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

use Components\Support\Palette;
use Components\Ui\CollectionIdentity;

class ColorChoice extends BaseField
{
    /**
     * Render an Alpine.js dropdown color picker using curated Tailwind utilities from Palette::colorTokensForChoice().
     * - Only colors from Palette::colorTokensForChoice() (optionally intersected with $opts['tokens']).
     * - No shades. One flat swatch per color (e.g., bg-blue-500).
     * - Clicking a color selects it, closes the dropdown, and the toggle shows the selected color.
     * - Posts the selected value via a hidden input named $name.
     *
     * Options (in $opts):
     * - help: string help text
     * - errors: array of error strings
     * - include_empty: bool add an empty option (default false)
     * - empty_label: string label for the empty option (default "Default")
     * - empty_value: string value for the empty option (default "")
     * - tokens: array of utility strings to restrict the palette (will be intersected with Palette::colorTokensForChoice)
     * - label_badge_html: string extra html to display next to label
     * - preview_name_field: string name of a sibling form field whose value is shown live in a preview badge next to the button
     */
    public static function render(string $name, string $label, $value = '', array $opts = []): string
    {
        $errors = $opts['errors'] ?? [];
        $help = $opts['help'] ?? '';
        $extra = $opts['input_attrs'] ?? '';
        $includeEmpty = (bool)($opts['include_empty'] ?? false);
        $emptyLabel = (string)($opts['empty_label'] ?? 'Default');
        $emptyValue = array_key_exists('empty_value', $opts) ? (string)$opts['empty_value'] : '';

        // Curated tokens and optional intersection restriction
        $choiceMap = Palette::colorTokensForChoice(true);
        $choiceMap = is_array($choiceMap) ? $choiceMap : [];
        $allowed = array_values(array_filter(array_map('strval', array_keys($choiceMap))));

        // Backward-compatible restriction handling:
        // accepts either plain `bg-*` tokens or composite utility strings containing `bg-*`.
        $provided = null;
        if (is_array($opts['tokens'] ?? null)) {
            $provided = [];
            foreach ($opts['tokens'] as $token) {
                $token = trim((string)$token);
                if ($token === '') {
                    continue;
                }
                if (preg_match('/\bbg-[^\s]+/', $token, $m)) {
                    $provided[] = (string)$m[0];
                } else {
                    $provided[] = $token;
                }
            }
            $provided = array_values(array_unique($provided));
        }
        $tokens = $provided ? array_values(array_intersect($allowed, $provided)) : $allowed;

        // Always keep a valid fallback set, even when the provided restriction resolves to none.
        if (empty($tokens)) {
            $tokens = $allowed;
        }

        $nameEsc = self::h($name);
        $labelEsc = self::h($label);
        $badge = $opts['label_badge_html'] ?? '';
        $helpHtml = self::helpHtml($help);
        $errorHtml = self::errorsHtml($errors);
        $clientErr = self::clientErrorHtml($name);
        $hasError = !empty($errors);
        $groupErrorClass = $hasError ? ' ring-1 ring-red-500/50 rounded-md p-1' : '';
        $validate = $opts['validate'] ?? [];
        $requiredAttr = (!empty($validate['required'])) ? ' required' : '';

        // Optional live preview: sibling field name whose value is shown next to the button
        $previewNameField = (string)($opts['preview_name_field'] ?? '');
        $previewNameFieldEsc = htmlspecialchars($previewNameField, ENT_QUOTES, 'UTF-8');

        // Normalize current value against allowed tokens / empty
        $current = (string)$value;
        $isAllowed = in_array($current, $tokens, true);
        if ($current === $emptyValue) {
            $selected = $emptyValue; // allow empty when configured
        } elseif ($isAllowed) {
            $selected = $current;
        } else {
            // Fallback: empty when permitted, else first token
            $selected = $includeEmpty ? $emptyValue : ((string)($tokens[0] ?? $emptyValue));
        }

        // Prepare Alpine JSON payloads
        $outlineByColor = [];
        foreach ($tokens as $token) {
            $outlineByColor[$token] = (string)($choiceMap[$token] ?? 'outline-brand-500 focus-visible:outline-brand-500');
        }

        $jsonTokens = htmlspecialchars(json_encode(array_values($tokens), JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8');
        $jsonEmptyVal = htmlspecialchars(json_encode($emptyValue, JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8');
        $jsonSelected = htmlspecialchars(json_encode($selected, JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8');
        $jsonOutlineByColor = htmlspecialchars(json_encode($outlineByColor, JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8');
        $includeEmptyJs = $includeEmpty ? 'true' : 'false';
        $emptyLabelEsc = self::h($emptyLabel);

        // Build optional preview badge HTML — matches CollectionsList item style (dot + name, no box)
        $previewHtml = '';
        if ($previewNameField !== '') {
            $previewHtml = CollectionIdentity::render('', '', [
                'base_class'   => 'inline-flex items-center gap-2.5 text-sm text-secondary min-w-0 w-1/2',
                'x_show'       => 'previewName',
                'dot_x_class'  => 'dotClass(selected)',
                'name_x_text'  => 'previewName',
            ]);
        }

        // Build component HTML
        $fieldHtml = <<<HTML
<div x-data="{
        open: false,
        options: {$jsonTokens},
        includeEmpty: {$includeEmptyJs},
        emptyValue: {$jsonEmptyVal},
        outlineByColor: {$jsonOutlineByColor},
        selected: {$jsonSelected},
        previewName: '',
        select(v){ this.selected = v; },
        isSelected(v){ return this.selected === v; },
        labelFor(v){ if(!v) return '{$emptyLabelEsc}'; const m = String(v).match(/^bg-([a-z]+)-/i); const n = m ? m[1] : String(v); return n.charAt(0).toUpperCase() + n.slice(1); },
        swatchClass(v){ const tone = this.outlineByColor[v] || 'outline-brand-500 focus-visible:outline-brand-500'; return this.isSelected(v) ? (tone + ' outline-2 outline-offset-2') : tone; },
        emptySwatchClass(){ const tone = 'outline-gray-400 focus-visible:outline-gray-400'; return this.isSelected(this.emptyValue) ? (tone + ' outline-2 outline-offset-2') : tone; },
        dotClass(v){ return (v && /^bg-/.test(v)) ? v : 'bg-gray-300'; },
        initPreview(fieldName){ if(!fieldName) return; const f=this.\$root.closest('form'); const el=f&&f.elements[fieldName]; if(!el) return; this.previewName=el.value; el.addEventListener('input',e=>{ this.previewName=e.target.value; }); },
    }" x-init="initPreview('{$previewNameFieldEsc}')" class="relative">
  <div class="flex items-center gap-3">
    <button type="button" @click="open = !open" :aria-expanded="open ? 'true' : 'false'" 
    class="mr-auto inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm font-medium text-gray-900 hover:bg-white">
      <span class="size-4 rounded-full" :class="dotClass(selected)"></span>
      <svg class="h-4 w-4 text-gray-500" :class="open ? 'rotate-180' : 'rotate-0'" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
        <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 10.94l3.71-3.71a.75.75 0 1 1 1.06 1.06l-4.24 4.25a.75.75 0 0 1-1.06 0L5.21 8.29a.75.75 0 0 1 .02-1.08z" clip-rule="evenodd" />
      </svg>
    </button>
    {$previewHtml}<input type="hidden" name="{$nameEsc}" x-model="selected"{$requiredAttr} {$extra} />
  </div>

  <div x-show="open"
       x-cloak
       x-transition:enter="transition ease-out duration-200"
       x-transition:enter-start="opacity-0 -translate-y-1"
       x-transition:enter-end="opacity-100 translate-y-0"
       x-transition:leave="transition ease-in duration-200"
       x-transition:leave-start="opacity-100 translate-y-0"
       x-transition:leave-end="opacity-0 -translate-y-1"
       @click.outside="open = false"
       class="mt-3 space-y-3">
    <div class="grid w-full grid-cols-6 justify-items-center gap-x-3 gap-y-3 sm:grid-cols-8 lg:grid-cols-9">
      <template x-if="includeEmpty">
        <div class="flex rounded-full outline -outline-offset-1 outline-black/10">
          <button type="button"
                  @click="select(emptyValue)"
                  :aria-label="'{$emptyLabelEsc}'"
                  class="relative size-8 appearance-none rounded-full bg-gray-300 forced-color-adjust-none focus-visible:outline-3 focus-visible:outline-offset-3"
                  :class="emptySwatchClass()"
                  :title="'{$emptyLabelEsc}'">
          </button>
        </div>
      </template>
      <template x-for="c in options" :key="c">
        <div class="flex rounded-full outline -outline-offset-1 outline-black/10">
          <button type="button"
                  @click="select(c)"
                  :aria-label="labelFor(c)"
                  class="relative size-8 appearance-none rounded-full forced-color-adjust-none focus-visible:outline-3 focus-visible:outline-offset-3"
                  :class="[c, swatchClass(c)]"
                  :title="labelFor(c)">
          </button>
        </div>
      </template>
    </div>
  </div>
</div>
HTML;

        // Wrap in fieldset with legend and compose
        return <<<HTML
<fieldset class="{$groupErrorClass}">
  <legend class="text-sm/6 font-semibold text-gray-900 dark:text-white">{$labelEsc}{$badge}</legend>
  {$fieldHtml}
  {$helpHtml}
  {$errorHtml}
  {$clientErr}
</fieldset>
HTML;
    }
}
