/**
 * NetworkManager.js
 * Manages WebSocket connection for PVP online multiplayer.
 * Handles connection lifecycle, heartbeat, reconnection, and message routing.
 */

const { NET_MSG } = require('../base/GameGlobal');

class NetworkManager {
  constructor() {
    /** @type {WebSocket|null} */
    this._ws = null;
    /** @type {string} Server URL */
    this._serverUrl = '';
    /** @type {boolean} */
    this._connected = false;
    /** @type {boolean} */
    this._connecting = false;
    /** @type {string|null} Current room ID */
    this._roomId = null;
    /** @type {number} Player slot (1 or 2) */
    this._playerSlot = 0;
    /** @type {string} Unique player ID */
    this._playerId = '';

    // Heartbeat
    this._heartbeatInterval = null;
    this._heartbeatTimeout = null;
    this._heartbeatMs = 5000;
    this._heartbeatTimeoutMs = 10000;

    // Reconnection
    this._reconnectAttempts = 0;
    this._maxReconnectAttempts = 3;
    this._reconnectDelay = 2000;
    this._reconnectTimer = null;
    this._shouldReconnect = false;

    // Message handlers
    /** @type {Map<string, Array<Function>>} */
    this._handlers = new Map();

    // Latency tracking
    this._lastPingTime = 0;
    this._latency = 0;

    // Generate a unique player ID
    this._playerId = this._generatePlayerId();

    // Connection mutex: queue of pending connect() callers
    /** @type {Array<{resolve: Function, timeoutMs: number}>} */
    this._connectQueue = [];
  }

  /**
   * Connect to the WebSocket server.
   * @param {string} serverUrl - WebSocket server URL (e.g., 'wss://example.com/ws').
   * @param {number} [timeoutMs=10000] - Connect timeout in milliseconds.
   * @returns {Promise<boolean>} Whether connection succeeded.
   */
  connect(serverUrl, timeoutMs = 10000) {
    return new Promise((resolve) => {
      // If already connected, resolve immediately
      if (this._connected) {
        resolve(true);
        return;
      }

      // If another connect() is in progress, queue this one and wait
      if (this._connecting) {
        this._connectQueue.push({ resolve, timeoutMs });
        return;
      }

      this._serverUrl = serverUrl;
      this._connecting = true;
      this._shouldReconnect = true;

      // Guard: make sure resolve is called exactly once.
      let settled = false;
      const finish = (ok, reason) => {
        if (settled) return;
        settled = true;
        if (connectTimer) {
          clearTimeout(connectTimer);
          connectTimer = null;
        }
        if (!ok) {
          // Tear down broken socket so next connect() starts clean.
          this._connecting = false;
          this._shouldReconnect = false; // a first-time failure should NOT auto-reconnect
          if (this._ws) {
            try { this._ws.close({}); } catch (e) { /* ignore */ }
            this._ws = null;
          }
          console.warn('[NetworkManager] connect() failed:', reason || 'unknown');
        }
        resolve(ok);

        // Resolve all queued connect() calls with the same result
        const queue = this._connectQueue;
        this._connectQueue = [];
        for (const pending of queue) {
          pending.resolve(ok);
        }
      };

      // Connection timeout guard (e.g. DNS/TLS hang on cellular).
      let connectTimer = setTimeout(() => {
        finish(false, `connect timeout after ${timeoutMs}ms, url=${serverUrl}`);
      }, timeoutMs);

      try {
        this._ws = wx.connectSocket({
          url: serverUrl,
          header: { 'content-type': 'application/json' },
          success: (res) => { console.log('[NetworkManager] wx.connectSocket invoked:', res && res.errMsg); },
          fail: (err) => {
            console.error('[NetworkManager] wx.connectSocket API failed:', JSON.stringify(err));
            finish(false, `wx.connectSocket fail: ${err && err.errMsg}`);
          },
        });

        this._ws.onOpen(() => {
          console.log('[NetworkManager] Connected to server:', serverUrl);
          this._connected = true;
          this._connecting = false;
          this._reconnectAttempts = 0;
          this._startHeartbeat();
          this._emit('connected');
          finish(true);
        });

        this._ws.onMessage((res) => {
          this._handleMessage(res.data);
        });

        this._ws.onError((err) => {
          console.error('[NetworkManager] WebSocket error:',
            (err && (err.errMsg || err.message)) || err,
            'url=', serverUrl);
          this._emit('error', err);
          if (!this._connected) {
            finish(false, `onError before open: ${err && (err.errMsg || err.message)}`);
          } else {
            this._connecting = false;
          }
        });

        this._ws.onClose((res) => {
          const code = res && res.code;
          const reason = res && res.reason;
          console.log('[NetworkManager] Connection closed:', code, reason, 'url=', serverUrl);

          const wasConnected = this._connected;
          this._connected = false;
          this._connecting = false;
          this._stopHeartbeat();
          this._emit('disconnected', { code, reason });

          if (!wasConnected) {
            finish(false, `onClose before open: code=${code} reason=${reason}`);
            return;
          }

          if (this._shouldReconnect && this._reconnectAttempts < this._maxReconnectAttempts) {
            this._attemptReconnect();
          }
        });
      } catch (e) {
        console.error('[NetworkManager] Failed to create WebSocket:', e);
        finish(false, `exception: ${e && e.message}`);
      }
    });
  }

