import { updateOwnProfile } from '@chat-app/shared/auth';
import {
  changeLocale as changeLocaleI18n,
  SUPPORTED_LOCALES,
  type SupportedLocale,
} from '@chat-app/shared/i18n';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';

import { Avatar } from '../components/Avatar';
import { BackupExportDialog } from '../components/BackupExportDialog';
import { BackupRestoreDialog } from '../components/BackupRestoreDialog';
import { MicTestSection } from '../components/MicTestSection';
import { NotificationSoundSettings } from '../components/NotificationSoundSettings';
import { RingtoneSettings } from '../components/RingtoneSettings';
import { SoundboardSettings } from '../components/SoundboardSettings';
import { LockIcon } from '../components/icons';
import { useAuth } from '../context/AuthContext';
import { useCall } from '../context/CallContext';
import { useTheme } from '../context/ThemeContext';
import { loadDevicePrivateKey } from '@chat-app/shared/auth';
import { isAutoStartEnabled, setAutoStart } from '../lib/autoStart';
import {
  AVATAR_TARGET_DIM,
  deleteAvatarObject,
  uploadAvatarBlob,
} from '../lib/avatarUpload';
import {
  BANNER_MAX_INPUT_BYTES,
  BANNER_TARGET_HEIGHT,
  BANNER_TARGET_WIDTH,
  deleteBannerObject,
  uploadBannerBlob,
} from '../lib/bannerUpload';
import { ImageCropDialog } from '../components/ImageCropDialog';
import { Lightbox } from '../components/Lightbox';
import { devLocalSecretStore } from '../lib/secretStore';
import {
  getPttSettings,
  keyCodeToLabel,
  type PttSettings,
  subscribePttSettings,
  updatePttSettings,
} from '../lib/pttSettings';
import {
  getVoiceHotkeys,
  subscribeVoiceHotkeys,
  updateVoiceHotkey,
  type VoiceHotkeyKind,
  type VoiceHotkeys,
} from '../lib/voiceHotkeys';
import {
  AUDIO_QUALITY_ORDER,
  type AudioQuality,
  type AudioSettings,
  getAudioQualityParams,
  getAudioSettings,
  subscribeAudioSettings,
  updateAudioSettings,
} from '../lib/audioSettings';
import {
  type CallE2EESettings,
  getCallE2EESettings,
  isE2EESupported,
  subscribeCallE2EESettings,
  updateCallE2EESettings,
} from '../lib/callE2EE';
import {
  getPresetParams,
  getScreenShareSettings,
  PRESET_ORDER,
  type ScreenSharePreset,
  type ScreenShareSettings,
  subscribeScreenShareSettings,
  updateScreenShareSettings,
} from '../lib/screenShareSettings';
import { supabase } from '../lib/supabase';

const LOCALE_LABELS: Record<SupportedLocale, string> = {
  en: 'English',
  de: 'Deutsch',
};

export function SettingsPage() {
  const { t, i18n } = useTranslation(['app', 'common', 'auth']);
  const { profile, device, refreshProfile, signOut } = useAuth();
  const [busy, setBusy] = useState(false);

  async function patchProfile(patch: Parameters<typeof updateOwnProfile>[1]) {
    setBusy(true);
    try {
      await updateOwnProfile(supabase, patch);
      await refreshProfile();
    } catch (err: unknown) {
      console.error('updateProfile failed', err);
    } finally {
      setBusy(false);
    }
  }

  async function handleLocaleChange(locale: SupportedLocale) {
    await changeLocaleI18n(locale);
    void patchProfile({ locale });
  }

  return (
    <div className="min-h-full bg-surface-3 text-fg">
      <div className="mx-auto flex max-w-3xl flex-col gap-6 px-6 py-8">
      <header className="mb-2">
        <h1 className="font-display text-2xl font-semibold tracking-tight text-fg">
          {t('app:settings.title')}
        </h1>
      </header>

      {/* Account */}
      <Section title={t('app:settings.section_account')}>
        <ProfileVisualsControls patchProfile={patchProfile} busy={busy} />
        <Row label={t('auth:signed_in.username')} value={profile?.username ?? '—'} />
        <DisplayNameControls patchProfile={patchProfile} busy={busy} />
        <Row label={t('auth:signed_in.email')} value={profile?.userId ?? '—'} mono />
      </Section>

      {/* Startup */}
      <Section title={t('app:settings.section_startup', { defaultValue: 'Start' })}>
        <AutoStartControls />
      </Section>

      {/* Appearance */}
      <Section title={t('app:settings.section_appearance')}>
        <ThemeRow />
        <SettingRow label={t('app:settings.language')}>
          <div className="inline-flex rounded-lg border border-line bg-surface-3 p-1">
            {SUPPORTED_LOCALES.map((locale) => {
              const active = (i18n.resolvedLanguage ?? i18n.language) === locale;
              return (
                <button
                  key={locale}
                  type="button"
                  disabled={busy}
                  onClick={() => void handleLocaleChange(locale)}
                  className={
                    'cursor-pointer rounded-md px-3 py-1.5 text-xs font-medium transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
                    (active
                      ? 'bg-accent text-accent-fg'
                      : 'text-fg-muted hover:text-fg')
                  }
                >
                  {LOCALE_LABELS[locale]}
                </button>
              );
            })}
          </div>
        </SettingRow>
      </Section>

      {/* Privacy */}
      <Section title={t('app:settings.section_privacy')}>
        <Toggle
          label={t('app:settings.show_read_receipts')}
          hint={t('app:settings.show_read_receipts_hint')}
          checked={profile?.showReadReceipts ?? true}
          disabled={busy || !profile}
          onChange={(v) => void patchProfile({ showReadReceipts: v })}
        />
        <Toggle
          label={t('app:settings.allow_dms_strangers')}
          hint={t('app:settings.allow_dms_strangers_hint')}
          checked={profile?.allowDmsFromStrangers ?? true}
          disabled={busy || !profile}
          onChange={(v) => void patchProfile({ allowDmsFromStrangers: v })}
        />
      </Section>

      {/* Notification sound (new messages) */}
      <Section
        title={t('app:settings.section_notifications', { defaultValue: 'Benachrichtigungen' })}
      >
        <NotificationSoundSettings disabled={busy} />
      </Section>

      {/* Ringtone (incoming custom) */}
      <Section title={t('app:settings.section_ringtone', { defaultValue: 'Klingelton' })}>
        <RingtoneSettings disabled={busy} />
      </Section>

      {/* Soundboard */}
      <Section title={t('app:settings.section_soundboard', { defaultValue: 'Soundboard' })}>
        <SoundboardSettings />
      </Section>

      {/* Voice / Push-to-Talk + Audio Quality + E2EE */}
      <Section title={t('app:settings.section_voice', { defaultValue: 'Sprache' })}>
        <AudioDeviceControls />
        <div className="mt-3 border-t border-line pt-3">
          <AudioQualityControls />
        </div>
        <div className="mt-3 border-t border-line pt-3">
          <PttControls />
        </div>
        <div className="mt-3 border-t border-line pt-3">
          <VoiceHotkeyControls kind="mute" />
        </div>
        <div className="mt-3 border-t border-line pt-3">
          <VoiceHotkeyControls kind="deafen" />
        </div>
        <div className="mt-3 border-t border-line pt-3">
          <VoiceHotkeyControls kind="hangup" />
        </div>
        <div className="mt-3 border-t border-line pt-3">
          <VoiceHotkeyControls kind="screenShare" />
        </div>
        <div className="mt-3 border-t border-line pt-3">
          <VoiceHotkeyControls kind="video" />
        </div>
        <div className="mt-3 border-t border-line pt-3">
          <CallE2EEControls />
        </div>
      </Section>

      {/* Screen-share quality */}
      <Section title={t('app:settings.section_screen_share', { defaultValue: 'Bildschirmfreigabe' })}>
        <ScreenShareControls />
      </Section>

      {/* Devices */}
      <Section title={t('app:settings.section_devices')}>
        {device && (
          <div className="rounded-xl border border-emerald-500/30 bg-emerald-500/10 p-4">
            <div className="flex items-center gap-2 text-sm font-semibold text-emerald-700 dark:text-emerald-200">
              <LockIcon className="h-4 w-4" />
              {t('app:settings.this_device')}
            </div>
            <dl className="mt-3 space-y-1.5 text-xs">
              <Row label={t('auth:signed_in.display_name')} value={device.name} />
              <Row label={t('auth:signed_in.device_platform')} value={device.platform} />
              <Row label={t('auth:signed_in.user_id')} value={device.id} mono />
            </dl>
          </div>
        )}
        <DeviceKeyBackupControls />
      </Section>

      {/* Danger zone */}
      <Section title={t('app:settings.danger_zone')}>
        <button
          type="button"
          onClick={() => void signOut()}
          className="cursor-pointer rounded-lg border border-rose-500/40 bg-rose-500/10 px-4 py-2.5 text-sm font-semibold text-rose-600 transition hover:bg-rose-500/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-400/50 dark:text-rose-300"
        >
          {t('app:settings.sign_out')}
        </button>
      </Section>
      </div>
    </div>
  );
}

