// Generic Content-tab editor for schema-driven widgets. Reads the widget's
// `fields` from the registry and renders one control per field. Slotted into
// PropertiesPanel's ContentTab `default` case, so it's ONLY reached by widgets
// with no bespoke content component — the 39 existing widgets never hit this.
//
// Controls are self-contained (reusing the existing .jalpi-input/.jalpi-select
// CSS classes for visual consistency) rather than importing PropertiesPanel's
// internal primitives — that keeps this file decoupled from the 9k-line panel
// and avoids widening that file's export surface.
import React from 'react';
import type { CBNode, IconValue } from '../types';
import type { EditorStore } from '../store/editorStore';
import { getWidgetFields, type WidgetField } from './registry';
import { IconInput, IconPickerModal } from '../components/panels/IconPickerModal';

/** WordPress media-library image picker for schema `image` fields. Opens
 * wp.media, stores the selected attachment URL, shows a thumbnail preview with
 * Change / Remove. Self-contained (reuses the existing .jalpi-media-picker CSS)
 * so GenericContent stays decoupled from the 9k-line PropertiesPanel. Falls
 * back to a URL prompt when wp.media isn't present. */
function GenericMediaInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
  const openPicker = () => {
    const wpGlobal = (window as any).wp;
    if (!wpGlobal?.media) {
      const url = window.prompt('Image URL:', value);
      if (url !== null) onChange(url);
      return;
    }
    const frame = wpGlobal.media({ title: 'Choose Image', button: { text: 'Use Image' }, multiple: false, library: { type: 'image' } });
    frame.on('select', () => { const a = frame.state().get('selection').first().toJSON(); onChange(a.url); });
    frame.open();
  };
  return (
    <div className="jalpi-media-picker">
      {value ? (
        <>
          <div className="jalpi-media-picker__preview" onClick={openPicker}>
            <img src={value} alt="" className="jalpi-media-picker__img" />
            <div className="jalpi-media-picker__hover-label">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
                <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
              </svg>
              Change
            </div>
          </div>
          <button type="button" className="jalpi-media-picker__remove" onClick={() => onChange('')}>
            <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
              <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
            </svg>
            Remove image
          </button>
        </>
      ) : (
        <button type="button" className="jalpi-media-picker__add" onClick={openPicker}>
          <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
            <rect x="3" y="3" width="18" height="18" rx="2"/>
            <circle cx="8.5" cy="8.5" r="1.5" fill="currentColor" stroke="none"/>
            <polyline points="21 15 16 10 5 21"/>
          </svg>
          <span>Choose Image</span>
        </button>
      )}
    </div>
  );
}

/** Elementor-style 4-side dimensions control (Top / Right / Bottom / Left) with
 * a link toggle. Stores a CSS-shorthand string: a single number when linked
 * ("8"), or four space-separated numbers when independent ("1 2 3 4"). The unit
 * is appended by the renderer, so values stay unit-agnostic here. */