  /**
   * Disconnect from the server.
   */
  disconnect() {
    this._shouldReconnect = false;
    this._stopHeartbeat();
    this._clearReconnectTimer();

    if (this._ws) {
      try {
        this._ws.close({});
      } catch (e) {
        // Ignore close errors
      }
      this._ws = null;
    }

    this._connected = false;
    this._connecting = false;
    this._roomId = null;
    this._playerSlot = 0;

    // Clear connect queue
    const queue = this._connectQueue;
    this._connectQueue = [];
    for (const pending of queue) {
      pending.resolve(false);
    }
  }

  /**
   * Send a message to the server.
   * @param {string} type - Message type from NET_MSG.
   * @param {object} [data={}] - Message payload.
   */
  send(type, data = {}) {
    if (!this._connected || !this._ws) {
      console.warn('[NetworkManager] Cannot send, not connected');
      return;
    }

    // Always include the player's current nickname and avatarUrl so the server
    // can propagate them to other clients. Falls back silently when the
    // profile is not yet available.
    const profile = (typeof GameGlobal !== 'undefined') ? GameGlobal.playerProfile : null;
    const nickname = (profile && profile.nickname) ? profile.nickname : '';
    const avatarUrl = (profile && profile.avatarUrl) ? profile.avatarUrl : '';
    const skinId = (GameGlobal && GameGlobal.skinManager) ? (GameGlobal.skinManager.getEquippedSkinId() || '') : '';

    const message = JSON.stringify({
      type,
      data,
      playerId: this._playerId,
      nickname,
      avatarUrl,
      skinId,
      roomId: this._roomId,
      timestamp: Date.now(),
    });

    try {
      this._ws.send({ data: message });
    } catch (e) {
      console.error('[NetworkManager] Send error:', e);
    }
  }

  /**
   * Create a new room on the server.
   */
  createRoom() {
    this.send(NET_MSG.CREATE_ROOM, {
      playerId: this._playerId,
    });
  }

  /**
   * Join an existing room.
   * @param {string} roomId - Room ID to join.
   */
  joinRoom(roomId) {
    this.send(NET_MSG.JOIN_ROOM, {
      playerId: this._playerId,
      roomId: roomId,
    });
  }

  /**
   * Send player input to the server.
   * @param {object} input - { direction, firing, x, y }
   */
  sendInput(input) {
    this.send(NET_MSG.PLAYER_INPUT, input);
  }

  /**
   * Send player state for synchronization.
   * @param {object} state - { x, y, direction, hp, alive }
   */
  sendState(state) {
    this.send(NET_MSG.PLAYER_STATE, state);
  }

