// settings.jsx — Heronry-Settings als eigenständiges Modul.
//
// Aus views.jsx extrahiert, damit die Settings-React-Tree auch außerhalb von
// Heronry mountbar wird (z. B. im Haikara-Hub via shared/settings-mount.js).
// Verbleibende externe Abhängigkeiten:
//   • React/ReactDOM (UMD)
//   • apiFetch (api.js)
//   • can()  (data.js)
//   • Icon, Btn, Modal, TagBadge, RoleBadge (ui.jsx)
//   • TweakSection, TweakColor, TweakSlider, TweakToggle, TweakSelect (tweaks-panel.jsx)
//   • AdminPanel (admin.jsx)
// Alle werden über window-Globals erreicht — gleiche Konvention wie der Rest
// der Codebase.

const { useState, useEffect, useRef } = React;

// ── SECURITY VIEW (2FA / Account) ─────────────────────────────────────────────
// Mit `embedded` wird der äußere Wrapper (Padding, Breadcrumb, H2) weggelassen —
// dafür das ist die Komponente in der SettingsView eingebettet.
const SecurityView = ({ user, refreshUser, setRoute, embedded = false }) => {
  const [status, setStatus] = useState(null); // { enabled, hasPendingSecret, backupCodesRemaining }
  const [tgStatus, setTgStatus] = useState(null); // { enabled, chatLinked, botConfigured, botUsername }
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  // Setup-Wizard state
  const [setupOpen, setSetupOpen] = useState(false);
  const [setupData, setSetupData] = useState(null); // { secret, otpauthUrl, qrDataUrl }
  const [setupCode, setSetupCode] = useState('');
  const [setupBusy, setSetupBusy] = useState(false);
  const [backupCodes, setBackupCodes] = useState(null); // String[] | null — nach erfolgreichem Enable

  // Disable + Regenerate state (TOTP)
  const [disableOpen, setDisableOpen] = useState(false);
  const [disableCode, setDisableCode] = useState('');
  const [disableBusy, setDisableBusy] = useState(false);
  const [regenOpen, setRegenOpen] = useState(false);
  const [regenCode, setRegenCode] = useState('');
  const [regenBusy, setRegenBusy] = useState(false);

  // Telegram state
  const [tgConnectOpen, setTgConnectOpen] = useState(false);
  const [tgConnectData, setTgConnectData] = useState(null); // { deepLink, token, expiresInSeconds }
  const [tgConnectBusy, setTgConnectBusy] = useState(false);
  const [tgDisableOpen, setTgDisableOpen] = useState(false);
  const [tgDisableCode, setTgDisableCode] = useState('');
  const [tgDisableBusy, setTgDisableBusy] = useState(false);
  const [tgOtpSending, setTgOtpSending] = useState(false);
  const tgPollTimer = useRef(null);

  // Email-2FA state
  const [emailStatus, setEmailStatus] = useState(null); // { enabled, emailConfigured, maskedEmail }
  const [emailSetupOpen, setEmailSetupOpen] = useState(false);
  const [emailSetupCode, setEmailSetupCode] = useState('');
  const [emailSetupSent, setEmailSetupSent] = useState(false);
  const [emailSetupBusy, setEmailSetupBusy] = useState(false);
  const [emailDisableOpen, setEmailDisableOpen] = useState(false);
  const [emailDisableCode, setEmailDisableCode] = useState('');
  const [emailDisableBusy, setEmailDisableBusy] = useState(false);
  const [emailOtpSending, setEmailOtpSending] = useState(false);

  const reload = async () => {
    setLoading(true); setError('');
    const [r1, r2, r3] = await Promise.all([
      apiFetch('/api/auth/2fa/status'),
      apiFetch('/api/auth/2fa/telegram/status'),
      apiFetch('/api/auth/2fa/email/status'),
    ]);
    if (r1.ok) setStatus(r1.data);
    else setError(r1.data?.error || 'Status konnte nicht geladen werden.');
    if (r2.ok) setTgStatus(r2.data);
    if (r3.ok) setEmailStatus(r3.data);
    setLoading(false);
  };
  useEffect(() => { reload(); }, []);
  useEffect(() => () => { if (tgPollTimer.current) clearInterval(tgPollTimer.current); }, []);

  const formatSecret = (s) => (s || '').toUpperCase().match(/.{1,4}/g)?.join(' ') || '';

  const startSetup = async () => {
    setError(''); setBackupCodes(null); setSetupCode('');
    const { ok, data } = await apiFetch('/api/auth/2fa/setup', { method: 'POST' });
    if (!ok) { window.toast?.(data?.error || '2FA-Setup fehlgeschlagen.', 'error'); return; }
    setSetupData(data);
    setSetupOpen(true);
  };
  const submitEnable = async () => {
    if (!setupCode.trim()) return;
    setSetupBusy(true);
    const { ok, data } = await apiFetch('/api/auth/2fa/enable', { method: 'POST', body: { code: setupCode } });
    setSetupBusy(false);
    if (!ok) { window.toast?.(data?.error || 'Code ungültig.', 'error'); return; }
    setBackupCodes(data.backupCodes);
    window.toast?.('2FA aktiviert.', 'success');
    refreshUser?.();
    reload();
  };
  const closeSetup = () => {
    setSetupOpen(false); setSetupData(null); setSetupCode(''); setBackupCodes(null);
  };

  const submitDisable = async () => {
    if (!disableCode.trim()) return;
    setDisableBusy(true);
    const isBackup = disableCode.includes('-');
    const body = isBackup ? { backupCode: disableCode } : { code: disableCode };
    const { ok, data } = await apiFetch('/api/auth/2fa/disable', { method: 'POST', body });
    setDisableBusy(false);
    if (!ok) { window.toast?.(data?.error || 'Code ungültig.', 'error'); return; }
    window.toast?.('2FA deaktiviert.', 'success');
    setDisableOpen(false); setDisableCode('');
    refreshUser?.(); reload();
  };

  const submitRegen = async () => {
    if (!regenCode.trim()) return;
    setRegenBusy(true);
    const { ok, data } = await apiFetch('/api/auth/2fa/regenerate', { method: 'POST', body: { code: regenCode } });
    setRegenBusy(false);
    if (!ok) { window.toast?.(data?.error || 'Code ungültig.', 'error'); return; }
    setBackupCodes(data.backupCodes);
    setRegenOpen(false); setRegenCode('');
    setSetupOpen(true); // Backup-Codes-Anzeige im gleichen Wizard
    window.toast?.('Backup-Codes neu generiert.', 'success');
    reload();
  };

  const downloadCodes = () => {
    if (!backupCodes) return;
    const txt = `Heronry — Backup-Codes für ${user.email}\nGeneriert am ${new Date().toLocaleString('de-DE')}\n\n${backupCodes.join('\n')}\n\nJeder Code ist nur einmal verwendbar.\n`;
    const blob = new Blob([txt], { type: 'text/plain;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = `heronry-2fa-backup-codes.txt`;
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  };

  // ── Telegram-2FA-Helper ────────────────────────────────────────────────────
  const stopTgPolling = () => {
    if (tgPollTimer.current) { clearInterval(tgPollTimer.current); tgPollTimer.current = null; }
  };
  const startTgSetup = async () => {
    setTgConnectBusy(true);
    const { ok, data } = await apiFetch('/api/auth/2fa/telegram/setup', { method: 'POST' });
    setTgConnectBusy(false);
    if (!ok) { window.toast?.(data?.error || 'Setup nicht möglich.', 'error'); return; }
    setTgConnectData(data);
    setTgConnectOpen(true);
    // Polling: alle 2.5s Status checken bis enabled=true (oder Modal manuell geschlossen).
    stopTgPolling();
    tgPollTimer.current = setInterval(async () => {
      const r = await apiFetch('/api/auth/2fa/telegram/status');
      if (r.ok && r.data?.enabled) {
        stopTgPolling();
        setTgConnectOpen(false); setTgConnectData(null);
        window.toast?.('Telegram verknüpft.', 'success');
        refreshUser?.();
        reload();
      }
    }, 2500);
  };
  const closeTgConnect = () => {
    stopTgPolling();
    setTgConnectOpen(false); setTgConnectData(null);
  };
  const sendTgOtpForDisable = async () => {
    setTgOtpSending(true);
    const { ok, data } = await apiFetch('/api/auth/2fa/telegram/send-otp', { method: 'POST' });
    setTgOtpSending(false);
    if (!ok) { window.toast?.(data?.error || 'OTP-Versand fehlgeschlagen.', 'error'); return; }
    window.toast?.('Code wurde an dein Telegram gesendet.', 'success');
  };
  const submitTgDisable = async () => {
    if (!tgDisableCode.trim()) return;
    setTgDisableBusy(true);
    const isBackup = tgDisableCode.includes('-');
    const body = isBackup ? { backupCode: tgDisableCode } : { code: tgDisableCode };
    const { ok, data } = await apiFetch('/api/auth/2fa/telegram/disable', { method: 'POST', body });
    setTgDisableBusy(false);
    if (!ok) { window.toast?.(data?.error || 'Code ungültig.', 'error'); return; }
    setTgDisableOpen(false); setTgDisableCode('');
    window.toast?.('Telegram-Verknüpfung gelöst.', 'success');
    refreshUser?.(); reload();
  };

  // ── Email-2FA-Helper ───────────────────────────────────────────────────────
  const startEmailSetup = async () => {
    setEmailSetupBusy(true);
    const { ok, data } = await apiFetch('/api/auth/2fa/email/setup', { method: 'POST' });
    setEmailSetupBusy(false);
    if (!ok) { window.toast?.(data?.error || 'Setup nicht möglich.', 'error'); return; }
    setEmailSetupOpen(true);
    setEmailSetupCode('');
    setEmailSetupSent(true);
    window.toast?.(`Code gesendet an ${data.maskedEmail || 'deine E-Mail'}.`, 'success');
  };
  const resendEmailSetup = async () => {
    setEmailSetupBusy(true);
    const { ok, data } = await apiFetch('/api/auth/2fa/email/setup', { method: 'POST' });
    setEmailSetupBusy(false);
    if (!ok) { window.toast?.(data?.error || 'Erneuter Versand fehlgeschlagen.', 'error'); return; }
    window.toast?.('Neuer Code unterwegs.', 'success');
  };
  const submitEmailEnable = async () => {
    if (!emailSetupCode.trim()) return;
    setEmailSetupBusy(true);
    const { ok, data } = await apiFetch('/api/auth/2fa/email/enable', { method: 'POST', body: { code: emailSetupCode } });
    setEmailSetupBusy(false);
    if (!ok) { window.toast?.(data?.error || 'Code ungültig.', 'error'); return; }
    setEmailSetupOpen(false); setEmailSetupCode(''); setEmailSetupSent(false);
    window.toast?.('E-Mail-2FA aktiviert.', 'success');
    refreshUser?.(); reload();
  };
  const closeEmailSetup = () => {
    setEmailSetupOpen(false); setEmailSetupCode(''); setEmailSetupSent(false);
  };
  const sendEmailOtpForDisable = async () => {
    setEmailOtpSending(true);
    const { ok, data } = await apiFetch('/api/auth/2fa/email/send-otp', { method: 'POST' });
    setEmailOtpSending(false);
    if (!ok) { window.toast?.(data?.error || 'OTP-Versand fehlgeschlagen.', 'error'); return; }
    window.toast?.('Code wurde an deine E-Mail gesendet.', 'success');
  };
  const submitEmailDisable = async () => {
    if (!emailDisableCode.trim()) return;
    setEmailDisableBusy(true);
    const isBackup = emailDisableCode.includes('-');
    const body = isBackup ? { backupCode: emailDisableCode } : { code: emailDisableCode };
    const { ok, data } = await apiFetch('/api/auth/2fa/email/disable', { method: 'POST', body });
    setEmailDisableBusy(false);
    if (!ok) { window.toast?.(data?.error || 'Code ungültig.', 'error'); return; }
    setEmailDisableOpen(false); setEmailDisableCode('');
    window.toast?.('E-Mail-2FA deaktiviert.', 'success');
    refreshUser?.(); reload();
  };

  const Wrapper = embedded ? React.Fragment : 'div';
  const wrapperProps = embedded ? {} : { style: { padding: 32, maxWidth: 720, width: '100%', margin: '0 auto', boxSizing: 'border-box' } };

  return (
    <Wrapper {...wrapperProps}>
      {!embedded && (
        <>
          <h2 style={{ margin: '16px 0 6px', fontSize: 22, fontWeight: 700, color: 'var(--text)' }}>Konto & Sicherheit</h2>
          <p style={{ margin: '0 0 24px', fontSize: 13, color: 'var(--text-muted)' }}>
            Eingeloggt als <strong style={{ color: 'var(--text)' }}>{user.name}</strong> · {user.email}
          </p>
        </>
      )}
      {embedded && (
        <h2 style={{ margin: '0 0 16px', fontSize: 22, fontWeight: 700, color: 'var(--text)' }}>Konto & Sicherheit</h2>
      )}

      {/* Profil-Card — Avatar + Name */}
      <ProfileCard user={user} refreshUser={refreshUser} />

      {/* 2FA Card */}
      <div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 12, padding: '20px 24px', marginBottom: 16 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
          <Icon name="lock" size={18} color={status?.enabled ? '#34d399' : 'var(--text-muted)'} />
          <h3 style={{ margin: 0, fontSize: 15, fontWeight: 600, color: 'var(--text)' }}>Zwei-Faktor-Authentifizierung</h3>
          <span style={{
            marginLeft: 'auto',
            padding: '3px 10px', borderRadius: 12, fontSize: 11, fontFamily: 'var(--font-mono)',
            background: status?.enabled ? 'rgba(52,211,153,0.15)' : 'var(--surface-2)',
            color: status?.enabled ? '#34d399' : 'var(--text-muted)',
            border: `1px solid ${status?.enabled ? 'rgba(52,211,153,0.3)' : 'var(--border)'}`,
          }}>
            {loading ? '…' : status?.enabled ? 'Aktiv' : 'Inaktiv'}
          </span>
        </div>
        <p style={{ margin: '0 0 14px', fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.6 }}>
          Beim Login wird zusätzlich zum Passwort ein 6-stelliger Code aus deiner Authenticator-App abgefragt.
          Empfohlene Apps: <em>Aegis</em>, <em>1Password</em>, <em>Google Authenticator</em>, <em>Authy</em>.
        </p>
        {error && <div style={{ fontSize: 12, color: '#f87171', marginBottom: 10 }}>{error}</div>}
        {!loading && !status?.enabled && (
          <Btn variant="primary" icon="lock" onClick={startSetup}>2FA einrichten</Btn>
        )}
        {!loading && status?.enabled && (
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
            <Btn variant="danger" icon="x" onClick={() => setDisableOpen(true)}>Deaktivieren</Btn>
            <Btn variant="secondary" icon="zap" onClick={() => setRegenOpen(true)}>Backup-Codes neu generieren</Btn>
            <span style={{ fontSize: 11, color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>
              {status.backupCodesRemaining} Backup-Codes übrig
            </span>
          </div>
        )}
      </div>

      {/* Telegram-2FA Card */}
      <div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 12, padding: '20px 24px', marginBottom: 16 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
          <Icon name="bell" size={18} color={tgStatus?.enabled ? '#34d399' : 'var(--text-muted)'} />
          <h3 style={{ margin: 0, fontSize: 15, fontWeight: 600, color: 'var(--text)' }}>Telegram als 2FA-Methode</h3>
          <span style={{
            marginLeft: 'auto',
            padding: '3px 10px', borderRadius: 12, fontSize: 11, fontFamily: 'var(--font-mono)',
            background: tgStatus?.enabled ? 'rgba(52,211,153,0.15)' : 'var(--surface-2)',
            color: tgStatus?.enabled ? '#34d399' : 'var(--text-muted)',
            border: `1px solid ${tgStatus?.enabled ? 'rgba(52,211,153,0.3)' : 'var(--border)'}`,
          }}>
            {loading ? '…' : tgStatus?.enabled ? 'Verknüpft' : 'Nicht verknüpft'}
          </span>
        </div>
        <p style={{ margin: '0 0 14px', fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.6 }}>
          Login-Codes kommen als Telegram-Nachricht von <em>{tgStatus?.botUsername ? `@${tgStatus.botUsername}` : 'unserem Bot'}</em>.
          Funktioniert parallel zur Authenticator-App — eine der beiden Methoden reicht zum Login.
        </p>
        {!loading && !tgStatus?.botConfigured && (
          <div style={{ fontSize: 12, color: '#fbbf24', background: 'rgba(251,191,36,0.1)', border: '1px solid rgba(251,191,36,0.3)', borderRadius: 6, padding: 10 }}>
            ⚠️ Telegram-Bot ist auf dem Server noch nicht konfiguriert (<code>TELEGRAM_BOT_TOKEN</code> in <code>.env</code>).
          </div>
        )}
        {!loading && tgStatus?.botConfigured && !tgStatus?.enabled && (
          <Btn variant="primary" icon="link" onClick={startTgSetup} disabled={tgConnectBusy}>
            {tgConnectBusy ? 'Lade…' : 'Telegram verknüpfen'}
          </Btn>
        )}
        {!loading && tgStatus?.enabled && (
          <Btn variant="danger" icon="x" onClick={() => setTgDisableOpen(true)}>Verknüpfung lösen</Btn>
        )}
      </div>

      {/* Email-2FA Card */}
      <div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 12, padding: '20px 24px', marginBottom: 16 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
          <Icon name="mail" size={18} color={emailStatus?.enabled ? '#34d399' : 'var(--text-muted)'} />
          <h3 style={{ margin: 0, fontSize: 15, fontWeight: 600, color: 'var(--text)' }}>E-Mail als 2FA-Methode</h3>
          <span style={{
            marginLeft: 'auto',
            padding: '3px 10px', borderRadius: 12, fontSize: 11, fontFamily: 'var(--font-mono)',
            background: emailStatus?.enabled ? 'rgba(52,211,153,0.15)' : 'var(--surface-2)',
            color: emailStatus?.enabled ? '#34d399' : 'var(--text-muted)',
            border: `1px solid ${emailStatus?.enabled ? 'rgba(52,211,153,0.3)' : 'var(--border)'}`,
          }}>
            {loading ? '…' : emailStatus?.enabled ? 'Aktiv' : 'Inaktiv'}
          </span>
        </div>
        <p style={{ margin: '0 0 14px', fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.6 }}>
          Login-Codes werden an deine registrierte E-Mail{emailStatus?.maskedEmail ? <> (<em>{emailStatus.maskedEmail}</em>)</> : null} geschickt.
          Funktioniert parallel zu Authenticator-App und Telegram — eine der Methoden reicht zum Login.
        </p>
        {!loading && !emailStatus?.emailConfigured && (
          <div style={{ fontSize: 12, color: '#fbbf24', background: 'rgba(251,191,36,0.1)', border: '1px solid rgba(251,191,36,0.3)', borderRadius: 6, padding: 10 }}>
            ⚠️ SMTP ist auf dem Server noch nicht konfiguriert. Setup nicht möglich.
          </div>
        )}
        {!loading && emailStatus?.emailConfigured && !emailStatus?.enabled && (
          <Btn variant="primary" icon="mail" onClick={startEmailSetup} disabled={emailSetupBusy}>
            {emailSetupBusy ? 'Sende…' : 'E-Mail-2FA aktivieren'}
          </Btn>
        )}
        {!loading && emailStatus?.enabled && (
          <Btn variant="danger" icon="x" onClick={() => setEmailDisableOpen(true)}>Deaktivieren</Btn>
        )}
      </div>

      {/* Email-Setup-Modal: Code an Mail → Code im UI bestätigen */}
      <Modal open={emailSetupOpen} onClose={closeEmailSetup} title="E-Mail-2FA aktivieren" width={460}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          <div style={{ fontSize: 13, color: 'var(--text-muted)', lineHeight: 1.6 }}>
            Wir haben einen 6-stelligen Code an{' '}
            <strong style={{ color: 'var(--text)' }}>{emailStatus?.maskedEmail || 'deine E-Mail'}</strong>{' '}
            geschickt. Trag ihn unten ein, um die Methode scharfzuschalten.
          </div>
          <div>
            <label style={{ fontSize: 12, color: 'var(--text-muted)', display: 'block', marginBottom: 6, fontFamily: 'var(--font-mono)' }}>Code</label>
            <Input
              value={emailSetupCode}
              onChange={e => setEmailSetupCode(e.target.value.replace(/[^\d]/g, '').slice(0, 6))}
              placeholder="123456"
              icon="lock"
            />
          </div>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
            <Btn variant="secondary" icon="mail" onClick={resendEmailSetup} disabled={emailSetupBusy}>
              {emailSetupBusy ? 'Sende…' : 'Code erneut senden'}
            </Btn>
            <div style={{ flex: 1 }} />
            <Btn variant="ghost" onClick={closeEmailSetup}>Abbrechen</Btn>
            <Btn variant="primary" icon="check" onClick={submitEmailEnable} disabled={!emailSetupCode.trim() || emailSetupBusy}>
              {emailSetupBusy ? 'Prüfe…' : 'Aktivieren'}
            </Btn>
          </div>
        </div>
      </Modal>

      {/* Email-Disable-Modal: TOTP / Backup / Telegram-OTP / Email-OTP */}
      <Modal open={emailDisableOpen} onClose={() => { setEmailDisableOpen(false); setEmailDisableCode(''); }} title="E-Mail-2FA deaktivieren" width={440}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div style={{ fontSize: 13, color: 'var(--text-muted)', lineHeight: 1.6 }}>
            Zur Bestätigung einen aktuellen Code eingeben — TOTP, Backup-Code (mit Bindestrich), Telegram-Code oder einen frischen E-Mail-Code (Button unten).
          </div>
          <Input value={emailDisableCode} onChange={e => setEmailDisableCode(e.target.value)} placeholder="123456 oder xxxx-xxxx" icon="lock" />
          <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
            <Btn variant="secondary" icon="mail" onClick={sendEmailOtpForDisable} disabled={emailOtpSending}>
              {emailOtpSending ? 'Sende…' : 'Code an E-Mail senden'}
            </Btn>
            <div style={{ flex: 1 }} />
            <Btn variant="ghost" onClick={() => { setEmailDisableOpen(false); setEmailDisableCode(''); }}>Abbrechen</Btn>
            <Btn variant="danger" icon="x" onClick={submitEmailDisable} disabled={!emailDisableCode.trim() || emailDisableBusy}>
              {emailDisableBusy ? 'Prüfe…' : 'Deaktivieren'}
            </Btn>
          </div>
        </div>
      </Modal>

      {/* Telegram-Connect-Modal */}
      <Modal open={tgConnectOpen} onClose={closeTgConnect} title="Telegram verknüpfen" width={460}>
        {tgConnectData && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
            <div style={{ fontSize: 13, color: 'var(--text-muted)', lineHeight: 1.6 }}>
              <strong style={{ color: 'var(--text)' }}>1.</strong> Klick den Link, um den Bot zu öffnen.<br />
              <strong style={{ color: 'var(--text)' }}>2.</strong> Tippe in Telegram auf <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4, fontFamily: 'var(--font-mono)' }}>Start</code> (oder schicke <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4, fontFamily: 'var(--font-mono)' }}>/start</code>).<br />
              <strong style={{ color: 'var(--text)' }}>3.</strong> Diese Seite erkennt das automatisch und schließt das Fenster.
            </div>
            <a href={tgConnectData.deepLink} target="_blank" rel="noopener noreferrer"
              style={{
                display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8,
                background: 'var(--accent)', color: '#fff', padding: '12px 16px',
                borderRadius: 'var(--radius)', fontWeight: 600, fontSize: 14,
                textDecoration: 'none',
              }}>
              <Icon name="link" size={16} color="#fff" /> Telegram öffnen
            </a>
            <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--text-muted)', wordBreak: 'break-all' }}>
              {tgConnectData.deepLink}
            </div>
            <div style={{ fontSize: 12, color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: 8 }}>
              <span style={{ width: 10, height: 10, borderRadius: '50%', background: 'var(--accent)', animation: 'pulse 1.5s ease-in-out infinite' }} />
              Warte auf Bestätigung im Telegram-Chat…
            </div>
            <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
              <Btn variant="ghost" onClick={closeTgConnect}>Abbrechen</Btn>
            </div>
          </div>
        )}
      </Modal>

      {/* Telegram-Disable-Modal */}
      <Modal open={tgDisableOpen} onClose={() => { setTgDisableOpen(false); setTgDisableCode(''); }} title="Telegram-Verknüpfung lösen" width={440}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div style={{ fontSize: 13, color: 'var(--text-muted)', lineHeight: 1.6 }}>
            Zur Bestätigung einen aktuellen Code eingeben.
            {status?.enabled
              ? ' Du kannst entweder einen TOTP-Code aus deiner App, einen Backup-Code (mit Bindestrich) oder einen Telegram-Code (Button unten) verwenden.'
              : ' Klick "Code an Telegram senden", öffne Telegram, kopiere den Code hierher.'}
          </div>
          <Input value={tgDisableCode} onChange={e => setTgDisableCode(e.target.value)} placeholder="123456 oder xxxx-xxxx" icon="lock" />
          <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
            <Btn variant="secondary" icon="bell" onClick={sendTgOtpForDisable} disabled={tgOtpSending}>
              {tgOtpSending ? 'Sende…' : 'Code an Telegram senden'}
            </Btn>
            <div style={{ flex: 1 }} />
            <Btn variant="ghost" onClick={() => { setTgDisableOpen(false); setTgDisableCode(''); }}>Abbrechen</Btn>
            <Btn variant="danger" icon="x" onClick={submitTgDisable} disabled={!tgDisableCode.trim() || tgDisableBusy}>
              {tgDisableBusy ? 'Prüfe…' : 'Lösen'}
            </Btn>
          </div>
        </div>
      </Modal>

      {/* Setup-Wizard / Backup-Codes-Anzeige */}
      <Modal open={setupOpen} onClose={closeSetup} title={backupCodes ? '2FA-Backup-Codes' : '2FA einrichten'} width={500}>
        {!backupCodes && setupData && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
            <div style={{ fontSize: 13, color: 'var(--text-muted)' }}>
              <strong style={{ color: 'var(--text)' }}>1.</strong> QR-Code in deiner Authenticator-App scannen oder den Schlüssel manuell eingeben.
            </div>
            <div style={{ display: 'flex', gap: 16, alignItems: 'center', background: 'var(--surface-2)', padding: 14, borderRadius: 8 }}>
              <img src={setupData.qrDataUrl} alt="2FA-QR-Code" style={{ width: 160, height: 160, borderRadius: 4, background: '#fff', padding: 4 }} />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 11, color: 'var(--text-muted)', fontFamily: 'var(--font-mono)', marginBottom: 4 }}>Manueller Schlüssel:</div>
                <div style={{ fontSize: 12, fontFamily: 'var(--font-mono)', color: 'var(--accent)', wordBreak: 'break-all', lineHeight: 1.5 }}>{formatSecret(setupData.secret)}</div>
                <button onClick={() => navigator.clipboard?.writeText(setupData.secret)}
                  style={{ marginTop: 8, padding: '4px 10px', fontSize: 11, fontFamily: 'var(--font-mono)', background: 'transparent', border: '1px solid var(--border)', color: 'var(--text-muted)', borderRadius: 5, cursor: 'pointer' }}>
                  Schlüssel kopieren
                </button>
              </div>
            </div>
            <div style={{ fontSize: 13, color: 'var(--text-muted)' }}>
              <strong style={{ color: 'var(--text)' }}>2.</strong> Den 6-stelligen Code aus der App eingeben:
            </div>
            <Input value={setupCode} onChange={e => setSetupCode(e.target.value.replace(/[^\d]/g, '').slice(0, 6))} placeholder="123456" icon="lock" />
            <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
              <Btn variant="ghost" onClick={closeSetup}>Abbrechen</Btn>
              <Btn variant="primary" icon="check" onClick={submitEnable} disabled={setupCode.length !== 6 || setupBusy}>
                {setupBusy ? 'Prüfe…' : 'Aktivieren'}
              </Btn>
            </div>
          </div>
        )}
        {backupCodes && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
            <div style={{ fontSize: 13, color: '#fbbf24', background: 'rgba(251,191,36,0.1)', border: '1px solid rgba(251,191,36,0.3)', borderRadius: 6, padding: 10 }}>
              ⚠️ Diese Codes werden nur <strong>jetzt</strong> angezeigt. Speichere sie an einem sicheren Ort. Jeder Code ist nur einmal verwendbar — z. B. wenn du dein Handy verlierst.
            </div>
            <div style={{
              display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 6,
              background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 8, padding: 14,
              fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--accent)',
            }}>
              {backupCodes.map((c, i) => <div key={i}>{c}</div>)}
            </div>
            <div style={{ display: 'flex', gap: 8 }}>
              <Btn variant="secondary" icon="upload" onClick={downloadCodes}>Als .txt herunterladen</Btn>
              <Btn variant="ghost" onClick={() => navigator.clipboard?.writeText(backupCodes.join('\n'))}>In Zwischenablage</Btn>
              <div style={{ flex: 1 }} />
              <Btn variant="primary" icon="check" onClick={closeSetup}>Habe ich gespeichert</Btn>
            </div>
          </div>
        )}
      </Modal>

      {/* Disable-Modal */}
      <Modal open={disableOpen} onClose={() => { setDisableOpen(false); setDisableCode(''); }} title="2FA deaktivieren" width={420}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div style={{ fontSize: 13, color: 'var(--text-muted)' }}>
            Aktuellen 6-stelligen Code aus deiner App eingeben — oder einen Backup-Code (mit Bindestrich).
          </div>
          <Input value={disableCode} onChange={e => setDisableCode(e.target.value)} placeholder="123456 oder xxxx-xxxx" icon="lock" />
          <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
            <Btn variant="ghost" onClick={() => { setDisableOpen(false); setDisableCode(''); }}>Abbrechen</Btn>
            <Btn variant="danger" icon="x" onClick={submitDisable} disabled={!disableCode.trim() || disableBusy}>
              {disableBusy ? 'Prüfe…' : 'Endgültig deaktivieren'}
            </Btn>
          </div>
        </div>
      </Modal>

      {/* Regenerate-Modal */}
      <Modal open={regenOpen} onClose={() => { setRegenOpen(false); setRegenCode(''); }} title="Backup-Codes neu generieren" width={420}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div style={{ fontSize: 13, color: 'var(--text-muted)' }}>
            Alle bisherigen Backup-Codes werden ungültig. Aktuellen 6-stelligen App-Code zur Bestätigung eingeben.
          </div>
          <Input value={regenCode} onChange={e => setRegenCode(e.target.value.replace(/[^\d]/g, '').slice(0, 6))} placeholder="123456" icon="lock" />
          <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
            <Btn variant="ghost" onClick={() => { setRegenOpen(false); setRegenCode(''); }}>Abbrechen</Btn>
            <Btn variant="primary" icon="zap" onClick={submitRegen} disabled={regenCode.length !== 6 || regenBusy}>
              {regenBusy ? 'Prüfe…' : 'Neu generieren'}
            </Btn>
          </div>
        </div>
      </Modal>
    </Wrapper>
  );
};

