'use client';

import { useEffect, useState } from 'react';
import { Target, MousePointerClick, Image as ImageIcon, Sparkles, Wand2, Loader2, X, AtSign, Info, ChevronDown, Settings2, LayoutTemplate, Trash2, Globe, CheckCircle2, ArrowRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import { type PickerRecipient, type RecipientFilterSnapshot } from '@/modules/recipients/ui/RecipientPicker';
import { ModeSwitcher } from './ModeSwitcher';
import { CustomSectionEditor } from './CustomSectionEditor';
// SuggestLoader-Import entfernt — wird jetzt am Page-Level (page.tsx) gerendert

export type Mode = 'preserve' | 'rewrite';
export type Language = 'auto' | 'de' | 'en';

/**
 * Ausgangspunkt des Users in Tab 2:
 *  - 'text' → Mailtext liegt fertig in Tab 1, mode=preserve, KI nur formatieren
 *  - 'idea' → Briefing-Daten und/oder Landing-Page-URL — KI schreibt Brief in Tab 1.
 *             URL ist optional: User kann Felder rein manuell ausfüllen, nur scrapen
 *             lassen, oder beides kombinieren (scrape + manuelle Verfeinerung).
 *
 * UI-only — wird nicht ans LLM gesendet (siehe `stripBriefingForApi`).
 */
export type BriefingIntent = 'text' | 'idea';

/**
 * Schedule-Konfiguration für den Versand-Tab.
 * `mode='now'` = sofort versenden (nach Klick auf Versand-Button).
 * `mode='scheduled'` = einmaliger Versand zu `scheduledAt` (ISO-Date).
 * `mode='recurring'` = wiederkehrender Versand ab `scheduledAt` mit Frequency.
 *
 * Throttle steuert die Ausspielung pro Stunde (anti-spam, M365-Rate-Limits) —
 * Default 100/h ist konservativ unter M365-Limit von ~30/min.
 */
export type ScheduleMode = 'now' | 'scheduled' | 'recurring';
export type ScheduleFrequency = 'daily' | 'weekly' | 'biweekly' | 'monthly';

export interface BriefingSchedule {
  mode: ScheduleMode;
  /** ISO-Date z.B. '2026-05-13T09:00:00.000Z' — bei mode='scheduled'/'recurring' Pflicht */
  scheduledAt?: string;
  /** Bei mode='recurring' — Wiederholungs-Rhythmus */
  frequency?: ScheduleFrequency;
  /** Bei frequency='weekly'/'biweekly' — Wochentag 0-6 (0=So). Bei 'monthly' Tag 1-31. */
  dayOfPeriod?: number;
  /** Throttle: max. Mails pro Stunde. Default 100. */
  throttlePerHour?: number;
  /** Max. Mails pro Kalendertag. Undefined = unbeschränkt. Nur wirksam bei scheduled/recurring. */
  maxPerDay?: number;
  /** Tägliches Sende-Fenster als 'HH:MM'. Beide gesetzt oder beide undefined. Nur scheduled/recurring. */
  sendWindowStart?: string;
  sendWindowEnd?: string;
  /** Erlaubte Wochentage (0=So..6=Sa). Undefined oder leer = alle 7 Tage. */
  allowedWeekdays?: number[];
  /** KI-Suggestion-Cache: zuletzt empfohlene Slots (vom Suggest-Endpoint) */
  suggestion?: {
    bestDays: string[]; // z.B. ['Tuesday', 'Wednesday']
    bestTimeWindow: string; // z.B. '09:00-11:00'
    timezone: string;
    reasoning: string;
    generatedAt: string;
  };
}

/**
 * UTM-Defaults der Campaign — bei aktivem Toggle werden alle Links im HTML zur Send-Zeit
 * mit diesen Parametern angereichert (Auto-Append). `utm_content` wird beim A/B-Send
 * automatisch pro Variante überschrieben (A/B/C).
 */
export interface BriefingUtm {
  source?: string;
  medium?: string;
  campaign?: string;
  content?: string;
  term?: string;
}

/**
 * Champion-Test-Konfiguration: nur effektiv wenn ≥ 2 Varianten existieren.
 * - testPercent: 0-100 (default 20)
 * - winnerHours: 1-24 (default 4)
 */
export interface BriefingChampionTest {
  testPercent: number;
  winnerHours: number;
}

/**
 * UI-only Toggle-State für die Optional-Sections.
 * Wird vor dem Senden an die API gestrippt — die KI sieht diese Flags nie.
 * Toggle OFF → entsprechende optional Felder werden beim Submit auf undefined gesetzt
 * (User-Input bleibt aber im State erhalten falls Toggle wieder ON).
 */
export interface BriefingOptional {
  /** Toggle für ctaSecondaryText/ctaSecondaryUrl */
  secondaryCta?: boolean;
  /** Toggle für mode/language Override (sonst bleiben die Defaults) */
  advanced?: boolean;
  /** Toggle für UTM-Auto-Append (Tracking) */
  utmTracking?: boolean;
  /** Toggle für Champion-Test (statt Even-Split bei A/B). Nur effektiv wenn ≥ 2 Varianten existieren. */
  championTest?: boolean;
  /** Toggle für die Sichtbarkeit der "Zusätzliche Sections"-Gruppe. Einzelne Sections haben ihren eigenen `enabled`-Flag. */
  extraSections?: boolean;
}

/**
 * Position einer Zusatz-Section im finalen MJML. Muss mit `SectionPosition` in
 * `mailforge/src/lib/email/inject-sections.ts` synchron bleiben.
 */
export type SectionPosition = 'before_cta' | 'after_cta' | 'before_signature' | 'after_signature';
export type SectionKind = 'contact_service' | 'app_download' | 'testimonial' | 'faq' | 'custom';

/**
 * Body-Modus einer Section. Steuert UI (Editor vs. Raw-Textarea) UND Pipeline-Verhalten:
 *  - tiptap   → Headline+Body-Editor, KI-Vorschlag aktiv, mjmlBlock enthält {{TIPTAP_HTML}}-Slot
 *  - mjml_raw → User-MJML als ganze Section, kein Slot, KI off
 *  - html_raw → User-HTML wird sanitized + in <mj-raw> gewrapped, kein Slot, KI off
 * Muss synchron bleiben mit `bodyMode` in `section_templates` (Drizzle-Schema)
 * und mit `ResolvedSection.bodyMode` in `inject-sections.ts`.
 */
export type SectionBodyMode = 'tiptap' | 'mjml_raw' | 'html_raw';

/**
 * Ein TipTap-Document mit minimaler Typedef — keine Lizenz auf TipTap-internal types,
 * damit der Briefing-Type Frontend-only bleiben kann.
 */
export interface BriefingTipTapDoc {
  type: 'doc';
  content: Array<{
    type: string;
    attrs?: Record<string, unknown>;
    content?: unknown[];
    marks?: Array<{ type: string }>;
    text?: string;
  }>;
}

/**
 * Eine Instanz einer Zusatz-Section, die der User im Briefing aktiviert hat.
 * Die `templateId` zeigt auf eine Row in `section_templates` (eine Workspace-spezifische
 * MJML-Vorlage), `contentJson` ist der editierbare Doc — Default kommt aus dem Template.
 *
 * Lifecycle: bei Toggle-ON wird ein neues Instance angelegt (mit `instanceId = crypto.randomUUID()`,
 * `contentJson = template.defaultContentJson`). User-Edits oder KI-Vorschläge ändern
 * `contentJson` und setzen `customized=true`.
 *
 * Beim Submit (siehe `stripBriefingForApi`) werden nur enabled-Instances durchgereicht,
 * `lastAiSuggestion` und `customized`-Flags werden gestrippt (UI-only).
 */
export interface BriefingSectionInstance {
  /** Lokale UUID — für Listen-Stabilität und Idempotenz beim MJML-Inject. */
  instanceId: string;
  /** FK auf `section_templates.id` — referenced beim Submit zum Lookup des MJML-Blocks. */
  templateId: string;
  kind: SectionKind;
  enabled: boolean;
  position: SectionPosition;
  contentJson: BriefingTipTapDoc;
  /** True sobald User oder KI den Default verändert hat (UI-Affordance "geändert"). */
  customized: boolean;
  /** Optionaler letzter KI-Vorschlag, ungescropt — falls User zurücksetzen will. */
  lastAiSuggestion?: BriefingTipTapDoc;
  /**
   * Optional: überschreibt den Body-Modus des Templates für genau diese Mail.
   * Wenn gesetzt + raw-Modus → Pipeline ignoriert template.mjmlBlock und nutzt customBody.
   * Wenn nicht gesetzt → fällt auf template.bodyMode zurück (heutiges Verhalten).
   */
  bodyMode?: SectionBodyMode;
  /** Bei bodyMode='mjml_raw' / 'html_raw' der vom User importierte Block. */
  customBody?: string;
}

export interface Briefing {
  mode: Mode;
  language: Language;
  campaignName: string;
  /**
   * Pflichtfeld im rewrite-Mode, optional im preserve-Mode. Im preserve-Mode darf das
   * Feld leer sein — Backend extrahiert Subject/Preheader/Hero-Headline-Kontext direkt
   * aus briefHtml. Bei leerem Wert sendet `stripBriefingForApi` ein `undefined`.
   */
  offer?: string;
  /** Analog zu offer — siehe dort. */
  painPoint?: string;
  ctaPrimaryText: string;
  ctaPrimaryUrl: string;
  ctaSecondaryText?: string;
  ctaSecondaryUrl?: string;
  workspaceSlug: string;
  heroEnabled: boolean;
  heroBriefHint?: string;
  /**
   * Optional: maximale Hero-Bild-Höhe in Pixeln (cropped via object-fit:cover in modernen Clients).
   * Default: undefined → MJML lässt das Bild seine natürliche Höhe behalten.
   */
  heroHeight?: number;
  /**
   * Wo das Hero-Bild gecroppt wird, wenn `heroHeight` gesetzt ist — wird zu `object-position`.
   * Default: undefined → 'center' (gleiches Verhalten wie vorher hardcoded).
   * Nur sinnvoll wenn `heroHeight` ebenfalls gesetzt ist — sonst wird kein Crop angewendet.
   */
  heroCropPosition?: 'top' | 'center' | 'bottom';
  recipients?: PickerRecipient[];
  /** Filter-Snapshot, mit dem die Recipients geladen wurden — UI-only, ermöglicht Re-Run mit gleichem Filter. */
  recipientFilter?: RecipientFilterSnapshot;
  /** Versand-Scheduling (Tab 5). Default mode='now' = sofort versenden. */
  schedule?: BriefingSchedule;
  /** UTM-Defaults für Auto-Append. Nur aktiv wenn `optional.utmTracking === true`. */
  utm?: BriefingUtm;
  /** Champion-Test-Config. Nur aktiv wenn `optional.championTest === true` UND ≥ 2 Varianten beim Send. */
  championTest?: BriefingChampionTest;
  /** URL die der User zum Scrape eingegeben hat (UI-Persistenz, damit Reload den Wert behält). */
  scrapedUrl?: string;
  /**
   * Lokal-gespeicherte Hero-Bild-URL — entweder aus Webseiten-Scrape übernommen oder eigenes Upload.
   * Wenn gesetzt → Pipeline überspringt fal.ai und nutzt direkt diese URL für {{HERO_URL}}.
   */
  heroImageUrl?: string;
  /** UI-only — siehe BriefingOptional. NICHT an die API senden. */
  optional?: BriefingOptional;
  /**
   * Modulare Zusatz-Sections (Kontakt, App-Download, Testimonial, FAQ). User kann pro
   * Section toggle/edit/KI-Vorschlag triggern. Beim Submit (`stripBriefingForApi`)
   * werden nur enabled-Sections durchgereicht.
   */
  extraSections?: BriefingSectionInstance[];
  /**
   * Welchen Ausgangspunkt hat der User in Tab 2 gewählt? UI-only — entscheidet welche
   * Tools (Scrape / KI-Brief / Editor-Hint) im Briefing-Panel sichtbar sind.
   * Leer = Auto-Detect beim ersten Render (siehe `detectIntent`).
   */
  intent?: BriefingIntent;
}

export const EMPTY_BRIEFING: Briefing = {
  mode: 'rewrite',
  language: 'auto',
  campaignName: '',
  offer: '',
  painPoint: '',
  ctaPrimaryText: 'Jetzt mehr erfahren',
  ctaPrimaryUrl: '',
  workspaceSlug: '',
  heroEnabled: true,
  schedule: {
    mode: 'now',
    throttlePerHour: 100,
  },
  optional: {
    secondaryCta: false,
    advanced: false,
    utmTracking: false,
    championTest: false,
    extraSections: false,
  },
  extraSections: [],
  utm: {
    medium: 'email',
  },
  championTest: {
    testPercent: 20,
    winnerHours: 4,
  },
};

/**
 * Strippt UI-only Toggle-State + setzt optional-Felder auf undefined wenn ihr Toggle OFF ist.
 * Output ist API-ready: kein optional-Property, keine Felder die der User „deaktiviert" hat.
 */
export function stripBriefingForApi(b: Briefing): Briefing {
  const opt = b.optional ?? {};
  // ESLint-Tausch: optional + intent (UI-only) bewusst raus-destrukturieren damit sie im Rest fehlen
  const { optional: _drop, intent: _dropIntent, ...rest } = b;
  void _drop;
  void _dropIntent;
  // Im preserve-Mode dürfen offer/painPoint leer sein — Backend extrahiert dann aus briefHtml.
  // Wir senden leere Strings als undefined, damit die Zod-Validierung nicht wegen '' falsch
  // interpretiert (siehe /api/generate/route.ts: Cross-Field-Validation greift bei undefined).
  const preserveMode = rest.mode === 'preserve';
  return {
    ...rest,
    offer: preserveMode && !rest.offer?.trim() ? undefined : rest.offer,
    painPoint: preserveMode && !rest.painPoint?.trim() ? undefined : rest.painPoint,
    ctaSecondaryText: opt.secondaryCta ? rest.ctaSecondaryText : undefined,
    ctaSecondaryUrl: opt.secondaryCta ? rest.ctaSecondaryUrl : undefined,
    // heroHeight nur wenn Hero aktiv — sonst macht's keinen Sinn.
    heroHeight: rest.heroEnabled ? rest.heroHeight : undefined,
    // heroCropPosition nur sinnvoll wenn Hero aktiv UND heroHeight gesetzt (sonst kein Crop).
    heroCropPosition: rest.heroEnabled && rest.heroHeight ? rest.heroCropPosition : undefined,
    // utm nur durchgereicht wenn der Tracking-Toggle aktiv ist — sonst kein Auto-Append.
    utm: opt.utmTracking ? rest.utm : undefined,
    // championTest nur durchgereicht wenn Toggle ON — sonst klassischer Even-Split-Send.
    championTest: opt.championTest ? rest.championTest : undefined,
    // ExtraSections: Wenn mindestens eine Section enabled ist, durchreichen — der Group-
    // Toggle wirkt nur noch als UI-Sichtbarkeits-Schalter (Block einklappen). Vorher hat
    // ein OFF-Group-Toggle bei aktivem Section-Toggle die Sections silent gedroppt.
    // UI-only-Felder (lastAiSuggestion, customized-Flag im map-Output reicht) bleiben raus.
    extraSections: (rest.extraSections ?? []).some((s) => s.enabled)
      ? (rest.extraSections ?? [])
          .filter((s) => s.enabled)
          .map((s) => ({
            instanceId: s.instanceId,
            templateId: s.templateId,
            kind: s.kind,
            enabled: true,
            position: s.position,
            contentJson: s.contentJson,
            customized: s.customized,
            // bodyMode + customBody nur durchreichen wenn raw-Modus aktiv — bei tiptap
            // sind die Felder undefined und die API fällt auf das Template-Default zurück.
            ...(s.bodyMode && s.bodyMode !== 'tiptap' && s.customBody
              ? { bodyMode: s.bodyMode, customBody: s.customBody }
              : {}),
          }))
      : undefined,
    // Mode + Language werden IMMER mitgesendet (Defaults sind valid),
    // 'advanced'-Toggle steuert nur die Sichtbarkeit der Override-UI.
  };
}
const EMPTY = EMPTY_BRIEFING;

/**
 * Auto-Detect für den Briefing-Intent: bestimmt aus Briefing-State + Editor-Text,
 * welcher der zwei Use-Cases am wahrscheinlichsten ist. Greift nur, wenn der User
 * noch nichts manuell gewählt hat (`b.intent === undefined`).
 *
 * Heuristik:
 *  - Editor-Text ≥ 100 Zeichen → 'text' (Mailtext liegt schon im Editor)
 *  - sonst → 'idea' (URL-Scrape ist Sub-Workflow innerhalb von 'idea')
 */
export function detectIntent(_b: Briefing, briefText: string): BriefingIntent {
  if ((briefText ?? '').trim().length >= 100) return 'text';
  return 'idea';
}

/**
 * Mode-aware Briefing-Validation:
 *  - rewrite: campaignName + offer + painPoint + workspaceSlug (KI braucht Inhalt)
 *  - preserve: campaignName + workspaceSlug (offer/painPoint optional, briefHtml-Pflicht wird im Footer-Submit geprüft)
 *
 * CTA-URL bewusst NICHT erforderlich — manche Mails haben keinen CTA, andere bekommen die URL später
 * per Re-Render-Flow nachgetragen (siehe handleReRender in page.tsx).
 */
export function isBriefingReady(b: Briefing): boolean {
  if (!b.campaignName || !b.workspaceSlug) return false;
  if (b.mode === 'preserve') return true;
  return Boolean(b.offer && b.painPoint);
}

export interface BriefingPanelProps {
  workspaces: Array<{ slug: string; name: string }>;
  /** Controlled-State — vom Parent verwaltet, damit der Footer denselben Briefing manipulieren kann */
  value: Briefing;
  onChange: (b: Briefing) => void;
  briefText?: string; // Tiptap-Plain-Text für KI-Vorschläge
  className?: string;
  /**
   * Layout-Mode:
   * - 'sidebar' (default): Single-Column, kompakt — wie früher als 380px-Sidebar
   * - 'responsive': 2-spaltig auf lg+ (≥1024px), 1-spaltig darunter — für full-width Tab-Layout
   */
  layout?: 'sidebar' | 'responsive';
  /**
   * KI-Suggest-State — gelifted auf Page-Level damit der Footer-CTA den Suggest auslösen kann
   * und der `<SuggestLoader />` als Page-weites Modal-Overlay rendert.
   * Optional weil `'sidebar'`-Mode (Single-Page-Standalone) ohne Footer-Trigger auskommt
   * und intern fallen-back zu lokalem State.
   */
  isSuggesting?: boolean;
  suggestFeedback?: SuggestionFeedback | null;
  suggestError?: string | null;
  inferredFields?: Set<string>;
  onSuggest?: () => void;
  onDismissFeedback?: () => void;
  onDismissError?: () => void;
  /**
   * Templates für den Picker am oberen Panel-Rand. Nur Templates der aktiven Workspace werden gezeigt.
   * Wenn Liste leer oder undefined → Picker wird nicht gerendert.
   */
  templates?: BriefingTemplateSummary[];
  onLoadTemplate?: (templateId: string) => void | Promise<void>;
  onDeleteTemplate?: (templateId: string) => void | Promise<void>;
  /**
   * Webseiten-Scrape-Integration (gelifted im Parent damit Auto-Suggest + Bild-Import dort orchestriert werden).
   * Wenn nicht gesetzt → Section wird nicht gerendert.
   */
  onScrapeUrl?: (url: string) => Promise<ScrapedSummary | null>;
  onSelectScrapedImage?: (imageUrl: string) => Promise<void>;
  isScraping?: boolean;
  isImportingImage?: boolean;
  scrapeResult?: ScrapedSummary | null;
  /**
   * KI-Brief-Schreiber — analog zu onScrapeUrl, aber mit gefülltem Briefing als Input.
   * Page.tsx ruft POST /api/draft-brief und füllt anschließend den TipTap-Editor in Tab 1.
   * Wenn nicht gesetzt → Section wird nicht gerendert.
   */
  onDraftBrief?: () => void;
  isDraftingBrief?: boolean;
  draftBriefError?: string | null;
  onDismissDraftBriefError?: () => void;
  /**
   * Sender-Pool aus M365 — wird unter dem Workspace-Selector als Dropdown angeboten.
   * Sender-Wahl ist eine Briefing-Entscheidung (gehört zur Workspace-Identität), nicht
   * mehr Teil des Editor-Headers. Im Versand-Footer (Tab 5) ist sie als
   * sekundäre Override-Option nochmal verfügbar.
   * Wenn `senders` leer/undefined → Section wird nicht gerendert (Workspace-Default greift).
   */
  senders?: Array<{ email: string; name: string; companyId?: number; note?: string }>;
  selectedSenderEmail?: string;
  onSelectSender?: (email: string) => void;
  /**
   * Callback um zum Editor-Tab (Tab 1) zu springen. Wird vom `TextReadyHint` für den
   * "Zum Editor"-Button genutzt, wenn der User Intent='text' gewählt hat.
   * Optional — Sidebar-Layout (Standalone) hat keinen Tab-Wechsel.
   */
  onJumpToEditor?: () => void;
}

/** Minimaler Scrape-Shape für die UI (vermeidet zirkuläre Imports auf API-Types). */
export interface ScrapedSummary {
  url: string;
  title: string;
  description: string;
  images: Array<{ url: string; alt: string; width?: number; height?: number }>;
}

/** Minimaler Template-Shape den der Picker braucht (vermeidet zirkuläre Import auf db). */
export interface BriefingTemplateSummary {
  id: string;
  name: string;
  description: string | null;
  workspaceId: string;
  workspaceSlug: string;
  brandName: string;
}

/**
 * Feedback-Shape für KI-Suggest-Ergebnis.
 * Wird in `<SuggestFeedbackPanel>` im Briefing-Header gerendert.
 * Lebt jetzt im Parent (`page.tsx`), wird als Prop runter gereicht — siehe `BriefingPanelProps`.
 */
export interface SuggestionFeedback {
  filledKeys: string[];
  inferredKeys: string[];
  reasoning?: string;
  durationMs: number;
}

export function BriefingPanel({
  workspaces,
  value,
  onChange,
  briefText,
  className,
  layout = 'sidebar',
  // KI-Suggest-State — vom Parent geliftet (page.tsx). Defaults für Standalone-Use.
  isSuggesting = false,
  suggestFeedback = null,
  suggestError = null,
  inferredFields = new Set<string>(),
  onSuggest,
  onDismissFeedback,
  onDismissError,
  templates,
  onLoadTemplate,
  onDeleteTemplate,
  onScrapeUrl,
  onSelectScrapedImage,
  isScraping = false,
  isImportingImage = false,
  scrapeResult = null,
  onDraftBrief,
  isDraftingBrief = false,
  draftBriefError = null,
  onDismissDraftBriefError,
  senders,
  selectedSenderEmail = '',
  onSelectSender,
  onJumpToEditor,
}: BriefingPanelProps) {
  const b = value;
  // Effective Intent: Auto-Detect solange der User nicht manuell gewählt hat.
  // Wird in einem Effect persistiert, damit der Picker nicht weiter springt sobald
  // sich Editor-Text/URL ändern (sonst wäre die Card-Auswahl flackernd).
  //
  // Legacy-Migration: alte Drafts haben evtl. `intent: 'url'` in der DB — der
  // Modus existiert nicht mehr, URL-Scrape ist jetzt Sub-Workflow von 'idea'.
  // Cast über `string` weil TS den Vergleich nach Type-Verengung sonst als never erkennt.
  const isLegacyUrlIntent = (b.intent as string | undefined) === 'url';
  const persistedIntent: BriefingIntent | undefined = isLegacyUrlIntent ? 'idea' : b.intent;
  const effectiveIntent: BriefingIntent = persistedIntent ?? detectIntent(b, briefText ?? '');
  useEffect(() => {
    if (!b.intent || isLegacyUrlIntent) onChange({ ...b, intent: effectiveIntent });
    // Einmaliges Bootstrap nach Mount; bewusst keine Deps, damit der Wert nicht weiter
    // rekursiv die Auto-Detect-Heuristik triggert. State-Updates danach übernimmt der User.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Field-Wrapper-Klasse: 1-col auf Sidebar/Tablet, 2-col auf Desktop wenn responsive
  const fieldsWrapperClass = layout === 'responsive'
    ? 'grid grid-cols-1 lg:grid-cols-2 gap-x-5 gap-y-3'
    : 'flex flex-col gap-3';
  // Wide-Field-Klasse: nur in 'responsive' Layout über beide Spalten
  const wideField = layout === 'responsive' ? 'lg:col-span-2' : '';
  // Compat-Alias: bestehende setB(next)-Aufrufe in handleSuggestHero
  // schreiben jetzt direkt in den controlled state.
  const setB = (next: Briefing) => onChange(next);
  // Hero-Suggest bleibt lokal (separater Concern, nicht Teil des Footer-CTA-State-Machine)
  const [isSuggestingHero, setIsSuggestingHero] = useState(false);
  // Alias für Render — `feedback` als kürzerer Name innerhalb der JSX
  const feedback = suggestFeedback;

  const handleSuggestHero = async () => {
    setIsSuggestingHero(true);
    try {
      const res = await fetch('/api/suggest-hero', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          campaignName: b.campaignName,
          offer: b.offer,
          painPoint: b.painPoint,
          workspaceSlug: b.workspaceSlug,
          briefText: briefText,
        }),
      });
      const data = await res.json();
      if (res.ok && data.heroBriefHint) {
        const next = { ...b, heroBriefHint: data.heroBriefHint };
        setB(next);
        onChange?.(next);
      }
    } finally { setIsSuggestingHero(false); }
  };

  const update = <K extends keyof Briefing>(k: K, v: Briefing[K]) => {
    const next = { ...b, [k]: v };
    setB(next);
    // Inferred-Flag-Auto-Clear bewusst weggelassen — der visuelle Pink-Ring um KI-vermutete Felder
    // bleibt nach manuellem Edit erhalten. Acceptable Trade-off für simplere State-Architektur;
    // kann via `onClearInferredField`-Callback nachgezogen werden falls UX-Bedarf entsteht.
  };

  // `EMPTY` und `isReady` werden seit dem State-Lift nicht mehr in dieser Komponente
  // verwendet (handleSuggest lebt jetzt im Parent, isReady kommt aus `isBriefingReady()`
  // im Footer). `briefHasContent` bleibt für die Hero-Suggest-Disabled-Logik.
  const briefHasContent = (briefText ?? '').trim().length >= 10;

  // Im 'sidebar'-Layout hat das Panel eigene Höhe + eigenen Scroll (alte Sidebar-Variante).
  // Im 'responsive'-Layout ist der Parent (page-main) bereits scrollbar — kein zweites Scroll-Container.
  // Im 'responsive'-Layout liegt der Inhalt direkt auf dem Page-Hintergrund (keine eigene Card),
  // damit nicht zwei verschachtelte Container-Ebenen entstehen.
  const containerClasses = layout === 'responsive'
    ? 'flex flex-col gap-4'
    : 'flex h-full flex-col gap-4 overflow-y-auto rounded-lg border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-950';

  // SuggestLoader wird jetzt am Page-Level (`page.tsx`) gerendert — als Modal-Overlay
  // muss er die ganze Seite abdecken, nicht nur das Panel. `isSuggesting` kommt als Prop.
  return (
    <aside className={cn(containerClasses, className)}>
      <header>
        <div className="flex items-center gap-2">
          <h2 className="text-lg font-bold tracking-tight text-zinc-900 dark:text-zinc-100">Briefing</h2>
          <InfoTip>
            Diese Inputs füttern den AI-Generator. Je präziser, desto besser die Mail.
            Pflichtfelder: <strong>Name, Angebot, Schmerzpunkt, Workspace</strong>.
          </InfoTip>
        </div>
        {feedback && (
          <div className="mt-3 flex items-start gap-2 rounded-md border border-pink-200 bg-pink-50 px-3 py-2 text-xs text-[#ff006a] dark:border-pink-900/50 dark:bg-pink-950/30 dark:text-pink-200">
            <Sparkles className="mt-0.5 h-3.5 w-3.5 shrink-0" />
            <div className="flex-1">
              {feedback.filledKeys.length > 0 ? (
                <>
                  <div className="font-medium">{feedback.filledKeys.length} Feld{feedback.filledKeys.length === 1 ? '' : 'er'} aus Brief gefüllt</div>
                  <div className="mt-0.5 opacity-80">{feedback.filledKeys.join(', ')}</div>
                  {feedback.inferredKeys.length > 0 && (
                    <div className="mt-1.5 rounded border border-pink-300 bg-pink-100/50 px-2 py-1 text-[11px] dark:border-pink-700 dark:bg-pink-900/30">
                      <span className="font-medium">{feedback.inferredKeys.length} davon AI-Vermutung</span>
                      <span className="opacity-70"> — markierte Felder ({feedback.inferredKeys.join(', ')}) bitte prüfen oder anpassen.</span>
                    </div>
                  )}
                </>
              ) : (
                <div>Keine zusätzlichen Vorschläge — Brief war zu vage oder Felder bereits gefüllt.</div>
              )}
              {feedback.reasoning && <div className="mt-1 italic opacity-70">{feedback.reasoning}</div>}
              <div className="mt-0.5 text-[10px] opacity-50">{feedback.durationMs}ms</div>
            </div>
            <button onClick={() => onDismissFeedback?.()} className="opacity-50 hover:opacity-100" aria-label="schließen">
              <X className="h-3.5 w-3.5" />
            </button>
          </div>
        )}
        {suggestError && (
          <div className="mt-3 flex items-start gap-2 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">
            <span className="font-medium">Fehler:</span>
            <span className="flex-1">{suggestError}</span>
            <button onClick={() => onDismissError?.()} className="opacity-50 hover:opacity-100" aria-label="schließen">
              <X className="h-3.5 w-3.5" />
            </button>
          </div>
        )}
      </header>

      {/* ─── Mode-Switcher ─────────────────────────────────────────────
           Einzige UX-Entscheidung oben: "Ich habe einen fertigen Text" (preserve)
           oder "KI soll für mich schreiben" (rewrite). Intent wird synchron mitgesetzt
           für DB-Backwards-Compat. Sprache rendert kompakt rechts daneben. */}
      <ModeSwitcher
        mode={b.mode}
        language={b.language}
        onModeChange={(nextMode) => {
          // intent synchron zum Mode: preserve → 'text', rewrite → 'idea'.
          // Auto-Suggest beim Wechsel nach preserve mit gefülltem Editor-Text:
          // existierender handleSuggest-Flow füllt offer/painPoint aus briefHtml.
          const nextIntent: BriefingIntent = nextMode === 'preserve' ? 'text' : 'idea';
          onChange({ ...b, mode: nextMode, intent: nextIntent });
          if (
            nextMode === 'preserve' &&
            (briefText ?? '').trim().length >= 100 &&
            (!b.offer?.trim() || !b.painPoint?.trim()) &&
            onSuggest
          ) {
            // Debounce nicht nötig — onChange ist synchron, ein doppelter Call beim
            // schnellen Hin-und-Her wird vom Backend-Existing-Schutz idempotent gemacht.
            onSuggest();
          }
        }}
        onLangChange={(l) => update('language', l)}
      />

      {/* Template-Picker — gefiltert auf aktive Workspace. Wenn keine vorhanden → Section nicht gerendert. */}
      <TemplatePicker
        templates={templates ?? []}
        activeBrandSlug={b.workspaceSlug}
        onLoad={(id) => onLoadTemplate?.(id)}
        onDelete={(id) => onDeleteTemplate?.(id)}
      />

      {/* Intent='text' → Editor-Hint: Text liegt in Tab 1, beim Generieren wird er 1:1 übernommen */}
      {effectiveIntent === 'text' && (
        <TextReadyHint
          briefHasContent={briefHasContent}
          onJumpToEditor={onJumpToEditor}
        />
      )}

      {/* Intent='idea' → Webseiten-Scrape ist optional: URL eingeben → Briefing-Felder + Bilder
           werden vorbefüllt. Wenn User keine URL hat, einfach leer lassen und Felder manuell befüllen. */}
      {onScrapeUrl && effectiveIntent === 'idea' && (
        <WebsiteScrapePanel
          urlValue={b.scrapedUrl ?? ''}
          onUrlChange={(v) => update('scrapedUrl', v || undefined)}
          isScraping={isScraping}
          isImportingImage={isImportingImage}
          scrapeResult={scrapeResult}
          selectedHeroUrl={b.heroImageUrl}
          onScrape={() => b.scrapedUrl && onScrapeUrl(b.scrapedUrl)}
          onSelectImage={(url) => onSelectScrapedImage?.(url)}
          onClearHero={() => update('heroImageUrl', undefined)}
        />
      )}

      {/* Intent='idea' → KI-Brief in Editor schreiben.
           Bei 'text' versteckt — der User hat seinen Text bereits selbst getippt. */}
      {onDraftBrief && effectiveIntent === 'idea' && (
        <DraftBriefBlock
          ready={isBriefingReady(b)}
          isDrafting={isDraftingBrief}
          error={draftBriefError ?? null}
          onDraft={onDraftBrief}
          onDismissError={onDismissDraftBriefError}
        />
      )}

      {/* KI-Suggest-Banner entfernt — Single-Source-of-Truth ist jetzt der Footer-CTA
           (siehe `BuilderFooter` in page.tsx). Der Pink-Button im Footer morpht je nach
           Briefing-Zustand zwischen "KI-Vorschlag erzeugen" und "Marketing-Email generieren". */}

      <div className={fieldsWrapperClass}>
      {/*
        Kampagnen-Name wird oben in der Editor-Header-h1 inline editiert (Klick → Input,
        Auto-Save). Hier kein doppeltes Feld mehr — Pflicht-Validation bleibt im Footer-CTA.
      */}

      <Field
        label="Workspace"
        info="Bestimmt Logo, Farben, Footer, Sender-Domain und Workspace-Voice. Konfigurierbar in den Einstellungen."
      >
        <select
          value={b.workspaceSlug}
          onChange={(e) => update('workspaceSlug', e.target.value)}
          className="w-full rounded-md border border-zinc-200 bg-white px-3 py-2 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"
        >
          <option value="">— Workspace auswählen —</option>
          {workspaces.map((br) => <option key={br.slug} value={br.slug}>{br.name}</option>)}
        </select>
      </Field>

      {senders && senders.length > 0 && onSelectSender && (
        <SenderOverride
          senders={senders}
          selectedSenderEmail={selectedSenderEmail}
          onSelectSender={onSelectSender}
        />
      )}

      {/*
        offer + painPoint sind im rewrite-Mode Pflichtfelder (KI braucht den Inhalt zum
        Schreiben). Im preserve-Mode sind sie OPTIONAL — der Mailtext kommt aus dem Editor,
        das LLM extrahiert Subject/Preheader/Hero-Headline direkt aus briefHtml.
        Deshalb rendern wir sie dort in einer collapsible "Optional: Kontext für KI"-Section
        mit einem KI-Suggest-Button, der die Felder aus dem Editor-Text vorbefüllt.
      */}
      {b.mode === 'rewrite' ? (
        <>
          <Field
            className={wideField}
            label="Angebot"
            info="Was wird konkret beworben? Wird zum 'roten Faden' der ganzen Mail. Beispiel: '15% Code auf alle Premium-Abos für 90 Tage Reaktivierungs-Lift'."
            inferred={inferredFields.has('offer')}
          >
            <Textarea value={b.offer ?? ''} onChange={(v) => update('offer', v)} placeholder="z.B. 15% Code auf alle Premium-Abos für 90 Tage Reaktivierungs-Lift" rows={3} />
          </Field>

          <Field
            className={wideField}
            label="Schmerzpunkt"
            info="Welches Problem löst dein Angebot? Die KI verwendet das in der Hero-Story um Empathie aufzubauen ('Sie kennen das Problem...')."
            inferred={inferredFields.has('painPoint')}
          >
            <Textarea value={b.painPoint ?? ''} onChange={(v) => update('painPoint', v)} placeholder="Welches Problem löst Ihr Angebot konkret? — wird in der Hero-Story verwendet" rows={3} />
          </Field>
        </>
      ) : (
        <PreserveContextOptional
          offer={b.offer ?? ''}
          painPoint={b.painPoint ?? ''}
          onOfferChange={(v) => update('offer', v)}
          onPainPointChange={(v) => update('painPoint', v)}
          inferredOffer={inferredFields.has('offer')}
          inferredPainPoint={inferredFields.has('painPoint')}
          canSuggest={(briefText ?? '').trim().length >= 100 && !!onSuggest}
          isSuggesting={isSuggesting}
          onSuggest={onSuggest}
          wideClass={wideField}
        />
      )}

      <Field
        icon={<MousePointerClick className="h-4 w-4" />}
        label="Primärer CTA"
        info="Der Haupt-Klick-Auslöser. Button-Text wird im Body groß gerendert. URL ist optional — ohne URL wird kein Button gerendert (kannst du nachträglich per Re-Render einfügen)."
        inferred={inferredFields.has('ctaPrimaryText') || inferredFields.has('ctaPrimaryUrl')}
      >
        <Input value={b.ctaPrimaryText} onChange={(v) => update('ctaPrimaryText', v)} placeholder="Button-Text" />
        <Input value={b.ctaPrimaryUrl} onChange={(v) => update('ctaPrimaryUrl', v)} placeholder="https://…  oder  {{LOGIN_LINK}}" type="text" />

        {/* Login-Link-Quick-Action — One-Click Setter für den 95%-Fall.
            type="text" oben statt "url" damit das Browser-URL-Validator-Pattern den
            Platzhalter-Wert {{LOGIN_LINK}} nicht als invalide URL ablehnt. */}
        <div className="flex 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="flex-1">
            <p className="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={() => update('ctaPrimaryUrl', '{{LOGIN_LINK}}')}
              disabled={b.ctaPrimaryUrl === '{{LOGIN_LINK}}'}
              className={cn(
                'mt-1.5 inline-flex items-center gap-1 rounded px-2 py-1 text-[11px] font-medium transition',
                b.ctaPrimaryUrl === '{{LOGIN_LINK}}'
                  ? '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',
              )}
            >
              {b.ctaPrimaryUrl === '{{LOGIN_LINK}}'
                ? '✓ Login-Link aktiv'
                : '+ Login-Link als CTA einsetzen'}
            </button>
            <p className="mt-1 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>

        <p className="text-[11px] leading-relaxed text-zinc-500 dark:text-zinc-400">
          Weitere Platzhalter:{' '}
          <code className="rounded bg-zinc-100 px-1 py-0.5 text-[10px] font-mono text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">{'{{RECIPIENT_ID}}'}</code>{' '}
          <code className="rounded bg-zinc-100 px-1 py-0.5 text-[10px] font-mono text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">{'{{CRM_ID}}'}</code>{' '}
          <code className="rounded bg-zinc-100 px-1 py-0.5 text-[10px] font-mono text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">{'{{EMAIL}}'}</code>{' '}
          <code className="rounded bg-zinc-100 px-1 py-0.5 text-[10px] font-mono text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">{'{{FIRST_NAME}}'}</code>
          {' '}+ alle CRM-Spalten der gewählten Datenquelle. Beispiel:{' '}
          <code className="rounded bg-zinc-100 px-1 py-0.5 text-[10px] font-mono text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">https://danke.de/?id={'{{CRM_ID}}'}&part=hp</code>
        </p>
      </Field>

      </div>

      {/* ─── ERWEITERT — Optional-Toggles ────────────────────────────── */}
      <div className="flex flex-col gap-2 pt-2">
        <h3 className="flex items-center gap-2 text-sm font-bold tracking-tight text-zinc-900 dark:text-zinc-100">
          <Settings2 className="h-4 w-4 text-zinc-500" />
          Erweitert
          <span className="text-xs font-normal text-zinc-400">— optional, je nach Use-Case</span>
        </h3>

        <OptionalSection
          icon={<MousePointerClick className="h-4 w-4" />}
          title="Sekundärer CTA"
          subtitle="Nebenlink unter dem Haupt-Button — z.B. 'Mehr erfahren' oder 'Termin vereinbaren'"
          enabled={b.optional?.secondaryCta ?? false}
          onToggle={(v) => update('optional', { ...b.optional, secondaryCta: v })}
          compactWhenOff
        >
          <div className="flex flex-col gap-2">
            <Input value={b.ctaSecondaryText ?? ''} onChange={(v) => update('ctaSecondaryText', v || undefined)} placeholder="z.B. Mehr erfahren" />
            {/* type="text" statt "url" damit der Platzhalter {{LOGIN_LINK}} nicht vom
                Browser-URL-Validator als invalid markiert wird — Worker substituiert
                den Tag pro Empfänger zur echten Login-URL. */}
            <Input
              value={b.ctaSecondaryUrl ?? ''}
              onChange={(v) => update('ctaSecondaryUrl', v || undefined)}
              placeholder="https://…  oder  {{LOGIN_LINK}}"
              type="text"
            />

            {/* Login-Link-Quick-Action — symmetrisch zur primären CTA-Section oben (Z. 791-816)
                und zur Variante im Strategy-Editor (page.tsx LoginLinkHint). Gleiche Optik,
                damit der User die Komponente in beiden Tabs sofort wiedererkennt. */}
            <div className="flex 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="flex-1">
                <p className="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={() => update('ctaSecondaryUrl', '{{LOGIN_LINK}}')}
                  disabled={b.ctaSecondaryUrl === '{{LOGIN_LINK}}'}
                  className={cn(
                    'mt-1.5 inline-flex items-center gap-1 rounded px-2 py-1 text-[11px] font-medium transition',
                    b.ctaSecondaryUrl === '{{LOGIN_LINK}}'
                      ? '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',
                  )}
                >
                  {b.ctaSecondaryUrl === '{{LOGIN_LINK}}'
                    ? '✓ Login-Link aktiv'
                    : '+ Login-Link als CTA einsetzen'}
                </button>
                <p className="mt-1 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>
          </div>
        </OptionalSection>

        <OptionalSection
          icon={<LayoutTemplate className="h-4 w-4" />}
          title="Eigene Sektionen"
          subtitle="Benannte MJML- oder HTML-Blöcke, die nach dem Generate an einer wählbaren Position eingefügt werden."
          enabled={b.optional?.extraSections ?? false}
          onToggle={(v) => update('optional', { ...b.optional, extraSections: v })}
        >
          <CustomSectionEditor
            workspaceSlug={b.workspaceSlug}
            value={b.extraSections ?? []}
            onChange={(v) => update('extraSections', v)}
          />
        </OptionalSection>

        <OptionalSection
          icon={<ImageIcon className="h-4 w-4" />}
          title="Hero-Bild"
          subtitle="KI generiert ein passendes Header-Bild via fal.ai · Standard: AN"
          enabled={b.heroEnabled}
          onToggle={(v) => update('heroEnabled', v)}
        >
          <div className="flex flex-col gap-2">
            <Textarea
              value={b.heroBriefHint ?? ''}
              onChange={(v) => update('heroBriefHint', v || undefined)}
              placeholder="Optional: Hinweis zur Bildkomposition (z.B. 'Junge Frau am Laptop im hellen Coworking, Spätsommer-Licht'). Leer = AI entscheidet aus Strategy."
              rows={2}
            />
            <button
              type="button"
              onClick={handleSuggestHero}
              disabled={isSuggestingHero || (!b.offer && !b.painPoint && !briefText)}
              title="KI schlägt einen Bildkompositions-Hinweis vor basierend auf deinem Briefing"
              className={cn(
                'inline-flex items-center gap-1.5 self-start rounded-md border px-2.5 py-1 text-[11px] font-medium transition',
                'border-pink-200 bg-pink-50 text-[#ff006a] hover:bg-pink-100',
                'dark:border-pink-900/50 dark:bg-pink-950/30 dark:text-pink-300',
                'disabled:cursor-not-allowed disabled:opacity-40',
              )}
            >
              {isSuggestingHero ? <Loader2 className="h-3 w-3 animate-spin" /> : <Wand2 className="h-3 w-3" />}
              {isSuggestingHero ? 'Analysiert …' : 'KI-Vorschlag für Bild'}
            </button>

            {/* Hero-Höhe + Crop-Position. Default = Auto (natürliche Höhe). */}
            <HeroCropControl
              value={b.heroHeight}
              position={b.heroCropPosition}
              onChange={(v) => update('heroHeight', v)}
              onChangePosition={(p) => update('heroCropPosition', p)}
            />
          </div>
        </OptionalSection>

        {/*
          UTM-Tracking, Champion-Test und Zusätzliche Sections wurden in den
          Versand-Tab (UTM, Champion) bzw. AdMail/EditPanel (Sections) verschoben.
          Sie betreffen thematisch nicht das Content-Briefing, sondern das Send-
          Verhalten bzw. die Mail-Bearbeitung nach dem ersten Generate.
          State liegt weiter auf briefing.utm / briefing.championTest /
          briefing.extraSections — Schema unverändert, nur UI-Render-Ort verschoben.
        */}

      </div>

      {/* Generate-Button + Pflichtfelder-Hinweis leben jetzt in der fixed Footer-Bar (page.tsx) */}
    </aside>
  );
}

