/**
 * WorkspaceInput -- Prompt input with @file autocomplete, attachment bar,
 * voice toggle (generic audio capture hook), and data source selection.
 */

import React, {
  useState,
  useCallback,
  useRef,
  useEffect,
  useImperativeHandle,
  forwardRef,
} from 'react';
import { ProviderMultiSelect } from '../../../components/ProviderSelector';
import type { ProviderSelection } from '../../../components/ProviderSelector';
import { getPageIcon } from '../../../config/pageRegistry';
import { useVoiceStream } from '../../../hooks/useSpeechAudioCapture';
import api from '../../../api';
import type { WorkspaceFile, DataSource, FeatureDataSource } from './useWorkspace';

import { useLanguage } from '../../../providers/language/LanguageContext';
import { useVoiceCatalog } from '../../../contexts/VoiceCatalogContext';

export interface TreeItemDrop {
  id: string;
  type: 'file' | 'group';
  name: string;
}

/** An attachment chip shown in the input bar.
 *  Groups are kept as-is (show as single chip); file IDs are resolved at send-time. */
export type AttachmentItem =
  | { type: 'file'; id: string; name: string }
  | { type: 'group'; id: string; name: string; fileIds: string[] };

/** Parent resolves groups to concrete file IDs using persisted group tree. */
export type ResolveTreeItemsToFileIds = (items: TreeItemDrop[]) => Promise<string[]>;

export interface WorkspaceInputHandle {
  attachFileIds: (ids: string[]) => void;
  attachTreeItems: (items: TreeItemDrop[]) => Promise<void>;
  ingestTreeDataTransfer: (dt: DataTransfer) => Promise<boolean>;
}

interface WorkspaceInputProps {
  instanceId: string;
  onSend: (prompt: string, fileIds?: string[], dataSourceIds?: string[], featureDataSourceIds?: string[], options?: { requireNeutralization?: boolean }) => void;
  isProcessing: boolean;
  onStop: () => void;
  files: WorkspaceFile[];
  dataSources: DataSource[];
  featureDataSources?: FeatureDataSource[];
  resolveTreeItemsToFileIds: ResolveTreeItemsToFileIds;
  onFileUploadClick?: () => void;
  uploading?: boolean;
  providerSelection?: ProviderSelection;
  onProviderSelectionChange?: (selection: ProviderSelection) => void;
  isMobile?: boolean;
  onFeatureSourceDrop?: (params: { featureInstanceId: string; featureCode: string; tableName?: string; objectKey: string; label: string; fieldName?: string }) => void;
  onDataSourceDrop?: (params: { connectionId: string; sourceType: string; path: string; label: string; displayPath?: string }) => void;
  pendingAttachDsId?: string;
  onPendingAttachDsConsumed?: () => void;
  pendingAttachFdsId?: string;
  onPendingAttachFdsConsumed?: () => void;
  onPasteAsFile?: (file: File) => void;
  draftAppend?: string;
  onDraftAppendConsumed?: () => void;
  workflowId?: string | null;
  loadedAttachedDataSourceIds?: string[];
  loadedAttachedFeatureDataSourceIds?: string[];
  loadedNonce?: number;
}

function _itemsFromTreeDataTransfer(dt: DataTransfer): TreeItemDrop[] | null {
  const groupId = dt.getData('application/group-id');
  if (groupId) {
    return [{ id: groupId, type: 'group', name: dt.getData('text/plain') || groupId }];
  }
  const portaG = dt.getData('application/porta-group');
  if (portaG) {
    return [{ id: portaG, type: 'group', name: dt.getData('text/plain') || portaG }];
  }
  const treeItemsJson = dt.getData('application/tree-items');
  if (treeItemsJson) {
    try {
      const items = JSON.parse(treeItemsJson) as TreeItemDrop[];
      return Array.isArray(items) && items.length ? items : null;
    } catch {
      return null;
    }
  }
  const fileIdsJson = dt.getData('application/file-ids');
  if (fileIdsJson) {
    try {
      const ids: string[] = JSON.parse(fileIdsJson);
      return ids.map(id => ({ id, type: 'file' as const, name: id }));
    } catch {
      return null;
    }
  }
  const singleFileId = dt.getData('application/file-id');
  if (singleFileId) {
    const lbl = dt.getData('text/plain');
    const name = lbl && lbl !== singleFileId ? lbl : singleFileId;
    return [{ id: singleFileId, type: 'file', name }];
  }
  return null;
}

