'use client';

import { Check, ChevronDown, ChevronRight, Info, Loader2, Settings2, X } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { cn } from '@/lib/utils';

// ─────────────────────────────────────────────────────────────────────────────
// Filter-Visibility — pro Tab (business/evb/orders) speichert der User welche
// Filter-Felder in der Sidebar überhaupt erscheinen sollen. Storage-Key-Prefix
// spiegelt das ColumnPicker-Pattern (`mailforge.colpicker.X` → `mailforge.filtervis.X`).
//
// Wichtig: Verstecken löscht NICHT den State. Wenn ein Filter mit Wert versteckt
// wird, bleibt er aktiv (filtert weiter). Das spart Daten beim Toggle, kann aber
// zu Verwirrung führen — daher zeigen wir "X/Y aktiv" im Picker-Button.
// ─────────────────────────────────────────────────────────────────────────────

export interface FilterDef {
  /** Stable Key für Storage + Visibility-Lookup. Nicht das State-Property — eine
   *  Visibility-ID kann mehrere State-Felder gruppieren (z.B. CollapsibleSection). */
  key: string;
  /** Human-readable Label im Picker-Popover. */
  label: string;
  /** Optionale Section-Gruppierung im Popover (z.B. "API-Filter", "Klassifizierung"). */
  group?: string;
  /**
   * Initial-Sichtbarkeit, wenn der User noch keine eigene Auswahl getroffen hat
   * (LocalStorage leer). `true` = sichtbar by default, `undefined`/`false` = versteckt.
   *
   * Whitelist-Default: nur explizit als `defaultVisible: true` markierte Filter
   * erscheinen initial. Sobald der User toggelt, wird seine Auswahl in
   * localStorage persistiert und ersetzt diese Defaults.
   */
  defaultVisible?: boolean;
}

const FILTER_VIS_STORAGE_PREFIX = 'mailforge.filtervis.';

/** null = "alle visible" (Initial-State, niemand hat noch nichts angefasst). */
type VisibilityState = string[] | null;

export function loadFilterVisibility(tabKey: string): VisibilityState {
  if (typeof window === 'undefined') return null;
  try {
    const raw = localStorage.getItem(FILTER_VIS_STORAGE_PREFIX + tabKey);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    if (Array.isArray(parsed) && parsed.every((x) => typeof x === 'string')) return parsed;
  } catch {
    /* ignore — fallback to all-visible */
  }
  return null;
}

export function saveFilterVisibility(tabKey: string, keys: string[] | null): void {
  if (typeof window === 'undefined') return;
  try {
    if (keys === null) localStorage.removeItem(FILTER_VIS_STORAGE_PREFIX + tabKey);
    else localStorage.setItem(FILTER_VIS_STORAGE_PREFIX + tabKey, JSON.stringify(keys));
  } catch {
    /* quota exceeded — ignore */
  }
}

/**
 * Hook: liefert `isVisible(key)` Predicate + State-Setter. Initial-State ist null
 * (= kuratierte Defaults aus `FilterDef.defaultVisible`), wird beim Mount aus
 * localStorage hydratet. Sobald der User toggelt, ersetzt seine Auswahl die Defaults.
 */
export function useFilterVisibility(tabKey: string, allDefs: FilterDef[]) {
  const [visible, setVisible] = useState<VisibilityState>(null);

  useEffect(() => {
    setVisible(loadFilterVisibility(tabKey));
  }, [tabKey]);

  // null = noch keine User-Auswahl → curated Defaults aus FilterDef.defaultVisible.
  // Sobald der User irgendetwas toggelt, wird `visible` materialisiert und gespeichert.
  const isVisible = (key: string): boolean => {
    if (visible === null) {
      const def = allDefs.find((d) => d.key === key);
      return def?.defaultVisible === true;
    }
    return visible.includes(key);
  };

  const setKeys = (next: string[] | null) => {
    setVisible(next);
    saveFilterVisibility(tabKey, next);
  };

  const visibleCount =
    visible === null
      ? allDefs.filter((d) => d.defaultVisible === true).length
      : visible.filter((k) => allDefs.some((d) => d.key === k)).length;
  const totalCount = allDefs.length;

  return { isVisible, setKeys, visible, visibleCount, totalCount };
}

/**
 * Settings-Popover für Filter-Sichtbarkeit. Render in Filter-Sidebar als Header.
 * Click-outside schließt (analog ColumnPicker). Reset-Button setzt auf null = alle.
 */