/**
 * Hero-Bild-Crop-Steuerung mit Preset-Pills + freiem Pixel-Input + Crop-Position-Toggle.
 * Auto (= height undefined) lässt MJML die natürliche Bild-Höhe verwenden.
 * Bei gesetzter Höhe wendet `render-template` `object-fit:cover` mit der gewählten
 * `object-position` an (Default: center), damit das Bild gecroppt statt gestaucht wird.
 * Gmail/Apple Mail/iOS Mail respektieren das; Outlook fällt auf Stauchung zurück.
 */
function HeroCropControl({
  value,
  position,
  onChange,
  onChangePosition,
}: {
  value: number | undefined;
  position: 'top' | 'center' | 'bottom' | undefined;
  onChange: (v: number | undefined) => void;
  onChangePosition: (p: 'top' | 'center' | 'bottom' | undefined) => void;
}) {
  const presets: Array<{ label: string; value: number | undefined; hint: string }> = [
    { label: 'Auto', value: undefined, hint: 'Natürliche Bildhöhe (kann sehr hoch werden)' },
    { label: 'Klein', value: 200, hint: '200px — kompakter Banner-Look' },
    { label: 'Mittel', value: 280, hint: '280px — balanciert zwischen Body und Bild' },
    { label: 'Groß', value: 360, hint: '360px — prominent, aber noch nicht dominant' },
  ];
  // Crop-Position ist nur sinnvoll wenn eine Höhe gesetzt ist (sonst kein object-fit:cover).
  // Buttons werden visuell disabled wenn value undefined ist.
  const cropDisabled = value === undefined;
  const positions: Array<{ label: string; value: 'top' | 'center' | 'bottom'; hint: string }> = [
    { label: 'Oben', value: 'top', hint: 'object-position: top — oberer Bildteil sichtbar' },
    { label: 'Mitte', value: 'center', hint: 'object-position: center — mittig (Default)' },
    { label: 'Unten', value: 'bottom', hint: 'object-position: bottom — unterer Bildteil sichtbar' },
  ];
  const activePosition = position ?? 'center';
  return (
    <div className="mt-1 flex flex-col gap-1.5 rounded-md border border-zinc-200 bg-white p-2 dark:border-zinc-700 dark:bg-zinc-900">
      <div className="text-[10px] font-semibold uppercase tracking-wider text-zinc-500">
        Hero-Höhe <span className="font-normal normal-case text-zinc-400">— Crop statt Stauchung in Gmail/Apple Mail</span>
      </div>
      <div className="flex flex-wrap items-center gap-1">
        {presets.map((p) => {
          const isActive = value === p.value;
          return (
            <button
              key={p.label}
              type="button"
              onClick={() => onChange(p.value)}
              title={p.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',
              )}
            >
              {p.label}
            </button>
          );
        })}
        <div className="ml-auto flex items-center gap-1">
          <input
            type="number"
            min={80}
            max={800}
            step={10}
            value={value ?? ''}
            onChange={(e) => {
              const n = e.target.value === '' ? undefined : Number(e.target.value);
              onChange(n && n > 0 ? n : undefined);
            }}
            placeholder="px"
            className="w-16 rounded border border-zinc-200 bg-white px-1.5 py-0.5 text-[11px] focus:border-zinc-900 focus:outline-none dark:border-zinc-700 dark:bg-zinc-950"
          />
          <span className="text-[10px] text-zinc-400">px</span>
        </div>
      </div>
      {/* Crop-Position: nur sichtbar/aktiv wenn Höhe gesetzt — sonst greift kein object-position. */}
      <div className="mt-1 flex items-center gap-1">
        <span className="text-[10px] font-semibold uppercase tracking-wider text-zinc-500">Crop</span>
        {positions.map((p) => {
          const isActive = !cropDisabled && activePosition === p.value;
          return (
            <button
              key={p.value}
              type="button"
              onClick={() => onChangePosition(p.value)}
              disabled={cropDisabled}
              title={cropDisabled ? 'Erst eine Höhe setzen, dann Crop-Position wählen' : p.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',
              )}
            >
              {p.label}
            </button>
          );
        })}
      </div>
    </div>
  );
}

