import { useEffect, useMemo, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { ArrowLeft, FileSpreadsheet, Loader2, Save, Trash2 } from 'lucide-react'
import { api } from '../utils/api'
import { csvPreviewTdStyle } from '../utils/csvPreviewCells'

const MODULE_LABEL = {
  nutrition: 'Ernährung',
  weight: 'Gewicht',
  blood_pressure: 'Blutdruck',
  activity: 'Aktivität',
  sleep: 'Schlaf',
  vitals_baseline: 'Vitalwerte (Baseline)',
}

/** Vorschläge für Freitext „Quelleinheit“ (Volumen, Stück …); Ziel kommt aus dem Datenmodell. */
const CUSTOM_SOURCE_UNIT_HINTS = [
  'ml',
  'l',
  'cl',
  'dl',
  'µl',
  'mg',
  'µg',
  'Stück',
  'Portion',
]

/** Eine gemeinsame Funktion für alle nicht-Schlüssel-Zielfelder (pro-Feld-Mix = spätere Ausbaustufe). */
const ROW_AGG_OPS = [
  { value: 'sum', label: 'Summe' },
  { value: 'mean', label: 'Mittelwert' },
  { value: 'min', label: 'Minimum' },
  { value: 'max', label: 'Maximum' },
  { value: 'median', label: 'Median' },
  { value: 'first', label: 'Erster Wert (Reihenfolge in der Datei)' },
  { value: 'last', label: 'Letzter Wert (Reihenfolge in der Datei)' },
]

const NUMERIC_ROW_AGG = new Set(['sum', 'mean', 'min', 'max', 'median'])

/** Wenn mehrere CSV-Zeilen denselben group_by-Schlüssel haben */
const MULTI_ROW_POLICY_OPTIONS = [
  { value: 'aggregate', label: 'Zusammenführen (Funktion unten auf alle übrigen Felder)' },
  { value: 'reject', label: 'Abweisen — Gruppe wird nicht importiert (Fehlerhinweis im Import)' },
  { value: 'first_row', label: 'Nur erste Zeile — keine Berechnung über die Duplikat-Zeilen' },
  { value: 'last_row', label: 'Nur letzte Zeile — keine Berechnung über die Duplikat-Zeilen' },
]

function parseStoredImportRowProcessing(irp) {
  const safe = irp && typeof irp === 'object' ? irp : null
  const dedupeFromStore = !!(safe && safe.dedupe_identical_rows)
  const mrpRaw = safe && safe.multi_row_policy != null ? String(safe.multi_row_policy) : null
  const multiRowPolicy =
    mrpRaw && MULTI_ROW_POLICY_OPTIONS.some((o) => o.value === mrpRaw) ? mrpRaw : 'aggregate'

  if (!safe || Object.keys(safe).length === 0) {
    return {
      useCustom: false,
      irregular: false,
      groupBy: [],
      mode: '',
      multiRowPolicy: 'aggregate',
      dedupeIdentical: false,
    }
  }
  const gb = safe.group_by
  const agg = safe.aggregates
  if (!Array.isArray(gb) || gb.length === 0 || agg == null || typeof agg !== 'object') {
    return {
      useCustom: true,
      irregular: true,
      groupBy: Array.isArray(gb) ? [...gb] : [],
      mode: '',
      multiRowPolicy,
      dedupeIdentical: dedupeFromStore,
    }
  }
  const ops = [...new Set(Object.values(agg).map((x) => String(x)))]
  if (ops.length !== 1) {
    return {
      useCustom: true,
      irregular: true,
      groupBy: [...gb],
      mode: '',
      multiRowPolicy,
      dedupeIdentical: dedupeFromStore,
    }
  }
  return {
    useCustom: true,
    irregular: false,
    groupBy: [...gb],
    mode: ops[0],
    multiRowPolicy,
    dedupeIdentical: dedupeFromStore,
  }
}

function buildImportRowProcessingSimple(modFields, fm, groupBy, mode, multiRowPolicy, dedupeIdentical) {
  const targets = new Set(
    Object.values(fm).filter((v) => v && v !== '-' && v !== '_skip'),
  )
  const aggregates = {}
  const gbSet = new Set(groupBy)
  for (const t of targets) {
    if (gbSet.has(t)) continue
    const typ = modFields?.[t]?.type
    if (mode === 'first' || mode === 'last') {
      aggregates[t] = mode
    } else if (NUMERIC_ROW_AGG.has(mode)) {
      if (typ === 'string') continue
      aggregates[t] = mode
    }
  }
  const out = {
    group_by: groupBy,
    aggregates,
    multi_row_policy: multiRowPolicy || 'aggregate',
  }
  if (dedupeIdentical) out.dedupe_identical_rows = true
  return out
}

/** Erlaubt Eingaben wie 1,03 oder 1.03 während des Tippens; Finale normalisiert bei Blur/Speichern. */
function normalizeDecimalInputString(raw) {
  let s = String(raw).trim().replace(/\s/g, '')
  if (s === '') return ''
  const lastComma = s.lastIndexOf(',')
  const lastDot = s.lastIndexOf('.')
  if (lastComma >= 0 && lastDot >= 0) {
    if (lastComma > lastDot) {
      s = s.replace(/\./g, '').replace(',', '.')
    } else {
      s = s.replace(/,/g, '')
    }
  } else if (lastComma >= 0) {
    s = s.replace(',', '.')
  }
  return s
}

/**
 * Semantik: „source_amount [source_unit_label] entspricht target_amount [canonical storage unit]“
 * → CSV-Zahl (in der linken Einheit) × (target_amount / source_amount) → Speicherwert.
 * custom_equivalence dient der Dokumentation im JSON; der Importer nutzt nur conversion_factor.
 */
function loadEquivalenceFromTc(tc, fieldKey) {
  const row = tc[fieldKey]
  if (!row || typeof row !== 'object') {
    return { srcAmt: '1', srcUnit: '', tgtAmt: '' }
  }
  const eq = row.custom_equivalence
  if (eq && typeof eq === 'object') {
    const sa = eq.source_amount
    const ta = eq.target_amount
    return {
      srcAmt: sa != null && sa !== '' ? String(sa) : '',
      srcUnit: eq.source_unit_label != null ? String(eq.source_unit_label) : '',
      tgtAmt: ta != null && ta !== '' ? String(ta) : '',
    }
  }
  const f = row.conversion_factor
  if (f != null && f !== '') {
    return { srcAmt: '1', srcUnit: '', tgtAmt: String(f) }
  }
  return { srcAmt: '1', srcUnit: '', tgtAmt: '' }
}

function applyCustomEquivalenceToTcObject(tc, fieldKey, draft, canonicalUnitLabel) {
  const base =
    tc[fieldKey] && typeof tc[fieldKey] === 'object'
      ? { ...tc[fieldKey] }
      : { type: 'float', decimal_separator: 'auto', flexible: true }
  const sa = normalizeDecimalInputString(draft.srcAmt ?? '')
  const ta = normalizeDecimalInputString(draft.tgtAmt ?? '')
  const unitLbl = String(draft.srcUnit ?? '').trim()
  const bothEmpty = !sa && !ta

  if (bothEmpty) {
    delete base.conversion_factor
    delete base.custom_equivalence
    base.source_unit = 'custom'
    delete base.target_unit
    tc[fieldKey] = base
    return { ok: true }
  }
  if (!sa || !ta) {
    return { ok: true, partial: true }
  }
  const srcNum = Number(sa)
  const tgtNum = Number(ta)
  if (Number.isNaN(srcNum) || Number.isNaN(tgtNum)) {
    return { ok: false, message: `Umrechnung (${fieldKey}): keine gültigen Zahlen.` }
  }
  if (srcNum === 0) {
    return { ok: false, message: `Umrechnung (${fieldKey}): die linke Menge darf nicht 0 sein.` }
  }
  const factor = tgtNum / srcNum
  base.conversion_factor = factor
  base.source_unit = 'custom'
  base.custom_equivalence = {
    source_amount: srcNum,
    source_unit_label: unitLbl || '(Quelleinheit)',
    target_amount: tgtNum,
    target_unit_label: canonicalUnitLabel || '(Ziel)',
  }
  delete base.target_unit
  tc[fieldKey] = base
  return { ok: true }
}

function SampleTable({ sampleRows, columns }) {
  if (!sampleRows?.length || !columns?.length) return null
  const showCols = columns.slice(0, 8)
  return (
    <div style={{ overflowX: 'auto', marginTop: 12 }}>
      <table style={{ width: '100%', fontSize: 12, borderCollapse: 'collapse' }}>
        <thead>
          <tr>
            {showCols.map((c) => (
              <th
                key={c}
                style={{
                  textAlign: 'left',
                  padding: '8px 6px',
                  borderBottom: '1px solid var(--border)',
                  color: 'var(--text2)',
                  whiteSpace: 'nowrap',
                }}
              >
                {c}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {sampleRows.slice(0, 5).map((row, i) => (
            <tr key={i}>
              {showCols.map((c) => (
                <td key={c} style={csvPreviewTdStyle(row[c] ?? '—')}>
                  {row[c] ?? '—'}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

export default function AdminCsvTemplateEditorPage() {
  const { id: routeId } = useParams()
  const navigate = useNavigate()
  const isNew = routeId === 'new'
  const templateId = isNew ? null : Number(routeId)

  const [modules, setModules] = useState([])
  const [module, setModule] = useState('nutrition')
  const [mappingName, setMappingName] = useState('')
  const [description, setDescription] = useState('')
  const [delimiter, setDelimiter] = useState(';')
  const [encoding, setEncoding] = useState('utf-8')
  const [hasHeader, setHasHeader] = useState(true)
  const [columnSignature, setColumnSignature] = useState([])
  const [columns, setColumns] = useState([])
  const [fieldMappings, setFieldMappings] = useState({})
  const [typeConversionsText, setTypeConversionsText] = useState('{}')
  const [sampleRows, setSampleRows] = useState([])
  const [seedHint, setSeedHint] = useState(null)

  const [file, setFile] = useState(null)
  const [delimiterOverride, setDelimiterOverride] = useState('')
  const [seedTemplateId, setSeedTemplateId] = useState('')
  const [seedOptions, setSeedOptions] = useState([])

  const [loading, setLoading] = useState(!isNew)
  const [analyzing, setAnalyzing] = useState(false)
  const [saving, setSaving] = useState(false)
  const [validating, setValidating] = useState(false)
  const [validationReport, setValidationReport] = useState(null)
  const [error, setError] = useState(null)
  /** Entwurf für „Quelle entspricht Ziel“ (nur source_unit custom); Commit bei Blur/Speichern. */
  const [customEquivalenceDraftByField, setCustomEquivalenceDraftByField] = useState({})

  /** Zeilenaggregation: null in DB = Modul-Standard; sonst import_row_processing */
  const [rowAggUseCustom, setRowAggUseCustom] = useState(false)
  const [rowAggGroupBy, setRowAggGroupBy] = useState([])
  const [rowAggMode, setRowAggMode] = useState('')
  const [rowAggIrregular, setRowAggIrregular] = useState(false)
  const [rowAggJsonText, setRowAggJsonText] = useState('{}')
  const [rowAggMultiRowPolicy, setRowAggMultiRowPolicy] = useState('aggregate')
  const [rowAggDedupeIdentical, setRowAggDedupeIdentical] = useState(false)

  const modMeta = useMemo(() => modules.find((m) => m.id === module), [modules, module])
  const aggregateSleepImport = modMeta?.import_mode === 'apple_sleep_aggregate'
  const targetOptions = useMemo(() => {
    if (!modMeta?.fields || aggregateSleepImport) return []
    return Object.entries(modMeta.fields).map(([key, meta]) => ({
      value: key,
      label: `${key}${meta.required ? ' *' : ''}`,
    }))
  }, [modMeta, aggregateSleepImport])

  const requiredTargets = useMemo(() => {
    if (!modMeta?.fields) return []
    return Object.entries(modMeta.fields)
      .filter(([, v]) => v.required)
      .map(([k]) => k)
  }, [modMeta])

  useEffect(() => {
    api
      .getCsvModules()
      .then((r) => setModules(r.modules || []))
      .catch(() => {})
  }, [])

  useEffect(() => {
    if (!module) return
    api
      .adminListCsvTemplates(module)
      .then((d) => setSeedOptions(d.templates || []))
      .catch(() => setSeedOptions([]))
  }, [module])

  useEffect(() => {
    if (!isNew) return
    setRowAggUseCustom(false)
    setRowAggIrregular(false)
    setRowAggGroupBy([])
    setRowAggMode('')
    setRowAggJsonText('{}')
    setRowAggMultiRowPolicy('aggregate')
    setRowAggDedupeIdentical(false)
  }, [module, isNew])

  useEffect(() => {
    if (isNew || !templateId) return
    let ok = true
    setLoading(true)
    setError(null)
    api
      .adminGetCsvTemplate(templateId)
      .then((t) => {
        if (!ok) return
        setModule(t.module)
        setMappingName(t.mapping_name || '')
        setDescription(t.description || '')
        setDelimiter(t.delimiter || ',')
        setEncoding(t.encoding || 'utf-8')
        setHasHeader(!!t.has_header)
        setColumnSignature(t.column_signature || [])
        const fm = t.field_mappings || {}
        setFieldMappings(fm)
        setColumns(Object.keys(fm))
        setTypeConversionsText(JSON.stringify(t.type_conversions || {}, null, 2))
        setCustomEquivalenceDraftByField({})
        setSampleRows([])
        setSeedHint(null)
        const rp = parseStoredImportRowProcessing(t.import_row_processing)
        setRowAggUseCustom(rp.useCustom)
        setRowAggIrregular(rp.irregular)
        setRowAggGroupBy(rp.groupBy)
        setRowAggMode(rp.mode)
        setRowAggMultiRowPolicy(rp.multiRowPolicy)
        setRowAggDedupeIdentical(rp.dedupeIdentical)
        setRowAggJsonText(JSON.stringify(t.import_row_processing || {}, null, 2))
      })
      .catch((e) => {
        if (ok) setError(e.message)
      })
      .finally(() => {
        if (ok) setLoading(false)
      })
    return () => {
      ok = false
    }
  }, [isNew, templateId])

  const assignedTargets = useMemo(() => {
    return new Set(
      Object.values(fieldMappings).filter((v) => v && v !== '-' && v !== '_skip'),
    )
  }, [fieldMappings])

  const rowAggGroupCandidates = useMemo(() => {
    return [...assignedTargets].filter((t) => modMeta?.fields?.[t]).sort()
  }, [assignedTargets, modMeta])

  useEffect(() => {
    const targets = new Set(
      Object.values(fieldMappings).filter((v) => v && v !== '-' && v !== '_skip'),
    )
    setRowAggGroupBy((prev) => prev.filter((g) => targets.has(g)))
  }, [fieldMappings])

  const missingRequired = useMemo(() => {
    return requiredTargets.filter((r) => !assignedTargets.has(r))
  }, [requiredTargets, assignedTargets])

  const unitTargets = useMemo(() => {
    if (!modMeta?.fields || aggregateSleepImport) return []
    const list = []
    const seen = new Set()
    for (const t of assignedTargets) {
      const opts = modMeta.fields[t]?.source_unit_options
      if (!opts?.length || seen.has(t)) continue
      seen.add(t)
      list.push({ field: t, options: opts })
    }
    return list.sort((a, b) => a.field.localeCompare(b.field))
  }, [modMeta, assignedTargets, aggregateSleepImport])

  const getSourceUnitSelectValue = (fieldKey) => {
    let tc
    try {
      tc = JSON.parse(typeConversionsText || '{}')
    } catch {
      return null
    }
    const opts = modMeta?.fields?.[fieldKey]?.source_unit_options || []
    const canonical = opts.find((o) => o.is_canonical)?.id || opts[0]?.id
    const su = tc[fieldKey]?.source_unit
    if (su != null && su !== '') {
      const sid = String(su).toLowerCase()
      const hit = opts.find((o) => o.id === sid)
      if (hit) return hit.id
    }
    const ce = tc[fieldKey]?.custom_equivalence
    if (ce && typeof ce === 'object' && opts.some((o) => o.id === 'custom')) {
      return 'custom'
    }
    if (tc[fieldKey]?.conversion_factor != null && tc[fieldKey]?.conversion_factor !== '') {
      const hasCustomOpt = opts.some((o) => o.id === 'custom')
      if (hasCustomOpt) return 'custom'
    }
    return canonical || ''
  }

  const updateSourceUnit = (fieldKey, sourceUnitId) => {
    let tc
    try {
      tc = JSON.parse(typeConversionsText || '{}')
    } catch {
      setError('type_conversions: ungültiges JSON (Quelleinheit kann nicht gesetzt werden).')
      return
    }
    const opts = modMeta?.fields?.[fieldKey]?.source_unit_options || []
    const canonical = opts.find((o) => o.is_canonical)?.id
    const base =
      tc[fieldKey] && typeof tc[fieldKey] === 'object'
        ? { ...tc[fieldKey] }
        : { type: 'float', decimal_separator: 'auto', flexible: true }
    delete base.target_unit
    if (sourceUnitId === canonical || sourceUnitId === '' || sourceUnitId == null) {
      delete base.source_unit
      delete base.conversion_factor
      delete base.custom_equivalence
    } else if (sourceUnitId === 'custom') {
      base.source_unit = 'custom'
    } else {
      base.source_unit = sourceUnitId
      delete base.conversion_factor
      delete base.custom_equivalence
    }
    tc[fieldKey] = base
    setTypeConversionsText(JSON.stringify(tc, null, 2))
    setCustomEquivalenceDraftByField((prev) => {
      const next = { ...prev }
      delete next[fieldKey]
      return next
    })
    setError(null)
  }

  const getCanonicalStorageUnitLabel = (fieldKey) => {
    const opts = modMeta?.fields?.[fieldKey]?.source_unit_options || []
    const c = opts.find((o) => o.is_canonical)
    return c?.canonical_unit || c?.id || '—'
  }

  const getEquivalenceDisplay = (fieldKey) => {
    if (customEquivalenceDraftByField[fieldKey]) {
      return customEquivalenceDraftByField[fieldKey]
    }
    try {
      const tc = JSON.parse(typeConversionsText || '{}')
      return loadEquivalenceFromTc(tc, fieldKey)
    } catch {
      return { srcAmt: '1', srcUnit: '', tgtAmt: '' }
    }
  }

  const mergeEquivalenceDraft = (fieldKey, patch) => {
    setCustomEquivalenceDraftByField((prev) => {
      let baseTc
      try {
        baseTc = JSON.parse(typeConversionsText || '{}')
      } catch {
        baseTc = {}
      }
      const cur = prev[fieldKey] || loadEquivalenceFromTc(baseTc, fieldKey)
      return { ...prev, [fieldKey]: { ...cur, ...patch } }
    })
  }

  const commitCustomEquivalenceOnBlur = (fieldKey) => {
    if (getSourceUnitSelectValue(fieldKey) !== 'custom') {
      setCustomEquivalenceDraftByField((prev) => {
        const next = { ...prev }
        delete next[fieldKey]
        return next
      })
      return
    }
    let tc
    try {
      tc = JSON.parse(typeConversionsText || '{}')
    } catch {
      setError('type_conversions: ungültiges JSON (Umrechnung kann nicht gespeichert werden).')
      return
    }
    const draft =
      customEquivalenceDraftByField[fieldKey] || loadEquivalenceFromTc(tc, fieldKey)
    const canon = getCanonicalStorageUnitLabel(fieldKey)
    const result = applyCustomEquivalenceToTcObject(tc, fieldKey, draft, canon)
    if (result.partial) {
      setError(null)
      return
    }
    if (!result.ok) {
      setError(result.message || 'Ungültige Umrechnung.')
      return
    }
    setTypeConversionsText(JSON.stringify(tc, null, 2))
    setCustomEquivalenceDraftByField((prev) => {
      const next = { ...prev }
      delete next[fieldKey]
      return next
    })
    setError(null)
  }

  const derivedFactorHintLine = (fieldKey) => {
    const d = getEquivalenceDisplay(fieldKey)
    const sa = normalizeDecimalInputString(d.srcAmt ?? '')
    const ta = normalizeDecimalInputString(d.tgtAmt ?? '')
    if (!sa || !ta) return null
    const a = Number(sa)
    const b = Number(ta)
    if (Number.isNaN(a) || Number.isNaN(b) || a === 0) return null
    const f = b / a
    const fDisp = Number.isFinite(f) ? Math.round(f * 1e9) / 1e9 : f
    const canon = getCanonicalStorageUnitLabel(fieldKey)
    return `Abgeleitet: CSV-Zahl × ${fDisp} → Wert in ${canon} (Speicher)`
  }

  const handleAnalyze = async () => {
    if (!file) {
      setError('Bitte eine CSV-Datei wählen.')
      return
    }
    setAnalyzing(true)
    setError(null)
    try {
      const delim = delimiterOverride || null
      const seed = seedTemplateId === '' ? null : Number(seedTemplateId)
      const res = await api.adminAnalyzeCsvTemplate(file, module, delim, seed)
      setColumns(res.columns || [])
      setColumnSignature(res.column_signature_normalized || [])
      setFieldMappings(res.field_mappings || {})
      setTypeConversionsText(JSON.stringify(res.type_conversions || {}, null, 2))
      setSampleRows(res.sample_rows || [])
      setDelimiter(res.delimiter || ';')
      setEncoding(res.encoding || 'utf-8')
      setSeedHint(res.seed_template || null)
      setCustomEquivalenceDraftByField({})
      setRowAggUseCustom(false)
      setRowAggIrregular(false)
      setRowAggGroupBy([])
      setRowAggMode('')
      setRowAggJsonText('{}')
      setRowAggMultiRowPolicy('aggregate')
      setRowAggDedupeIdentical(false)
    } catch (e) {
      setError(e.message || 'Analyse fehlgeschlagen')
    } finally {
      setAnalyzing(false)
    }
  }

  const updateMapping = (col, dbField) => {
    setFieldMappings((prev) => ({ ...prev, [col]: dbField || '-' }))
  }

  const handleFormatCheck = async () => {
    setError(null)
    setValidationReport(null)
    let tc
    try {
      tc = JSON.parse(typeConversionsText || '{}')
      if (tc !== null && typeof tc !== 'object') throw new Error()
    } catch {
      setError('type_conversions: ungültiges JSON.')
      return
    }
    if (!module) {
      setError('Modul wählen.')
      return
    }
    setValidating(true)
    try {
      const r = await api.adminValidateCsvTemplate({
        module,
        field_mappings: fieldMappings,
        type_conversions: tc,
        import_row_processing: null,
        column_signature: columnSignature.length ? columnSignature : null,
      })
      setValidationReport(r)
    } catch (e) {
      setError(e.message || 'Formatprüfung fehlgeschlagen')
    } finally {
      setValidating(false)
    }
  }

  const handleSave = async () => {
    setError(null)
    let textForTc = typeConversionsText
    const pendingEquivalenceDrafts = { ...customEquivalenceDraftByField }
    if (Object.keys(pendingEquivalenceDrafts).length > 0) {
      try {
        const tco = JSON.parse(textForTc || '{}')
        for (const fk of Object.keys(pendingEquivalenceDrafts)) {
          const su = String(tco[fk]?.source_unit || '').toLowerCase()
          if (su !== 'custom') continue
          const draft = pendingEquivalenceDrafts[fk]
          const opts = modMeta?.fields?.[fk]?.source_unit_options || []
          const c = opts.find((o) => o.is_canonical)
          const canon = c?.canonical_unit || c?.id || '—'
          const result = applyCustomEquivalenceToTcObject(tco, fk, draft, canon)
          if (result.partial) {
            setError(
              `Benutzerdefinierte Umrechnung (${fk}): beide Mengen ausfüllen oder Entwurf verwerfen (Tab durch alle Felder).`,
            )
            return
          }
          if (!result.ok) {
            setError(result.message || 'Benutzerdefinierte Umrechnung ungültig.')
            return
          }
        }
        textForTc = JSON.stringify(tco, null, 2)
        setTypeConversionsText(textForTc)
        setCustomEquivalenceDraftByField({})
      } catch {
        setError('type_conversions: ungültiges JSON.')
        return
      }
    }
    let tc = null
    try {
      tc = JSON.parse(textForTc || '{}')
      if (tc !== null && typeof tc !== 'object') throw new Error()
    } catch {
      setError('type_conversions: ungültiges JSON.')
      return
    }
    if (!mappingName.trim()) {
      setError('Bitte einen Namen für die Vorlage eingeben.')
      return
    }
    if (!columns.length || !Object.keys(fieldMappings).length) {
      setError('Keine Spalten-Zuordnung: CSV analysieren oder Vorlage laden.')
      return
    }
    if (missingRequired.length) {
      setError(`Pflicht-Zielfelder fehlen: ${missingRequired.join(', ')}`)
      return
    }

    let import_row_processing = null
    if (!aggregateSleepImport && rowAggUseCustom) {
      if (rowAggIrregular) {
        try {
          import_row_processing = JSON.parse(rowAggJsonText || '{}')
          if (!import_row_processing || typeof import_row_processing !== 'object') throw new Error('bad')
        } catch {
          setError('Zeilenaggregation: ungültiges JSON.')
          return
        }
        const gb = import_row_processing.group_by
        if (!Array.isArray(gb) || !gb.length) {
          setError('Zeilenaggregation (JSON): „group_by“ muss eine nicht-leere Liste sein.')
          return
        }
      } else {
        if (!rowAggGroupBy.length) {
          setError('Zeilenaggregation: mindestens ein Schlüsselfeld auswählen.')
          return
        }
        if (!rowAggMode) {
          setError('Zeilenaggregation: eine Funktion wählen (Summe, Mittelwert, …).')
          return
        }
        for (const g of rowAggGroupBy) {
          if (!assignedTargets.has(g)) {
            setError(`Zeilenaggregation: Schlüsselfeld „${g}“ muss einer CSV-Spalte zugeordnet sein.`)
            return
          }
        }
        import_row_processing = buildImportRowProcessingSimple(
          modMeta?.fields,
          fieldMappings,
          rowAggGroupBy,
          rowAggMode,
          rowAggMultiRowPolicy,
          rowAggDedupeIdentical,
        )
      }
    }

    const payload = {
      module,
      mapping_name: mappingName.trim(),
      description: description.trim() || null,
      column_signature: columnSignature.length ? columnSignature : null,
      delimiter,
      encoding: encoding || 'utf-8',
      has_header: hasHeader,
      field_mappings: fieldMappings,
      type_conversions: tc,
      import_row_processing,
    }

    if (!payload.column_signature?.length) {
      setError('column_signature fehlt — bitte CSV erneut analysieren.')
      return
    }

    setSaving(true)
    try {
      if (isNew) {
        await api.adminCreateCsvTemplate(payload)
      } else {
        const { module: _m, ...patch } = payload
        await api.adminUpdateCsvTemplate(templateId, patch)
      }
      navigate('/admin/csv-templates')
    } catch (e) {
      setError(e.message || 'Speichern fehlgeschlagen')
    } finally {
      setSaving(false)
    }
  }

  const handleDelete = async () => {
    if (isNew || !templateId) return
    if (!confirm('System-Vorlage wirklich löschen?')) return
    setError(null)
    try {
      await api.adminDeleteCsvTemplate(templateId)
      navigate('/admin/csv-templates')
    } catch (e) {
      setError(e.message || 'Löschen fehlgeschlagen')
    }
  }

  if (loading) {
    return (
      <div style={{ padding: 24, textAlign: 'center' }}>
        <Loader2 size={28} className="spin" style={{ animation: 'spin 0.8s linear infinite' }} />
      </div>
    )
  }

  return (
    <div style={{ padding: '16px 16px 96px', maxWidth: 920, margin: '0 auto' }}>
      <Link
        to="/admin/csv-templates"
        className="btn btn-secondary"
        style={{ display: 'inline-flex', alignItems: 'center', gap: 8, marginBottom: 16, textDecoration: 'none' }}
      >
        <ArrowLeft size={18} /> Zur Liste
      </Link>

      <h1 className="page-title" style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
        <FileSpreadsheet size={26} strokeWidth={2} />
        {isNew ? 'Neue CSV-Vorlage' : 'Vorlage bearbeiten'}
      </h1>

      {error && (
        <div className="card" style={{ padding: 12, borderColor: 'var(--danger)', color: 'var(--danger)', marginBottom: 16 }}>
          {error}
        </div>
      )}

      <div className="card" style={{ padding: 16, marginBottom: 16 }}>
        <div className="form-label">Modul</div>
        <select
          className="form-input"
          value={module}
          disabled={!isNew}
          onChange={(e) => setModule(e.target.value)}
          style={{ width: '100%', marginTop: 8, textAlign: 'left', minHeight: 46, padding: '11px 14px' }}
        >
          {modules.map((m) => (
            <option key={m.id} value={m.id}>
              {MODULE_LABEL[m.id] || m.id}
            </option>
          ))}
        </select>
        {!isNew && (
          <p style={{ fontSize: 13, color: 'var(--text3)', marginTop: 8 }}>
            Modul bestehender Vorlagen kann nicht geändert werden. System-Vorlagen können hier bearbeitet und
            gespeichert werden (Signatur, Trennzeichen, Zuordnungen).
          </p>
        )}
        {aggregateSleepImport && (
          <p
            style={{
              fontSize: 13,
              color: 'var(--text2)',
              marginTop: 12,
              lineHeight: 1.55,
              padding: 12,
              background: 'var(--surface2)',
              borderRadius: 12,
            }}
          >
            <strong>Schlaf (Apple-Aggregat):</strong> Die Zeilen werden nicht über Spalten-Ziele importiert,
            sondern vom Apple-Schlaf-Parser ausgewertet (Schlafanalyse oder Segment-Export). Alle CSV-Spalten
            bleiben auf „ignorieren“. Wichtig sind die{' '}
            <strong>gespeicherte Spalten-Signatur</strong> und das passende Datei-Format — damit die Datei
            in der Nutzer-Auswahl erkannt wird.
          </p>
        )}
      </div>

      <div className="card" style={{ padding: 16, marginBottom: 16 }}>
        <div className="form-label">1. Beispiel-CSV (wie Import)</div>
        <input
          type="file"
          accept=".csv,text/csv"
          className="form-input"
          style={{ marginTop: 8, width: '100%' }}
          onChange={(e) => setFile(e.target.files?.[0] || null)}
        />
        <div style={{ marginTop: 12, display: 'grid', gap: 12 }}>
          <label style={{ fontSize: 14, color: 'var(--text2)' }}>
            Trennzeichen (optional, sonst automatisch):
            <select
              className="form-input"
              style={{ width: '100%', marginTop: 6, textAlign: 'left', minHeight: 44, padding: '10px 12px' }}
              value={delimiterOverride}
              onChange={(e) => setDelimiterOverride(e.target.value)}
            >
              <option value="">Auto</option>
              <option value=";">Semikolon</option>
              <option value=",">Komma</option>
              <option value="\t">Tab</option>
            </select>
          </label>
          <label style={{ fontSize: 14, color: 'var(--text2)' }}>
            Optional: feste Seed-Vorlage für Vorschläge:
            <select
              className="form-input"
              style={{ width: '100%', marginTop: 6, textAlign: 'left', minHeight: 44, padding: '10px 12px' }}
              value={seedTemplateId}
              onChange={(e) => setSeedTemplateId(e.target.value)}
            >
              <option value="">Beste passende System-Vorlage (Abdeckung der Vorlagen-Spalten)</option>
              {seedOptions.map((s) => (
                <option key={s.id} value={String(s.id)}>
                  {s.mapping_name}
                </option>
              ))}
            </select>
          </label>
        </div>
        <button
          type="button"
          className="btn btn-primary"
          style={{ marginTop: 16, width: '100%' }}
          disabled={!file || analyzing}
          onClick={handleAnalyze}
        >
          {analyzing ? (
            <>
              <Loader2 size={18} style={{ marginRight: 8, animation: 'spin 0.7s linear infinite' }} />
              Analysiere …
            </>
          ) : (
            'CSV analysieren & Vorschläge'
          )}
        </button>
        {seedHint && (
          <p style={{ fontSize: 13, color: 'var(--text2)', marginTop: 12, lineHeight: 1.5 }}>
            Seed: <strong>{seedHint.mapping_name}</strong> · Vorlage abgedeckt{' '}
            <strong>{Math.round((seedHint.confidence || 0) * 100)} %</strong>
            {seedHint.columns_matched != null && seedHint.columns_in_template != null
              ? ` (${seedHint.columns_matched}/${seedHint.columns_in_template} Spalten)`
              : ''}
            {seedHint.jaccard != null && (
              <>
                {' '}
                · Jaccard <strong>{Math.round(seedHint.jaccard * 100)} %</strong>
              </>
            )}
          </p>
        )}
        {sampleRows.length > 0 && <SampleTable sampleRows={sampleRows} columns={columns} />}
      </div>

      <div className="card" style={{ padding: 16, marginBottom: 16 }}>
        <div className="form-label">2. Stammdaten</div>
        <label className="form-label" style={{ marginTop: 12 }}>
          Name *
        </label>
        <input
          className="form-input"
          style={{ width: '100%', textAlign: 'left' }}
          value={mappingName}
          onChange={(e) => setMappingName(e.target.value)}
          placeholder="z. B. FDDB Export 2026"
        />
        <label className="form-label" style={{ marginTop: 12 }}>
          Beschreibung
        </label>
        <textarea
          className="form-input"
          style={{ width: '100%', minHeight: 64, textAlign: 'left' }}
          value={description}
          onChange={(e) => setDescription(e.target.value)}
        />
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginTop: 12 }}>
          <label>
            <span className="form-label">Trennzeichen (gespeichert)</span>
            <select
              className="form-input"
              style={{ width: '100%', marginTop: 6, textAlign: 'left', minHeight: 44, padding: '10px 12px' }}
              value={delimiter}
              onChange={(e) => setDelimiter(e.target.value)}
            >
              <option value=";">Semikolon</option>
              <option value=",">Komma</option>
              <option value="\t">Tab</option>
            </select>
          </label>
          <label>
            <span className="form-label">Kopfzeile</span>
            <select
              className="form-input"
              style={{ width: '100%', marginTop: 6, textAlign: 'left', minHeight: 44, padding: '10px 12px' }}
              value={hasHeader ? 'yes' : 'no'}
              onChange={(e) => setHasHeader(e.target.value === 'yes')}
            >
              <option value="yes">Ja</option>
              <option value="no">Nein (nicht unterstützt im Import)</option>
            </select>
          </label>
        </div>
        <label className="form-label" style={{ marginTop: 16, display: 'block' }}>
          Spalten-Signatur (Vorlagen-Matching — eine Original-Überschrift pro Zeile, manuell anpassbar)
        </label>
        <textarea
          className="form-input"
          style={{
            width: '100%',
            minHeight: 100,
            marginTop: 8,
            fontFamily: 'monospace',
            fontSize: 12,
            textAlign: 'left',
          }}
          value={columnSignature.join('\n')}
          onChange={(e) =>
            setColumnSignature(
              e.target.value
                .split('\n')
                .map((s) => s.trim())
                .filter(Boolean),
            )
          }
          placeholder={'Date/Time\nStart\nEnd\nTotal Sleep (hr)\n…'}
        />
      </div>

      <div className="card" style={{ padding: 16, marginBottom: 16 }}>
        <div className="form-label">3. Spalten → Zielfelder (* = Pflicht)</div>
        {!columns.length ? (
          <p style={{ fontSize: 14, color: 'var(--text2)', marginTop: 8 }}>
            Nach CSV-Analyse erscheinen die Zeilen hier. Bei Schlaf-Vorlagen ohne Analyse: Signatur oben pflegen,
            speichern, oder Beispiel-CSV analysieren.
          </p>
        ) : (
          <div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 10 }}>
            {columns.map((col) => (
              <div
                key={col}
                style={{
                  display: 'grid',
                  gridTemplateColumns: 'minmax(0, 1fr) minmax(280px, min(52vw, 440px))',
                  gap: '10px 16px',
                  alignItems: 'center',
                }}
              >
                <code style={{ fontSize: 12, wordBreak: 'break-word', color: 'var(--text2)', textAlign: 'left' }}>
                  {col}
                </code>
                <select
                  className="form-input"
                  value={fieldMappings[col] || '-'}
                  onChange={(e) => updateMapping(col, e.target.value)}
                  disabled={aggregateSleepImport}
                  style={{
                    width: '100%',
                    minHeight: 46,
                    textAlign: 'left',
                    padding: '11px 14px',
                    fontSize: 15,
                  }}
                >
                  <option value="-">— ignorieren</option>
                  {targetOptions.map((o) => (
                    <option key={o.value} value={o.value}>
                      {o.label}
                    </option>
                  ))}
                </select>
              </div>
            ))}
          </div>
        )}
        {missingRequired.length > 0 && (
          <p style={{ fontSize: 13, color: 'var(--danger)', marginTop: 12 }}>
            Noch zuzuweisen: {missingRequired.join(', ')}
          </p>
        )}
      </div>

      {!aggregateSleepImport && (
        <div className="card" style={{ padding: 16, marginBottom: 16 }}>
          <div className="form-label">3a. Zeilenaggregation</div>
          {!modMeta?.fields || Object.keys(modMeta.fields).length === 0 ? (
            <p style={{ fontSize: 14, color: 'var(--text2)', marginTop: 8 }}>
              Modul-Metadaten laden … bitte Seite kurz offen lassen oder neu laden.
            </p>
          ) : (
            <>
          <p style={{ fontSize: 13, color: 'var(--text2)', marginTop: 8, lineHeight: 1.55 }}>
            <strong>Schlüsselfelder</strong> bestimmen, wann CSV-Zeilen dieselbe „Gruppe“ sind. Was bei{' '}
            <strong>mehr als einer Zeile pro Gruppe</strong> passiert, steuern Sie unten (
            <em>Zusammenführen / Abweisen / nur erste oder letzte Zeile</em>). Optional können{' '}
            <strong>völlig identische</strong> gemappte Zeilen vorher entfernt werden.
          </p>
          <label
            style={{
              display: 'flex',
              alignItems: 'flex-start',
              gap: 10,
              marginTop: 14,
              cursor: 'pointer',
              fontSize: 14,
              color: 'var(--text1)',
            }}
          >
            <input
              type="checkbox"
              checked={rowAggUseCustom}
              onChange={(e) => {
                const on = e.target.checked
                setRowAggUseCustom(on)
                if (!on) {
                  setRowAggIrregular(false)
                  setRowAggGroupBy([])
                  setRowAggMode('')
                  setRowAggJsonText('{}')
                  setRowAggMultiRowPolicy('aggregate')
                  setRowAggDedupeIdentical(false)
                }
              }}
              style={{ marginTop: 3 }}
            />
            <span>
              <strong>Eigene Zeilenlogik in dieser Vorlage speichern.</strong> Wenn deaktiviert, nutzt der Import den{' '}
              <strong>Legacy-Fallback im Server-Code</strong> (nur solange die Vorlage kein JSON speichert —
              mittelfristig sollen alle Vorlagen explizit sein).
            </span>
          </label>
          {modMeta.import_row_processing_default && (
            <details style={{ marginTop: 12, fontSize: 13, color: 'var(--text2)' }}>
              <summary style={{ cursor: 'pointer' }}>
                Legacy-Fallback im Code (Referenz, wenn der Haken oben aus ist)
              </summary>
              <pre
                style={{
                  marginTop: 8,
                  padding: 12,
                  background: 'var(--surface2)',
                  borderRadius: 8,
                  overflow: 'auto',
                  fontSize: 12,
                  textAlign: 'left',
                }}
              >
                {JSON.stringify(modMeta.import_row_processing_default, null, 2)}
              </pre>
            </details>
          )}
          {rowAggUseCustom && (
            <>
              {modMeta.import_row_processing_default && (
                <button
                  type="button"
                  className="btn btn-secondary"
                  style={{ marginTop: 12 }}
                  onClick={() => {
                    const d = modMeta.import_row_processing_default
                    const p = parseStoredImportRowProcessing(d)
                    setRowAggIrregular(p.irregular)
                    setRowAggGroupBy(p.groupBy)
                    setRowAggMode(p.mode)
                    setRowAggMultiRowPolicy(p.multiRowPolicy)
                    setRowAggDedupeIdentical(p.dedupeIdentical)
                    setRowAggJsonText(JSON.stringify(d, null, 2))
                  }}
                >
                  Modul-Vorgabe übernehmen
                </button>
              )}
              {rowAggIrregular ? (
                <>
                  <p style={{ fontSize: 13, color: 'var(--text2)', marginTop: 14, lineHeight: 1.55 }}>
                    Diese Vorlage nutzt <strong>unterschiedliche</strong> Aggregations-Funktionen pro Feld. JSON
                    anpassen oder vereinheitlichen (pro-Feld-Auswahl später). Optional:{' '}
                    <code>multi_row_policy</code> (<code>aggregate</code> | <code>reject</code> |{' '}
                    <code>first_row</code> | <code>last_row</code>), <code>dedupe_identical_rows</code>: true.
                  </p>
                </>
              ) : (
                <>
                  <div className="form-label" style={{ marginTop: 16 }}>
                    Schlüsselfelder (Mehrfachauswahl)
                  </div>
                  <p style={{ fontSize: 12, color: 'var(--text3)', marginTop: 6 }}>
                    Nur bereits zugewiesene Zielfelder (Abschnitt 3).
                  </p>
                  {rowAggGroupCandidates.length === 0 ? (
                    <p style={{ fontSize: 13, color: 'var(--text2)', marginTop: 8 }}>
                      Noch keine Zielfelder zugewiesen — nach Zuweisung erscheinen die Schlüssel hier.
                    </p>
                  ) : (
                    <div
                      style={{
                        marginTop: 10,
                        display: 'flex',
                        flexDirection: 'column',
                        gap: 8,
                        alignItems: 'flex-start',
                      }}
                    >
                      {rowAggGroupCandidates.map((key) => (
                        <label
                          key={key}
                          style={{
                            display: 'flex',
                            alignItems: 'center',
                            gap: 8,
                            cursor: 'pointer',
                            fontSize: 14,
                          }}
                        >
                          <input
                            type="checkbox"
                            checked={rowAggGroupBy.includes(key)}
                            onChange={() => {
                              setRowAggIrregular(false)
                              setRowAggGroupBy((prev) =>
                                prev.includes(key) ? prev.filter((x) => x !== key) : [...prev, key],
                              )
                            }}
                          />
                          <code>{key}</code>
                        </label>
                      ))}
                    </div>
                  )}
                  <label className="form-label" style={{ marginTop: 16, display: 'block' }}>
                    Funktion für alle übrigen Zielfelder
                  </label>
                  <select
                    className="form-input"
                    style={{
                      width: '100%',
                      maxWidth: 420,
                      marginTop: 8,
                      textAlign: 'left',
                      minHeight: 46,
                    }}
                    value={rowAggMode}
                    onChange={(e) => {
                      setRowAggIrregular(false)
                      setRowAggMode(e.target.value)
                    }}
                  >
                    <option value="">— wählen —</option>
                    {ROW_AGG_OPS.map((o) => (
                      <option key={o.value} value={o.value}>
                        {o.label}
                      </option>
                    ))}
                  </select>
                  <label className="form-label" style={{ marginTop: 16, display: 'block' }}>
                    Mehrere Zeilen pro Schlüssel
                  </label>
                  <select
                    className="form-input"
                    style={{
                      width: '100%',
                      maxWidth: 520,
                      marginTop: 8,
                      textAlign: 'left',
                      minHeight: 46,
                    }}
                    value={rowAggMultiRowPolicy}
                    onChange={(e) => {
                      setRowAggIrregular(false)
                      setRowAggMultiRowPolicy(e.target.value)
                    }}
                  >
                    {MULTI_ROW_POLICY_OPTIONS.map((o) => (
                      <option key={o.value} value={o.value}>
                        {o.label}
                      </option>
                    ))}
                  </select>
                  <label
                    style={{
                      display: 'flex',
                      alignItems: 'flex-start',
                      gap: 10,
                      marginTop: 14,
                      cursor: 'pointer',
                      fontSize: 14,
                      color: 'var(--text1)',
                    }}
                  >
                    <input
                      type="checkbox"
                      checked={rowAggDedupeIdentical}
                      onChange={(e) => {
                        setRowAggIrregular(false)
                        setRowAggDedupeIdentical(e.target.checked)
                      }}
                      style={{ marginTop: 3 }}
                    />
                    <span>
                      <strong>Identische Zeilen vorher entfernen</strong> (alle gemappten Felder gleich — nur die erste
                      Zeile jeder Kopie bleibt).
                    </span>
                  </label>
                </>
              )}
              <div className="form-label" style={{ marginTop: 18 }}>
                import_row_processing (JSON)
              </div>
              <p style={{ fontSize: 13, color: 'var(--text2)', marginTop: 6, lineHeight: 1.55 }}>
                {rowAggIrregular ? (
                  <>
                    <strong>Bearbeitbar</strong> — wie bei <code>type_conversions</code>; beim Speichern prüft das
                    Backend die Struktur.
                  </>
                ) : (
                  <>
                    <strong>Nur Lesen</strong> — Vorschau aus den Feldern oben; dasselbe JSON wird beim Speichern
                    geschrieben.
                  </>
                )}
              </p>
              <textarea
                className="form-input"
                readOnly={!rowAggIrregular}
                style={{
                  width: '100%',
                  minHeight: rowAggIrregular ? 200 : 160,
                  marginTop: 8,
                  fontFamily: 'monospace',
                  fontSize: 12,
                  textAlign: 'left',
                  opacity: rowAggIrregular ? 1 : 0.95,
                  background: rowAggIrregular ? undefined : 'var(--surface2)',
                }}
                value={
                  rowAggIrregular
                    ? rowAggJsonText
                    : rowAggGroupBy.length && rowAggMode
                      ? JSON.stringify(
                          buildImportRowProcessingSimple(
                            modMeta?.fields,
                            fieldMappings,
                            rowAggGroupBy,
                            rowAggMode,
                            rowAggMultiRowPolicy,
                            rowAggDedupeIdentical,
                          ),
                          null,
                          2,
                        )
                      : JSON.stringify(
                          {
                            _hinweis:
                              'Mindestens ein Schlüsselfeld und eine gemeinsame Funktion wählen — dann erscheint hier das finale JSON.',
                          },
                          null,
                          2,
                        )
                }
                onChange={rowAggIrregular ? (e) => setRowAggJsonText(e.target.value) : undefined}
                spellCheck={false}
              />
            </>
          )}
            </>
          )}
        </div>
      )}

      {unitTargets.length > 0 && (
        <div className="card" style={{ padding: 16, marginBottom: 16 }}>
          <div className="form-label">3b. Quelleinheit (optional)</div>
          <p style={{ fontSize: 13, color: 'var(--text2)', marginTop: 8, lineHeight: 1.55 }}>
            Ziel-Einheit kommt aus dem Datenmodell. Standard-Umrechnungen im Dropdown; bei &quot;Benutzerdefiniert&quot;
            die Bezugsgröße eintragen: <strong>Menge [Quelleinheit] entspricht Menge [Zieleinheit]</strong> (Ziel ist die
            Speicher-Einheit des Feldes). Im JSON siehst du weiterhin <code>conversion_factor</code> und{' '}
            <code>custom_equivalence</code> zur Dokumentation.
          </p>
          <div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 16 }}>
            {unitTargets.map(({ field: fkey, options }) => (
              <div key={fkey}>
                <label style={{ display: 'block' }}>
                  <span className="form-label" style={{ display: 'block', marginBottom: 6 }}>
                    {fkey}
                  </span>
                  <select
                    className="form-input"
                    value={getSourceUnitSelectValue(fkey) || ''}
                    onChange={(e) => updateSourceUnit(fkey, e.target.value)}
                    style={{
                      width: '100%',
                      minHeight: 46,
                      textAlign: 'left',
                      padding: '11px 14px',
                      fontSize: 15,
                    }}
                  >
                    {options.map((o) => (
                      <option key={o.id} value={o.id}>
                        {o.label}
                      </option>
                    ))}
                  </select>
                </label>
                {getSourceUnitSelectValue(fkey) === 'custom' && (
                  <div style={{ marginTop: 12 }}>
                    <span className="form-label" style={{ display: 'block', marginBottom: 8 }}>
                      Bezugsgröße (CSV steht in der linken Einheit)
                    </span>
                    <datalist id={`csv-custom-units-${fkey}`}>
                      {CUSTOM_SOURCE_UNIT_HINTS.map((h) => (
                        <option key={h} value={h} />
                      ))}
                    </datalist>
                    <div
                      style={{
                        display: 'flex',
                        flexWrap: 'wrap',
                        alignItems: 'center',
                        gap: 10,
                        rowGap: 12,
                      }}
                    >
                      <input
                        type="text"
                        inputMode="decimal"
                        className="form-input"
                        placeholder="Menge"
                        aria-label="Menge in Quelleinheit"
                        value={getEquivalenceDisplay(fkey).srcAmt}
                        onChange={(e) => mergeEquivalenceDraft(fkey, { srcAmt: e.target.value })}
                        onBlur={() => commitCustomEquivalenceOnBlur(fkey)}
                        style={{ width: 96, textAlign: 'left', minHeight: 46, padding: '11px 12px' }}
                      />
                      <input
                        type="text"
                        className="form-input"
                        placeholder="Quelleinheit"
                        list={`csv-custom-units-${fkey}`}
                        aria-label="Quelleinheit (z. B. ml)"
                        value={getEquivalenceDisplay(fkey).srcUnit}
                        onChange={(e) => mergeEquivalenceDraft(fkey, { srcUnit: e.target.value })}
                        onBlur={() => commitCustomEquivalenceOnBlur(fkey)}
                        style={{ width: 120, textAlign: 'left', minHeight: 46, padding: '11px 12px' }}
                      />
                      <span style={{ fontSize: 14, color: 'var(--text2)' }}>entspricht</span>
                      <input
                        type="text"
                        inputMode="decimal"
                        className="form-input"
                        placeholder="Menge"
                        aria-label="Menge in Speicher-Einheit"
                        value={getEquivalenceDisplay(fkey).tgtAmt}
                        onChange={(e) => mergeEquivalenceDraft(fkey, { tgtAmt: e.target.value })}
                        onBlur={() => commitCustomEquivalenceOnBlur(fkey)}
                        style={{ width: 96, textAlign: 'left', minHeight: 46, padding: '11px 12px' }}
                      />
                      <span style={{ fontSize: 15, fontWeight: 600, color: 'var(--text1)' }}>
                        {getCanonicalStorageUnitLabel(fkey)}
                      </span>
                      <span style={{ fontSize: 13, color: 'var(--text3)' }}>(Ziel / Speicher)</span>
                    </div>
                    {derivedFactorHintLine(fkey) ? (
                      <p style={{ fontSize: 12, color: 'var(--text2)', marginTop: 10 }}>
                        {derivedFactorHintLine(fkey)}
                      </p>
                    ) : null}
                    <p style={{ fontSize: 12, color: 'var(--text3)', marginTop: 8, lineHeight: 1.5 }}>
                      Beispiel: <strong>1</strong> <strong>ml</strong> entspricht <strong>1,03</strong>{' '}
                      <strong>{getCanonicalStorageUnitLabel(fkey)}</strong>, wenn die CSV Milliliter liefert und die
                      Dichte etwa 1,03&nbsp;g/ml ist. Alle drei Felder leer lassen = keine Zusatz-Umrechnung. Tab /
                      Fokus weg oder Speichern übernimmt.
                    </p>
                  </div>
                )}
              </div>
            ))}
          </div>
        </div>
      )}

      <div className="card" style={{ padding: 16, marginBottom: 16 }}>
        <div className="form-label">4. type_conversions (JSON)</div>
        <p style={{ fontSize: 13, color: 'var(--text2)', marginTop: 8, lineHeight: 1.55 }}>
          Vom Vorschlag übernommen; bei Dropdowns 3b werden <code>source_unit</code>, ggf.{' '}
          <code>conversion_factor</code> und <code>custom_equivalence</code> gesetzt. Zusätzlich manuell z. B.
          Datumsformat.
        </p>
        <textarea
          className="form-input"
          style={{
            width: '100%',
            minHeight: 200,
            marginTop: 8,
            fontFamily: 'monospace',
            fontSize: 12,
            textAlign: 'left',
          }}
          value={typeConversionsText}
          onChange={(e) => {
            setTypeConversionsText(e.target.value)
            setCustomEquivalenceDraftByField({})
          }}
        />
      </div>

      {validationReport ? (
        <div className="card" style={{ padding: 16, marginBottom: 16, borderColor: validationReport.valid ? 'var(--accent)' : 'var(--danger)' }}>
          <div className="form-label" style={{ marginBottom: 8 }}>
            Formatprüfung (Vorlage){' '}
            {validationReport.valid ? <span style={{ color: 'var(--accent)' }}>— speicherfähig</span> : <span style={{ color: 'var(--danger)' }}>— Fehler beheben</span>}
          </div>
          <p style={{ fontSize: 12, color: 'var(--text3)', marginBottom: 10 }}>
            Ohne Zeilenaggregations-JSON; vollständige Prüfung inkl. Aggregation beim Speichern. Warnungen blockieren nicht.
          </p>
          {validationReport.errors?.length ? (
            <ul style={{ margin: '0 0 12px 1rem', color: 'var(--danger)', fontSize: 14 }}>
              {validationReport.errors.map((e, i) => (
                <li key={`e-${i}`}>
                  {e.message}
                  {e.hint ? <span style={{ display: 'block', fontSize: 12, color: 'var(--text2)', marginTop: 4 }}>{e.hint}</span> : null}
                </li>
              ))}
            </ul>
          ) : null}
          {validationReport.warnings?.length ? (
            <ul style={{ margin: '0 0 0 1rem', color: 'var(--text2)', fontSize: 13 }}>
              {validationReport.warnings.map((w, i) => (
                <li key={`w-${i}`}>
                  {w.message}
                  {w.hint ? <span style={{ display: 'block', fontSize: 12, color: 'var(--text3)', marginTop: 4 }}>{w.hint}</span> : null}
                </li>
              ))}
            </ul>
          ) : null}
        </div>
      ) : null}

      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
        <button type="button" className="btn btn-secondary" disabled={validating || saving} onClick={handleFormatCheck} style={{ minWidth: 160 }}>
          {validating ? (
            <>
              <Loader2 size={18} style={{ marginRight: 8, animation: 'spin 0.7s linear infinite' }} />
              Prüfen …
            </>
          ) : (
            'Format prüfen'
          )}
        </button>
        <button type="button" className="btn btn-primary" disabled={saving} onClick={handleSave} style={{ flex: 1, minWidth: 160 }}>
          {saving ? (
            <>
              <Loader2 size={18} style={{ marginRight: 8, animation: 'spin 0.7s linear infinite' }} />
              Speichern …
            </>
          ) : (
            <>
              <Save size={18} style={{ marginRight: 8 }} />
              Speichern
            </>
          )}
        </button>
        {!isNew && (
          <button type="button" className="btn btn-secondary" onClick={handleDelete} style={{ color: 'var(--danger)', borderColor: 'var(--danger)' }}>
            <Trash2 size={18} style={{ marginRight: 8 }} />
            Löschen
          </button>
        )}
      </div>
    </div>
  )
}
