// Per-participant output volume overrides. Values are 0..1 (HTMLMediaElement
// scale). Persisted so the user's mixing choices survive reconnects.
//
// The store is intentionally tiny — a flat Record keyed by LiveKit identity
// (our `userId`). `attachTrack` reads from here when a remote audio track
// first lands; live changes are pushed to any already-attached audio
// elements via the `data-participant` attribute selector.

const STORAGE_KEY = 'call.participantVolumes.v1';
const DEFAULT_VOLUME = 1;

type VolumeMap = Record<string, number>;
type Listener = (map: VolumeMap) => void;

function load(): VolumeMap {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return {};
    const parsed: unknown = JSON.parse(raw);
    if (!parsed || typeof parsed !== 'object') return {};
    const out: VolumeMap = {};
    for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
      if (typeof v === 'number' && Number.isFinite(v)) {
        out[k] = clamp(v);
      }
    }
    return out;
  } catch {
    return {};
  }
}

// Matches Discord's slider range — up to 200% via a WebAudio GainNode in
// remoteAudioPipelines (HTMLMediaElement.volume caps at 1.0 on its own).
const MAX_VOLUME = 2;

function clamp(v: number): number {
  if (!Number.isFinite(v)) return 0;
  if (v < 0) return 0;
  if (v > MAX_VOLUME) return MAX_VOLUME;
  return v;
}

let current: VolumeMap = load();
const listeners = new Set<Listener>();

function persist(): void {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(current));
  } catch {
    /* ignore quota */
  }
}

function notify(): void {
  for (const fn of listeners) fn(current);
}

export function getParticipantVolume(userId: string): number {
  return current[userId] ?? DEFAULT_VOLUME;
}

export function setParticipantVolume(userId: string, volume: number): void {
  const next = clamp(volume);
  if (next === (current[userId] ?? DEFAULT_VOLUME)) return;
  current = { ...current, [userId]: next };
  persist();
  applyToAttachedElements(userId, next);
  notify();
}

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

// Apply a volume to any audio elements already attached for this user.
// Attached elements are tagged with `data-participant` in attachTrack.
// HTMLMediaElement.volume is hard-clamped to [0, 1] — anything above 1
// throws IndexSizeError. Values above 1 are only meaningful on the
// WebAudio path (remoteAudioPipelines' GainNode handles them); on the
// plain-element fallback path we clip at 1.0 so the user just hears the
// loudest level the element supports rather than an exception.
function applyToAttachedElements(userId: string, volume: number): void {
  const elVolume = Math.min(1, Math.max(0, volume));
  const nodes = document.querySelectorAll<HTMLAudioElement>(
    'audio[data-participant="' + cssEscape(userId) + '"]',
  );
  nodes.forEach((el) => {
    el.volume = elVolume;
  });
}

function cssEscape(v: string): string {
  if (typeof (globalThis as { CSS?: { escape?: (s: string) => string } }).CSS
    ?.escape === 'function') {
    return (globalThis as { CSS: { escape: (s: string) => string } }).CSS.escape(v);
  }
  return v.replace(/"/g, '\\"');
}
