<?php
namespace PpPhp\Plugins;
use PpPhp\Core\EditPlugin;

/**
 * Lookup Wizard — a full-featured foreign-key control (PHPRunner-style).
 *
 * The field stores the KEY column of another table while the user picks by the
 * DISPLAY column. Options are read live from the database, so this renders in
 * the Designer preview and inside the generated application alike.
 *
 * Display modes: dropdown (optionally multiple) · radio · checkboxes
 * (vertical/horizontal, inherently multiple, stored CSV) · ajax (edit box with
 * live suggestions, optional free input).
 * Behaviour: cascading parent/filter, WHERE, order, distinct, prevent
 * duplicates (build-enforced), add/edit lookup values on the fly (popup).
 */
class LookupWizard implements EditPlugin
{
    public static function id(): string { return 'LookupWizard'; }
    public static function label(): string { return 'Lookup Wizard'; }
    public static function version(): string { return '1.0.0'; }
    public static function description(): string { return 'Foreign-key dropdown that pulls options from a related table.'; }

    private const SIZES = [
        'md' => 'padding:.6rem .8rem;font-size:1rem;min-height:2.75rem;',
        'lg' => 'padding:.8rem 1rem;font-size:1.1rem;min-height:3.25rem;',
    ];

    public static function render(string $fieldName, mixed $value, array $settings): string {
        $name = htmlspecialchars($fieldName, ENT_QUOTES, 'UTF-8');
        $cur  = (string) ($value ?? '');
        $mode = (string) ($settings['control'] ?? 'dropdown');
        // ajax needs a table to search live — fall back to a dropdown for a manual list.
        if (($settings['source'] ?? 'table') === 'manual' && $mode === 'ajax') $mode = 'dropdown';
        $size = (string) ($settings['size'] ?? 'sm');
        $style = self::SIZES[$size] ?? '';
        $font = $size === 'lg' ? 'font-size:1.1rem;' : ($size === 'md' ? 'font-size:1rem;' : '');

        $html = match ($mode) {
            'radio'      => self::renderChoices($name, $cur, self::options($settings), $settings, false, $font),
            'checkboxes' => self::renderChoices($name, $cur, self::options($settings), $settings, true, $font),
            'ajax'       => self::renderAjax($name, $cur, $settings, $style),
            default      => self::renderSelect($name, $cur, self::options($settings), $settings, $style),
        };
        return $html . self::flyButtons($name, $settings);
    }

    /** Dropdown — single or multiple (multiple posts name[] and stores CSV). */
    private static function renderSelect(string $name, string $cur, array $opts, array $settings, string $style): string {
        $multi = ($settings['multiple'] ?? 'no') === 'yes';
        $selected = $multi ? array_filter(array_map('trim', explode(',', $cur)), 'strlen') : [$cur];
        $empty = trim((string) ($settings['emptyLabel'] ?? ''));
        if ($empty === '') $empty = function_exists('pp_t') ? pp_t('ui.choose', 'Choose…') : 'Choose…';

        // Full width like every other form control (the generated .pp-input
        // width rule doesn't reach selects, so the browser shrink-wraps them).
        $html = '<select name="' . $name . ($multi ? '[]' : '') . '" id="field_' . $name . '" class="pp-input"'
            . ($multi ? ' multiple size="5"' : '') . ' style="width:100%;flex:1 1 auto;' . $style . '">';
        if (!$multi) $html .= '<option value="">' . htmlspecialchars($empty, ENT_QUOTES, 'UTF-8') . '</option>';
        $found = [];
        foreach ($opts as $key => $display) {
            $sel = in_array((string) $key, $selected, true) ? ' selected' : '';
            if ($sel !== '') $found[] = (string) $key;
            $html .= '<option value="' . htmlspecialchars((string) $key, ENT_QUOTES, 'UTF-8') . '"' . $sel . '>'
                . htmlspecialchars((string) $display, ENT_QUOTES, 'UTF-8') . '</option>';
        }
        // Off-list current values stay selectable so edits never clear them.
        foreach (array_diff(array_filter($selected, 'strlen'), $found) as $miss) {
            $safe = htmlspecialchars($miss, ENT_QUOTES, 'UTF-8');
            $html .= '<option value="' . $safe . '" selected>' . $safe . '</option>';
        }
        $html .= '</select>';

        // Multiple: wrap the (soon hidden) native select in a chip-style combo.
        // Progressive enhancement — without JS the native multi-select still works.
        if ($multi) {
            $ph = htmlspecialchars($empty, ENT_QUOTES, 'UTF-8');
            $html = '<div class="pp-msel" data-pp-msel="' . $name . '" style="position:relative;width:100%;flex:1 1 auto;min-width:0;">'
                . $html
                . '<div class="pp-msel-box" tabindex="0" style="' . 'display:none;width:100%;flex-wrap:wrap;align-items:center;gap:.3rem;min-height:2.4rem;padding:.35rem .6rem;'
                . 'border:1px solid var(--pp-border,#cbd5e1);border-radius:.5rem;background:var(--pp-surface,#fff);cursor:pointer;' . $style . '">'
                .   '<span class="pp-msel-chips" style="display:contents;"></span>'
                .   '<span class="pp-msel-ph" style="color:#94a3b8;">' . $ph . '</span>'
                .   '<i class="bi bi-chevron-down" style="margin-left:auto;color:#64748b;font-size:.75rem;"></i>'
                . '</div>'
                . '<div class="pp-msel-panel" hidden style="position:absolute;z-index:1000;left:0;right:0;top:100%;margin-top:.25rem;max-height:240px;'
                . 'overflow-y:auto;background:#fff;border:1px solid #e2e8f0;border-radius:.5rem;box-shadow:0 10px 30px rgba(2,6,23,.15);"></div>'
                . '</div>' . self::mselScript();
        }
        return $html;
    }