function AutoStartControls() {
  const { t } = useTranslation(['app']);
  const [enabled, setEnabled] = useState<boolean | null>(null);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;
    void (async () => {
      const on = await isAutoStartEnabled();
      if (!cancelled) setEnabled(on);
    })();
    return () => {
      cancelled = true;
    };
  }, []);

  async function handleToggle(next: boolean) {
    setBusy(true);
    setError(null);
    try {
      await setAutoStart(next);
      setEnabled(next);
    } catch (err: unknown) {
      setError(err instanceof Error ? err.message : 'autostart failed');
    } finally {
      setBusy(false);
    }
  }

  return (
    <>
      <Toggle
        label={t('app:settings.autostart', {
          defaultValue: 'Mit Windows starten',
        })}
        hint={t('app:settings.autostart_hint', {
          defaultValue:
            'ChatApp automatisch mitstarten wenn du dich am System anmeldest.',
        })}
        checked={enabled ?? false}
        disabled={busy || enabled === null}
        onChange={(v) => void handleToggle(v)}
      />
      {error && <p className="text-xs text-rose-600 dark:text-rose-300">{error}</p>}
    </>
  );
}

function PttControls() {
  const { t } = useTranslation(['app']);
  const [ptt, setPtt] = useState<PttSettings>(() => getPttSettings());
  const [capturing, setCapturing] = useState(false);

  useEffect(() => {
    return subscribePttSettings(setPtt);
  }, []);

  useEffect(() => {
    if (!capturing) return;
    const onKey = (e: KeyboardEvent) => {
      e.preventDefault();
      if (e.code === 'Escape') {
        setCapturing(false);
        return;
      }
      updatePttSettings({ key: e.code, keyLabel: keyCodeToLabel(e.code) });
      setCapturing(false);
    };
    window.addEventListener('keydown', onKey, { capture: true });
    return () => window.removeEventListener('keydown', onKey, { capture: true });
  }, [capturing]);

  return (
    <>
      <Toggle
        label={t('app:settings.ptt_enabled', { defaultValue: 'Push-to-Talk' })}
        hint={t('app:settings.ptt_enabled_hint', {
          defaultValue:
            'Mic bleibt stumm bis die Taste gedrückt wird. Sonst overrides der normale Mute-Button.',
        })}
        checked={ptt.enabled}
        onChange={(v) => updatePttSettings({ enabled: v })}
      />
      <SettingRow
        label={t('app:settings.ptt_key', { defaultValue: 'Hotkey' })}
      >
        <button
          type="button"
          onClick={() => setCapturing((v) => !v)}
          className={
            'inline-flex min-w-[7rem] cursor-pointer items-center justify-center rounded-lg border px-3 py-1.5 text-xs font-mono font-semibold transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
            (capturing
              ? 'border-accent bg-accent/20 text-fg animate-pulse'
              : 'border-line bg-surface-3 text-fg hover:brightness-95')
          }
        >
          {capturing
            ? t('app:settings.ptt_press_key', { defaultValue: 'Taste drücken…' })
            : ptt.keyLabel}
        </button>
      </SettingRow>
    </>
  );
}