// ── SETTINGS HUB ─────────────────────────────────────────────────────────────
// Einheitliche Einstellungs-Seite mit Side-Tabs. Sammelt: Erscheinungsbild
// (Tweaks), Benachrichtigungen, Konto & Sicherheit (2FA), Verwaltung (nur Admin).
// Sub-Views (SecurityView, AdminPanel) werden mit `embedded` gerendert, damit
// ihre eigenen Breadcrumbs/Headers nicht doppeln.
const SettingsView = (props) => {
  const {
    user, refreshUser, setRoute, route, userCan = () => true,
    tweaks, setTweaks, darkMode, setDarkMode,
    // forwarded an AdminPanel:
    roles, setRoles, permissionCatalog, topicAreas, sections, refreshStructure, onSettingsChange,
    onReopenOnboarding,
    // App-Kontext: 'heronry' (Default — Heronry-eigenes Settings-Menü) /
    // 'hub' (Aggregator über alle Apps) / 'egret' / 'plumage'.
    app = 'heronry',
  } = props;

  const isAdmin = userCan('users:manage') || userCan('roles:manage') || userCan('settings:manage') || userCan('topic_areas:manage') || userCan('sections:manage');

  const tabs = [
    { id: 'appearance',    label: 'Erscheinungsbild',  icon: 'sliders' },
    { id: 'notifications', label: 'Benachrichtigungen',icon: 'bell' },
    { id: 'security',      label: 'Konto & Sicherheit',icon: 'lock' },
    isAdmin ? { id: 'admin', label: 'Verwaltung',     icon: 'user' } : null,
  ].filter(Boolean);

  const initial = tabs.find(t => t.id === route.tab)?.id || 'appearance';
  const [active, setActive] = useState(initial);
  useEffect(() => {
    if (route.tab && tabs.find(t => t.id === route.tab)) setActive(route.tab);
  }, [route.tab]);

  const goTab = (id) => {
    setActive(id);
    setRoute({ page: 'settings', tab: id });
  };

  return (
    <div style={{ display: 'flex', maxWidth: active === 'admin' ? 1280 : 1080, width: '100%', margin: '0 auto', padding: '24px 24px 40px', gap: 24, boxSizing: 'border-box', alignItems: 'flex-start' }}>
      {/* Sidebar / Tab-Nav */}
      <aside style={{
        position: 'sticky', top: 24,
        flexShrink: 0, width: 220,
        background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 12,
        padding: 8,
      }}>
        <div style={{ padding: '8px 12px 10px', borderBottom: '1px solid var(--border)', marginBottom: 6 }}>
          <div style={{ fontSize: 11, color: 'var(--text-muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.08em' }}>Einstellungen</div>
          <div style={{ fontSize: 12, color: 'var(--text-dim)', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{user.email}</div>
        </div>
        {tabs.map(t => {
          const isActive = active === t.id;
          return (
            <button key={t.id} onClick={() => goTab(t.id)}
              style={{
                display: 'flex', alignItems: 'center', gap: 10,
                width: '100%', padding: '8px 12px',
                background: isActive ? 'var(--accent-dim)' : 'transparent',
                border: 'none', borderRadius: 6, cursor: 'pointer',
                color: isActive ? 'var(--accent)' : 'var(--text)',
                fontSize: 13, fontWeight: isActive ? 600 : 500, fontFamily: 'var(--font-sans)',
                textAlign: 'left', transition: 'background 0.1s',
                marginBottom: 2,
              }}
              onMouseEnter={e => { if (!isActive) e.currentTarget.style.background = 'var(--surface-2)'; }}
              onMouseLeave={e => { if (!isActive) e.currentTarget.style.background = 'transparent'; }}
            >
              <Icon name={t.icon} size={14} color={isActive ? 'var(--accent)' : 'var(--text-muted)'} />
              {t.label}
            </button>
          );
        })}
      </aside>

      {/* Content */}
      <section style={{ flex: 1, minWidth: 0 }}>
        {active === 'appearance'    && <AppearanceTab    tweaks={tweaks} setTweaks={setTweaks} darkMode={darkMode} setDarkMode={setDarkMode} app={app} />}
        {active === 'notifications' && <NotificationsTab tweaks={tweaks} setTweaks={setTweaks} onReopenOnboarding={onReopenOnboarding} app={app} />}
        {active === 'security'      && <SecurityView    user={user} refreshUser={refreshUser} setRoute={setRoute} embedded />}
        {active === 'admin' && isAdmin && (
          <AdminPanel
            currentUser={user} setRoute={setRoute}
            roles={roles} setRoles={setRoles}
            permissionCatalog={permissionCatalog}
            topicAreas={topicAreas} sections={sections}
            refreshStructure={refreshStructure}
            refreshUser={refreshUser}
            onSettingsChange={onSettingsChange}
            embedded
          />
        )}
      </section>
    </div>
  );
};

// Card-Wrapper für die einzelnen Settings-Tabs.
const SettingsCard = ({ title, description, children }) => (
  <div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 12, padding: '20px 24px', marginBottom: 16 }}>
    <h3 style={{ margin: 0, fontSize: 15, fontWeight: 600, color: 'var(--text)' }}>{title}</h3>
    {description && <p style={{ margin: '4px 0 16px', fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.6 }}>{description}</p>}
    {!description && <div style={{ height: 12 }} />}
    {children}
  </div>
);

