// DM-Frontend: DMsView (Conversation-Liste links + ausgewählte Konversation
// rechts), NewDMModal (User-Picker mit Live-Search), useDMUnread (Hook für den
// Sidebar-Badge mit WS-Auto-Update + Polling-Fallback).
//
// Konvention wie der Rest der Codebase: Plain-JS-Komponenten via window-Globals
// (keine Module). Babel-standalone transpiliert in-browser.

const { useState, useEffect, useRef, useCallback } = React;

// ─── Hook: Unread-Badge ─────────────────────────────────────────────────────
// Wird in der Sidebar genutzt, um die Zahl ungelesener DMs anzuzeigen. Holt sich
// initial den aggregierten Count via REST und hört über window.ChatClient
// (singleton WS) auf Events `dm-message`/`dm-read`/`dm-deleted` für Live-
// Updates. 60-Sekunden-Polling als Fallback.
function useDMUnread(user) {
  const [count, setCount] = useState(0);

  const refresh = useCallback(async () => {
    if (!user) return;
    const { ok, data } = await window.apiFetch('/api/dm/unread-count');
    if (ok) setCount(data?.count || 0);
  }, [user]);

  useEffect(() => {
    if (!user) return;
    refresh();
    const t = setInterval(refresh, 60 * 1000);
    return () => clearInterval(t);
  }, [user, refresh]);

  // Live-Update über ChatClient (singleton). 'any' feuert für jede WS-Nachricht.
  useEffect(() => {
    const cc = window.ChatClient;
    if (!cc) return;
    const off = cc.on('any', (msg) => {
      if (!msg) return;
      if (msg.type === 'dm-message' || msg.type === 'dm-read' || msg.type === 'dm-deleted') {
        refresh();
      }
    });
    return off;
  }, [refresh]);

  return { count, refresh };
}

