'use client';

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Mail, Save, Eye, Send, Sparkles, AlertTriangle, CheckCircle2, XCircle, Loader2, RefreshCw, X, Settings, FileText, MailOpen, UserPlus, ArrowRight, ArrowLeft, Target, ChevronRight, LayoutTemplate, MoreHorizontal, ChevronDown, Wand2, CalendarClock, Pencil, Code2, Shield, AlignLeft, Trash2, Upload, ImageIcon, BarChart3, ExternalLink, FileCode2, Lock, Unlock, ListOrdered } from 'lucide-react';
import { MarketingEditor, type MarketingEditorHandle } from '@/modules/editor/ui/MarketingEditor';
import { BriefingPanel, ToggleSwitch, type Briefing, type BriefingIntent, type SuggestionFeedback, type ScrapedSummary, EMPTY_BRIEFING, isBriefingReady, stripBriefingForApi, detectIntent } from '@/modules/briefing/ui/BriefingPanel';
import { SettingsDrawer } from '@/components/settings/SettingsDrawer';
import { PublishTriggerDialog } from '@/modules/trigger-templates/PublishTriggerDialog';
import { GenerateLoader } from '@/modules/briefing/ui/GenerateLoader';
import { SuggestLoader } from '@/modules/briefing/ui/SuggestLoader';
import { DraftBriefLoader } from '@/modules/briefing/ui/DraftBriefLoader';
import { RecipientPicker, type RecipientPickerHandle } from '@/modules/recipients/ui/RecipientPicker';
import { isInlineEditMessage, isInlineActionMessage, isInlineResizeMessage, injectInlineEditorScript } from '@/modules/editor/lib/inline-editor';
import { applyPreviewPlaceholders } from '@/modules/editor/lib/preview-placeholders';
import type { PickerRecipient } from '@/modules/recipients/ui/RecipientPicker';
import { SchedulerPanel } from '@/modules/scheduler/ui/SchedulerPanel';
import { CampaignStatsPanel } from '@/modules/stats/ui/CampaignStatsPanel';
import { DashboardPanel } from '@/modules/stats/ui/DashboardPanel';
import { Sidebar } from '@/components/sidebar/Sidebar';
import { VersionHistoryPanel } from '@/components/sidebar/VersionHistoryPanel';
import { AutoSaveIndicator } from '@/components/sidebar/AutoSaveIndicator';
import { useDraft } from '@/hooks/useDraft';
import { useAutoSave } from '@/hooks/useAutoSave';
import { useMediaQuery } from '@/hooks/useMediaQuery';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import type { DraftFull } from '@/lib/db/draft-repo';
import type { TemplateSummary } from '@/lib/db/template-repo';
import { cn } from '@/lib/utils';
import { isModuleEnabled } from '@/core/modules';

interface BrandLite {
  id: string;
  slug: string;
  name: string;
  primaryColor: string;
  /** Optional: M365-Pool-Mapping (siehe Settings → Sender-Pool). */
  companyId?: number | null;
  /** Optional: Default-Sender für diesen Workspace. */
  senderEmail?: string;
}

interface EditorState {
  json: object | null;
  html: string;
  text: string;
}

interface EditableStrategy {
  subject_variants: string[];
  preheader: string;
  hero_headline: string;
  intro: string;
  pain_point: string;
  solution: string;
  benefits: string[];
  cta_primary_text: string;
  cta_secondary_text?: string;
  ps_line: string;
  image_brief: string;
}

interface GenerateResponse {
  ok: true;
  strategy: EditableStrategy;
  html: string;
  text: string;
  htmlBytes: number;
  mjml: string;
  mjmlErrors: Array<{ line: number; message: string; tagName?: string }>;
  spam: {
    score: number;
    verdict: 'ship' | 'fix' | 'reject';
    findings: Array<{ rule: string; severity: 'info' | 'warning' | 'error'; points: number; message: string; detail?: string }>;
    metrics: Record<string, unknown>;
  };
  hero?: { url: string; absoluteUrl: string; bytes: number; cached: boolean; durationMs: number; prompt: string } | null;
  meta: { modelUsed: string; tokensIn?: number; tokensOut?: number; durationMs: number };
  /**
   * Frontend-only Flag: signalisiert dass der User MJML oder HTML im Source-Editor manuell
   * angepasst hat. Solange `'mjml' | 'html'` gesetzt ist, wird der debounced Re-Render aus
   * Strategy-Edits unterdrückt — sonst würde der nächste Tipp-Burst die Manual-Edits killen.
   * Reset auf `null` via "Auf Strategy zurücksetzen"-Button im Override-Banner.
   */
  manualOverride?: 'mjml' | 'html' | null;
}

type TabName = 'edit' | 'spam' | 'stats' | 'mjml' | 'html' | 'text';