  /**
   * Send bullet fire event.
   * @param {object} bulletData - { x, y, direction }
   */
  sendBulletFire(bulletData) {
    this.send(NET_MSG.BULLET_FIRE, bulletData);
  }

  // ============================================================
  // 3v3 Team Methods
  // ============================================================

  /**
   * Create a new team for 3v3 mode.
   * @param {string} [battleMode='3v3'] - Battle mode ('1v1', '2v2', '3v3').
   */
  createTeam(battleMode = '3v3') {
    this.send(NET_MSG.CREATE_TEAM, {
      playerId: this._playerId,
      battleMode,
    });
  }

  /**
   * Join an existing team by teamId.
   * @param {string} teamId - Team ID to join.
   */
  joinTeam(teamId) {
    this.send(NET_MSG.JOIN_TEAM, {
      playerId: this._playerId,
      teamId,
    });
  }

  /**
   * Leave the current team.
   */
  leaveTeam() {
    this.send(NET_MSG.LEAVE_TEAM, {
      playerId: this._playerId,
    });
  }

  /**
   * Toggle ready state in team room.
   * @param {boolean} ready - Whether the player is ready.
   */
  teamReady(ready) {
    this.send(NET_MSG.TEAM_READY, {
      playerId: this._playerId,
      ready,
    });
  }

  /**
   * Start matchmaking (leader only).
   */
  startMatch() {
    this.send(NET_MSG.MATCH_START, {
      playerId: this._playerId,
    });
  }

  /**
   * Cancel matchmaking (leader only).
   */
  cancelMatch() {
    this.send(NET_MSG.MATCH_CANCEL, {
      playerId: this._playerId,
    });
  }

  /**
   * Kick a player from the team (leader only).
   * @param {string} targetPlayerId - Player ID to kick.
   */
  kickPlayer(targetPlayerId) {
    this.send(NET_MSG.TEAM_KICK, {
      playerId: this._playerId,
      targetPlayerId,
    });
  }

  /**
   * Disband the team (leader only).
   */
  disbandTeam() {
    this.send(NET_MSG.TEAM_DISBAND, {
      playerId: this._playerId,
    });
  }

  /**
   * Start solo matchmaking for 3v3.
   * @param {string} [battleMode='3v3'] - Battle mode ('1v1', '2v2', '3v3').
   */
  soloMatch(battleMode = '3v3') {
    this.send(NET_MSG.SOLO_MATCH, {
      playerId: this._playerId,
      battleMode,
    });
  }

  /**
   * Attempt to reconnect to an ongoing team game.
   * @param {string} teamId - Team room ID.
   */
  reconnectToTeam(teamId) {
    this.send(NET_MSG.RECONNECT, {
      teamId,
      playerId: this._playerId,
    });
  }

  /**
   * Register a handler for a message type.
   * @param {string} type - Message type.
   * @param {Function} handler - Callback function(data).
   * @returns {Function} Unsubscribe function.
   */
  on(type, handler) {
    if (!this._handlers.has(type)) {
      this._handlers.set(type, []);
    }
    this._handlers.get(type).push(handler);

    return () => {
      const list = this._handlers.get(type);
      if (list) {
        const idx = list.indexOf(handler);
        if (idx !== -1) list.splice(idx, 1);
      }
    };
  }

  /**
   * Remove a handler for a message type.
   * @param {string} type
   * @param {Function} handler
   */
  off(type, handler) {
    const list = this._handlers.get(type);
    if (list) {
      const idx = list.indexOf(handler);
      if (idx !== -1) list.splice(idx, 1);
    }
  }

  /**
   * Remove all handlers.
   */
  clearHandlers() {
    this._handlers.clear();
  }

  // ============================================================
  // Private Methods
  // ============================================================

