import { writable, type Readable } from './store';
import { writeLocalState } from './localstate';
import { pushControlDialog } from './controldialogs';
import type { SignalRef, SignalValue, SignalMeta, HistoryPoint } from './types';

// ── Types ────────────────────────────────────────────────────────────────────

type WsStatus = 'connecting' | 'connected' | 'disconnected';

interface SubscriberEntry {
  onUpdate: (v: SignalValue) => void;
  onMeta: (m: SignalMeta) => void;
}

// A control-logic node-execution event pushed by the server while a debug
// session is active (live observation or dry-run simulate).
export interface DebugNodeEvent {
  graphId: string;
  nodeId: string;
  value: number;
  hasValue: boolean;
  ts: number;
}

// A control-logic graph payload sent for dry-run simulation.
export interface DebugGraph {
  id?: string;
  name?: string;
  nodes: any[];
  wires: any[];
  groups?: any[];
}

// ── WsClient ─────────────────────────────────────────────────────────────────

class WsClient {
  private ws: WebSocket | null = null;
  private url = '';

  // Reconnect back-off state
  private retryDelay = 500;
  private readonly maxDelay = 30_000;
  private retryTimer: ReturnType<typeof setTimeout> | null = null;
  private intentionalClose = false;

  // Status store
  private _statusStore = writable<WsStatus>('connecting');
  readonly status: Readable<WsStatus> = { subscribe: this._statusStore.subscribe };

  // Subscription tracking: key is "ds\0name"
  // ref-count: how many callers are subscribed
  private refCounts = new Map<string, number>();
  // subscriber callbacks per signal
  private subscribers = new Map<string, Set<SubscriberEntry>>();
  // pending history request callbacks keyed by "ds\0name"
  private histCallbacks = new Map<string, Array<(pts: HistoryPoint[]) => void>>();
  // single control-logic debug listener (the editor currently watching)
  private debugListener: ((ev: DebugNodeEvent) => void) | null = null;

  connect(url: string): void {
    this.url = url;
    this.intentionalClose = false;
    this._open();
  }

  private _open(): void {
    if (this.intentionalClose) return;

    this._statusStore.set('connecting');

    try {
      // Resolve relative URL to absolute ws:// URL
      const wsUrl = this._resolveWsUrl(this.url);
      this.ws = new WebSocket(wsUrl);
    } catch {
      this._scheduleReconnect();
      return;
    }

    this.ws.addEventListener('open', () => {
      this.retryDelay = 500; // reset back-off on success
      this._statusStore.set('connected');

      // Re-subscribe all active signals after reconnect
      const signals: Array<{ ds: string; name: string }> = [];
      for (const key of this.refCounts.keys()) {
        if ((this.refCounts.get(key) ?? 0) > 0) {
          const [ds, name] = key.split('\0');
          signals.push({ ds, name });
        }
      }
      if (signals.length > 0) {
        this._send({ type: 'subscribe', signals });
      }
    });

    this.ws.addEventListener('message', (ev) => {
      this._handleMessage(ev.data as string);
    });

    this.ws.addEventListener('close', () => {
      this._statusStore.set('disconnected');
      if (!this.intentionalClose) {
        this._scheduleReconnect();
      }
    });

    this.ws.addEventListener('error', () => {
      // The close event fires right after, which will handle the reconnect.
      this._statusStore.set('disconnected');
    });
  }

  private _resolveWsUrl(path: string): string {
    if (path.startsWith('ws://') || path.startsWith('wss://')) {
      return path;
    }
    const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
    return `${proto}//${location.host}${path}`;
  }

  private _scheduleReconnect(): void {
    if (this.retryTimer !== null) return;
    this.retryTimer = setTimeout(() => {
      this.retryTimer = null;
      this.retryDelay = Math.min(this.retryDelay * 2, this.maxDelay);
      this._open();
    }, this.retryDelay);
  }