// Helper: pro-App-Theme-Toggle, liest/schreibt tweaks['<app>.theme'] = 'dark'|'light'
const AppThemeToggle = ({ app, tweaks, setTweaks }) => {
  const key = app + '.theme';
  // Default ist dark. Nur expliziter 'light'-Wert dreht's um.
  const isDark = tweaks[key] !== 'light';
  return (
    <TweakToggle label="Dark Mode" value={isDark}
      onChange={v => setTweaks(p => ({ ...p, [key]: v ? 'dark' : 'light' }))} />
  );
};

// Heronry-Tweaks-Block (wiederverwendet sowohl in heronry-Standalone als auch
// in der Hub-Aggregator-View).
const HeronryAppearanceFields = ({ tweaks, setTweaks, darkMode, setDarkMode }) => (
  <>
    {darkMode !== undefined && setDarkMode && (
      <TweakToggle label="Dark Mode" value={darkMode} onChange={setDarkMode} />
    )}
    <TweakColor  label="Akzentfarbe"   value={tweaks.accentColor}  onChange={v => setTweaks(p => ({ ...p, accentColor: v }))} />
    <TweakSlider label="Sidebar-Breite" value={tweaks.sidebarWidth} min={200} max={340} step={10} unit="px" onChange={v => setTweaks(p => ({ ...p, sidebarWidth: v }))} />
    <TweakSlider label="Schriftgröße"   value={tweaks.fontSize}     min={11}  max={16}  step={1}  unit="px" onChange={v => setTweaks(p => ({ ...p, fontSize: v }))} />
    <TweakSlider label="Seiten-Zoom"    value={Number(tweaks.pageZoom) || 100} min={80} max={150} step={5} unit="%" onChange={v => setTweaks(p => ({ ...p, pageZoom: v }))} />
    <TweakSelect label="Mono-Font" value={tweaks.fontMono} options={['JetBrains Mono', 'Fira Code', 'Source Code Pro', 'Cascadia Code', 'Courier New']} onChange={v => setTweaks(p => ({ ...p, fontMono: v }))} />
  </>
);

