// Delta Exchange public WebSocket client — real-time ticker stream with
// automatic reconnect + exponential backoff and a connection health signal.
// Docs: https://docs.delta.exchange/#public-channels ("v2/ticker")

export type ConnectionState = "idle" | "connecting" | "open" | "reconnecting" | "closed";

export interface TickerUpdate {
  symbol: string;
  price: number;          // best price for display: mid(bid,ask) if fresh L1, else mark_price, else close
  mark_price?: number;
  last_price?: number;    // last trade price (close)
  bid?: number;           // best bid from L1 orderbook
  ask?: number;           // best ask from L1 orderbook
  spread_bps?: number;    // (ask-bid)/mid * 10_000
  change24h?: number;
  volume24h?: number;
  timestamp: number;
  source: "ticker" | "l1" | "mark";
}


type Listener = (t: TickerUpdate) => void;
type StateListener = (s: { state: ConnectionState; attempt: number; lastMessageAt: number | null }) => void;

const DEFAULT_URL =
  (import.meta.env.VITE_DELTA_WS_URL as string | undefined) ?? "wss://socket.india.delta.exchange";

export class DeltaWsClient {
  private ws: WebSocket | null = null;
  // Ref-counted subscriptions: two components subscribing to the same symbol
  // must not cancel each other on unmount. Track how many consumers hold each.
  private subs = new Map<string, number>();
  private listeners = new Set<Listener>();
  private stateListeners = new Set<StateListener>();
  private state: ConnectionState = "idle";
  private attempt = 0;
  private lastMessageAt: number | null = null;
  private reconnectTimer: number | null = null;
  private heartbeat: number | null = null;
  private stopped = false;

  constructor(private url: string = DEFAULT_URL) {}

  get connectionState() {
    return this.state;
  }

  onTick(fn: Listener) {
    this.listeners.add(fn);
    return () => this.listeners.delete(fn);
  }

  onState(fn: StateListener) {
    this.stateListeners.add(fn);
    fn({ state: this.state, attempt: this.attempt, lastMessageAt: this.lastMessageAt });
    return () => this.stateListeners.delete(fn);
  }

  subscribe(symbols: string[]) {
    const added: string[] = [];
    for (const s of symbols) {
      const n = this.subs.get(s) ?? 0;
      this.subs.set(s, n + 1);
      if (n === 0) added.push(s);
    }
    if (added.length && this.state === "open") this.sendSubscribe(added);
    if (this.state === "idle" || this.state === "closed") this.connect();
  }

  unsubscribe(symbols: string[]) {
    const removed: string[] = [];
    for (const s of symbols) {
      const n = this.subs.get(s) ?? 0;
      if (n <= 1) {
        this.subs.delete(s);
        if (n === 1) removed.push(s);
      } else {
        this.subs.set(s, n - 1);
      }
    }
    if (removed.length && this.state === "open") {
      this.ws?.send(
        JSON.stringify({
          type: "unsubscribe",
          payload: {
            channels: [
              { name: "v2/ticker", symbols: removed },
              { name: "l1_orderbook", symbols: removed },
            ],
          },
        }),
      );
    }

  }


  start() {
    this.stopped = false;
    if (!this.ws) this.connect();
  }

  stop() {
    this.stopped = true;
    if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
    if (this.heartbeat) clearInterval(this.heartbeat);
    this.ws?.close();
    this.ws = null;
    this.setState("closed");
  }

  private setState(s: ConnectionState) {
    this.state = s;
    this.stateListeners.forEach((l) =>
      l({ state: s, attempt: this.attempt, lastMessageAt: this.lastMessageAt }),
    );
  }