function Dimensions4Control({ value, unit, onChange }: { value: string; unit: string; onChange: (v: string) => void }) {
  // Parse the stored shorthand into [top,right,bottom,left] bare numbers.
  const parts = String(value ?? '').trim().split(/\s+/).filter(Boolean).map((p) => p.replace(new RegExp(`${unit}$`), ''));
  let t = '', r = '', b = '', l = '';
  if (parts.length === 1) { t = r = b = l = parts[0]; }
  else if (parts.length === 4) { [t, r, b, l] = parts; }
  else if (parts.length === 2) { t = b = parts[0]; r = l = parts[1]; }
  else if (parts.length === 3) { t = parts[0]; r = l = parts[1]; b = parts[2]; }

  const sides = [t, r, b, l];
  const nonEmpty = sides.filter((v) => v !== '');
  const allEqual = nonEmpty.length === 0 || nonEmpty.every((v) => v === nonEmpty[0]);
  const [linked, setLinked] = React.useState(allEqual);

  const emit = (next: [string, string, string, string]) => {
    const [nt, nr, nb, nl] = next;
    if (nt === '' && nr === '' && nb === '' && nl === '') { onChange(''); return; }
    // Serialize: single value when all equal (linked-style), else four values.
    if (nt === nr && nr === nb && nb === nl) onChange(nt);
    else onChange([nt, nr, nb, nl].map((v) => v || '0').join(' '));
  };

  const setSide = (idx: number, raw: string) => {
    const v = raw.replace(/[^\d.-]/g, '');
    if (linked) { emit([v, v, v, v]); return; }
    const next = [...sides] as [string, string, string, string];
    next[idx] = v;
    emit(next);
  };
  const toggleLink = () => {
    if (!linked) {
      const rep = sides.find((v) => v !== '') || '';
      if (rep) emit([rep, rep, rep, rep]);
    }
    setLinked((v) => !v);
  };

  const labels = ['Top', 'Right', 'Bottom', 'Left'];
  return (
    <div className="jalpi-spacing-grid">
      <div className="jalpi-spacing-grid__row">
        <div className="jalpi-spacing-grid__fields">
          {sides.map((v, i) => (
            <div key={i} className="jalpi-spacing-field">
              <input
                type="number"
                className="jalpi-spacing-field__input"
                value={v}
                placeholder="—"
                onChange={(e) => setSide(i, e.target.value)}
              />
              <span className="jalpi-spacing-field__label">{labels[i]}</span>
            </div>
          ))}
        </div>
        <button
          type="button"
          className={`jalpi-spacing-grid__link${linked ? ' is-linked' : ''}`}
          onClick={toggleLink}
          title={linked ? 'Values linked' : 'Values independent'}
          aria-pressed={linked}
        >
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
          </svg>
        </button>
      </div>
    </div>
  );
}

function FieldControl({
  field, value, onChange,
}: {
  field: WidgetField;
  value: unknown;
  onChange: (v: unknown) => void;
}) {
  switch (field.control) {
    case 'textarea':
      return (
        <textarea
          className="jalpi-input"
          rows={3}
          value={String(value ?? '')}
          placeholder={field.placeholder}
          onChange={(e) => onChange(e.target.value)}
        />
      );
    case 'number':
      return (
        <input
          type="number"
          className="jalpi-input"
          value={String(value ?? '')}
          placeholder={field.placeholder}
          onChange={(e) => onChange(e.target.value)}
        />
      );
    case 'toggle':
      return (
        <label className="jalpi-switch">
          <input
            type="checkbox"
            checked={!!value}
            onChange={(e) => onChange(e.target.checked)}
          />
          <span className="jalpi-switch__slider" />
        </label>
      );
    case 'select':
      return (
        <select
          className="jalpi-select"
          value={String(value ?? '')}
          onChange={(e) => onChange(e.target.value)}
        >
          {(field.options ?? []).map((o) => (
            <option key={o.value} value={o.value}>{o.label}</option>
          ))}
        </select>
      );
    case 'buttongroup': {
      // Elementor-style segmented switcher: one pill per option, the active one
      // highlighted. Single-select — clicking a pill sets that option's value.
      const opts = field.options ?? [];
      const cur = String(value ?? (opts[0]?.value ?? ''));
      return (
        <div className="jalpi-btngroup" role="group">
          {opts.map((o) => (
            <button
              key={o.value}
              type="button"
              className={`jalpi-btngroup__btn${cur === o.value ? ' is-active' : ''}`}
              onClick={() => onChange(o.value)}
              aria-pressed={cur === o.value}
              title={o.label}
            >
              {o.glyph && <span className="jalpi-btngroup__glyph" aria-hidden="true">{o.glyph}</span>}
              <span>{o.label}</span>
            </button>
          ))}
        </div>
      );
    }
    case 'icon':
      // Full icon-library picker (the shared modal). Stores an IconValue.
      return (
        <IconInput
          value={value as string | IconValue | undefined}
          onChange={(v) => onChange(v)}
        />
      );
    case 'multiicon': {
      // Value is a comma-separated list of selected option keys. Clicking a
      // chip toggles it; order follows the option list so the output stays
      // stable regardless of click order.
      const selected = String(value ?? '').split(',').map((s) => s.trim()).filter(Boolean);
      const isOn = (v: string) => selected.includes(v);
      const toggle = (v: string) => {
        const next = (field.options ?? [])
          .map((o) => o.value)
          .filter((k) => (k === v ? !isOn(v) : isOn(k)));
        onChange(next.join(','));
      };
      return (
        <div className="jalpi-multiicon">
          {(field.options ?? []).map((o) => (
            <button
              key={o.value}
              type="button"
              className={`jalpi-multiicon__chip${isOn(o.value) ? ' is-on' : ''}`}
              onClick={() => toggle(o.value)}
              title={o.label}
              aria-pressed={isOn(o.value)}
              style={isOn(o.value) && o.color ? { borderColor: o.color, background: o.color, color: '#fff' } : undefined}
            >
              <span className="jalpi-multiicon__glyph" aria-hidden="true">{o.glyph ?? o.label.charAt(0)}</span>
              <span className="jalpi-multiicon__label">{o.label}</span>
            </button>
          ))}
        </div>
      );
    }
    case 'color':
      return (
        <input
          type="color"
          className="jalpi-input jalpi-input--color"
          value={String(value ?? '#000000')}
          onChange={(e) => onChange(e.target.value)}
        />
      );
    case 'image':
      return (
        <GenericMediaInput
          value={String(value ?? '')}
          onChange={(v) => onChange(v)}
        />
      );
    case 'slider': {
      const min = field.min ?? 0;
      const max = field.max ?? 100;
      const step = field.step ?? 1;
      const raw = String(value ?? '');
      // Slider position falls back to the placeholder (the field's default)
      // when unset, so the thumb sits sensibly before the user touches it.
      const num = raw === '' ? Number(field.placeholder ?? min) : Number(raw);
      const safe = Number.isFinite(num) ? num : min;
      const pct = max > min ? ((Math.min(max, Math.max(min, safe)) - min) / (max - min)) * 100 : 0;
      return (
        <div className="jalpi-slider-dim jalpi-slider-dim--compact">
          <input
            type="range"
            className="jalpi-slider-dim__track"
            min={min} max={max} step={step}
            value={safe}
            style={{ ['--pct' as any]: `${pct}%` }}
            onChange={(e) => onChange(e.target.value)}
          />
          <div className="jalpi-slider-dim__row">
            <input
              type="number"
              className="jalpi-slider-dim__num"
              min={min} max={max} step={step}
              value={raw}
              placeholder={field.placeholder}
              onChange={(e) => onChange(e.target.value)}
            />
            {field.unit && <span className="jalpi-slider-dim__unit" style={{ cursor: 'default' }}>{field.unit}</span>}
          </div>
        </div>
      );
    }
    case 'dimensions4':
      return (
        <Dimensions4Control
          value={String(value ?? '')}
          unit={field.unit ?? 'px'}
          onChange={(v) => onChange(v)}
        />
      );
    case 'url':
    case 'text':
    default:
      return (
        <input
          type="text"
          className="jalpi-input"
          value={String(value ?? '')}
          placeholder={field.placeholder}
          onChange={(e) => onChange(e.target.value)}
        />
      );
  }
}