// Erscheinungsbild — UI-Tweaks. App-aware: Heronry zeigt seine eigenen Tweaks
// (Akzent, Sidebar-Breite, Schriftgröße, …), Hub zeigt alle Apps als
// Sub-Sektionen, Egret/Plumage zeigen nur die eigene App.
const AppearanceTab = ({ tweaks, setTweaks, darkMode, setDarkMode, app = 'heronry' }) => {
  if (app === 'hub') {
    return (
      <>
        <h2 style={{ margin: '0 0 16px', fontSize: 22, fontWeight: 700, color: 'var(--text)' }}>Erscheinungsbild</h2>
        <SettingsCard title="Hub" description="Plattform-Hub auf haikara.it.">
          <AppThemeToggle app="hub" tweaks={tweaks} setTweaks={setTweaks} />
        </SettingsCard>
        <SettingsCard title="Heronry" description="Wissens-App. Akzentfarbe, Sidebar-Breite und Typografie. Änderungen wirken sofort beim nächsten Heronry-Besuch.">
          <HeronryAppearanceFields tweaks={tweaks} setTweaks={setTweaks} />
        </SettingsCard>
        <SettingsCard title="Egret" description="Subnet-Calculator.">
          <AppThemeToggle app="egret" tweaks={tweaks} setTweaks={setTweaks} />
        </SettingsCard>
        <SettingsCard title="Plumage" description="Cheatsheet-Sammlung.">
          <AppThemeToggle app="plumage" tweaks={tweaks} setTweaks={setTweaks} />
        </SettingsCard>
      </>
    );
  }
  if (app === 'egret' || app === 'plumage') {
    const niceName = app === 'egret' ? 'Egret' : 'Plumage';
    return (
      <>
        <h2 style={{ margin: '0 0 16px', fontSize: 22, fontWeight: 700, color: 'var(--text)' }}>Erscheinungsbild</h2>
        <SettingsCard title={niceName + '-Theme'} description="Hell/Dunkel-Umschaltung für diese App. Andere Apps haben eigene Themes — global in den Hub-Einstellungen.">
          <AppThemeToggle app={app} tweaks={tweaks} setTweaks={setTweaks} />
        </SettingsCard>
      </>
    );
  }
  // Default: 'heronry' — eigenständige Heronry-View.
  return (
    <>
      <h2 style={{ margin: '0 0 16px', fontSize: 22, fontWeight: 700, color: 'var(--text)' }}>Erscheinungsbild</h2>
      <SettingsCard title="Theme">
        <TweakToggle label="Dark Mode" value={darkMode} onChange={setDarkMode} />
      </SettingsCard>
      <SettingsCard title="Design" description="Akzentfarbe und Layout. Änderungen werden sofort live übernommen.">
        <TweakColor  label="Akzentfarbe"   value={tweaks.accentColor}  onChange={v => setTweaks(p => ({ ...p, accentColor: v }))} />
        <TweakSlider label="Sidebar-Breite" value={tweaks.sidebarWidth} min={200} max={340} step={10} unit="px" onChange={v => setTweaks(p => ({ ...p, sidebarWidth: v }))} />
        <TweakSlider label="Schriftgröße"   value={tweaks.fontSize}     min={11}  max={16}  step={1}  unit="px" onChange={v => setTweaks(p => ({ ...p, fontSize: v }))} />
        <TweakSlider label="Seiten-Zoom"    value={Number(tweaks.pageZoom) || 100} min={80} max={150} step={5} unit="%" onChange={v => setTweaks(p => ({ ...p, pageZoom: v }))} />
      </SettingsCard>
      <SettingsCard title="Typografie">
        <TweakSelect label="Mono-Font" value={tweaks.fontMono} options={['JetBrains Mono', 'Fira Code', 'Source Code Pro', 'Cascadia Code', 'Courier New']} onChange={v => setTweaks(p => ({ ...p, fontMono: v }))} />
      </SettingsCard>
    </>
  );
};