// ─── Helper: Avatar-URL absolutifizieren (gleiche Logik wie ui.jsx Avatar) ──
function dmAvatar(path) {
  if (!path) return null;
  if (/^https?:\/\//i.test(path) || path.startsWith('data:')) return path;
  const apiOrigin = window.HAIKARA_APPS?.api;
  return (apiOrigin || '') + (path.startsWith('/') ? path : '/' + path);
}

// ─── Helper: relative Zeit ("2 Min", "gestern", "12. Mai") ──────────────────
function formatRelative(iso) {
  if (!iso) return '';
  const d = new Date(iso);
  const diff = Date.now() - d.getTime();
  const m = Math.floor(diff / 60000);
  if (m < 1) return 'gerade';
  if (m < 60) return `${m} Min`;
  const h = Math.floor(m / 60);
  if (h < 24) return `${h} Std`;
  const days = Math.floor(h / 24);
  if (days < 7) return `${days} T`;
  return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
}
function formatTime(iso) {
  if (!iso) return '';
  return new Date(iso).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
}

// ─── ConversationList ───────────────────────────────────────────────────────
const ConversationList = ({ conversations, selectedId, onSelect, onNew, currentUserId }) => {
  return (
    <div style={{
      display: 'flex', flexDirection: 'column',
      width: 280, flexShrink: 0,
      borderRight: '1px solid var(--border)',
      background: 'var(--surface)',
      height: '100%', minHeight: 0,
    }}>
      <div style={{ padding: '14px 14px 10px', borderBottom: '1px solid var(--border)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <h3 style={{ margin: 0, fontSize: 14, fontWeight: 700, color: 'var(--text)', flex: 1 }}>
            Direktnachrichten
          </h3>
          <button onClick={onNew} title="Neue Nachricht"
            style={{
              background: 'var(--accent)', border: 'none', borderRadius: 6,
              color: '#fff', cursor: 'pointer', padding: '5px 8px',
              display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, fontWeight: 600,
            }}>
            <Icon name="plus" size={12} color="#fff" /> Neu
          </button>
        </div>
      </div>
      <div style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
        {conversations.length === 0 && (
          <div style={{ padding: '32px 16px', textAlign: 'center', fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.6 }}>
            Noch keine Konversationen.<br/>
            Klicke <strong>Neu</strong>, um jemanden anzuschreiben.
          </div>
        )}
        {conversations.map(c => {
          const isActive = c.id === selectedId;
          const isUnread = c.unreadCount > 0;
          const lastBody = c.lastMessage?.deletedAt
            ? '[Nachricht gelöscht]'
            : (c.lastMessage?.body || 'Noch keine Nachricht');
          const lastIsMine = c.lastMessage?.senderId === currentUserId;
          return (
            <button
              key={c.id}
              onClick={() => onSelect(c.id)}
              style={{
                display: 'flex', alignItems: 'center', gap: 10,
                width: '100%', padding: '10px 14px',
                background: isActive ? 'var(--accent-dim)' : 'transparent',
                border: 'none', borderBottom: '1px solid var(--border)',
                cursor: 'pointer', textAlign: 'left',
                color: 'var(--text)',
              }}
              onMouseEnter={e => { if (!isActive) e.currentTarget.style.background = 'var(--surface-2)'; }}
              onMouseLeave={e => { if (!isActive) e.currentTarget.style.background = 'transparent'; }}
            >
              <Avatar initials={c.other.initials || '??'} avatar={c.other.avatar} size={36} />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
                  <div style={{ fontSize: 13, fontWeight: isUnread ? 700 : 600, color: 'var(--text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                    {c.other.name}
                  </div>
                  <div style={{ flex: 1 }} />
                  <div style={{ fontSize: 10, color: 'var(--text-dim)', fontFamily: 'var(--font-mono)' }}>
                    {c.lastMessageAt ? formatRelative(c.lastMessageAt) : ''}
                  </div>
                </div>
                <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 1 }}>
                  <div style={{
                    fontSize: 11, color: isUnread ? 'var(--text)' : 'var(--text-muted)',
                    fontWeight: isUnread ? 600 : 400,
                    overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1,
                  }}>
                    {lastIsMine && c.lastMessage && <span style={{ color: 'var(--text-dim)' }}>Du: </span>}
                    {lastBody}
                  </div>
                  {isUnread && (
                    <span style={{
                      background: 'var(--accent)', color: '#fff',
                      fontSize: 10, fontWeight: 700, fontFamily: 'var(--font-mono)',
                      padding: '1px 6px', borderRadius: 10, minWidth: 16, textAlign: 'center',
                    }}>{c.unreadCount}</span>
                  )}
                </div>
              </div>
            </button>
          );
        })}
      </div>
    </div>
  );
};

// ─── ConversationView ───────────────────────────────────────────────────────
const ConversationView = ({ conversation, currentUserId, onMessageSent, onMessagesRead }) => {
  const [messages, setMessages] = useState([]);
  const [loading, setLoading] = useState(true);
  const [hasMore, setHasMore] = useState(false);
  const [loadingOlder, setLoadingOlder] = useState(false);
  const [input, setInput] = useState('');
  const [sending, setSending] = useState(false);
  const [editingId, setEditingId] = useState(null);
  const [editingBody, setEditingBody] = useState('');
  const listRef = useRef(null);
  const bottomRef = useRef(null);
  const inputRef = useRef(null);

  // Initial load + reset bei Konversationswechsel
  useEffect(() => {
    if (!conversation) return;
    let cancelled = false;
    setLoading(true);
    setMessages([]);
    (async () => {
      const { ok, data } = await window.apiFetch(`/api/dm/${conversation.id}/messages`);
      if (cancelled) return;
      if (ok) {
        setMessages(data.messages || []);
        setHasMore(!!data.hasMore);
      }
      setLoading(false);
      // Mark als gelesen wenn fokussiert
      window.apiFetch(`/api/dm/${conversation.id}/read`, { method: 'POST', body: {} })
        .then(() => onMessagesRead?.());
      setTimeout(() => bottomRef.current?.scrollIntoView({ behavior: 'auto' }), 50);
    })();
    return () => { cancelled = true; };
  }, [conversation?.id]);

  // WS-Listener: live message/edit/delete-Events für diese Konversation
  useEffect(() => {
    if (!conversation) return;
    const handler = (msg) => {
      if (!msg || msg.conversationId !== conversation.id) return;
      if (msg.type === 'dm-message') {
        setMessages(prev => {
          if (prev.some(m => m.id === msg.message.id)) return prev;
          return [...prev, msg.message];
        });
        setTimeout(() => bottomRef.current?.scrollIntoView({ behavior: 'smooth' }), 30);
        // Auto-Read für eingehende, wenn gerade in dieser Konversation
        if (msg.message.senderId !== currentUserId) {
          window.apiFetch(`/api/dm/${conversation.id}/read`, { method: 'POST', body: { messageId: parseInt(msg.message.id, 10) } })
            .then(() => onMessagesRead?.());
        }
      } else if (msg.type === 'dm-edited') {
        setMessages(prev => prev.map(m => m.id === msg.message.id ? msg.message : m));
      } else if (msg.type === 'dm-deleted') {
        setMessages(prev => prev.map(m => m.id === msg.messageId ? { ...m, deletedAt: new Date().toISOString(), body: '' } : m));
      }
    };
    const cc = window.ChatClient;
    if (!cc) return;
    return cc.on('any', handler);
  }, [conversation?.id, currentUserId]);

  const loadOlder = async () => {
    if (!messages.length || loadingOlder) return;
    setLoadingOlder(true);
    const oldestId = messages[0].id;
    const { ok, data } = await window.apiFetch(`/api/dm/${conversation.id}/messages?before=${oldestId}`);
    setLoadingOlder(false);
    if (ok) {
      setMessages(prev => [...(data.messages || []), ...prev]);
      setHasMore(!!data.hasMore);
    }
  };

  const handleSend = async () => {
    const body = input.trim();
    if (!body || sending) return;
    setSending(true);
    const { ok, data } = await window.apiFetch(`/api/dm/${conversation.id}/messages`, {
      method: 'POST', body: { body },
    });
    setSending(false);
    if (!ok) {
      window.toast?.(data?.error || 'Senden fehlgeschlagen.', 'error');
      return;
    }
    setInput('');
    // Optimistisch — der WS-Push fügt es nochmal hinzu, aber dedupe via id
    setMessages(prev => prev.some(m => m.id === data.message.id) ? prev : [...prev, data.message]);
    setTimeout(() => bottomRef.current?.scrollIntoView({ behavior: 'smooth' }), 30);
    onMessageSent?.();
  };

  const startEdit = (m) => { setEditingId(m.id); setEditingBody(m.body); };
  const cancelEdit = () => { setEditingId(null); setEditingBody(''); };
  const saveEdit = async () => {
    if (!editingBody.trim()) return;
    const { ok, data } = await window.apiFetch(`/api/dm/messages/${editingId}`, {
      method: 'PATCH', body: { body: editingBody.trim() },
    });
    if (!ok) { window.toast?.(data?.error || 'Edit fehlgeschlagen.', 'error'); return; }
    cancelEdit();
  };
  const removeMessage = async (id) => {
    if (!confirm('Diese Nachricht löschen?')) return;
    const { ok, data } = await window.apiFetch(`/api/dm/messages/${id}`, { method: 'DELETE' });
    if (!ok) window.toast?.(data?.error || 'Löschen fehlgeschlagen.', 'error');
  };
  const canEdit = (m) => m.senderId === currentUserId && !m.deletedAt
    && (Date.now() - new Date(m.createdAt).getTime() < 15 * 60 * 1000);
  const canDelete = (m) => m.senderId === currentUserId && !m.deletedAt;

  const handleKeyDown = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      if (editingId) saveEdit(); else handleSend();
    } else if (e.key === 'Escape' && editingId) {
      e.preventDefault();
      cancelEdit();
    }
  };

  if (!conversation) {
    return (
      <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-muted)', fontSize: 14, padding: 30 }}>
        Wähle links eine Konversation oder starte eine neue.
      </div>
    );
  }

  return (
    <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
      {/* Header */}
      <div style={{
        padding: '12px 18px', borderBottom: '1px solid var(--border)',
        display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0,
        background: 'var(--surface)',
      }}>
        <Avatar initials={conversation.other.initials || '??'} avatar={conversation.other.avatar} size={36} />
        <div>
          <div style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>{conversation.other.name}</div>
          <div style={{ fontSize: 11, color: 'var(--text-dim)' }}>1:1 Direktnachricht</div>
        </div>
      </div>

      {/* Messages */}
      <div ref={listRef} style={{
        flex: 1, overflowY: 'auto', padding: '12px 18px',
        display: 'flex', flexDirection: 'column', gap: 8, minHeight: 0,
      }}>
        {loading && <div style={{ textAlign: 'center', color: 'var(--text-muted)', fontSize: 12, padding: 20 }}>Lade Verlauf…</div>}
        {!loading && hasMore && (
          <button onClick={loadOlder} disabled={loadingOlder}
            style={{ background: 'transparent', border: '1px solid var(--border)', borderRadius: 6, padding: '4px 10px', color: 'var(--text-muted)', fontSize: 11, cursor: 'pointer', fontFamily: 'var(--font-mono)' }}>
            {loadingOlder ? '…' : 'Älteres laden'}
          </button>
        )}
        {!loading && messages.length === 0 && (
          <div style={{ textAlign: 'center', color: 'var(--text-dim)', fontSize: 12, padding: 30 }}>
            Noch keine Nachrichten. Sag Hallo.
          </div>
        )}
        {messages.map((m, idx) => {
          const isMine = m.senderId === currentUserId;
          const isDeleted = !!m.deletedAt;
          const prev = messages[idx - 1];
          const sameAuthor = prev && prev.senderId === m.senderId && !isDeleted && !prev.deletedAt
            && (new Date(m.createdAt) - new Date(prev.createdAt) < 5 * 60 * 1000);
          return (
            <div key={m.id} style={{
              display: 'flex', gap: 10, alignItems: 'flex-start',
              marginTop: sameAuthor ? 0 : 6,
            }}>
              <div style={{ width: 32, flexShrink: 0, paddingTop: sameAuthor ? 0 : 2 }}>
                {!sameAuthor && !isDeleted && (
                  <Avatar
                    initials={m.senderInitials || (isMine ? '' : conversation.other.initials) || '??'}
                    avatar={m.senderAvatar || (isMine ? null : conversation.other.avatar)}
                    size={32}
                  />
                )}
              </div>
              <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 2 }}>
                {!sameAuthor && !isDeleted && (
                  <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
                    <span style={{ fontSize: 12, fontWeight: 600, color: isMine ? 'var(--accent)' : 'var(--text)' }}>
                      {isMine ? 'Du' : (m.senderName || conversation.other.name)}
                    </span>
                    <span style={{ fontSize: 10, color: 'var(--text-dim)', fontFamily: 'var(--font-mono)' }}>
                      {formatTime(m.createdAt)}
                      {m.editedAt && <span title={`bearbeitet ${formatTime(m.editedAt)}`}> · bearb.</span>}
                    </span>
                  </div>
                )}
                {editingId === m.id ? (
                  <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                    <textarea
                      value={editingBody}
                      onChange={e => setEditingBody(e.target.value)}
                      onKeyDown={handleKeyDown}
                      autoFocus
                      style={{
                        background: 'var(--surface-2)', border: '1px solid var(--accent)', borderRadius: 6,
                        color: 'var(--text)', fontSize: 13, padding: 8, fontFamily: 'inherit',
                        resize: 'vertical', minHeight: 40, outline: 'none',
                      }}
                    />
                    <div style={{ display: 'flex', gap: 6, fontSize: 11, color: 'var(--text-muted)' }}>
                      <span>Enter speichern · Esc abbrechen</span>
                      <div style={{ flex: 1 }} />
                      <button onClick={cancelEdit} style={{ background: 'none', border: 'none', color: 'var(--text-muted)', cursor: 'pointer', fontSize: 11 }}>Abbrechen</button>
                      <button onClick={saveEdit} style={{ background: 'var(--accent)', border: 'none', color: '#fff', borderRadius: 4, padding: '2px 8px', cursor: 'pointer', fontSize: 11 }}>Speichern</button>
                    </div>
                  </div>
                ) : (
                  <div className="dm-row" style={{
                    fontSize: 13,
                    color: isDeleted ? 'var(--text-dim)' : 'var(--text)',
                    fontStyle: isDeleted ? 'italic' : 'normal',
                    lineHeight: 1.5, wordBreak: 'break-word', whiteSpace: 'pre-wrap',
                    paddingRight: 28, position: 'relative',
                  }}>
                    {isDeleted ? '[Nachricht gelöscht]' : m.body}
                    {!isDeleted && (canEdit(m) || canDelete(m)) && (
                      <span className="dm-row-actions" style={{
                        position: 'absolute', right: 0, top: 0, display: 'flex', gap: 2, opacity: 0, transition: 'opacity 0.15s',
                      }}>
                        {canEdit(m) && (
                          <button onClick={() => startEdit(m)} title="Bearbeiten"
                            style={{ background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 4, padding: 3, color: 'var(--text-muted)', cursor: 'pointer', display: 'flex' }}>
                            <Icon name="edit" size={11} />
                          </button>
                        )}
                        {canDelete(m) && (
                          <button onClick={() => removeMessage(m.id)} title="Löschen"
                            style={{ background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 4, padding: 3, color: '#f87171', cursor: 'pointer', display: 'flex' }}>
                            <Icon name="trash" size={11} />
                          </button>
                        )}
                      </span>
                    )}
                  </div>
                )}
              </div>
            </div>
          );
        })}
        <div ref={bottomRef} />
      </div>

      {/* Input */}
      <div style={{ padding: 12, borderTop: '1px solid var(--border)', flexShrink: 0 }}>
        <div style={{ display: 'flex', gap: 6, alignItems: 'flex-end' }}>
          <textarea
            ref={inputRef}
            value={input}
            onChange={e => setInput(e.target.value)}
            onKeyDown={handleKeyDown}
            placeholder={`Nachricht an ${conversation.other.name}…`}
            rows={2}
            style={{
              flex: 1, background: 'var(--surface-2)', border: '1px solid var(--border)',
              borderRadius: 8, color: 'var(--text)', fontSize: 13, padding: 8,
              fontFamily: 'inherit', resize: 'none', outline: 'none', minHeight: 36, maxHeight: 140,
            }}
            onFocus={e => e.currentTarget.style.borderColor = 'var(--accent)'}
            onBlur={e => e.currentTarget.style.borderColor = 'var(--border)'}
          />
          <button onClick={handleSend} disabled={!input.trim() || sending}
            style={{
              background: 'var(--accent)', border: 'none', borderRadius: 8,
              color: '#fff', cursor: 'pointer', padding: '8px 12px', display: 'flex', alignItems: 'center',
              opacity: !input.trim() || sending ? 0.5 : 1,
            }}>
            <Icon name="check" size={14} color="#fff" />
          </button>
        </div>
        <div style={{ fontSize: 10, color: 'var(--text-dim)', marginTop: 4, fontFamily: 'var(--font-mono)' }}>
          Enter senden · Shift+Enter Zeilenumbruch
        </div>
      </div>
      <style>{`.dm-row:hover .dm-row-actions { opacity: 1 !important; }`}</style>
    </div>
  );
};