    /** One-time (guarded) enhancement script for chip multi-selects. */
    private static function mselScript(): string {
        return <<<'JS'
<script>(function(){
if(!window.__ppMsel){window.__ppMsel=1;
var CHIP='display:inline-flex;align-items:center;gap:.3rem;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;border-radius:999px;padding:.1rem .3rem .1rem .6rem;font-size:.82rem;font-weight:600;';
var ITEM='display:flex;align-items:center;gap:.5rem;padding:.45rem .7rem;cursor:pointer;font-size:.9rem;';
function chips(w){var s=w.querySelector('select');var c=w.querySelector('.pp-msel-chips');var ph=w.querySelector('.pp-msel-ph');
 var sel=[].filter.call(s.options,function(o){return o.selected&&o.value!=='';});
 c.innerHTML='';ph.style.display=sel.length?'none':'';
 sel.forEach(function(o){var ch=document.createElement('span');ch.setAttribute('style',CHIP);
  var t=document.createElement('span');t.textContent=o.text;ch.appendChild(t);
  var x=document.createElement('b');x.textContent='×';x.setAttribute('data-ppx',o.value);
  x.setAttribute('style','cursor:pointer;color:#60a5fa;font-weight:700;padding:0 .25rem;');ch.appendChild(x);c.appendChild(ch);});}
function panel(w){var s=w.querySelector('select');var p=w.querySelector('.pp-msel-panel');p.innerHTML='';
 [].forEach.call(s.options,function(o){if(o.value==='')return;var it=document.createElement('div');
  it.setAttribute('style',ITEM);it.setAttribute('data-ppv',o.value);
  it.innerHTML='<input type="checkbox"'+(o.selected?' checked':'')+' style="pointer-events:none;accent-color:#2563eb;width:14px;height:14px;"><span></span>';
  it.lastChild.textContent=o.text;
  it.addEventListener('mouseenter',function(){it.style.background='#eff6ff';});
  it.addEventListener('mouseleave',function(){it.style.background='';});p.appendChild(it);});}
document.addEventListener('click',function(e){
 var w=e.target.closest?e.target.closest('.pp-msel'):null;
 [].forEach.call(document.querySelectorAll('.pp-msel-panel'),function(p){if(!w||!w.contains(p))p.hidden=true;});
 if(!w)return;
 var s=w.querySelector('select');var p=w.querySelector('.pp-msel-panel');
 var x=e.target.closest('[data-ppx]');
 if(x){[].forEach.call(s.options,function(o){if(o.value===x.getAttribute('data-ppx'))o.selected=false;});
  s.dispatchEvent(new Event('change',{bubbles:true}));return;}
 var it=e.target.closest('[data-ppv]');
 if(it){[].forEach.call(s.options,function(o){if(o.value===it.getAttribute('data-ppv'))o.selected=!o.selected;});
  panel(w);s.dispatchEvent(new Event('change',{bubbles:true}));return;}
 if(e.target.closest('.pp-msel-box')){if(p.hidden){panel(w);p.hidden=false;}else{p.hidden=true;}}
});
document.addEventListener('change',function(e){var w=e.target.closest?e.target.closest('.pp-msel'):null;if(w)chips(w);});
window.__ppMselInit=function(){[].forEach.call(document.querySelectorAll('.pp-msel'),function(w){
 var s=w.querySelector('select');s.style.display='none';w.querySelector('.pp-msel-box').style.display='flex';chips(w);});};
}
if(document.readyState!=='loading'){window.__ppMselInit();}else{document.addEventListener('DOMContentLoaded',window.__ppMselInit);}
})();</script>
JS;
    }

