/**
 * Notification manager — drives global banners and toasts based on app state changes.
 */

import { deesCatalog } from '../plugins.js';
import { appState, type IAppState } from './appstate.js';

type BannerType = 'info' | 'success' | 'warning' | 'error';
type ToastType = 'info' | 'success' | 'warning' | 'error';

export class NotificationManager {
  private appdash: InstanceType<typeof deesCatalog.DeesSimpleAppDash> | null = null;
  private prevState: IAppState | null = null;
  private unsubscribe: (() => void) | null = null;

  private activeBannerIds = new Set<string>();
  private userDismissedIds = new Set<string>();

  private suppressToastsUntil = 0;
  private wsDisconnectTimer: ReturnType<typeof setTimeout> | null = null;
  private wsWasConnected = false;

  init(appdash: InstanceType<typeof deesCatalog.DeesSimpleAppDash>): void {
    this.appdash = appdash;
    this.suppressToastsUntil = Date.now() + 3000;

    appdash.addEventListener('message-dismiss', ((e: CustomEvent) => {
      const id = e.detail?.id;
      if (id) {
        this.userDismissedIds.add(id);
        this.activeBannerIds.delete(id);
      }
    }) as EventListener);

    this.unsubscribe = appState.subscribe((state) => this.onStateChange(state));
  }

  destroy(): void {
    this.unsubscribe?.();
    this.unsubscribe = null;
    if (this.wsDisconnectTimer) {
      clearTimeout(this.wsDisconnectTimer);
      this.wsDisconnectTimer = null;
    }
  }

  // ---------------------------------------------------------------------------
  // Change detection
  // ---------------------------------------------------------------------------

  private onStateChange(newState: IAppState): void {
    const prev = this.prevState;

    // First state: snapshot and exit without notifications.
    if (!prev) {
      this.prevState = newState;
      // Exception: if we start disconnected, show the banner immediately.
      if (!newState.connected) {
        this.addBanner('ws-disconnected', 'error', 'Connection to server lost. Reconnecting...', { dismissible: false });
      }
      return;
    }

    this.checkWebSocketConnectivity(prev, newState);
    this.checkProviders(prev, newState);
    this.checkDevices(prev, newState);
    this.checkOriginatedCalls(prev, newState);

    this.prevState = newState;
  }

  // ---------------------------------------------------------------------------
  // WebSocket connectivity
  // ---------------------------------------------------------------------------

  private checkWebSocketConnectivity(prev: IAppState, next: IAppState): void {
    if (prev.connected && !next.connected) {
      // Disconnected.
      this.addBanner('ws-disconnected', 'error', 'Connection to server lost. Reconnecting...', { dismissible: false });
      this.wsWasConnected = true;
      // Debounce: set timer so we can suppress toast on rapid reconnect.
      if (this.wsDisconnectTimer) clearTimeout(this.wsDisconnectTimer);
      this.wsDisconnectTimer = setTimeout(() => { this.wsDisconnectTimer = null; }, 500);
    } else if (!prev.connected && next.connected) {
      // Reconnected.
      this.removeBanner('ws-disconnected');
      if (this.wsWasConnected && !this.wsDisconnectTimer) {
        this.showToast('Reconnected to server', 'success');
      }
      if (this.wsDisconnectTimer) {
        clearTimeout(this.wsDisconnectTimer);
        this.wsDisconnectTimer = null;
      }
    }
  }

  // ---------------------------------------------------------------------------
  // Provider registration
  // ---------------------------------------------------------------------------

  private checkProviders(prev: IAppState, next: IAppState): void {
    const prevMap = new Map(prev.providers.map((p) => [p.id, p]));
    const nextMap = new Map(next.providers.map((p) => [p.id, p]));

    for (const [id, np] of nextMap) {
      const pp = prevMap.get(id);
      if (!pp) continue; // New provider appeared — no transition to report.

      const bannerId = `provider-unreg-${id}`;

      if (pp.registered && !np.registered) {
        // Registration lost.
        this.addBanner(bannerId, 'warning', `${np.displayName}: registration lost`);
      } else if (!pp.registered && np.registered) {
        // Registration restored.
        this.removeBanner(bannerId);
        this.showToast(`${np.displayName} registered`, 'success');
      }
    }
  }

  // ---------------------------------------------------------------------------
  // Device connectivity
  // ---------------------------------------------------------------------------

  private checkDevices(prev: IAppState, next: IAppState): void {
    const prevMap = new Map(prev.devices.map((d) => [d.id, d]));
    const nextMap = new Map(next.devices.map((d) => [d.id, d]));

    for (const [id, nd] of nextMap) {
      // Skip browser devices for banners — connections are transient.
      if (nd.isBrowser) continue;

      const pd = prevMap.get(id);
      if (!pd) continue; // New device appeared — no transition.

      const bannerId = `device-offline-${id}`;

      if (pd.connected && !nd.connected) {
        this.addBanner(bannerId, 'warning', `${nd.displayName} disconnected`);
      } else if (!pd.connected && nd.connected) {
        this.removeBanner(bannerId);
        this.showToast(`${nd.displayName} connected`, 'success');
      }
    }

    // Handle devices that disappeared entirely (browser device left).
    for (const [id, pd] of prevMap) {
      if (pd.isBrowser) continue;
      if (!nextMap.has(id) && pd.connected) {
        this.addBanner(`device-offline-${id}`, 'warning', `${pd.displayName} disconnected`);
      }
    }
  }

  // ---------------------------------------------------------------------------
  // Originated call state transitions
  // ---------------------------------------------------------------------------

  private checkOriginatedCalls(prev: IAppState, next: IAppState): void {
    const prevMap = new Map(prev.calls.map((c) => [c.id, c]));

    for (const nc of next.calls) {
      const pc = prevMap.get(nc.id);
      if (!pc) continue;

      const label = nc.calleeNumber || nc.callerNumber || nc.id;
      if (pc.state !== 'connected' && nc.state === 'connected') {
        this.showToast(`Call ${label} connected`, 'success');
      } else if (pc.state !== 'terminated' && nc.state === 'terminated') {
        this.showToast(`Call ${label} ended`, 'info');
      }
    }
  }

  // ---------------------------------------------------------------------------
  // Banner helpers
  // ---------------------------------------------------------------------------

  private addBanner(id: string, type: BannerType, message: string, opts?: { dismissible?: boolean }): void {
    if (!this.appdash) return;
    if (this.activeBannerIds.has(id)) return;
    if (this.userDismissedIds.has(id)) return;

    this.appdash.addMessage({
      id,
      type,
      message,
      dismissible: opts?.dismissible ?? true,
    });
    this.activeBannerIds.add(id);
  }

  private removeBanner(id: string): void {
    if (!this.appdash) return;
    if (!this.activeBannerIds.has(id)) {
      // Also clear user-dismissed tracking so the banner can appear again if the problem recurs.
      this.userDismissedIds.delete(id);
      return;
    }

    this.appdash.removeMessage(id);
    this.activeBannerIds.delete(id);
    this.userDismissedIds.delete(id);
  }

  // ---------------------------------------------------------------------------
  // Toast helper
  // ---------------------------------------------------------------------------

  private showToast(message: string, type: ToastType = 'info', duration = 3000): void {
    if (Date.now() < this.suppressToastsUntil) return;
    deesCatalog.DeesToast.show({ message, type, duration, position: 'top-right' });
  }
}