function VoiceHotkeyControls({ kind }: { kind: VoiceHotkeyKind }) {
  const { t } = useTranslation(['app']);
  const [hotkeys, setHotkeys] = useState<VoiceHotkeys>(() => getVoiceHotkeys());
  const [capturing, setCapturing] = useState(false);

  useEffect(() => subscribeVoiceHotkeys(setHotkeys), []);

  useEffect(() => {
    if (!capturing) return;
    const onKey = (e: KeyboardEvent) => {
      // Modifier-only presses shouldn't bind — wait for a real key to
      // arrive. Escape aborts the capture.
      if (e.code === 'Escape') {
        e.preventDefault();
        setCapturing(false);
        return;
      }
      if (
        e.code === 'ControlLeft' ||
        e.code === 'ControlRight' ||
        e.code === 'ShiftLeft' ||
        e.code === 'ShiftRight' ||
        e.code === 'AltLeft' ||
        e.code === 'AltRight' ||
        e.code === 'MetaLeft' ||
        e.code === 'MetaRight'
      ) {
        return;
      }
      e.preventDefault();
      updateVoiceHotkey(kind, {
        key: e.code,
        ctrl: e.ctrlKey || e.metaKey,
        shift: e.shiftKey,
        alt: e.altKey,
      });
      setCapturing(false);
    };
    window.addEventListener('keydown', onKey, { capture: true });
    return () => window.removeEventListener('keydown', onKey, { capture: true });
  }, [capturing, kind]);

  const binding = hotkeys[kind];
  const labels: Record<VoiceHotkeyKind, { label: string; hint: string }> = {
    mute: {
      label: t('app:settings.hotkey_mute_enabled', { defaultValue: 'Mute-Hotkey' }),
      hint: t('app:settings.hotkey_mute_hint', {
        defaultValue:
          'Schaltet dein Mikro an/aus — funktioniert auch wenn ChatApp im Hintergrund ist.',
      }),
    },
    deafen: {
      label: t('app:settings.hotkey_deafen_enabled', { defaultValue: 'Deafen-Hotkey' }),
      hint: t('app:settings.hotkey_deafen_hint', {
        defaultValue: 'Schaltet eingehendes Audio + dein Mikro stumm (wie in Discord).',
      }),
    },
    hangup: {
      label: t('app:settings.hotkey_hangup_enabled', { defaultValue: 'Auflegen-Hotkey' }),
      hint: t('app:settings.hotkey_hangup_hint', {
        defaultValue: 'Beendet den aktiven Anruf sofort.',
      }),
    },
    screenShare: {
      label: t('app:settings.hotkey_screenshare_enabled', {
        defaultValue: 'Bildschirmfreigabe-Hotkey',
      }),
      hint: t('app:settings.hotkey_screenshare_hint', {
        defaultValue: 'Startet oder stoppt die Bildschirmfreigabe.',
      }),
    },
    video: {
      label: t('app:settings.hotkey_video_enabled', { defaultValue: 'Kamera-Hotkey' }),
      hint: t('app:settings.hotkey_video_hint', {
        defaultValue: 'Schaltet die Kamera während eines Anrufs an oder aus.',
      }),
    },
  };
  const toggleLabel = labels[kind].label;
  const toggleHint = labels[kind].hint;

  return (
    <>
      <Toggle
        label={toggleLabel}
        hint={toggleHint}
        checked={binding.enabled}
        onChange={(v) => updateVoiceHotkey(kind, { enabled: v })}
      />
      <SettingRow
        label={t('app:settings.hotkey_binding', { defaultValue: 'Hotkey' })}
      >
        <button
          type="button"
          onClick={() => setCapturing((v) => !v)}
          className={
            'inline-flex min-w-[10rem] cursor-pointer items-center justify-center rounded-lg border px-3 py-1.5 text-xs font-mono font-semibold transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
            (capturing
              ? 'border-accent bg-accent/20 text-fg animate-pulse'
              : 'border-line bg-surface-3 text-fg hover:brightness-95')
          }
        >
          {capturing
            ? t('app:settings.hotkey_press_combo', {
                defaultValue: 'Kombination drücken…',
              })
            : binding.keyLabel}
        </button>
      </SettingRow>
    </>
  );
}

function CallE2EEControls() {
  const { t } = useTranslation(['app']);
  const [cfg, setCfg] = useState<CallE2EESettings>(() => getCallE2EESettings());
  const [supported] = useState<boolean>(() => isE2EESupported());

  useEffect(() => subscribeCallE2EESettings(setCfg), []);

  return (
    <>
      <Toggle
        label={t('app:settings.e2ee_calls', { defaultValue: 'Ende-zu-Ende-Verschlüsselung (Calls)' })}
        hint={
          supported
            ? t('app:settings.e2ee_calls_hint', {
                defaultValue:
                  'Audio + Video werden vor dem Upload verschlüsselt. Der Server sieht nur Ciphertext. Alle Teilnehmer müssen die Option aktiv haben.',
              })
            : t('app:settings.e2ee_calls_unsupported', {
                defaultValue:
                  'Dein Browser unterstützt keine Insertable Streams. E2EE-Calls nicht verfügbar.',
              })
        }
        checked={cfg.enabled && supported}
        disabled={!supported}
        onChange={(v) => updateCallE2EESettings({ enabled: v })}
      />
    </>
  );
}

function AudioQualityControls() {
  const { t } = useTranslation(['app']);
  const [cfg, setCfg] = useState<AudioSettings>(() => getAudioSettings());

  useEffect(() => subscribeAudioSettings(setCfg), []);

  const params = getAudioQualityParams(cfg.quality);
  const labels: Record<AudioQuality, string> = {
    voice: t('app:settings.audio_voice', { defaultValue: 'Sprache (Empfohlen)' }),
    hifi: t('app:settings.audio_hifi', { defaultValue: 'HiFi / Musik' }),
  };

  return (
    <>
      <SettingRow label={t('app:settings.audio_quality', { defaultValue: 'Audio-Qualität' })}>
        <div className="inline-flex rounded-lg border border-line bg-surface-2 p-1">
          {AUDIO_QUALITY_ORDER.map((q) => {
            const active = cfg.quality === q;
            return (
              <button
                key={q}
                type="button"
                onClick={() => updateAudioSettings({ quality: q })}
                className={
                  'cursor-pointer rounded-md px-3 py-1 text-xs font-medium transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
                  (active
                    ? 'bg-accent text-accent-fg'
                    : 'text-fg-muted hover:text-fg')
                }
              >
                {labels[q]}
              </button>
            );
          })}
        </div>
      </SettingRow>
      <div className="rounded-lg border border-line bg-surface-3 p-3 text-[11px] text-fg-muted">
        <div className="grid grid-cols-4 gap-3">
          <Stat label="Bitrate" value={params.bitrateKbps + ' kbps'} />
          <Stat label="Channels" value={params.stereo ? 'Stereo' : 'Mono'} />
          <Stat label="Sample" value={params.sampleRateHz / 1000 + ' kHz'} />
          <Stat label="DSP" value={params.echoCancellation ? 'On' : 'Off'} />
        </div>
      </div>
      <p className="text-xs text-fg-muted">
        {cfg.quality === 'hifi'
          ? t('app:settings.audio_hifi_hint', {
              defaultValue:
                'Stereo 510 kbps Opus ohne Noise-Suppression/Echo-Cancellation — bester Musik/Broadcast-Sound. Erfordert ruhige Umgebung.',
            })
          : t('app:settings.audio_voice_hint', {
              defaultValue:
                'Mono 48 kbps mit Noise-Suppression, Echo-Cancellation und Auto-Gain. Optimiert für Sprache im Raum.',
            })}
      </p>

      <SettingRow
        label={t('app:settings.noise_suppression', { defaultValue: 'Noise Suppression' })}
      >
        <InlineToggle
          checked={cfg.noiseSuppression}
          onChange={(v) => updateAudioSettings({ noiseSuppression: v })}
        />
      </SettingRow>
      <p className="text-xs text-fg-muted">
        {t('app:settings.noise_suppression_hint', {
          defaultValue:
            'Unterdrückt Hintergrundgeräusche (Tastatur, Lüfter, Café-Lärm). Ausschalten nur bei Musik/Instrumenten.',
        })}
      </p>

      <SettingRow
        label={t('app:settings.video_blur', {
          defaultValue: 'Video-Hintergrund unscharf',
        })}
      >
        <InlineToggle
          checked={cfg.videoBackgroundBlur}
          onChange={(v) => updateAudioSettings({ videoBackgroundBlur: v })}
        />
      </SettingRow>
      <p className="text-xs text-fg-muted">
        {t('app:settings.video_blur_hint', {
          defaultValue:
            'Blendet den Hintergrund hinter dir aus. Braucht etwas GPU-Leistung und lädt beim ersten Aktivieren ~1,5 MB Modell nach.',
        })}
      </p>

      <SettingRow
        label={t('app:settings.voice_threshold', {
          defaultValue: 'Sprach-Erkennungs-Schwelle',
        })}
      >
        <div className="flex w-48 items-center gap-2">
          <input
            type="range"
            min={0.005}
            max={0.1}
            step={0.005}
            value={cfg.voiceThreshold}
            onChange={(e) =>
              updateAudioSettings({ voiceThreshold: Number(e.target.value) })
            }
            className="flex-1 accent-accent"
          />
          <span className="w-10 tabular-nums text-right text-[11px] text-fg-muted">
            {(cfg.voiceThreshold * 100).toFixed(1)}
          </span>
        </div>
      </SettingRow>
      <p className="text-xs text-fg-muted">
        {t('app:settings.voice_threshold_hint', {
          defaultValue:
            'Wann der grüne Sprech-Ring aufleuchtet. Niedriger = empfindlicher (leise Stimme erfassen), höher = tolerant gegen Raumlärm.',
        })}
      </p>
    </>
  );
}