  /**
   * Handle incoming WebSocket message.
   * @private
   */
  _handleMessage(rawData) {
    try {
      const msg = JSON.parse(rawData);
      const { type, data } = msg;

      // Handle system messages
      if (type === NET_MSG.PONG) {
        this._latency = Date.now() - this._lastPingTime;
        this._resetHeartbeatTimeout();
        return;
      }

      if (type === NET_MSG.ROOM_CREATED) {
        this._roomId = data.roomId;
        this._playerSlot = 1; // Creator is player 1
      } else if (type === NET_MSG.ROOM_JOINED) {
        this._roomId = data.roomId;
        this._playerSlot = data.playerSlot || 2;
      }

      // Emit to registered handlers
      this._emit(type, data);
    } catch (e) {
      console.error('[NetworkManager] Failed to parse message:', e, rawData);
    }
  }

  /**
   * Emit an event to registered handlers.
   * @private
   */
  _emit(type, data) {
    const list = this._handlers.get(type);
    if (list) {
      for (const handler of list) {
        try {
          handler(data);
        } catch (e) {
          console.error(`[NetworkManager] Handler error for "${type}":`, e);
        }
      }
    }
  }

  /**
   * Start heartbeat ping/pong.
   * @private
   */
  _startHeartbeat() {
    this._stopHeartbeat();
    this._heartbeatInterval = setInterval(() => {
      if (this._connected) {
        this._lastPingTime = Date.now();
        this.send(NET_MSG.PING);
        this._startHeartbeatTimeout();
      }
    }, this._heartbeatMs);
  }

  /**
   * Stop heartbeat.
   * @private
   */
  _stopHeartbeat() {
    if (this._heartbeatInterval) {
      clearInterval(this._heartbeatInterval);
      this._heartbeatInterval = null;
    }
    this._resetHeartbeatTimeout();
  }

  /**
   * Start heartbeat timeout (disconnect if no pong received).
   * @private
   */
  _startHeartbeatTimeout() {
    this._resetHeartbeatTimeout();
    this._heartbeatTimeout = setTimeout(() => {
      console.warn('[NetworkManager] Heartbeat timeout, disconnecting');
      this.disconnect();
    }, this._heartbeatTimeoutMs);
  }

  /**
   * Reset heartbeat timeout.
   * @private
   */
  _resetHeartbeatTimeout() {
    if (this._heartbeatTimeout) {
      clearTimeout(this._heartbeatTimeout);
      this._heartbeatTimeout = null;
    }
  }

  /**
   * Attempt to reconnect to the server.
   * @private
   */
  _attemptReconnect() {
    this._clearReconnectTimer();
    this._reconnectAttempts++;
    console.log(`[NetworkManager] Reconnect attempt ${this._reconnectAttempts}/${this._maxReconnectAttempts}`);

    this._emit('reconnecting', { attempt: this._reconnectAttempts });

    this._reconnectTimer = setTimeout(() => {
      this.connect(this._serverUrl);
    }, this._reconnectDelay * this._reconnectAttempts);
  }

  /**
   * Clear reconnect timer.
   * @private
   */
  _clearReconnectTimer() {
    if (this._reconnectTimer) {
      clearTimeout(this._reconnectTimer);
      this._reconnectTimer = null;
    }
  }

  /**
   * Generate a unique player ID.
   * @private
   * @returns {string}
   */
  _generatePlayerId() {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let id = 'p_';
    for (let i = 0; i < 8; i++) {
      id += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return id + '_' + Date.now().toString(36);
  }

  // ============================================================
  // Getters
  // ============================================================

  /** Whether currently connected. */
  get connected() {
    return this._connected;
  }

  /** Current room ID. */
  get roomId() {
    return this._roomId;
  }

  /** Player slot (1 or 2). */
  get playerSlot() {
    return this._playerSlot;
  }

  /** Player unique ID. */
  get playerId() {
    return this._playerId;
  }

  /** Player display nickname (may be empty until profile is fetched). */
  get nickname() {
    const profile = (typeof GameGlobal !== 'undefined') ? GameGlobal.playerProfile : null;
    return (profile && profile.nickname) ? profile.nickname : '';
  }

  /** Current latency in ms. */
  get latency() {
    return this._latency;
  }

  /** Whether currently connecting. */
  get connecting() {
    return this._connecting;
  }
}

module.exports = NetworkManager;