/**
 * Toggle-Card für Optional-Sections.
 * Zugeklappt: nur Header (~52px). Aufgeklappt: Header + Inhalt darunter.
 * Toggle-State wird vom Parent kontrolliert (controlled component).
 */
export function OptionalSection({
  icon, title, subtitle, enabled, onToggle, children, compactWhenOff = false,
}: {
  icon: React.ReactNode;
  title: string;
  subtitle: string;
  enabled: boolean;
  onToggle: (next: boolean) => void;
  children: React.ReactNode;
  /**
   * Wenn true UND enabled=false: rendert einen kompakten One-Liner statt der vollen Card.
   * Spart vertikalen Platz für selten genutzte Optionen, ohne den Switch zu verstecken.
   */
  compactWhenOff?: boolean;
}) {
  // Kompakter Off-Mode: schmale Zeile mit Icon + Title + Switch, kein Subtitle.
  if (compactWhenOff && !enabled) {
    return (
      <button
        type="button"
        onClick={() => onToggle(true)}
        className="flex w-full items-center gap-2 rounded-md border border-zinc-200/60 bg-transparent px-3 py-1.5 text-left text-zinc-500 transition hover:border-zinc-300 hover:bg-zinc-50 dark:border-zinc-800/60 dark:hover:border-zinc-700 dark:hover:bg-zinc-900/40"
      >
        <span className="shrink-0 text-zinc-400">{icon}</span>
        <span className="flex-1 text-xs font-medium">{title}</span>
        <ToggleSwitch checked={false} />
      </button>
    );
  }

  return (
    <div
      className={cn(
        'rounded-lg border transition',
        enabled
          ? 'border-zinc-300 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900'
          : 'border-zinc-200 bg-zinc-50/50 dark:border-zinc-800 dark:bg-zinc-900/30',
      )}
    >
      <button
        type="button"
        onClick={() => onToggle(!enabled)}
        className="flex w-full items-center gap-3 px-3.5 py-2.5 text-left"
      >
        <span className={cn('shrink-0 transition', enabled ? 'text-zinc-700 dark:text-zinc-300' : 'text-zinc-400')}>
          {icon}
        </span>
        <div className="flex-1 min-w-0">
          <div className={cn('text-sm font-semibold', enabled ? 'text-zinc-900 dark:text-zinc-100' : 'text-zinc-600 dark:text-zinc-400')}>
            {title}
          </div>
          <div className="mt-0.5 truncate text-[11px] text-zinc-500 dark:text-zinc-500">{subtitle}</div>
        </div>
        <ToggleSwitch checked={enabled} />
      </button>
      {enabled && (
        <div className="border-t border-zinc-200 px-3.5 py-3 dark:border-zinc-800">
          {children}
        </div>
      )}
    </div>
  );
}