// ─── NewDMModal — User-Picker mit Live-Search ───────────────────────────────
const NewDMModal = ({ open, onClose, onPicked }) => {
  const [q, setQ] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!open) { setQ(''); setResults([]); return; }
  }, [open]);

  useEffect(() => {
    if (!open) return;
    if (!q.trim()) { setResults([]); return; }
    const t = setTimeout(async () => {
      setLoading(true);
      const { ok, data } = await window.apiFetch(`/api/users/search?q=${encodeURIComponent(q.trim())}`);
      setLoading(false);
      if (ok) setResults(data.users || []);
    }, 250);
    return () => clearTimeout(t);
  }, [q, open]);

  if (!open) return null;
  return (
    <Modal open={open} onClose={onClose} title="Neue Direktnachricht" width={420}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        <input
          type="text"
          value={q}
          onChange={e => setQ(e.target.value)}
          placeholder="Name oder E-Mail suchen…"
          autoFocus
          style={{
            background: 'var(--surface-2)', border: '1px solid var(--border)',
            borderRadius: 8, color: 'var(--text)', fontSize: 13, padding: '8px 12px',
            fontFamily: 'inherit', outline: 'none',
          }}
          onFocus={e => e.currentTarget.style.borderColor = 'var(--accent)'}
          onBlur={e => e.currentTarget.style.borderColor = 'var(--border)'}
        />
        <div style={{ maxHeight: 320, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 2 }}>
          {loading && <div style={{ textAlign: 'center', color: 'var(--text-muted)', fontSize: 12, padding: 12 }}>Suche…</div>}
          {!loading && q.trim() && results.length === 0 && (
            <div style={{ textAlign: 'center', color: 'var(--text-dim)', fontSize: 12, padding: 16 }}>Keine User gefunden.</div>
          )}
          {results.map(u => (
            <button key={u.id} onClick={() => onPicked(u)}
              style={{
                display: 'flex', alignItems: 'center', gap: 10,
                background: 'transparent', border: 'none', borderRadius: 6,
                padding: '8px 10px', cursor: 'pointer', textAlign: 'left',
                color: 'var(--text)', fontSize: 13,
              }}
              onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-2)'}
              onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
            >
              <Avatar initials={u.initials} avatar={u.avatar} size={32} />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{u.name}</div>
                <div style={{ fontSize: 11, color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>{u.id}</div>
              </div>
            </button>
          ))}
        </div>
      </div>
    </Modal>
  );
};