// Benachrichtigungen — Mails, Reminder-Defaults, What's-New-Popup. App-aware:
// account-level Notifications (Patch-Notes-Mails) sind global; Heronry-spezifische
// Items (Termin-Reminder, Heronry-Welcome-Tour) zeigen wir nur im Heronry-Kontext.
// Tour-Reset für Hub/Egret/Plumage über shared/tour.js — weckt die Spotlight-Tour.
const NotificationsTab = ({ tweaks, setTweaks, onReopenOnboarding, app = 'heronry' }) => {
  const isHeronry = app === 'heronry';
  const resetAppTour = () => {
    if (window.hkTour && typeof window.hkTour.reset === 'function') {
      window.hkTour.reset(app);
    } else {
      try { localStorage.removeItem('hk_tour_done_' + app); } catch (_) { /* ignore */ }
    }
    window.toast?.('Tour zurückgesetzt — beim nächsten Besuch zeigt sie sich wieder.', 'info');
  };
  const niceAppName = { hub: 'Hub', egret: 'Egret', plumage: 'Plumage' }[app];

  return (
    <>
      <h2 style={{ margin: '0 0 16px', fontSize: 22, fontWeight: 700, color: 'var(--text)' }}>Benachrichtigungen</h2>
      <SettingsCard title="E-Mail" description="Wenn aktiviert, bekommst du eine Mail bei neuen Patch-Notes der Plattform.">
        <TweakToggle label="Patch-Notes per Mail" value={!!tweaks.releaseNotifications} onChange={v => setTweaks(p => ({ ...p, releaseNotifications: v }))} />
      </SettingsCard>
      {isHeronry && (
        <>
          <SettingsCard title="Was-ist-neu-Popup" description="Beim Login wird automatisch das Patch-Note-Popup gezeigt, wenn ein neues Release erschienen ist.">
            <TweakToggle label="Popup beim Login zeigen" value={tweaks.releasePopupsDisabled !== true} onChange={v => setTweaks(p => ({ ...p, releasePopupsDisabled: !v }))} />
          </SettingsCard>
          <SettingsCard title="Termin-Reminder" description="Standard-Vorlaufzeit für neue Reminder. Pro Termin individuell überschreibbar.">
            <TweakSlider label="Vorlaufzeit (Default)" value={Number(tweaks.reminderLeadDaysDefault) || 1} min={1} max={14} step={1} unit=" Tag(e)" onChange={v => setTweaks(p => ({ ...p, reminderLeadDaysDefault: v }))} />
          </SettingsCard>
          {onReopenOnboarding && (
            <SettingsCard title="Erste-Schritte-Tour" description="Die kurze Willkommens-Tour wird normalerweise nur einmal nach der Registrierung gezeigt. Hier kannst du sie erneut aufrufen.">
              <Btn variant="secondary" icon="info" onClick={onReopenOnboarding}>Tour erneut anzeigen</Btn>
            </SettingsCard>
          )}
        </>
      )}
      {!isHeronry && niceAppName && (
        <SettingsCard title={niceAppName + '-Tour'} description="Beim ersten Besuch zeigen wir eine kurze Spotlight-Tour. Hier kannst du sie zurücksetzen.">
          <Btn variant="secondary" icon="info" onClick={resetAppTour}>Tour zurücksetzen</Btn>
        </SettingsCard>
      )}
    </>
  );
};