function InlineToggle({
  checked,
  onChange,
}: {
  checked: boolean;
  onChange: (next: boolean) => void;
}) {
  return (
    <button
      type="button"
      role="switch"
      aria-checked={checked}
      onClick={() => onChange(!checked)}
      className={
        'relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
        (checked ? 'bg-accent' : 'bg-surface')
      }
    >
      <span
        className={
          'absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white shadow transition ' +
          (checked ? 'translate-x-5' : '')
        }
      />
    </button>
  );
}

interface AvatarControlsProps {
  patchProfile: (patch: Parameters<typeof updateOwnProfile>[1]) => Promise<void>;
  busy: boolean;
}

function DisplayNameControls({ patchProfile, busy }: AvatarControlsProps) {
  const { t } = useTranslation(['app', 'auth']);
  const { profile } = useAuth();
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState('');
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const inputRef = useRef<HTMLInputElement | null>(null);

  function startEdit() {
    setDraft(profile?.displayName ?? '');
    setError(null);
    setEditing(true);
    // Focus on next tick so the input has mounted.
    window.setTimeout(() => inputRef.current?.focus(), 0);
  }

  function cancel() {
    setEditing(false);
    setDraft('');
    setError(null);
  }

  async function save() {
    const trimmed = draft.trim();
    if (trimmed.length === 0) {
      setError(
        t('app:settings.display_name_required', {
          defaultValue: 'Anzeigename darf nicht leer sein.',
        }),
      );
      return;
    }
    if (trimmed === profile?.displayName) {
      cancel();
      return;
    }
    setSaving(true);
    setError(null);
    try {
      await patchProfile({ displayName: trimmed });
      setEditing(false);
    } catch (err: unknown) {
      setError(err instanceof Error ? err.message : 'save failed');
    } finally {
      setSaving(false);
    }
  }

  if (!editing) {
    return (
      <div className="flex items-center justify-between gap-4">
        <dt className="text-sm text-fg-muted">{t('auth:signed_in.display_name')}</dt>
        <div className="flex min-w-0 items-center gap-2">
          <dd className="max-w-[40ch] truncate text-right text-sm text-fg" title={profile?.displayName ?? ''}>
            {profile?.displayName ?? '—'}
          </dd>
          <button
            type="button"
            onClick={startEdit}
            disabled={busy || !profile}
            className="cursor-pointer rounded-md border border-line bg-surface-3 px-2.5 py-1 text-xs font-semibold text-fg transition hover:bg-surface-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-[#313338] dark:hover:bg-[#2b2d31]"
          >
            {t('app:settings.edit', { defaultValue: 'Bearbeiten' })}
          </button>
        </div>
      </div>
    );
  }

  return (
    <div className="flex flex-col gap-2">
      <div className="flex items-center justify-between gap-4">
        <span className="text-sm text-fg-muted">{t('auth:signed_in.display_name')}</span>
      </div>
      <div className="flex flex-wrap items-center gap-2">
        <input
          ref={inputRef}
          type="text"
          value={draft}
          onChange={(e) => setDraft(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === 'Enter') {
              e.preventDefault();
              void save();
            } else if (e.key === 'Escape') {
              e.preventDefault();
              cancel();
            }
          }}
          maxLength={64}
          disabled={saving}
          className="flex-1 min-w-[12rem] rounded-lg border border-line bg-surface-3 px-3 py-1.5 text-sm text-fg outline-none focus:border-accent focus:ring-2 focus:ring-accent/30 disabled:opacity-60 dark:bg-[#313338]"
        />
        <button
          type="button"
          onClick={() => void save()}
          disabled={saving || busy}
          className="cursor-pointer rounded-lg bg-accent px-3 py-1.5 text-xs font-semibold text-accent-fg transition hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-50"
        >
          {saving
            ? t('app:settings.display_name_saving', { defaultValue: 'Speichere…' })
            : t('app:settings.save', { defaultValue: 'Speichern' })}
        </button>
        <button
          type="button"
          onClick={cancel}
          disabled={saving}
          className="cursor-pointer rounded-lg border border-line bg-surface-3 px-3 py-1.5 text-xs font-semibold text-fg transition hover:bg-surface-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-[#313338] dark:hover:bg-[#2b2d31]"
        >
          {t('app:settings.cancel', { defaultValue: 'Abbrechen' })}
        </button>
      </div>
      {error && (
        <p className="text-xs text-rose-500 dark:text-rose-300">{error}</p>
      )}
    </div>
  );
}

// Default banner gradient when the user hasn't uploaded their own. Sits on
// the same accent + surface tokens as the rest of the app so it never clashes
// with theme changes. Used both here in settings and in UserProfilePopover.
export const DEFAULT_BANNER_CLASS =
  'bg-gradient-to-br from-accent/40 via-accent/15 to-surface-3';