export function ToggleSwitch({
  checked,
  onChange,
  ariaLabel,
}: {
  checked: boolean;
  onChange?: (next: boolean) => void;
  ariaLabel?: string;
}) {
  const visual = (
    <span
      className={cn(
        'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition',
        checked ? 'bg-[#ff006a]' : 'bg-zinc-300 dark:bg-zinc-700',
      )}
    >
      <span
        className={cn(
          'inline-block h-4 w-4 transform rounded-full bg-white shadow transition',
          checked ? 'translate-x-4' : 'translate-x-0.5',
        )}
      />
    </span>
  );

  // Standalone-Mode: eigenes <button>-Element mit role="switch", wenn ein onChange-Handler
  // gegeben ist. Dadurch ist der Switch in Headern (z.B. EditGroup.headerSlot) klickbar
  // ohne umschliessenden Button. Stop-Propagation, damit der Click nicht durch einen
  // klickbaren Parent geht.
  if (onChange) {
    return (
      <button
        type="button"
        role="switch"
        aria-checked={checked}
        aria-label={ariaLabel}
        onClick={(e) => {
          e.stopPropagation();
          onChange(!checked);
        }}
        className="cursor-pointer rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ff006a]/40"
      >
        {visual}
      </button>
    );
  }

  // Legacy-Mode: rein visuelles Element, role="switch" am <span> — wird durch einen
  // umschliessenden <button> (z.B. in OptionalSection) bedient.
  return (
    <span role="switch" aria-checked={checked} aria-label={ariaLabel}>
      {visual}
    </span>
  );
}