// ─── Profil-Card mit Avatar-Upload ──────────────────────────────────────────
// Pipeline: User picked Datei → Cropper-Modal mit Pan + Zoom + runder Maske
// (Discord-Style) → User klickt Anwenden → Canvas rendert das Crop-Window auf
// 256×256 → WebP-Blob → POST. Der Crop ist genau die quadratische Bounding-Box
// um den Kreis (das gerenderte <img> ist sowieso runde Maskierung über
// border-radius:50%; wir speichern das Quadrat, der Kreis ist nur visuelles
// Feedback). Damit bleibt Server frei von libvips/sharp und der Wire klein.

// Lädt eine File in ein HTMLImageElement (für Canvas-Operations).
function fileToImage(file) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    const url = URL.createObjectURL(file);
    img.onload = () => resolve({ img, url });
    img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('Bild ungültig oder beschädigt.')); };
    img.src = url;
  });
}

// Rendert den aktuellen Crop-State (Bild + Pan + Zoom) auf ein 256×256-Canvas
// und gibt einen WebP-Blob (mit PNG-Fallback) zurück.
async function renderCropToBlob(img, viewport, zoom, ox, oy, target = 256) {
  // Cover-fitScale: kleinster Multiplier, sodass das Bild den Viewport komplett füllt.
  const fitScale = Math.max(viewport / img.width, viewport / img.height);
  const s = fitScale * zoom;
  // Quell-Rect im Image-Koordinatensystem: das, was im Viewport sichtbar ist.
  const srcW = viewport / s;
  const srcH = viewport / s;
  // Bild-Mittelpunkt ist im Viewport bei (vp/2 + ox, vp/2 + oy);
  // links davon liegen (vp/2 + ox)/s Bild-Pixel.
  const srcX = (img.width  / 2) - (viewport / 2 + ox) / s;
  const srcY = (img.height / 2) - (viewport / 2 + oy) / s;

  const canvas = document.createElement('canvas');
  canvas.width = target;
  canvas.height = target;
  const ctx = canvas.getContext('2d');
  ctx.imageSmoothingQuality = 'high';
  ctx.drawImage(img, srcX, srcY, srcW, srcH, 0, 0, target, target);

  const tryBlob = (mime, quality) => new Promise(r => canvas.toBlob(r, mime, quality));
  let blob = await tryBlob('image/webp', 0.88);
  if (blob) return { blob, mime: 'image/webp' };
  blob = await tryBlob('image/png');
  if (blob) return { blob, mime: 'image/png' };
  throw new Error('Bild konnte nicht kodiert werden.');
}