function ProfileVisualsControls({ patchProfile, busy }: AvatarControlsProps) {
  const { t } = useTranslation(['app']);
  const { profile } = useAuth();

  const avatarInputRef = useRef<HTMLInputElement | null>(null);
  const bannerInputRef = useRef<HTMLInputElement | null>(null);
  const [avatarBusy, setAvatarBusy] = useState(false);
  const [bannerBusy, setBannerBusy] = useState(false);
  const [avatarError, setAvatarError] = useState<string | null>(null);
  const [bannerError, setBannerError] = useState<string | null>(null);
  // Crop-dialog plumbing. The picked File lives here until the user
  // confirms a crop or cancels; on confirm we hand the resulting Blob to
  // the matching upload helper. Keeping `kind` separate from `file` lets
  // the same dialog component drive both flows with different aspect
  // ratios.
  const [cropFile, setCropFile] = useState<File | null>(null);
  const [cropKind, setCropKind] = useState<'avatar' | 'banner' | null>(null);
  // Lightbox toggle for the avatar live-preview. Clicking the in-page
  // avatar opens a fullscreen view; clicking outside / Esc dismisses.
  const [avatarPreviewOpen, setAvatarPreviewOpen] = useState(false);

  const userId = profile?.userId;
  const avatarUrl = profile?.avatarUrl ?? null;
  const bannerUrl = profile?.bannerUrl ?? null;

  // Avatar pick → open crop dialog. Legacy `uploadAvatar` (center-crop) is
  // kept around for callers that bypass the picker, but the SettingsPage
  // path always goes through the crop flow now so the user controls the
  // framing.
  function openAvatarCrop(file: File) {
    if (!userId) return;
    if (!file.type.startsWith('image/')) {
      setAvatarError('only image files are accepted');
      return;
    }
    setAvatarError(null);
    setCropFile(file);
    setCropKind('avatar');
  }

  async function handleAvatarCropConfirm(blob: Blob) {
    if (!userId) return;
    setAvatarBusy(true);
    setAvatarError(null);
    try {
      const newUrl = await uploadAvatarBlob(userId, blob);
      const oldUrl = avatarUrl;
      await patchProfile({ avatarUrl: newUrl });
      if (oldUrl) {
        void deleteAvatarObject(oldUrl).catch(() => undefined);
      }
      closeCropDialog();
    } catch (err: unknown) {
      setAvatarError(err instanceof Error ? err.message : 'upload failed');
    } finally {
      setAvatarBusy(false);
    }
  }

  function closeCropDialog() {
    setCropFile(null);
    setCropKind(null);
    if (avatarInputRef.current) avatarInputRef.current.value = '';
    if (bannerInputRef.current) bannerInputRef.current.value = '';
  }

  async function handleAvatarRemove() {
    if (!userId || !avatarUrl) return;
    setAvatarError(null);
    setAvatarBusy(true);
    try {
      await patchProfile({ avatarUrl: null });
      void deleteAvatarObject(avatarUrl).catch(() => undefined);
    } catch (err: unknown) {
      setAvatarError(err instanceof Error ? err.message : 'remove failed');
    } finally {
      setAvatarBusy(false);
    }
  }

  function openBannerCrop(file: File) {
    if (!userId) return;
    if (!file.type.startsWith('image/')) {
      setBannerError('only image files are accepted');
      return;
    }
    if (file.size > BANNER_MAX_INPUT_BYTES) {
      setBannerError('image must be 8 MB or smaller');
      return;
    }
    setBannerError(null);
    setCropFile(file);
    setCropKind('banner');
  }

  async function handleBannerCropConfirm(blob: Blob) {
    if (!userId) return;
    setBannerBusy(true);
    setBannerError(null);
    try {
      const newUrl = await uploadBannerBlob(userId, blob);
      const oldUrl = bannerUrl;
      await patchProfile({ bannerUrl: newUrl });
      if (oldUrl) {
        void deleteBannerObject(oldUrl).catch(() => undefined);
      }
      closeCropDialog();
    } catch (err: unknown) {
      setBannerError(err instanceof Error ? err.message : 'upload failed');
    } finally {
      setBannerBusy(false);
    }
  }

  async function handleBannerRemove() {
    if (!userId || !bannerUrl) return;
    setBannerError(null);
    setBannerBusy(true);
    try {
      await patchProfile({ bannerUrl: null });
      void deleteBannerObject(bannerUrl).catch(() => undefined);
    } catch (err: unknown) {
      setBannerError(err instanceof Error ? err.message : 'remove failed');
    } finally {
      setBannerBusy(false);
    }
  }

  const displayName = profile?.displayName ?? profile?.username;
  const lockedAll = busy || avatarBusy || bannerBusy;

  return (
    <div className="space-y-4">
      {/* Live preview — banner with avatar overlapping bottom-left, mirrors
          how the profile shows up in UserProfilePopover. The avatar row is
          explicitly stacked above the banner via `relative z-10`; without
          it, browsers can paint the negatively-margin'd avatar behind the
          banner's background image when the parent doesn't establish a
          stacking context. */}
      <div className="relative overflow-hidden rounded-xl border border-line bg-surface-3">
        <div
          className={
            'relative z-0 aspect-[3/1] w-full bg-cover bg-center ' +
            (bannerUrl ? '' : DEFAULT_BANNER_CLASS)
          }
          style={bannerUrl ? { backgroundImage: 'url("' + bannerUrl + '")' } : undefined}
        />
        <div
          className="relative z-10 flex items-end gap-3 px-4 pb-3"
          style={{ marginTop: '-2rem' }}
        >
          <button
            type="button"
            onClick={() => {
              if (avatarUrl) setAvatarPreviewOpen(true);
            }}
            // Disabled when there's no uploaded avatar — clicking the
            // generated-initial placeholder would open an empty lightbox.
            disabled={!avatarUrl}
            aria-label={
              avatarUrl
                ? t('app:settings.avatar_preview', { defaultValue: 'Profilbild vergrößern' })
                : undefined
            }
            // appearance-none + reset border/bg/padding so the native
            // button chrome (outset border, button-face background, 1px
            // padding) doesn't draw a box around the avatar circle.
            className={
              'appearance-none border-0 bg-transparent p-0 rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
              (avatarUrl ? 'cursor-zoom-in' : 'cursor-default')
            }
          >
            <Avatar
              url={avatarUrl}
              displayName={displayName}
              className="h-16 w-16 rounded-full text-2xl ring-4 ring-surface-3"
            />
          </button>
          <div className="min-w-0 flex-1 pb-1">
            <div className="truncate text-sm font-semibold text-fg">
              {displayName ?? '—'}
            </div>
            {profile?.username && (
              <div className="truncate text-xs text-fg-muted">@{profile.username}</div>
            )}
          </div>
        </div>
      </div>

      {/* Banner controls */}
      <div className="flex flex-wrap items-center gap-2">
        <div className="min-w-0 flex-1">
          <div className="text-sm font-medium text-fg">
            {t('app:settings.banner', { defaultValue: 'Banner' })}
          </div>
          <div className="text-xs text-fg-muted">
            {t('app:settings.banner_hint', {
              defaultValue: '3:1 Format, max 8 MB. Standard ist ein Farbverlauf.',
            })}
          </div>
          {bannerError && (
            <div className="mt-1 text-xs text-rose-500 dark:text-rose-300">{bannerError}</div>
          )}
        </div>
        <input
          ref={bannerInputRef}
          type="file"
          accept="image/*"
          className="hidden"
          onChange={(e) => {
            const f = e.target.files?.[0];
            if (f) openBannerCrop(f);
          }}
        />
        <button
          type="button"
          onClick={() => bannerInputRef.current?.click()}
          disabled={lockedAll}
          className="cursor-pointer rounded-lg bg-accent px-3 py-1.5 text-xs font-semibold text-accent-fg transition hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-50"
        >
          {bannerBusy
            ? t('app:settings.avatar_uploading', { defaultValue: 'Lade…' })
            : bannerUrl
              ? t('app:settings.avatar_change', { defaultValue: 'Ändern' })
              : t('app:settings.avatar_upload', { defaultValue: 'Hochladen' })}
        </button>
        {bannerUrl && (
          <button
            type="button"
            onClick={() => void handleBannerRemove()}
            disabled={lockedAll}
            className="cursor-pointer rounded-lg border border-rose-500/40 bg-rose-500/10 px-3 py-1.5 text-xs font-semibold text-rose-600 transition hover:bg-rose-500/20 disabled:cursor-not-allowed disabled:opacity-50 dark:text-rose-300"
          >
            {t('app:settings.avatar_remove', { defaultValue: 'Entfernen' })}
          </button>
        )}
      </div>

      {/* Avatar controls */}
      <div className="flex flex-wrap items-center gap-2 border-t border-line pt-4">
        <div className="min-w-0 flex-1">
          <div className="text-sm font-medium text-fg">
            {t('app:settings.avatar', { defaultValue: 'Avatar' })}
          </div>
          <div className="text-xs text-fg-muted">
            {t('app:settings.avatar_hint', {
              defaultValue: 'Quadratisch, max 512×512, WebP. Sichtbar für deine Freunde.',
            })}
          </div>
          {avatarError && (
            <div className="mt-1 text-xs text-rose-500 dark:text-rose-300">{avatarError}</div>
          )}
        </div>
        <input
          ref={avatarInputRef}
          type="file"
          accept="image/*"
          className="hidden"
          onChange={(e) => {
            const f = e.target.files?.[0];
            if (f) openAvatarCrop(f);
          }}
        />
        <button
          type="button"
          onClick={() => avatarInputRef.current?.click()}
          disabled={lockedAll}
          className="cursor-pointer rounded-lg bg-accent px-3 py-1.5 text-xs font-semibold text-accent-fg transition hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-50"
        >
          {avatarBusy
            ? t('app:settings.avatar_uploading', { defaultValue: 'Lade…' })
            : avatarUrl
              ? t('app:settings.avatar_change', { defaultValue: 'Ändern' })
              : t('app:settings.avatar_upload', { defaultValue: 'Hochladen' })}
        </button>
        {avatarUrl && (
          <button
            type="button"
            onClick={() => void handleAvatarRemove()}
            disabled={lockedAll}
            className="cursor-pointer rounded-lg border border-rose-500/40 bg-rose-500/10 px-3 py-1.5 text-xs font-semibold text-rose-600 transition hover:bg-rose-500/20 disabled:cursor-not-allowed disabled:opacity-50 dark:text-rose-300"
          >
            {t('app:settings.avatar_remove', { defaultValue: 'Entfernen' })}
          </button>
        )}
      </div>

      <ImageCropDialog
        open={cropFile !== null && cropKind !== null}
        file={cropFile}
        aspect={cropKind === 'banner' ? 3 : 1}
        outputWidth={cropKind === 'banner' ? BANNER_TARGET_WIDTH : AVATAR_TARGET_DIM}
        outputHeight={cropKind === 'banner' ? BANNER_TARGET_HEIGHT : AVATAR_TARGET_DIM}
        title={
          cropKind === 'banner'
            ? t('app:settings.crop_banner_title', { defaultValue: 'Banner zuschneiden' })
            : t('app:settings.crop_avatar_title', { defaultValue: 'Profilbild zuschneiden' })
        }
        onConfirm={(blob) => {
          if (cropKind === 'banner') void handleBannerCropConfirm(blob);
          else if (cropKind === 'avatar') void handleAvatarCropConfirm(blob);
        }}
        onClose={closeCropDialog}
      />
      {avatarPreviewOpen && avatarUrl && (
        <Lightbox url={avatarUrl} onClose={() => setAvatarPreviewOpen(false)} />
      )}
    </div>
  );
}