/** Parse a "key:value,key:value" map string into an object. Tolerant of
 * spaces and malformed pairs (skips them). */
function parseColorMap(raw: unknown): Record<string, string> {
  const out: Record<string, string> = {};
  String(raw ?? '').split(',').forEach((pair) => {
    const [k, v] = pair.split(':').map((s) => s.trim());
    if (k && v) out[k] = v;
  });
  return out;
}
function serializeColorMap(map: Record<string, string>): string {
  return Object.entries(map).filter(([, v]) => v).map(([k, v]) => `${k}:${v}`).join(',');
}

/** Elementor-style repeater. Renders the ordered items (from the field's `key`
 * comma-string) as collapsible rows — each with an icon, name, per-item Color
 * (Official / Custom, stored in the companion `colorKey` map), and
 * duplicate/delete. An "+ Add Item" button appends any option not yet present.
 * Reads/writes ONLY the two string fields, so the PHP renderer is unchanged. */
function RepeaterControl({
  field, content, setField,
}: {
  field: WidgetField;
  content: Record<string, unknown>;
  setField: (key: string, val: unknown) => void;
}) {
  const options = field.options ?? [];
  const optByValue = new Map(options.map((o) => [o.value, o]));
  const items = String(content[field.key] ?? '').split(',').map((s) => s.trim()).filter(Boolean);
  const colorMap = field.colorKey ? parseColorMap(content[field.colorKey]) : {};
  const [openIdx, setOpenIdx] = React.useState<number>(0);

  const writeItems = (next: string[]) => setField(field.key, next.join(','));
  const writeColor = (itemKey: string, color: string | null) => {
    if (!field.colorKey) return;
    const next = { ...colorMap };
    if (color) next[itemKey] = color; else delete next[itemKey];
    setField(field.colorKey, serializeColorMap(next));
  };

  const removeAt = (i: number) => {
    const removed = items[i];
    const next = items.filter((_, idx) => idx !== i);
    writeItems(next);
    // If no other item uses this key, drop its color entry too.
    if (field.colorKey && !next.includes(removed)) writeColor(removed, null);
  };
  const duplicateAt = (i: number) => {
    const next = [...items];
    next.splice(i + 1, 0, items[i]);
    writeItems(next);
  };

  // Options not yet in the list — offered by the "+ Add Item" picker.
  const available = options.filter((o) => !items.includes(o.value));
  const [adding, setAdding] = React.useState(false);
  const addItem = (v: string) => { writeItems([...items, v]); setAdding(false); setOpenIdx(items.length); };

  return (
    <div className="jalpi-repeater">
      {items.map((itemKey, i) => {
        const opt = optByValue.get(itemKey);
        const isOpen = openIdx === i;
        const official = opt?.color ?? '#666';
        const custom = colorMap[itemKey];
        const mode = custom ? 'custom' : 'official';
        return (
          <div key={`${itemKey}-${i}`} className={`jalpi-repeater__item${isOpen ? ' is-open' : ''}`}>
            <div className="jalpi-repeater__head" onClick={() => setOpenIdx(isOpen ? -1 : i)}>
              <span className="jalpi-repeater__glyph" style={{ color: custom || official }} aria-hidden="true">{opt?.glyph ?? '•'}</span>
              <span className="jalpi-repeater__name">{opt?.label ?? itemKey}</span>
              <button type="button" className="jalpi-repeater__btn" title="Duplicate" onClick={(e) => { e.stopPropagation(); duplicateAt(i); }}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
              </button>
              <button type="button" className="jalpi-repeater__btn" title="Delete" onClick={(e) => { e.stopPropagation(); removeAt(i); }}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </div>
            {isOpen && (
              <div className="jalpi-repeater__body">
                {field.colorKey && (
                  <>
                    <div className="jalpi-prop-row jalpi-prop-row--stack">
                      <label className="jalpi-prop-row__label">Color</label>
                      <div className="jalpi-prop-row__control">
                        <select
                          className="jalpi-select"
                          value={mode}
                          onChange={(e) => writeColor(itemKey, e.target.value === 'custom' ? (custom || official) : null)}
                        >
                          <option value="official">Official Color</option>
                          <option value="custom">Custom</option>
                        </select>
                      </div>
                    </div>
                    {mode === 'custom' && (
                      <div className="jalpi-prop-row">
                        <label className="jalpi-prop-row__label">Custom Color</label>
                        <div className="jalpi-prop-row__control">
                          <input
                            type="color"
                            className="jalpi-input jalpi-input--color"
                            value={custom || official}
                            onChange={(e) => writeColor(itemKey, e.target.value)}
                          />
                        </div>
                      </div>
                    )}
                  </>
                )}
              </div>
            )}
          </div>
        );
      })}

      {adding && available.length > 0 && (
        <div className="jalpi-repeater__picker">
          {available.map((o) => (
            <button key={o.value} type="button" className="jalpi-repeater__pick" onClick={() => addItem(o.value)}>
              <span className="jalpi-repeater__glyph" style={{ color: o.color }} aria-hidden="true">{o.glyph}</span>
              {o.label}
            </button>
          ))}
        </div>
      )}

      <button
        type="button"
        className="jalpi-repeater__add"
        disabled={available.length === 0}
        onClick={() => setAdding((a) => !a)}
      >
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
        Add Item
      </button>
    </div>
  );
}

