/* eslint-disable @typescript-eslint/member-ordering */
import { Injectable, signal } from '@angular/core';

/**
 * All known sound effects shipped with the application.
 *
 * Each key maps to a file in `src/assets/audio/`.
 */
export enum AppSound {
  Call = 'call',
  Joining = 'joining',
  Leave = 'leave',
  Notification = 'notification'
}

/** Path prefix for audio assets (served from the `assets/audio/` folder). */
const AUDIO_BASE = 'assets/audio';
/** File extension used for all sound-effect assets. */
const AUDIO_EXT = 'wav';
/** localStorage key for persisting notification volume. */
const STORAGE_KEY_NOTIFICATION_VOLUME = 'metoyou_notification_volume';
/** Default notification volume (0 - 1). */
const DEFAULT_VOLUME = 0.2;

/**
 * A lightweight audio playback service that pre-loads the shipped
 * sound-effect files and lets any component / service trigger them
 * by name.
 *
 * Usage:
 * ```ts
 * audioService.play(AppSound.Joining);
 * ```
 */
@Injectable({ providedIn: 'root' })
export class NotificationAudioService {
  /** Pre-loaded audio buffers keyed by {@link AppSound}. */
  private readonly cache = new Map<AppSound, HTMLAudioElement>();

  private readonly sources = new Map<AppSound, string>();

  private readonly activeLoops = new Map<AppSound, HTMLAudioElement>();

  /** Reactive notification volume (0 - 1), persisted to localStorage. */
  readonly notificationVolume = signal(this.loadVolume());

  /** When true, all sound playback is suppressed (Do Not Disturb). */
  readonly dndMuted = signal(false);

  constructor() {
    this.preload();
  }

  /** Eagerly create (and start loading) an {@link HTMLAudioElement} for every known sound. */
  private preload(): void {
    for (const sound of Object.values(AppSound)) {
      const src = this.resolveAudioUrl(sound);
      const audio = new Audio();

      audio.preload = 'auto';
      audio.src = src;
      audio.load();

      this.sources.set(sound, src);
      this.cache.set(sound, audio);
    }
  }

  private resolveAudioUrl(sound: AppSound): string {
    return new URL(`${AUDIO_BASE}/${sound}.${AUDIO_EXT}`, document.baseURI).toString();
  }

  /** Read persisted volume from localStorage, falling back to the default. */
  private loadVolume(): number {
    try {
      const raw = localStorage.getItem(STORAGE_KEY_NOTIFICATION_VOLUME);

      if (raw !== null) {
        const parsed = parseFloat(raw);

        if (!isNaN(parsed))
          return Math.max(0, Math.min(1, parsed));
      }
    } catch {}

    return DEFAULT_VOLUME;
  }

  /**
   * Update the notification volume and persist it.
   *
   * @param volume - A value between 0 (silent) and 1 (full).
   */
  setNotificationVolume(volume: number): void {
    const clamped = Math.max(0, Math.min(1, volume));

    this.notificationVolume.set(clamped);

    try {
      localStorage.setItem(STORAGE_KEY_NOTIFICATION_VOLUME, String(clamped));
    } catch {}
  }

  /**
   * Play a sound effect at the current notification volume.
   *
   * If playback fails (e.g. browser autoplay policy) the error is
   * silently swallowed - sound effects are non-critical.
   *
   * @param sound - The {@link AppSound} to play.
   * @param volumeOverride - Optional explicit volume (0 - 1). When omitted
   *                         the persisted {@link notificationVolume} is used.
   */
  play(sound: AppSound, volumeOverride?: number): void {
    if (this.dndMuted())
      return;

    const cached = this.cache.get(sound);
    const src = this.sources.get(sound);

    if (!cached || !src)
      return;

    const vol = volumeOverride ?? this.notificationVolume();

    if (vol === 0)
      return; // skip playback when muted

    if (cached.readyState === HTMLMediaElement.HAVE_NOTHING) {
      cached.load();
    }

    // Clone so overlapping plays don't cut each other off.
    const clone = cached.cloneNode(true) as HTMLAudioElement;

    clone.preload = 'auto';
    clone.volume = Math.max(0, Math.min(1, vol));
    clone.play().catch(() => {
      const fallback = new Audio(src);

      fallback.preload = 'auto';
      fallback.volume = clone.volume;
      fallback.play().catch(() => {
        /* swallow autoplay errors */
      });
    });
  }

  playLoop(sound: AppSound, volumeOverride?: number): void {
    if (this.dndMuted() || this.activeLoops.has(sound))
      return;

    const src = this.sources.get(sound) ?? this.resolveAudioUrl(sound);
    const vol = volumeOverride ?? this.notificationVolume();

    if (vol === 0)
      return;

    const audio = new Audio(src);

    audio.loop = true;
    audio.preload = 'auto';
    audio.volume = Math.max(0, Math.min(1, vol));
    this.activeLoops.set(sound, audio);
    audio.play().catch(() => {
      this.activeLoops.delete(sound);
    });
  }

  stop(sound: AppSound): void {
    const audio = this.activeLoops.get(sound);

    if (!audio)
      return;

    audio.pause();
    audio.currentTime = 0;
    audio.remove();
    this.activeLoops.delete(sound);
  }
}