function DeviceKeyBackupControls() {
  const { t } = useTranslation(['app']);
  const { profile, device } = useAuth();
  const [open, setOpen] = useState(false);
  const [restoreOpen, setRestoreOpen] = useState(false);
  const [privateKey, setPrivateKey] = useState<Uint8Array | null>(null);
  const [err, setErr] = useState<string | null>(null);
  const canRun = !!profile?.userId && !!device?.id;

  async function handleOpen() {
    if (!canRun) return;
    setErr(null);
    try {
      const priv = await loadDevicePrivateKey(devLocalSecretStore, profile.userId, device.id);
      if (!priv) throw new Error(t('app:backup.no_key_here', {
        defaultValue: 'Kein Geräteschlüssel auf dieser Installation.',
      }));
      setPrivateKey(priv);
      setOpen(true);
    } catch (e: unknown) {
      setErr(e instanceof Error ? e.message : 'failed to load key');
    }
  }

  function handleClose() {
    setOpen(false);
    if (privateKey) {
      for (let i = 0; i < privateKey.length; i++) privateKey[i] = 0;
    }
    setPrivateKey(null);
  }

  return (
    <div className="mt-4 rounded-xl border border-line bg-surface-3 p-4">
      <div className="text-sm font-semibold text-fg">
        {t('app:settings.device_key_backup', { defaultValue: 'Geräteschlüssel-Backup' })}
      </div>
      <p className="mt-1 text-xs text-fg-muted">
        {t('app:settings.device_key_backup_hint_v2', {
          defaultValue:
            'Backup deines Geräteschlüssels. Ohne Backup kein Zugriff auf alte Nachrichten wenn Gerät oder Browser-Storage verloren geht. Wiederherstellung passiert im Gerät-Einrichtungsbildschirm.',
        })}
      </p>
      <div className="mt-3 flex flex-wrap gap-2">
        <button
          type="button"
          disabled={!canRun}
          onClick={() => void handleOpen()}
          className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg bg-accent px-3 py-2 text-sm font-semibold text-accent-fg transition hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-50"
        >
          {t('app:backup.export_open', { defaultValue: 'Backup erstellen' })}
        </button>
        <button
          type="button"
          disabled={!profile?.userId}
          onClick={() => setRestoreOpen(true)}
          className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-line bg-surface-2 px-3 py-2 text-sm font-semibold text-fg transition hover:bg-surface-3 disabled:cursor-not-allowed disabled:opacity-50"
        >
          {t('app:backup.restore_open', { defaultValue: 'Backup wiederherstellen' })}
        </button>
      </div>
      {err && (
        <p className="mt-2 text-xs text-rose-600 dark:text-rose-300">{err}</p>
      )}

      {open && profile && device && privateKey && (
        <BackupExportDialog
          open={open}
          userId={profile.userId}
          deviceId={device.id}
          privateKey={privateKey}
          onClose={handleClose}
        />
      )}

      {profile && (
        <BackupRestoreDialog
          open={restoreOpen}
          userId={profile.userId}
          onClose={() => setRestoreOpen(false)}
        />
      )}
    </div>
  );
}