  private _send(obj: unknown): void {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(obj));
    }
  }

  private _handleMessage(data: string): void {
    let msg: any;
    try {
      msg = JSON.parse(data);
    } catch {
      return;
    }

    const key = msg.ds && msg.name ? `${msg.ds}\0${msg.name}` : '';

    switch (msg.type) {
      case 'update': {
        const subs = this.subscribers.get(key);
        if (!subs) break;
        const val: SignalValue = {
          value: msg.value,
          quality: msg.quality ?? 'unknown',
          ts: msg.ts ?? null,
          severity: msg.severity ?? 0,
          status: msg.status ?? 0,
        };
        for (const s of subs) s.onUpdate(val);
        break;
      }
      case 'meta': {
        const subs = this.subscribers.get(key);
        if (!subs || !msg.meta) break;
        const meta: SignalMeta = {
          type: msg.meta.type ?? 'unknown',
          unit: msg.meta.unit,
          displayLow: msg.meta.displayLow ?? 0,
          displayHigh: msg.meta.displayHigh ?? 100,
          writable: msg.meta.writable ?? false,
          enumStrings: msg.meta.enumStrings,
          description: msg.meta.description,
        };
        for (const s of subs) s.onMeta(meta);
        break;
      }
      case 'history': {
        const hcbs = this.histCallbacks.get(key);
        if (hcbs) {
          const pts: HistoryPoint[] = (msg.points ?? []).map((p: any) => ({
            ts: p.ts ?? p.TS,
            value: p.value ?? p.Value,
          }));
          this.histCallbacks.delete(key);
          for (const cb of hcbs) cb(pts);
        }
        break;
      }
      case 'dialog': {
        // Server-side control logic requesting a user notification / input.
        pushControlDialog({
          id: String(msg.id),
          kind: msg.kind === 'error' || msg.kind === 'input' ? msg.kind : 'info',
          title: msg.title,
          message: msg.message,
        });
        break;
      }
      case 'debugNode': {
        // Server-side control-logic node execution (live or simulate).
        this.debugListener?.({
          graphId: String(msg.graphId ?? ''),
          nodeId: String(msg.nodeId ?? ''),
          value: typeof msg.value === 'number' ? msg.value : 0,
          hasValue: !!msg.hasValue,
          ts: typeof msg.ts === 'number' ? msg.ts : Date.now(),
        });
        break;
      }
      case 'error':
        // Resolve any pending history callbacks with empty data on error
        if (key && this.histCallbacks.has(key)) {
          const hcbs = this.histCallbacks.get(key)!;
          this.histCallbacks.delete(key);
          for (const cb of hcbs) cb([]);
        }
        console.warn('[ws] server error', msg.code, msg.message);
        break;
      default:
        break;
    }
  }

  /**
   * Subscribe to a signal. Reference-counted: only one WS subscribe message
   * is sent even if multiple callers subscribe to the same signal.
   * Returns a cleanup function that unsubscribes when all callers are done.
   */
  subscribe(
    ref: SignalRef,
    onUpdate: (v: SignalValue) => void,
    onMeta: (m: SignalMeta) => void,
  ): () => void {
    const key = `${ref.ds}\0${ref.name}`;
    const entry: SubscriberEntry = { onUpdate, onMeta };

    // Add to subscriber set
    let set = this.subscribers.get(key);
    if (!set) {
      set = new Set();
      this.subscribers.set(key, set);
    }
    set.add(entry);

    // Reference counting — send WS subscribe only on first subscriber
    const prev = this.refCounts.get(key) ?? 0;
    this.refCounts.set(key, prev + 1);
    if (prev === 0) {
      this._send({ type: 'subscribe', signals: [{ ds: ref.ds, name: ref.name }] });
    }

    return () => {
      const subs = this.subscribers.get(key);
      subs?.delete(entry);

      const count = (this.refCounts.get(key) ?? 0) - 1;
      this.refCounts.set(key, Math.max(0, count));

      if (count <= 0) {
        this.subscribers.delete(key);
        this.refCounts.delete(key);
        this._send({ type: 'unsubscribe', signals: [{ ds: ref.ds, name: ref.name }] });
      }
    };
  }

  /**
   * Request historical data for a signal.
   * Resolves with an array of HistoryPoint once the server responds.
   * Resolves with [] if the datasource doesn't support history or times out.
   */
  history(
    ref: SignalRef,
    start: string,
    end: string,
    maxPoints = 5000,
  ): Promise<HistoryPoint[]> {
    return new Promise((resolve) => {
      const key = `${ref.ds}\0${ref.name}`;

      // Timeout after 15 s — resolves with empty array
      const timer = setTimeout(() => {
        const cbs = this.histCallbacks.get(key);
        if (cbs) {
          const idx = cbs.indexOf(wrappedResolve);
          if (idx >= 0) cbs.splice(idx, 1);
          if (cbs.length === 0) this.histCallbacks.delete(key);
        }
        resolve([]);
      }, 15_000);

      const wrappedResolve = (pts: HistoryPoint[]) => {
        clearTimeout(timer);
        resolve(pts);
      };

      let cbs = this.histCallbacks.get(key);
      if (!cbs) {
        cbs = [];
        this.histCallbacks.set(key, cbs);
      }
      cbs.push(wrappedResolve);

      this._send({ type: 'history', ds: ref.ds, name: ref.name, start, end, maxPoints });
    });
  }

  write(ref: SignalRef, value: any): void {
    // Panel-local variables never go to the server — update them in place.
    if (ref.ds === 'local') {
      writeLocalState(ref.name, value);
      return;
    }
    console.log('[ws] write', ref.ds, ref.name, value, 'ws state:', this.ws?.readyState);
    this._send({ type: 'write', ds: ref.ds, name: ref.name, value });
  }

  /**
   * Answer a control-logic input dialog. `value === null` cancels (dismisses)
   * the dialog without writing its target server variable.
   */
  sendDialogResponse(id: string, value: number | null): void {
    this._send({ type: 'dialogResponse', id, value });
  }

  // ── Control-logic debug ──────────────────────────────────────────────────

  /** Register the listener that receives node-execution events. Returns a
   *  cleanup that clears it (only one editor watches at a time). */
  onDebugNode(cb: (ev: DebugNodeEvent) => void): () => void {
    this.debugListener = cb;
    return () => {
      if (this.debugListener === cb) this.debugListener = null;
    };
  }

  /** Observe the running, enabled control-logic graph identified by graphId. */
  debugSubscribeLive(graphId: string): void {
    this._send({ type: 'debugSubscribe', mode: 'live', graphId });
  }

  /** Dry-run an unsaved control-logic graph in a server sandbox (no real
   *  writes); node events come back as debugNode messages. */
  debugSubscribeSimulate(graph: DebugGraph): void {
    this._send({ type: 'debugSubscribe', mode: 'simulate', graph });
  }

  /** Stop the current debug session (tears down any simulate sandbox). */
  debugUnsubscribe(): void {
    this._send({ type: 'debugUnsubscribe' });
  }

  /** Force a trigger node of the current debug session (live or simulate) to
   *  fire now. nodeId must be a trigger node in the graph being debugged. */
  debugFireTrigger(nodeId: string): void {
    this._send({ type: 'fireTrigger', nodeId });
  }
}

// Singleton instance
export const wsClient = new WsClient();