// ─── DMsView — Top-Level Page ───────────────────────────────────────────────
const DMsView = ({ user, route, setRoute, onUnreadChange }) => {
  const [conversations, setConversations] = useState([]);
  const [selectedId, setSelectedIdState] = useState(route?.id || null);
  const [loading, setLoading] = useState(true);
  const [newOpen, setNewOpen] = useState(false);

  const setSelectedId = (id) => {
    setSelectedIdState(id);
    setRoute?.({ page: 'dms', id: id || undefined });
  };

  const refresh = useCallback(async () => {
    const { ok, data } = await window.apiFetch('/api/dm');
    if (ok) {
      setConversations(data.conversations || []);
      // Falls noch keine Auswahl, erste Konversation auswählen
      if (!selectedId && data.conversations?.[0]) {
        setSelectedIdState(data.conversations[0].id);
      }
    }
    setLoading(false);
  }, [selectedId]);

  useEffect(() => { refresh(); }, [refresh]);

  // WS-Listener: Conversation-Liste live aktualisieren bei DM-Events
  useEffect(() => {
    const cc = window.ChatClient;
    if (!cc) return;
    return cc.on('any', (msg) => {
      if (!msg) return;
      if (msg.type === 'dm-message' || msg.type === 'dm-read' || msg.type === 'dm-deleted' || msg.type === 'dm-edited') {
        refresh();
        onUnreadChange?.();
      }
    });
  }, [refresh, onUnreadChange]);

  // Route-Sync (URL-Hash → State)
  useEffect(() => {
    if (route?.id && route.id !== selectedId) setSelectedIdState(route.id);
  }, [route?.id]);

  const handleNewPicked = async (other) => {
    setNewOpen(false);
    const { ok, data } = await window.apiFetch('/api/dm', {
      method: 'POST', body: { recipientId: other.id },
    });
    if (!ok) {
      window.toast?.(data?.error || 'Konversation konnte nicht gestartet werden.', 'error');
      return;
    }
    await refresh();
    setSelectedId(data.conversation.id);
  };

  const selected = conversations.find(c => c.id === selectedId) || null;

  return (
    <div style={{
      display: 'flex',
      width: '100%',
      height: 'calc(100vh - 60px)', // Topbar/Layout-Adjust
      maxWidth: 1280, margin: '0 auto',
      background: 'var(--bg)',
      border: '1px solid var(--border)',
      borderRadius: 12, overflow: 'hidden',
      minHeight: 0,
    }}>
      <ConversationList
        conversations={conversations}
        selectedId={selectedId}
        onSelect={setSelectedId}
        onNew={() => setNewOpen(true)}
        currentUserId={user.id}
      />
      <ConversationView
        conversation={selected}
        currentUserId={user.id}
        onMessageSent={refresh}
        onMessagesRead={onUnreadChange}
      />
      <NewDMModal
        open={newOpen}
        onClose={() => setNewOpen(false)}
        onPicked={handleNewPicked}
      />
    </div>
  );
};

Object.assign(window, { DMsView, ConversationList, ConversationView, NewDMModal, useDMUnread });