function Field({ icon, label, children, inferred, className, info }: { icon?: React.ReactNode; label: string; children: React.ReactNode; inferred?: boolean; className?: string; info?: string }) {
  return (
    <div className={cn('flex flex-col gap-2', className)}>
      <label className="flex items-center gap-1.5 text-sm font-semibold text-zinc-800 dark:text-zinc-200">
        {icon && <span className="text-zinc-500 dark:text-zinc-400">{icon}</span>}
        <span>{label}</span>
        {info && <InfoTip>{info}</InfoTip>}
        {inferred && (
          <span
            title="AI-Vermutung — bitte prüfen oder anpassen"
            className="inline-flex items-center gap-0.5 rounded bg-pink-100 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-[#ff006a] dark:bg-pink-950/50 dark:text-pink-300"
          >
            <Sparkles className="h-2.5 w-2.5" /> AI-Vermutung
          </span>
        )}
      </label>
      <div className={cn('flex flex-col gap-2', inferred && 'ring-2 ring-pink-200 ring-offset-1 rounded-md dark:ring-pink-900/50')}>{children}</div>
    </div>
  );
}

/**
 * Pure-CSS Hover-Tooltip — kein JS, kein Radix.
 * Nutzt `group-hover:` und `pointer-events-none` damit der Tooltip nicht selbst hover-flickert.
 * Auf Touch-Devices triggert Tap durch `focus-visible:` ebenfalls.
 */