function ScreenShareControls() {
  const { t } = useTranslation(['app']);
  const [cfg, setCfg] = useState<ScreenShareSettings>(() => getScreenShareSettings());

  useEffect(() => subscribeScreenShareSettings(setCfg), []);

  const params = getPresetParams(cfg.preset);

  return (
    <>
      <SettingRow label={t('app:settings.screen_share_quality', { defaultValue: 'Qualität' })}>
        <select
          value={cfg.preset}
          onChange={(e) =>
            updateScreenShareSettings({ preset: e.target.value as ScreenSharePreset })
          }
          className="cursor-pointer rounded-lg border border-line bg-surface-3 px-3 py-1.5 text-xs text-fg focus:border-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
        >
          {PRESET_ORDER.map((p) => (
            <option key={p} value={p}>
              {getPresetParams(p).label}
            </option>
          ))}
        </select>
      </SettingRow>
      <div className="rounded-lg border border-line bg-surface-3 p-3 text-[11px] text-fg-muted">
        <div className="grid grid-cols-3 gap-3">
          <Stat label="Bitrate (max)" value={formatBitrate(params.bitrateKbps)} />
          <Stat
            label="Resolution"
            value={params.dims ? params.dims.width + '×' + params.dims.height : 'Auto'}
          />
          <Stat label="Framerate" value={params.framerate + ' fps'} />
        </div>
      </div>
      <p className="text-xs text-fg-muted">
        {t('app:settings.screen_share_hint', {
          defaultValue:
            'WebRTC passt Bitrate + Auflösung dynamisch an die Netzwerkqualität an (SVC/VP9). Die Werte sind Obergrenzen. Änderungen greifen beim nächsten Call.',
        })}
      </p>
    </>
  );
}

function Stat({ label, value }: { label: string; value: string }) {
  return (
    <div>
      <div className="text-[10px] uppercase tracking-[0.1em] text-fg-muted">{label}</div>
      <div className="mt-0.5 font-mono text-fg">{value}</div>
    </div>
  );
}

function formatBitrate(kbps: number): string {
  if (kbps >= 1000) {
    return (kbps / 1000).toFixed(kbps % 1000 === 0 ? 0 : 1) + ' Mbps';
  }
  return kbps + ' kbps';
}

function Section({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <section className="rounded-2xl border border-line bg-surface-2 p-5">
      <h2 className="mb-4 text-xs font-semibold uppercase tracking-[0.1em] text-fg-muted">
        {title}
      </h2>
      <div className="space-y-3">{children}</div>
    </section>
  );
}

