'use client';

import { useEffect, useMemo, useState, useCallback, forwardRef, useImperativeHandle } from 'react';
import {
  X,
  Loader2,
  Users,
  Search,
  AlertTriangle,
  Check,
  CheckCircle2,
  Database,
  FileUp,
  FileText,
  ShoppingBag,
  Tag,
  ChevronDown,
  ChevronRight,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { CsvSourcePanel } from './CsvSourcePanel';
import {
  BusinessFilters,
  DEFAULT_BUSINESS_STATE,
  serializeBusinessFilter,
  type BusinessFilterState,
} from './filters/BusinessFilters';
import {
  EvbFilters,
  DEFAULT_EVB_STATE,
  serializeEvbFilter,
  type EvbFilterState,
} from './filters/EvbFilters';
import {
  OrderFilters,
  DEFAULT_ORDER_STATE,
  serializeOrderFilter,
  type OrderFilterState,
} from './filters/OrderFilters';
import { FilterField, FilterInput, matchesDynamicFilter, type DynamicFilterValue } from './filters/_shared';
import { ColumnPicker, DEFAULT_COLUMNS, loadColumnsFromStorage } from './filters/ColumnPicker';
import type { ColumnInfo, SchemaSnapshot } from '@/lib/crm/cumas-client';

// ─────────────────────────────────────────────────────────────────────────────
// Types
// ─────────────────────────────────────────────────────────────────────────────

export interface PickerRecipient {
  email: string;
  firstName?: string;
  lastName?: string;
  fullName?: string;
  company?: string;
  source: 'business' | 'orders' | 'evb' | 'csv';
  cumasId?: number;
  /**
   * Pool-Discriminator für Login-Direktlinks (Kompass-Magic-Links).
   * Bei Cumas-Quellen wird er normalerweise aus `source` abgeleitet
   * (business → business, sonst private). Bei CSV-Uploads kommt er
   * aus dem Panel-Toggle. Wenn nicht gesetzt: Submit-Mapping in page.tsx
   * fällt auf die source-Heuristik zurück.
   */
  poolType?: 'private' | 'business';
  /**
   * Raw-Cumas-Record — Snapshot aller Felder für dynamische Spalten-Anzeige im Result-Table.
   * Bei `source='csv'` undefined.
   */
  rawRecord?: Record<string, unknown>;
}

type SourceMode = 'business' | 'evb' | 'orders' | 'csv';

export interface RecipientFilterSnapshot {
  source: SourceMode;
  business?: BusinessFilterState;
  evb?: EvbFilterState;
  orders?: OrderFilterState;
  maxRecords: number;
}

export interface RecipientPickerProps {
  open: boolean;
  onClose: () => void;
  onConfirm: (recipients: PickerRecipient[], snapshot: RecipientFilterSnapshot | null) => void;
  /** Pre-fill für Workspace-Filter aus dem Briefing (heuristisch: ITS/KSM → business, Spartarif → orders). */
  workspaceSlug?: string;
  /** Persistierter Filter-Snapshot — falls vorhanden, wird er als initial state geladen. */
  initialFilter?: RecipientFilterSnapshot;
  displayMode?: 'modal' | 'inline';
  /**
   * Optional: wird aufgerufen wenn sich der Loading-State ändert.
   * Nützlich für externe Trigger (z.B. AudienceFooter mit "Empfänger laden"-Button)
   * die einen Spinner/Disabled-Zustand reactive brauchen.
   */
  onLoadingChange?: (loading: boolean) => void;
}

/** Imperative API für Parent-Komponenten — exposed via forwardRef. */
export interface RecipientPickerHandle {
  /** Triggert das Laden der Empfänger mit aktueller Filter-Konfiguration. */
  loadPreview: () => void;
}

interface Facets {
  businessItsKsm: string[];
  businessReseller: string[];
  evbTyp: string[];
  evbGesellschaft: string[];
  evbReseller: string[];
  orderProducts: string[];
}

const EMPTY_FACETS: Facets = {
  businessItsKsm: [],
  businessReseller: [],
  evbTyp: [],
  evbGesellschaft: [],
  evbReseller: [],
  orderProducts: [],
};

// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Default mode/filters fürs Recipient-Picker-UI — generisch im Plugin-Build.
 * Wer Workspace-spezifische Vor-Filter braucht, kann hier per Workspace-Slug
 * mappen oder die UI per CrmAdapter-Hint vorbelegen.
 */
function defaultModeForBrand(_workspaceSlug?: string): SourceMode {
  return 'business';
}

function defaultBusinessForBrand(_workspaceSlug?: string): BusinessFilterState {
  return DEFAULT_BUSINESS_STATE;
}

function countActiveFilters<T extends Record<string, unknown>>(obj: T): number {
  let n = 0;
  for (const v of Object.values(obj)) {
    if (v === undefined || v === null) continue;
    if (typeof v === 'string' && v === '') continue;
    if (Array.isArray(v) && v.length === 0) continue;
    n++;
  }
  return n;
}

// ─────────────────────────────────────────────────────────────────────────────
// Component
// ─────────────────────────────────────────────────────────────────────────────

export const RecipientPicker = forwardRef<RecipientPickerHandle, RecipientPickerProps>(function RecipientPicker({
  open,
  onClose,
  onConfirm,
  workspaceSlug,
  initialFilter,
  displayMode = 'modal',
  onLoadingChange,
}, ref) {
  const isInline = displayMode === 'inline';

  // CSV-Headers — Spiegel des CsvSourcePanel-State, damit die PlaceholderLegend bei mode='csv'
  // die Spaltennamen der hochgeladenen CSV als Chips rendern kann. Wird via onHeadersChange-Callback gefüttert.
  const [csvHeaders, setCsvHeaders] = useState<string[]>([]);

  // Mode (Tab)
  const [mode, setMode] = useState<SourceMode>(initialFilter?.source ?? defaultModeForBrand(workspaceSlug));

  // Filter-States pro Tab
  const [businessState, setBusinessState] = useState<BusinessFilterState>(
    initialFilter?.business ?? defaultBusinessForBrand(workspaceSlug),
  );
  const [evbState, setEvbState] = useState<EvbFilterState>(initialFilter?.evb ?? DEFAULT_EVB_STATE);
  const [orderState, setOrderState] = useState<OrderFilterState>(initialFilter?.orders ?? DEFAULT_ORDER_STATE);

  // Limit
  const [maxRecords, setMaxRecords] = useState(initialFilter?.maxRecords ?? 200);

  // Facets
  const [facets, setFacets] = useState<Facets>(EMPTY_FACETS);
  const [facetsLoading, setFacetsLoading] = useState(false);

  // Schema (Phase 1) — pro Tab eine Liste der verfügbaren Spalten mit Type/TopValues
  const [schema, setSchema] = useState<SchemaSnapshot | null>(null);

  // Sample-Größe pro Endpoint (Pages-Count). Default = Server-Default. User kann
  // via SampleSizeBanner inkrementell mehr Pages nachladen, um seltene Distinct-
  // Werte zu erwischen (z.B. SPA/SSPA in its_ksm tauchen erst spät auf).
  // Beide Werte starten getrennt, weil schema/facets unterschiedliche Defaults haben
  // (5 Pages für Facets vs. 1 Page für Schema). loadMoreSamples() bumped beide um +1.
  const [sampleSize, setSampleSize] = useState<{ facets: number; schema: number }>({ facets: 5, schema: 1 });
  const [loadingMoreSamples, setLoadingMoreSamples] = useState(false);

  // Column-Picker-State (Phase 2) — pro Tab eine Auswahl, die in localStorage persistiert wird.
  // Initial leer; die echten Default-Werte werden im useEffect (clientside) aus localStorage hydratet.
  const [selectedBusinessCols, setSelectedBusinessCols] = useState<string[]>(DEFAULT_COLUMNS.business);
  const [selectedEvbCols, setSelectedEvbCols] = useState<string[]>(DEFAULT_COLUMNS.evb);
  const [selectedOrderCols, setSelectedOrderCols] = useState<string[]>(DEFAULT_COLUMNS.orders);

  // Dynamic-Filter-State (Phase 3) — pro Tab User-Choice an zusätzlichen Schema-Filtern.
  // Jede Tab hat ihre eigene Liste — wenn der User zwischen Tabs wechselt bleibt der Filter erhalten.
  const [dynamicBusinessFilters, setDynamicBusinessFilters] = useState<DynamicFilterValue[]>([]);
  const [dynamicEvbFilters, setDynamicEvbFilters] = useState<DynamicFilterValue[]>([]);
  const [dynamicOrderFilters, setDynamicOrderFilters] = useState<DynamicFilterValue[]>([]);

  // Facet-Counts aus dem Schema-Endpoint (topValues pro Spalte) — wird an die Multi-Select-Pills
  // gereicht, damit User die Cardinalitäten sieht ("ITS (24.391)").
  // Nur die Spalten die im curated UI genutzt werden — andere ignorieren wir.
  const facetCounts = useMemo(() => {
    if (!schema) return undefined;
    const toCountMap = (cols: ColumnInfo[], colName: string): Record<string, number> | undefined => {
      const col = cols.find((c) => c.name === colName);
      if (!col?.topValues) return undefined;
      const map: Record<string, number> = {};
      for (const tv of col.topValues) map[tv.value] = tv.count;
      return map;
    };
    return {
      business: {
        businessItsKsm: toCountMap(schema.business, 'its_ksm'),
        businessReseller: toCountMap(schema.business, 'reseller'),
      },
      evb: {
        evbGesellschaft: toCountMap(schema.evb, 'gesellschaft'),
        evbReseller: toCountMap(schema.evb, 'reseller'),
      },
      orders: {
        orderProducts: toCountMap(schema.orders, 'product'),
      },
    };
  }, [schema]);

  // Result-State
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [recipients, setRecipients] = useState<PickerRecipient[]>([]);
  const [totalAvailable, setTotalAvailable] = useState(0);
  const [matchedEvbs, setMatchedEvbs] = useState(0);
  const [stubMode, setStubMode] = useState(false);

  // Hydrate ColumnPicker-Selection aus localStorage beim Mount (clientside-only,
  // damit kein SSR-Hydration-Mismatch entsteht).
  useEffect(() => {
    setSelectedBusinessCols(loadColumnsFromStorage('business'));
    setSelectedEvbCols(loadColumnsFromStorage('evb'));
    setSelectedOrderCols(loadColumnsFromStorage('orders'));
  }, []);

  // Schema laden — parallel zu Facets, einmal pro Component-Lifetime (Endpoint cached 60min server-seitig)
  useEffect(() => {
    if (!open && !isInline) return;
    let cancelled = false;
    void (async () => {
      try {
        const r = await fetch('/api/crm/schema');
        const data = await r.json();
        if (cancelled || !data.ok) return;
        setSchema({ business: data.business, evb: data.evb, orders: data.orders, cachedAt: data.cachedAt });
      } catch { /* silent — UI funktioniert auch ohne Schema (zeigt nur Default-Spalten) */ }
    })();
    return () => { cancelled = true; };
  }, [open, isInline]);

  // Facets laden — einmalig beim ersten Aufruf, danach gecached
  useEffect(() => {
    if (!open && !isInline) return;
    let cancelled = false;
    void (async () => {
      if (!cancelled) setFacetsLoading(true);
      try {
        const r = await fetch('/api/crm/facets');
        const data = await r.json();
        if (cancelled) return;
        setFacets({
          businessItsKsm: data.businessItsKsm ?? [],
          businessReseller: data.businessReseller ?? [],
          evbTyp: data.evbTyp ?? [],
          evbGesellschaft: data.evbGesellschaft ?? [],
          evbReseller: data.evbReseller ?? [],
          orderProducts: data.orderProducts ?? [],
        });
      } catch {
        // silent: Stub-Facets fallback ist ok
      } finally {
        if (!cancelled) setFacetsLoading(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [open, isInline]);

  /**
   * Lädt eine zusätzliche Page für Facets UND Schema (parallel) und merged die
   * neuen Distinct-Werte in den vorhandenen State. Wird vom SampleSizeBanner
   * ausgelöst. `refresh=1` ignoriert den Server-Cache für den neuen pages-Wert,
   * sodass User wirklich frische Daten sehen.
   */
  const loadMoreSamples = useCallback(async () => {
    if (loadingMoreSamples) return;
    const nextFacets = sampleSize.facets + 1;
    const nextSchema = sampleSize.schema + 1;
    setLoadingMoreSamples(true);
    try {
      const [fRes, sRes] = await Promise.all([
        fetch(`/api/crm/facets?pages=${nextFacets}&refresh=1`),
        fetch(`/api/crm/schema?pages=${nextSchema}&refresh=1`),
      ]);
      const [fData, sData] = await Promise.all([fRes.json(), sRes.json()]);
      if (fData?.ok) {
        setFacets({
          businessItsKsm: fData.businessItsKsm ?? [],
          businessReseller: fData.businessReseller ?? [],
          evbTyp: fData.evbTyp ?? [],
          evbGesellschaft: fData.evbGesellschaft ?? [],
          evbReseller: fData.evbReseller ?? [],
          orderProducts: fData.orderProducts ?? [],
        });
      }
      if (sData?.ok) {
        setSchema({ business: sData.business, evb: sData.evb, orders: sData.orders, cachedAt: sData.cachedAt });
      }
      setSampleSize({ facets: nextFacets, schema: nextSchema });
    } catch {
      // silent: Banner bleibt sichtbar, User kann erneut klicken
    } finally {
      setLoadingMoreSamples(false);
    }
  }, [loadingMoreSamples, sampleSize.facets, sampleSize.schema]);

  /**
   * Dynamic-Filter wendet sich client-seitig auf bereits geladene Empfänger an.
   * Nur business-Tab (für jetzt) — EVB/Orders folgen demselben Pattern, kommt im nächsten Iteration-Zyklus.
   * Live-Filter ohne Re-Fetch: User toggelt eine Spalte aus dem SchemaBrowser → Tabelle filtert sofort.
   */
  // Aktive Dynamic-Filter pro Mode bestimmen
  const activeDynamicFilters = useMemo<DynamicFilterValue[]>(() => {
    if (mode === 'business') return dynamicBusinessFilters;
    if (mode === 'evb') return dynamicEvbFilters;
    if (mode === 'orders') return dynamicOrderFilters;
    return [];
  }, [mode, dynamicBusinessFilters, dynamicEvbFilters, dynamicOrderFilters]);

  const displayedRecipients = useMemo(() => {
    if (activeDynamicFilters.length === 0) return recipients;
    return recipients.filter((r) => {
      const raw = r.rawRecord;
      if (!raw) return true; // CSV oder fehlendes raw → bestehen lassen (kein Filter wirkt)
      return activeDynamicFilters.every((f) => matchesDynamicFilter(raw, f));
    });
  }, [recipients, activeDynamicFilters]);

  // Workspace-Slug Re-Init beim Öffnen (nur wenn kein initialFilter):
  // Sync von externer Prop in lokalen State — React-Doku empfiehlt für solche Cases derived state oder
  // key-Prop, aber hier triggern mehrere State-Setter; eine einzelne batched setState-Aktion via Microtask
  // hält die Effect-Body frei von Cascade-Warnings.
  useEffect(() => {
    if (!open || initialFilter) return;
    queueMicrotask(() => {
      setMode(defaultModeForBrand(workspaceSlug));
      setBusinessState(defaultBusinessForBrand(workspaceSlug));
      setEvbState(DEFAULT_EVB_STATE);
      setOrderState(DEFAULT_ORDER_STATE);
      setRecipients([]);
      setError(null);
      setStubMode(false);
    });
  }, [open, workspaceSlug, initialFilter]);

  // Stable callback für CsvSourcePanel
  const handleCsvRecipients = useCallback((next: PickerRecipient[]) => {
    setRecipients(next);
    setTotalAvailable(next.length);
    setStubMode(false);
    setError(null);
  }, []);

  // Active-Filter-Counter
  const activeFilterCount = useMemo(() => {
    if (mode === 'business')
      return countActiveFilters(businessState.server) + countActiveFilters(businessState.client);
    if (mode === 'evb') return countActiveFilters(evbState.server) + countActiveFilters(evbState.client);
    if (mode === 'orders') return countActiveFilters(orderState.server) + countActiveFilters(orderState.client);
    return 0;
  }, [mode, businessState, evbState, orderState]);

  // Inline = always rendered; Modal = nur wenn open
  if (!isInline && !open) return null;

  const buildSnapshot = (): RecipientFilterSnapshot => ({
    source: mode,
    business: mode === 'business' ? businessState : undefined,
    evb: mode === 'evb' ? evbState : undefined,
    orders: mode === 'orders' ? orderState : undefined,
    maxRecords,
  });

  const loadPreview = async () => {
    setLoading(true);
    setError(null);
    try {
      let body: unknown;
      if (mode === 'business') {
        const { server, client } = serializeBusinessFilter(businessState);
        body = { source: 'business', server, client, maxRecords, preview: false };
      } else if (mode === 'evb') {
        const { server, client } = serializeEvbFilter(evbState);
        body = { source: 'evb', server, client, maxRecords, preview: false };
      } else if (mode === 'orders') {
        const { server, client } = serializeOrderFilter(orderState);
        body = { source: 'orders', server, client, maxRecords, preview: false };
      }
      const res = await fetch('/api/crm/audience', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(body),
      });
      const data = await res.json();
      if (!res.ok) {
        setError(data.error ?? `Server returned ${res.status}`);
        if (data.issues) setError(`${data.error}: ${data.issues.join(', ')}`);
        return;
      }
      setRecipients(data.recipients ?? []);
      setTotalAvailable(data.totalAvailable ?? 0);
      setMatchedEvbs(data.matchedEvbs ?? 0);
      setStubMode(data.meta?.source === 'stub');
    } catch (e) {
      setError(e instanceof Error ? e.message : 'Network error');
    } finally {
      setLoading(false);
    }
  };

  // Imperative API für externe Trigger (z.B. AudienceFooter mit "Empfänger laden"-Button).
  // useImperativeHandle re-bindet bei jedem Render — loadPreview ist eine Closure über den
  // aktuellen State, sodass externe Aufrufer den frischen State sehen.
  useImperativeHandle(ref, () => ({ loadPreview }));

  // Loading-State nach außen propagieren — der Footer-Button braucht das reactive für Spinner/Disabled.
  useEffect(() => {
    onLoadingChange?.(loading);
  }, [loading, onLoadingChange]);

  // Auto-Confirm (nur Inline-Mode): sobald die Liste geladen ist, übermittle sie automatisch
  // an den Parent. Dadurch entfällt der explizite "Auswahl übernehmen"-Klick — der globale
  // AudienceFooter zeigt direkt den "Weiter zu Versand"-CTA.
  // - skip-on-empty: schützt davor, dass ein Tab-Wechsel (setRecipients([])) eine bereits
  //   übernommene Auswahl im Parent löscht.
  // - skip-while-loading: warten bis fetch abgeschlossen ist.
  // - reactive auf displayedRecipients: ändert der User dynamische Schema-Filter, wird die
  //   gefilterte Liste automatisch nachübermittelt — Workflow-State bleibt synchron mit UI.
  useEffect(() => {
    if (!isInline) return;
    if (loading) return;
    if (displayedRecipients.length === 0) return;
    onConfirm(displayedRecipients, mode === 'csv' ? null : buildSnapshot());
    // buildSnapshot ist eine Closure über mode/businessState/evbState/orderState/maxRecords —
    // diese ändern sich nicht ohne dass auch displayedRecipients neu evaluiert wird (durch
    // Mode-Wechsel → setRecipients([]) → skip-on-empty), daher kein Stale-Snapshot-Risiko.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [displayedRecipients, loading, isInline, onConfirm]);

  return (
    <>
      {!isInline && <div className="fixed inset-0 z-40 bg-black/40 backdrop-blur-sm" onClick={onClose} />}
      <div
        className={cn(
          'flex flex-col',
          isInline
            ? 'mx-auto w-full max-w-[1200px]'
            : 'fixed left-1/2 top-1/2 z-50 h-[90vh] w-full max-w-[1100px] -translate-x-1/2 -translate-y-1/2 rounded-lg border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950',
        )}
      >
        {/* Toolbar — Source-Dropdown + Filter-Hinweise + Close-X */}
        <div className="flex shrink-0 items-center justify-between gap-3 border-b border-zinc-200 px-4 py-2 dark:border-zinc-800">
          <div className="flex items-center gap-2">
            <SourceSelect
              value={mode}
              onChange={(next) => {
                setMode(next);
                setRecipients([]);
                setError(null);
              }}
            />
            {mode !== 'csv' && activeFilterCount > 0 && (
              <span className="rounded-full bg-zinc-900 px-1.5 py-0.5 text-[10px] font-semibold text-white dark:bg-zinc-100 dark:text-zinc-900">
                {activeFilterCount} Filter
              </span>
            )}
            {facetsLoading && (
              <span className="text-[10px] text-zinc-400">· Werte werden geladen…</span>
            )}
          </div>
          {!isInline && (
            <button
              onClick={onClose}
              className="rounded-md p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800"
            >
              <X className="h-4 w-4" />
            </button>
          )}
        </div>

        {mode === 'csv' ? (
          <div className="flex flex-1 flex-col">
            {csvHeaders.length > 0 && (
              <div className="border-b border-zinc-200 bg-zinc-50/50 px-6 py-3 dark:border-zinc-800 dark:bg-zinc-900/40">
                <PlaceholderLegend columnNames={csvHeaders} />
              </div>
            )}
            <CsvSourcePanel
              onRecipientsChange={handleCsvRecipients}
              onHeadersChange={setCsvHeaders}
            />
          </div>
        ) : (
          // Layout: Result-Panel LINKS (großer Lesebereich), Filter-Sidebar RECHTS.
          // User-Flow: erst Tabelle scannen, dann gezielt filtern.
          <div className={cn(
            'grid flex-1 grid-cols-[1fr_360px] gap-0',
            !isInline && 'overflow-hidden',
          )}>
            {/* Result-Panel — links, großer Bereich */}
            <div className={cn('flex flex-col', !isInline && 'overflow-hidden')}>
              {error && (
                <div className="m-4 flex items-start gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-xs text-red-800 dark:border-red-900/50 dark:bg-red-950/30">
                  <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
                  <span>{error}</span>
                </div>
              )}

              {stubMode && (
                <div className="m-4 rounded-md border border-amber-200 bg-amber-50 p-3 text-xs text-amber-800 dark:border-amber-900/50 dark:bg-amber-950/30">
                  <strong>Stub-Modus:</strong> CRM_USERNAME / CRM_PASSWORD nicht in <code>.env.local</code>{' '}
                  gesetzt. Es werden Mock-Empfänger angezeigt zur UI-Verifikation.
                </div>
              )}

              {displayedRecipients.length === 0 && !loading && !error ? (
                <EmptyState mode={mode} />
              ) : (
                <ResultTable
                  mode={mode}
                  isInline={isInline}
                  recipients={displayedRecipients}
                  totalAvailable={totalAvailable}
                  matchedEvbs={matchedEvbs}
                  selectedColumns={
                    mode === 'business' ? selectedBusinessCols
                    : mode === 'evb' ? selectedEvbCols
                    : mode === 'orders' ? selectedOrderCols
                    : ['email']
                  }
                  availableColumns={
                    mode === 'business' ? (schema?.business ?? [])
                    : mode === 'evb' ? (schema?.evb ?? [])
                    : mode === 'orders' ? (schema?.orders ?? [])
                    : []
                  }
                  onColumnsChange={(cols) => {
                    if (mode === 'business') setSelectedBusinessCols(cols);
                    else if (mode === 'evb') setSelectedEvbCols(cols);
                    else if (mode === 'orders') setSelectedOrderCols(cols);
                  }}
                />
              )}
            </div>

            {/* Filter-Sidebar — rechts, fixe 360px Breite */}
            <div className={cn(
              'space-y-5 border-l border-zinc-200 p-4 dark:border-zinc-800',
              !isInline && 'overflow-y-auto',
            )}>
              {/* Placeholder-Legend — Click auf Spalten-Chip kopiert `{{name}}` in die Zwischenablage. */}
              <PlaceholderLegend
                columns={
                  mode === 'business' ? schema?.business
                  : mode === 'evb' ? schema?.evb
                  : mode === 'orders' ? schema?.orders
                  : undefined
                }
              />
              {mode === 'business' && (
                <BusinessFilters
                  state={businessState}
                  onChange={setBusinessState}
                  facets={facets}
                  facetCounts={facetCounts?.business}
                  schemaColumns={schema?.business}
                  dynamicFilters={dynamicBusinessFilters}
                  onDynamicFiltersChange={setDynamicBusinessFilters}
                  totalSampled={sampleSize.facets * 100}
                  onLoadMoreSamples={loadMoreSamples}
                  loadingMoreSamples={loadingMoreSamples}
                />
              )}
              {mode === 'evb' && (
                <EvbFilters
                  state={evbState}
                  onChange={setEvbState}
                  facets={facets}
                  facetCounts={facetCounts?.evb}
                  schemaColumns={schema?.evb}
                  dynamicFilters={dynamicEvbFilters}
                  onDynamicFiltersChange={setDynamicEvbFilters}
                  totalSampled={sampleSize.facets * 100}
                  onLoadMoreSamples={loadMoreSamples}
                  loadingMoreSamples={loadingMoreSamples}
                />
              )}
              {mode === 'orders' && (
                <OrderFilters
                  state={orderState}
                  onChange={setOrderState}
                  facets={facets}
                  facetCounts={facetCounts?.orders}
                  schemaColumns={schema?.orders}
                  dynamicFilters={dynamicOrderFilters}
                  onDynamicFiltersChange={setDynamicOrderFilters}
                  totalSampled={sampleSize.facets * 100}
                  onLoadMoreSamples={loadMoreSamples}
                  loadingMoreSamples={loadingMoreSamples}
                />
              )}

              <FilterField label="Max. Empfänger" hint="Limit pro Versand">
                <FilterInput
                  value={maxRecords.toString()}
                  onChange={(v) =>
                    setMaxRecords(Math.max(1, Math.min(5000, parseInt(v, 10) || 200)))
                  }
                  type="number"
                />
              </FilterField>

              {/* Modal-Mode: dedizierter Lade-Button im Filter-Panel.
                  Inline-Mode: AudienceFooter (page.tsx) triggert via Ref → kein doppelter Button. */}
              {!isInline && (
                <button
                  onClick={loadPreview}
                  disabled={loading}
                  className="inline-flex w-full items-center justify-center gap-1.5 rounded-md bg-zinc-900 px-3 py-2 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50 dark:bg-zinc-100 dark:text-zinc-900"
                >
                  {loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
                  {loading ? 'Lade …' : 'Empfänger laden'}
                </button>
              )}
            </div>
          </div>
        )}

        {/* Modal-Mode: interner Footer mit Confirm/Cancel.
            Inline-Mode: Auto-Confirm via useEffect → AudienceFooter zeigt Status & Workflow-CTAs. */}
        {!isInline && (
          <footer className="flex items-center justify-between border-t border-zinc-200 px-6 py-3 dark:border-zinc-800">
            <span className="text-xs text-zinc-500">
              {displayedRecipients.length > 0 && (
                <>
                  Diese {displayedRecipients.length} Empfänger werden beim Versand verwendet.
                  {activeDynamicFilters.length > 0 && recipients.length !== displayedRecipients.length && (
                    <span className="ml-1 text-zinc-400">
                      (von {recipients.length} nach Schema-Filter)
                    </span>
                  )}
                </>
              )}
            </span>
            <div className="flex gap-2">
              <button
                onClick={onClose}
                className="rounded-md border border-zinc-200 px-3 py-1.5 text-xs font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-300"
              >
                Abbrechen
              </button>
              <button
                onClick={() => {
                  onConfirm(displayedRecipients, mode === 'csv' ? null : buildSnapshot());
                  onClose();
                }}
                disabled={displayedRecipients.length === 0}
                className="inline-flex items-center gap-1.5 rounded-md bg-[#ff006a] px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-[#e60060] disabled:opacity-50 disabled:hover:bg-[#ff006a]"
              >
                <CheckCircle2 className="h-4 w-4" /> Auswahl übernehmen ({displayedRecipients.length})
              </button>
            </div>
          </footer>
        )}
      </div>
    </>
  );
});

// ─────────────────────────────────────────────────────────────────────────────
// Sub-Components
// ─────────────────────────────────────────────────────────────────────────────

const SOURCE_OPTIONS: ReadonlyArray<{
  value: SourceMode;
  label: string;
  hint: string;
  icon: React.ReactNode;
}> = [
  { value: 'business', label: 'Geschäftskunden', hint: 'its_business_datas · ~143k', icon: <Database className="h-3.5 w-3.5" /> },
  { value: 'evb',      label: 'EVBs',            hint: 'evbs · ~380k',               icon: <FileText className="h-3.5 w-3.5" /> },
  { value: 'orders',   label: 'Privat-Orders',   hint: 'order_private_shops · ~52k', icon: <ShoppingBag className="h-3.5 w-3.5" /> },
  { value: 'csv',      label: 'CSV-Upload',      hint: 'Externe Liste',              icon: <FileUp className="h-3.5 w-3.5" /> },
];

function SourceSelect({
  value,
  onChange,
}: {
  value: SourceMode;
  onChange: (next: SourceMode) => void;
}) {
  const [open, setOpen] = useState(false);
  const active = SOURCE_OPTIONS.find((o) => o.value === value)!;

  return (
    <div className="relative">
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        className="flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-1.5 text-xs hover:bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-900"
      >
        {active.icon}
        <span className="font-medium">{active.label}</span>
        <span className="font-mono text-[10px] text-zinc-500">{active.hint}</span>
        <ChevronDown className="h-3 w-3 text-zinc-400" />
      </button>

      {open && (
        <>
          <div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
          <ul
            role="listbox"
            className="absolute left-0 top-full z-20 mt-1 w-[260px] overflow-hidden rounded-md border border-zinc-200 bg-white shadow-lg dark:border-zinc-800 dark:bg-zinc-950"
          >
            {SOURCE_OPTIONS.map((opt) => (
              <li key={opt.value}>
                <button
                  type="button"
                  onClick={() => {
                    onChange(opt.value);
                    setOpen(false);
                  }}
                  className={cn(
                    'flex w-full items-center gap-2 px-3 py-2 text-left text-xs hover:bg-zinc-50 dark:hover:bg-zinc-900',
                    opt.value === value && 'bg-zinc-100 dark:bg-zinc-900',
                  )}
                >
                  {opt.icon}
                  <span className="flex flex-1 flex-col leading-tight">
                    <span className="font-medium">{opt.label}</span>
                    <span className="font-mono text-[10px] text-zinc-500">{opt.hint}</span>
                  </span>
                  {opt.value === value && <Check className="h-3 w-3 text-zinc-600 dark:text-zinc-300" />}
                </button>
              </li>
            ))}
          </ul>
        </>
      )}
    </div>
  );
}

function EmptyState({ mode }: { mode: SourceMode }) {
  const hints: Record<Exclude<SourceMode, 'csv'>, string> = {
    business:
      'Default: nur Kunden mit Briefstatus „11" und Email, ohne Negativ-Status (kein Interesse, Inkasso, Abmahnung).',
    evb:
      'EVBs werden nach API-Filtern + Status gefiltert, dann werden die zugehörigen Geschäftskunden mit Email aufgelöst.',
    orders:
      'Alle Privat-Bestellungen mit gültiger Email, dedupliziert. Date-Range begrenzt den Zeitraum serverseitig.',
  };
  return (
    <div className="flex flex-1 flex-col items-center justify-center gap-2 text-center">
      <Users className="h-10 w-10 text-zinc-300" />
      <p className="text-sm text-zinc-500">
        Klicke <strong>Empfänger laden</strong> um die Liste zu sehen
      </p>
      <p className="max-w-[440px] text-[11px] text-zinc-400">{hints[mode as Exclude<SourceMode, 'csv'>]}</p>
    </div>
  );
}

function ResultTable({
  mode,
  isInline = false,
  recipients,
  totalAvailable,
  matchedEvbs,
  selectedColumns,
  availableColumns,
  onColumnsChange,
}: {
  mode: SourceMode;
  isInline?: boolean;
  recipients: PickerRecipient[];
  totalAvailable: number;
  matchedEvbs: number;
  selectedColumns: string[];
  availableColumns: ColumnInfo[];
  onColumnsChange: (cols: string[]) => void;
}) {
  // Tab-Key für ColumnPicker → bestimmt localStorage-Slot
  const tableKey: 'business' | 'evb' | 'orders' | null =
    mode === 'csv' ? null : mode;

  return (
    <>
      <div className="flex items-center justify-between border-b border-zinc-200 px-4 py-2 dark:border-zinc-800">
        <span className="text-xs text-zinc-600 dark:text-zinc-400">
          <strong>{recipients.length}</strong> Empfänger
          {totalAvailable > recipients.length && (
            <span className="ml-1 text-zinc-400">
              · von {totalAvailable.toLocaleString('de-DE')} verfügbar
            </span>
          )}
          {mode === 'evb' && matchedEvbs > 0 && (
            <span className="ml-1 text-zinc-400">· aus {matchedEvbs.toLocaleString('de-DE')} EVBs aufgelöst</span>
          )}
        </span>
        {tableKey && availableColumns.length > 0 && (
          <ColumnPicker
            tableKey={tableKey}
            available={availableColumns}
            value={selectedColumns}
            onChange={onColumnsChange}
          />
        )}
      </div>
      <div className={cn('flex-1', !isInline && 'overflow-y-auto')}>
        <table className="w-full text-xs">
          <thead className="sticky top-0 bg-zinc-50 dark:bg-zinc-900">
            <tr className="text-left text-[10px] uppercase tracking-wider text-zinc-500">
              {selectedColumns.map((col) => (
                <th key={col} className="px-4 py-2 font-mono normal-case">{col}</th>
              ))}
            </tr>
          </thead>
          <tbody>
            {recipients.map((r, i) => (
              <tr
                key={r.email + i}
                className="border-b border-zinc-100 hover:bg-zinc-50 dark:border-zinc-900 dark:hover:bg-zinc-900/50"
              >
                {selectedColumns.map((col) => (
                  <td key={col} className="px-4 py-1.5 text-zinc-600 dark:text-zinc-400">
                    <CellValue recipient={r} column={col} />
                  </td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </>
  );
}

/** Liest einen Spalten-Wert aus dem Recipient — primär aus rawRecord, mit Fallback auf top-level Felder. */
function CellValue({ recipient, column }: { recipient: PickerRecipient; column: string }) {
  // Top-level-Synonyme (für CSV oder fehlendes rawRecord)
  if (column === 'email') return <span className="font-mono text-[11px]">{recipient.email}</span>;

  const raw = recipient.rawRecord?.[column];
  if (raw === null || raw === undefined || raw === '') {
    return <span className="text-zinc-300">—</span>;
  }
  if (typeof raw === 'boolean') {
    return <span className={raw ? 'text-emerald-600' : 'text-zinc-400'}>{raw ? '✓' : '✗'}</span>;
  }
  if (typeof raw === 'number') {
    return <span className="font-mono text-[11px]">{raw.toLocaleString('de-DE')}</span>;
  }
  const str = String(raw);
  // Date-Heuristik: ISO-Format → menschenlesbar formatieren
  if (/^\d{4}-\d{2}-\d{2}/.test(str)) {
    try {
      return <span className="text-[11px]">{new Date(str).toLocaleDateString('de-DE')}</span>;
    } catch { /* fall through */ }
  }
  return <span className="text-[11px]">{str}</span>;
}

// ─────────────────────────────────────────────────────────────────────────────
// PlaceholderLegend — klickbare Chip-Liste der verfügbaren Spalten
// ─────────────────────────────────────────────────────────────────────────────

interface PlaceholderLegendProps {
  /** ColumnInfo[] aus dem Schema-Snapshot (business/evb/orders). Wenn null/undefined → loading. */
  columns?: ColumnInfo[];
  /** Fallback für CSV-Mode: einfache Liste von Header-Strings ohne Schema-Metadaten. */
  columnNames?: string[];
}

/**
 * Rendert die verfügbaren Spalten als klickbare Pills oberhalb der Filter.
 * Sortierung nach Fill-Rate desc (filledCount/totalSampled), damit nützliche Felder
 * wie `email`, `firma`, `vorname` zuerst erscheinen — leere/seltene zuletzt.
 *
 * Hover-Tooltip zeigt Fill-Rate + bis zu 2 Beispielwerte aus topValues.
 */
const PLACEHOLDER_COLLAPSE_KEY = 'mailforge.placeholder-legend.collapsed';

function PlaceholderLegend({ columns, columnNames }: PlaceholderLegendProps) {
  // Collapse-State persistiert in localStorage (analog ColumnPicker-Pattern).
  // Default: ausgeklappt (= falsy), damit Platzhalter beim ersten Besuch sichtbar sind.
  const [collapsed, setCollapsed] = useState(false);
  // copiedKey: Spalten-Name der zuletzt kopierten Pill — rendert kurzes Erfolgs-Feedback.
  // Auto-Reset nach 1.5s. errorKey analog für Clipboard-Fehler (z.B. fehlender Secure-Context).
  const [copiedKey, setCopiedKey] = useState<string | null>(null);
  const [errorKey, setErrorKey] = useState<string | null>(null);
  useEffect(() => {
    if (typeof window === 'undefined') return;
    try {
      setCollapsed(localStorage.getItem(PLACEHOLDER_COLLAPSE_KEY) === '1');
    } catch { /* ignore */ }
  }, []);
  const toggleCollapsed = () => {
    setCollapsed((prev) => {
      const next = !prev;
      try { localStorage.setItem(PLACEHOLDER_COLLAPSE_KEY, next ? '1' : '0'); } catch { /* ignore */ }
      return next;
    });
  };

  const copyPlaceholder = async (columnName: string) => {
    const text = `{{${columnName}}}`;
    try {
      if (typeof navigator === 'undefined' || !navigator.clipboard) throw new Error('clipboard unavailable');
      await navigator.clipboard.writeText(text);
      setCopiedKey(columnName);
      setErrorKey(null);
      window.setTimeout(() => setCopiedKey((k) => (k === columnName ? null : k)), 1500);
    } catch {
      // Fallback für nicht-Secure-Contexts: textarea-Trick. Schlägt in modernen Browsern
      // ohne user-gesture nicht fehl, aber wir markieren bei finalem Fehler die Pill rot.
      try {
        const ta = document.createElement('textarea');
        ta.value = text;
        ta.style.position = 'fixed';
        ta.style.opacity = '0';
        document.body.appendChild(ta);
        ta.select();
        const ok = document.execCommand('copy');
        document.body.removeChild(ta);
        if (!ok) throw new Error('execCommand failed');
        setCopiedKey(columnName);
        setErrorKey(null);
        window.setTimeout(() => setCopiedKey((k) => (k === columnName ? null : k)), 1500);
      } catch {
        setErrorKey(columnName);
        window.setTimeout(() => setErrorKey((k) => (k === columnName ? null : k)), 2000);
      }
    }
  };

  const items = useMemo(() => {
    if (columns) {
      // Sortierung: Fill-Rate desc, dann alphabetisch. Leere Spalten ans Ende.
      return [...columns].sort((a, b) => {
        const ar = a.totalSampled > 0 ? a.filledCount / a.totalSampled : 0;
        const br = b.totalSampled > 0 ? b.filledCount / b.totalSampled : 0;
        if (br !== ar) return br - ar;
        return a.name.localeCompare(b.name);
      });
    }
    if (columnNames) {
      return columnNames.map((name) => ({ name, kind: 'string' as const, filledCount: 0, totalSampled: 0, topValues: [], distinctCount: 0 }));
    }
    return null;
  }, [columns, columnNames]);

  if (items === null) {
    // Schema lädt noch — Skeleton zeigt Erwartung an, statt das ganze Panel zu hidden.
    return (
      <div className="rounded-md border border-zinc-200 bg-zinc-50/50 p-3 dark:border-zinc-800 dark:bg-zinc-900/40">
        <div className="mb-2 flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-zinc-500">
          <Tag className="h-3 w-3" />
          Verfügbare Platzhalter
        </div>
        <div className="flex flex-wrap gap-1">
          {Array.from({ length: 6 }).map((_, i) => (
            <div key={i} className="h-5 w-16 animate-pulse rounded bg-zinc-200 dark:bg-zinc-800" />
          ))}
        </div>
      </div>
    );
  }

  if (items.length === 0) {
    // CSV-Mode ohne Upload, oder Schema-Empty (sollte nicht passieren).
    return (
      <div className="rounded-md border border-dashed border-zinc-300 bg-zinc-50/50 p-3 text-[11px] text-zinc-500 dark:border-zinc-700 dark:bg-zinc-900/40">
        <div className="mb-1 flex items-center gap-1.5 font-medium uppercase tracking-wide">
          <Tag className="h-3 w-3" />
          Verfügbare Platzhalter
        </div>
        Lade zuerst eine CSV hoch, um Spalten als Platzhalter zu nutzen.
      </div>
    );
  }

  return (
    <div className="rounded-md border border-zinc-200 bg-zinc-50/50 p-3 dark:border-zinc-800 dark:bg-zinc-900/40">
      <button
        type="button"
        onClick={toggleCollapsed}
        className="flex w-full items-center justify-between text-left"
        aria-expanded={!collapsed}
      >
        <div className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300">
          {collapsed ? <ChevronRight className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
          <Tag className="h-3 w-3" />
          Verfügbare Platzhalter
          <span className="ml-1 rounded bg-zinc-200/60 px-1 py-0.5 text-[9px] font-semibold text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400">
            {items.length}
          </span>
        </div>
        {!collapsed && <span className="text-[10px] text-zinc-400">Klick kopiert in Zwischenablage</span>}
      </button>
      {!collapsed && (
      <div className="mt-2 flex flex-wrap gap-1">
        {items.map((col) => {
          const fillRate = col.totalSampled > 0 ? Math.round((col.filledCount / col.totalSampled) * 100) : null;
          const examples = col.topValues.slice(0, 2).map((v) => v.value).join(', ');
          const tooltipBase = [
            fillRate !== null ? `Fill-Rate: ${fillRate}%` : null,
            examples ? `Beispiele: ${examples}` : null,
          ].filter(Boolean).join(' · ');
          const isCopied = copiedKey === col.name;
          const isError = errorKey === col.name;
          const tooltip = isError
            ? 'Konnte nicht kopieren — manuell auswählen & Cmd/Ctrl+C'
            : isCopied
              ? 'Kopiert!'
              : tooltipBase || `{{${col.name}}}  · klick zum Kopieren`;
          return (
            <button
              key={col.name}
              type="button"
              onClick={() => copyPlaceholder(col.name)}
              title={tooltip}
              className={cn(
                'inline-flex items-center gap-1 rounded border px-1.5 py-0.5 font-mono text-[11px] transition',
                isCopied
                  ? 'border-emerald-400 bg-emerald-50 text-emerald-800 dark:border-emerald-600 dark:bg-emerald-950/40 dark:text-emerald-200'
                  : isError
                    ? 'border-rose-400 bg-rose-50 text-rose-800 dark:border-rose-600 dark:bg-rose-950/40 dark:text-rose-200'
                    : 'border-zinc-200 bg-white text-zinc-700 hover:border-zinc-900 hover:bg-zinc-900 hover:text-white dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-300 dark:hover:bg-zinc-100 dark:hover:text-zinc-900',
                fillRate !== null && fillRate < 30 && !isCopied && !isError && 'opacity-60',
              )}
            >
              {isCopied ? (
                <>
                  <Check className="h-3 w-3" />
                  kopiert
                </>
              ) : (
                `{{${col.name}}}`
              )}
            </button>
          );
        })}
      </div>
      )}
    </div>
  );
}