function InfoTip({ children }: { children: React.ReactNode }) {
  return (
    <span className="group relative inline-flex shrink-0">
      <button
        type="button"
        tabIndex={0}
        aria-label="Mehr Info"
        className="rounded-full p-0.5 text-zinc-400 transition hover:bg-zinc-100 hover:text-zinc-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
      >
        <Info className="h-3.5 w-3.5" />
      </button>
      <span
        role="tooltip"
        className="pointer-events-none invisible absolute left-1/2 top-full z-30 mt-1 w-64 -translate-x-1/2 rounded-md bg-zinc-900 px-3 py-2 text-[11px] font-normal leading-relaxed text-white shadow-lg group-hover:visible group-focus-within:visible dark:bg-zinc-100 dark:text-zinc-900"
      >
        {children}
      </span>
    </span>
  );
}

/**
 * Confirmation-Card für den Text-1:1-Workflow: User hat Text in Tab 1, beim Generate
 * wird er wortwörtlich übernommen (mode=preserve). Springt-zurück-zum-Editor-Button
 * für letzte Edits, Hinweis falls Editor noch leer ist (Empty-State führt zurück).
 */
function TextReadyHint({
  briefHasContent,
  onJumpToEditor,
}: {
  briefHasContent: boolean;
  onJumpToEditor?: () => void;
}) {
  if (!briefHasContent) {
    return (
      <div className="rounded-md border border-amber-200 bg-amber-50 p-3 dark:border-amber-900/50 dark:bg-amber-950/20">
        <div className="mb-1 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-amber-700 dark:text-amber-300">
          <Info className="h-3 w-3" />
          Editor noch leer
        </div>
        <div className="flex items-start gap-2">
          <div className="flex-1 text-[11px] leading-relaxed text-amber-900 dark:text-amber-200">
            Du hast „Text liegt fertig vor“ gewählt — der Mailtext gehört in den Editor in Tab 1.
            Wechsel zurück und tippe deinen Text dort ein, bevor du das Briefing fertigstellst.
          </div>
          {onJumpToEditor && (
            <button
              type="button"
              onClick={onJumpToEditor}
              className="inline-flex shrink-0 items-center gap-1.5 rounded-md bg-amber-600 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-amber-700"
            >
              Zum Editor <ArrowRight className="h-3.5 w-3.5" />
            </button>
          )}
        </div>
      </div>
    );
  }
  return (
    <div className="rounded-md border border-emerald-200 bg-emerald-50 p-3 dark:border-emerald-900/40 dark:bg-emerald-950/20">
      <div className="mb-1 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-emerald-700 dark:text-emerald-300">
        <CheckCircle2 className="h-3 w-3" />
        Dein Text steht in Tab 1
      </div>
      <div className="flex items-start gap-2">
        <div className="flex-1 text-[11px] leading-relaxed text-emerald-900 dark:text-emerald-200">
          Beim Generieren wird dein Text 1:1 übernommen — KI fügt nur Anrede, Footer und Format hinzu
          (Modus „Text 1:1“). Fülle hier nur noch Briefing-Daten und Sections aus.
        </div>
        {onJumpToEditor && (
          <button
            type="button"
            onClick={onJumpToEditor}
            className="inline-flex shrink-0 items-center gap-1.5 rounded-md border border-emerald-300 bg-white px-3 py-1.5 text-xs font-medium text-emerald-800 transition hover:bg-emerald-50 dark:border-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-200"
          >
            Editor öffnen
          </button>
        )}
      </div>
    </div>
  );
}

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

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