function Row({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
  return (
    <div className="flex items-center justify-between gap-4">
      <dt className="text-sm text-fg-muted">{label}</dt>
      <dd
        className={
          'max-w-[60%] truncate text-right text-sm text-fg ' +
          (mono ? 'font-mono text-xs' : '')
        }
        title={value}
      >
        {value}
      </dd>
    </div>
  );
}

function SettingRow({ label, children }: { label: string; children: React.ReactNode }) {
  return (
    <div className="flex items-center justify-between gap-4">
      <span className="text-sm text-fg">{label}</span>
      {children}
    </div>
  );
}

// Theme picker row inside the Appearance section. Same pill-segmented style
// as the language selector so the two siblings read as one control surface.
// The toggle was previously a rail icon in the sidebar; moved here so it
// sits with the other appearance preferences.
function ThemeRow() {
  const { t } = useTranslation(['app']);
  const { theme, setTheme } = useTheme();
  const options: Array<{ value: 'light' | 'dark'; label: string }> = [
    {
      value: 'light',
      label: t('app:theme.light', { defaultValue: 'Light' }),
    },
    {
      value: 'dark',
      label: t('app:theme.dark', { defaultValue: 'Dark' }),
    },
  ];
  return (
    <SettingRow label={t('app:settings.theme', { defaultValue: 'Design' })}>
      <div className="inline-flex rounded-lg border border-line bg-surface-3 p-1">
        {options.map((o) => {
          const active = theme === o.value;
          return (
            <button
              key={o.value}
              type="button"
              onClick={() => setTheme(o.value)}
              className={
                'cursor-pointer rounded-md px-3 py-1.5 text-xs font-medium transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
                (active
                  ? 'bg-accent text-accent-fg'
                  : 'text-fg-muted hover:text-fg')
              }
            >
              {o.label}
            </button>
          );
        })}
      </div>
    </SettingRow>
  );
}

function Toggle({
  label,
  hint,
  checked,
  disabled,
  onChange,
}: {
  label: string;
  hint?: string;
  checked: boolean;
  disabled?: boolean;
  onChange: (next: boolean) => void;
}) {
  return (
    <label className="flex cursor-pointer items-start justify-between gap-4">
      <span className="min-w-0 flex-1">
        <span className="block text-sm text-fg">{label}</span>
        {hint && <span className="mt-1 block text-xs text-fg-muted">{hint}</span>}
      </span>
      <span className="relative mt-0.5 inline-flex h-6 w-11 shrink-0">
        <input
          type="checkbox"
          checked={checked}
          disabled={disabled}
          onChange={(e) => onChange(e.target.checked)}
          className="peer sr-only"
        />
        <span className="inline-block h-6 w-11 rounded-full bg-surface transition peer-checked:bg-accent peer-disabled:opacity-50" />
        <span className="absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white shadow transition peer-checked:translate-x-5" />
      </span>
    </label>
  );
}

// ---------------------------------------------------------------------------
// Audio device selection — persisted input + output deviceIds, hot-swap on
// active calls. Output swap uses HTMLMediaElement.setSinkId on our attached
// <audio> elements (LiveKit's switchActiveDevice only tracks elements it
// attached itself).
// ---------------------------------------------------------------------------

function AudioDeviceControls() {
  const { t } = useTranslation(['app']);
  const { setAudioInputDevice, setAudioOutputDevice, setVideoInputDevice } = useCall();
  const [inputs, setInputs] = useState<MediaDeviceInfo[]>([]);
  const [outputs, setOutputs] = useState<MediaDeviceInfo[]>([]);
  const [cameras, setCameras] = useState<MediaDeviceInfo[]>([]);
  const [inputId, setInputId] = useState<string | null>(
    () => getAudioSettings().inputDeviceId,
  );
  const [outputId, setOutputId] = useState<string | null>(
    () => getAudioSettings().outputDeviceId,
  );
  const [cameraId, setCameraId] = useState<string | null>(
    () => getAudioSettings().videoInputDeviceId,
  );
  const [error, setError] = useState<string | null>(null);
  const [permission, setPermission] = useState<'unknown' | 'granted' | 'denied'>(
    'unknown',
  );

  const refresh = useCallback(async () => {
    try {
      const list = await navigator.mediaDevices.enumerateDevices();
      setInputs(list.filter((d) => d.kind === 'audioinput'));
      setOutputs(list.filter((d) => d.kind === 'audiooutput'));
      setCameras(list.filter((d) => d.kind === 'videoinput'));
      // If labels are empty, permission hasn't been granted yet — browsers
      // mask device names until a getUserMedia call succeeds at least once.
      const hasLabels = list.some(
        (d) => (d.kind === 'audioinput' || d.kind === 'audiooutput') && d.label.length > 0,
      );
      setPermission(hasLabels ? 'granted' : 'unknown');
    } catch (err: unknown) {
      setError(err instanceof Error ? err.message : 'enumerateDevices failed');
    }
  }, []);

  useEffect(() => {
    void refresh();
    const onChange = () => void refresh();
    try {
      navigator.mediaDevices.addEventListener('devicechange', onChange);
    } catch {
      /* some browsers omit devicechange */
    }
    const unsubSettings = subscribeAudioSettings((s) => {
      setInputId(s.inputDeviceId);
      setOutputId(s.outputDeviceId);
      setCameraId(s.videoInputDeviceId);
    });
    return () => {
      try {
        navigator.mediaDevices.removeEventListener('devicechange', onChange);
      } catch {
        /* ignore */
      }
      unsubSettings();
    };
  }, [refresh]);

  const requestPermission = useCallback(async () => {
    setError(null);
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      stream.getTracks().forEach((t) => t.stop());
      setPermission('granted');
      await refresh();
    } catch (err: unknown) {
      setPermission('denied');
      setError(err instanceof Error ? err.message : 'permission denied');
    }
  }, [refresh]);

  const handleInput = useCallback(
    async (id: string) => {
      const next = id === '' ? null : id;
      setInputId(next);
      await setAudioInputDevice(next);
    },
    [setAudioInputDevice],
  );

  const handleOutput = useCallback(
    async (id: string) => {
      const next = id === '' ? null : id;
      setOutputId(next);
      await setAudioOutputDevice(next);
    },
    [setAudioOutputDevice],
  );

  const handleCamera = useCallback(
    async (id: string) => {
      const next = id === '' ? null : id;
      setCameraId(next);
      await setVideoInputDevice(next);
    },
    [setVideoInputDevice],
  );

  const outputSupported =
    typeof HTMLAudioElement !== 'undefined' &&
    typeof HTMLAudioElement.prototype.setSinkId === 'function';

  return (
    <div className="space-y-4">
      <div>
        <div className="text-sm font-semibold text-fg">
          {t('app:settings.mic_title', { defaultValue: 'Mikrofon' })}
        </div>
        <p className="mt-1 text-xs text-fg-muted">
          {t('app:settings.mic_hint', {
            defaultValue: 'Eingabegerät. Bei aktivem Anruf wird live umgeschaltet.',
          })}
        </p>
        <div className="mt-2 flex items-center gap-2">
          <select
            value={inputId ?? ''}
            onChange={(e) => void handleInput(e.target.value)}
            className="flex-1 rounded-lg border border-line bg-surface-2 px-3 py-2 text-sm text-fg focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30"
          >
            <option value="">
              {t('app:settings.mic_default', { defaultValue: 'Systemstandard' })}
            </option>
            {inputs.map((d) => (
              <option key={d.deviceId} value={d.deviceId}>
                {d.label || t('app:settings.mic_unnamed', { defaultValue: 'Unbenannt' })}
              </option>
            ))}
          </select>
          <button
            type="button"
            onClick={() => void refresh()}
            className="cursor-pointer rounded-lg border border-line bg-surface-2 px-3 py-2 text-xs font-medium text-fg transition hover:bg-surface-3"
          >
            {t('app:settings.mic_refresh', { defaultValue: 'Neu laden' })}
          </button>
        </div>
      </div>

      <div>
        <div className="text-sm font-semibold text-fg">
          {t('app:settings.speaker_title', { defaultValue: 'Ausgabegerät' })}
        </div>
        <p className="mt-1 text-xs text-fg-muted">
          {t('app:settings.speaker_hint', {
            defaultValue:
              'Lautsprecher oder Kopfhörer. Wird auf alle aktiven Audio-Streams angewendet.',
          })}
        </p>
        {!outputSupported && (
          <p className="mt-1 text-xs text-amber-600 dark:text-amber-300">
            {t('app:settings.speaker_unsupported', {
              defaultValue: 'Browser unterstützt setSinkId nicht — Ausgabe folgt System-Default.',
            })}
          </p>
        )}
        <div className="mt-2 flex items-center gap-2">
          <select
            value={outputId ?? ''}
            disabled={!outputSupported}
            onChange={(e) => void handleOutput(e.target.value)}
            className="flex-1 rounded-lg border border-line bg-surface-2 px-3 py-2 text-sm text-fg focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30 disabled:opacity-50"
          >
            <option value="">
              {t('app:settings.mic_default', { defaultValue: 'Systemstandard' })}
            </option>
            {outputs.map((d) => (
              <option key={d.deviceId} value={d.deviceId}>
                {d.label || t('app:settings.mic_unnamed', { defaultValue: 'Unbenannt' })}
              </option>
            ))}
          </select>
        </div>
      </div>

      <div>
        <div className="text-sm font-semibold text-fg">
          {t('app:settings.camera_title', { defaultValue: 'Kamera' })}
        </div>
        <p className="mt-1 text-xs text-fg-muted">
          {t('app:settings.camera_hint', {
            defaultValue:
              'Bevorzugte Kamera. Bei aktivem Anruf wird live umgeschaltet.',
          })}
        </p>
        <div className="mt-2 flex items-center gap-2">
          <select
            value={cameraId ?? ''}
            onChange={(e) => void handleCamera(e.target.value)}
            className="flex-1 rounded-lg border border-line bg-surface-2 px-3 py-2 text-sm text-fg focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30"
          >
            <option value="">
              {t('app:settings.mic_default', { defaultValue: 'Systemstandard' })}
            </option>
            {cameras.map((d) => (
              <option key={d.deviceId} value={d.deviceId}>
                {d.label || t('app:settings.mic_unnamed', { defaultValue: 'Unbenannt' })}
              </option>
            ))}
          </select>
        </div>
      </div>

      <div className="border-t border-line pt-3">
        <MicTestSection />
      </div>

      {permission !== 'granted' && (
        <div className="flex items-center justify-between gap-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-xs text-amber-700 dark:text-amber-200">
          <span>
            {t('app:settings.mic_permission_hint', {
              defaultValue:
                'Ohne Mikrofon-Freigabe erscheinen Gerätenamen nicht. Einmal erlauben und Liste lädt neu.',
            })}
          </span>
          <button
            type="button"
            onClick={() => void requestPermission()}
            className="shrink-0 cursor-pointer rounded-md bg-amber-600 px-2.5 py-1 text-xs font-semibold text-white transition hover:bg-amber-500"
          >
            {t('app:settings.mic_grant', { defaultValue: 'Freigeben' })}
          </button>
        </div>
      )}

      {error && (
        <p className="text-xs text-rose-600 dark:text-rose-300">{error}</p>
      )}
    </div>
  );
}
