import { runWhenIdle } from '../../shared/rxjs/idle';

/**
 * Detects environments where writes should bypass the idle queue
 * and flush synchronously (unit tests, SSR, etc.). Browsers with a
 * real document/window get the deferred path.
 */
function isSyncFlushEnvironment(): boolean {
  if (typeof window === 'undefined' || typeof document === 'undefined') {
    return true;
  }

  // Vitest / Jest set NODE_ENV=test
  const proc = (globalThis as unknown as { process?: { env?: Record<string, string> } }).process;

  return proc?.env?.['NODE_ENV'] === 'test' || proc?.env?.['VITEST'] === 'true';
}

/**
 * Coalesced-write wrapper around `localStorage`.
 *
 * - **Reads** are still synchronous (you typically need the value during
 *   service initialisation) but cached after the first hit so repeat
 *   parses do not block the render thread.
 * - **Writes** are queued and flushed in a single `requestIdleCallback`
 *   (or `setTimeout(0)` fallback). Rapid signal updates (e.g. drag-resize,
 *   typing) coalesce into one `JSON.stringify` + write.
 * - **Equality** uses a cheap reference / shallow compare before scheduling
 *   a flush so the heavy `JSON.stringify` only runs when the value really
 *   changed.
 *
 * This service exists so domain stores (plugins, themes, voice settings,
 * server endpoints, friend list, ICE servers, etc.) can stop hand-rolling
 * `JSON.parse` / `JSON.stringify` calls on the render path.
 */
export class JsonStorageService {
  private readonly pendingWrites = new Map<string, unknown>();
  private readonly subscribers = new Map<string, Set<(value: unknown) => void>>();
  private flushScheduled = false;
  private cancelScheduledFlush: (() => void) | null = null;

  read<T>(key: string, fallback: T): T {
    if (this.pendingWrites.has(key)) {
      return this.pendingWrites.get(key) as T;
    }

    const raw = this.safeReadRaw(key);

    if (raw === null) {
      return fallback;
    }

    try {
      return JSON.parse(raw) as T;
    } catch {
      return fallback;
    }
  }

  /**
   * Queue a write. Multiple writes to the same key within the same idle
   * window collapse to a single stringify + `localStorage.setItem`.
   *
   * Pass `equals` for a fast custom equality test (e.g. an `id` compare)
   * to skip flushes when the payload is structurally unchanged.
   */
  write<T>(key: string, value: T, equals?: (prev: T, next: T) => boolean): void {
    const previous = this.pendingWrites.has(key) ? this.pendingWrites.get(key) as T : undefined;

    if (previous !== undefined && equals && equals(previous, value)) {
      return;
    }

    if (previous === value) {
      return;
    }

    this.pendingWrites.set(key, value);
    this.notify(key, value);
    this.scheduleFlush();
  }

  remove(key: string): void {
    this.pendingWrites.delete(key);

    try {
      localStorage.removeItem(key);
    } catch {
      // ignore quota / access errors
    }

    this.notify(key, undefined);
  }

  /**
   * Subscribe to value changes for a key. Fires synchronously on each
   * `write` (with the queued value) so signal-bridges stay reactive even
   * if the flush is deferred. Returns an unsubscribe function.
   */
  subscribe<T>(key: string, listener: (value: T | undefined) => void): () => void {
    let subs = this.subscribers.get(key);

    if (!subs) {
      subs = new Set();
      this.subscribers.set(key, subs);
    }

    subs.add(listener as (value: unknown) => void);

    return () => {
      const current = this.subscribers.get(key);

      if (!current) {
        return;
      }

      current.delete(listener as (value: unknown) => void);

      if (current.size === 0) {
        this.subscribers.delete(key);
      }
    };
  }

  /** Force any queued writes to disk immediately. */
  flush(): void {
    if (this.cancelScheduledFlush) {
      this.cancelScheduledFlush();
      this.cancelScheduledFlush = null;
    }

    this.flushScheduled = false;
    this.runFlush();
  }

  private scheduleFlush(): void {
    if (isSyncFlushEnvironment()) {
      this.runFlush();
      return;
    }

    if (this.flushScheduled) {
      return;
    }

    this.flushScheduled = true;
    this.cancelScheduledFlush = runWhenIdle(() => {
      this.cancelScheduledFlush = null;
      this.flushScheduled = false;
      this.runFlush();
    });
  }

  private runFlush(): void {
    if (this.pendingWrites.size === 0) {
      return;
    }

    const snapshot = Array.from(this.pendingWrites.entries());

    this.pendingWrites.clear();

    for (const [key, value] of snapshot) {
      try {
        const serialized = JSON.stringify(value);

        localStorage.setItem(key, serialized);
      } catch {
        // quota exceeded / privacy mode - keep the value in cache so the
        // caller still observes the update, just no longer persisted
      }
    }
  }

  private safeReadRaw(key: string): string | null {
    try {
      return localStorage.getItem(key);
    } catch {
      return null;
    }
  }

  private notify(key: string, value: unknown): void {
    const subs = this.subscribers.get(key);

    if (!subs) {
      return;
    }

    for (const listener of subs) {
      try {
        listener(value);
      } catch {
        // a subscriber failure should not block the others
      }
    }
  }
}

/**
 * Process-wide singleton. Consumers should prefer this over instantiating
 * the class directly so writes coalesce across services.
 */
export const jsonStorage = new JsonStorageService();