/**
 * Template-Picker am oberen BriefingPanel-Rand. Zeigt nur Templates der aktiven Workspace.
 * Wenn keine vorhanden → rendert nichts (wir wollen kein leeres Element auf Newcomer-Briefings).
 *
 * UX: kompakte Pills-Liste, jeder Template-Eintrag ist klickbar zum Laden, mit dezentem
 * × beim Hover zum Löschen. Pink-Border um die Section, damit klar ist „das ist ein Shortcut".
 */
/**
 * Webseiten-Scrape: User gibt eine URL ein, der Server extrahiert Title/Description/Bilder,
 * die KI füllt aus dem Text die Briefing-Felder, und der User kann optional ein Bild als Hero übernehmen.
 *
 * Visueller Aufbau:
 *  1. URL-Input + Scrape-Button (Pink-CTA)
 *  2. Nach erfolgreichem Scrape: Title-Card + Image-Grid (5 Spalten)
 *  3. Klick auf Bild → /api/import-image-from-url → lokale URL → in briefing.heroImageUrl
 *  4. Aktuell-gewähltes Hero-Bild kriegt einen Pink-Border + Häkchen-Overlay
 */
function WebsiteScrapePanel({
  urlValue,
  onUrlChange,
  isScraping,
  isImportingImage,
  scrapeResult,
  selectedHeroUrl,
  onScrape,
  onSelectImage,
  onClearHero,
}: {
  urlValue: string;
  onUrlChange: (v: string) => void;
  isScraping: boolean;
  isImportingImage: boolean;
  scrapeResult: ScrapedSummary | null;
  selectedHeroUrl: string | undefined;
  onScrape: () => void;
  onSelectImage: (originalImageUrl: string) => void;
  onClearHero: () => void;
}) {
  const canScrape = !!urlValue && !isScraping;
  return (
    <div className="rounded-md border border-pink-200 bg-pink-50/40 p-3 dark:border-pink-900/50 dark:bg-pink-950/20">
      <div className="mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-zinc-500">
        <Globe className="h-3 w-3 text-[#ff006a]" />
        Webseite analysieren
        <span className="font-normal normal-case text-zinc-400">— scrapen + Briefing automatisch füllen</span>
      </div>

      <div className="flex gap-2">
        <input
          type="url"
          value={urlValue}
          onChange={(e) => onUrlChange(e.target.value)}
          placeholder="https://beispiel.de/landing-page"
          onKeyDown={(e) => {
            if (e.key === 'Enter' && canScrape) {
              e.preventDefault();
              onScrape();
            }
          }}
          className="flex-1 rounded-md border border-zinc-200 bg-white px-2.5 py-1.5 text-xs focus:border-zinc-900 focus:outline-none dark:border-zinc-700 dark:bg-zinc-900"
        />
        <button
          type="button"
          onClick={onScrape}
          disabled={!canScrape}
          className="inline-flex shrink-0 items-center gap-1.5 rounded-md bg-[#ff006a] px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-[#e60060] disabled:cursor-not-allowed disabled:opacity-40"
        >
          {isScraping ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Wand2 className="h-3.5 w-3.5" />}
          {isScraping ? 'Lade & analysiere …' : 'Scrapen + Briefing füllen'}
        </button>
      </div>

      {/* Result-Card: erscheint nach erfolgreichem Scrape */}
      {scrapeResult && (
        <div className="mt-3 space-y-2">
          {(scrapeResult.title || scrapeResult.description) && (
            <div className="rounded-md border border-zinc-200 bg-white p-2.5 dark:border-zinc-800 dark:bg-zinc-950">
              {scrapeResult.title && (
                <div className="text-[11px] font-semibold leading-snug text-zinc-900 dark:text-zinc-100">
                  {scrapeResult.title}
                </div>
              )}
              {scrapeResult.description && (
                <div className="mt-1 line-clamp-2 text-[10px] leading-relaxed text-zinc-600 dark:text-zinc-400">
                  {scrapeResult.description}
                </div>
              )}
            </div>
          )}

          {scrapeResult.images.length > 0 && (
            <div>
              <div className="mb-1.5 flex items-center justify-between">
                <div className="text-[10px] font-semibold uppercase tracking-wider text-zinc-500">
                  Bilder von der Seite ({scrapeResult.images.length})
                </div>
                {selectedHeroUrl && (
                  <button
                    type="button"
                    onClick={onClearHero}
                    className="inline-flex items-center gap-1 text-[10px] text-zinc-500 hover:text-red-600"
                  >
                    <X className="h-3 w-3" />
                    Hero-Auswahl zurücksetzen
                  </button>
                )}
              </div>
              <div className="grid max-h-72 grid-cols-3 gap-2 overflow-y-auto sm:grid-cols-4 lg:grid-cols-5">
                {scrapeResult.images.map((img, i) => {
                  // Auswahl-Indikator: wir können die ORIGINAL-URL nicht 1:1 vergleichen mit der lokalen
                  // heroImageUrl (die ist umgemappt nach Import). Stattdessen markieren wir das aktuell-
                  // klickte Bild via einer Index-Heuristik nicht — sondern: wir zeigen das Häkchen nur wenn
                  // KEIN Hero gesetzt ist NICHT, sondern wir zeigen nur das Original-Image-URL-Match falls
                  // es noch nicht imported wurde. Nach Import zeigt der EditPanel-Hero-Thumbnail die Auswahl.
                  return (
                    <button
                      key={`${img.url}-${i}`}
                      type="button"
                      onClick={() => onSelectImage(img.url)}
                      disabled={isImportingImage}
                      title={img.alt || img.url}
                      className="group relative aspect-[4/3] overflow-hidden rounded border border-zinc-200 bg-zinc-50 transition hover:border-[#ff006a] disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-700 dark:bg-zinc-900"
                    >
                      {/* eslint-disable-next-line @next/next/no-img-element */}
                      <img
                        src={img.url}
                        alt={img.alt}
                        loading="lazy"
                        className="h-full w-full object-cover transition group-hover:scale-105"
                        onError={(e) => {
                          // Fallback: ausgegraut bei Lade-Fehler (CORS / 404)
                          (e.currentTarget as HTMLImageElement).style.opacity = '0.2';
                        }}
                      />
                      <div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-black/0 opacity-0 transition group-hover:bg-black/30 group-hover:opacity-100">
                        <span className="rounded bg-white px-2 py-0.5 text-[10px] font-semibold text-[#ff006a]">
                          Als Hero
                        </span>
                      </div>
                    </button>
                  );
                })}
              </div>
            </div>
          )}

          {selectedHeroUrl && (
            <div className="flex items-start gap-2 rounded-md border border-emerald-200 bg-emerald-50 px-2.5 py-2 text-[11px] text-emerald-800 dark:border-emerald-900/50 dark:bg-emerald-950/30 dark:text-emerald-200">
              <CheckCircle2 className="mt-0.5 h-3.5 w-3.5 shrink-0" />
              <div className="flex-1">
                <div className="font-medium">Hero-Bild gewählt</div>
                <div className="mt-0.5 truncate text-[10px] opacity-70">{selectedHeroUrl}</div>
                <div className="mt-1 text-[10px] opacity-70">Beim Generate wird kein KI-Bild erzeugt — diese Datei wird verwendet.</div>
              </div>
            </div>
          )}
        </div>
      )}

      {isImportingImage && (
        <div className="mt-2 flex items-center gap-1.5 text-[11px] text-zinc-500">
          <Loader2 className="h-3 w-3 animate-spin" />
          Bild wird heruntergeladen …
        </div>
      )}
    </div>
  );
}

/**
 * KI-Brief-Block — separater zweiter Schritt nach Scrapen + Briefing füllen.
 * Aus den Briefing-Pflichtfeldern (campaignName, offer, painPoint, ctaPrimaryText) plus optional
 * dem letzten Scrape-Kontext schreibt die KI einen Marketing-Brief und setzt ihn in den TipTap-Editor
 * (Tab 1). Disabled solange Briefing nicht ready ist; Tooltip führt den User zum nächsten Schritt.
 */
function DraftBriefBlock({
  ready,
  isDrafting,
  error,
  onDraft,
  onDismissError,
}: {
  ready: boolean;
  isDrafting: boolean;
  error: string | null;
  onDraft: () => void;
  onDismissError?: () => void;
}) {
  const canDraft = ready && !isDrafting;
  return (
    <div className="rounded-md border border-pink-200 bg-pink-50/40 p-3 dark:border-pink-900/50 dark:bg-pink-950/20">
      <div className="mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-zinc-500">
        <Sparkles className="h-3 w-3 text-[#ff006a]" />
        KI-Brief schreiben
        <span className="font-normal normal-case text-zinc-400">— Marketing-Brief in Editor (Tab 1) erzeugen</span>
      </div>

      <div className="flex items-start gap-2">
        <div className="flex-1 text-[11px] leading-relaxed text-zinc-600 dark:text-zinc-400">
          Aus deinem Briefing schreibt die KI einen vorformulierten Marketing-Brief
          (Schmerzpunkt → Lösung → Vorteile → CTA → P.S.) direkt in den Editor in Tab 1.
          Du kannst ihn dort noch nachbearbeiten, bevor du <span className="font-medium">{'„Generate“'}</span> drückst.
        </div>
        <button
          type="button"
          onClick={onDraft}
          disabled={!canDraft}
          title={!ready ? 'Erst Briefing-Pflichtfelder ausfüllen (Kampagne, Angebot, Schmerzpunkt, Workspace)' : undefined}
          className="inline-flex shrink-0 items-center gap-1.5 rounded-md bg-[#ff006a] px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-[#e60060] disabled:cursor-not-allowed disabled:opacity-40"
        >
          {isDrafting ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Sparkles className="h-3.5 w-3.5" />}
          {isDrafting ? 'Schreibt …' : 'KI-Brief in Editor schreiben'}
        </button>
      </div>

      {!ready && (
        <div className="mt-2 flex items-start gap-1.5 text-[10px] text-zinc-500">
          <Info className="mt-0.5 h-3 w-3 shrink-0" />
          Pflichtfelder fehlen: Kampagne, Angebot, Schmerzpunkt und Workspace. Scrapen + Briefing füllen oder manuell ausfüllen.
        </div>
      )}

      {error && (
        <div className="mt-2 flex items-start gap-2 rounded-md border border-red-200 bg-red-50 px-2.5 py-2 text-[11px] text-red-800 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-300">
          <span className="font-medium">Fehler:</span>
          <span className="flex-1">{error}</span>
          {onDismissError && (
            <button onClick={onDismissError} className="opacity-50 hover:opacity-100" aria-label="schließen">
              <X className="h-3.5 w-3.5" />
            </button>
          )}
        </div>
      )}
    </div>
  );
}

