// Audio quality preferences for outgoing voice/music. `voice` is the default
// — Opus 48 kbps stereo with full DSP (echo cancellation + noise suppression
// + AGC). `hifi` bumps to 96 kbps stereo Opus and disables all DSP so music,
// instruments, or broadcast-style voice streams stay uncoloured.

const STORAGE_KEY = 'chatapp.audio';

export type AudioQuality = 'voice' | 'hifi';

export interface AudioSettings {
  quality: AudioQuality;
  // Preferred input deviceId from enumerateDevices. null = use browser default
  // (whatever the OS points at). Persisted across sessions, so "grandma's
  // mic is default" stays even after browser picks the wrong device.
  inputDeviceId: string | null;
  // Preferred output (speaker/headphone) deviceId. null = system default.
  // Applied to every <audio> element we attach via HTMLMediaElement.setSinkId.
  outputDeviceId: string | null;
  // RMS threshold (0..1) for the "is this participant talking" ring. Lower =
  // more sensitive. Default 0.03 catches soft speech without lighting up on
  // keyboard noise. Users on quiet mics can lower; users in noisy rooms bump up.
  voiceThreshold: number;
  // Per-publish DSP toggle. Off lets hifi-style music go through unmodified;
  // on cleans up voice when the quality preset doesn't already imply it.
  // Defaults to "follow the quality preset".
  noiseSuppression: boolean;
  // Background blur on the local camera track. Lazy — requires
  // @livekit/track-processors + its MediaPipe selfie-segmentation model
  // (~1.5MB) which downloads on first activation.
  videoBackgroundBlur: boolean;
  // Preferred camera deviceId from enumerateDevices. null = use browser
  // default. Persisted across sessions; applied when toggleCamera publishes
  // a new track. Discord-parity: lets users with multiple cams pin one.
  videoInputDeviceId: string | null;
  // Ringtone volume for both the generated oscillator fallback and the
  // custom incoming-call audio file. 0..1; applied on top of the base
  // oscillator gain so the fallback stays audible at 100% without being
  // harsh at 25%. Separate from any system / call audio volume so users
  // can have loud rings + soft in-call audio.
  ringtoneVolume: number;
}

const DEFAULTS: AudioSettings = {
  quality: 'voice',
  inputDeviceId: null,
  outputDeviceId: null,
  voiceThreshold: 0.03,
  // Off by default — browser-native NS colours voice audibly on some
  // mics and is a frequent "why does my voice sound weird" report.
  // Users who want it enable it explicitly in Settings → Sprache.
  noiseSuppression: false,
  videoBackgroundBlur: false,
  videoInputDeviceId: null,
  ringtoneVolume: 0.9,
};

export interface AudioQualityParams {
  label: string;
  bitrateKbps: number;
  stereo: boolean;
  sampleRateHz: number;
  // DSP toggles — off for hifi so music isn't coloured by noise-suppression.
  echoCancellation: boolean;
  noiseSuppression: boolean;
  autoGainControl: boolean;
}

const PARAMS: Record<AudioQuality, AudioQualityParams> = {
  voice: {
    label: 'Voice · 48 kbps',
    bitrateKbps: 48,
    stereo: false,
    sampleRateHz: 48_000,
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true,
  },
  hifi: {
    label: 'HiFi · 510 kbps Stereo',
    bitrateKbps: 510, // Opus max, ~CD-quality stereo
    stereo: true,
    sampleRateHz: 48_000,
    echoCancellation: false,
    noiseSuppression: false,
    autoGainControl: false,
  },
};

export const AUDIO_QUALITY_ORDER: ReadonlyArray<AudioQuality> = ['voice', 'hifi'];

export function getAudioQualityParams(q: AudioQuality): AudioQualityParams {
  return PARAMS[q];
}

type Listener = (s: AudioSettings) => void;
const listeners = new Set<Listener>();

let cached: AudioSettings | null = null;

function isQuality(v: unknown): v is AudioQuality {
  return v === 'voice' || v === 'hifi';
}

function read(): AudioSettings {
  if (cached) return cached;
  try {
    const raw = window.localStorage.getItem(STORAGE_KEY);
    if (!raw) {
      cached = DEFAULTS;
      return cached;
    }
    const parsed = JSON.parse(raw) as Partial<AudioSettings>;
    const rawThreshold = typeof parsed.voiceThreshold === 'number' ? parsed.voiceThreshold : NaN;
    cached = {
      quality: isQuality(parsed.quality) ? parsed.quality : DEFAULTS.quality,
      inputDeviceId:
        typeof parsed.inputDeviceId === 'string' && parsed.inputDeviceId.length > 0
          ? parsed.inputDeviceId
          : DEFAULTS.inputDeviceId,
      outputDeviceId:
        typeof parsed.outputDeviceId === 'string' && parsed.outputDeviceId.length > 0
          ? parsed.outputDeviceId
          : DEFAULTS.outputDeviceId,
      voiceThreshold:
        Number.isFinite(rawThreshold) && rawThreshold >= 0.005 && rawThreshold <= 0.2
          ? rawThreshold
          : DEFAULTS.voiceThreshold,
      noiseSuppression:
        typeof parsed.noiseSuppression === 'boolean'
          ? parsed.noiseSuppression
          : DEFAULTS.noiseSuppression,
      videoBackgroundBlur:
        typeof parsed.videoBackgroundBlur === 'boolean'
          ? parsed.videoBackgroundBlur
          : DEFAULTS.videoBackgroundBlur,
      videoInputDeviceId:
        typeof parsed.videoInputDeviceId === 'string' && parsed.videoInputDeviceId.length > 0
          ? parsed.videoInputDeviceId
          : DEFAULTS.videoInputDeviceId,
      ringtoneVolume:
        typeof parsed.ringtoneVolume === 'number' &&
        Number.isFinite(parsed.ringtoneVolume) &&
        parsed.ringtoneVolume >= 0 &&
        parsed.ringtoneVolume <= 1
          ? parsed.ringtoneVolume
          : DEFAULTS.ringtoneVolume,
    };
    return cached;
  } catch {
    cached = DEFAULTS;
    return cached;
  }
}

function write(s: AudioSettings): void {
  cached = s;
  try {
    window.localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
  } catch {
    /* quota / private mode */
  }
  for (const l of listeners) l(s);
}

export function getAudioSettings(): AudioSettings {
  return read();
}

export function updateAudioSettings(patch: Partial<AudioSettings>): AudioSettings {
  const next = { ...read(), ...patch };
  write(next);
  return next;
}

export function subscribeAudioSettings(listener: Listener): () => void {
  listeners.add(listener);
  return () => listeners.delete(listener);
}
