import {
  WS_RECONNECT_BASE_MS,
  WS_RECONNECT_MAX_MS,
  WS_HEARTBEAT_INTERVAL_MS,
  WS_HEARTBEAT_TIMEOUT_MS,
  WS_PATH,
} from '../shared/constants';
import { getTelegramInitData } from '../shared/telegram';

// ---- Message Types ----

export interface ServerMessage {
  type: string;
  payload: unknown;
}

export type MessageHandler = (msg: ServerMessage) => void;

export type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';

export type ConnectionStateHandler = (state: ConnectionState) => void;

// ---- WebSocket Client ----

/**
 * WebSocket client with auto-reconnect, heartbeat, and typed message handling.
 *
 * Features:
 * - Automatic reconnection with exponential backoff
 * - Heartbeat ping/pong to detect dead connections
 * - Message queue for messages sent while disconnected
 * - Typed message handlers by message type
 */
export class GameWebSocket {
  private ws: WebSocket | null = null;
  private url: string;
  private handlers = new Map<string, MessageHandler[]>();
  private connectionStateHandler: ConnectionStateHandler | null = null;
  private state: ConnectionState = 'disconnected';

  // Reconnect state
  private reconnectAttempts = 0;
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  private shouldReconnect = true;

  // Heartbeat state
  private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
  private heartbeatTimeout: ReturnType<typeof setTimeout> | null = null;

  // Telegram user ID for dev-mode WS identification
  private _telegramId: number | null = null;

  // Message queue (buffered while disconnected)
  private messageQueue: string[] = [];

  constructor(url?: string) {
    // Build WS URL from current location if not provided
    if (url) {
      this.url = url;
    } else {
      const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
      this.url = `${proto}//${window.location.host}${WS_PATH}`;
    }
  }

  /** Set telegram user ID for WS identification (dev mode fallback). */
  setTelegramId(id: number): void {
    this._telegramId = id;
  }

  /** Register a handler for a specific message type */
  on(type: string, handler: MessageHandler): void {
    const existing = this.handlers.get(type) ?? [];
    existing.push(handler);
    this.handlers.set(type, existing);
  }

  /** Remove a handler */
  off(type: string, handler: MessageHandler): void {
    const existing = this.handlers.get(type);
    if (!existing) return;
    const idx = existing.indexOf(handler);
    if (idx >= 0) existing.splice(idx, 1);
  }

  /** Register a connection state change handler */
  onConnectionState(handler: ConnectionStateHandler): void {
    this.connectionStateHandler = handler;
  }

  /** Connect to the server */
  connect(): void {
    if (this.ws?.readyState === WebSocket.OPEN) return;

    this.shouldReconnect = true;
    this.setConnectionState('connecting');

    // Append Telegram auth data or telegramId for dev mode
    const initData = getTelegramInitData();
    const urlParams = new URLSearchParams();
    if (initData) {
      urlParams.set('initData', initData);
    }
    if (this._telegramId) {
      urlParams.set('telegramId', String(this._telegramId));
    }
    const qs = urlParams.toString();
    const separator = this.url.includes('?') ? '&' : '?';
    const fullUrl = qs ? `${this.url}${separator}${qs}` : this.url;

    this.ws = new WebSocket(fullUrl);

    this.ws.onopen = () => {
      this.reconnectAttempts = 0;
      this.setConnectionState('connected');
      this.startHeartbeat();
      this.flushQueue();
      console.info('[WS] Connected');
    };

    this.ws.onmessage = (event: MessageEvent) => {
      this.resetHeartbeatTimeout();

      if (event.data === 'pong') return;

      try {
        const msg = JSON.parse(event.data as string) as ServerMessage;
        // Handle JSON pong response from server
        if (msg.type === 'pong') return;
        this.dispatch(msg);
      } catch (err) {
        console.warn('[WS] Failed to parse message:', err);
      }
    };

    this.ws.onclose = (event) => {
      console.info(`[WS] Closed: code=${event.code} reason=${event.reason}`);
      this.cleanup();
      if (this.shouldReconnect) {
        this.scheduleReconnect();
      } else {
        this.setConnectionState('disconnected');
      }
    };

    this.ws.onerror = () => {
      // Error details are intentionally opaque in browsers.
      // The onclose handler will fire next and handle reconnection.
      console.warn('[WS] Connection error');
    };
  }

  /** Send a typed message to the server */
  send(type: string, payload: unknown): void {
    const msg = JSON.stringify({ type, payload });

    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(msg);
    } else {
      // Buffer for when we reconnect
      this.messageQueue.push(msg);
    }
  }

  /** Disconnect and stop reconnecting */
  disconnect(): void {
    this.shouldReconnect = false;
    this.cleanup();
    this.ws?.close(1000, 'client disconnect');
    this.ws = null;
    this.setConnectionState('disconnected');
  }

  /** Get current connection state */
  getState(): ConnectionState {
    return this.state;
  }

  // ---- Private ----

  private setConnectionState(state: ConnectionState): void {
    this.state = state;
    this.connectionStateHandler?.(state);
  }

  private dispatch(msg: ServerMessage): void {
    const handlers = this.handlers.get(msg.type);
    if (handlers) {
      for (const handler of handlers) {
        handler(msg);
      }
    }

    // Also dispatch to wildcard handlers
    const wildcardHandlers = this.handlers.get('*');
    if (wildcardHandlers) {
      for (const handler of wildcardHandlers) {
        handler(msg);
      }
    }
  }

  private startHeartbeat(): void {
    this.stopHeartbeat();
    this.heartbeatInterval = setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.ws.send('ping');
        this.heartbeatTimeout = setTimeout(() => {
          console.warn('[WS] Heartbeat timeout, closing connection');
          this.ws?.close(4000, 'heartbeat timeout');
        }, WS_HEARTBEAT_TIMEOUT_MS);
      }
    }, WS_HEARTBEAT_INTERVAL_MS);
  }

  private resetHeartbeatTimeout(): void {
    if (this.heartbeatTimeout) {
      clearTimeout(this.heartbeatTimeout);
      this.heartbeatTimeout = null;
    }
  }

  private stopHeartbeat(): void {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
      this.heartbeatInterval = null;
    }
    this.resetHeartbeatTimeout();
  }

  private scheduleReconnect(): void {
    this.setConnectionState('reconnecting');
    const delay = Math.min(
      WS_RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempts),
      WS_RECONNECT_MAX_MS,
    );
    this.reconnectAttempts++;
    console.info(`[WS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
    this.reconnectTimer = setTimeout(() => {
      this.connect();
    }, delay);
  }

  private flushQueue(): void {
    while (this.messageQueue.length > 0) {
      const msg = this.messageQueue.shift();
      if (msg && this.ws?.readyState === WebSocket.OPEN) {
        this.ws.send(msg);
      }
    }
  }

  private cleanup(): void {
    this.stopHeartbeat();
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
  }
}