type RepeaterItem = Record<string, unknown>;

/** Decode the JSON-string items list into an array of objects (tolerant of a
 * bad/empty value). */
function parseItems(raw: unknown): RepeaterItem[] {
  if (Array.isArray(raw)) return raw as RepeaterItem[];
  try {
    const v = JSON.parse(String(raw ?? '[]'));
    return Array.isArray(v) ? v : [];
  } catch {
    return [];
  }
}

/** A single control inside an itemsRepeater row — same widgets as the top-level
 * FieldControl but scoped to one item's sub-field. */
function ItemFieldControl({ field, value, onChange }: { field: WidgetField; value: unknown; onChange: (v: unknown) => void }) {
  return <FieldControl field={field} value={value} onChange={onChange} />;
}

/** Elementor-style rich repeater: an ordered list of OBJECT items, each editable
 * via its own sub-fields (icon-library picker, link, color…). The whole list is
 * stored as a JSON string in the field's key, so the PHP text sanitizer keeps it
 * intact and a dedicated PHP renderer decodes it. Add / duplicate / delete /
 * collapse per item, plus an "+ Add Item" that appends a blank item. */
function ItemsRepeaterControl({
  field, content, setField,
}: {
  field: WidgetField;
  content: Record<string, unknown>;
  setField: (key: string, val: unknown) => void;
}) {
  const items = parseItems(content[field.key]);
  const subFields = field.itemFields ?? [];
  const [openIdx, setOpenIdx] = React.useState<number>(items.length ? 0 : -1);
  // When addWithIconPicker is set, "+ Add Item" opens the icon library first.
  const [pickerOpen, setPickerOpen] = React.useState(false);

  const write = (next: RepeaterItem[]) => setField(field.key, JSON.stringify(next));
  const setItemField = (i: number, key: string, val: unknown) => {
    const next = items.map((it, idx) => (idx === i ? { ...it, [key]: val } : it));
    write(next);
  };
  const blankItem = (): RepeaterItem => {
    const blank: RepeaterItem = {};
    subFields.forEach((f) => { blank[f.key] = ''; });
    return blank;
  };
  const addItem = () => {
    if (field.addWithIconPicker) { setPickerOpen(true); return; }
    const next = [...items, blankItem()];
    write(next);
    setOpenIdx(next.length - 1);
  };
  // Called when an icon is chosen from the library — appends a new item whose
  // `icon` sub-field is the picked value.
  const addItemWithIcon = (icon: unknown) => {
    const item = blankItem();
    item.icon = icon;
    const next = [...items, item];
    write(next);
    setOpenIdx(next.length - 1);
    setPickerOpen(false);
  };
  const duplicateAt = (i: number) => {
    const next = [...items];
    next.splice(i + 1, 0, { ...items[i] });
    write(next);
  };
  const removeAt = (i: number) => write(items.filter((_, idx) => idx !== i));

  // Header label: the item's title-key value (e.g. its link), else "Item N".
  const titleFor = (it: RepeaterItem, i: number): string => {
    const t = field.itemTitleKey ? String(it[field.itemTitleKey] ?? '').trim() : '';
    return t || `Item #${i + 1}`;
  };

  return (
    <div className="jalpi-repeater">
      {items.map((it, i) => {
        const isOpen = openIdx === i;
        return (
          <div key={i} className={`jalpi-repeater__item${isOpen ? ' is-open' : ''}`}>
            <div className="jalpi-repeater__head" onClick={() => setOpenIdx(isOpen ? -1 : i)}>
              <span className="jalpi-repeater__name">{titleFor(it, i)}</span>
              <button type="button" className="jalpi-repeater__btn" title="Duplicate" onClick={(e) => { e.stopPropagation(); duplicateAt(i); }}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
              </button>
              <button type="button" className="jalpi-repeater__btn" title="Delete" onClick={(e) => { e.stopPropagation(); removeAt(i); }}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </div>
            {isOpen && (
              <div className="jalpi-repeater__body">
                {subFields.map((f) => {
                  const stack = f.control === 'icon' || f.control === 'url' || f.control === 'textarea';
                  return (
                    <div key={f.key} className={`jalpi-prop-row${stack ? ' jalpi-prop-row--stack' : ''}`}>
                      <label className="jalpi-prop-row__label" title={f.label}>{f.label}</label>
                      <div className="jalpi-prop-row__control">
                        <ItemFieldControl field={f} value={it[f.key]} onChange={(v) => setItemField(i, f.key, v)} />
                      </div>
                    </div>
                  );
                })}
              </div>
            )}
          </div>
        );
      })}
      <button type="button" className="jalpi-repeater__add" onClick={addItem}>
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
        {field.addLabel ?? 'Add Item'}
      </button>
      {pickerOpen && (
        <IconPickerModal
          onChange={(icon) => addItemWithIcon(icon)}
          onClose={() => setPickerOpen(false)}
        />
      )}
    </div>
  );
}