  private connect() {
    if (this.stopped) return;
    this.setState(this.attempt === 0 ? "connecting" : "reconnecting");
    try {
      this.ws = new WebSocket(this.url);
    } catch {
      this.scheduleReconnect();
      return;
    }
    this.ws.addEventListener("open", () => {
      this.attempt = 0;
      this.setState("open");
      if (this.subs.size) this.sendSubscribe([...this.subs.keys()]);
      // Delta expects periodic pings; also use it as a liveness signal.
      if (this.heartbeat) clearInterval(this.heartbeat);
      this.heartbeat = window.setInterval(() => {
        try {
          this.ws?.send(JSON.stringify({ type: "ping" }));
        } catch {
          /* noop */
        }
      }, 25_000);
    });
    this.ws.addEventListener("message", (e) => {
      this.lastMessageAt = Date.now();
      let msg: Record<string, unknown>;
      try {
        msg = JSON.parse(typeof e.data === "string" ? e.data : "");
      } catch {
        return;
      }
      const symbol = (msg.symbol as string) ?? "";
      if (!symbol) return;

      // L1 orderbook update — tightest, freshest price signal (bid/ask).
      // Delta emits `l1_orderbook` with best_bid / best_ask fields.
      const bid = num(msg.best_bid ?? (msg as Record<string, unknown>).buy_price);
      const ask = num(msg.best_ask ?? (msg as Record<string, unknown>).sell_price);
      if (bid != null && ask != null && bid > 0 && ask > 0 && ask >= bid) {
        const mid = (bid + ask) / 2;
        const spread_bps = ((ask - bid) / mid) * 10_000;
        const update: TickerUpdate = {
          symbol,
          price: mid,
          bid,
          ask,
          spread_bps,
          mark_price: num(msg.mark_price),
          last_price: num(msg.close ?? msg.last_price),
          timestamp: Date.now(),
          source: "l1",
        };
        this.listeners.forEach((l) => l(update));
        return;
      }

      // v2/ticker snapshot/update — has close/mark_price/spot_price + 24h stats.
      if (msg.close != null || msg.mark_price != null || msg.spot_price != null) {
        const mark = num(msg.mark_price);
        const last = num(msg.close ?? msg.last_price);
        // Prefer mark_price for perpetuals (matches Delta's PnL/liquidation math);
        // fall back to last trade, then spot.
        const price = mark ?? last ?? num(msg.spot_price);
        if (price == null || price <= 0) return;
        const update: TickerUpdate = {
          symbol,
          price,
          mark_price: mark,
          last_price: last,
          change24h: num(msg.mark_change_24h ?? msg.change_24h),
          volume24h: num(msg.volume ?? msg.turnover),
          timestamp: Date.now(),
          source: mark != null ? "mark" : "ticker",
        };
        this.listeners.forEach((l) => l(update));
      }
    });
    this.ws.addEventListener("close", () => this.scheduleReconnect());
    this.ws.addEventListener("error", () => this.ws?.close());
  }

  private sendSubscribe(symbols: string[]) {
    this.ws?.send(
      JSON.stringify({
        type: "subscribe",
        payload: {
          channels: [
            { name: "v2/ticker", symbols },
            // L1 orderbook = best bid/ask; gives us a real mid-price instead of
            // last-trade prints. Delta throttles this at ~10Hz per symbol.
            { name: "l1_orderbook", symbols },
          ],
        },
      }),
    );
  }


  private scheduleReconnect() {
    if (this.heartbeat) {
      clearInterval(this.heartbeat);
      this.heartbeat = null;
    }
    if (this.stopped) return;
    this.attempt += 1;
    // Exponential backoff with jitter, capped at 30s.
    const base = Math.min(30_000, 500 * 2 ** (this.attempt - 1));
    const delay = base / 2 + Math.random() * base;
    this.setState("reconnecting");
    this.reconnectTimer = window.setTimeout(() => this.connect(), delay);
  }
}

// Coerce a Delta payload field to a number. Returns `undefined` when the
// value is missing/non-numeric so downstream UI can distinguish "no data"
// from a real zero (previously any missing field surfaced as `0`, which
// rendered as "0.00%" / "$0" instead of "—").
function num(v: unknown): number | undefined {
  if (v == null) return undefined;
  const n = typeof v === "string" ? parseFloat(v) : typeof v === "number" ? v : NaN;
  return Number.isFinite(n) ? n : undefined;
}

let singleton: DeltaWsClient | null = null;
export function getDeltaWs(): DeltaWsClient {
  if (typeof window === "undefined") throw new Error("DeltaWsClient is browser-only");
  if (!singleton) singleton = new DeltaWsClient();
  return singleton;
}