export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputProps>(function WorkspaceInput({
  instanceId,
  onSend,
  isProcessing,
  onStop,
  files,
  dataSources,
  featureDataSources = [],
  resolveTreeItemsToFileIds,
  onFileUploadClick,
  uploading = false,
  providerSelection,
  onProviderSelectionChange,
  isMobile = false,
  onFeatureSourceDrop,
  onDataSourceDrop,
  pendingAttachDsId,
  onPendingAttachDsConsumed,
  pendingAttachFdsId,
  onPendingAttachFdsConsumed,
  onPasteAsFile,
  draftAppend,
  onDraftAppendConsumed,
  workflowId,
  loadedAttachedDataSourceIds,
  loadedAttachedFeatureDataSourceIds,
  loadedNonce,
}, ref) {
  const { t } = useLanguage();
  const { languages: voiceCatalogLanguages } = useVoiceCatalog();
  const [prompt, setPrompt] = useState('');
  const [showAutocomplete, setShowAutocomplete] = useState(false);
  const [autocompleteFilter, setAutocompleteFilter] = useState('');
  const [treeDropOver, setTreeDropOver] = useState(false);
  const textareaAreaDragDepth = useRef(0);
  const [voiceActive, setVoiceActive] = useState(false);
  const [voiceLanguage, setVoiceLanguage] = useState('de-DE');
  const [showLangPicker, setShowLangPicker] = useState(false);
  const _sttPrefsLoaded = useRef(false);
  const [attachments, setAttachments] = useState<AttachmentItem[]>([]);
  const [attachedDataSourceIds, setAttachedDataSourceIds] = useState<string[]>([]);
  const [attachedFeatureDataSourceIds, setAttachedFeatureDataSourceIds] = useState<string[]>([]);
  const [neutralizeActive, setNeutralizeActive] = useState(false);
  const textareaRef = useRef<HTMLTextAreaElement>(null);

  const _appendAttachment = useCallback((item: AttachmentItem) => {
    setAttachments(prev => prev.some(a => a.id === item.id) ? prev : [...prev, item]);
  }, []);

  const _appendFileIds = useCallback((ids: string[]) => {
    if (!ids.length) return;
    setAttachments(prev => {
      const existing = new Set(prev.map(a => a.id));
      const added = ids.filter(id => !existing.has(id)).map(id => ({ type: 'file' as const, id, name: id }));
      return added.length ? [...prev, ...added] : prev;
    });
  }, []);

  useEffect(() => {
    if (draftAppend) {
      setPrompt(prev => prev + (prev ? '\n' : '') + draftAppend);
      onDraftAppendConsumed?.();
    }
  }, [draftAppend, onDraftAppendConsumed]);

  const _persistAttachments = useCallback((dsIds: string[], fdsIds: string[]) => {
    if (!instanceId || !workflowId) return;
    api.patch(`/api/workspace/${instanceId}/workflows/${workflowId}/attachments`, {
      dataSourceIds: dsIds,
      featureDataSourceIds: fdsIds,
    }).catch(err => console.warn('Failed to persist chat attachments:', err));
  }, [instanceId, workflowId]);

  useEffect(() => {
    if (!pendingAttachDsId) return;
    setAttachedDataSourceIds(prev => {
      if (prev.includes(pendingAttachDsId)) return prev;
      const next = [...prev, pendingAttachDsId];
      _persistAttachments(next, attachedFeatureDataSourceIds);
      return next;
    });
    onPendingAttachDsConsumed?.();
  }, [pendingAttachDsId, onPendingAttachDsConsumed, _persistAttachments, attachedFeatureDataSourceIds]);

  useEffect(() => {
    if (!pendingAttachFdsId) return;
    setAttachedFeatureDataSourceIds(prev => {
      if (prev.includes(pendingAttachFdsId)) return prev;
      const next = [...prev, pendingAttachFdsId];
      _persistAttachments(attachedDataSourceIds, next);
      return next;
    });
    onPendingAttachFdsConsumed?.();
  }, [pendingAttachFdsId, onPendingAttachFdsConsumed, _persistAttachments, attachedDataSourceIds]);

  const _prevWorkflowId = useRef<string | null | undefined>(undefined);
  useEffect(() => {
    if (_prevWorkflowId.current === undefined) {
      _prevWorkflowId.current = workflowId ?? null;
      return;
    }
    const wasNull = !_prevWorkflowId.current;
    _prevWorkflowId.current = workflowId ?? null;
    if (wasNull && workflowId && (attachedDataSourceIds.length > 0 || attachedFeatureDataSourceIds.length > 0)) {
      _persistAttachments(attachedDataSourceIds, attachedFeatureDataSourceIds);
    }
  }, [workflowId, _persistAttachments, attachedDataSourceIds, attachedFeatureDataSourceIds]);

  useEffect(() => {
    if (loadedNonce === undefined) return;
    setAttachments([]);
    setAttachedDataSourceIds(Array.isArray(loadedAttachedDataSourceIds) ? [...loadedAttachedDataSourceIds] : []);
    setAttachedFeatureDataSourceIds(Array.isArray(loadedAttachedFeatureDataSourceIds) ? [...loadedAttachedFeatureDataSourceIds] : []);
  }, [loadedNonce, loadedAttachedDataSourceIds, loadedAttachedFeatureDataSourceIds]);

  const _reconciledDsForNonce = useRef<number | undefined>(undefined);
  const _reconciledFdsForNonce = useRef<number | undefined>(undefined);

  useEffect(() => {
    if (loadedNonce === undefined) return;
    if (_reconciledDsForNonce.current === loadedNonce) return;
    if (dataSources.length === 0) return;
    _reconciledDsForNonce.current = loadedNonce;
    const validIds = new Set(dataSources.map(d => d.id));
    setAttachedDataSourceIds(prev => {
      const filtered = prev.filter(id => validIds.has(id));
      return filtered.length === prev.length ? prev : filtered;
    });
  }, [loadedNonce, dataSources]);

  useEffect(() => {
    if (loadedNonce === undefined) return;
    if (_reconciledFdsForNonce.current === loadedNonce) return;
    if (featureDataSources.length === 0) return;
    _reconciledFdsForNonce.current = loadedNonce;
    const validIds = new Set(featureDataSources.map(d => d.id));
    setAttachedFeatureDataSourceIds(prev => {
      const filtered = prev.filter(id => validIds.has(id));
      return filtered.length === prev.length ? prev : filtered;
    });
  }, [loadedNonce, featureDataSources]);

  const promptBeforeVoiceRef = useRef('');
  const finalizedTextRef = useRef('');
  const currentInterimRef = useRef('');

  useEffect(() => {
    if (_sttPrefsLoaded.current) return;
    _sttPrefsLoaded.current = true;
    fetch('/api/voice/preferences', { credentials: 'include' })
      .then(r => r.ok ? r.json() : null)
      .then(data => { if (data?.sttLanguage) setVoiceLanguage(data.sttLanguage); })
      .catch(() => {});
  }, []);

  const _resolveGroupItem = useCallback(async (item: TreeItemDrop): Promise<AttachmentItem> => {
    const fileIds = await resolveTreeItemsToFileIds([item]);
    return { type: 'group', id: item.id, name: item.name, fileIds };
  }, [resolveTreeItemsToFileIds]);

  /** Ingest a DataTransfer and append the right attachment chips. Returns true if handled. */
  const _ingestDataTransfer = useCallback(async (dt: DataTransfer): Promise<boolean> => {
    // Group with drag-time snapshot of its file IDs
    const groupId = dt.getData('application/group-id') || dt.getData('application/porta-group');
    if (groupId) {
      const name = dt.getData('text/plain') || groupId;
      const snapshotJson = dt.getData('application/group-file-ids');
      let fileIds: string[] = [];
      if (snapshotJson) {
        try {
          const parsed: unknown = JSON.parse(snapshotJson);
          if (Array.isArray(parsed)) fileIds = parsed.filter((f): f is string => typeof f === 'string');
        } catch { /* ignore */ }
      }
      if (!fileIds.length) {
        fileIds = await resolveTreeItemsToFileIds([{ id: groupId, type: 'group', name }]);
      }
      _appendAttachment({ type: 'group', id: groupId, name, fileIds });
      return true;
    }
    // Generic tree-items (may contain groups or files)
    const items = _itemsFromTreeDataTransfer(dt);
    if (!items?.length) return false;
    await Promise.all(items.map(async item => {
      if (item.type === 'group') {
        _appendAttachment(await _resolveGroupItem(item));
      } else {
        _appendAttachment({ type: 'file', id: item.id, name: item.name });
      }
    }));
    return true;
  }, [resolveTreeItemsToFileIds, _appendAttachment, _resolveGroupItem]);

  useImperativeHandle(ref, () => ({
    attachFileIds: (ids: string[]) => _appendFileIds(ids),
    attachTreeItems: async (items: TreeItemDrop[]) => {
      await Promise.all(items.map(async item => {
        if (item.type === 'group') {
          _appendAttachment(await _resolveGroupItem(item));
        } else {
          _appendAttachment({ type: 'file', id: item.id, name: item.name });
        }
      }));
    },
    ingestTreeDataTransfer: (dt: DataTransfer) => _ingestDataTransfer(dt),
  }), [_appendFileIds, _appendAttachment, _resolveGroupItem, _ingestDataTransfer]);

  const _extractFileRefs = useCallback(
    (text: string): string[] => {
      const pattern = /@([\w.-]+)/g;
      const matched: string[] = [];
      let match;
      while ((match = pattern.exec(text)) !== null) {
        const ref = match[1];
        const file = files.find(
          f => f.fileName === ref || f.fileName.toLowerCase() === ref.toLowerCase(),
        );
        if (file && !matched.includes(file.id)) {
          matched.push(file.id);
        }
      }
      return matched;
    },
    [files],
  );

  const hasFileOrSourceAttachments =
    attachments.length > 0 || attachedDataSourceIds.length > 0 || attachedFeatureDataSourceIds.length > 0;
  const _canSend = Boolean(prompt.trim()) || attachments.length > 0;

  const _handleSend = useCallback(() => {
    if ((!prompt.trim() && attachments.length === 0) || isProcessing) return;
    if (voiceActive) {
      voiceStream.stop();
      setVoiceActive(false);
      promptBeforeVoiceRef.current = '';
      finalizedTextRef.current = '';
      currentInterimRef.current = '';
    }
    const inlineFileIds = _extractFileRefs(prompt);
    const attachedFileIds = attachments.flatMap(a => a.type === 'file' ? [a.id] : a.fileIds);
    const allFileIds = [...new Set([...attachedFileIds, ...inlineFileIds])];
    const options = neutralizeActive ? { requireNeutralization: true } : undefined;
    onSend(prompt.trim(), allFileIds, attachedDataSourceIds, attachedFeatureDataSourceIds, options);
    setPrompt('');
    setShowAutocomplete(false);
    setAttachments([]);
  }, [prompt, isProcessing, voiceActive, voiceStream, _extractFileRefs, attachments, attachedDataSourceIds, attachedFeatureDataSourceIds, neutralizeActive, onSend]);

  const _handleKeyDown = useCallback(
    (e: React.KeyboardEvent) => {
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        _handleSend();
      }
    },
    [_handleSend],
  );

  const _handleChange = useCallback(
    (e: React.ChangeEvent<HTMLTextAreaElement>) => {
      const value = e.target.value;
      setPrompt(value);
      const cursorPos = e.target.selectionStart;
      const textBeforeCursor = value.slice(0, cursorPos);
      const atMatch = textBeforeCursor.match(/@([\w.-]*)$/);
      if (atMatch) {
        setAutocompleteFilter(atMatch[1].toLowerCase());
        setShowAutocomplete(true);
      } else {
        setShowAutocomplete(false);
      }
    },
    [],
  );

  const _insertFileRef = useCallback(
    (fileName: string) => {
      const textarea = textareaRef.current;
      if (!textarea) return;
      const cursorPos = textarea.selectionStart;
      const textBefore = prompt.slice(0, cursorPos);
      const textAfter = prompt.slice(cursorPos);
      const atStart = textBefore.lastIndexOf('@');
      const newText = textBefore.slice(0, atStart) + `@${fileName} ` + textAfter;
      setPrompt(newText);
      setShowAutocomplete(false);
      textarea.focus();
    },
    [prompt],
  );

  const _removeAttachment = useCallback((id: string) => {
    setAttachments(prev => prev.filter(a => a.id !== id));
  }, []);

  const _removeAttachedDataSource = useCallback((dsId: string) => {
    setAttachedDataSourceIds(prev => {
      const next = prev.filter(id => id !== dsId);
      _persistAttachments(next, attachedFeatureDataSourceIds);
      return next;
    });
  }, [_persistAttachments, attachedFeatureDataSourceIds]);

  const _toggleFeatureDataSource = useCallback((fdsId: string) => {
    setAttachedFeatureDataSourceIds(prev => {
      const next = prev.includes(fdsId) ? prev.filter(id => id !== fdsId) : [...prev, fdsId];
      _persistAttachments(attachedDataSourceIds, next);
      return next;
    });
  }, [_persistAttachments, attachedDataSourceIds]);

  const _buildPromptFromRefs = useCallback(() => {
    const parts = [
      promptBeforeVoiceRef.current,
      finalizedTextRef.current,
      currentInterimRef.current,
    ].filter(Boolean);
    return parts.join(' ');
  }, []);

  const voiceStream = useVoiceStream({
    onFinal: (text) => {
      finalizedTextRef.current = finalizedTextRef.current
        ? `${finalizedTextRef.current} ${text}`
        : text;
      currentInterimRef.current = '';
      setPrompt(_buildPromptFromRefs());
    },
    onInterim: (text) => {
      currentInterimRef.current = text;
      setPrompt(_buildPromptFromRefs());
    },
    onError: (error) => {
      console.warn('Workspace voice stream error', error);
      setVoiceActive(false);
    },
    onStatusChange: (nextStatus) => {
      if (nextStatus === 'idle' || nextStatus === 'error') {
        setVoiceActive(false);
      }
    },
  });

  const _stopVoiceCapture = useCallback(() => {
    if (currentInterimRef.current) {
      finalizedTextRef.current = finalizedTextRef.current
        ? `${finalizedTextRef.current} ${currentInterimRef.current}`
        : currentInterimRef.current;
      currentInterimRef.current = '';
    }
    setPrompt(_buildPromptFromRefs());
    voiceStream.stop();
    setVoiceActive(false);
  }, [voiceStream, _buildPromptFromRefs]);

  const _toggleVoice = useCallback(async () => {
    if (voiceActive) {
      _stopVoiceCapture();
      return;
    }

    promptBeforeVoiceRef.current = prompt;
    finalizedTextRef.current = '';
    currentInterimRef.current = '';
    try {
      setVoiceActive(true);
      await voiceStream.start(voiceLanguage);
    } catch {
      setVoiceActive(false);
    }
  }, [voiceActive, prompt, voiceStream, voiceLanguage, _stopVoiceCapture]);

  const filteredFiles = showAutocomplete
    ? files.filter(f => f.fileName.toLowerCase().includes(autocompleteFilter))
    : [];

  const hasAttachments = hasFileOrSourceAttachments;
  const _horizontalPadding = isMobile ? 12 : 24;
  const _controlSize = isMobile ? 38 : 40;

  const _handlePaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
    if (!onPasteAsFile) return;
    const text = e.clipboardData.getData('text/plain');
    if (text && text.length >= 1000) {
      e.preventDefault();
      const blob = new Blob([text], { type: 'text/plain' });
      const file = new File([blob], `pasted-text-${Date.now()}.txt`, { type: 'text/plain' });
      onPasteAsFile(file);
    }
  }, [onPasteAsFile]);

  const _isTreeMimeDrag = useCallback((e: React.DragEvent) => {
    const types = e.dataTransfer.types;
    return (
      types.includes('application/tree-items') ||
      types.includes('application/group-file-ids') ||
      types.includes('application/group-id') ||
      types.includes('application/porta-group') ||
      types.includes('application/file-id') ||
      types.includes('application/file-ids')
    );
  }, []);

  const _handlePromptDragOver = useCallback((e: React.DragEvent) => {
    if (
      _isTreeMimeDrag(e) ||
      e.dataTransfer.types.includes('application/chat-id') ||
      e.dataTransfer.types.includes('application/feature-source') ||
      e.dataTransfer.types.includes('application/datasource')
    ) {
      e.preventDefault();
      e.dataTransfer.dropEffect = 'copy';
      setTreeDropOver(true);
    }
  }, [_isTreeMimeDrag]);

  const _handlePromptDragLeave = useCallback((e: React.DragEvent) => {
    if (!e.relatedTarget || !(e.currentTarget as Node).contains(e.relatedTarget as Node)) {
      setTreeDropOver(false);
    }
  }, []);

  const _handleTextareaDragEnter = useCallback((e: React.DragEvent) => {
    if (!_isTreeMimeDrag(e)) return;
    e.preventDefault();
    textareaAreaDragDepth.current += 1;
    setTreeDropOver(true);
  }, [_isTreeMimeDrag]);

  const _handleTextareaDragLeave = useCallback((e: React.DragEvent) => {
    if (!_isTreeMimeDrag(e)) return;
    e.preventDefault();
    textareaAreaDragDepth.current = Math.max(0, textareaAreaDragDepth.current - 1);
    if (textareaAreaDragDepth.current === 0) {
      setTreeDropOver(false);
    }
  }, [_isTreeMimeDrag]);

  const _handleTextareaDragOver = useCallback((e: React.DragEvent) => {
    if (!_isTreeMimeDrag(e)) return;
    e.preventDefault();
    e.stopPropagation();
    e.dataTransfer.dropEffect = 'copy';
  }, [_isTreeMimeDrag]);

  const _handlePromptDrop = useCallback(async (e: React.DragEvent) => {
    textareaAreaDragDepth.current = 0;
    setTreeDropOver(false);

    const chatId = e.dataTransfer.getData('application/chat-id');
    if (chatId) {
      e.preventDefault();
      const chatLabel = e.dataTransfer.getData('text/plain');
      const refLabel = chatLabel ? `[Chat: ${chatLabel}]` : `[Chat: ${chatId.slice(0, 8)}]`;
      setPrompt(prev => (prev ? `${prev} ${refLabel}` : refLabel));
      return;
    }

    const featureSourceJson = e.dataTransfer.getData('application/feature-source');
    if (featureSourceJson && onFeatureSourceDrop) {
      e.preventDefault();
      const params = JSON.parse(featureSourceJson);
      onFeatureSourceDrop(params);
      return;
    }

    const dataSourceJson = e.dataTransfer.getData('application/datasource');
    if (dataSourceJson && onDataSourceDrop) {
      e.preventDefault();
      const params = JSON.parse(dataSourceJson);
      onDataSourceDrop(params);
      return;
    }

    const handled = await _ingestDataTransfer(e.dataTransfer);
    if (handled) {
      e.preventDefault();
      textareaRef.current?.focus();
    }
  }, [_ingestDataTransfer, onFeatureSourceDrop, onDataSourceDrop]);

  return (
    <div
      style={{
        borderTop: '1px solid var(--border-color, #e0e0e0)',
        position: 'relative',
        flexShrink: 0,
        outline: treeDropOver ? '2px dashed var(--primary-color, #F25843)' : 'none',
        background: treeDropOver ? 'var(--primary-dark-bg, rgba(242, 88, 67, 0.08))' : undefined,
        transition: 'background 0.15s, outline 0.15s',
      }}
      onDragOver={_handlePromptDragOver}
      onDragLeave={_handlePromptDragLeave}
      onDrop={e => void _handlePromptDrop(e)}
    >
      {hasAttachments && (
        <div style={{
          padding: `8px ${_horizontalPadding}px`,
          display: 'flex',
          gap: 6,
          flexWrap: 'wrap',
          borderBottom: '1px solid var(--border-color, #f0f0f0)',
          background: '#fafafa',
        }}>
          {attachments.map(att => {
            const isGroup = att.type === 'group';
            const label = isGroup
              ? att.name
              : (files.find(f => f.id === att.id)?.fileName || att.name || att.id);
            const chipBg = isGroup ? '#e8f5e9' : '#e3f2fd';
            const chipColor = isGroup ? '#1b5e20' : '#1565c0';
            const chipBorder = isGroup ? '1px solid #c8e6c9' : '1px solid #bbdefb';
            const countBadge = isGroup ? ` (${att.fileIds.length})` : '';
            return (
              <span
                key={att.id}
                title={isGroup ? `${att.fileIds.length} Datei(en) in dieser Gruppe` : label}
                style={{
                  display: 'inline-flex', alignItems: 'center', gap: 4,
                  padding: '3px 8px', borderRadius: 12, fontSize: 11,
                  background: chipBg, color: chipColor, fontWeight: 500,
                  border: chipBorder,
                }}
              >
                {isGroup ? '📁' : '📄'} {label}{countBadge}
                <button
                  type="button"
                  onClick={() => _removeAttachment(att.id)}
                  style={{
                    border: 'none', background: 'none', cursor: 'pointer',
                    fontSize: 12, color: chipColor, padding: 0, lineHeight: 1,
                  }}
                >
                  ×
                </button>
              </span>
            );
          })}
          {attachedDataSourceIds.map(dsId => {
            const ds = dataSources.find(d => d.id === dsId);
            return (
              <span
                key={dsId}
                style={{
                  display: 'inline-flex', alignItems: 'center', gap: 4,
                  padding: '3px 8px', borderRadius: 12, fontSize: 11,
                  background: '#e8f5e9', color: '#2e7d32', fontWeight: 500,
                }}
              >
                🔗 {ds?.label || ds?.path || dsId}
                <button
                  type="button"
                  onClick={() => _removeAttachedDataSource(dsId)}
                  style={{
                    border: 'none', background: 'none', cursor: 'pointer',
                    fontSize: 12, color: '#2e7d32', padding: 0, lineHeight: 1,
                  }}
                >
                  ×
                </button>
              </span>
            );
          })}
          {attachedFeatureDataSourceIds.map(fdsId => {
            const fds = featureDataSources.find(d => d.id === fdsId);
            const fdsIcon = fds ? getPageIcon(`feature.${fds.featureCode}`) : null;
            return (
              <span
                key={fdsId}
                style={{
                  display: 'inline-flex', alignItems: 'center', gap: 4,
                  padding: '3px 8px', borderRadius: 12, fontSize: 11,
                  background: '#f3e5f5', color: '#7b1fa2', fontWeight: 500,
                }}
              >
                <span style={{ display: 'flex', alignItems: 'center', fontSize: 12 }}>{fdsIcon || '\uD83D\uDDC3\uFE0F'}</span>
                {fds?.label || fdsId} – {fds?.tableName || ''}
                <button
                  type="button"
                  onClick={() => _toggleFeatureDataSource(fdsId)}
                  style={{
                    border: 'none', background: 'none', cursor: 'pointer',
                    fontSize: 12, color: '#7b1fa2', padding: 0, lineHeight: 1,
                  }}
                >
                  ×
                </button>
              </span>
            );
          })}
        </div>
      )}

      {showAutocomplete && filteredFiles.length > 0 && (
        <div style={{
          position: 'absolute',
          bottom: '100%',
          left: _horizontalPadding,
          right: _horizontalPadding,
          maxHeight: 200,
          overflowY: 'auto',
          background: '#fff',
          border: '1px solid var(--border-color, #e0e0e0)',
          borderRadius: 8,
          boxShadow: '0 -2px 8px rgba(0,0,0,0.1)',
          zIndex: 10,
        }}>
          {filteredFiles.slice(0, 10).map(f => (
            <div
              key={f.id}
              role="presentation"
              onClick={() => _insertFileRef(f.fileName)}
              style={{
                padding: '8px 12px',
                cursor: 'pointer',
                fontSize: 13,
                borderBottom: '1px solid #f0f0f0',
              }}
              onMouseEnter={e => (e.currentTarget.style.background = '#f5f5f5')}
              onMouseLeave={e => (e.currentTarget.style.background = '')}
            >
              @{f.fileName}
              <span style={{ color: '#999', marginLeft: 8, fontSize: 11 }}>
                {f.mimeType} · {(f.fileSize / 1024).toFixed(1)}KB
              </span>
            </div>
          ))}
        </div>
      )}

      <div style={{
        padding: `8px ${_horizontalPadding}px 12px`,
        display: 'flex',
        gap: 8,
        alignItems: isMobile ? 'stretch' : 'flex-end',
        flexWrap: isMobile ? 'wrap' : 'nowrap',
      }}>
        <textarea
          ref={textareaRef}
          value={prompt}
          onChange={_handleChange}
          onKeyDown={_handleKeyDown}
          onPaste={_handlePaste}
          onDragEnter={_handleTextareaDragEnter}
          onDragLeave={_handleTextareaDragLeave}
          onDragOver={_handleTextareaDragOver}
          onDrop={e => void _handlePromptDrop(e)}
          placeholder={
            attachments.length > 0
              ? t('Nachricht eingeben … ({n} Anhang/Anhänge)', { n: String(attachments.length) })
              : t('Geben Sie eine Nachricht ein — Dateien hierher ziehen oder @file verwenden')
          }
          disabled={isProcessing}
          style={{
            flex: 1,
            minHeight: isMobile ? 52 : 48,
            maxHeight: 120,
            resize: 'vertical',
            padding: '10px 14px',
            borderRadius: 8,
            border: treeDropOver ? '2px dashed var(--primary-color, #F25843)' : '1px solid var(--border-color, #ccc)',
            fontSize: 14,
            fontFamily: 'inherit',
            outline: 'none',
            flexBasis: isMobile ? '100%' : undefined,
            boxSizing: 'border-box',
          }}
          rows={1}
        />

        <button
          type="button"
          onClick={onFileUploadClick}
          disabled={uploading || isProcessing}
          title={t('Datei anhängen')}
          style={{
            width: _controlSize, height: _controlSize, borderRadius: 8, border: '1px solid var(--border-color, #ddd)',
            background: 'var(--secondary-bg, #f5f5f5)',
            color: uploading ? 'var(--primary-color, #F25843)' : 'var(--text-secondary, #666)',
            cursor: uploading || isProcessing ? 'not-allowed' : 'pointer',
            fontSize: 16, display: 'flex', alignItems: 'center', justifyContent: 'center',
            opacity: isProcessing ? 0.5 : 1,
          }}
        >
          {uploading ? '...' : '+'}
        </button>

        {onProviderSelectionChange && providerSelection && (
          <ProviderMultiSelect
            selection={providerSelection}
            onChange={onProviderSelectionChange}
            showLabel={false}
            disabled={isProcessing}
          />
        )}

        <div style={{ position: 'relative', display: 'flex', gap: 2 }}>
          <button
            type="button"
            onClick={() => setShowLangPicker(prev => !prev)}
            title={t('Sprache wählen')}
            style={{
              height: _controlSize, borderRadius: '8px 0 0 8px', border: '1px solid var(--border-color, #ddd)',
              borderRight: 'none',
              background: 'var(--secondary-bg, #f5f5f5)',
              color: '#666', cursor: 'pointer', fontSize: 10, padding: '0 6px',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
            }}
          >
            {voiceLanguage.split('-')[0].toUpperCase()}
          </button>
          <button
            type="button"
            onClick={_toggleVoice}
            title={voiceActive ? t('Aufnahme stoppen') : t('Sprachaufnahme starten')}
            style={{
              width: _controlSize, height: _controlSize, borderRadius: '0 8px 8px 0', border: 'none',
              background: voiceActive ? '#f44336' : 'var(--secondary-bg, #f5f5f5)',
              color: voiceActive ? '#fff' : '#666',
              cursor: 'pointer', fontSize: 18, display: 'flex', alignItems: 'center', justifyContent: 'center',
            }}
          >
            {voiceActive ? '■' : '\uD83C\uDFA4'}
          </button>
          {showLangPicker && (
            <div style={{
              position: 'absolute', bottom: '100%', right: 0, marginBottom: 4,
              background: '#fff', border: '1px solid var(--border-color, #e0e0e0)',
              borderRadius: 8, boxShadow: '0 -2px 8px rgba(0,0,0,0.1)', zIndex: 20,
              maxHeight: 240, overflowY: 'auto', minWidth: 160,
            }}>
              {voiceCatalogLanguages.map(lang => (
                <div
                  key={lang.bcp47}
                  role="presentation"
                  onClick={() => {
                    setVoiceLanguage(lang.bcp47);
                    setShowLangPicker(false);
                    fetch('/api/voice/preferences', { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sttLanguage: lang.bcp47 }) }).catch(() => {});
                  }}
                  style={{
                    padding: '8px 12px', cursor: 'pointer', fontSize: 13,
                    background: lang.bcp47 === voiceLanguage ? 'var(--primary-color, #F25843)' : 'transparent',
                    color: lang.bcp47 === voiceLanguage ? '#fff' : 'var(--text-primary, #333)',
                  }}
                  onMouseEnter={e => { if (lang.bcp47 !== voiceLanguage) e.currentTarget.style.background = 'var(--bg-secondary, #f5f5f5)'; }}
                  onMouseLeave={e => { if (lang.bcp47 !== voiceLanguage) e.currentTarget.style.background = ''; }}
                >
                  {lang.flag ? `${lang.flag} ` : ''}{lang.label} ({lang.bcp47})
                </div>
              ))}
            </div>
          )}
        </div>

        <button
          type="button"
          onClick={() => setNeutralizeActive(v => !v)}
          title={neutralizeActive ? t('Neutralisierung aktiv, klicken zum Deaktivieren') : t('Neutralisierung aus, klicken zum Aktivieren')}
          style={{
            padding: '8px 10px', borderRadius: 8, border: '1px solid',
            borderColor: neutralizeActive ? '#166534' : 'var(--border-color, #d1d5db)',
            background: neutralizeActive ? '#dcfce7' : 'transparent',
            cursor: 'pointer', fontSize: '1rem', lineHeight: 1,
            opacity: neutralizeActive ? 1 : 0.5,
            transition: 'all 0.15s',
          }}
        >
          🔒
        </button>

        {isProcessing ? (
          <button
            type="button"
            onClick={onStop}
            style={{
              padding: '10px 20px', borderRadius: 8, border: 'none',
              background: '#f44336', color: '#fff', cursor: 'pointer', fontWeight: 600,
              minWidth: isMobile ? 84 : undefined,
            }}
          >
            Stop
          </button>
        ) : (
          <button
            type="button"
            onClick={_handleSend}
            disabled={!_canSend}
            style={{
              padding: '10px 20px', borderRadius: 8, border: 'none',
              background: _canSend ? 'var(--primary-color, #F25843)' : 'var(--color-gray-disabled, #ccc)',
              color: '#fff', cursor: _canSend ? 'pointer' : 'default', fontWeight: 600,
              minWidth: isMobile ? 84 : undefined,
            }}
          >
            Send
          </button>
        )}
      </div>
    </div>
  );
});