// Cropper-Modal: zeigt das Bild mit runder Maske, Drag-to-Pan, Zoom-Slider.
// Beim Klick auf Anwenden wird der aktuelle Crop als WebP-Blob via
// onApply(blob, mime) zurückgereicht. Abbrechen → onClose().
const VP = 320; // Viewport-Größe in Pixel (CSS-Quadrat)
const AvatarCropperModal = ({ open, file, onApply, onClose }) => {
  const [img, setImg] = useState(null);
  const [imgUrl, setImgUrl] = useState(null);
  const [zoom, setZoom] = useState(1);
  const [offset, setOffset] = useState({ x: 0, y: 0 });
  const [busy, setBusy] = useState(false);
  const dragRef = useRef(null); // { startX, startY, startOX, startOY }

  // Bild laden / wechseln
  useEffect(() => {
    if (!open || !file) return;
    let cancelled = false;
    (async () => {
      try {
        const { img: loaded, url } = await fileToImage(file);
        if (cancelled) { URL.revokeObjectURL(url); return; }
        setImg(loaded);
        setImgUrl(url);
        setZoom(1);
        setOffset({ x: 0, y: 0 });
      } catch (err) {
        window.toast?.(err.message || 'Bild konnte nicht geladen werden.', 'error');
        onClose?.();
      }
    })();
    return () => { cancelled = true; };
  }, [open, file]);

  // Cleanup Object-URL beim Schließen
  useEffect(() => {
    if (!open && imgUrl) {
      URL.revokeObjectURL(imgUrl);
      setImgUrl(null);
      setImg(null);
    }
  }, [open]);

  // Aktuelle "displayed dimensions" und Pan-Limits ableiten
  const fitScale = img ? Math.max(VP / img.width, VP / img.height) : 1;
  const s = fitScale * zoom;
  const dw = img ? img.width * s : VP;
  const dh = img ? img.height * s : VP;
  const maxOX = Math.max(0, (dw - VP) / 2);
  const maxOY = Math.max(0, (dh - VP) / 2);
  const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
  const clampOffset = (o) => ({ x: clamp(o.x, -maxOX, maxOX), y: clamp(o.y, -maxOY, maxOY) });

  // Wenn Zoom geändert wird, Offset re-clampen (evtl. wandert Bild aus Viewport raus)
  useEffect(() => { setOffset(o => clampOffset(o)); }, [zoom, img]);

  const onPointerDown = (e) => {
    if (!img) return;
    e.currentTarget.setPointerCapture(e.pointerId);
    dragRef.current = { startX: e.clientX, startY: e.clientY, startOX: offset.x, startOY: offset.y };
  };
  const onPointerMove = (e) => {
    if (!dragRef.current) return;
    const dx = e.clientX - dragRef.current.startX;
    const dy = e.clientY - dragRef.current.startY;
    setOffset(clampOffset({ x: dragRef.current.startOX + dx, y: dragRef.current.startOY + dy }));
  };
  const onPointerUp = (e) => {
    dragRef.current = null;
    try { e.currentTarget.releasePointerCapture(e.pointerId); } catch {}
  };
  const onWheel = (e) => {
    if (!img) return;
    e.preventDefault();
    const delta = -e.deltaY * 0.0015;
    setZoom(z => clamp(z + delta * z, 1, 4));
  };

  const reset = () => { setZoom(1); setOffset({ x: 0, y: 0 }); };

  const apply = async () => {
    if (!img || busy) return;
    setBusy(true);
    try {
      const { blob, mime } = await renderCropToBlob(img, VP, zoom, offset.x, offset.y, 256);
      await onApply(blob, mime);
    } catch (err) {
      window.toast?.(err.message || 'Crop fehlgeschlagen.', 'error');
    } finally {
      setBusy(false);
    }
  };

  if (!open) return null;
  return (
    <div
      onClick={(e) => { if (e.target === e.currentTarget && !busy) onClose?.(); }}
      style={{
        position: 'fixed', inset: 0, zIndex: 1100,
        background: 'rgba(0,0,0,0.72)',
        display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24,
      }}
    >
      <div style={{
        background: 'var(--surface)', border: '1px solid var(--border)',
        borderRadius: 14, width: '100%', maxWidth: 460, padding: '20px 24px 18px',
        boxShadow: '0 20px 60px rgba(0,0,0,0.45)',
      }}>
        <div style={{ display: 'flex', alignItems: 'center', marginBottom: 14 }}>
          <h3 style={{ margin: 0, fontSize: 16, fontWeight: 600, color: 'var(--text)' }}>Bild bearbeiten</h3>
          <div style={{ flex: 1 }} />
          <button
            onClick={() => !busy && onClose?.()}
            aria-label="Schließen"
            style={{ background: 'transparent', border: 'none', color: 'var(--text-muted)', cursor: 'pointer', padding: 4, display: 'flex' }}
          >
            <Icon name="x" size={16} />
          </button>
        </div>

        {/* Crop-Viewport */}
        <div
          onPointerDown={onPointerDown}
          onPointerMove={onPointerMove}
          onPointerUp={onPointerUp}
          onPointerCancel={onPointerUp}
          onWheel={onWheel}
          style={{
            position: 'relative',
            width: VP, height: VP, margin: '0 auto',
            overflow: 'hidden',
            background: '#000',
            borderRadius: 8,
            cursor: dragRef.current ? 'grabbing' : 'grab',
            touchAction: 'none', userSelect: 'none',
          }}
        >
          {imgUrl && (
            <img
              src={imgUrl}
              alt=""
              draggable={false}
              style={{
                position: 'absolute', left: '50%', top: '50%',
                width: dw, height: dh,
                transform: `translate(calc(-50% + ${offset.x}px), calc(-50% + ${offset.y}px))`,
                pointerEvents: 'none',
                maxWidth: 'none', maxHeight: 'none',
              }}
            />
          )}
          {/* Runde Maske: dunkler box-shadow rundherum, Kreis selbst transparent.
              pointer-events:none damit Drag durch die Maske durchgeht. */}
          <div style={{
            position: 'absolute', inset: 0,
            borderRadius: '50%',
            boxShadow: '0 0 0 9999px rgba(0,0,0,0.62)',
            border: '2px solid rgba(255,255,255,0.9)',
            pointerEvents: 'none',
          }} />
        </div>

        {/* Zoom-Slider */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '16px 4px 4px' }}>
          <Icon name="image" size={14} color="var(--text-muted)" />
          <input
            type="range"
            min="1" max="4" step="0.01"
            value={zoom}
            onChange={(e) => setZoom(parseFloat(e.target.value))}
            style={{ flex: 1, accentColor: 'var(--accent)', cursor: 'pointer' }}
            aria-label="Zoom"
          />
          <Icon name="image" size={20} color="var(--text-muted)" />
        </div>

        {/* Footer: Reset links, Cancel/Apply rechts */}
        <div style={{ display: 'flex', alignItems: 'center', marginTop: 10 }}>
          <button
            onClick={reset}
            disabled={busy}
            style={{
              background: 'transparent', border: 'none',
              color: 'var(--accent)', cursor: 'pointer',
              fontSize: 13, padding: '6px 4px', fontWeight: 500,
            }}
          >
            Zurücksetzen
          </button>
          <div style={{ flex: 1 }} />
          <Btn variant="secondary" onClick={() => !busy && onClose?.()}>Abbrechen</Btn>
          <div style={{ width: 8 }} />
          <Btn variant="primary" onClick={apply} disabled={busy || !img}>
            {busy ? 'Lade…' : 'Anwenden'}
          </Btn>
        </div>
      </div>
    </div>
  );
};