    /** Radio group (single) or checkbox list (multiple, CSV) — vertical/horizontal. */
    private static function renderChoices(string $name, string $cur, array $opts, array $settings, bool $multi, string $font): string {
        $selected = $multi ? array_filter(array_map('trim', explode(',', $cur)), 'strlen') : [$cur];
        $horizontal = ($settings['layout'] ?? 'vertical') === 'horizontal';
        $flow = $horizontal ? 'flex-direction:row;flex-wrap:wrap;gap:.35rem 1rem;' : 'flex-direction:column;gap:.4rem;';
        $type = $multi ? 'checkbox' : 'radio';
        $inputName = $name . ($multi ? '[]' : '');

        $html = '<div class="pp-lk-choices" id="field_' . $name . '" style="display:flex;' . $flow . 'padding:.35rem 0;' . $font . '">';
        if ($multi) {
            // A hidden sentinel so clearing every box still submits the field.
            $html .= '<input type="hidden" name="' . $name . '[]" value="">';
        }
        $emit = function (string $key, string $display) use (&$html, $type, $inputName, $selected): void {
            $chk = in_array($key, $selected, true) && $key !== '' ? ' checked' : '';
            $html .= '<label style="display:inline-flex;align-items:center;gap:.4rem;cursor:pointer;margin:0;">'
                . '<input type="' . $type . '" name="' . $inputName . '" value="' . htmlspecialchars($key, ENT_QUOTES, 'UTF-8') . '"' . $chk . '>'
                . '<span>' . htmlspecialchars($display, ENT_QUOTES, 'UTF-8') . '</span></label>';
        };
        $found = [];
        foreach ($opts as $key => $display) {
            if (in_array((string) $key, $selected, true)) $found[] = (string) $key;
            $emit((string) $key, (string) $display);
        }
        foreach (array_diff(array_filter($selected, 'strlen'), $found) as $miss) $emit($miss, $miss);
        return $html . '</div>';
    }

    /** Edit box with AJAX suggestions — hidden key + visible display text. */
    private static function renderAjax(string $name, string $cur, array $settings, string $style): string {
        $free = ($settings['freeInput'] ?? 'no') === 'yes';
        $display = $cur !== '' ? self::displayFor($settings, $cur) : '';
        // Free input stores the typed text itself, so the visible box IS the field.
        if ($free && $display === $cur) $display = $cur;
        $lookupId = htmlspecialchars((string) ($settings['_lookupId'] ?? ''), ENT_QUOTES, 'UTF-8');
        return '<input type="hidden" name="' . $name . '" id="field_' . $name . '" value="' . htmlspecialchars($cur, ENT_QUOTES, 'UTF-8') . '">'
            . '<input type="text" class="pp-input" id="field_' . $name . '_display" value="' . htmlspecialchars($display, ENT_QUOTES, 'UTF-8') . '"'
            . ' data-pp-lkajax="' . $name . '" data-pp-lkid="' . $lookupId . '" data-pp-lkfree="' . ($free ? '1' : '0') . '"'
            . ' placeholder="' . htmlspecialchars(trim((string) ($settings['emptyLabel'] ?? '')) ?: 'Type to search…', ENT_QUOTES, 'UTF-8') . '"'
            . ' autocomplete="off" style="width:100%;flex:1 1 auto;' . $style . '">';
    }