/** Renders one tab's worth of schema fields as PropGroups. Shared by the
 * Content tab (default fields) and the Style tab (fields marked tab:'style'
 * — see GenericStyleExtras). */
function FieldGroups({ node, store, tab }: { node: CBNode; store: EditorStore; tab: 'content' | 'style' }) {
  const all = getWidgetFields(node.type);
  const fields = (all ?? []).filter((f) => (f.tab ?? 'content') === tab);
  if (fields.length === 0) return null;

  const content = (node.content ?? {}) as Record<string, unknown>;
  const setField = (key: string, val: unknown) =>
    store.updateNode(node.id, { content: { ...content, [key]: val } });

  // Conditional visibility — hide fields whose showIf condition isn't met, so
  // unused controls (e.g. gradient fields when Type=Classic) don't show.
  const isVisible = (f: WidgetField): boolean => {
    if (!f.showIf) return true;
    const cur = content[f.showIf.key];
    if (f.showIf.truthy !== undefined) return f.showIf.truthy ? !!cur : !cur;
    if (f.showIf.equals !== undefined) return String(cur ?? '') === f.showIf.equals;
    if (f.showIf.notEquals !== undefined) return String(cur ?? '') !== f.showIf.notEquals;
    return true;
  };
  const visibleFields = fields.filter(isVisible);

  // Group fields by their optional `group` (ungrouped first, under "Content").
  // Empty groups (all fields hidden) are dropped so no bare header shows.
  const groups: Array<{ label: string; fields: WidgetField[] }> = [];
  const byLabel = new Map<string, WidgetField[]>();
  for (const f of visibleFields) {
    const label = f.group ?? 'Content';
    if (!byLabel.has(label)) { byLabel.set(label, []); groups.push({ label, fields: byLabel.get(label)! }); }
    byLabel.get(label)!.push(f);
  }

  return (
    <>
      {groups.map((g) => (
        <div key={g.label} className="jalpi-prop-group is-open">
          <div className="jalpi-prop-group__header" style={{ cursor: 'default' }}>
            <div className="jalpi-prop-group__header-left">
              <span className="jalpi-prop-group__title">{g.label}</span>
            </div>
          </div>
          <div className="jalpi-prop-group__body">
            {g.fields.map((f) => {
              // Repeater needs the whole content object (it reads/writes two
              // keys), so it renders as its own full-width block, no label row.
              if (f.control === 'repeater') {
                return (
                  <RepeaterControl
                    key={f.key}
                    field={f}
                    content={content}
                    setField={setField}
                  />
                );
              }
              // Rich object-list repeater (icon + link + color per item).
              if (f.control === 'itemsRepeater') {
                return (
                  <ItemsRepeaterControl
                    key={f.key}
                    field={f}
                    content={content}
                    setField={setField}
                  />
                );
              }
              // Segmented button groups read best full-width, so stack the
              // label above the control (Elementor-style) for that control only.
              const stack = f.control === 'buttongroup' || f.control === 'multiicon' || f.control === 'image' || f.control === 'slider' || f.control === 'dimensions4';
              return (
                <div key={f.key} className={`jalpi-prop-row${stack ? ' jalpi-prop-row--stack' : ''}`}>
                  <label className="jalpi-prop-row__label" title={f.label}>{f.label}</label>
                  <div className="jalpi-prop-row__control">
                    <FieldControl
                      field={f}
                      value={content[f.key]}
                      onChange={(v) => setField(f.key, v)}
                    />
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      ))}
    </>
  );
}

export function GenericContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const all = getWidgetFields(node.type);
  if (!all || all.length === 0) return null;
  return (
    <div className="jalpi-props-section">
      <FieldGroups node={node} store={store} tab="content" />
    </div>
  );
}

/** Widget-specific style controls (fields marked tab:'style'), rendered at the
 * top of the generic Style tab so style-ish controls live where users expect
 * them — alongside Background/Border/Spacing. Returns null for bespoke
 * widgets and widgets with no style fields. */
export function GenericStyleExtras({ node, store }: { node: CBNode; store: EditorStore }) {
  return <FieldGroups node={node} store={store} tab="style" />;
}