const ProfileCard = ({ user, refreshUser }) => {
  const fileInputRef = useRef(null);
  const [busy, setBusy] = useState(false);
  const [removing, setRemoving] = useState(false);
  const [cropFile, setCropFile] = useState(null); // wenn gesetzt → Cropper-Modal offen

  const onPick = () => fileInputRef.current?.click();

  // Datei aus File-Picker → Cropper öffnen statt direkt hochladen
  const onChange = (e) => {
    const file = e.target.files?.[0];
    e.target.value = ''; // gleiche Datei kann nochmal getriggert werden
    if (!file) return;
    if (!file.type.startsWith('image/')) {
      window.toast?.('Nur Bilddateien (PNG, JPG, WebP, GIF).', 'error');
      return;
    }
    if (file.size > 10 * 1024 * 1024) {
      window.toast?.('Bild zu groß (max 10 MB Original).', 'error');
      return;
    }
    setCropFile(file);
  };

  // Cropper hat den finalen Crop gerendert → hochladen
  const onCropApply = async (blob, mime) => {
    setBusy(true);
    try {
      const ext = mime === 'image/webp' ? 'webp' : 'png';
      const form = new FormData();
      form.append('avatar', blob, `avatar.${ext}`);

      const token = localStorage.getItem('tb_token');
      // API_BASE nur, wenn cross-origin (Hub/Plumage/Egret-Mount) — sonst
      // bleibt's relativ same-origin auf Heronry.
      const apiBase = window.API_BASE || window.HAIKARA_APPS?.api || '';
      const res = await fetch(apiBase + '/api/users/me/avatar', {
        method: 'POST',
        headers: token ? { Authorization: `Bearer ${token}` } : {},
        body: form,
        credentials: 'include',
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(data?.error || 'Upload fehlgeschlagen.');
      window.toast?.('Profilbild aktualisiert.', 'success');
      setCropFile(null);
      await refreshUser?.();
    } catch (err) {
      console.error('[avatar.upload]', err);
      window.toast?.(err.message || 'Upload fehlgeschlagen.', 'error');
    } finally {
      setBusy(false);
    }
  };

  const onRemove = async () => {
    if (!user.avatar) return;
    if (!confirm('Profilbild entfernen?')) return;
    setRemoving(true);
    const { ok, data } = await apiFetch('/api/users/me/avatar', { method: 'DELETE' });
    setRemoving(false);
    if (!ok) {
      window.toast?.(data?.error || 'Löschen fehlgeschlagen.', 'error');
      return;
    }
    window.toast?.('Profilbild entfernt.', 'success');
    refreshUser?.();
  };

  return (
    <div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 12, padding: '20px 24px', marginBottom: 16 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 14 }}>
        <Icon name="user" size={18} color="var(--text-muted)" />
        <h3 style={{ margin: 0, fontSize: 15, fontWeight: 600, color: 'var(--text)' }}>Profil</h3>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 18, flexWrap: 'wrap' }}>
        <Avatar initials={user.initials} avatar={user.avatar} size={72} />
        <div style={{ flex: 1, minWidth: 220 }}>
          <div style={{ fontSize: 16, fontWeight: 600, color: 'var(--text)' }}>{user.name}</div>
          <div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 2 }}>{user.email}</div>
          <div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 8, lineHeight: 1.5 }}>
            Nach dem Auswählen kannst du das Bild verschieben und zoomen.
            Output: 256×256, sichtbar überall im Hub und in den Apps.
          </div>
        </div>
        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          <input
            ref={fileInputRef}
            type="file"
            accept="image/png,image/jpeg,image/webp,image/gif"
            onChange={onChange}
            style={{ display: 'none' }}
          />
          <Btn variant="primary" icon="upload" onClick={onPick} disabled={busy}>
            {busy ? 'Lade…' : (user.avatar ? 'Bild ändern' : 'Bild hochladen')}
          </Btn>
          {user.avatar && (
            <Btn variant="secondary" icon="x" onClick={onRemove} disabled={removing}>
              {removing ? 'Entferne…' : 'Entfernen'}
            </Btn>
          )}
        </div>
      </div>

      <AvatarCropperModal
        open={!!cropFile}
        file={cropFile}
        onApply={onCropApply}
        onClose={() => setCropFile(null)}
      />
    </div>
  );
};

Object.assign(window, { SecurityView, SettingsView, SettingsCard, AppearanceTab, NotificationsTab, ProfileCard, AvatarCropperModal });