export default function BuilderPage() {
  const [content, setContent] = useState<EditorState>({ json: null, html: '', text: '' });
  const [briefing, setBriefing] = useState<Briefing>(EMPTY_BRIEFING);
  const [isGenerating, setIsGenerating] = useState(false);
  const [output, setOutput] = useState<GenerateResponse | null>(null);
  /**
   * Snapshot der Hero-Crop-Settings (Höhe + Position) wie sie beim letzten erfolgreichen
   * Render verwendet wurden. Quelle der Wahrheit für den `dirty`-Check im Bearbeiten-Tab —
   * Briefing-Felder (heroHeight/heroCropPosition) liegen außerhalb der EditableStrategy,
   * daher reicht der bestehende JSON.stringify-Strategy-Vergleich nicht aus.
   */
  const [lastRenderedHeroSettings, setLastRenderedHeroSettings] = useState<{
    height?: number;
    position?: 'top' | 'center' | 'bottom';
  }>({});
  /**
   * A/B-Test: weitere Varianten neben `output` (= Variante A).
   * Bei Send wird [output, ...extraVariants] als Variants-Array übergeben → Even-Split im Backend.
   * `activeVariantIdx`: 0 = output (A), 1..N = extraVariants[idx-1] (B, C, …).
   */
  const [extraVariants, setExtraVariants] = useState<GenerateResponse[]>([]);
  const [activeVariantIdx, setActiveVariantIdx] = useState(0);
  // Live-reported scrollHeight des Preview-iframes (via postMessage aus inline-editor-script).
  // Der iframe wächst dann mit dem Mail-Content statt internen Scroll zu zeigen.
  // null = noch kein Resize-Message angekommen → Fallback-Höhe wird genutzt.
  const [previewHeight, setPreviewHeight] = useState<number | null>(null);
  // Audience-Tab: Ref + Loading-State um den "Empfänger laden"-Button vom Footer aus zu triggern
  const audiencePickerRef = useRef<RecipientPickerHandle | null>(null);
  const [audienceLoading, setAudienceLoading] = useState(false);
  // Editor-Ref — exposed via forwardRef. Wird von der Zielgruppe-Tab genutzt um Merge-Tags
  // (`{{column_name}}`) an der aktuellen Cursor-Position einzufügen. Der Editor MUSS dafür
  // mounted bleiben (siehe Render-Logik unten: `hidden`-Toggle statt conditional render).
  const editorRef = useRef<MarketingEditorHandle | null>(null);
  // Templates für den BriefingPanel-Picker — werden bei Workspace-Change neu geladen.
  const [availableTemplates, setAvailableTemplates] = useState<TemplateSummary[]>([]);
  // Webseiten-Scrape-State — Result + Loading-Flags. State liegt im Parent damit Auto-Suggest
  // nach Scrape orchestriert und der Hero-Import handler-zentral verwaltet werden kann.
  const [scrapeResult, setScrapeResult] = useState<ScrapedSummary | null>(null);
  const [isScraping, setIsScraping] = useState(false);
  const [isImportingImage, setIsImportingImage] = useState(false);
  const refreshTemplates = useCallback(async () => {
    try {
      const res = await fetch('/api/templates');
      const data = await res.json();
      if (Array.isArray(data.templates)) setAvailableTemplates(data.templates);
    } catch {
      // Stillschweigend — Templates sind nicht-essentiell, nur eine Convenience.
    }
  }, []);
  useEffect(() => { void refreshTemplates(); }, [refreshTemplates]);
  const [error, setError] = useState<string | null>(null);
  const [errorIssues, setErrorIssues] = useState<string[] | null>(null);
  const [workspaces, setBrands] = useState<BrandLite[]>([]);
  const [settingsOpen, setSettingsOpen] = useState(false);
  // Trigger-Template-Freischaltung: öffnet einen modalen Dialog der den aktuellen
  // Output (MJML/HTML/Text + Subject + Preheader) als wiederverwendbares Trigger-
  // Template einfriert und einen API-Key generiert. Aktiver State nur wenn Output da.
  const [publishTriggerOpen, setPublishTriggerOpen] = useState(false);
  // Tab-Reihenfolge: 1.Editor → 2.Briefing → 3.AdMail → 4.Zielgruppe → 5.Versand
  // Linear-Flow: WAS sagen (Editor) → WIE klingt's (Briefing) → SEHEN wie's aussieht (AdMail) →
  //              WEM schicken (Zielgruppe) → WANN versenden (Versand-Scheduler).
  // Inhaltlich-First-Approach: User sieht erst die Mail, dann entscheidet er zielgerichtet die Audience.
  const [pageView, setPageView] = useState<PageView>('editor');
  const [isSendingTest, setIsSendingTest] = useState(false);
  const [testSendResult, setTestSendResult] = useState<{
    ok: boolean;
    message: string;
    kind?: 'network' | 'auth' | 'graph' | 'config' | 'unknown';
    endpoint?: string;
    httpStatus?: number;
    hint?: string;
  } | null>(null);
  // Test-Empfänger — wird in localStorage persistiert. Leer = Default M365_AUDIT_BCC.
  const [testRecipient, setTestRecipient] = useState<string>('');
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const v = window.localStorage.getItem('mailforge:testRecipient');
    if (v) setTestRecipient(v);
  }, []);
  const [isSendingBulk, setIsSendingBulk] = useState(false);
  const [bulkResult, setBulkResult] = useState<{ ok: boolean; message: string; campaignId?: string } | null>(null);
  // Letzter erfolgreich versendeter Campaign — wird vom Stats-Tab im AdMail genutzt
  // um Open/Click-Stats pro Variante zu pollen. Nicht in `output` weil Output ein Frontend-Snapshot ist
  // (kann verändert werden ohne Re-Send), während die Campaign-ID server-side persistent ist.
  const [lastSentCampaignId, setLastSentCampaignId] = useState<string | null>(null);

  // ─── KI-Suggest-State (gelifted aus BriefingPanel) ──────────────────────────
  // Suggest-State lebt am Page-Level damit (a) der Footer-CTA den Suggest auslösen kann,
  // (b) `<SuggestLoader />` als Page-weites Modal-Overlay rendert, (c) BriefingPanel
  // presentational bleibt und nur die Feedback/Error-Panels rendert.
  const [isSuggesting, setIsSuggesting] = useState(false);
  const [suggestFeedback, setSuggestFeedback] = useState<SuggestionFeedback | null>(null);
  const [suggestError, setSuggestError] = useState<string | null>(null);
  const [inferredFields, setInferredFields] = useState<Set<string>>(new Set());

  // ─── KI-Brief-State (POST /api/draft-brief → Editor-Auto-Fill) ───────────
  // Wird sowohl vom BriefingPanel-Block als auch vom MarketingEditor-Toolbar-Button getriggert.
  // Beide rufen denselben handleDraftBrief() — der schreibt das Resultat via editorRef in den Editor.
  const [isDraftingBrief, setIsDraftingBrief] = useState(false);
  const [draftBriefError, setDraftBriefError] = useState<string | null>(null);

  // M365-Sender-Pool — geladen aus /api/m365/senders, gilt für Test- UND Massenversand
  const [m365Senders, setM365Senders] = useState<Array<{ email: string; name: string; companyId?: number; note?: string }>>([]);
  const [selectedSenderEmail, setSelectedSenderEmail] = useState<string>('');

  // ─── Sidebar / Drafts / Versions / Auto-Save ──────────────────────────────
  const [versionPanelDraftId, setVersionPanelDraftId] = useState<string | null>(null);

  // Hydratet kompletten Editor-State aus einem Draft (Sidebar-Switch oder Restore).
  const hydrateFromDraft = useCallback((d: DraftFull) => {
    // Briefing rehydraten — UI-only `optional` aus dem persistierten JSON wiederherstellen, falls vorhanden.
    const persistedBriefing = d.briefingJson as Partial<Briefing> | null;
    if (persistedBriefing && Object.keys(persistedBriefing).length > 0) {
      setBriefing({ ...EMPTY_BRIEFING, ...persistedBriefing, workspaceSlug: persistedBriefing.workspaceSlug ?? d.workspaceSlug });
    } else {
      setBriefing({ ...EMPTY_BRIEFING, workspaceSlug: d.workspaceSlug });
    }
    // Tiptap-Inhalt — der MarketingEditor liest seinen Initial-Content über die JSON-Prop NUR
    // beim Mount. Beim Draft-Switch greift `key={currentDraftId}` und remountet das Subtree.
    // ABER: beim Initial-Page-Load wird `currentDraftId` zuerst aus localStorage gesetzt (Editor
    // mountet leer), und der nachfolgende `loadDraft(savedId)` ruft `setCurrentDraftId(savedId)`
    // erneut auf — selbe ID → kein Remount → Editor bleibt leer obwohl `content.json` korrekt
    // hydriert ist. Deshalb: zusätzlich imperativ über editorRef.replaceContent syncen.
    const tiptapJson = (d.tiptapJson as object) ?? null;
    setContent({ json: tiptapJson, html: '', text: '' });
    if (tiptapJson) {
      editorRef.current?.replaceContent(tiptapJson);
    }

    // Output rehydraten, falls die Strategy-JSON eine generierte Mail enthält
    const strat = d.strategyJson as {
      _generated?: { emailHtml: string; emailText: string; hero?: GenerateResponse['hero'] };
      subject_variants?: string[];
    } | null;
    if (strat && strat._generated && strat._generated.emailHtml) {
      const gen = strat._generated;
      setOutput({
        ok: true,
        strategy: strat as unknown as EditableStrategy,
        html: gen.emailHtml,
        text: gen.emailText,
        htmlBytes: gen.emailHtml.length,
        mjml: '',
        mjmlErrors: [],
        spam: (d.spamReportJson as GenerateResponse['spam']) ?? { score: 0, verdict: 'ship', findings: [], metrics: {} },
        // hero aus _generated zurücklesen — alte Drafts ohne dieses Feld fallen auf null zurück.
        hero: gen.hero ?? null,
        meta: { modelUsed: 'restored', durationMs: 0 },
      });
      setPageView('output');
    } else {
      setOutput(null);
      setPageView('briefing');
    }
    setError(null);
  }, []);

  const draft = useDraft({
    onLoad: hydrateFromDraft,
    onClear: () => {
      setBriefing(EMPTY_BRIEFING);
      setContent({ json: null, html: '', text: '' });
      setOutput(null);
      setError(null);
    },
  });

  // Auto-Save-Payload baut sich aus den drei Haupt-State-Slots zusammen. Wenn output existiert,
  // schreibt der Patch auch strategyJson + Spam — sodass ein Reload die generierte Mail behält.
  const autoSavePayload = {
    name: briefing.campaignName || 'Unbenannter Entwurf',
    briefingJson: briefing as unknown as Record<string, unknown>,
    tiptapJson: (content.json as Record<string, unknown>) ?? {},
    strategyJson: output ? ({
      ...(output.strategy as unknown as Record<string, unknown>),
      _generated: {
        emailHtml: output.html,
        emailText: output.text,
        // hero mit-persistieren, sonst zeigt das Form-Thumbnail nach Restore "Kein Bild"
        // während output.html das gebackene <img src=…> immer noch enthält → Drift.
        hero: output.hero ?? null,
      },
    } as Record<string, unknown>) : null,
    spamScore: output?.spam.score ?? null,
    spamVerdict: output?.spam.verdict ?? null,
    spamReportJson: (output?.spam as unknown as Record<string, unknown>) ?? null,
    workspaceId: workspaces.find((b) => b.slug === briefing.workspaceSlug)?.id,
  };

  const autoSave = useAutoSave({
    draftId: draft.currentDraftId,
    payload: autoSavePayload,
    enabled: !!draft.currentDraftId,
    debounceMs: 3000,
    autoSnapshotMs: 5 * 60_000,
  });

  // Lazy-Create: Sobald der User in einem leeren Zustand anfängt zu tippen UND eine Workspace gewählt hat
  // UND noch kein Draft offen ist, erzeugen wir einen.
  const lazyCreateGuardRef = useRef(false);
  const currentDraftId = draft.currentDraftId;
  const adoptDraft = draft.adoptDraft;
  const hasContentSig = `${briefing.campaignName}|${briefing.offer}|${briefing.painPoint}|${briefing.workspaceSlug}|${content.text}`;
  useEffect(() => {
    if (currentDraftId) { lazyCreateGuardRef.current = false; return; }
    if (lazyCreateGuardRef.current) return;
    const hasContent = !!briefing.campaignName || !!briefing.offer || !!briefing.painPoint || !!content.text;
    const workspace = workspaces.find((b) => b.slug === briefing.workspaceSlug);
    if (hasContent && workspace) {
      lazyCreateGuardRef.current = true;
      // adoptDraft hängt sich an einen frisch angelegten Draft, OHNE den lokalen State zu rehydraten
      // — sonst würde der gerade getippte Inhalt mit dem leeren Initial-Briefing überschrieben.
      void adoptDraft({
        workspaceId: workspace.id,
        name: briefing.campaignName || 'Unbenannter Entwurf',
        briefingJson: briefing as unknown as Record<string, unknown>,
        tiptapJson: (content.json as Record<string, unknown>) ?? {},
      });
    }
    // hasContentSig ist eine stabile Zusammenfassung aller relevanten User-Eingaben → fixiert die Deps-Anzahl
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [hasContentSig, currentDraftId, workspaces, adoptDraft]);

  // Save-as-Template-Handler
  const handleSaveAsTemplate = async () => {
    if (!draft.currentDraftId) return;
    const name = prompt('Name der Vorlage:', briefing.campaignName || 'Vorlage');
    if (!name) return;
    const description = prompt('Beschreibung (optional):') ?? undefined;
    const res = await fetch('/api/templates', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ draftId: draft.currentDraftId, name, description }),
    });
    if (res.ok) {
      void refreshTemplates();
    }
  };

  const handleLoadTemplate = async (templateId: string) => {
    const res = await fetch(`/api/templates/${templateId}`, { method: 'POST' });
    if (!res.ok) return;
    const data = await res.json();
    if (data.draft?.id) await draft.loadDraft(data.draft.id);
  };

  const handleDeleteTemplate = async (templateId: string) => {
    await fetch(`/api/templates/${templateId}`, { method: 'DELETE' });
    void refreshTemplates();
  };


  const refreshM365Senders = useCallback(() => {
    void fetch('/api/m365/senders')
      .then((r) => r.json())
      .then((d) => setM365Senders(d.senders ?? []))
      .catch(() => setM365Senders([]));
  }, []);

  // Beim Mount + wenn der Einstellungen-Drawer geschlossen wird (oder Pool dort gespeichert)
  useEffect(() => {
    refreshM365Senders();
  }, [settingsOpen, refreshM365Senders]);

  // Auto-Select: wenn die Workspace wechselt UND noch keine Sender-Wahl existiert (oder die alte
  // nicht mehr zur neuen Workspace passt), pre-fille mit dem zur Workspace
  // passenden Sender (Match via workspace.companyId — Integrator setzt
  // companyId-Werte in der Settings-UI pro Workspace + Sender-Pool).
  useEffect(() => {
    if (m365Senders.length === 0 || !briefing.workspaceSlug) return;
    const targetWorkspace = workspaces.find((w) => w.slug === briefing.workspaceSlug);
    const targetCompanyId = targetWorkspace?.companyId ?? undefined;
    const matchingSender = targetCompanyId
      ? m365Senders.find((s) => s.companyId === targetCompanyId)
      : m365Senders.find((s) => s.email === targetWorkspace?.senderEmail);
    // Reset nur wenn aktuelle Wahl nicht (mehr) im Pool oder keine Wahl existiert
    const currentValid = m365Senders.some((s) => s.email === selectedSenderEmail);
    if (!currentValid && matchingSender) {
      setSelectedSenderEmail(matchingSender.email);
    }
  }, [briefing.workspaceSlug, m365Senders, selectedSenderEmail, workspaces]);

  const handleSendBulk = async () => {
    if (!output || !briefing) return;
    const recipientCount = briefing.recipients?.length ?? 0;
    if (recipientCount === 0) {
      setBulkResult({ ok: false, message: 'Keine Empfänger ausgewählt — bitte im Zielgruppe-Tab Empfänger laden.' });
      setTimeout(() => setBulkResult(null), 8000);
      return;
    }
    if (!confirm(`Versand starten?\n\n${recipientCount} Empfänger werden in die Queue gestellt. Der Worker (\`npm run worker\`) schickt sie raus.`)) {
      return;
    }
    setIsSendingBulk(true);
    setBulkResult(null);
    try {
      // Strippt UI-only Toggle-State; utmDefaults entsteht hier (nur wenn utmTracking aktiv)
      const stripped = stripBriefingForApi(briefing);
      const res = await fetch('/api/campaign/send', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          workspaceSlug: briefing.workspaceSlug,
          campaignName: briefing.campaignName,
          briefingJson: stripped,
          tiptapJson: content.json ?? {},
          /**
           * Variants-Array: 1 Element = normaler Send, 2+ = A/B-Test mit Even-Split im Backend.
           * Labels A, B, C, … entsprechen der UI-Reihenfolge in der Sidebar.
           */
          variants: [output, ...extraVariants].map((v, i) => ({
            label: String.fromCharCode(65 + i), // 0→A, 1→B, 2→C, …
            subject: v.strategy.subject_variants[0] ?? briefing.campaignName,
            preheader: v.strategy.preheader,
            emailHtml: v.html,
            emailText: v.text,
            mjml: v.mjml,
            strategy: v.strategy,
            spamReport: v.spam,
          })),
          /** UTM-Defaults aus dem Briefing-Toggle (nur wenn aktiv → stripped.utm gesetzt). */
          utmDefaults: stripped.utm,
          /** Champion-Test-Config (nur wenn Toggle ON UND ≥ 2 Varianten Backend-seitig effektiv). */
          championTest: stripped.championTest,
          senderEmail: selectedSenderEmail || undefined,
          // rawRecord → customData: alle CRM-Spalten landen in `recipients.customDataJson`
          // und ermöglichen generische `{{column_name}}`-Substitution im Worker.
          //
          // poolType (Login-Direktlink-Routing): explizites poolType (CSV-Toggle) gewinnt,
          // sonst Heuristik aus source — 'business' → business_places, alles andere
          // (orders/evb/csv ohne Override) → address_pools.
          recipients: (briefing.recipients ?? []).map((r) => ({
            ...r,
            poolType: r.poolType ?? (r.source === 'business' ? 'business' : 'private'),
            customData: r.rawRecord,
          })),
        }),
      });
      const data = await res.json();
      if (!res.ok) {
        setBulkResult({ ok: false, message: data.error ?? `HTTP ${res.status}` });
      } else {
        setBulkResult({ ok: true, message: data.message, campaignId: data.campaignId });
        setLastSentCampaignId(data.campaignId);
        // Versand-Milestone — schliesst den Lifecycle dieses Drafts.
        void autoSave.snapshot({ source: 'milestone', milestone: 'send', label: `Versand: ${recipientCount} Empfänger` });
      }
    } catch (e) {
      setBulkResult({ ok: false, message: e instanceof Error ? e.message : 'Network error' });
    } finally {
      setIsSendingBulk(false);
      setTimeout(() => setBulkResult(null), 12000);
    }
  };

  const handleSendTest = async (recipient?: string) => {
    if (!output || !briefing) return;
    setIsSendingTest(true);
    setTestSendResult(null);
    const trimmed = (recipient ?? '').trim();
    if (typeof window !== 'undefined') {
      if (trimmed) window.localStorage.setItem('mailforge:testRecipient', trimmed);
      else window.localStorage.removeItem('mailforge:testRecipient');
    }
    try {
      const res = await fetch('/api/send-test', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          workspaceSlug: briefing.workspaceSlug,
          subject: output.strategy.subject_variants[0],
          bodyHtml: output.html,
          bodyText: output.text,
          preheader: output.strategy.preheader,
          senderEmail: selectedSenderEmail || undefined,
          ...(trimmed ? { to: trimmed } : {}),
        }),
      });
      const data = await res.json();
      if (!res.ok) {
        setTestSendResult({
          ok: false,
          message: data.error ?? `HTTP ${res.status}`,
          kind: data.errorKind,
          endpoint: data.endpoint,
          httpStatus: data.httpStatus,
          hint: data.hint,
        });
      } else {
        setTestSendResult({ ok: true, message: data.message });
      }
    } catch (e) {
      setTestSendResult({
        ok: false,
        message: e instanceof Error ? e.message : 'Network error',
        kind: 'network',
        hint: 'Browser konnte /api/send-test nicht erreichen — läuft der Dev-Server noch?',
      });
    } finally {
      setIsSendingTest(false);
      // Auto-clear nach 12s — länger wegen Hint-Text
      setTimeout(() => setTestSendResult(null), 12000);
    }
  };

  // Workspaces aus DB laden + bei Settings-Änderungen refreshen
  const loadBrands = () => {
    fetch('/api/workspaces')
      .then(r => r.json())
      .then(d => setBrands(
        (d.workspaces ?? []).map((b: { id: string; slug: string; name: string; primaryColor: string }) => ({
          id: b.id,
          slug: b.slug,
          name: b.name,
          primaryColor: b.primaryColor,
        }))
      ))
      .catch(() => setBrands([]));
  };
  useEffect(loadBrands, []);
  const [activeTab, setActiveTab] = useState<TabName>('edit');
  const [isReRendering, setIsReRendering] = useState(false);

  /**
   * Re-Render mit optionalen Hero-Overrides — wird vom EditPanel genutzt um
   * pro Render Hero zu entfernen, regenerieren oder durch Upload zu ersetzen.
   * Wenn `overrides.heroUrl === null` → Hero komplett deaktivieren (heroEnabled=false).
   * Wenn `overrides.heroUrl === string` → diese URL verwenden statt Active-Variant-Hero.
   */
  const handleReRender = async (
    updatedStrategy: EditableStrategy,
    overrides?: { heroEnabled?: boolean; heroUrl?: string | null },
  ) => {
    if (!output || !briefing) return;
    // Active Variant ermitteln — Re-Render schreibt in genau diese, nicht immer in A.
    const activeOutput = activeVariantIdx === 0 ? output : extraVariants[activeVariantIdx - 1];
    if (!activeOutput) return;

    // Effektive Hero-Werte: Override > Active-Variant-State > Briefing-Default
    const heroDeleted = overrides?.heroUrl === null;
    const effectiveHeroEnabled = heroDeleted
      ? false
      : (overrides?.heroEnabled ?? briefing.heroEnabled);
    const effectiveHeroUrl = heroDeleted
      ? undefined
      : (overrides?.heroUrl ?? activeOutput.hero?.absoluteUrl);

    setIsReRendering(true);
    try {
      // ExtraSections beim Re-Render mitschicken — sonst verschwinden Custom-Sections
      // (App-Banner, Kontakt, Testimonial, FAQ) sobald der User den Body editiert.
      // Filter+Strip identisch zu stripBriefingForApi (UI-only-Felder droppen).
      const stripExtraForApi = briefing.extraSections
        ?.filter((s) => s.enabled)
        .map((s) => ({
          instanceId: s.instanceId,
          templateId: s.templateId,
          kind: s.kind,
          position: s.position,
          contentJson: s.contentJson,
          ...(s.bodyMode && s.bodyMode !== 'tiptap' && s.customBody
            ? { bodyMode: s.bodyMode, customBody: s.customBody }
            : {}),
        }));

      // Sekundärer CTA wird vom Render-Template (render-template.ts Z. 91) nur gerendert
      // wenn cta_secondary_text UND ctaSecondaryUrl truthy sind. Wenn der Toggle im
      // Briefing/Strategy-Editor OFF ist, übergeben wir beide Werte als undefined —
      // Daten im briefing/strategy-State bleiben unberührt (User-Wunsch: "beibehalten,
      // nur ausblenden"), nur das gerenderte Email lässt den Sekundärlink weg.
      const isSecondaryCtaOn = briefing.optional?.secondaryCta ?? false;
      const strategyForRender = isSecondaryCtaOn
        ? updatedStrategy
        : { ...updatedStrategy, cta_secondary_text: undefined };

      const res = await fetch('/api/render', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          strategy: strategyForRender,
          workspaceSlug: briefing.workspaceSlug,
          ctaPrimaryUrl: briefing.ctaPrimaryUrl,
          ctaSecondaryUrl: isSecondaryCtaOn ? briefing.ctaSecondaryUrl : undefined,
          heroUrl: effectiveHeroUrl,
          heroEnabled: effectiveHeroEnabled,
          heroHeight: effectiveHeroEnabled ? briefing.heroHeight : undefined,
          heroCropPosition: effectiveHeroEnabled && briefing.heroHeight ? briefing.heroCropPosition : undefined,
          extraSections: stripExtraForApi && stripExtraForApi.length > 0 ? stripExtraForApi : undefined,
        }),
      });
      const data = await res.json();
      if (!res.ok) {
        setError(data.error ?? `Re-render failed (${res.status})`);
        return;
      }
      // Hero-State: bei Override aktualisieren, sonst den existierenden behalten
      const newHero = heroDeleted
        ? undefined
        : overrides?.heroUrl
          ? {
              url: overrides.heroUrl,
              absoluteUrl: overrides.heroUrl,
              bytes: 0,
              cached: false,
              durationMs: 0,
              prompt: activeOutput.hero?.prompt ?? '',
            }
          : activeOutput.hero;

      const merged: GenerateResponse = {
        ...activeOutput,
        strategy: updatedStrategy,
        html: data.html,
        text: data.text,
        htmlBytes: data.htmlBytes,
        mjml: data.mjml,
        mjmlErrors: data.mjmlErrors,
        spam: data.spam,
        hero: newHero,
        meta: { ...activeOutput.meta, modelUsed: 'local-template (re-render)', durationMs: data.meta.durationMs },
      };
      // Schreibe Re-Render-Result in die AKTIVE Variante
      if (activeVariantIdx === 0) {
        setOutput(merged);
      } else {
        const next = [...extraVariants];
        next[activeVariantIdx - 1] = merged;
        setExtraVariants(next);
      }
      // Snapshot der Hero-Settings für dirty-Check — was lief WIRKLICH durch /api/render
      setLastRenderedHeroSettings({
        height: effectiveHeroEnabled ? briefing.heroHeight : undefined,
        position: effectiveHeroEnabled && briefing.heroHeight ? briefing.heroCropPosition : undefined,
      });
      // setActiveTab('preview') wurde hier entfernt — bei Auto-Render aus dem
      // Bearbeiten-Tab würde das visuell den Sidebar-Highlight wegspringen lassen.
      // Beide Tabs zeigen seit Split-Layout ohnehin dieselbe Ansicht.
      void autoSave.snapshot({ source: 'milestone', milestone: 'render', label: 'Re-Render' });
    } catch (e) {
      setError(e instanceof Error ? e.message : 'Re-render error');
    } finally {
      setIsReRendering(false);
    }
  };

  // ─── Hero-Image Aktionen ──────────────────────────────────────────────────
  // Drei Aktionen im EditPanel (Bearbeiten-Tab → Hero-Bild-Section):
  //  • Delete: schickt heroUrl=null Override an Re-Render → komplett ohne Hero
  //  • Regenerate: ruft /api/regenerate-hero mit aktuellem image_brief → setzt neue URL → Re-Render
  //  • Upload: lädt File via /api/upload-image hoch → setzt neue URL → Re-Render
  const [isHeroBusy, setIsHeroBusy] = useState(false);

  const handleDeleteHero = useCallback(
    (s: EditableStrategy) => {
      void handleReRender(s, { heroUrl: null });
    },
    // handleReRender wird stable referenced über handleReRenderRef im inline-edit listener;
    // hier reicht die direkte closure-binding
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [briefing, output, extraVariants, activeVariantIdx],
  );

  const handleRegenerateHero = useCallback(
    async (s: EditableStrategy, imageBrief: string) => {
      if (!briefing?.workspaceSlug) {
        setError('Bitte erst eine Workspace im Briefing wählen');
        return;
      }
      setIsHeroBusy(true);
      try {
        const res = await fetch('/api/regenerate-hero', {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({ imageBrief, workspaceSlug: briefing.workspaceSlug, aspectRatio: '3:2' }),
        });
        const data = await res.json();
        if (!res.ok) {
          setError(data.error ?? `Hero-Regenerate fehlgeschlagen (${res.status})`);
          return;
        }
        // Strategy mit neuem image_brief mergen + Re-Render mit neuer URL
        const nextStrategy = { ...s, image_brief: imageBrief };
        await handleReRender(nextStrategy, { heroEnabled: true, heroUrl: data.hero.absoluteUrl });
      } catch (e) {
        setError(e instanceof Error ? e.message : 'Hero-Regenerate Fehler');
      } finally {
        setIsHeroBusy(false);
      }
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [briefing, output, extraVariants, activeVariantIdx],
  );

  const handleUploadHero = useCallback(
    async (s: EditableStrategy, file: File) => {
      setIsHeroBusy(true);
      try {
        const fd = new FormData();
        fd.append('file', file);
        const res = await fetch('/api/upload-image', { method: 'POST', body: fd });
        const data = await res.json();
        if (!res.ok) {
          setError(data.error ?? `Upload fehlgeschlagen (${res.status})`);
          return;
        }
        await handleReRender(s, { heroEnabled: true, heroUrl: data.hero.absoluteUrl });
      } catch (e) {
        setError(e instanceof Error ? e.message : 'Upload-Fehler');
      } finally {
        setIsHeroBusy(false);
      }
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [briefing, output, extraVariants, activeVariantIdx],
  );

  // ─── Inline-Editing ────────────────────────────────────────────────────────
  // Refs für Debounce + stable closures. handleReRender wird "frisch" pro Aufruf gelesen,
  // damit keine stale strategy-Snapshots in setTimeout-Closures eingefroren werden.
  const reRenderTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const handleReRenderRef = useRef(handleReRender);
  useEffect(() => { handleReRenderRef.current = handleReRender; });

  /** Aktualisiert ein Strategy-Feld immutably — flat (string), benefits-array oder subject_variants-array (mit index). */
  const applyStrategyFieldUpdate = useCallback(
    (s: EditableStrategy, key: string, index: number | undefined, value: string): EditableStrategy => {
      if (key === 'benefits' && index !== undefined) {
        const next = [...s.benefits];
        next[index] = value;
        return { ...s, benefits: next };
      }
      if (key === 'subject_variants' && index !== undefined) {
        const next = [...s.subject_variants];
        next[index] = value;
        return { ...s, subject_variants: next };
      }
      // Flache String-Felder — nur erlaubte Keys, sonst no-op
      const allowed: ReadonlyArray<keyof EditableStrategy> = [
        'preheader', 'hero_headline', 'intro', 'pain_point', 'solution',
        'cta_primary_text', 'cta_secondary_text', 'ps_line', 'image_brief',
      ];
      if (!allowed.includes(key as keyof EditableStrategy)) return s;
      return { ...s, [key]: value };
    },
    [],
  );

  /**
   * Schreibt eine neue Strategy in die aktive Variante (optimistic) und triggert einen
   * debounced Re-Render. Gemeinsamer Pfad für (a) iframe-Inline-Edits via postMessage und
   * (b) Form-Edits aus dem EditPanel — beide Quellen sehen denselben Optimistic-Flow und
   * dieselbe Debounce-Window, sodass schnelle Tipp-Bursts zu genau einem /api/render-Call
   * collapsen.
   */
  const commitStrategyChange = useCallback(
    (newStrategy: EditableStrategy, debounceMs: number) => {
      if (!output) return;
      const activeOutput = activeVariantIdx === 0 ? output : extraVariants[activeVariantIdx - 1];
      if (!activeOutput) return;

      const optimistic = { ...activeOutput, strategy: newStrategy };
      if (activeVariantIdx === 0) {
        setOutput(optimistic);
      } else {
        setExtraVariants(extraVariants.map((v, i) => (i === activeVariantIdx - 1 ? optimistic : v)));
      }

      // Manual-Override-Lock: User hat MJML/HTML im Source-Editor angepasst.
      // Strategy-Edits dürfen weiter im State landen (das Bearbeiten-Tab bleibt
      // bedienbar), aber wir triggern KEINEN Re-Render — der würde die manuelle
      // Anpassung in HTML/MJML komplett überschreiben.
      if (activeOutput.manualOverride) {
        if (reRenderTimerRef.current) clearTimeout(reRenderTimerRef.current);
        reRenderTimerRef.current = null;
        return;
      }

      if (reRenderTimerRef.current) clearTimeout(reRenderTimerRef.current);
      reRenderTimerRef.current = setTimeout(() => {
        handleReRenderRef.current(newStrategy);
        reRenderTimerRef.current = null;
      }, debounceMs);
    },
    [output, extraVariants, activeVariantIdx],
  );

  /** EditPanel → einzelnes Feld geändert (Form-Input). 500ms Debounce, etwas knackiger als iframe. */
  const handleFieldUpdate = useCallback(
    (key: string, index: number | undefined, value: string) => {
      if (!output) return;
      const activeOutput = activeVariantIdx === 0 ? output : extraVariants[activeVariantIdx - 1];
      if (!activeOutput) return;
      const newStrategy = applyStrategyFieldUpdate(activeOutput.strategy, key, index, value);
      commitStrategyChange(newStrategy, 500);
    },
    [output, extraVariants, activeVariantIdx, applyStrategyFieldUpdate, commitStrategyChange],
  );

  /** EditPanel → ganzes Strategy-Objekt ersetzen (z.B. Add/Remove Benefit, was applyStrategyFieldUpdate nicht abdeckt). */
  const handleStrategyReplace = useCallback(
    (newStrategy: EditableStrategy) => {
      commitStrategyChange(newStrategy, 500);
    },
    [commitStrategyChange],
  );

  /**
   * EditPanel → CTA-URL geändert. URL lebt im Briefing-State (Single-Source-of-Truth),
   * nicht in der Strategy. Wir setzen den State und triggern denselben Debounce-Re-Render
   * wie bei Strategy-Edits — handleReRender liest beim nächsten Tick die neuen URLs aus
   * dem `briefing`-Closure (via handleReRenderRef-Refresh nach State-Commit).
   *
   * Leerer String für sekundäre URL → undefined (Schema-Konvention im Briefing).
   */
  const handleCtaUrlChange = useCallback(
    (field: 'primary' | 'secondary', value: string) => {
      if (!briefing || !output) return;
      setBriefing({
        ...briefing,
        ...(field === 'primary'
          ? { ctaPrimaryUrl: value }
          : { ctaSecondaryUrl: value || undefined }),
      });
      const activeOutput = activeVariantIdx === 0 ? output : extraVariants[activeVariantIdx - 1];
      if (!activeOutput) return;
      commitStrategyChange(activeOutput.strategy, 500);
    },
    [briefing, output, extraVariants, activeVariantIdx, commitStrategyChange],
  );

  /**
   * EditPanel → Hero-Crop-Setting (Höhe oder Position) geändert. Felder leben auf `briefing`,
   * nicht auf Strategy — gleicher Pfad wie handleCtaUrlChange: setBriefing + Strategy-Commit
   * triggert den debounced Re-Render, der `briefing.heroHeight`/`heroCropPosition` aus dem
   * frischen Closure (via handleReRenderRef) liest.
   */
  const handleHeroSettingChange = useCallback(
    (next: { heroHeight?: number | undefined; heroCropPosition?: 'top' | 'center' | 'bottom' | undefined }) => {
      if (!briefing || !output) return;
      setBriefing({ ...briefing, ...next });
      const activeOutput = activeVariantIdx === 0 ? output : extraVariants[activeVariantIdx - 1];
      if (!activeOutput) return;
      commitStrategyChange(activeOutput.strategy, 500);
    },
    [briefing, output, extraVariants, activeVariantIdx, commitStrategyChange],
  );

  /**
   * Listener für Inline-Edits aus dem Preview-iframe.
   * Optimistic-UI: Strategy wird sofort im State aktualisiert (sodass der „Bearbeiten"-Tab synchron bleibt),
   * Re-Render via /api/render läuft debounced (600ms) — mehrere schnell aufeinander folgende Edits
   * triggern nur einen API-Call.
   */
  useEffect(() => {
    function onMessage(e: MessageEvent) {
      if (!output) return;
      const activeOutput = activeVariantIdx === 0 ? output : extraVariants[activeVariantIdx - 1];
      if (!activeOutput) return;

      if (isInlineEditMessage(e.data)) {
        const { key, index, value } = e.data;
        const newStrategy = applyStrategyFieldUpdate(activeOutput.strategy, key, index, value);
        commitStrategyChange(newStrategy, 600);
        return;
      }

      if (isInlineActionMessage(e.data)) {
        const { action, index } = e.data;
        if (action === 'add-benefit') {
          const next = { ...activeOutput.strategy, benefits: [...activeOutput.strategy.benefits, ''] };
          commitStrategyChange(next, 0);
        } else if (action === 'remove-benefit' && typeof index === 'number') {
          const next = {
            ...activeOutput.strategy,
            benefits: activeOutput.strategy.benefits.filter((_, i) => i !== index),
          };
          commitStrategyChange(next, 0);
        }
        return;
      }

      if (isInlineResizeMessage(e.data)) {
        // Iframe meldet seine scrollHeight — wir setzen iframe.style.height entsprechend,
        // damit die Page natürlich scrollt statt internem iframe-Scroll.
        setPreviewHeight(e.data.height);
      }
    }
    window.addEventListener('message', onMessage);
    return () => {
      window.removeEventListener('message', onMessage);
    };
  }, [output, extraVariants, activeVariantIdx, applyStrategyFieldUpdate, commitStrategyChange]);

  // Timer-Cleanup gehört NICHT zur Listener-Effect oben — sonst killt jede Dep-Änderung
  // (output ändert sich pro Tastendruck via Optimistic-Update in commitStrategyChange)
  // den gerade gesetzten Debounce-Timer, bevor /api/render feuern kann. commitStrategyChange
  // räumt vorherige Timer ohnehin selbst auf — hier brauchen wir nur Unmount-Cleanup.
  useEffect(() => {
    return () => {
      if (reRenderTimerRef.current) clearTimeout(reRenderTimerRef.current);
    };
  }, []);

  // Auto-Re-Render nach Hydrate ODER frischem Generate: beide Pfade liefern HTML ohne
  // data-edit-key-Spans (DB-Snapshot kommt von alten Renders ohne Marker; LLM-Output
  // emittiert keine, weil das System-Prompt sie nicht verlangt) → Preview-iframe wäre
  // nicht inline-editierbar. Ein einmaliger /api/render-Call überschreibt output.html
  // via render-template.ts mit deterministisch markiertem HTML. handleReRender setzt
  // dann meta.modelUsed='local-template (re-render)' — diesen einen Wert schließen wir
  // hier aus, damit der Effect nicht in einem Endlos-Loop landet.
  // Manual-Override (User hat MJML/HTML im Source-Tab editiert) wird respektiert.
  useEffect(() => {
    if (!output) return;
    if (output.meta.modelUsed === 'local-template (re-render)') return;
    if (!briefing.workspaceSlug) return;
    if (output.manualOverride) return;
    void handleReRenderRef.current(output.strategy);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [output?.meta.modelUsed, briefing.workspaceSlug]);

  /**
   * KI-Suggest-Trigger — extrahiert Briefing-Felder aus dem Editor-Text via /api/suggest.
   * Logik 1:1 aus BriefingPanel.handleSuggest verschoben (State-Lift) inkl. Auto-Workspace-Pick.
   * Wird vom Footer-CTA aufgerufen wenn der Briefing-Tab im `suggest`-State ist.
   */
  const handleSuggest = async () => {
    setIsSuggesting(true);
    setSuggestError(null);
    setSuggestFeedback(null);
    try {
      // Bestehende User-Eingaben mitsenden — die KI füllt nur LEERE Felder, überschreibt nichts
      const existing: Record<string, unknown> = {};
      (Object.keys(briefing) as Array<keyof Briefing>).forEach((k) => {
        const v = briefing[k];
        if (v !== undefined && v !== null && v !== '' && v !== EMPTY_BRIEFING[k]) {
          existing[k as string] = v;
        }
      });
      const res = await fetch('/api/suggest', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ briefText: content.text ?? '', existing }),
      });
      const data = await res.json();
      if (!res.ok) {
        setSuggestError(data.error ?? `Server returned ${res.status}`);
        return;
      }
      const sugg = data.suggestions as Partial<Briefing> & { reasoning?: string; inferred?: string[] };
      const filled: string[] = [];
      const inferred: string[] = [];
      const next: Briefing = { ...briefing };
      const incomingInferred = new Set(sugg.inferred ?? []);
      (Object.keys(sugg) as Array<keyof Briefing>).forEach((k) => {
        if (k === 'reasoning' as keyof Briefing || k === 'inferred' as keyof Briefing) return;
        const val = sugg[k];
        if (val === undefined || val === null || val === '') return;
        const cur = briefing[k];
        const isEmpty = cur === undefined || cur === null || cur === '' || cur === EMPTY_BRIEFING[k];
        if (isEmpty) {
          (next[k] as unknown) = val;
          filled.push(k as string);
          if (incomingInferred.has(k as string)) inferred.push(k as string);
        }
      });
      // Auto-Pick Workspace: wenn KI keinen Workspace inferiert hat aber mind. eine konfiguriert ist,
      // setze automatisch die erste — sonst bliebe der Generate-Button disabled trotz aller anderen Felder.
      if (!next.workspaceSlug && workspaces.length > 0) {
        next.workspaceSlug = workspaces[0].slug;
        if (!filled.includes('workspaceSlug')) filled.push('workspaceSlug (auto)');
      }
      setBriefing(next);
      setInferredFields(new Set(inferred));
      setSuggestFeedback({
        filledKeys: filled,
        inferredKeys: inferred,
        reasoning: sugg.reasoning,
        durationMs: data.meta?.durationMs ?? 0,
      });
    } catch (e) {
      setSuggestError(e instanceof Error ? e.message : 'Network error');
    } finally {
      setIsSuggesting(false);
    }
  };

  /**
   * KI-Brief-Trigger — POST /api/draft-brief mit gefülltem Briefing (+ optional Scrape-Kontext).
   * Der Server liefert TipTap-JSON zurück; wir setzen es via editorRef.replaceContent() in den
   * MarketingEditor (Tab 1) und switchen den Page-View dorthin, damit der User den Brief sofort sieht.
   * Wird sowohl vom BriefingPanel-Block (Tab 2) als auch vom Editor-Toolbar-Sparkles-Button (Tab 1) aufgerufen.
   */
  const handleDraftBrief = async () => {
    if (!isBriefingReady(briefing)) {
      setDraftBriefError('Briefing-Pflichtfelder fehlen — Kampagne, Angebot, Schmerzpunkt und Workspace werden gebraucht.');
      return;
    }
    setIsDraftingBrief(true);
    setDraftBriefError(null);
    try {
      const stripped = stripBriefingForApi(briefing);
      // Scrape-Kontext aus dem letzten Scrape-Result mitgeben — gibt dem LLM mehr Wording-Inspiration.
      // Body-Text vom Scrape-Endpoint kommt im Result als `briefText` (siehe handleScrapeUrl).
      const scrapeContext = scrapeResult
        ? {
            title: scrapeResult.title || undefined,
            description: scrapeResult.description || undefined,
            // briefText ist auf ScrapedSummary nicht typed — kommt aber im Server-Response mit.
            // Cast bleibt hier minimal weil Server-Schema ihn akzeptiert.
            bodyText: (scrapeResult as ScrapedSummary & { briefText?: string }).briefText,
          }
        : undefined;
      const res = await fetch('/api/draft-brief', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          campaignName: stripped.campaignName,
          offer: stripped.offer,
          painPoint: stripped.painPoint,
          ctaPrimaryText: stripped.ctaPrimaryText,
          workspaceSlug: stripped.workspaceSlug,
          heroBriefHint: stripped.heroBriefHint,
          language: stripped.language ?? 'auto',
          scrapeContext,
        }),
      });
      const data = await res.json();
      if (!res.ok) {
        const issues = (data.issues as string[] | undefined)?.join('; ');
        setDraftBriefError(issues ? `${data.error}: ${issues}` : data.error ?? `Server returned ${res.status}`);
        return;
      }
      // 1. Editor füllen via imperative API (Editor bleibt mounted dank `hidden`-Toggle).
      //    setContent(json, true) emit's onUpdate → unsere Page-State + Auto-Save werden mit-aktualisiert.
      editorRef.current?.replaceContent(data.tiptapJson);
      // 2. Defensiver Fallback: falls der Editor noch nicht mounted ist, halten wir den Content
      //    direkt im Page-State — beim nächsten Mount würde initialJson via ref-API gesetzt.
      setContent({ json: data.tiptapJson, html: data.html, text: data.text });
      // 3. User auf den Editor-Tab schicken, damit er den Brief sofort sieht.
      setPageView('editor');
    } catch (e) {
      setDraftBriefError(e instanceof Error ? e.message : 'Netzwerkfehler beim KI-Brief-Schreiben');
    } finally {
      setIsDraftingBrief(false);
    }
  };

  /**
   * Scrapt eine Webseite und triggert anschließend den KI-Suggest mit dem extrahierten Text.
   * Doppelt-API-Call ist absichtlich: Scrape liefert Bilder + Text, Suggest mappt den Text auf Briefing-Felder.
   */
  const handleScrapeUrl = async (url: string): Promise<ScrapedSummary | null> => {
    setIsScraping(true);
    setSuggestError(null);
    try {
      const res = await fetch('/api/scrape-url', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ url }),
      });
      const data = await res.json();
      if (!res.ok) {
        setSuggestError(data.error ?? `Scrape fehlgeschlagen (${res.status})`);
        return null;
      }
      const result = data.scrape as ScrapedSummary & { briefText: string };
      setScrapeResult(result);

      // Auto-Suggest mit dem gescrapten Text — nur empty Felder werden befüllt
      try {
        setIsSuggesting(true);
        const existing: Record<string, unknown> = {};
        (Object.keys(briefing) as Array<keyof Briefing>).forEach((k) => {
          const v = briefing[k];
          if (v !== undefined && v !== null && v !== '' && v !== EMPTY_BRIEFING[k]) {
            existing[k as string] = v;
          }
        });
        const sugRes = await fetch('/api/suggest', {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({ briefText: result.briefText, existing }),
        });
        const sugData = await sugRes.json();
        if (sugRes.ok) {
          const sugg = sugData.suggestions as Partial<Briefing> & { reasoning?: string; inferred?: string[] };
          const filled: string[] = [];
          const inferred: string[] = [];
          const next: Briefing = { ...briefing };
          const incomingInferred = new Set(sugg.inferred ?? []);
          (Object.keys(sugg) as Array<keyof Briefing>).forEach((k) => {
            if (k === 'reasoning' as keyof Briefing || k === 'inferred' as keyof Briefing) return;
            const val = sugg[k];
            if (val === undefined || val === null || val === '') return;
            const cur = briefing[k];
            const isEmpty = cur === undefined || cur === null || cur === '' || cur === EMPTY_BRIEFING[k];
            if (isEmpty) {
              (next[k] as unknown) = val;
              filled.push(k as string);
              if (incomingInferred.has(k as string)) inferred.push(k as string);
            }
          });
          if (!next.workspaceSlug && workspaces.length > 0) next.workspaceSlug = workspaces[0].slug;
          setBriefing(next);
          setInferredFields(new Set(inferred));
          setSuggestFeedback({
            filledKeys: filled,
            inferredKeys: inferred,
            reasoning: `Aus der Webseite ${url} extrahiert${sugg.reasoning ? ' — ' + sugg.reasoning : ''}`,
            durationMs: sugData.meta?.durationMs ?? 0,
          });
        }
      } finally {
        setIsSuggesting(false);
      }
      return result;
    } catch (e) {
      setSuggestError(e instanceof Error ? e.message : 'Scrape error');
      return null;
    } finally {
      setIsScraping(false);
    }
  };

  /**
   * Importiert ein Bild von der gescrapten Webseite — Server lädt Remote-URL,
   * speichert lokal in `public/assets/upload/`, briefing.heroImageUrl bekommt die lokale URL.
   */
  const handleSelectScrapedImage = async (originalImageUrl: string) => {
    setIsImportingImage(true);
    try {
      const res = await fetch('/api/import-image-from-url', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ url: originalImageUrl }),
      });
      const data = await res.json();
      if (!res.ok) {
        setSuggestError(data.error ?? `Import fehlgeschlagen (${res.status})`);
        return;
      }
      // Absolute URL nutzen damit Email-Clients (extern) sie sicher laden können
      setBriefing({ ...briefing, heroImageUrl: data.hero.absoluteUrl });
    } catch (e) {
      setSuggestError(e instanceof Error ? e.message : 'Import error');
    } finally {
      setIsImportingImage(false);
    }
  };

  const handleGenerate = async (b: Briefing) => {
    setIsGenerating(true);
    setOutput(null);
    setError(null);
    setErrorIssues(null);
    setPageView('output'); // Auto-Switch zum Email-Tab — User sieht den Loader
    try {
      // Defensive URL-Normalisierung: prepend https:// wenn fehlt
      const normalizeUrl = (u?: string) => {
        if (!u) return u;
        const t = u.trim();
        if (!t) return undefined;
        if (/^(https?:|mailto:|tel:)/i.test(t)) return t;
        return `https://${t}`;
      };
      // stripBriefingForApi entfernt UI-only `optional`-Toggle-State und setzt
      // deaktivierte Optional-Felder (Toggle OFF) auf undefined → AI sieht nur was User aktiv haben will.
      const stripped = stripBriefingForApi(b);
      const payload = {
        ...stripped,
        ctaPrimaryUrl: normalizeUrl(stripped.ctaPrimaryUrl) ?? '',
        ctaSecondaryUrl: normalizeUrl(stripped.ctaSecondaryUrl),
        briefHtml: content.html || undefined,
      };
      const res = await fetch('/api/generate', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(payload),
      });
      const data = await res.json();
      if (!res.ok) {
        setError(data.error ?? `Server returned ${res.status}`);
        if (Array.isArray(data.issues)) setErrorIssues(data.issues);
        return;
      }
      setOutput(data as GenerateResponse);
      // Snapshot der Hero-Settings für dirty-Check im Bearbeiten-Tab
      setLastRenderedHeroSettings({
        height: briefing.heroEnabled ? briefing.heroHeight : undefined,
        position: briefing.heroEnabled && briefing.heroHeight ? briefing.heroCropPosition : undefined,
      });
      // Wenn der Editor leer war (User hat direkt aus Briefing/Template generiert ohne Brief zu tippen),
      // schreiben wir die generierten Body-Texte als editierbaren Brief zurück in den Editor.
      // So kann der User in Tab 1 nachfeilen und mit dem überarbeiteten Brief erneut generieren.
      // Wenn der User bereits selbst getippt hat, lassen wir seinen Inhalt unangetastet.
      //
      // `strategyToTiptapDoc` (unten in dieser Datei definiert) baut aus den Strategy-Feldern
      // einen Tiptap-Doc: H2-Headline + Paragraphs + Bullet-List + P.S. + Signatur. Das ist
      // bewusst LOSSY (kein 1:1-Roundtrip) — der Editor zeigt die Kern-Botschaft als Brief, der
      // User feilt nach, das nächste Generate liest `content.html` als `briefHtml`-Input.
      if (!content.text.trim()) {
        const tiptapDoc = strategyToTiptapDoc((data as GenerateResponse).strategy);
        // replaceContent feuert onUpdate UND ruft onChange direkt nochmal mit den echten
        // editor.getJSON()/getHTML()/getText()-Werten auf (siehe MarketingEditor.tsx). Damit ist
        // content.{json,html,text} korrekt synchronisiert. KEIN zusätzliches setContent() hier —
        // ein nachgelagertes setContent({..., html: '', text: ''}) würde via React-Batching die
        // korrekten Werte mit leeren Strings überschreiben → nächster Re-Generate-Run sendet
        // briefHtml: undefined und verliert den Editor-Kontext.
        editorRef.current?.replaceContent(tiptapDoc);
      }
      // Frischer Generate-Run → A/B-State resetten (alte Varianten gehören zum vorherigen Output)
      setExtraVariants([]);
      setActiveVariantIdx(0);
      setActiveTab('edit');
      // Milestone-Snapshot — nachdem der State sich gesetzt hat, läuft der Auto-Save-Debounce.
      // snapshot() ruft intern flush(), wartet also auf den persistierten Stand.
      void autoSave.snapshot({ source: 'milestone', milestone: 'generate', label: 'Generate-Run' });
    } catch (e) {
      setError(e instanceof Error ? e.message : 'Network error');
    } finally {
      setIsGenerating(false);
    }
  };

  return (
    <div className="flex h-screen w-screen overflow-hidden bg-zinc-50 dark:bg-zinc-950">
      <Sidebar
        drafts={draft.drafts}
        workspaces={workspaces}
        currentDraftId={draft.currentDraftId}
        autoSaveStatus={autoSave.status}
        autoSaveLastAt={autoSave.lastSavedAt}
        autoSaveError={autoSave.error}
        onSelectDraft={(id) => void draft.loadDraft(id)}
        onCreateDraft={(workspaceId) => void draft.createDraft({ workspaceId })}
        onDuplicateDraft={(id) => void draft.duplicateDraft(id)}
        onDeleteDraft={(id) => void draft.deleteDraft(id)}
        onOpenVersions={(id) => setVersionPanelDraftId(id)}
        onLoadTemplate={handleLoadTemplate}
        onDeleteTemplate={handleDeleteTemplate}
      />
      <VersionHistoryPanel
        open={!!versionPanelDraftId}
        draftId={versionPanelDraftId}
        draftName={draft.drafts.find((d) => d.id === versionPanelDraftId)?.name ?? ''}
        onClose={() => setVersionPanelDraftId(null)}
        onAfterRestore={async () => {
          if (versionPanelDraftId) await draft.loadDraft(versionPanelDraftId);
        }}
      />
      <div className="flex h-full min-w-0 flex-1 flex-col overflow-y-auto" key={draft.currentDraftId ?? 'no-draft'}>
      <div className="sticky top-0 z-30 shrink-0">
        <EditorHeader
          draftName={briefing.campaignName || draft.currentDraft?.name || null}
          onRenameDraft={(next) => setBriefing((prev) => ({ ...prev, campaignName: next }))}
          nameMissing={!briefing.campaignName?.trim()}
          nameInferred={inferredFields.has('campaignName')}
          onSave={() => void autoSave.snapshot({ source: 'manual', label: `Manueller Snapshot — ${new Date().toLocaleString('de-DE')}` })}
          onSaveAsTemplate={handleSaveAsTemplate}
          onPublishTrigger={() => setPublishTriggerOpen(true)}
          onOpenSettings={() => setSettingsOpen(true)}
          pageView={pageView}
          onChangePageView={setPageView}
          hasOutput={!!output}
          isGenerating={isGenerating}
          recipientCount={briefing.recipients?.length ?? 0}
        />
      </div>
      <div className="flex gap-4 p-4">
        {/*
         * MarketingEditor wird IMMER gemountet (visibility-toggle via `hidden`).
         * Grund: TipTap zerstört bei Unmount die Editor-Instanz inkl. Cursor-Position —
         * wir brauchen die Cursor-Position aber persistent, damit handleInsertPlaceholder
         * vom Zielgruppe-Tab aus an der zuletzt aktiven Stelle einfügen kann.
         * `hidden` setzt CSS `display: none` → React-Subtree bleibt mounted, useEditor lebt.
         */}
        <main className={cn('flex flex-1 min-w-0 flex-col gap-3', pageView !== 'editor' && 'hidden')}>
          <MarketingEditor
            ref={editorRef}
            className="shrink-0 min-h-[600px]"
            // initialJson wird nur beim Mount von TipTap gelesen (Editor-Internal).
            // Der äußere Wrapper-Div hat `key={draft.currentDraftId}` (s. oben) — bei
            // Draft-Wechsel remountet der gesamte Subtree, inkl. Editor. Der frisch
            // gemountete Editor übernimmt dann diesen Initial-Wert. Ohne diesen Prop
            // blieb der Editor nach Draft-Reload leer, obwohl `content.json` korrekt
            // aus `hydrateFromDraft` befüllt ist (Z. 181).
            initialJson={(content.json as object | undefined) ?? undefined}
            onChange={(json, html, text) => setContent({ json, html, text })}
            onAiDraftBrief={handleDraftBrief}
            aiDraftBriefDisabled={!isBriefingReady(briefing)}
            aiDraftBriefBusy={isDraftingBrief}
          />
        </main>
        {pageView === 'briefing' ? (
          // Block-Layout statt flex-row — sonst zwingt `align-items: stretch` den aside auf
          // die Container-Höhe und der Inhalt überläuft den Border. mx-auto zentriert horizontal.
          <main className="flex-1 min-w-0">
            <BriefingPanel
              workspaces={workspaces}
              value={briefing}
              onChange={setBriefing}
              briefText={content.text}
              layout="responsive"
              className="mx-auto w-full max-w-[1200px]"
              // Geliftete KI-Suggest-Props — Footer-CTA triggert handleSuggest, Panel rendert nur Feedback/Error
              isSuggesting={isSuggesting}
              suggestFeedback={suggestFeedback}
              suggestError={suggestError}
              inferredFields={inferredFields}
              onDismissFeedback={() => setSuggestFeedback(null)}
              onDismissError={() => setSuggestError(null)}
              // Template-Picker oben im Panel — Liste der Templates für die aktive Workspace
              templates={availableTemplates}
              onLoadTemplate={handleLoadTemplate}
              onDeleteTemplate={handleDeleteTemplate}
              // Webseiten-Scrape — extrahiert Title/Description/Bilder, fülle Briefing automatisch
              onScrapeUrl={handleScrapeUrl}
              onSelectScrapedImage={handleSelectScrapedImage}
              isScraping={isScraping}
              isImportingImage={isImportingImage}
              scrapeResult={scrapeResult}
              // KI-Brief — separater zweiter Schritt nach Scrapen+Briefing füllen
              onDraftBrief={handleDraftBrief}
              isDraftingBrief={isDraftingBrief}
              draftBriefError={draftBriefError}
              onDismissDraftBriefError={() => setDraftBriefError(null)}
              // Sender-Picker — gehört zur Workspace-Identität, war vorher im Header
              senders={m365Senders}
              selectedSenderEmail={selectedSenderEmail}
              onSelectSender={setSelectedSenderEmail}
              // Intent='text' Workflow erlaubt schnellen Sprung zurück in Tab 1 zum Edit
              onJumpToEditor={() => setPageView('editor')}
            />
          </main>
        ) : pageView === 'audience' ? (
          // Tab 3: Zielgruppe — full-Tab Inline-Variante des RecipientPickers (CRM-Audience-Builder).
          // Reuse via `displayMode='inline'` — keine Modal-Chrome, keine Backdrop. onConfirm speichert
          // direkt in briefing.recipients ohne Tab-Switch (User entscheidet selbst wann er weitergeht).
          // Ref + onLoadingChange exposen die loadPreview()-Funktion an den AudienceFooter,
          // damit der "Empfänger laden"-Button immer unten sichtbar bleibt.
          <main className="flex-1 min-w-0 p-4">
            <RecipientPicker
              ref={audiencePickerRef}
              open={true}
              onClose={() => { /* no-op im Inline-Mode */ }}
              onConfirm={(rs, snap) => setBriefing({ ...briefing, recipients: rs, recipientFilter: snap ?? briefing.recipientFilter })}
              workspaceSlug={briefing.workspaceSlug}
              initialFilter={briefing.recipientFilter}
              displayMode="inline"
              onLoadingChange={setAudienceLoading}
            />
          </main>
        ) : pageView === 'scheduler' ? (
          // Tab 5: Versand — Scheduler mit KI-Vorschlag für optimale Versandzeit.
          // Audience-Source wird aus dem RecipientFilter abgeleitet (Cumas-Section determines B2B/B2C).
          // Kein eigener Card-Wrapper mehr — Inhalt liegt direkt auf dem Page-Hintergrund.
          <main className="flex-1 min-w-0">
            <div className="mx-auto w-full max-w-[1100px]">
              <SchedulerPanel
                value={briefing.schedule ?? { mode: 'now', throttlePerHour: 100 }}
                onChange={(next) => setBriefing({ ...briefing, schedule: next })}
                briefing={briefing}
                onBriefingPatch={(patch) => setBriefing((prev) => ({ ...prev, ...patch }))}
                variantCount={1 + extraVariants.length}
                audienceSource={briefing.recipientFilter?.source}
              />
            </div>
          </main>
        ) : pageView === 'dashboard' ? (
          // Cross-Campaign-Dashboard: unabhängig von der aktuellen Campaign-Auswahl,
          // KPIs der letzten 30 Tage über alle Workspaces. Eigener Tab mit Separator
          // (kein Pipeline-Schritt, sondern globale Analyse-Sicht).
          <main className="flex-1 min-w-0">
            <div className="mx-auto w-full max-w-[1300px]">
              <DashboardPanel />
            </div>
          </main>
        ) : pageView === 'output' ? (
          <main className="flex flex-1 min-w-0 flex-col gap-4">
            {error && (
              <div className="flex items-start gap-2 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-300">
                <XCircle className="mt-0.5 h-4 w-4 shrink-0" />
                <div className="min-w-0 flex-1">
                  <div className="font-medium">Generierung fehlgeschlagen</div>
                  <div className="text-xs opacity-80">{error}</div>
                  {errorIssues && errorIssues.length > 0 && (
                    <ul className="mt-2 space-y-0.5 text-xs">
                      {errorIssues.map((issue, i) => (
                        <li key={i} className="font-mono">
                          <span className="opacity-60">·</span> {issue}
                        </li>
                      ))}
                    </ul>
                  )}
                </div>
              </div>
            )}
            {isGenerating && !output && (
              <div className="flex flex-1 items-center justify-center rounded-lg border border-zinc-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-950">
                <GenerateLoader heroEnabled={briefing?.heroEnabled ?? true} />
              </div>
            )}
            {output && (
              <OutputSection
                output={activeVariantIdx === 0 ? output : (extraVariants[activeVariantIdx - 1] ?? output)}
                activeTab={activeTab}
                setActiveTab={setActiveTab}
                onFieldUpdate={handleFieldUpdate}
                onStrategyReplace={handleStrategyReplace}
                ctaUrls={{ primary: briefing.ctaPrimaryUrl, secondary: briefing.ctaSecondaryUrl }}
                onCtaUrlChange={handleCtaUrlChange}
                isReRendering={isReRendering}
                variantLabels={['A', ...extraVariants.map((_, i) => String.fromCharCode(66 + i))]}
                activeVariantIdx={activeVariantIdx}
                onSwitchVariant={(idx) => {
                  // Pending Debounce-Render gehört zur alten Variant — flushen, sonst landen die letzten
                  // Edits (mit Verzögerung) auf der falschen Variant.
                  if (reRenderTimerRef.current) {
                    clearTimeout(reRenderTimerRef.current);
                    reRenderTimerRef.current = null;
                  }
                  setActiveVariantIdx(idx);
                }}
                onAddVariant={() => {
                  // Neue Variante = exakte Kopie der aktuell aktiven (User editiert dann in Bearbeiten-Tab)
                  const src = activeVariantIdx === 0 ? output : extraVariants[activeVariantIdx - 1];
                  if (!src) return;
                  setExtraVariants([...extraVariants, structuredClone(src)]);
                  setActiveVariantIdx(extraVariants.length + 1); // springe direkt auf neue Variante
                }}
                onRemoveVariant={(idx) => {
                  if (idx === 0) return; // A nicht löschbar
                  const next = extraVariants.filter((_, i) => i !== idx - 1);
                  setExtraVariants(next);
                  setActiveVariantIdx(Math.min(activeVariantIdx, next.length));
                }}
                heroControls={{
                  current: (activeVariantIdx === 0 ? output : extraVariants[activeVariantIdx - 1])?.hero,
                  isBusy: isHeroBusy,
                  onDelete: handleDeleteHero,
                  onRegenerate: handleRegenerateHero,
                  onUpload: handleUploadHero,
                }}
                heroHeight={briefing.heroHeight}
                heroCropPosition={briefing.heroCropPosition}
                onChangeHeroSetting={handleHeroSettingChange}
                onSaveAsTemplate={handleSaveAsTemplate}
                sentCampaignId={lastSentCampaignId}
                workspaceSlug={briefing.workspaceSlug}
                brandName={workspaces.find((b) => b.slug === briefing.workspaceSlug)?.name ?? ''}
                briefingForPreview={{
                  ctaPrimaryUrl: briefing.ctaPrimaryUrl,
                  ctaSecondaryUrl: briefing.ctaSecondaryUrl,
                  heroUrl: (activeVariantIdx === 0 ? output : extraVariants[activeVariantIdx - 1])?.hero?.absoluteUrl,
                }}
                firstRecipient={briefing.recipients?.[0]}
                onUpdateActiveOutput={(updater) => {
                  const activeOutput = activeVariantIdx === 0 ? output : extraVariants[activeVariantIdx - 1];
                  if (!activeOutput) return;
                  const next = updater(activeOutput);
                  if (activeVariantIdx === 0) {
                    setOutput(next);
                  } else {
                    setExtraVariants(extraVariants.map((v, i) => (i === activeVariantIdx - 1 ? next : v)));
                  }
                }}
                onResetOverrideAndRerender={() => {
                  const activeOutput = activeVariantIdx === 0 ? output : extraVariants[activeVariantIdx - 1];
                  if (!activeOutput) return;
                  const next = { ...activeOutput, manualOverride: null };
                  if (activeVariantIdx === 0) {
                    setOutput(next);
                  } else {
                    setExtraVariants(extraVariants.map((v, i) => (i === activeVariantIdx - 1 ? next : v)));
                  }
                  // Sofortiger Re-Render aus Strategy — kein Debounce, damit der User
                  // sieht, dass Strategy-Edits jetzt wieder greifen.
                  if (reRenderTimerRef.current) clearTimeout(reRenderTimerRef.current);
                  handleReRenderRef.current(activeOutput.strategy);
                }}
                secondaryCtaEnabled={briefing.optional?.secondaryCta ?? false}
                onToggleSecondaryCta={(v) => {
                  setBriefing({ ...briefing, optional: { ...(briefing.optional ?? {}), secondaryCta: v } });
                  // Toggle alleine triggert sonst keinen Re-Render → das gerenderte HTML
                  // würde den Sekundärlink weiter zeigen (bzw. nicht zeigen). Gleicher Pfad
                  // wie handleCtaUrlChange / handleHeroSettingChange: setState + commit.
                  const activeOutput = activeVariantIdx === 0 ? output : extraVariants[activeVariantIdx - 1];
                  if (activeOutput) commitStrategyChange(activeOutput.strategy, 500);
                }}
                briefing={briefing}
                onBriefingPatch={(patch) => setBriefing((prev) => ({ ...prev, ...patch }))}
                previewHeight={previewHeight}
              />
            )}
            {!isGenerating && !output && !error && (
              <div className="flex flex-1 flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-zinc-300 bg-white p-12 text-center dark:border-zinc-700 dark:bg-zinc-950">
                <MailOpen className="h-10 w-10 text-zinc-300" />
                <p className="text-sm text-zinc-500">Noch keine Email generiert</p>
                <button
                  onClick={() => setPageView('briefing')}
                  className="rounded-md bg-zinc-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900"
                >
                  Zum Briefing
                </button>
              </div>
            )}
          </main>
        ) : null}
      </div>
      {pageView === 'editor' && (
        <footer className="sticky bottom-0 z-20 mt-auto flex shrink-0 items-center justify-between gap-4 border-t border-zinc-200 bg-white px-4 py-3 dark:border-zinc-800 dark:bg-zinc-950">
          <span className="flex-1 text-xs text-zinc-500">
            <span className="font-medium text-zinc-700 dark:text-zinc-300">Schritt 1 von 3</span>
            {' — '}wenn du fertig getippt hast, geht&apos;s zu Briefing &amp; Goal
          </span>
          <div className="shrink-0">
            <AutoSaveIndicator status={autoSave.status} lastSavedAt={autoSave.lastSavedAt} error={autoSave.error} />
          </div>
          <div className="flex flex-1 items-center justify-end gap-2">
            {/*
              "AdMail aus diesem Brief aktualisieren" — nur sichtbar wenn bereits eine AdMail
              generiert wurde. Triggert handleGenerate(briefing), das den aktuellen Editor-Inhalt
              automatisch als briefHtml mitschickt (s. Z. 1078). pageView wechselt im
              handleGenerate selbst auf 'output', damit der User das Ergebnis sieht.
            */}
            {output && (
              <button
                onClick={() => void handleGenerate(briefing)}
                disabled={isGenerating || !isBriefingReady(briefing)}
                title={
                  !isBriefingReady(briefing)
                    ? 'Briefing-Pflichtfelder fehlen — Kampagne, Angebot, Schmerzpunkt und Workspace werden gebraucht.'
                    : 'Generiert die AdMail mit dem aktuellen Editor-Text als Vorlage neu.'
                }
                className="inline-flex items-center gap-2 rounded-md border border-zinc-300 bg-white px-4 py-2.5 text-sm font-semibold text-zinc-700 shadow-sm transition hover:bg-zinc-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800"
              >
                {isGenerating ? (
                  <Loader2 className="h-4 w-4 animate-spin" />
                ) : (
                  <RefreshCw className="h-4 w-4" />
                )}
                AdMail aus diesem Brief aktualisieren
              </button>
            )}
            <button
              onClick={() => setPageView('briefing')}
              className="inline-flex items-center gap-2 rounded-md bg-[#ff006a] px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-[#e60060]"
            >
              Weiter zu Briefing <ArrowRight className="h-4 w-4" />
            </button>
          </div>
        </footer>
      )}
      {pageView === 'output' && output && (
        <AdMailFooter
          spamVerdict={output.spam.verdict}
          spamScore={output.spam.score}
          recipientCount={briefing.recipients?.length ?? 0}
          onGoToSpamTab={() => setActiveTab('spam')}
          onContinueToAudience={() => setPageView('audience')}
          autoSaveStatus={autoSave.status}
          autoSaveLastAt={autoSave.lastSavedAt}
          autoSaveError={autoSave.error}
        />
      )}
      {pageView === 'audience' && (
        <AudienceFooter
          loading={audienceLoading}
          recipientCount={briefing.recipients?.length ?? 0}
          onLoad={() => audiencePickerRef.current?.loadPreview()}
          onContinue={() => setPageView('scheduler')}
          autoSaveStatus={autoSave.status}
          autoSaveLastAt={autoSave.lastSavedAt}
          autoSaveError={autoSave.error}
        />
      )}
      {pageView === 'briefing' && (
        <BuilderFooter
          briefing={briefing}
          briefHasContent={(content.text ?? '').trim().length >= 10}
          onGenerate={() => handleGenerate(briefing)}
          onSuggest={handleSuggest}
          onJumpToEditor={() => setPageView('editor')}
          onScrape={() => briefing.scrapedUrl && handleScrapeUrl(briefing.scrapedUrl)}
          onDraftBrief={handleDraftBrief}
          isGenerating={isGenerating}
          isSuggesting={isSuggesting}
          isScraping={isScraping}
          isDraftingBrief={isDraftingBrief}
          hasScrapeResult={!!scrapeResult}
          autoSaveStatus={autoSave.status}
          autoSaveLastAt={autoSave.lastSavedAt}
          autoSaveError={autoSave.error}
        />
      )}
      {pageView === 'scheduler' && (
        <SchedulerFooter
          autoSaveStatus={autoSave.status}
          autoSaveLastAt={autoSave.lastSavedAt}
          autoSaveError={autoSave.error}
          senders={m365Senders}
          selectedSenderEmail={selectedSenderEmail}
          onSelectSender={setSelectedSenderEmail}
          hasOutput={!!output}
          recipientCount={briefing.recipients?.length ?? 0}
          onSendTest={handleSendTest}
          isSendingTest={isSendingTest}
          testSendResult={testSendResult}
          testRecipient={testRecipient}
          onChangeTestRecipient={setTestRecipient}
          onSendBulk={handleSendBulk}
          isSendingBulk={isSendingBulk}
          bulkResult={bulkResult}
        />
      )}
      {/* SuggestLoader — Modal-Overlay über die ganze Page während KI-Brief-Analyse läuft */}
      {isSuggesting && <SuggestLoader />}
      {/* DraftBriefLoader — Modal-Overlay während die KI einen Marketing-Brief in den Editor schreibt */}
      {isDraftingBrief && <DraftBriefLoader />}
      <SettingsDrawer
        open={settingsOpen}
        onClose={() => setSettingsOpen(false)}
        onM365SendersPoolSaved={refreshM365Senders}
        onBrandsChanged={(savedSlug) => {
          loadBrands();
          // Auto-Re-Render der Live-Preview, wenn die gerade gespeicherte Workspace auch
          // die aktive Workspace ist. Header/Footer-Änderungen (Phone, Tagline,
          // Gesellschaftsdaten, Pflichtangaben) leben im brandKit und werden bei
          // /api/render frisch aus der DB gezogen — ein einzelner Render-Call genügt.
          if (!savedSlug || !briefing || !output) return;
          if (briefing.workspaceSlug !== savedSlug) return;
          const activeOutput = activeVariantIdx === 0 ? output : extraVariants[activeVariantIdx - 1];
          if (!activeOutput) return;
          void handleReRenderRef.current(activeOutput.strategy);
        }}
      />
      <PublishTriggerDialog
        open={publishTriggerOpen}
        onClose={() => setPublishTriggerOpen(false)}
        m365Senders={m365Senders.map(s => ({ email: s.email, name: s.name }))}
        draft={(() => {
          if (!output) return null;
          const workspaceId = workspaces.find(b => b.slug === briefing.workspaceSlug)?.id;
          if (!workspaceId) return null;
          // Subject aus Strategy: bevorzugt erstes subject_variant, fallback campaignName.
          const subjectVariants = (output.strategy?.subject_variants ?? []) as string[];
          const subject = subjectVariants[0]?.trim() || briefing.campaignName || 'Trigger-Mail';
          const preheader = (output.strategy?.preheader as string | undefined) ?? '';
          return {
            workspaceId,
            subject,
            preheader,
            mjml: output.mjml,
            html: output.html,
            htmlText: output.text,
          };
        })()}
      />
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// AudienceFooter — Bottom-Bar auf der Zielgruppe-Tab. Pink-CTA „Empfänger laden"
// triggert den Picker per Ref, sodass der Button auch bei langen Filter-Listen
// immer sichtbar bleibt. Wenn schon Empfänger geladen → "Weiter"-Button erscheint.
// ─────────────────────────────────────────────────────────────────────────────

function AudienceFooter({
  loading,
  recipientCount,
  onLoad,
  onContinue,
  autoSaveStatus,
  autoSaveLastAt,
  autoSaveError,
}: {
  loading: boolean;
  recipientCount: number;
  onLoad: () => void;
  onContinue: () => void;
  autoSaveStatus: import('@/hooks/useAutoSave').AutoSaveStatus;
  autoSaveLastAt: Date | null;
  autoSaveError?: string | null;
}) {
  const hasRecipients = recipientCount > 0;

  // State-Machine — Single Source of Action für die Zielgruppe-Tab:
  //   A · idle    → Pink-CTA "Empfänger laden"
  //   B · loading → CTA disabled mit Spinner
  //   C · loaded  → Pink-CTA "Weiter zu Versand", sekundär "Erneut laden"
  // Auto-Confirm (im RecipientPicker, Inline-Mode) sorgt dafür, dass C sofort nach Load
  // erreicht wird — ohne separaten "Auswahl übernehmen"-Klick.
  return (
    <footer className="sticky bottom-0 z-20 mt-auto flex shrink-0 items-center justify-between gap-4 border-t border-zinc-200 bg-white px-4 py-3 dark:border-zinc-800 dark:bg-zinc-950">
      {/* LEFT — Status-Hint, kontext-abhängig */}
      <div className="min-w-0 flex-1 text-xs">
        {loading ? (
          <span className="inline-flex items-center gap-1.5 text-zinc-500">
            <Loader2 className="h-3.5 w-3.5 animate-spin" />
            Lade Empfänger aus dem CRM …
          </span>
        ) : hasRecipients ? (
          <span className="inline-flex items-center gap-1.5 text-emerald-700 dark:text-emerald-400">
            <CheckCircle2 className="h-3.5 w-3.5" />
            <span>
              <span className="font-medium">{recipientCount.toLocaleString('de-DE')} Empfänger</span> für den Versand bereit
            </span>
          </span>
        ) : (
          <span className="text-zinc-500">
            <span className="font-medium text-zinc-700 dark:text-zinc-300">Filter setzen</span>
            {' — '}danach klicke <span className="font-medium text-[#ff006a]">Empfänger laden</span> um die Liste zu sehen
          </span>
        )}
      </div>

      {/* MIDDLE — AutoSave-Indikator + sekundärer „Erneut laden"-Knopf nur wenn schon Empfänger da sind */}
      <div className="flex shrink-0 items-center gap-2">
        <AutoSaveIndicator status={autoSaveStatus} lastSavedAt={autoSaveLastAt} error={autoSaveError} />
        {hasRecipients && !loading && (
          <button
            type="button"
            onClick={onLoad}
            className="inline-flex items-center gap-2 rounded-md border border-zinc-300 bg-white px-3 py-2 text-xs font-medium text-zinc-700 transition hover:border-zinc-400 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300"
          >
            <RefreshCw className="h-3.5 w-3.5" /> Erneut laden
          </button>
        )}
      </div>

      {/* RIGHT — Primary CTA wechselt: vor Load = "Empfänger laden", nach Load = "Weiter zu Versand" */}
      <button
        type="button"
        onClick={loading ? undefined : hasRecipients ? onContinue : onLoad}
        disabled={loading}
        className={cn(
          'inline-flex shrink-0 items-center gap-2 rounded-md px-5 py-2.5 text-sm font-semibold transition',
          'bg-[#ff006a] text-white shadow-sm hover:bg-[#e60060]',
          'disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:bg-[#ff006a]',
        )}
      >
        {loading ? (
          <>
            <Loader2 className="h-4 w-4 animate-spin" /> Lade Empfänger …
          </>
        ) : hasRecipients ? (
          <>
            Weiter zu Versand <ArrowRight className="h-4 w-4" />
          </>
        ) : (
          <>
            <UserPlus className="h-4 w-4" /> Empfänger laden
          </>
        )}
      </button>
    </footer>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// AdMailFooter — Bottom-Bar auf Tab 3 (AdMail). State-Machine für nächsten Schritt:
//   • spamReject → Rot-CTA "Spam-Issues beheben" (öffnet Spam-Sub-Tab)
//   • spamFix    → Amber-Status-Hint + sekundärer "Spam ansehen" + Pink-CTA "Trotzdem weiter"
//   • ship       → Grüner Status-Hint "Spam-Score X · ship" + Pink-CTA "Weiter zu Zielgruppe"
// Damit ist der Footer konsequent mit Editor/Briefing/Audience/Scheduler-Tabs und
// der User hat eine klare Next-Action — egal ob die Mail clean ist oder Issues hat.
// ─────────────────────────────────────────────────────────────────────────────

function AdMailFooter({
  spamVerdict,
  spamScore,
  recipientCount,
  onGoToSpamTab,
  onContinueToAudience,
  autoSaveStatus,
  autoSaveLastAt,
  autoSaveError,
}: {
  spamVerdict: 'ship' | 'fix' | 'reject';
  spamScore: number;
  recipientCount: number;
  onGoToSpamTab: () => void;
  onContinueToAudience: () => void;
  autoSaveStatus: import('@/hooks/useAutoSave').AutoSaveStatus;
  autoSaveLastAt: Date | null;
  autoSaveError?: string | null;
}) {
  const hasRecipients = recipientCount > 0;

  type CtaState = 'spamReject' | 'spamFix' | 'ship';
  const ctaState: CtaState =
    spamVerdict === 'reject' ? 'spamReject'
    : spamVerdict === 'fix' ? 'spamFix'
    : 'ship';

  return (
    <footer className="sticky bottom-0 z-20 mt-auto flex shrink-0 items-center justify-between gap-4 border-t border-zinc-200 bg-white px-4 py-3 dark:border-zinc-800 dark:bg-zinc-950">
      {/* LEFT — Status-Hint, kontext-abhängig */}
      <div className="min-w-0 flex-1 text-xs">
        {ctaState === 'spamReject' ? (
          <span className="inline-flex items-center gap-1.5 text-red-700 dark:text-red-400">
            <XCircle className="h-3.5 w-3.5" />
            <span>
              <span className="font-medium">Spam-Score {spamScore} · reject</span>
              {' — '}Diese Mail würde in Spam-Filtern hängen bleiben.
            </span>
          </span>
        ) : ctaState === 'spamFix' ? (
          <span className="inline-flex items-center gap-1.5 text-amber-700 dark:text-amber-400">
            <AlertTriangle className="h-3.5 w-3.5" />
            <span>
              <span className="font-medium">Spam-Score {spamScore} · fix</span>
              {' — '}Verbesserungen empfohlen, Versand möglich.
            </span>
          </span>
        ) : (
          <span className="inline-flex items-center gap-1.5 text-emerald-700 dark:text-emerald-400">
            <CheckCircle2 className="h-3.5 w-3.5" />
            <span>
              <span className="font-medium">Spam-Score {spamScore} · ship</span>
              {' — '}
              {hasRecipients
                ? `${recipientCount.toLocaleString('de-DE')} Empfänger bereit · weiter zum Versand`
                : 'Mail ist versandbereit · Zielgruppe wählen'}
            </span>
          </span>
        )}
      </div>

      {/* MIDDLE — AutoSave + sekundärer „Spam ansehen"-Button im fix-State */}
      <div className="flex shrink-0 items-center gap-2">
        <AutoSaveIndicator status={autoSaveStatus} lastSavedAt={autoSaveLastAt} error={autoSaveError} />
        {ctaState === 'spamFix' && (
          <button
            type="button"
            onClick={onGoToSpamTab}
            className="inline-flex items-center gap-2 rounded-md border border-zinc-300 bg-white px-3 py-2 text-xs font-medium text-zinc-700 transition hover:border-zinc-400 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300"
          >
            <Shield className="h-3.5 w-3.5" /> Spam ansehen
          </button>
        )}
      </div>

      {/* RIGHT — Primary CTA: morpht je nach State */}
      <button
        type="button"
        onClick={ctaState === 'spamReject' ? onGoToSpamTab : onContinueToAudience}
        className={cn(
          'inline-flex shrink-0 items-center gap-2 rounded-md px-5 py-2.5 text-sm font-semibold transition',
          ctaState === 'spamReject'
            ? 'bg-red-600 text-white shadow-sm hover:bg-red-700'
            : 'bg-[#ff006a] text-white shadow-sm hover:bg-[#e60060]',
        )}
      >
        {ctaState === 'spamReject' ? (
          <>
            <Shield className="h-4 w-4" />
            Spam-Issues beheben
          </>
        ) : (
          <>
            Weiter zu Zielgruppe
            <ArrowRight className="h-4 w-4" />
          </>
        )}
      </button>
    </footer>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// BuilderFooter — fixed Bottom-Bar mit Empfänger-Picker-Trigger + Generate-CTA
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Footer mit context-aware Primary CTA (State-Machine).
 *
 * Der rechte Pink-Button ist **Single-Source-of-Truth** für die nächste Aktion und morpht je nach Briefing-State:
 *
 *   • `noContent`  (briefHasContent === false)             → "Schreibe zuerst im Editor"  · ArrowLeft  · setPageView('editor')  · Outline-Style
 *   • `suggest`    (briefHasContent && !isBriefingReady)   → "KI-Vorschlag erzeugen"      · Wand2      · onSuggest()             · Pink-Solid
 *   • `generate`   (isBriefingReady)                       → "Marketing-Email generieren" · Sparkles   · onGenerate()            · Pink-Solid
 *
 * Loading-Overrides: `isSuggesting` und `isGenerating` zeigen Spinner + disabled.
 * Damit ist der Button **fast nie** disabled — UX-Prinzip "always show next possible action".
 */
function BuilderFooter({
  briefing,
  briefHasContent,
  onGenerate,
  onSuggest,
  onJumpToEditor,
  onScrape,
  onDraftBrief,
  isGenerating,
  isSuggesting,
  isScraping,
  isDraftingBrief,
  hasScrapeResult,
  autoSaveStatus,
  autoSaveLastAt,
  autoSaveError,
}: {
  briefing: Briefing;
  briefHasContent: boolean;
  onGenerate: () => void;
  onSuggest: () => void;
  onJumpToEditor: () => void;
  /** Trigger Webseiten-Scrape — nutzt briefing.scrapedUrl. Greift nur wenn URL eingegeben aber Felder leer. */
  onScrape: () => void;
  /** Trigger KI-Brief-Generierung in den Editor (Tab 1). Nur relevant für intent='idea'. */
  onDraftBrief: () => void;
  isGenerating: boolean;
  isSuggesting: boolean;
  isScraping: boolean;
  isDraftingBrief: boolean;
  /** Wahr sobald der User mind. einen Scrape erfolgreich abgeschlossen hat. */
  hasScrapeResult: boolean;
  autoSaveStatus: import('@/hooks/useAutoSave').AutoSaveStatus;
  autoSaveLastAt: Date | null;
  autoSaveError?: string | null;
}) {
  const ready = isBriefingReady(briefing);
  // Name (campaignName) wird oben in der Header-h1 inline editiert — wird daher separat
  // gemeldet ("Kampagnen-Name in der Titelzeile setzen"), nicht in der missing-Liste,
  // weil der User sonst suchen würde wo das Feld liegt.
  const nameMissing = !briefing.campaignName?.trim();
  const missing: string[] = [];
  if (!briefing.offer) missing.push('Angebot');
  if (!briefing.painPoint) missing.push('Schmerzpunkt');
  if (!briefing.workspaceSlug) missing.push('Workspace');

  // Effective Intent: Auto-Detect wenn der User noch nichts gewählt hat.
  // Briefing-Panel persistiert den Intent beim Mount, aber während der ersten Render-Frame
  // ist b.intent ggf. noch undefined — defensiv mit detectIntent berechnen.
  const intent: BriefingIntent = briefing.intent ?? detectIntent(briefing, briefHasContent ? 'X'.repeat(100) : '');

  // ─── State-Machine: welche Action ist als nächstes möglich? ─────────────────
  // States, die VOR der "ready"-Prüfung greifen:
  //   • scrape     — intent='idea', User hat URL eingegeben aber Felder noch leer + nicht gescraped
  //   • draftBrief — intent='idea', Briefing ready, Editor noch leer
  type CtaState = 'noContent' | 'scrape' | 'suggest' | 'draftBrief' | 'generate';
  const ctaState: CtaState = (() => {
    if (intent === 'text') {
      if (!briefHasContent) return 'noContent';
      if (!ready) return 'suggest';
      return 'generate';
    }
    // intent === 'idea': URL-Scrape ist optionaler Sub-Workflow.
    // Nur als CTA vorschlagen wenn der User aktiv eine URL eingegeben hat aber die
    // Pflichtfelder noch leer sind — sonst bleibt der Standard-Flow (suggest/draftBrief/generate).
    // `hasScrapeResult` ist lokaler Page-State und nach Reload immer false → ohne Skip nach 'ready'
    // bliebe der CTA in "Scrapen + Briefing füllen" hängen, obwohl alle Pflichtfelder gesetzt sind.
    const urlDrivenFill = !!briefing.scrapedUrl && !briefing.offer && !briefing.painPoint;
    if (urlDrivenFill && !ready && !hasScrapeResult) return 'scrape';
    if (!ready) return 'suggest';
    if (!briefHasContent) return 'draftBrief';
    return 'generate';
  })();
  const isLoading = isSuggesting || isGenerating || isScraping || isDraftingBrief;

  // CTA-Konfiguration als Daten (deklarativ statt verschachtelte Conditionals).
  // Loading-Overrides: Label wird zum Spinner-Status, onClick wird no-op (button ist eh disabled).
  const ctaConfig: Record<CtaState, { label: string; icon: typeof Wand2; onClick: () => void; primary: boolean; disabled?: boolean }> = {
    noContent:   { label: 'Schreibe zuerst im Editor',   icon: ArrowLeft, onClick: onJumpToEditor, primary: false },
    scrape:      { label: 'Webseite scrapen + Briefing füllen', icon: Wand2, onClick: onScrape,    primary: true  },
    suggest:     { label: 'KI-Vorschlag erzeugen',       icon: Wand2,     onClick: onSuggest,      primary: true  },
    draftBrief:  { label: 'KI-Brief in Editor schreiben', icon: Sparkles, onClick: onDraftBrief,   primary: true  },
    generate:    { label: 'Marketing-Email generieren',  icon: Sparkles,  onClick: onGenerate,     primary: true  },
  };
  const cfg = ctaConfig[ctaState];
  const Icon = isLoading ? Loader2 : cfg.icon;
  const label = isSuggesting ? 'Analysiere deinen Brief …'
    : isGenerating ? 'Generiere …'
    : isScraping ? 'Lade & analysiere …'
    : isDraftingBrief ? 'Schreibt …'
    : cfg.label;

  return (
    <footer className="sticky bottom-0 z-20 mt-auto flex shrink-0 items-center justify-between gap-4 border-t border-zinc-200 bg-white px-4 py-3 dark:border-zinc-800 dark:bg-zinc-950">
      {/* LEFT — kontext-sensitiver Status-Hint */}
      <div className="min-w-0 flex-1 text-xs">
        {ctaState === 'generate' ? (
          <span className="inline-flex items-center gap-1.5 text-emerald-700 dark:text-emerald-400">
            <CheckCircle2 className="h-3.5 w-3.5" />
            Briefing vollständig — bereit zum Generieren
          </span>
        ) : ctaState === 'suggest' ? (
          <span className="text-zinc-500">
            {missing.length > 0 ? (
              <>
                <span className="font-medium">{missing.length}&nbsp;Pflichtfeld{missing.length === 1 ? '' : 'er'} fehlen:</span> {missing.join(', ')}
                {nameMissing && <span className="ml-1">·&nbsp;Kampagnen-Name oben in der Titelzeile</span>}
                <span className="ml-2 text-zinc-400">— KI-Vorschlag füllt sie aus deinem Brief</span>
              </>
            ) : nameMissing ? (
              <>
                <span className="font-medium">Kampagnen-Name fehlt</span>
                <span className="ml-1 text-zinc-400">— klick oben auf den Titel zum Umbenennen</span>
              </>
            ) : null}
          </span>
        ) : ctaState === 'scrape' ? (
          <span className="text-zinc-500">
            <span className="font-medium text-zinc-700 dark:text-zinc-300">URL bereit</span>
            {' — '}Klick scrapt die Seite und füllt Briefing-Felder + Bilder
          </span>
        ) : ctaState === 'draftBrief' ? (
          <span className="text-zinc-500">
            <span className="font-medium text-zinc-700 dark:text-zinc-300">Briefing fertig</span>
            {' — '}KI schreibt jetzt den Marketing-Brief in den Editor
          </span>
        ) : (
          <span className="text-zinc-500">
            <span className="font-medium text-zinc-700 dark:text-zinc-300">Brief noch leer</span>
            {' — '}Editor füllen, dann KI extrahiert die Pflichtfelder automatisch
          </span>
        )}
      </div>

      {/* MIDDLE — AutoSave-Indikator */}
      <div className="flex shrink-0 items-center gap-2">
        <AutoSaveIndicator status={autoSaveStatus} lastSavedAt={autoSaveLastAt} error={autoSaveError} />
      </div>

      {/* RIGHT — morphender Primary-CTA */}
      <button
        type="button"
        onClick={isLoading || cfg.disabled ? undefined : cfg.onClick}
        disabled={isLoading || !!cfg.disabled}
        className={cn(
          'inline-flex shrink-0 items-center gap-2 rounded-md px-5 py-2.5 text-sm font-semibold transition',
          cfg.primary
            ? 'bg-[#ff006a] text-white shadow-sm hover:bg-[#e60060] disabled:hover:bg-[#ff006a]'
            : 'border border-zinc-300 bg-white text-zinc-700 hover:border-zinc-400 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300',
          'disabled:cursor-not-allowed disabled:opacity-60',
        )}
      >
        <Icon className={cn('h-4 w-4', isLoading && 'animate-spin')} />
        {label}
      </button>
    </footer>
  );
}

/**
 * SchedulerFooter — Bottom-Bar auf der Versand-Tab (Tab 5).
 *
 * Hier liegen die Test-Versand- und Versand-Buttons + Sender-Picker, die vorher
 * permanent im Header sichtbar waren. Sie werden ausschließlich in dieser Tab
 * gebraucht, deshalb tauchen sie nur hier auf.
 *
 * `hasOutput` schaltet Test-Versand und Versand frei — vorher gibt es schlicht keinen
 * gerenderten Mail-Body, den man verschicken könnte.
 */
function SchedulerFooter({
  autoSaveStatus,
  autoSaveLastAt,
  autoSaveError,
  senders,
  selectedSenderEmail,
  onSelectSender,
  hasOutput,
  recipientCount,
  onSendTest,
  isSendingTest,
  testSendResult,
  testRecipient,
  onChangeTestRecipient,
  onSendBulk,
  isSendingBulk,
  bulkResult,
}: {
  autoSaveStatus: import('@/hooks/useAutoSave').AutoSaveStatus;
  autoSaveLastAt: Date | null;
  autoSaveError?: string | null;
  senders: Array<{ email: string; name: string; companyId?: number; note?: string }>;
  selectedSenderEmail: string;
  onSelectSender: (email: string) => void;
  hasOutput: boolean;
  recipientCount: number;
  onSendTest: (recipient?: string) => void;
  isSendingTest: boolean;
  testSendResult: {
    ok: boolean;
    message: string;
    kind?: 'network' | 'auth' | 'graph' | 'config' | 'unknown';
    endpoint?: string;
    httpStatus?: number;
    hint?: string;
  } | null;
  testRecipient: string;
  onChangeTestRecipient: (v: string) => void;
  onSendBulk: () => void;
  isSendingBulk: boolean;
  bulkResult: { ok: boolean; message: string; campaignId?: string } | null;
}) {
  return (
    <footer className="sticky bottom-0 z-20 mt-auto flex shrink-0 items-center justify-between gap-4 border-t border-zinc-200 bg-white px-4 py-3 dark:border-zinc-800 dark:bg-zinc-950">
      {/* LEFT — Sender-Override (sekundär — Default kommt aus Briefing-Tab) */}
      <div className="flex min-w-0 flex-1 items-center gap-2">
        {senders.length > 0 && (
          <SenderPickerCompact
            senders={senders}
            selectedSenderEmail={selectedSenderEmail}
            onSelectSender={onSelectSender}
          />
        )}
      </div>

      {/* MIDDLE — AutoSave-Indikator */}
      <div className="shrink-0">
        <AutoSaveIndicator status={autoSaveStatus} lastSavedAt={autoSaveLastAt} error={autoSaveError} />
      </div>

      {/* RIGHT — Test-Versand + Versand */}
      <div className="flex flex-1 items-center justify-end gap-2">
        {!hasOutput ? (
          <span className="text-xs text-zinc-500">
            Erst <span className="font-medium text-zinc-700 dark:text-zinc-300">AdMail generieren</span> (Tab 3) — dann ist Versand möglich
          </span>
        ) : (
          <>
            <TestSendButton
              isSendingTest={isSendingTest}
              testRecipient={testRecipient}
              onChangeTestRecipient={onChangeTestRecipient}
              onSendTest={onSendTest}
              testSendResult={testSendResult}
            />
            <div className="relative">
              <button
                onClick={onSendBulk}
                disabled={isSendingBulk || recipientCount === 0}
                title={recipientCount === 0 ? 'Erst Empfänger im Briefing auswählen' : `${recipientCount} Empfänger in Versand-Queue stellen`}
                className="inline-flex items-center gap-1.5 rounded-md bg-[#ff006a] px-4 py-2 text-sm font-semibold text-white transition hover:bg-[#e60060] disabled:opacity-40 disabled:hover:bg-[#ff006a]"
              >
                {isSendingBulk ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
                {isSendingBulk ? 'Stelle ein …' : `Versand${recipientCount > 0 ? ` (${recipientCount})` : ''}`}
              </button>
              {bulkResult && <BulkResultPopover result={bulkResult} />}
            </div>
          </>
        )}
      </div>
    </footer>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Page-Tabs (Briefing / Email)
// ─────────────────────────────────────────────────────────────────────────────

type PageView = 'editor' | 'briefing' | 'output' | 'audience' | 'scheduler' | 'dashboard';

function PageTab({ active, onClick, icon, label, muted, badge }: {
  active: boolean; onClick: () => void; icon: React.ReactNode; label: string; muted?: boolean; badge?: string;
}) {
  return (
    <button
      onClick={onClick}
      className={cn(
        'inline-flex items-center gap-2 border-b-2 px-4 py-2.5 text-sm font-medium transition',
        active
          ? 'border-zinc-900 text-zinc-900 dark:border-zinc-100 dark:text-zinc-100'
          : 'border-transparent text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300',
        muted && 'text-zinc-400 dark:text-zinc-600',
      )}
    >
      {icon}
      {label}
      {badge && <span className="ml-1 rounded bg-emerald-100 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700 dark:bg-emerald-950/50 dark:text-emerald-300">{badge}</span>}
    </button>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Briefing-View (Editor + Briefing-Panel)
// ─────────────────────────────────────────────────────────────────────────────


/**
 * Hero-Bild-Aktionen aus dem EditPanel — wird vom BuilderPage durchgereicht.
 * Alle 3 Aktionen brauchen das aktuelle `EditableStrategy` (für Re-Render-Call) und der Brief
 * für `onRegenerate`. Page-Level-Handler kümmern sich um API-Calls + Re-Render-Trigger.
 */
type HeroControls = {
  current: GenerateResponse['hero'];
  isBusy: boolean;
  onDelete: (s: EditableStrategy) => void;
  onRegenerate: (s: EditableStrategy, imageBrief: string) => Promise<void>;
  onUpload: (s: EditableStrategy, file: File) => Promise<void>;
};

function OutputSection({
  output,
  activeTab,
  setActiveTab,
  onFieldUpdate,
  onStrategyReplace,
  isReRendering,
  variantLabels,
  activeVariantIdx,
  onSwitchVariant,
  onAddVariant,
  onRemoveVariant,
  heroControls,
  heroHeight,
  heroCropPosition,
  onChangeHeroSetting,
  onSaveAsTemplate,
  sentCampaignId,
  ctaUrls,
  onCtaUrlChange,
  workspaceSlug,
  brandName,
  briefingForPreview,
  firstRecipient,
  onUpdateActiveOutput,
  onResetOverrideAndRerender,
  secondaryCtaEnabled,
  onToggleSecondaryCta,
  briefing,
  onBriefingPatch,
  previewHeight,
}: {
  output: GenerateResponse;
  activeTab: TabName;
  setActiveTab: (t: TabName) => void;
  /** Single-Field-Edit aus dem EditPanel — debounced Re-Render im Parent. */
  onFieldUpdate: (key: string, index: number | undefined, value: string) => void;
  /** Voll-Strategy-Replace (für Add/Remove Benefit) — debounced Re-Render im Parent. */
  onStrategyReplace: (s: EditableStrategy) => void;
  isReRendering: boolean;
  /** Labels aller Varianten (z.B. ['A','B','C']). Bei Länge 1 wird das A/B-UI nicht gerendert. */
  variantLabels: string[];
  activeVariantIdx: number;
  onSwitchVariant: (idx: number) => void;
  onAddVariant: () => void;
  onRemoveVariant: (idx: number) => void;
  /** Hero-Bild-Aktionen für den Bearbeiten-Tab. */
  heroControls: HeroControls;
  /** Aktuelle Hero-Höhe aus briefing — kontrolliert. undefined = Auto. */
  heroHeight: number | undefined;
  /** Aktuelle Crop-Position aus briefing — kontrolliert. undefined → 'center' im Render. */
  heroCropPosition: 'top' | 'center' | 'bottom' | undefined;
  /** Update Briefing.heroHeight und/oder Briefing.heroCropPosition + debounced Re-Render. */
  onChangeHeroSetting: (next: { heroHeight?: number | undefined; heroCropPosition?: 'top' | 'center' | 'bottom' | undefined }) => void;
  /** „Als Template speichern"-Action im Header. Speichert das aktive Briefing+Editor-State als Template. */
  onSaveAsTemplate?: () => void;
  /** Letzter erfolgreich versendeter Campaign — Stats-Tab pollt damit Open/Click-Counter. */
  sentCampaignId: string | null;
  /** CTA-URLs aus dem Briefing-State — werden im Bearbeiten-Tab editierbar gemacht. */
  ctaUrls: { primary: string; secondary?: string };
  /** Callback für URL-Edits im Bearbeiten-Tab — Parent updated `briefing` + Re-Render. */
  onCtaUrlChange: (field: 'primary' | 'secondary', value: string) => void;
  /** Workspace-Slug für /api/render-mjml — sonst kann der Endpoint die Workspace-Terms für Spam-Check nicht laden. */
  workspaceSlug: string;
  /** Workspace-Display-Name — für `{{BRAND_NAME}}`-Substitution in der Standalone-Preview. */
  brandName: string;
  /** Briefing-URLs für Preview-Substitution (CTA + Hero). */
  briefingForPreview: { ctaPrimaryUrl?: string; ctaSecondaryUrl?: string; heroUrl?: string };
  /** Erster geladener Recipient als Sample für die Standalone-Preview (optional). */
  firstRecipient?: PickerRecipient;
  /** Updater für den aktiven Output (A oder Variante) — Manual-Save aus dem Source-Editor. */
  onUpdateActiveOutput: (updater: (current: GenerateResponse) => GenerateResponse) => void;
  /** "Auf Strategy zurücksetzen": Override clearen + sofortiger Re-Render aus Strategy. */
  onResetOverrideAndRerender: () => void;
  /** Spiegelt briefing.optional.secondaryCta — Source-of-Truth ist das Briefing-Objekt. */
  secondaryCtaEnabled: boolean;
  onToggleSecondaryCta: (enabled: boolean) => void;
  /** Briefing wird ans EditPanel für die ExtraSections-Group weitergereicht. */
  briefing: Briefing;
  /** Partial-Patch aufs Briefing — wird für extraSections im EditPanel genutzt. */
  onBriefingPatch: (patch: Partial<Briefing>) => void;
  /** Live-reported scrollHeight des Preview-iframes — null = noch kein Resize-Message angekommen. */
  previewHeight: number | null;
}) {
  const verdict = output.spam.verdict;
  const VerdictIcon = verdict === 'ship' ? CheckCircle2 : verdict === 'fix' ? AlertTriangle : XCircle;
  const verdictColor = verdict === 'ship' ? 'text-emerald-600' : verdict === 'fix' ? 'text-amber-600' : 'text-red-600';
  const verdictBg = verdict === 'ship' ? 'bg-emerald-50 border-emerald-200 dark:bg-emerald-950/30 dark:border-emerald-900/50' : verdict === 'fix' ? 'bg-amber-50 border-amber-200 dark:bg-amber-950/30 dark:border-amber-900/50' : 'bg-red-50 border-red-200 dark:bg-red-950/30 dark:border-red-900/50';

  // Inline-Editor: injectiert Style + Script in den HTML-srcDoc des iframes — Click-to-Edit für getaggte Slots.
  // useMemo verhindert Re-Inject (und damit iframe-Reload + Fokus-Verlust) bei Re-Renders die nicht den HTML ändern.
  const previewHtml = useMemo(() => injectInlineEditorScript(output.html), [output.html]);

  // Live-Preview-Toggle für den Bearbeiten-Tab. Wenn aus, bekommt das EditPanel volle Breite.
  // "In neuem Tab öffnen" (im Header) bleibt davon unberührt.
  const [livePreviewOn, setLivePreviewOn] = useState(true);

  // Resizable Spaltenlayout nur auf Desktop (≥1280 px). Auf Mobile/Tablet bleibt das Stacked-Fallback.
  // Sizes werden via PanelGroup `autoSaveId` automatisch in localStorage persistiert.
  const isDesktop = useMediaQuery('(min-width: 1280px)');

  return (
    <section className="flex shrink-0 flex-col rounded-lg border border-zinc-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-950">
      <header className="flex items-center justify-between gap-3 border-b border-zinc-200 px-4 py-3 dark:border-zinc-800">
        <div className="flex min-w-0 items-center gap-3">
          <Sparkles className="h-4 w-4 shrink-0 text-zinc-400" />
          <h3 className="text-sm font-semibold">Generierte Email</h3>
          <span className="truncate text-xs text-zinc-500">{output.meta.modelUsed} · {output.meta.durationMs}ms{output.meta.tokensIn ? ` · ${output.meta.tokensIn}→${output.meta.tokensOut} tok` : ''}</span>
        </div>
        <div className="flex shrink-0 items-center gap-2">
          <button
            type="button"
            onClick={() => {
              const finalHtml = applyPreviewPlaceholders(
                output.html,
                {
                  workspace: { name: brandName },
                  briefing: briefingForPreview,
                  recipient: firstRecipient
                    ? {
                        firstName: firstRecipient.firstName,
                        lastName: firstRecipient.lastName,
                        email: firstRecipient.email,
                        crmId: firstRecipient.cumasId != null ? String(firstRecipient.cumasId) : null,
                        customData: firstRecipient.rawRecord ?? null,
                      }
                    : null,
                },
                { stripUnknownTags: true },
              );
              const blob = new Blob([finalHtml], { type: 'text/html;charset=utf-8' });
              const url = URL.createObjectURL(blob);
              window.open(url, '_blank', 'noopener,noreferrer');
              // Memory-Leak-Schutz — Browser hält den Blob solange der Tab offen ist,
              // 30s Window reicht damit der Tab den Object-URL gefetched hat.
              setTimeout(() => URL.revokeObjectURL(url), 30_000);
            }}
            className="inline-flex items-center gap-1.5 rounded-md border border-zinc-200 bg-white px-2.5 py-1 text-xs font-medium text-zinc-700 transition hover:border-[#ff006a] hover:text-[#ff006a] dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300"
            title={firstRecipient
              ? `Mail in neuem Tab anzeigen — Platzhalter mit Daten von ${firstRecipient.email} abgefüllt`
              : 'Mail in neuem Tab anzeigen — Platzhalter mit Fiktiv-Daten (Max Mustermann) abgefüllt'}
          >
            <ExternalLink className="h-3.5 w-3.5" />
            In neuem Tab öffnen
          </button>
          {onSaveAsTemplate && (
            <button
              type="button"
              onClick={onSaveAsTemplate}
              className="inline-flex items-center gap-1.5 rounded-md border border-zinc-200 bg-white px-2.5 py-1 text-xs font-medium text-zinc-700 transition hover:border-[#ff006a] hover:text-[#ff006a] dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300"
              title="Aktuelles Briefing + Editor-State als wiederverwendbares Template speichern"
            >
              <LayoutTemplate className="h-3.5 w-3.5" />
              Als Template speichern
            </button>
          )}
          <div className={cn('inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium', verdictBg, verdictColor)}>
            <VerdictIcon className="h-3.5 w-3.5" />
            Spam-Score {output.spam.score} · {verdict}
          </div>
        </div>
      </header>

      <div className="min-h-0">
        {/* Top-Bar: horizontale Inhalt-Tabs links + kompakte A/B-Pillen rechts.
            Ersetzt die vorherige vertikale Sidebar — gibt Preview die volle Breite und
            entfernt das doppelte Subject/Preheader (jetzt nur noch in EditPanel mit Char-Counter). */}
        <div className="flex items-center justify-between gap-3 border-b border-zinc-200 px-3 py-1.5 dark:border-zinc-800">
          <div role="tablist" aria-label="Editor-Modi" className="flex items-center gap-0.5 overflow-x-auto">
            {(
              [
                { key: 'edit', label: 'Bearbeiten', icon: Pencil },
                { key: 'spam', label: `Spam (${output.spam.findings.length})`, icon: Shield },
                { key: 'stats', label: 'Stats', icon: BarChart3 },
                { key: 'mjml', label: 'MJML', icon: Code2 },
                { key: 'html', label: 'HTML', icon: FileCode2 },
                { key: 'text', label: 'Text', icon: AlignLeft },
              ] as const
            ).map((t) => {
              const Icon = t.icon;
              const isActive = activeTab === t.key;
              return (
                <button
                  key={t.key}
                  role="tab"
                  aria-selected={isActive}
                  onClick={() => setActiveTab(t.key as TabName)}
                  className={cn(
                    'flex shrink-0 items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium transition',
                    isActive
                      ? 'bg-zinc-100 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-100'
                      : 'text-zinc-500 hover:bg-zinc-50 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800/50 dark:hover:text-zinc-200',
                  )}
                >
                  <Icon className={cn('h-3.5 w-3.5 shrink-0', isActive ? 'text-[#ff006a]' : 'text-zinc-400')} />
                  <span>{t.label}</span>
                </button>
              );
            })}
          </div>

          {/* A/B-Varianten-Pillen — kompakt rechts in der Top-Bar.
              Immer sichtbar (auch bei nur Variante A), damit der "+ Variante"-Button auffindbar bleibt.
              Empfänger werden bei ≥2 Varianten gleichmäßig aufgeteilt; utm_content trägt das Label. */}
          <div className="flex shrink-0 items-center gap-1">
            <span className="hidden text-[10px] font-semibold uppercase tracking-wider text-zinc-400 sm:inline">
              {variantLabels.length > 1 ? `A/B · ${variantLabels.length}` : 'Variante'}
            </span>
            {variantLabels.map((label, idx) => {
              const isActive = idx === activeVariantIdx;
              return (
                <div key={idx} className="group relative">
                  <button
                    type="button"
                    onClick={() => onSwitchVariant(idx)}
                    className={cn(
                      'inline-flex h-6 min-w-[24px] items-center justify-center rounded px-1.5 text-[11px] font-bold uppercase transition',
                      isActive
                        ? 'bg-[#ff006a] text-white'
                        : 'bg-zinc-100 text-zinc-700 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700',
                    )}
                    title={`Variante ${label}${isActive ? ' (aktiv)' : ''}`}
                  >
                    {label}
                  </button>
                  {idx > 0 && (
                    <button
                      type="button"
                      onClick={() => onRemoveVariant(idx)}
                      className="absolute -right-1 -top-1 hidden h-3 w-3 items-center justify-center rounded-full bg-zinc-700 text-[8px] text-white hover:bg-red-600 group-hover:flex"
                      aria-label={`Variante ${label} löschen`}
                      title={`Variante ${label} entfernen`}
                    >
                      ×
                    </button>
                  )}
                </div>
              );
            })}
            <button
              type="button"
              onClick={onAddVariant}
              disabled={variantLabels.length >= 5}
              className="inline-flex h-6 items-center gap-0.5 rounded border border-dashed border-zinc-300 px-1.5 text-[10px] font-medium text-zinc-600 hover:border-[#ff006a] hover:text-[#ff006a] disabled:cursor-not-allowed disabled:opacity-40 dark:border-zinc-700 dark:text-zinc-400"
              title={variantLabels.length > 1
                ? 'Neue Variante als Kopie der aktuellen anlegen — Empfänger werden gleichmäßig aufgeteilt, utm_content trägt automatisch das Variant-Label'
                : 'Neue Variante als Kopie der aktuellen anlegen'}
            >
              + Variante
            </button>
          </div>
        </div>

        {/* Content-Bereich — min-w-0 damit lange Inhalte nicht den Container sprengen */}
        <div className="min-w-0 p-4">
        {activeTab === 'edit' && (
          // Split-Layout: Edit-Form links, optionale Live-Preview rechts (≥1280 px / xl breakpoint).
          // `livePreviewOn` (Toolbar oben) blendet die rechte Spalte aus — Edit-Form bekommt dann volle Breite.
          // `min-w-0` an beiden Spalten verhindert Flex-Overflow durch lange Inhalte.
          <div>
            {/* Toolbar: Live-Preview-Toggle. "In neuem Tab öffnen" sitzt im Header (oben rechts). */}
            <div className="mb-3 flex items-center justify-between gap-2 rounded-md border border-zinc-200 bg-zinc-50 px-3 py-1.5 text-[11px] dark:border-zinc-800 dark:bg-zinc-900/50">
              <label className="inline-flex cursor-pointer items-center gap-2 select-none">
                <input
                  type="checkbox"
                  checked={livePreviewOn}
                  onChange={(e) => setLivePreviewOn(e.target.checked)}
                  className="h-3.5 w-3.5 cursor-pointer accent-[#ff006a]"
                />
                <Eye className={cn('h-3.5 w-3.5', livePreviewOn ? 'text-[#ff006a]' : 'text-zinc-400')} />
                <span className="font-medium text-zinc-700 dark:text-zinc-300">
                  Live-Preview {livePreviewOn ? 'an' : 'aus'}
                </span>
              </label>
              {livePreviewOn && isReRendering && (
                <span className="inline-flex items-center gap-1 text-[10px] text-zinc-500">
                  <Loader2 className="h-3 w-3 animate-spin" /> rendert …
                </span>
              )}
            </div>

            {(() => {
              const editPanelEl = (
                <EditPanel
                  strategy={output.strategy}
                  onFieldUpdate={onFieldUpdate}
                  onStrategyReplace={onStrategyReplace}
                  isReRendering={isReRendering}
                  heroControls={heroControls}
                  ctaUrls={ctaUrls}
                  onCtaUrlChange={onCtaUrlChange}
                  heroHeight={heroHeight}
                  heroCropPosition={heroCropPosition}
                  onChangeHeroSetting={onChangeHeroSetting}
                  secondaryCtaEnabled={secondaryCtaEnabled}
                  onToggleSecondaryCta={onToggleSecondaryCta}
                  briefing={briefing}
                  onBriefingPatch={onBriefingPatch}
                />
              );
              // iframe-Höhe wird von previewHeight gesteuert (kommt via postMessage aus dem
              // inline-editor-script). Initial-Fallback 600px bis erste Resize-Message ankommt.
              const iframeStyle = { height: `${previewHeight ?? 600}px` };
              const previewEl = (
                <>
                  <iframe
                    title="Email-Vorschau"
                    srcDoc={previewHtml}
                    style={iframeStyle}
                    className="w-full rounded border border-zinc-200 bg-white transition-[height] duration-150 dark:border-zinc-700"
                  />
                  <div className="mt-2 shrink-0 text-right text-[11px] text-zinc-500">{(output.htmlBytes / 1024).toFixed(1)} KB HTML</div>
                </>
              );

              if (isDesktop) {
                // Desktop ≥1280 px: Resizable PanelGroup. Sizes werden via autoSaveId persistiert.
                // KEIN min-h auf der Group, KEIN h-full / overflow auf den inneren Wrappers —
                // damit die ganze Page natürlich scrollt statt nested Scroll-Container.
                return (
                  <PanelGroup
                    direction="horizontal"
                    autoSaveId="admail-edit"
                  >
                    <Panel defaultSize={28} minSize={22} maxSize={45} className="min-w-0">
                      <div className="min-w-0 pr-2">{editPanelEl}</div>
                    </Panel>
                    {livePreviewOn && (
                      <>
                        <PanelResizeHandle className="group relative mx-1 flex w-1.5 items-center justify-center">
                          <div className="h-12 w-0.5 rounded-full bg-zinc-300 transition group-hover:bg-[#ff006a] group-data-[resize-handle-state=drag]:bg-[#ff006a] dark:bg-zinc-700" />
                        </PanelResizeHandle>
                        <Panel defaultSize={72} minSize={45} className="min-w-0">
                          <div className="flex flex-col min-w-0">{previewEl}</div>
                        </Panel>
                      </>
                    )}
                  </PanelGroup>
                );
              }

              // Mobile/Tablet: Stacked-Fallback ohne Resizer (Touch-Resize ist auf kleinen Screens unsinnig).
              return (
                <div className="flex flex-col gap-4">
                  <div className="min-w-0 w-full">{editPanelEl}</div>
                  {livePreviewOn && (
                    <div className="flex min-w-0 flex-col">
                      {previewEl}
                    </div>
                  )}
                </div>
              );
            })()}
          </div>
        )}

        {activeTab === 'spam' && (
          <div className="space-y-2 text-sm">
            {output.spam.findings.length === 0 ? (
              <div className="flex items-center gap-2 rounded-md border border-emerald-200 bg-emerald-50 p-3 text-emerald-800 dark:border-emerald-900/50 dark:bg-emerald-950/30 dark:text-emerald-300">
                <CheckCircle2 className="h-4 w-4" />
                Keine Probleme gefunden.
              </div>
            ) : (
              output.spam.findings.map((f, i) => (
                <div
                  key={i}
                  className={cn(
                    'rounded-md border px-3 py-2 text-xs',
                    f.severity === 'error'
                      ? 'border-red-200 bg-red-50 dark:border-red-900/50 dark:bg-red-950/30'
                      : f.severity === 'warning'
                        ? 'border-amber-200 bg-amber-50 dark:border-amber-900/50 dark:bg-amber-950/30'
                        : 'border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900',
                  )}
                >
                  <div className="flex items-center justify-between">
                    <span className="font-mono text-[10px] uppercase tracking-wider text-zinc-500">{f.rule}</span>
                    <span className="font-mono text-[10px] text-zinc-500">+{f.points}</span>
                  </div>
                  <div className="mt-1 font-medium">{f.message}</div>
                  {f.detail && <div className="mt-0.5 text-zinc-600 dark:text-zinc-400">{f.detail}</div>}
                </div>
              ))
            )}
            <details className="mt-3">
              <summary className="cursor-pointer text-xs text-zinc-500 hover:text-zinc-700">Metrics anzeigen</summary>
              <pre className="mt-2 overflow-auto rounded-md bg-zinc-100 p-2 text-[10px] leading-tight dark:bg-zinc-900">{JSON.stringify(output.spam.metrics, null, 2)}</pre>
            </details>
          </div>
        )}

        {activeTab === 'stats' && (
          sentCampaignId
            ? <CampaignStatsPanel campaignId={sentCampaignId} />
            : <StatsEmpty />
        )}

        {activeTab === 'mjml' && (
          <SourceEditor
            mode="mjml"
            output={output}
            workspaceSlug={workspaceSlug}
            onUpdateActiveOutput={onUpdateActiveOutput}
            onResetOverrideAndRerender={onResetOverrideAndRerender}
          />
        )}

        {activeTab === 'html' && (
          <SourceEditor
            mode="html"
            output={output}
            workspaceSlug={workspaceSlug}
            onUpdateActiveOutput={onUpdateActiveOutput}
            onResetOverrideAndRerender={onResetOverrideAndRerender}
          />
        )}

        {activeTab === 'text' && (
          <pre className="max-h-[600px] overflow-auto whitespace-pre-wrap rounded-md bg-zinc-100 p-3 font-mono text-xs leading-relaxed dark:bg-zinc-900">{output.text}</pre>
        )}
        </div>
      </div>
    </section>
  );
}

/**
 * SourceEditor — manueller MJML- oder HTML-Editor im AdMail-Tab.
 *
 * Beim ersten Save wird `output.manualOverride` auf `'mjml'` bzw. `'html'` gesetzt.
 * Damit blockt `commitStrategyChange` im Parent jeden weiteren auto-Re-Render aus
 * Strategy-Edits — sonst würde der nächste TipTap-Tipp die Manual-Edits killen.
 *
 * MJML-Mode: ruft `/api/render-mjml` auf, ersetzt `mjml` UND `html` UND `text`.
 * HTML-Mode: ersetzt nur `html` (kein Server-Roundtrip — der User bearbeitet das
 * fertig kompilierte Output direkt für letzte kosmetische Anpassungen).
 */
function SourceEditor({
  mode,
  output,
  workspaceSlug,
  onUpdateActiveOutput,
  onResetOverrideAndRerender,
}: {
  mode: 'mjml' | 'html';
  output: GenerateResponse;
  workspaceSlug: string;
  onUpdateActiveOutput: (updater: (current: GenerateResponse) => GenerateResponse) => void;
  onResetOverrideAndRerender: () => void;
}) {
  const sourceFromOutput = mode === 'mjml' ? output.mjml : output.html;
  const [draft, setDraft] = useState<string>(sourceFromOutput);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Sync draft mit output, wenn Strategy → Re-Render von außen das Source neu setzt.
  // Aber nur wenn der draft noch nie editiert wurde (undirty) — sonst wird der User-Input
  // beim debounced Re-Render-Tick weggeworfen.
  useEffect(() => {
    setDraft(sourceFromOutput);
    setError(null);
    // Warning: wir reagieren absichtlich nur auf Output-Source-Änderung, nicht auf draft-Eigen-Edits.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [sourceFromOutput]);

  const isOverridden = output.manualOverride === mode;
  const otherOverride = output.manualOverride && output.manualOverride !== mode;
  const dirty = draft !== sourceFromOutput;

  async function handleSave() {
    setBusy(true);
    setError(null);
    try {
      if (mode === 'mjml') {
        const res = await fetch('/api/render-mjml', {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({
            mjml: draft,
            workspaceSlug,
            subject: output.strategy.subject_variants[0] ?? '(ohne Betreff)',
            preheader: output.strategy.preheader,
          }),
        });
        const data = await res.json();
        if (!res.ok || !data.ok) {
          setError(data.error ?? `HTTP ${res.status}`);
          return;
        }
        onUpdateActiveOutput((current) => ({
          ...current,
          mjml: data.mjml,
          html: data.html,
          text: data.text,
          htmlBytes: data.htmlBytes,
          mjmlErrors: data.mjmlErrors ?? [],
          spam: data.spam ?? current.spam,
          manualOverride: 'mjml',
        }));
      } else {
        // HTML-Mode: kein Server-Call — direkt im Frontend ersetzen.
        const bytes = new TextEncoder().encode(draft).length;
        onUpdateActiveOutput((current) => ({
          ...current,
          html: draft,
          htmlBytes: bytes,
          manualOverride: 'html',
        }));
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Render-Fehler');
    } finally {
      setBusy(false);
    }
  }

  function handleTabIndent(e: React.KeyboardEvent<HTMLTextAreaElement>) {
    if (e.key !== 'Tab') return;
    e.preventDefault();
    const ta = e.currentTarget;
    const start = ta.selectionStart;
    const end = ta.selectionEnd;
    const next = draft.slice(0, start) + '  ' + draft.slice(end);
    setDraft(next);
    // Cursor nach den eingefügten Spaces zurücksetzen
    requestAnimationFrame(() => {
      ta.selectionStart = ta.selectionEnd = start + 2;
    });
  }

  return (
    <div className="space-y-2">
      {isOverridden && (
        <div className="flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900 dark:border-amber-900/50 dark:bg-amber-950/30 dark:text-amber-200">
          <Lock className="mt-0.5 h-3.5 w-3.5 shrink-0" />
          <div className="flex-1">
            <strong>Manueller {mode.toUpperCase()}-Mode aktiv.</strong> Edits in „Bearbeiten" und „Strategy"
            werden NICHT mehr in die Preview übernommen — sie würden deinen manuellen Quellcode überschreiben.
          </div>
          <button
            type="button"
            onClick={onResetOverrideAndRerender}
            className="shrink-0 inline-flex items-center gap-1 rounded border border-amber-300 bg-white px-2 py-0.5 font-medium text-amber-900 hover:bg-amber-100 dark:border-amber-800 dark:bg-amber-900/40 dark:text-amber-100"
            title="Manuellen Override verwerfen und Preview frisch aus Strategy rendern"
          >
            <Unlock className="h-3 w-3" />
            Auf Strategy zurücksetzen
          </button>
        </div>
      )}
      {otherOverride && (
        <div className="rounded-md border border-zinc-200 bg-zinc-50 px-3 py-2 text-xs text-zinc-600 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400">
          Hinweis: Der {output.manualOverride === 'mjml' ? 'MJML' : 'HTML'}-Editor hält gerade die
          Manual-Override-Sperre. Edits hier werden nur lokal angezeigt — beim Speichern übernimmt
          dieser Tab die Sperre.
        </div>
      )}

      <div className="flex items-center justify-between gap-2">
        <div className="text-[11px] text-zinc-500">
          {mode === 'mjml'
            ? 'MJML wird beim Speichern serverseitig zu HTML kompiliert.'
            : 'HTML wird ohne weitere Verarbeitung in die Preview übernommen.'}
          {dirty && <span className="ml-2 text-amber-600 dark:text-amber-400">• ungespeicherte Änderungen</span>}
        </div>
        <div className="flex items-center gap-2">
          <button
            type="button"
            disabled={!dirty || busy}
            onClick={() => setDraft(sourceFromOutput)}
            className="rounded-md border border-zinc-200 bg-white px-2.5 py-1 text-xs font-medium text-zinc-700 transition hover:border-zinc-300 disabled:cursor-not-allowed disabled:opacity-40 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300"
          >
            Verwerfen
          </button>
          <button
            type="button"
            disabled={!dirty || busy}
            onClick={handleSave}
            className="inline-flex items-center gap-1.5 rounded-md bg-[#ff006a] px-3 py-1 text-xs font-semibold text-white transition hover:bg-[#e60060] disabled:cursor-not-allowed disabled:opacity-40"
          >
            {busy ? <Loader2 className="h-3 w-3 animate-spin" /> : <Save className="h-3 w-3" />}
            {mode === 'mjml' ? 'Rendern & Speichern' : 'Speichern'}
          </button>
        </div>
      </div>

      <textarea
        value={draft}
        onChange={(e) => setDraft(e.target.value)}
        onKeyDown={handleTabIndent}
        spellCheck={false}
        className="block h-[600px] w-full resize-y rounded-md border border-zinc-200 bg-zinc-50 p-3 font-mono text-[11px] leading-tight text-zinc-900 outline-none focus:border-[#ff006a] focus:ring-1 focus:ring-[#ff006a]/30 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
      />

      <div className="text-right text-[11px] text-zinc-500">
        {draft.length.toLocaleString('de-DE')} Zeichen ·{' '}
        {(new TextEncoder().encode(draft).length / 1024).toFixed(1)} KB
      </div>

      {error && (
        <div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-300">
          {error}
        </div>
      )}

      {mode === 'mjml' && output.mjmlErrors.length > 0 && (
        <details className="rounded-md border border-zinc-200 bg-zinc-50 px-3 py-2 text-xs dark:border-zinc-800 dark:bg-zinc-900">
          <summary className="cursor-pointer font-medium text-zinc-700 dark:text-zinc-300">
            MJML-Validierung: {output.mjmlErrors.length} Hinweis{output.mjmlErrors.length === 1 ? '' : 'e'}
          </summary>
          <ul className="mt-2 space-y-1 text-zinc-600 dark:text-zinc-400">
            {output.mjmlErrors.map((e, i) => (
              <li key={i} className="font-mono text-[10px]">
                Zeile {e.line}: {e.message}
                {e.tagName ? ` (${e.tagName})` : ''}
              </li>
            ))}
          </ul>
        </details>
      )}
    </div>
  );
}

/**
 * Controlled EditPanel — `strategy` aus Props ist Single-Source-of-Truth.
 * Jede Eingabe emittiert sofort `onFieldUpdate` (Field-Replace) bzw. `onStrategyReplace`
 * (Add/Remove Benefit), der Parent macht Optimistic-Update + debounced /api/render.
 *
 * Kein lokaler `s`-State mehr — damit ist EditPanel automatisch synchron mit
 * Inline-Edits aus dem Preview-iframe (beide Pfade gehen durch denselben Reducer).
 */
function EditPanel({
  strategy,
  onFieldUpdate,
  onStrategyReplace,
  isReRendering,
  heroControls,
  ctaUrls,
  onCtaUrlChange,
  heroHeight,
  heroCropPosition,
  onChangeHeroSetting,
  secondaryCtaEnabled,
  onToggleSecondaryCta,
  briefing,
  onBriefingPatch,
}: {
  strategy: EditableStrategy;
  onFieldUpdate: (key: string, index: number | undefined, value: string) => void;
  onStrategyReplace: (s: EditableStrategy) => void;
  isReRendering: boolean;
  heroControls?: HeroControls;
  ctaUrls: { primary: string; secondary?: string };
  onCtaUrlChange: (field: 'primary' | 'secondary', value: string) => void;
  /** Aktuelle Hero-Höhe aus dem Briefing (kontrolliert) — undefined = Auto. */
  heroHeight: number | undefined;
  /** Aktuelle Crop-Position aus dem Briefing — undefined fällt auf 'center' im Render. */
  heroCropPosition: 'top' | 'center' | 'bottom' | undefined;
  /** Update Briefing.heroHeight und/oder Briefing.heroCropPosition + debounced Re-Render. */
  onChangeHeroSetting: (next: { heroHeight?: number | undefined; heroCropPosition?: 'top' | 'center' | 'bottom' | undefined }) => void;
  /** Spiegelt briefing.optional.secondaryCta — Source-of-Truth im Briefing-Objekt, beide Tabs synchron. */
  secondaryCtaEnabled: boolean;
  onToggleSecondaryCta: (enabled: boolean) => void;
  /** Briefing als Kontext für die ExtraSections (Workspace, Goal, Segment, Texte zum KI-Suggest). */
  briefing: Briefing;
  /** Partial-Update aufs Briefing — wird für extraSections + optional.extraSections-Toggle genutzt. */
  onBriefingPatch: (patch: Partial<Briefing>) => void;
}) {
  const updateField = (k: string, v: string | undefined) => onFieldUpdate(k, undefined, v ?? '');
  const updateSubject = (i: number, v: string) => onFieldUpdate('subject_variants', i, v);

  // Benefits-Manipulation läuft inline in der Preview (postMessage → inline-action-Branch
  // in page.tsx). Die Helper hier sind entfallen, weil keine Sidebar-Bullets mehr existieren.

  return (
    <div className="space-y-5 text-sm min-w-0">
      <div className="flex items-center justify-between rounded-md border border-pink-200 bg-pink-50 px-3 py-2 text-xs dark:border-pink-900/50 dark:bg-pink-950/30">
        <span className="text-[#ff006a] dark:text-pink-200">
          Änderungen werden <strong>automatisch gerendert</strong> (~500 ms nach letztem Tippstopp). Live-Preview rechts.
        </span>
        {isReRendering && (
          <span className="inline-flex items-center gap-1 text-[10px] text-pink-700 dark:text-pink-300">
            <Loader2 className="h-3.5 w-3.5 animate-spin" /> rendert …
          </span>
        )}
      </div>

      <EditGroup title="Subject & Preheader">
        {/* Nur das aktive Subject (subject_variants[0]) editieren — A/B-Tests laufen jetzt
            via Variant-System in der Sidebar. Schema bleibt Array für Backwards-Compat. */}
        <EditField label="Subject">
          <EditInput value={strategy.subject_variants[0] ?? ''} onChange={(v) => updateSubject(0, v)} />
          <EditCharCount value={strategy.subject_variants[0] ?? ''} max={50} />
        </EditField>
        <EditField label="Preheader">
          <EditInput value={strategy.preheader} onChange={(v) => updateField('preheader', v)} />
          <EditCharCount value={strategy.preheader} max={110} />
        </EditField>
      </EditGroup>

      {heroControls && (
        <EditGroup title="Hero-Bild">
          <HeroImageControl
            current={heroControls.current}
            isBusy={heroControls.isBusy}
            isReRendering={isReRendering}
            imageBrief={strategy.image_brief}
            onChangeImageBrief={(v) => updateField('image_brief', v)}
            onDelete={() => heroControls.onDelete(strategy)}
            onRegenerate={(brief) => heroControls.onRegenerate(strategy, brief)}
            onUpload={(file) => heroControls.onUpload(strategy, file)}
            heroHeight={heroHeight}
            heroCropPosition={heroCropPosition}
            onChangeHeroSetting={onChangeHeroSetting}
          />
        </EditGroup>
      )}

      {/*
        Hero, Body und Benefits werden komplett inline in der Preview-iframe editiert.
        Klick auf einen Textblock im Preview → contenteditable → Enter speichert,
        Hover auf einen Bullet zeigt ✖, Klick auf "+ Vorteil hinzufügen" am Listenende.
        Die Strategy-Felder hero_headline, intro, pain_point, solution, benefits[]
        und ps_line haben dafür `data-edit-key`-Wrapper im render-template.

        Zusätzliche Sections (eigene MJML/HTML-Blöcke) werden ausschließlich im
        Briefing-Tab (CustomSectionEditor) verwaltet — nicht mehr im Edit-Panel.
      */}

      <EditGroup
        title="CTA"
        headerSlot={
          <span className="flex items-center gap-2 text-[10px] font-normal normal-case tracking-normal text-zinc-500 dark:text-zinc-400">
            <span>Sekundärer CTA</span>
            <ToggleSwitch
              checked={secondaryCtaEnabled}
              onChange={onToggleSecondaryCta}
              ariaLabel="Sekundären CTA anzeigen"
            />
          </span>
        }
      >
        {/* CTA-Button-Text wird inline in der Preview editiert (data-edit-key="cta_primary_text").
            Hier nur die URL — sie ist nicht im Body sichtbar, gehört in die Sidebar. */}
        <EditField label="Primärer Button-Link (URL)">
          {/* type="text" statt "url" damit der Browser-URL-Validator den Platzhalter
              {{LOGIN_LINK}} nicht als invalid markiert. Server-side wird der Tag im
              Worker zur echten workspace-spezifischen Login-URL pro Empfänger expandiert. */}
          <EditInput
            value={ctaUrls.primary}
            onChange={(v) => onCtaUrlChange('primary', v)}
            type="text"
            placeholder="https://…  oder  {{LOGIN_LINK}}"
          />
          <LoginLinkHint
            currentValue={ctaUrls.primary}
            onApply={() => onCtaUrlChange('primary', '{{LOGIN_LINK}}')}
          />
        </EditField>
        {/* Sekundärer CTA — nur sichtbar wenn Toggle aktiv. Daten in strategy.cta_secondary_text
            und briefing.ctaSecondaryUrl bleiben erhalten (User-Wunsch: "beibehalten, nur ausblenden");
            der Render-Call (siehe Z. ~544) übergibt sie als undefined wenn Toggle OFF, damit das
            MJML-Template (render-template.ts Z. 91) den Sekundärlink nicht rendert. */}
        {secondaryCtaEnabled && (
          <>
            {/* Sekundärer Link-Text wird inline editiert (data-edit-key="cta_secondary_text"). */}
            <EditField label="Sekundärer Link (URL, optional)">
              <EditInput
                value={ctaUrls.secondary ?? ''}
                onChange={(v) => onCtaUrlChange('secondary', v)}
                type="text"
                placeholder="https://…  oder  {{LOGIN_LINK}}"
              />
              <LoginLinkHint
                currentValue={ctaUrls.secondary ?? ''}
                onApply={() => onCtaUrlChange('secondary', '{{LOGIN_LINK}}')}
              />
            </EditField>
          </>
        )}
      </EditGroup>

    </div>
  );
}

/**
 * Stats-Empty-State — wird gezeigt solange noch keine Campaign versendet ist
 * (sentCampaignId === null). Sobald versendet wurde, übernimmt CampaignStatsPanel.
 */
function StatsEmpty() {
  return (
    <div className="flex flex-col items-center justify-center gap-2 rounded-md border border-dashed border-zinc-300 bg-zinc-50 p-8 text-center text-xs text-zinc-500 dark:border-zinc-700 dark:bg-zinc-900">
      <BarChart3 className="h-8 w-8 text-zinc-300" />
      <div className="font-medium text-zinc-700 dark:text-zinc-300">Noch keine Daten</div>
      <p className="max-w-md leading-relaxed">
        Stats erscheinen sobald du diese Email versendet hast und Empfänger sie öffnen.
        Open-Tracking via Pixel, Click-Tracking via Redirect, Conversions via URL-Parameter
        oder API-Polling — alles automatisch beim Send aktiv.
      </p>
    </div>
  );
}

// Legacy StatsPanel + StatTile entfernt — ersetzt durch CampaignStatsPanel
// (src/components/stats/CampaignStatsPanel.tsx) mit Top-Links + Time-Series + Unique-Counts.
function EditGroup({
  title,
  headerSlot,
  children,
}: {
  title: string;
  /** Optional: rechtsbündiges Header-Element (z.B. Toggle für sekundären CTA). */
  headerSlot?: React.ReactNode;
  children: React.ReactNode;
}) {
  return (
    <fieldset className="space-y-3 rounded-lg border border-zinc-200 p-4 dark:border-zinc-800">
      {headerSlot ? (
        <div className="-mt-2 flex items-center justify-between">
          <legend className="px-1 text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{title}</legend>
          {headerSlot}
        </div>
      ) : (
        <legend className="px-1 text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{title}</legend>
      )}
      {children}
    </fieldset>
  );
}

function EditField({ label, children }: { label: string; children: React.ReactNode }) {
  return (
    <div className="flex flex-col gap-1">
      <label className="text-[11px] font-medium text-zinc-600 dark:text-zinc-400">{label}</label>
      <div className="flex flex-col gap-1">{children}</div>
    </div>
  );
}

/**
 * Login-Link-Quick-Action für CTA-URL-Felder im AdMail/Strategy-Editor.
 *
 * Zeigt einen kompakten grünen Hinweis-Block UNTER dem URL-Input mit:
 *  - Kurzer Erklärung was Magic-Link / Login-Direktlink ist
 *  - Toggle-Button: setzt {{LOGIN_LINK}} ins Feld (oder zeigt "aktiv" wenn schon gesetzt)
 *  - Voraussetzungs-Hinweis (Workspace-Shop-Domain, Cumas-ID, Pool-Typ)
 *
 * Symmetrisch zur Variante im BriefingPanel — gleiche Optik, damit User die Komponente
 * sofort wiedererkennt. Worker substituiert {{LOGIN_LINK}} pro Empfänger zur echten URL
 * (siehe src/worker/index.ts SYSTEM_TAGS und src/lib/crm/kompass-link.ts).
 */
function LoginLinkHint({ currentValue, onApply }: { currentValue: string; onApply: () => void }) {
  const isActive = currentValue === '{{LOGIN_LINK}}';
  return (
    <div className="mt-1 flex min-w-0 items-start gap-2 rounded-md border border-emerald-200 bg-emerald-50/60 px-2.5 py-2 dark:border-emerald-900/40 dark:bg-emerald-950/20">
      <div className="mt-0.5 h-1.5 w-1.5 shrink-0 rounded-full bg-emerald-500" aria-hidden />
      <div className="min-w-0 flex-1">
        <p className="break-words text-[11px] leading-relaxed text-emerald-900 dark:text-emerald-200">
          <strong>Login-Direktlink (Magic-Link):</strong> Pro Empfänger wird automatisch ein verschlüsselter Link in den Workspace-Shop gebaut — basierend auf der Cumas-User-ID. Loggt den Empfänger direkt in seinen Account ein.
        </p>
        <button
          type="button"
          onClick={onApply}
          disabled={isActive}
          className={cn(
            'mt-1.5 inline-flex items-center gap-1 rounded px-2 py-1 text-[11px] font-medium transition',
            isActive
              ? 'cursor-default bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300'
              : 'bg-emerald-600 text-white hover:bg-emerald-700 dark:bg-emerald-700 dark:hover:bg-emerald-600',
          )}
        >
          {isActive ? '✓ Login-Link aktiv' : '+ Login-Link als CTA einsetzen'}
        </button>
        <p className="mt-1 break-words text-[10px] leading-relaxed text-emerald-800/80 dark:text-emerald-300/70">
          Voraussetzung: Workspace hat eine Shop-Domain (Settings → Workspaces), Empfänger haben eine Cumas-ID, der Pool-Typ ist korrekt (Privat/Geschäft).
        </p>
      </div>
    </div>
  );
}

function EditInput({
  value,
  onChange,
  type = 'text',
  placeholder,
}: {
  value: string;
  onChange: (v: string) => void;
  type?: 'text' | 'url' | 'email';
  placeholder?: string;
}) {
  return (
    <input
      value={value}
      onChange={(e) => onChange(e.target.value)}
      type={type}
      placeholder={placeholder}
      className="w-full rounded-md border border-zinc-200 bg-white px-2.5 py-1.5 text-sm transition focus:border-zinc-900 focus:outline-none focus:ring-2 focus:ring-zinc-900/10 dark:border-zinc-700 dark:bg-zinc-900"
    />
  );
}

function EditTextarea({ value, onChange, rows = 3 }: { value: string; onChange: (v: string) => void; rows?: number }) {
  return (
    <textarea
      value={value}
      onChange={(e) => onChange(e.target.value)}
      rows={rows}
      className="w-full resize-y rounded-md border border-zinc-200 bg-white px-2.5 py-1.5 text-sm transition focus:border-zinc-900 focus:outline-none focus:ring-2 focus:ring-zinc-900/10 dark:border-zinc-700 dark:bg-zinc-900"
    />
  );
}

function EditCharCount({ value, max }: { value: string; max: number }) {
  const len = value.length;
  const over = len > max;
  return (
    <div className={cn('text-right text-[10px]', over ? 'text-red-500' : 'text-zinc-400')}>
      {len} / {max} Zeichen{over && ' — über Limit'}
    </div>
  );
}

/**
 * Hero-Bild-Steuerung im Bearbeiten-Tab. 3 Aktionen + Image-Brief-Editor:
 *  • Entfernen → komplett ohne Hero rendern
 *  • Neu generieren → fal.ai mit (potenziell modifiziertem) image_brief, forceRegenerate
 *  • Hochladen → File-Upload, ersetzt Hero durch Custom-Image
 *
 * Visualisierung: Thumbnail des aktuellen Bildes (gross genug zum Erkennen, aber kompakt) +
 * 3 Action-Buttons in einer Reihe + Image-Brief-Textarea darunter.
 */
function HeroImageControl({
  current,
  isBusy,
  isReRendering,
  imageBrief,
  onChangeImageBrief,
  onDelete,
  onRegenerate,
  onUpload,
  heroHeight,
  heroCropPosition,
  onChangeHeroSetting,
}: {
  current: GenerateResponse['hero'];
  isBusy: boolean;
  isReRendering: boolean;
  imageBrief: string;
  onChangeImageBrief: (v: string) => void;
  onDelete: () => void;
  onRegenerate: (brief: string) => Promise<void>;
  onUpload: (file: File) => Promise<void>;
  /** Aktuelle Bild-Höhe in px für Crop. undefined = Auto (natürliche Höhe). */
  heroHeight: number | undefined;
  /** Crop-Position. undefined → 'center' im Render-Pfad. */
  heroCropPosition: 'top' | 'center' | 'bottom' | undefined;
  /** Setzt Höhe oder Position auf Briefing-Level + triggert debounced Re-Render. */
  onChangeHeroSetting: (next: { heroHeight?: number | undefined; heroCropPosition?: 'top' | 'center' | 'bottom' | undefined }) => void;
}) {
  const fileInputRef = useRef<HTMLInputElement | null>(null);
  const busy = isBusy || isReRendering;

  const handleFileChosen = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;
    await onUpload(file);
    // Reset, damit das selbe File erneut gewählt werden kann
    if (fileInputRef.current) fileInputRef.current.value = '';
  };

  return (
    <div className="space-y-3">
      {/* Thumbnail + Actions side-by-side */}
      <div className="flex items-start gap-3">
        <div className="h-24 w-36 shrink-0 overflow-hidden rounded border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
          {current?.absoluteUrl ? (
            // eslint-disable-next-line @next/next/no-img-element
            <img
              src={current.absoluteUrl}
              alt="Hero-Vorschau"
              className="h-full w-full object-cover"
            />
          ) : (
            <div className="flex h-full w-full flex-col items-center justify-center gap-1 text-[10px] text-zinc-400">
              <ImageIcon className="h-5 w-5" />
              Kein Bild
            </div>
          )}
        </div>

        <div className="flex min-w-0 flex-1 flex-col gap-1.5">
          <button
            type="button"
            onClick={() => onRegenerate(imageBrief)}
            disabled={busy || !imageBrief?.trim()}
            className="inline-flex items-center justify-center gap-1.5 rounded-md bg-[#ff006a] px-2.5 py-1.5 text-xs font-medium text-white hover:bg-[#e60060] disabled:cursor-not-allowed disabled:opacity-40"
            title="Bild mit aktuellem Image-Brief neu generieren (fal.ai)"
          >
            {busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Wand2 className="h-3.5 w-3.5" />}
            {isBusy ? 'Generiere …' : 'Mit KI neu generieren'}
          </button>
          <button
            type="button"
            onClick={() => fileInputRef.current?.click()}
            disabled={busy}
            className="inline-flex items-center justify-center gap-1.5 rounded-md border border-zinc-300 bg-white px-2.5 py-1.5 text-xs font-medium text-zinc-700 hover:bg-zinc-50 disabled:cursor-not-allowed disabled:opacity-40 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300"
          >
            <Upload className="h-3.5 w-3.5" />
            Eigenes Bild hochladen
          </button>
          <input
            ref={fileInputRef}
            type="file"
            accept="image/png,image/jpeg,image/webp,image/gif"
            onChange={handleFileChosen}
            className="hidden"
          />
          {current && (
            <button
              type="button"
              onClick={onDelete}
              disabled={busy}
              className="inline-flex items-center justify-center gap-1.5 rounded-md border border-red-200 bg-red-50/60 px-2.5 py-1.5 text-xs font-medium text-red-700 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-40 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-300"
            >
              <Trash2 className="h-3.5 w-3.5" />
              Bild entfernen
            </button>
          )}
        </div>
      </div>

      {/* Image-Brief — der Prompt der bei „Neu generieren" verwendet wird */}
      <EditField label="Image-Brief (Prompt für KI-Bild-Generierung)">
        <EditTextarea
          value={imageBrief ?? ''}
          onChange={onChangeImageBrief}
          rows={3}
        />
        <p className="mt-1 text-[10px] leading-relaxed text-zinc-500">
          Beschreibe die Szene, das Licht, den Stil. „Neu generieren" verwendet diesen Text als Prompt.
        </p>
      </EditField>

      {/* Bild-Höhe + Crop-Position. Identische Semantik zum HeroCropControl im Briefing-Tab,
          aber inline-kompakt damit's neben dem Brief passt. Beide schreiben in dieselben
          briefing.heroHeight + briefing.heroCropPosition Felder via onChangeHeroSetting.
          Auto-Re-Render via 500ms Debounce nach jedem Klick — kein separater Button nötig. */}
      <EditField label="Bild-Höhe & Crop">
        <div className="flex flex-wrap items-center gap-2">
          <input
            type="number"
            min={80}
            max={800}
            step={10}
            value={heroHeight ?? ''}
            onChange={(e) => {
              const raw = e.target.value;
              const n = raw === '' ? undefined : Number(raw);
              const next = n && n > 0 ? n : undefined;
              onChangeHeroSetting({ heroHeight: next });
            }}
            placeholder="Auto"
            className="w-20 rounded 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"
          />
          <span className="text-[10px] text-zinc-400">px</span>
          <div className="ml-auto flex items-center gap-1">
            <span className="mr-1 text-[10px] font-semibold uppercase tracking-wider text-zinc-500">Crop</span>
            {(['top', 'center', 'bottom'] as const).map((pos) => {
              // Wenn keine Höhe gesetzt ist, hat object-position keinen Effekt → Buttons disabled.
              const cropDisabled = heroHeight === undefined;
              const isActive = !cropDisabled && (heroCropPosition ?? 'center') === pos;
              const label = pos === 'top' ? 'Oben' : pos === 'center' ? 'Mitte' : 'Unten';
              const hint = cropDisabled
                ? 'Erst eine Bild-Höhe setzen, dann Crop-Position wählen'
                : pos === 'top'
                  ? 'object-position: top — oberer Bildteil sichtbar'
                  : pos === 'center'
                    ? 'object-position: center — mittig (Default)'
                    : 'object-position: bottom — unterer Bildteil sichtbar';
              return (
                <button
                  key={pos}
                  type="button"
                  onClick={() => onChangeHeroSetting({ heroCropPosition: pos })}
                  disabled={cropDisabled}
                  title={hint}
                  className={cn(
                    'rounded px-2 py-0.5 text-[11px] font-medium transition',
                    isActive
                      ? 'bg-[#ff006a] text-white'
                      : 'bg-zinc-100 text-zinc-700 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700',
                    cropDisabled && 'cursor-not-allowed opacity-40 hover:bg-zinc-100 dark:hover:bg-zinc-800',
                  )}
                >
                  {label}
                </button>
              );
            })}
          </div>
        </div>
        <p className="mt-1 text-[10px] leading-relaxed text-zinc-500">
          Beschneidet das Bild auf die angegebene Höhe (object-fit: cover). Outlook fällt auf Stauchung zurück.
        </p>
      </EditField>
    </div>
  );
}

/**
 * TopBar — entrümpelt nach progressive-disclosure-Pattern:
 *
 *  Immer sichtbar (Status, keine Action):
 *    - Draft-Name + Auto-Save-Indicator (links)
 *
 *  Sichtbar wenn Output existiert (Versand-Phase):
 *    - Sender-Picker (kompakt, nur wenn Sender im Pool)
 *    - Test-Versand
 *    - Versand starten
 *
 *  Im "⋯ Mehr"-Menü (selten gebraucht):
 *    - Snapshot erstellen
 *    - Als Vorlage speichern
 *    - Einstellungen
 *
 *  Begründung: Vorher waren 6+ Buttons + Selector ständig sichtbar. Die meisten
 *  davon sind situational — Snapshot/Vorlage erst nach Inhalt sinnvoll, Settings
 *  selten, Sender + Versand erst nach Generate. Disabled-but-visible-Buttons sind
 *  UX-Schmerz: User sieht "geht aber gerade nicht" — Frustrationsquelle.
 */
/**
 * Einreihiger Editor-Header: Draft-Name (8-char truncate + Tooltip) ◦ Inline-Tabs ◦ Kebab-Menü.
 *
 * Send-Buttons (Test-Versand, Versand) und Sender-Picker leben **nicht** mehr hier — sie
 * wandern in den `SchedulerFooter` (Tab 5, der einzige Ort an dem sie gebraucht werden).
 * Der AutoSave-Indicator zieht in die Footer der jeweiligen Tabs (zentriert).
 *
 * Padding `py-1.5` reduziert die Header-Höhe von ~82 px (zweireihig) auf ~40 px.
 */
function EditorHeader({
  draftName,
  onRenameDraft,
  nameMissing = false,
  nameInferred = false,
  onSave,
  onSaveAsTemplate,
  onPublishTrigger,
  onOpenSettings,
  pageView,
  onChangePageView,
  hasOutput,
  isGenerating,
  recipientCount,
}: {
  draftName: string | null;
  /** Inline-Edit-Commit: leerer String erlaubt — Parent darf den User notfalls auf den Footer-Hint hinweisen. */
  onRenameDraft: (next: string) => void;
  /** Wenn true: campaignName ist Pflicht aber leer → Header visuell hervorheben (orangener Border). */
  nameMissing?: boolean;
  /** Wenn true: KI hat den Namen vorgeschlagen → Pink-Glow am Header. */
  nameInferred?: boolean;
  onSave: () => void;
  onSaveAsTemplate: () => void;
  onPublishTrigger: () => void;
  onOpenSettings: () => void;
  pageView: PageView;
  onChangePageView: (v: PageView) => void;
  hasOutput: boolean;
  isGenerating: boolean;
  recipientCount: number;
}) {
  const router = useRouter();
  const [moreOpen, setMoreOpen] = useState(false);
  const moreMenuRef = useRef<HTMLDivElement | null>(null);
  // Inline-Edit-State für den Draft-Namen. `editValue` startet mit dem aktuellen Namen
  // beim Klick auf h1, Enter/Blur committed, Esc revertiert (= setIsEditing(false) ohne commit).
  const [isEditing, setIsEditing] = useState(false);
  const [editValue, setEditValue] = useState('');
  const inputRef = useRef<HTMLInputElement | null>(null);

  useEffect(() => {
    if (isEditing && inputRef.current) {
      inputRef.current.focus();
      inputRef.current.select();
    }
  }, [isEditing]);

  useEffect(() => {
    if (!moreOpen) return;
    const handler = (e: MouseEvent) => {
      if (moreMenuRef.current && !moreMenuRef.current.contains(e.target as Node)) {
        setMoreOpen(false);
      }
    };
    const escHandler = (e: KeyboardEvent) => {
      if (e.key === 'Escape') setMoreOpen(false);
    };
    document.addEventListener('mousedown', handler);
    document.addEventListener('keydown', escHandler);
    return () => {
      document.removeEventListener('mousedown', handler);
      document.removeEventListener('keydown', escHandler);
    };
  }, [moreOpen]);

  const trimmed = draftName?.trim() ?? '';
  const isPlaceholder = trimmed.length === 0;
  const fullName = isPlaceholder ? 'Neuer Entwurf' : trimmed;
  const truncated = fullName.length > 18 ? `${fullName.slice(0, 18)}…` : fullName;

  const commitEdit = () => {
    const next = editValue.trim();
    // Leer-Commit erlaubt — Footer-Hint führt den User dann zur Pflicht-Eingabe.
    // Nur wenn unverändert: kein Update, sonst feuert Auto-Save unnötig.
    if (next !== trimmed) onRenameDraft(next);
    setIsEditing(false);
  };

  return (
    <header className="flex items-center gap-3 border-b border-zinc-200 bg-white px-4 py-1.5 dark:border-zinc-800 dark:bg-zinc-950">
      {/* LEFT: Draft-Name — Inline-editable. Klick → input, Enter/Blur → commit, Esc → revert. */}
      {isEditing ? (
        <input
          ref={inputRef}
          type="text"
          value={editValue}
          onChange={(e) => setEditValue(e.target.value)}
          onBlur={commitEdit}
          onKeyDown={(e) => {
            if (e.key === 'Enter') {
              e.preventDefault();
              commitEdit();
            } else if (e.key === 'Escape') {
              e.preventDefault();
              setIsEditing(false);
            }
          }}
          placeholder="Kampagnen-Name (z.B. Q2-Reaktivierung)"
          className={cn(
            'shrink-0 rounded border bg-white px-1.5 py-0.5 text-sm font-semibold leading-tight text-zinc-900 outline-none transition focus:border-[#ff006a] focus:ring-2 focus:ring-[#ff006a]/20 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100',
            'min-w-[200px]',
          )}
        />
      ) : (
        <h1
          title={isPlaceholder ? 'Klick zum Umbenennen' : `${fullName} — klick zum Umbenennen`}
          onClick={() => {
            setEditValue(isPlaceholder ? '' : trimmed);
            setIsEditing(true);
          }}
          className={cn(
            'shrink-0 cursor-text rounded px-1.5 py-0.5 text-sm font-semibold leading-tight transition',
            'hover:bg-zinc-100 dark:hover:bg-zinc-800',
            isPlaceholder
              ? 'italic text-zinc-400 dark:text-zinc-500'
              : 'text-zinc-900 dark:text-zinc-100',
            nameMissing && 'animate-pulse ring-2 ring-amber-400 ring-offset-1 dark:ring-amber-500',
            nameInferred && !nameMissing && 'ring-1 ring-[#ff006a]/40',
          )}
        >
          {truncated}
        </h1>
      )}

      {/* MIDDLE: Tabs inline */}
      <nav className="flex flex-1 items-center justify-center gap-0">
        <PageTab
          active={pageView === 'editor'}
          onClick={() => onChangePageView('editor')}
          icon={<FileText className="h-4 w-4" />}
          label="1. Email & Editor"
        />
        <ChevronRight className="h-3.5 w-3.5 shrink-0 text-zinc-300 dark:text-zinc-700" />
        <PageTab
          active={pageView === 'briefing'}
          onClick={() => onChangePageView('briefing')}
          icon={<Target className="h-4 w-4" />}
          label="2. Briefing & Goal"
        />
        <ChevronRight className="h-3.5 w-3.5 shrink-0 text-zinc-300 dark:text-zinc-700" />
        <PageTab
          active={pageView === 'output'}
          onClick={() => onChangePageView('output')}
          icon={isGenerating ? <Loader2 className="h-4 w-4 animate-spin" /> : <MailOpen className="h-4 w-4" />}
          label={isGenerating ? '3. Wird generiert …' : '3. AdMail'}
          muted={!hasOutput && !isGenerating}
          badge={hasOutput && !isGenerating ? 'bereit' : undefined}
        />
        <ChevronRight className="h-3.5 w-3.5 shrink-0 text-zinc-300 dark:text-zinc-700" />
        <PageTab
          active={pageView === 'audience'}
          onClick={() => onChangePageView('audience')}
          icon={<UserPlus className="h-4 w-4" />}
          label="4. Zielgruppe"
          badge={recipientCount > 0 ? `${recipientCount.toLocaleString('de-DE')}` : undefined}
        />
        <ChevronRight className="h-3.5 w-3.5 shrink-0 text-zinc-300 dark:text-zinc-700" />
        <PageTab
          active={pageView === 'scheduler'}
          onClick={() => onChangePageView('scheduler')}
          icon={<CalendarClock className="h-4 w-4" />}
          label="5. Versand"
        />
        {/* Separator + Cross-Campaign-Dashboard (kein Pipeline-Schritt, daher visuell abgetrennt) */}
        <span className="mx-3 h-5 w-px bg-zinc-200 dark:bg-zinc-800" aria-hidden="true" />
        <PageTab
          active={pageView === 'dashboard'}
          onClick={() => onChangePageView('dashboard')}
          icon={<BarChart3 className="h-4 w-4" />}
          label="Dashboard"
        />
      </nav>

      {/* RIGHT: ⋯-Mehr-Menü (Snapshot, Vorlage, Einstellungen) */}
      <div className="relative shrink-0" ref={moreMenuRef}>
        <button
          type="button"
          onClick={() => setMoreOpen((v) => !v)}
          aria-haspopup="menu"
          aria-expanded={moreOpen}
          title="Mehr Optionen"
          className={cn(
            'flex h-8 w-8 items-center justify-center rounded-md border transition-colors',
            moreOpen
              ? 'border-zinc-300 bg-zinc-100 text-zinc-900 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-100'
              : 'border-zinc-200 text-zinc-500 hover:bg-zinc-50 hover:text-zinc-900 dark:border-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-900 dark:hover:text-zinc-100',
          )}
        >
          <MoreHorizontal className="h-4 w-4" />
        </button>
        {moreOpen && (
          <div role="menu" className="absolute right-0 top-full z-40 mt-1 w-56 overflow-hidden rounded-md border border-zinc-200 bg-white shadow-lg dark:border-zinc-700 dark:bg-zinc-900">
            <MoreMenuItem
              icon={<Save className="h-3.5 w-3.5" />}
              label="Snapshot erstellen"
              hint="Manuelle Version mit Zeitstempel"
              onClick={() => { onSave(); setMoreOpen(false); }}
            />
            <MoreMenuItem
              icon={<LayoutTemplate className="h-3.5 w-3.5" />}
              label="Als Vorlage speichern"
              hint="Wiederverwendbar in der Sidebar"
              onClick={() => { onSaveAsTemplate(); setMoreOpen(false); }}
            />
            <MoreMenuItem
              icon={<Send className="h-3.5 w-3.5" />}
              label="Als Trigger freischalten"
              hint={hasOutput ? 'API-Endpoint für Online-Shop / CRM' : 'Erst Mail generieren'}
              onClick={() => { if (hasOutput) onPublishTrigger(); setMoreOpen(false); }}
            />
            {isModuleEnabled('sending') && (
              <MoreMenuItem
                icon={<ListOrdered className="h-3.5 w-3.5" />}
                label="Sende-Warteschlange"
                hint="Jobs, Status & Fehler"
                onClick={() => {
                  router.push('/queue');
                  setMoreOpen(false);
                }}
              />
            )}
            <div className="my-1 h-px bg-zinc-100 dark:bg-zinc-800" />
            <MoreMenuItem
              icon={<Settings className="h-3.5 w-3.5" />}
              label="Einstellungen"
              hint="Workspaces, Zielgruppen, KI-Prompts"
              onClick={() => { onOpenSettings(); setMoreOpen(false); }}
            />
          </div>
        )}
      </div>
    </header>
  );
}

// ─── TopBar-Subkomponenten ──────────────────────────────────────────────────

function SenderPickerCompact({ senders, selectedSenderEmail, onSelectSender }: {
  senders: Array<{ email: string; name: string; companyId?: number; note?: string }>;
  selectedSenderEmail: string;
  onSelectSender: (email: string) => void;
}) {
  const selected = senders.find((s) => s.email === selectedSenderEmail);
  const label = selected ? selected.name : 'Workspace-Default';
  return (
    <div className="relative inline-flex items-center gap-1 rounded-md border border-zinc-200 bg-zinc-50 px-2 py-1 dark:border-zinc-700 dark:bg-zinc-900">
      <span className="text-[10px] font-semibold uppercase tracking-wider text-zinc-500">Von</span>
      <select
        value={selectedSenderEmail}
        onChange={(e) => onSelectSender(e.target.value)}
        title={selected?.note ?? `Sender: ${selectedSenderEmail || 'Workspace-Default'}`}
        className="appearance-none bg-transparent pr-4 text-xs font-medium text-zinc-800 focus:outline-none dark:text-zinc-200"
      >
        <option value="">Workspace-Default</option>
        {senders.map((s) => (
          <option key={s.email} value={s.email}>{s.name}</option>
        ))}
      </select>
      <ChevronDown className="pointer-events-none absolute right-1.5 h-3 w-3 text-zinc-400" />
      <span className="sr-only">{label}</span>
    </div>
  );
}

function MoreMenuItem({ icon, label, hint, onClick }: {
  icon: React.ReactNode;
  label: string;
  hint: string;
  onClick: () => void;
}) {
  return (
    <button
      type="button"
      role="menuitem"
      onClick={onClick}
      className="flex w-full items-start gap-2 px-3 py-2 text-left transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800"
    >
      <span className="mt-0.5 text-zinc-500 dark:text-zinc-400">{icon}</span>
      <div className="min-w-0 flex-1">
        <div className="text-xs font-medium text-zinc-800 dark:text-zinc-200">{label}</div>
        <div className="text-[10px] text-zinc-500 dark:text-zinc-500">{hint}</div>
      </div>
    </button>
  );
}

/**
 * Test-Versand-Button mit Inline-Popover für Empfänger-Eingabe.
 * Klick auf Button öffnet Popover statt direkt zu senden — User kann eine Test-Adresse
 * eingeben (oder leer lassen für Default M365_AUDIT_BCC) und einen Diagnose-Endpoint
 * aufrufen, der ohne Mail-Versand prüft, ob Token + Graph erreichbar sind.
 *
 * Persistierung: handleSendTest speichert den Wert in localStorage, sodass beim nächsten
 * Öffnen des Popovers die zuletzt verwendete Test-Adresse vorausgefüllt ist.
 */
function TestSendButton({ isSendingTest, testRecipient, onChangeTestRecipient, onSendTest, testSendResult }: {
  isSendingTest: boolean;
  testRecipient: string;
  onChangeTestRecipient: (v: string) => void;
  onSendTest: (recipient?: string) => void;
  testSendResult: {
    ok: boolean;
    message: string;
    kind?: 'network' | 'auth' | 'graph' | 'config' | 'unknown';
    endpoint?: string;
    httpStatus?: number;
    hint?: string;
  } | null;
}) {
  const [open, setOpen] = useState(false);
  const [diagnosing, setDiagnosing] = useState(false);
  const [diagnoseResult, setDiagnoseResult] = useState<{ ok: boolean; stage: string; detail: string } | null>(null);
  const containerRef = useRef<HTMLDivElement | null>(null);
  const inputRef = useRef<HTMLInputElement | null>(null);

  // Outside-Click + ESC schließen — gleiches Pattern wie das Mehr-Menü oben.
  useEffect(() => {
    if (!open) return;
    const handler = (e: MouseEvent) => {
      if (containerRef.current && !containerRef.current.contains(e.target as Node)) setOpen(false);
    };
    const escHandler = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', handler);
    document.addEventListener('keydown', escHandler);
    return () => {
      document.removeEventListener('mousedown', handler);
      document.removeEventListener('keydown', escHandler);
    };
  }, [open]);

  // Auto-Focus auf Input beim Öffnen
  useEffect(() => {
    if (open) setTimeout(() => inputRef.current?.focus(), 0);
  }, [open]);

  // Sobald Result eintrudelt, Popover schließen — ResultPopover übernimmt die Anzeige.
  useEffect(() => {
    if (testSendResult) setOpen(false);
  }, [testSendResult]);

  const submit = () => {
    onSendTest(testRecipient.trim() || undefined);
  };

  const runDiagnose = async () => {
    setDiagnosing(true);
    setDiagnoseResult(null);
    try {
      const probe = testRecipient.trim();
      const url = probe ? `/api/m365-diagnose?probe=${encodeURIComponent(probe)}` : '/api/m365-diagnose';
      const res = await fetch(url);
      const data = await res.json();
      setDiagnoseResult({ ok: !!data.ok, stage: data.stage ?? 'unknown', detail: data.detail ?? `HTTP ${res.status}` });
    } catch (e) {
      setDiagnoseResult({ ok: false, stage: 'fetch', detail: e instanceof Error ? e.message : 'Network error' });
    } finally {
      setDiagnosing(false);
      setTimeout(() => setDiagnoseResult(null), 12000);
    }
  };

  return (
    <div className="relative" ref={containerRef}>
      <button
        onClick={() => setOpen((v) => !v)}
        disabled={isSendingTest}
        title="Test-Mail an eine angegebene Adresse oder die Audit-Inbox senden"
        className="inline-flex items-center gap-1.5 rounded-md border border-zinc-200 px-3 py-1.5 text-xs font-medium text-zinc-700 transition hover:bg-zinc-50 disabled:opacity-40 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-900"
      >
        {isSendingTest ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Eye className="h-3.5 w-3.5" />}
        {isSendingTest ? 'Sende …' : 'Test-Versand'}
      </button>

      {open && !testSendResult && (
        <div className="absolute right-0 top-full z-50 mt-2 w-80 rounded-md border border-zinc-200 bg-white p-3 text-xs shadow-lg dark:border-zinc-700 dark:bg-zinc-900">
          <label className="mb-1 block font-medium text-zinc-700 dark:text-zinc-300">Empfänger</label>
          <input
            ref={inputRef}
            type="email"
            value={testRecipient}
            onChange={(e) => onChangeTestRecipient(e.target.value)}
            onKeyDown={(e) => { if (e.key === 'Enter') submit(); }}
            placeholder="Default: M365_AUDIT_BCC"
            className="w-full rounded border border-zinc-200 bg-white px-2 py-1.5 text-xs focus:border-zinc-400 focus:outline-none dark:border-zinc-700 dark:bg-zinc-950"
          />
          <p className="mt-1 text-[10px] leading-snug text-zinc-500">
            Leer lassen → Test geht an die Audit-Inbox aus <code>.env.local</code>.
          </p>

          <div className="mt-3 flex items-center justify-between gap-2">
            <button
              type="button"
              onClick={runDiagnose}
              disabled={diagnosing}
              className="text-[11px] text-zinc-500 underline-offset-2 hover:text-zinc-800 hover:underline disabled:opacity-40 dark:hover:text-zinc-200"
            >
              {diagnosing ? 'Diagnose läuft …' : 'Diagnose'}
            </button>
            <div className="flex items-center gap-2">
              <button
                type="button"
                onClick={() => setOpen(false)}
                className="rounded px-2 py-1 text-[11px] text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800"
              >
                Abbrechen
              </button>
              <button
                type="button"
                onClick={submit}
                className="rounded bg-zinc-900 px-3 py-1 text-[11px] font-medium text-white hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
              >
                Senden
              </button>
            </div>
          </div>

          {diagnoseResult && (
            <div className={cn(
              'mt-2 rounded border p-2 text-[11px]',
              diagnoseResult.ok
                ? 'border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-900/50 dark:bg-emerald-950/30 dark:text-emerald-200'
                : 'border-red-200 bg-red-50 text-red-800 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-200',
            )}>
              <div className="font-medium">Stage: {diagnoseResult.stage}</div>
              <div>{diagnoseResult.detail}</div>
            </div>
          )}
        </div>
      )}

      {testSendResult && <ResultPopover result={testSendResult} />}
    </div>
  );
}

function ResultPopover({ result }: {
  result: {
    ok: boolean;
    message: string;
    kind?: 'network' | 'auth' | 'graph' | 'config' | 'unknown';
    endpoint?: string;
    httpStatus?: number;
    hint?: string;
  };
}) {
  return (
    <div className={cn(
      'absolute right-0 top-full z-50 mt-2 w-80 rounded-md border p-3 text-xs shadow-lg',
      result.ok
        ? 'border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-900/50 dark:bg-emerald-950/30 dark:text-emerald-200'
        : 'border-red-200 bg-red-50 text-red-800 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-200',
    )}>
      <div className="flex items-start gap-2">
        {result.ok ? <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" /> : <XCircle className="mt-0.5 h-4 w-4 shrink-0" />}
        <div className="min-w-0 flex-1">
          <div className="break-words">{result.message}</div>
          {!result.ok && (result.kind || result.endpoint || result.httpStatus) && (
            <div className="mt-1 text-[10px] uppercase tracking-wide opacity-70">
              {result.kind && <span>{result.kind}</span>}
              {result.endpoint && <span> · {result.endpoint}</span>}
              {result.httpStatus && <span> · HTTP {result.httpStatus}</span>}
            </div>
          )}
          {!result.ok && result.hint && (
            <div className="mt-2 rounded bg-white/40 p-1.5 text-[11px] leading-snug dark:bg-black/20">
              {result.hint}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function BulkResultPopover({ result }: { result: { ok: boolean; message: string; campaignId?: string } }) {
  return (
    <div className={cn(
      'absolute right-0 top-full z-50 mt-2 w-80 rounded-md border p-3 text-xs shadow-lg',
      result.ok
        ? 'border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-900/50 dark:bg-emerald-950/30 dark:text-emerald-200'
        : 'border-red-200 bg-red-50 text-red-800 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-200',
    )}>
      <div className="flex items-start gap-2">
        {result.ok ? <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" /> : <XCircle className="mt-0.5 h-4 w-4 shrink-0" />}
        <div className="flex-1">
          <div className="font-medium">{result.ok ? 'Versand-Queue gefüllt' : 'Fehler'}</div>
          <div className="mt-0.5 opacity-80">{result.message}</div>
          {result.campaignId && (
            <div className="mt-1 font-mono text-[10px] opacity-60">Campaign: {result.campaignId.slice(0, 8)}…</div>
          )}
        </div>
      </div>
    </div>
  );
}

// Konvertiert die generierte Strategy in einen Tiptap-Doc, der wie ein
// editierbarer Marketing-Brief aussieht: Headline + Intro + Solution +
// Vorteile-Liste + P.S. + Signatur. Wird nach dem ersten Generate-Run in den
// Editor geschrieben, wenn der User nichts selbst getippt hat — damit Tab 1 nach
// dem Generieren bearbeitbaren Brief-Text zeigt und ein Re-Generate möglich ist.
function strategyToTiptapDoc(s: EditableStrategy): object {
  const text = (t: string) => ({ type: 'text', text: t });
  const para = (t: string) => ({ type: 'paragraph', content: t.trim() ? [text(t)] : [] });
  const heading = (level: number, t: string) => ({ type: 'heading', attrs: { level }, content: [text(t)] });

  const blocks: Array<Record<string, unknown>> = [];
  if (s.hero_headline) blocks.push(heading(2, s.hero_headline));
  if (s.intro) blocks.push(para(s.intro));
  if (s.solution) blocks.push(para(s.solution));
  if (s.benefits.length > 0) {
    blocks.push({
      type: 'bulletList',
      content: s.benefits.map((b) => ({
        type: 'listItem',
        content: [para(b)],
      })),
    });
  }
  if (s.ps_line) blocks.push(para(s.ps_line));
  return { type: 'doc', content: blocks };
}
