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 { 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 { loadDevicePrivateKey } from '@chat-app/shared/auth';
import { deleteAvatarObject, uploadAvatar } from '../lib/avatarUpload';
import { devLocalSecretStore } from '../lib/secretStore';
import {
  getPttSettings,
  keyCodeToLabel,
  type PttSettings,
  subscribePttSettings,
  updatePttSettings,
} from '../lib/pttSettings';
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')}>
        <AvatarControls
          patchProfile={patchProfile}
          busy={busy}
        />
        <Row label={t('auth:signed_in.username')} value={profile?.username ?? '—'} />
        <Row label={t('auth:signed_in.display_name')} value={profile?.displayName ?? '—'} />
        <Row label={t('auth:signed_in.email')} value={profile?.userId ?? '—'} mono />
      </Section>

      {/* Appearance */}
      <Section title={t('app:settings.section_appearance')}>
        <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>

      {/* 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">
          <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 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 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 AvatarControls({ patchProfile, busy }: AvatarControlsProps) {
  const { t } = useTranslation(['app']);
  const { profile } = useAuth();
  const inputRef = useRef<HTMLInputElement | null>(null);
  const [uploading, setUploading] = useState(false);
  const [error, setError] = useState<string | null>(null);

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

  async function handleFile(file: File) {
    if (!userId) return;
    setError(null);
    setUploading(true);
    try {
      const newUrl = await uploadAvatar(userId, file);
      const oldUrl = url;
      await patchProfile({ avatarUrl: newUrl });
      if (oldUrl) {
        // Best-effort cleanup of the previous file (don't block on it).
        void deleteAvatarObject(oldUrl).catch(() => {
          /* ignore */
        });
      }
    } catch (err: unknown) {
      setError(err instanceof Error ? err.message : 'upload failed');
    } finally {
      setUploading(false);
      if (inputRef.current) inputRef.current.value = '';
    }
  }

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

  return (
    <div className="flex items-center gap-4">
      <Avatar
        url={url}
        displayName={profile?.displayName ?? profile?.username}
        className="h-16 w-16 text-2xl"
      />

      <div className="flex flex-1 flex-col gap-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>
        {error && (
          <div className="text-xs text-rose-500 dark:text-rose-300">{error}</div>
        )}
      </div>

      <input
        ref={inputRef}
        type="file"
        accept="image/*"
        className="hidden"
        onChange={(e) => {
          const f = e.target.files?.[0];
          if (f) void handleFile(f);
        }}
      />
      <button
        type="button"
        onClick={() => inputRef.current?.click()}
        disabled={busy || uploading}
        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"
      >
        {uploading
          ? t('app:settings.avatar_uploading', { defaultValue: 'Lade…' })
          : url
            ? t('app:settings.avatar_change', { defaultValue: 'Ändern' })
            : t('app:settings.avatar_upload', { defaultValue: 'Hochladen' })}
      </button>
      {url && (
        <button
          type="button"
          onClick={() => void handleRemove()}
          disabled={busy || uploading}
          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>
  );
}

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>
  );
}

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 } = useCall();
  const [inputs, setInputs] = useState<MediaDeviceInfo[]>([]);
  const [outputs, setOutputs] = useState<MediaDeviceInfo[]>([]);
  const [inputId, setInputId] = useState<string | null>(
    () => getAudioSettings().inputDeviceId,
  );
  const [outputId, setOutputId] = useState<string | null>(
    () => getAudioSettings().outputDeviceId,
  );
  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'));
      // 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);
    });
    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 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>

      {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>
  );
}