export function FilterVisibilityPicker({
  tabKey,
  defs,
}: {
  tabKey: string;
  defs: FilterDef[];
}) {
  const { isVisible, setKeys, visible, visibleCount, totalCount } = useFilterVisibility(tabKey, defs);
  const [open, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!open) return;
    const handler = (e: MouseEvent) => {
      if (!ref.current?.contains(e.target as Node)) setOpen(false);
    };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [open]);

  const toggle = (key: string) => {
    // Materialisiere null → kuratierte Defaults aus FilterDef.defaultVisible, dann toggle.
    // So bleibt die User-Auswahl nach der ersten Interaktion explizit gegen die Defaults
    // definiert (= bei späterem Hinzufügen neuer Defs erweitert sich seine Auswahl nicht ungewollt).
    const current = visible === null ? defs.filter((d) => d.defaultVisible === true).map((d) => d.key) : visible;
    const next = current.includes(key) ? current.filter((k) => k !== key) : [...current, key];
    setKeys(next);
  };

  // Group-by group für Section-Rendering
  const grouped = defs.reduce<Record<string, FilterDef[]>>((acc, d) => {
    const g = d.group ?? '';
    (acc[g] ??= []).push(d);
    return acc;
  }, {});
  const groupOrder = Object.keys(grouped);

  return (
    <div ref={ref} className="relative inline-block w-full">
      <button
        type="button"
        onClick={() => setOpen((o) => !o)}
        className={cn(
          'inline-flex w-full items-center justify-between gap-1.5 rounded-md border border-zinc-200 bg-white px-2.5 py-1.5 text-[11px] font-medium text-zinc-700 transition hover:border-zinc-300 hover:bg-zinc-50',
          'dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800',
        )}
      >
        <span className="inline-flex items-center gap-1.5">
          <Settings2 className="h-3 w-3" />
          Sichtbare Filter
        </span>
        <span className="rounded bg-zinc-100 px-1 py-0.5 text-[9px] font-semibold text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400">
          {visibleCount}/{totalCount}
        </span>
      </button>

      {open && (
        <div className="absolute left-0 top-full z-30 mt-1 w-full max-w-xs rounded-lg border border-zinc-200 bg-white shadow-xl dark:border-zinc-800 dark:bg-zinc-950">
          <div className="flex items-center justify-between border-b border-zinc-200 px-3 py-2 dark:border-zinc-800">
            <span className="text-xs font-semibold text-zinc-700 dark:text-zinc-300">Filter anzeigen</span>
            <button
              type="button"
              onClick={() => setOpen(false)}
              className="rounded p-0.5 text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800"
              aria-label="Schließen"
            >
              <X className="h-3.5 w-3.5" />
            </button>
          </div>
          <div className="max-h-96 overflow-y-auto p-1">
            {groupOrder.map((groupName) => (
              <div key={groupName || '_'}>
                {groupName && (
                  <div className="mt-2 mb-0.5 px-2 text-[9px] font-semibold uppercase tracking-wider text-zinc-400 first:mt-0">
                    {groupName}
                  </div>
                )}
                {grouped[groupName].map((def) => {
                  const checked = isVisible(def.key);
                  return (
                    <button
                      key={def.key}
                      type="button"
                      onClick={() => toggle(def.key)}
                      className={cn(
                        'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs transition',
                        checked
                          ? 'bg-pink-50 text-pink-900 hover:bg-pink-100 dark:bg-pink-950/30 dark:text-pink-100'
                          : 'text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800',
                      )}
                    >
                      <span
                        className={cn(
                          'flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border',
                          checked ? 'border-[#ff006a] bg-[#ff006a]' : 'border-zinc-300 dark:border-zinc-600',
                        )}
                      >
                        {checked && <Check className="h-2.5 w-2.5 text-white" />}
                      </span>
                      <span className="flex-1">{def.label}</span>
                    </button>
                  );
                })}
              </div>
            ))}
          </div>
          <div className="flex items-center justify-between gap-2 border-t border-zinc-200 px-3 py-2 text-[10px] text-zinc-500 dark:border-zinc-800">
            <button
              type="button"
              onClick={() => setKeys(null)}
              title="Auf kuratierte Default-Filter zurücksetzen"
              className="text-zinc-600 underline hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-200"
            >
              Standard
            </button>
            <button
              type="button"
              onClick={() => setKeys(defs.map((d) => d.key))}
              title="Alle Filter sichtbar machen"
              className="text-zinc-600 underline hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-200"
            >
              Alle anzeigen
            </button>
            <button
              type="button"
              onClick={() => setKeys([])}
              className="text-zinc-600 underline hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-200"
            >
              Alle verstecken
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

export function FilterField({
  label,
  hint,
  layout = 'inline',
  children,
}: {
  label: string;
  hint?: string;
  layout?: 'inline' | 'stacked';
  children: React.ReactNode;
}) {
  if (layout === 'stacked') {
    return (
      <div className="flex flex-col gap-0.5">
        <label className="text-[11px] font-medium text-zinc-700 leading-tight dark:text-zinc-300">
          {label} {hint && <span className="text-[10px] font-normal text-zinc-400">· {hint}</span>}
        </label>
        {children}
      </div>
    );
  }
  return (
    <div className="grid grid-cols-[90px_1fr] items-center gap-x-2">
      <label
        className="truncate text-[11px] font-medium leading-tight text-zinc-700 dark:text-zinc-300"
        title={label}
      >
        {label}
      </label>
      <div className="min-w-0">{children}</div>
      {hint && (
        <div className="col-start-2 text-[10px] font-normal leading-tight text-zinc-400">{hint}</div>
      )}
    </div>
  );
}

export function FilterInput({
  value,
  onChange,
  placeholder,
  type = 'text',
}: {
  value: string;
  onChange: (v: string) => void;
  placeholder?: string;
  type?: string;
}) {
  return (
    <input
      type={type}
      value={value}
      onChange={(e) => onChange(e.target.value)}
      placeholder={placeholder}
      className="w-full rounded-md border border-zinc-200 bg-white px-2 py-1 text-xs focus:border-zinc-900 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900"
    />
  );
}

/** Multi-Select Checkbox-Liste — generisch für alle Code-Maps. */
export function CheckboxGroup<K extends string>({
  options,
  value,
  onChange,
  compact = false,
}: {
  options: Record<K, string>;
  value: K[];
  onChange: (next: K[]) => void;
  compact?: boolean;
}) {
  const toggle = (code: K) => {
    if (value.includes(code)) onChange(value.filter((c) => c !== code));
    else onChange([...value, code]);
  };
  const entries = Object.entries(options) as [K, string][];
  return (
    <div className={cn('grid gap-1', compact ? 'grid-cols-2' : 'grid-cols-1')}>
      {entries.map(([code, label]) => (
        <label
          key={code}
          className="flex cursor-pointer items-center gap-1.5 rounded px-1.5 py-1 text-[11px] hover:bg-zinc-100 dark:hover:bg-zinc-800"
        >
          <input
            type="checkbox"
            checked={value.includes(code)}
            onChange={() => toggle(code)}
            className="h-3 w-3"
          />
          <span className="font-mono text-[10px] text-zinc-500">{code}</span>
          <span className="truncate text-zinc-700 dark:text-zinc-300">{label}</span>
        </label>
      ))}
    </div>
  );
}

/**
 * Multi-Select für eine flache String-Liste (dynamisch aus Facets).
 * Optional `counts`: Record<value, count> aus dem Schema-Endpoint — wenn gesetzt,
 * wird neben jedem Pill der Count gerendert ("ITS (24.391)") und Werte mit
 * count=0 werden ausgegraut + nicht klickbar (no-op-Pill).
 *
 * Optional `allowCustomAdd`: rendert ein Input-Feld unter den Pills, mit dem
 * der User Werte manuell hinzufügen kann. Notwendig für Felder wo das Sample
 * (5×100 Records bei `getFacets`, 100 bei `/api/crm/schema`) seltene Werte
 * verfehlt — z.B. its_ksm: KSM/SPA/SSPA tauchen evtl. nicht im Sample auf,
 * existieren aber in der DB. Manuell hinzugefügte Werte werden direkt aktiviert
 * und in `state.client.itsKsmIn` aufgenommen — Filter läuft client-side gegen
 * die geladenen Records (siehe applyBusinessClientFilters).
 */
export function StringMultiSelect({
  options,
  value,
  onChange,
  emptyLabel,
  counts,
  allowCustomAdd = false,
  customAddPlaceholder,
}: {
  options: string[];
  value: string[];
  onChange: (next: string[]) => void;
  emptyLabel?: string;
  counts?: Record<string, number>;
  allowCustomAdd?: boolean;
  customAddPlaceholder?: string;
}) {
  const [extraOptions, setExtraOptions] = useState<string[]>([]);
  const [draft, setDraft] = useState('');

  const toggle = (v: string) => {
    if (value.includes(v)) onChange(value.filter((x) => x !== v));
    else onChange([...value, v]);
  };

  const addCustom = () => {
    const v = draft.trim();
    if (!v) return;
    // Case-insensitive Dedup gegen options + extraOptions
    const known = new Set([...options, ...extraOptions].map((x) => x.toLowerCase()));
    if (!known.has(v.toLowerCase())) {
      setExtraOptions((prev) => [...prev, v]);
    }
    // Wert direkt aktivieren (auch wenn er schon in der Liste war)
    if (!value.some((x) => x.toLowerCase() === v.toLowerCase())) {
      onChange([...value, v]);
    }
    setDraft('');
  };

  // Merge options + extraOptions, dedupe (extraOptions zuerst sodass Custom-Werte priorisiert
  // bleiben sollte sich später ein Schema-Refresh denselben Wert ergeben).
  const allOptions = (() => {
    if (extraOptions.length === 0) return options;
    const seen = new Set<string>();
    const out: string[] = [];
    for (const v of [...extraOptions, ...options]) {
      const k = v.toLowerCase();
      if (!seen.has(k)) {
        seen.add(k);
        out.push(v);
      }
    }
    return out;
  })();

  if (allOptions.length === 0 && !allowCustomAdd) {
    return <p className="rounded border border-dashed border-zinc-300 px-2 py-1 text-[10px] text-zinc-500 dark:border-zinc-700">{emptyLabel ?? 'Keine Werte verfügbar'}</p>;
  }
  // Counts-Sortierung: wenn Counts vorhanden, sortiere absteigend → relevanteste Werte zuerst
  const sortedOptions = counts
    ? [...allOptions].sort((a, b) => (counts[b] ?? 0) - (counts[a] ?? 0))
    : allOptions;
  return (
    <div className="flex flex-col gap-1.5">
      {sortedOptions.length > 0 && (
        <div className="flex flex-wrap gap-1">
          {sortedOptions.map((opt) => {
            const active = value.includes(opt);
            const count = counts?.[opt];
            const empty = counts !== undefined && (count === undefined || count === 0);
            return (
              <button
                key={opt}
                type="button"
                onClick={() => toggle(opt)}
                disabled={empty && !active}
                title={count !== undefined ? `${opt}: ${count.toLocaleString('de-DE')} Datensätze` : opt}
                className={cn(
                  'rounded-full border px-2 py-0.5 text-[10px] font-mono transition',
                  active
                    ? 'border-zinc-900 bg-zinc-900 text-white dark:border-zinc-100 dark:bg-zinc-100 dark:text-zinc-900'
                    : empty
                      ? 'border-zinc-100 bg-zinc-50 text-zinc-400 italic cursor-not-allowed dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-600'
                      : 'border-zinc-200 bg-white text-zinc-700 hover:bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300',
                )}
              >
                {opt}
                {count !== undefined && (
                  <span className={cn('ml-1', active ? 'opacity-70' : 'opacity-50')}>
                    ({count.toLocaleString('de-DE')})
                  </span>
                )}
              </button>
            );
          })}
        </div>
      )}
      {allowCustomAdd && (
        <div className="flex items-center gap-1">
          <input
            type="text"
            value={draft}
            onChange={(e) => setDraft(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === 'Enter') {
                e.preventDefault();
                addCustom();
              }
            }}
            placeholder={customAddPlaceholder ?? 'Eigenen Wert hinzufügen…'}
            className="flex-1 rounded-md border border-zinc-200 bg-white px-2 py-0.5 text-[10px] font-mono focus:border-zinc-900 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900"
          />
          <button
            type="button"
            onClick={addCustom}
            disabled={!draft.trim()}
            className="rounded-md border border-zinc-200 bg-white px-2 py-0.5 text-[10px] font-medium text-zinc-700 transition hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-40 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800"
          >
            + Add
          </button>
        </div>
      )}
    </div>
  );
}

/** Tri-State Toggle: undefined = "egal", true = "muss", false = "darf nicht". */
export function TriStateToggle({
  label,
  value,
  onChange,
}: {
  label: string;
  value: boolean | undefined;
  onChange: (v: boolean | undefined) => void;
}) {
  const states: Array<{ v: boolean | undefined; label: string }> = [
    { v: undefined, label: 'egal' },
    { v: true, label: 'ja' },
    { v: false, label: 'nein' },
  ];
  return (
    <div className="flex items-center justify-between gap-2">
      <span className="text-[11px] text-zinc-700 dark:text-zinc-300">{label}</span>
      <div className="flex gap-0.5 rounded-md border border-zinc-200 p-0.5 dark:border-zinc-700">
        {states.map((s) => (
          <button
            key={String(s.v)}
            type="button"
            onClick={() => onChange(s.v)}
            className={cn(
              'rounded px-2 py-0.5 text-[10px] transition',
              value === s.v
                ? 'bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900'
                : 'text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800',
            )}
          >
            {s.label}
          </button>
        ))}
      </div>
    </div>
  );
}

export function CollapsibleSection({
  title,
  defaultOpen = false,
  children,
}: {
  title: string;
  defaultOpen?: boolean;
  children: React.ReactNode;
}) {
  const [open, setOpen] = useState(defaultOpen);
  return (
    <div>
      <button
        type="button"
        onClick={() => setOpen(!open)}
        className="flex w-full items-center gap-1 text-[11px] font-semibold uppercase tracking-wider text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100"
      >
        {open ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
        {title}
      </button>
      {open && (
        <div className="mt-1.5 space-y-1 rounded-md border border-zinc-100 bg-zinc-50/50 p-2 dark:border-zinc-900 dark:bg-zinc-900/30">
          {children}
        </div>
      )}
    </div>
  );
}

export function SectionLabel({ children }: { children: React.ReactNode }) {
  return (
    <div className="mt-3 mb-1 text-[11px] font-semibold uppercase tracking-wider text-zinc-500 first:mt-0">
      {children}
    </div>
  );
}

export function FilterHint({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false);
  return (
    <button
      type="button"
      onClick={() => setOpen((o) => !o)}
      className={cn(
        'block w-full rounded-md border border-zinc-200 bg-zinc-50 px-2 py-1 text-left text-[10px] leading-snug text-zinc-600 hover:bg-zinc-100 dark:border-zinc-800 dark:bg-zinc-900/50 dark:text-zinc-400',
        !open && 'line-clamp-2',
      )}
      title={open ? 'Klick zum Einklappen' : 'Klick für ganzen Text'}
    >
      {children}
    </button>
  );
}

/**
 * Sample-Hinweis-Banner für die Filter-Sidebar des Zielgruppen-Tabs.
 *
 * Die dynamischen Multi-Select-Filter (Reseller, its_ksm, Versicherer, Produkt,
 * SchemaBrowser-Enums) leiten ihre Pill-Werte aus einem Datenbank-Sample ab —
 * standardmäßig 5 × 100 Records bei Facets, 1 × 100 bei Schema. Bei großen
 * Tabellen (143k Business / 380k EVB) sind das nicht alle existierenden Werte.
 *
 * Banner zeigt aktuellen Sample-Umfang und einen Inkrement-Button "+100 laden".
 * Der Button wird vom Parent (RecipientPicker) verdrahtet, der dann beide
 * Endpoints mit `?pages=N&refresh=1` neu zieht und die Antworten in den State
 * merged (siehe loadMoreSamples()).
 */
export function SampleSizeBanner({
  totalSampled,
  loading,
  onLoadMore,
}: {
  totalSampled: number;
  loading: boolean;
  onLoadMore: () => void;
}) {
  return (
    <div className="flex items-center gap-2 rounded-md border border-blue-200 bg-blue-50 px-2 py-1.5 text-[10px] leading-snug text-blue-900 dark:border-blue-900/50 dark:bg-blue-950/30 dark:text-blue-200">
      <Info className="h-3 w-3 shrink-0" />
      <span className="flex-1">
        Filter-Werte aus den ersten <span className="font-semibold">{totalSampled.toLocaleString('de-DE')}</span> Datensätzen — seltene Werte fehlen evtl.
      </span>
      <button
        type="button"
        onClick={onLoadMore}
        disabled={loading}
        className={cn(
          'inline-flex shrink-0 items-center gap-1 rounded border border-blue-300 bg-white px-1.5 py-0.5 text-[10px] font-medium text-blue-900 transition hover:bg-blue-100 disabled:cursor-not-allowed disabled:opacity-50',
          'dark:border-blue-800 dark:bg-blue-950/50 dark:text-blue-100 dark:hover:bg-blue-900/40',
        )}
        title="Lädt 100 weitere Datensätze und erweitert die Filter-Listen"
      >
        {loading ? <Loader2 className="h-2.5 w-2.5 animate-spin" /> : '+'}
        100 laden
      </button>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Dynamic Filter (Phase 3) — Type-aware Filter-Controls für vom User dynamisch
// hinzugefügte Spalten aus dem SchemaBrowser. Jedes Element ist eine Discriminated
// Union nach `kind`, sodass TypeScript-safe per kind-switch das richtige Sub-Control
// und die richtige Filter-Anwendung gewählt werden kann.
// ─────────────────────────────────────────────────────────────────────────────

export type DynamicFilterValue =
  | { kind: 'string'; column: string; contains: string }
  | { kind: 'number'; column: string; min?: number; max?: number }
  | { kind: 'enum'; column: string; allowedValues: string[] }
  | { kind: 'date'; column: string; from?: string; to?: string }
  | { kind: 'boolean'; column: string; expected: boolean | undefined };

export function DynamicFilterControl({
  value,
  onChange,
  topValues,
}: {
  value: DynamicFilterValue;
  onChange: (next: DynamicFilterValue) => void;
  /** Sample-Top-Values aus Schema (für enum/string-Auto-Complete-Hints) */
  topValues?: Array<{ value: string; count: number }>;
}) {
  switch (value.kind) {
    case 'string':
      return (
        <FilterInput
          value={value.contains}
          onChange={(v) => onChange({ ...value, contains: v })}
          placeholder={topValues && topValues.length > 0 ? `z.B. „${topValues[0].value}"` : 'Substring oder Wert'}
        />
      );
    case 'number':
      return (
        <div className="flex items-center gap-1">
          <FilterInput
            value={value.min?.toString() ?? ''}
            onChange={(v) => onChange({ ...value, min: v ? Number(v) : undefined })}
            placeholder="min"
            type="number"
          />
          <span className="text-[10px] text-zinc-400">–</span>
          <FilterInput
            value={value.max?.toString() ?? ''}
            onChange={(v) => onChange({ ...value, max: v ? Number(v) : undefined })}
            placeholder="max"
            type="number"
          />
        </div>
      );
    case 'enum':
      return (
        <StringMultiSelect
          options={(topValues ?? []).map((tv) => tv.value)}
          value={value.allowedValues}
          onChange={(next) => onChange({ ...value, allowedValues: next })}
          emptyLabel="Keine Werte aus Schema bekannt — eigenen Wert eintragen"
          allowCustomAdd
          customAddPlaceholder="Eigener Wert (nicht im Sample)…"
        />
      );
    case 'date':
      return (
        <div className="flex items-center gap-1">
          <FilterInput
            value={value.from ?? ''}
            onChange={(v) => onChange({ ...value, from: v || undefined })}
            type="date"
          />
          <span className="text-[10px] text-zinc-400">–</span>
          <FilterInput
            value={value.to ?? ''}
            onChange={(v) => onChange({ ...value, to: v || undefined })}
            type="date"
          />
        </div>
      );
    case 'boolean':
      return (
        <TriStateToggle
          label=""
          value={value.expected}
          onChange={(v) => onChange({ ...value, expected: v })}
        />
      );
    default:
      return null;
  }
}

/** Default-Initial-Wert für einen neuen Dynamic-Filter, basierend auf ColumnKind. */
export function makeDefaultDynamicFilter(column: string, kind: string): DynamicFilterValue {
  switch (kind) {
    case 'number': return { kind: 'number', column };
    case 'date': return { kind: 'date', column };
    case 'boolean': return { kind: 'boolean', column, expected: undefined };
    case 'enum': return { kind: 'enum', column, allowedValues: [] };
    default: return { kind: 'string', column, contains: '' };
  }
}

/** Wendet einen Dynamic-Filter client-seitig auf ein einzelnes raw-Record an. */
export function matchesDynamicFilter(rec: Record<string, unknown>, f: DynamicFilterValue): boolean {
  const v = rec[f.column];
  switch (f.kind) {
    case 'string':
      if (!f.contains) return true;
      return v != null && String(v).toLowerCase().includes(f.contains.toLowerCase());
    case 'number': {
      if (typeof v !== 'number') return f.min === undefined && f.max === undefined;
      if (f.min !== undefined && v < f.min) return false;
      if (f.max !== undefined && v > f.max) return false;
      return true;
    }
    case 'enum':
      if (f.allowedValues.length === 0) return true;
      return v != null && f.allowedValues.includes(String(v));
    case 'date': {
      if (typeof v !== 'string') return f.from === undefined && f.to === undefined;
      if (f.from && v < f.from) return false;
      if (f.to && v > f.to) return false;
      return true;
    }
    case 'boolean':
      if (f.expected === undefined) return true;
      return v === f.expected;
  }
}