    /** "+ add" / "✎ edit" popup buttons for managing the lookup table's values. */
    private static function flyButtons(string $name, array $settings): string {
        $add  = ($settings['allowAdd'] ?? 'no') === 'yes';
        $edit = ($settings['allowEdit'] ?? 'no') === 'yes';
        if (!$add && !$edit) return '';
        $table = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($settings['table'] ?? ''));
        if ($table === '' || !defined('APP_BASE')) return '';
        $base = constant('APP_BASE') . '/generated/' . $table;
        $btn = 'display:inline-flex;align-items:center;justify-content:center;width:1.9rem;height:1.9rem;'
            . 'border:1px solid var(--pp-border,#e2e8f0);border-radius:.5rem;background:var(--pp-surface,#fff);'
            . 'color:var(--pp-primary,#2563eb);cursor:pointer;margin-left:.35rem;vertical-align:middle;';
        $html = '<span class="pp-lk-fly" data-pp-lkfly="' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '">';
        if ($add) {
            $html .= '<button type="button" style="' . $btn . '" title="Add a new value"'
                . ' onclick="window.open(\'' . $base . '/add.php?pp_popup=1\',\'ppLk\',\'width=920,height=760\')">'
                . '<i class="bi bi-plus-lg"></i></button>';
        }
        if ($edit) {
            $html .= '<button type="button" style="' . $btn . '" title="Edit the selected value"'
                . ' onclick="var f=document.getElementById(\'field_' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '\');'
                . 'var v=f&&f.value?String(f.value).split(\',\')[0]:\'\';'
                . 'if(v)window.open(\'' . $base . '/edit.php?pp_popup=1&id=\'+encodeURIComponent(v),\'ppLk\',\'width=920,height=760\');'
                . 'else alert(\'Select a value first.\')">'
                . '<i class="bi bi-pencil"></i></button>';
        }
        return $html . '</span>';
    }

    /** Display value for one key (used by the AJAX box and edit prefill). */
    public static function displayFor(array $settings, string $key): string {
        $table   = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($settings['table'] ?? ''));
        $k       = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($settings['linkField'] ?? 'id')) ?: 'id';
        $display = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($settings['displayField'] ?? ''));
        if ($table === '' || $display === '' || !class_exists('\PpPhp\Core\DB')) return $key;
        try {
            $d = \PpPhp\Core\DB::FetchOne("SELECT `$display` FROM `$table` WHERE `$k` = ?", [$key]);
            return $d !== false && $d !== null ? (string) $d : $key;
        } catch (\Throwable $e) { return $key; }
    }

    /**
     * key => display pairs honouring WHERE / order / distinct / cascade filter /
     * live search (_search from the AJAX box). Identifiers whitelisted.
     *
     * @return array<int|string, string>
     */
    public static function options(array $settings): array {
        // Manual source — a fixed list typed in the Designer (e.g. Gender:
        // male, female, other). Supports value:Label pairs. Same parser the
        // Dropdown control uses, so behaviour is consistent.
        if (($settings['source'] ?? 'table') === 'manual') {
            return \PpPhp\Core\Designer\FieldControl\LookupOptions::manualOptions((string) ($settings['manualOptions'] ?? ''));
        }
        $table   = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($settings['table'] ?? ''));
        $key     = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($settings['linkField'] ?? 'id')) ?: 'id';
        $display = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($settings['displayField'] ?? ''));
        if ($table === '' || $display === '' || !class_exists('\PpPhp\Core\DB')) return [];

        $orderBy = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($settings['orderBy'] ?? '')) ?: $display;
        $dir     = strtoupper((string) ($settings['orderDir'] ?? 'ASC')) === 'DESC' ? 'DESC' : 'ASC';
        $where   = self::whereSql($settings);
        $sel     = (($settings['unique'] ?? 'no') === 'yes') ? 'SELECT DISTINCT' : 'SELECT';
        $args    = [];
        $limit   = 1000;

        $filterField = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($settings['filterField'] ?? ''));
        if ($filterField !== '' && trim((string) ($settings['parentField'] ?? '')) !== '') {
            $pv = (string) ($settings['_parentValue'] ?? '');
            if ($pv === '') return [];
            $where .= ($where === '' ? ' WHERE ' : ' AND ') . "`$filterField` = ?";
            $args[] = $pv;
        }
        $q = trim((string) ($settings['_search'] ?? ''));
        if ($q !== '') {
            $where .= ($where === '' ? ' WHERE ' : ' AND ') . "`$display` LIKE ?";
            $args[] = '%' . $q . '%';
            $limit = 25;
        }

        try {
            $rows = \PpPhp\Core\DB::FetchAll(
                "$sel `$key` AS k, `$display` AS d FROM `$table`$where ORDER BY `$orderBy` $dir LIMIT $limit",
                $args
            ) ?: [];
        } catch (\Throwable $e) {
            return [];
        }
        $out = [];
        foreach ($rows as $r) { $out[(string) ($r['k'] ?? '')] = (string) ($r['d'] ?? ''); }
        unset($out['']);
        return $out;
    }

    /** Designer-authored WHERE condition, hardened: no statement chaining. */
    public static function whereSql(array $settings): string {
        $w = trim((string) ($settings['where'] ?? ''));
        $w = str_replace(["\0", ';'], '', $w);
        $w = preg_replace('/^(where|and)\s+/i', '', $w) ?? $w;
        // Expand SQL variables so lookups can depend on the logged-in user/session
        // (WHERE owner_id = :userid) AND on other fields of the same form
        // (WHERE country_id = :field.country) — the cascading-dropdown case.
        if ($w !== '' && class_exists('\PpPhp\Core\SqlVars')) {
            $w = \PpPhp\Core\SqlVars::expand($w, ['field' => (array) ($settings['_formFields'] ?? [])]);
        }
        return $w === '' ? '' : ' WHERE (' . $w . ')';
    }

    public static function settingsSchema(): array {
        // `source` toggles the option origin. Everything table-specific is gated
        // to source=table; the manual list shows only for source=manual. A field
        // with no saved source defaults to 'table' (backward compatible).
        $tableOnly  = ['key' => 'source', 'in' => ['table']];
        $manualOnly = ['key' => 'source', 'in' => ['manual']];
        return [
            ['key' => 'source', 'type' => 'radiogroup', 'options' => ['table', 'manual'], 'label' => 'Values from', 'group' => 'Source', 'default' => 'table',
             'hint' => 'Read options from a database table, or type a fixed manual list (e.g. Gender: male, female, other).'],
            ['key' => 'manualOptions', 'type' => 'text', 'full' => true, 'label' => 'Manual options (A, B or 1:Active, 0:Off)', 'group' => 'Source',
             'showIf' => $manualOnly, 'placeholder' => 'male, female, other · or m:Male, f:Female', 'default' => '',
             'hint' => 'Comma-separated. Use value:Label pairs to store a code but show a friendly label (e.g. m:Male, f:Female).'],
            ['key' => 'table', 'type' => 'table', 'label' => 'Lookup table', 'group' => 'Source', 'showIf' => $tableOnly,
             'hint' => 'The table that holds the options (e.g. customers)', 'default' => ''],
            ['key' => 'linkField', 'inline' => true, 'type' => 'column', 'tableKey' => 'table', 'label' => 'Key column', 'group' => 'Source', 'showIf' => $tableOnly,
             'hint' => 'The value stored in this field — usually id', 'default' => 'id'],
            ['key' => 'displayField', 'inline' => true, 'type' => 'column', 'tableKey' => 'table', 'label' => 'Display column', 'group' => 'Source', 'showIf' => $tableOnly,
             'hint' => 'What the user sees (e.g. name)', 'default' => ''],

            ['key' => 'where', 'type' => 'text', 'full' => true, 'label' => 'Where (condition)', 'group' => 'Options', 'showIf' => $tableOnly,
             'hint' => "Optional SQL condition limiting the options. Reads other form fields (:field.name — cascading dropdowns), the user (:userid, :user.col), session (:session.key), dates (:today, :daysago.30) — and sub-queries for other tables.",
             'placeholder' => "status = 'active'  ·  country_id = :field.country  ·  owner_id = :userid",
             'note' => 'Filter the options. Read <b>another field on this form</b> with <code>:field.name</code> (cascading dropdowns — it re-filters live), the user with <code>:userid</code>/<code>:user.col</code>, <code>:session.key</code>, dates like <code>:today</code>/<code>:daysago.30</code> — and a sub-query for <b>other tables</b>: <code>id IN (SELECT … WHERE x = :field.y)</code>.',
             'default' => ''],
            ['key' => 'orderBy', 'inline' => true, 'type' => 'column', 'tableKey' => 'table', 'label' => 'Order by', 'group' => 'Options', 'showIf' => $tableOnly,
             'hint' => 'Sort column — defaults to the display column', 'default' => ''],
            ['key' => 'orderDir', 'inline' => true, 'type' => 'radiogroup', 'options' => ['ASC', 'DESC'], 'label' => 'Direction', 'group' => 'Options', 'showIf' => $tableOnly, 'default' => 'ASC'],
            ['key' => 'unique', 'inline' => true, 'type' => 'bool', 'label' => 'Unique values', 'group' => 'Options', 'showIf' => $tableOnly,
             'hint' => 'Collapse duplicate option pairs (SELECT DISTINCT)', 'default' => 'no'],

            ['key' => 'control', 'inline' => true, 'type' => 'radiogroup', 'options' => ['dropdown', 'radio', 'checkboxes', 'ajax'], 'label' => 'Display as', 'group' => 'Display',
             'hint' => 'dropdown · radio group · checkbox list (multiple) · ajax = edit box with live suggestions', 'default' => 'dropdown'],
            ['key' => 'layout', 'inline' => true, 'type' => 'radiogroup', 'options' => ['vertical', 'horizontal'], 'label' => 'List layout', 'group' => 'Display',
             'showIf' => ['key' => 'control', 'in' => ['radio', 'checkboxes']], 'default' => 'vertical'],
            ['key' => 'multiple', 'inline' => true, 'type' => 'bool', 'label' => 'Multiple selection (values stored comma-separated)', 'group' => 'Display',
             'showIf' => ['key' => 'control', 'in' => ['dropdown', '']], 'default' => 'no'],
            ['key' => 'size', 'inline' => true, 'type' => 'radiogroup', 'options' => ['sm', 'md', 'lg'], 'label' => 'Size', 'group' => 'Display', 'default' => 'sm'],
            ['key' => 'emptyLabel', 'inline' => true, 'newline' => true, 'type' => 'text', 'label' => 'Empty option label', 'group' => 'Display', 'placeholder' => 'Choose…', 'default' => ''],
            ['key' => 'freeInput', 'inline' => true, 'type' => 'bool', 'label' => 'Allow free input (store values not in the list)', 'group' => 'Display',
             'showIf' => ['key' => 'control', 'in' => ['ajax']], 'default' => 'no'],

            ['key' => 'parentField', 'inline' => true, 'type' => 'maincolumn', 'label' => 'Parent field (this form)', 'group' => 'Behaviour', 'showIf' => $tableOnly,
             'hint' => 'Cascading dropdown: this field on the SAME form drives the options', 'default' => ''],
            ['key' => 'filterField', 'inline' => true, 'type' => 'column', 'tableKey' => 'table', 'label' => 'Filter column (lookup table)', 'group' => 'Behaviour', 'showIf' => $tableOnly,
             'showIfSet' => 'parentField', 'hint' => 'Column in the lookup table matched against the parent value', 'default' => ''],
            ['key' => 'preventDup', 'inline' => true, 'newline' => true, 'type' => 'bool', 'label' => 'Prevent duplicate values', 'group' => 'Behaviour',
             'hint' => 'Reject the save when another record already uses this value', 'default' => 'no'],
            ['key' => 'allowAdd', 'inline' => true, 'type' => 'bool', 'label' => 'Add new values on the fly (+ button)', 'group' => 'Behaviour', 'showIf' => $tableOnly,
             'hint' => "Opens the lookup table's Add page in a popup; options refresh on save", 'default' => 'no'],
            ['key' => 'allowEdit', 'inline' => true, 'type' => 'bool', 'label' => 'Allow editing values (✎ button)', 'group' => 'Behaviour', 'showIf' => $tableOnly,
             'hint' => "Opens the lookup table's Edit page for the selected value", 'default' => 'no'],

            ['key' => 'displayAs', 'inline' => true, 'newline' => true, 'type' => 'radiogroup', 'options' => ['label', 'value'], 'label' => 'Show on list & view', 'group' => 'Display', 'default' => 'label',
             'hint' => 'label = the display column · value = the raw stored key (id).'],
        ];
    }

    public static function assets(): array { return ['js' => [], 'css' => []]; }
}