function TemplatePicker({
  templates,
  activeBrandSlug,
  onLoad,
  onDelete,
}: {
  templates: BriefingTemplateSummary[];
  activeBrandSlug: string;
  onLoad: (templateId: string) => void;
  onDelete: (templateId: string) => void;
}) {
  // Nur Templates der aktiven Workspace anzeigen — andere passen nicht zur Configuration
  const filtered = activeBrandSlug
    ? templates.filter((t) => t.workspaceSlug === activeBrandSlug)
    : templates;
  if (filtered.length === 0) return null;

  return (
    <div className="rounded-md border border-pink-200 bg-pink-50/40 p-2.5 dark:border-pink-900/50 dark:bg-pink-950/20">
      <div className="mb-1.5 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-zinc-500">
        <LayoutTemplate className="h-3 w-3 text-[#ff006a]" />
        Template laden
        <span className="font-normal normal-case text-zinc-400">— vorausfüllen mit gespeicherter Konfiguration</span>
      </div>
      <div className="flex flex-wrap gap-1.5">
        {filtered.map((t) => (
          <div key={t.id} className="group relative">
            <button
              type="button"
              onClick={() => onLoad(t.id)}
              className="inline-flex items-center gap-1.5 rounded-full border border-zinc-200 bg-white px-2.5 py-1 text-[11px] text-zinc-700 transition hover:border-[#ff006a] hover:bg-pink-50 hover:text-[#ff006a] dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300"
              title={t.description ?? `Template laden: ${t.name}`}
            >
              <LayoutTemplate className="h-3 w-3 opacity-50 group-hover:opacity-100" />
              <span className="font-medium">{t.name}</span>
            </button>
            <button
              type="button"
              onClick={() => {
                if (confirm(`Template "${t.name}" löschen?`)) onDelete(t.id);
              }}
              className="absolute -right-1 -top-1 hidden h-4 w-4 items-center justify-center rounded-full bg-zinc-700 text-[9px] text-white hover:bg-red-600 group-hover:flex"
              aria-label={`Template ${t.name} löschen`}
              title="Template löschen"
            >
              <Trash2 className="h-2.5 w-2.5" />
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

/**
 * Preserve-Mode-Kontext-Block: offer + painPoint sind hier optional, weil das LLM
 * Subject/Preheader/Hero-Headline aus briefHtml extrahieren kann. Standardmäßig
 * eingeklappt — User kann ihn aufklappen, wenn er der KI gezielt Kontext geben will.
 * "KI aus Editor-Text extrahieren"-Button füllt beide Felder über den existierenden
 * Suggest-Flow (mit Existing-Schutz: bereits getippte Werte bleiben unverändert).
 */
function PreserveContextOptional({
  offer,
  painPoint,
  onOfferChange,
  onPainPointChange,
  inferredOffer,
  inferredPainPoint,
  canSuggest,
  isSuggesting,
  onSuggest,
  wideClass,
}: {
  offer: string;
  painPoint: string;
  onOfferChange: (v: string) => void;
  onPainPointChange: (v: string) => void;
  inferredOffer: boolean;
  inferredPainPoint: boolean;
  canSuggest: boolean;
  isSuggesting: boolean;
  onSuggest?: () => void;
  wideClass: string;
}) {
  const hasContent = !!offer.trim() || !!painPoint.trim();
  const [open, setOpen] = useState(hasContent || inferredOffer || inferredPainPoint);

  return (
    <div className={cn(wideClass, 'rounded-lg border border-dashed border-zinc-300 bg-zinc-50/40 dark:border-zinc-700 dark:bg-zinc-900/30')}>
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        className="flex w-full items-center gap-2 px-3 py-2 text-left"
      >
        <ChevronDown className={cn('h-4 w-4 shrink-0 text-zinc-400 transition', open ? 'rotate-0' : '-rotate-90')} />
        <div className="flex-1 min-w-0">
          <div className="text-sm font-medium text-zinc-700 dark:text-zinc-300">
            Optional: Kontext für die KI
          </div>
          <div className="text-[11px] text-zinc-500 dark:text-zinc-400">
            {hasContent
              ? 'Gesetzt — KI nutzt das für schärfere Subject-Varianten und das Hero-Bild. Editor-Text bleibt 1:1.'
              : 'Subject, Preheader und Hero-Headline werden aus dem Editor-Text extrahiert — diese Section darfst du leer lassen.'}
          </div>
        </div>
      </button>
      {open && (
        <div className="space-y-3 border-t border-zinc-200 px-3 py-3 dark:border-zinc-800">
          {onSuggest && (
            <div className="flex items-center justify-between gap-2">
              <span className="text-[11px] text-zinc-500 dark:text-zinc-400">
                Lass die KI die Felder aus deinem Editor-Text füllen — bereits getippte Werte bleiben.
              </span>
              <button
                type="button"
                onClick={onSuggest}
                disabled={!canSuggest || isSuggesting}
                title={!canSuggest ? 'Editor-Text zu kurz (≥ 100 Zeichen erforderlich)' : undefined}
                className="inline-flex items-center gap-1 rounded-md border border-pink-200 bg-pink-50 px-2.5 py-1.5 text-[11px] font-medium text-[#ff006a] transition hover:bg-pink-100 disabled:cursor-not-allowed disabled:opacity-50 dark:border-pink-900/50 dark:bg-pink-950/30 dark:hover:bg-pink-900/40"
              >
                {isSuggesting ? (
                  <>
                    <Loader2 className="h-3 w-3 animate-spin" /> KI denkt nach…
                  </>
                ) : (
                  <>
                    <Sparkles className="h-3 w-3" /> KI aus Editor-Text extrahieren
                  </>
                )}
              </button>
            </div>
          )}
          <Field
            label="Angebot"
            info="Optional im Preserve-Mode — die KI nimmt den Text aus dem Editor 1:1. Füll das Feld nur aus, wenn du gezielt Subject/Preheader steuern willst."
            inferred={inferredOffer}
          >
            <Textarea value={offer} onChange={onOfferChange} placeholder="z.B. 15% Code auf alle Premium-Abos" rows={2} />
          </Field>
          <Field
            label="Schmerzpunkt"
            info="Optional — wird in Subject/Preheader-Generierung berücksichtigt."
            inferred={inferredPainPoint}
          >
            <Textarea value={painPoint} onChange={onPainPointChange} placeholder="Welches Problem löst das Angebot konkret?" rows={2} />
          </Field>
        </div>
      )}
    </div>
  );
}

/**
 * Absender-Override mit Progressive Disclosure: In ~95% der Fälle reicht der Workspace-Default
 * (Worker resolved automatisch per companyId). Wer einen abweichenden Sender braucht
 * (z.B. workspace-übergreifender Account wie Richard Knöpfle), klickt den Link auf — dann
 * erscheint der Select. Default-Render ist ein einzeiliger Hint, kein eigenes Form-Field.
 *
 * Auto-expand wenn beim Mount bereits ein expliziter Sender gesetzt ist, damit der User
 * sieht, dass ein Override aktiv ist (sonst wirkt der Override unsichtbar).
 */
function SenderOverride({
  senders,
  selectedSenderEmail,
  onSelectSender,
}: {
  senders: Array<{ email: string; name: string; companyId?: number; note?: string }>;
  selectedSenderEmail: string;
  onSelectSender: (email: string) => void;
}) {
  const hasExplicit = !!selectedSenderEmail;
  // Derived state: expanded ist true wenn der User es manuell aufgeklappt hat ODER
  // ein expliziter Sender gesetzt ist. Vermeidet `setState-in-effect`-Anti-Pattern.
  const [userExpanded, setUserExpanded] = useState(false);
  const expanded = userExpanded || hasExplicit;

  if (!expanded) {
    return (
      <button
        type="button"
        onClick={() => setUserExpanded(true)}
        className="flex w-fit items-center gap-1.5 rounded text-[11px] font-medium text-zinc-500 transition hover:text-[#ff006a] dark:text-zinc-400 dark:hover:text-[#ff006a]"
      >
        <AtSign className="h-3 w-3" />
        Anderen Absender wählen?
        <span className="text-[10px] font-normal text-zinc-400">— Standard ist der Workspace-Default</span>
      </button>
    );
  }

  return (
    <Field
      label="Absender"
      info="Override des Workspace-Default-Postfachs. Leer-lassen oder 'Workspace-Default' wählen für den Standard."
    >
      <select
        value={selectedSenderEmail}
        onChange={(e) => onSelectSender(e.target.value)}
        className="w-full rounded-md border border-zinc-200 bg-white px-3 py-2 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"
      >
        <option value="">Workspace-Default</option>
        {senders.map((s) => (
          <option key={s.email} value={s.email}>
            {s.name} — {s.email}
          </option>
        ))}
      </select>
      {!hasExplicit && (
        <button
          type="button"
          onClick={() => setUserExpanded(false)}
          className="self-start text-[10px] text-zinc-400 transition hover:text-zinc-600 dark:hover:text-zinc-300"
        >
          ← Ausblenden, Workspace-Default nutzen
        </button>
      )}
    </Field>
  );
}
