const WebSocket = require('ws');

const db = require('../database');
const { getWebSocketManager } = require('./websocket-manager');
const { getRoomInfo } = require('./jackbox-api');

class EcastShardClient {
  static parsePlayersFromHere(here) {
    if (here == null || typeof here !== 'object') {
      return { playerCount: 0, playerNames: [] };
    }
    const names = [];
    const keys = Object.keys(here).sort((a, b) => Number(a) - Number(b));
    for (const key of keys) {
      const conn = here[key];
      if (conn?.roles?.player) {
        names.push(conn.roles.player.name ?? '');
      }
    }
    return { playerCount: names.length, playerNames: names };
  }

  static parseRoomEntity(roomVal) {
    if (roomVal == null || typeof roomVal !== 'object') {
      return {
        gameState: null,
        lobbyState: null,
        gameCanStart: false,
        gameIsStarting: false,
        gameStarted: false,
        gameFinished: false,
      };
    }
    return {
      gameState: roomVal.state ?? null,
      lobbyState: roomVal.lobbyState ?? null,
      gameCanStart: !!roomVal.gameCanStart,
      gameIsStarting: !!roomVal.gameIsStarting,
      gameStarted: roomVal.state != null && roomVal.state !== 'Lobby',
      gameFinished: !!roomVal.gameFinished,
    };
  }

  static parsePlayerJoinFromTextDescriptions(val) {
    if (val == null || typeof val !== 'object') {
      return [];
    }
    const latest = val.latestDescriptions;
    if (!Array.isArray(latest)) {
      return [];
    }
    const out = [];
    for (const desc of latest) {
      if (!desc || typeof desc !== 'object') continue;
      const { category, text } = desc;
      if (category !== 'TEXT_DESCRIPTION_PLAYER_JOINED' && category !== 'TEXT_DESCRIPTION_PLAYER_JOINED_VIP') {
        continue;
      }
      if (typeof text !== 'string') continue;
      const joinedIdx = text.indexOf(' joined');
      if (joinedIdx === -1) continue;
      const before = text.slice(0, joinedIdx).trim();
      const name = before.split(/\s+/)[0] || before;
      out.push({
        name,
        isVIP: category === 'TEXT_DESCRIPTION_PLAYER_JOINED_VIP',
      });
    }
    return out;
  }

  constructor({ sessionId, gameId, roomCode, maxPlayers, onEvent }) {
    this.sessionId = sessionId;
    this.gameId = gameId;
    this.roomCode = roomCode;
    this.maxPlayers = maxPlayers;
    this.onEvent = onEvent || (() => {});

    this.ws = null;
    this.shardId = null;
    this.secret = null;
    this.host = null;
    this.playerCount = 0;
    this.playerNames = [];
    this.lobbyState = null;
    this.gameState = null;
    this.gameStarted = false;
    this.gameFinished = false;
    this.manuallyStopped = false;
    this.seq = 0;
    this.appTag = null;
    this.reconnecting = false;
    this.statusInterval = null;
  }

  getSnapshot() {
    return {
      sessionId: this.sessionId,
      gameId: this.gameId,
      roomCode: this.roomCode,
      appTag: this.appTag,
      maxPlayers: this.maxPlayers,
      playerCount: this.playerCount,
      players: [...this.playerNames],
      lobbyState: this.lobbyState,
      gameState: this.gameState,
      gameStarted: this.gameStarted,
      gameFinished: this.gameFinished,
      monitoring: true,
    };
  }

  startStatusBroadcast() {
    this.stopStatusBroadcast();
    this.statusInterval = setInterval(() => {
      this._refreshPlayerCount().finally(() => {
        if (!this.manuallyStopped && !this.gameFinished) {
          this.onEvent('game.status', this.getSnapshot());
        }
      });
    }, 20000);
  }

  _refreshPlayerCount() {
    if (!this.host || this.gameFinished || this.manuallyStopped) {
      return Promise.resolve();
    }
    return new Promise((resolve) => {
      const url = `wss://${this.host}/api/v2/rooms/${this.roomCode}/play?role=shard&name=GamePickerProbe&format=json`;
      let resolved = false;
      let welcomed = false;
      const done = (probe) => {
        if (!resolved) {
          resolved = true;
          if (probe) {
            try { probe.removeAllListeners(); probe.terminate(); } catch (_) {}
          }
          resolve();
        }
      };

      try {
        const probe = new WebSocket(url, ['ecast-v0'], {
          headers: { Origin: 'https://jackbox.tv' },
          handshakeTimeout: 8000,
        });

        const timeout = setTimeout(() => done(probe), 10000);

        probe.on('message', (data) => {
          if (welcomed || this.manuallyStopped) { clearTimeout(timeout); done(probe); return; }
          try {
            const msg = JSON.parse(data.toString());
            if (msg.opcode === 'client/welcome') {
              welcomed = true;
              const { playerCount, playerNames } = EcastShardClient.parsePlayersFromHere(msg.result.here);
              if (playerCount > this.playerCount || playerNames.length !== this.playerNames.length) {
                this.playerCount = playerCount;
                this.playerNames = playerNames;
                if (!this.manuallyStopped) {
                  this.onEvent('lobby.player-joined', {
                    sessionId: this.sessionId,
                    gameId: this.gameId,
                    roomCode: this.roomCode,
                    playerName: playerNames[playerNames.length - 1] || '',
                    playerCount,
                    players: [...playerNames],
                    maxPlayers: this.maxPlayers,
                  });
                }
              } else if (playerCount !== this.playerCount) {
                this.playerCount = playerCount;
                this.playerNames = playerNames;
              }
            } else if (msg.opcode === 'error' && msg.result?.code === 2027) {
              this.gameFinished = true;
            }
          } catch (_) {}
          clearTimeout(timeout);
          done(probe);
        });

        probe.on('error', () => { clearTimeout(timeout); done(probe); });
        probe.on('close', () => { clearTimeout(timeout); done(null); });
      } catch (_) {
        done(null);
      }
    });
  }

  stopStatusBroadcast() {
    if (this.statusInterval) {
      clearInterval(this.statusInterval);
      this.statusInterval = null;
    }
  }

  buildReconnectUrl() {
    return `wss://${this.host}/api/v2/rooms/${this.roomCode}/play?role=shard&name=GamePicker&format=json&secret=${this.secret}&id=${this.shardId}`;
  }

  handleMessage(message) {
    if (this.manuallyStopped || this.gameFinished) return;
    switch (message.opcode) {
      case 'client/welcome':
        this.handleWelcome(message.result);
        break;
      case 'object':
        this.handleEntityUpdate(message.result);
        break;
      case 'client/connected':
        this.handleClientConnected(message.result);
        break;
      case 'client/disconnected':
        break;
      case 'room/lock':
        this.handleRoomLock();
        break;
      case 'room/exit':
        this.handleRoomExit(message.result);
        break;
      case 'error':
        this.handleError(message.result);
        break;
      default:
        break;
    }
  }

  handleWelcome(result) {
    this.shardId = result.id;
    this.secret = result.secret;

    const { playerCount, playerNames } = EcastShardClient.parsePlayersFromHere(result.here);
    this.playerCount = playerCount;
    this.playerNames = playerNames;

    const roomEntity = result.entities?.room || result.entities?.['bc:room'];
    if (roomEntity) {
      const roomVal = Array.isArray(roomEntity) ? roomEntity[1]?.val : roomEntity.val;
      if (roomVal) {
        const roomState = EcastShardClient.parseRoomEntity(roomVal);
        this.lobbyState = roomState.lobbyState;
        this.gameState = roomState.gameState;
        this.gameStarted = roomState.gameStarted;
        this.gameFinished = roomState.gameFinished;
      }
    }

    console.log(
      `[Shard Monitor] Welcome: id=${this.shardId}, players=${this.playerCount} [${this.playerNames.join(', ')}], state=${this.gameState}, lobby=${this.lobbyState}`
    );

    this.onEvent('room.connected', {
      sessionId: this.sessionId,
      gameId: this.gameId,
      roomCode: this.roomCode,
      appTag: this.appTag,
      maxPlayers: this.maxPlayers,
      playerCount: this.playerCount,
      players: [...this.playerNames],
      lobbyState: this.lobbyState,
      gameState: this.gameState,
    });

    this.startStatusBroadcast();
  }

  handleEntityUpdate(result) {
    if (!result?.key) return;

    if (result.key === 'room' || result.key === 'bc:room') {
      if (result.val) {
        const prevLobbyState = this.lobbyState;
        const prevGameStarted = this.gameStarted;
        const prevGameFinished = this.gameFinished;

        const roomState = EcastShardClient.parseRoomEntity(result.val);
        this.lobbyState = roomState.lobbyState;
        this.gameState = roomState.gameState;
        this.gameStarted = roomState.gameStarted;
        this.gameFinished = roomState.gameFinished;

        if (this.lobbyState !== prevLobbyState && !this.gameStarted) {
          this.onEvent('lobby.updated', {
            sessionId: this.sessionId,
            gameId: this.gameId,
            roomCode: this.roomCode,
            lobbyState: this.lobbyState,
            gameCanStart: roomState.gameCanStart,
            gameIsStarting: roomState.gameIsStarting,
            playerCount: this.playerCount,
          });
        }

        if (this.gameStarted && !prevGameStarted) {
          this.onEvent('game.started', {
            sessionId: this.sessionId,
            gameId: this.gameId,
            roomCode: this.roomCode,
            playerCount: this.playerCount,
            players: [...this.playerNames],
            maxPlayers: this.maxPlayers,
          });
        }

        if (this.gameFinished && !prevGameFinished) {
          this.onEvent('game.ended', {
            sessionId: this.sessionId,
            gameId: this.gameId,
            roomCode: this.roomCode,
            playerCount: this.playerCount,
            players: [...this.playerNames],
          });
          this.onEvent('room.disconnected', {
            sessionId: this.sessionId,
            gameId: this.gameId,
            roomCode: this.roomCode,
            reason: 'room_closed',
            finalPlayerCount: this.playerCount,
          });
          activeShards.delete(`${this.sessionId}-${this.gameId}`);
          this.disconnect();
        }
      }
    }

    if (result.key === 'textDescriptions') {
      if (result.val) {
        const joins = EcastShardClient.parsePlayerJoinFromTextDescriptions(result.val);
        for (const join of joins) {
          if (!this.playerNames.includes(join.name)) {
            this.playerNames.push(join.name);
            this.playerCount = this.playerNames.length;

            this.onEvent('lobby.player-joined', {
              sessionId: this.sessionId,
              gameId: this.gameId,
              roomCode: this.roomCode,
              playerName: join.name,
              playerCount: this.playerCount,
              players: [...this.playerNames],
              maxPlayers: this.maxPlayers,
            });
          }
        }
      }
    }
  }

  handleClientConnected(result) {
    if (!result) return;
    if (result.roles?.player) {
      const name = result.roles.player.name ?? '';
      if (!this.playerNames.includes(name)) {
        this.playerNames.push(name);
        this.playerCount = this.playerNames.length;

        this.onEvent('lobby.player-joined', {
          sessionId: this.sessionId,
          gameId: this.gameId,
          roomCode: this.roomCode,
          playerName: name,
          playerCount: this.playerCount,
          players: [...this.playerNames],
          maxPlayers: this.maxPlayers,
        });
      }
    }
  }

  handleRoomLock() {
    if (!this.gameStarted) {
      console.log(`[Shard Monitor] Room ${this.roomCode} locked (game starting)`);
      this.gameStarted = true;
      this.gameState = this.gameState || 'Gameplay';
      this.onEvent('game.started', {
        sessionId: this.sessionId,
        gameId: this.gameId,
        roomCode: this.roomCode,
        playerCount: this.playerCount,
        players: [...this.playerNames],
        maxPlayers: this.maxPlayers,
      });
    }
  }

  handleRoomExit() {
    if (this.gameFinished) return;
    console.log(`[Shard Monitor] Room ${this.roomCode} exited`);
    this.gameFinished = true;
    this.onEvent('game.ended', {
      sessionId: this.sessionId,
      gameId: this.gameId,
      roomCode: this.roomCode,
      playerCount: this.playerCount,
      players: [...this.playerNames],
    });
    this.onEvent('room.disconnected', {
      sessionId: this.sessionId,
      gameId: this.gameId,
      roomCode: this.roomCode,
      reason: 'room_closed',
      finalPlayerCount: this.playerCount,
    });
    activeShards.delete(`${this.sessionId}-${this.gameId}`);
    this.disconnect();
  }

  handleError(result) {
    console.error(`[Shard Monitor] Ecast error ${result?.code}: ${result?.msg}`);
    if (result?.code === 2027) {
      this.gameFinished = true;
      this.onEvent('game.ended', {
        sessionId: this.sessionId,
        gameId: this.gameId,
        roomCode: this.roomCode,
        playerCount: this.playerCount,
        players: [...this.playerNames],
      });
      this.onEvent('room.disconnected', {
        sessionId: this.sessionId,
        gameId: this.gameId,
        roomCode: this.roomCode,
        reason: 'room_closed',
        finalPlayerCount: this.playerCount,
      });
      activeShards.delete(`${this.sessionId}-${this.gameId}`);
      this.disconnect();
    }
  }

  _openWebSocket(url) {
    return new Promise((resolve, reject) => {
      let welcomeTimeoutId = null;

      const cleanupWelcomeTimeout = () => {
        if (welcomeTimeoutId != null) {
          clearTimeout(welcomeTimeoutId);
          welcomeTimeoutId = null;
        }
      };

      this.ws = new WebSocket(url, ['ecast-v0'], {
        headers: { Origin: 'https://jackbox.tv' },
        handshakeTimeout: 10000,
      });

      this.ws.on('open', () => {
        console.log(`[Shard Monitor] Connected to room ${this.roomCode}`);
      });

      this.ws.on('message', (data) => {
        try {
          const message = JSON.parse(data.toString());
          this.handleMessage(message);
          if (message.opcode === 'client/welcome') {
            cleanupWelcomeTimeout();
            resolve();
          }
        } catch (e) {
          console.error('[Shard Monitor] Failed to parse message:', e.message);
        }
      });

      this.ws.on('error', (err) => {
        cleanupWelcomeTimeout();
        console.error(`[Shard Monitor] WebSocket error for room ${this.roomCode}:`, err.message);
        reject(err);
      });

      const thisWs = this.ws;
      this.ws.on('close', (code, reason) => {
        console.log(`[Shard Monitor] Disconnected from room ${this.roomCode} (code: ${code})`);
        if (this.ws === thisWs) {
          this.ws = null;
          if (!this.manuallyStopped && !this.gameFinished && this.secret != null && this.host != null) {
            void this.reconnectWithBackoff();
          }
        }
      });

      welcomeTimeoutId = setTimeout(() => {
        welcomeTimeoutId = null;
        if (!this.shardId) {
          reject(new Error('Timeout waiting for client/welcome'));
          this.disconnect();
        }
      }, 15000);
    });
  }

  async connect(roomInfo, reconnectUrl) {
    this.disconnect();
    this.shardId = null;
    this.secret = null;
    this.host = roomInfo.host;
    this.maxPlayers = roomInfo.maxPlayers || this.maxPlayers;
    this.appTag = roomInfo.appTag;

    const url =
      reconnectUrl ||
      `wss://${this.host}/api/v2/rooms/${this.roomCode}/play?role=shard&name=GamePicker&userId=gamepicker-${this.sessionId}&format=json`;

    return this._openWebSocket(url);
  }

  async reconnect() {
    const url = this.buildReconnectUrl();
    this.disconnect();
    this.shardId = null;
    return this._openWebSocket(url);
  }

  async reconnectWithBackoff() {
    if (this.reconnecting || this.manuallyStopped || this.gameFinished) {
      return false;
    }
    this.reconnecting = true;
    const delays = [2000, 4000, 8000];

    try {
      for (let i = 0; i < delays.length; i++) {
        await new Promise((r) => setTimeout(r, delays[i]));

        const roomInfo = await getRoomInfo(this.roomCode);

        if (!roomInfo.exists) {
          this.gameFinished = true;
          this.onEvent('game.ended', {
            sessionId: this.sessionId,
            gameId: this.gameId,
            roomCode: this.roomCode,
            playerCount: this.playerCount,
            players: [...this.playerNames],
          });
          this.onEvent('room.disconnected', {
            sessionId: this.sessionId,
            gameId: this.gameId,
            roomCode: this.roomCode,
            reason: 'room_closed',
            finalPlayerCount: this.playerCount,
          });
          activeShards.delete(`${this.sessionId}-${this.gameId}`);
          return false;
        }

        try {
          await this.reconnect();
          console.log(`[Shard Monitor] Reconnected to room ${this.roomCode} (attempt ${i + 1})`);
          return true;
        } catch (e) {
          console.error(`[Shard Monitor] Reconnect attempt ${i + 1} failed:`, e.message);
        }
      }

      this.onEvent('room.disconnected', {
        sessionId: this.sessionId,
        gameId: this.gameId,
        roomCode: this.roomCode,
        reason: 'connection_failed',
        finalPlayerCount: this.playerCount,
      });
      activeShards.delete(`${this.sessionId}-${this.gameId}`);
      return false;
    } finally {
      this.reconnecting = false;
    }
  }

  disconnect() {
    this.stopStatusBroadcast();
    if (this.ws) {
      try {
        this.ws.close(1000, 'Monitor stopped');
      } catch (e) {
        // Ignore close errors
      }
      this.ws = null;
    }
  }

  sendMessage(opcode, params = {}) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.seq++;
      this.ws.send(JSON.stringify({ seq: this.seq, opcode, params }));
    }
  }
}

const activeShards = new Map();

function broadcastAndPersist(sessionId, gameId) {
  return (eventType, eventData) => {
    const wsManager = getWebSocketManager();
    if (wsManager) {
      wsManager.broadcastEvent(eventType, eventData, parseInt(sessionId, 10));
    }

    if (['room.connected', 'lobby.player-joined', 'game.started', 'game.ended'].includes(eventType)) {
      const checkStatus = eventType === 'game.ended' ? 'completed' : 'monitoring';
      try {
        if (eventType === 'game.ended') {
          db.prepare(
            'UPDATE session_games SET player_count = ?, player_count_check_status = ?, status = ? WHERE session_id = ? AND id = ?'
          ).run(eventData.playerCount ?? null, checkStatus, 'played', sessionId, gameId);
        } else {
          db.prepare(
            'UPDATE session_games SET player_count = ?, player_count_check_status = ? WHERE session_id = ? AND id = ?'
          ).run(eventData.playerCount ?? null, checkStatus, sessionId, gameId);
        }
      } catch (e) {
        console.error('[Shard Monitor] DB update failed:', e.message);
      }
    }

    if (eventType === 'room.disconnected') {
      const reason = eventData.reason;
      const checkStatus =
        reason === 'room_closed' ? 'completed' : reason === 'manually_stopped' ? 'stopped' : 'failed';
      try {
        const game = db
          .prepare('SELECT player_count_check_status, status FROM session_games WHERE session_id = ? AND id = ?')
          .get(sessionId, gameId);
        if (game && game.player_count_check_status !== 'completed') {
          db.prepare('UPDATE session_games SET player_count_check_status = ? WHERE session_id = ? AND id = ?').run(
            checkStatus,
            sessionId,
            gameId
          );
        }
        if (game && reason === 'room_closed' && game.status === 'playing') {
          db.prepare('UPDATE session_games SET status = ? WHERE session_id = ? AND id = ?').run(
            'played',
            sessionId,
            gameId
          );
        }
      } catch (e) {
        console.error('[Shard Monitor] DB update failed:', e.message);
      }
    }
  };
}

async function startMonitor(sessionId, gameId, roomCode, maxPlayers = 8) {
  const monitorKey = `${sessionId}-${gameId}`;

  if (activeShards.has(monitorKey)) {
    console.log(`[Shard Monitor] Already monitoring ${monitorKey}`);
    return;
  }

  console.log(`[Shard Monitor] Starting monitor for room ${roomCode} (${monitorKey})`);

  const roomInfo = await getRoomInfo(roomCode);
  if (!roomInfo.exists) {
    console.log(`[Shard Monitor] Room ${roomCode} not found`);
    const onEvent = broadcastAndPersist(sessionId, gameId);
    onEvent('room.disconnected', {
      sessionId,
      gameId,
      roomCode,
      reason: 'room_not_found',
      finalPlayerCount: null,
    });
    return;
  }

  const onEvent = broadcastAndPersist(sessionId, gameId);

  try {
    db.prepare('UPDATE session_games SET player_count_check_status = ? WHERE session_id = ? AND id = ?').run(
      'monitoring',
      sessionId,
      gameId
    );
  } catch (e) {
    console.error('[Shard Monitor] DB update failed:', e.message);
  }

  const client = new EcastShardClient({
    sessionId,
    gameId,
    roomCode,
    maxPlayers: roomInfo.maxPlayers || maxPlayers,
    onEvent,
  });

  activeShards.set(monitorKey, client);

  try {
    await client.connect(roomInfo);
  } catch (e) {
    console.error(`[Shard Monitor] Failed to connect to room ${roomCode}:`, e.message);
    activeShards.delete(monitorKey);
    onEvent('room.disconnected', {
      sessionId,
      gameId,
      roomCode,
      reason: 'connection_failed',
      finalPlayerCount: null,
    });
  }
}

async function stopMonitor(sessionId, gameId) {
  const monitorKey = `${sessionId}-${gameId}`;
  const client = activeShards.get(monitorKey);

  if (client) {
    client.manuallyStopped = true;
    client.disconnect();
    activeShards.delete(monitorKey);

    const game = db
      .prepare('SELECT player_count_check_status FROM session_games WHERE session_id = ? AND id = ?')
      .get(sessionId, gameId);

    if (game && game.player_count_check_status !== 'completed' && game.player_count_check_status !== 'failed') {
      db.prepare('UPDATE session_games SET player_count_check_status = ? WHERE session_id = ? AND id = ?').run(
        'stopped',
        sessionId,
        gameId
      );
    }

    client.onEvent('room.disconnected', {
      sessionId,
      gameId,
      roomCode: client.roomCode,
      reason: 'manually_stopped',
      finalPlayerCount: client.playerCount,
    });

    console.log(`[Shard Monitor] Stopped monitor for ${monitorKey}`);
  }
}

async function cleanupAllShards() {
  for (const [, client] of activeShards) {
    client.manuallyStopped = true;
    client.disconnect();
  }
  activeShards.clear();
  console.log('[Shard Monitor] Cleaned up all active shards');
}

function getMonitorSnapshot(sessionId, gameId) {
  const client = activeShards.get(`${sessionId}-${gameId}`);
  return client ? client.getSnapshot() : null;
}

module.exports = { EcastShardClient, startMonitor, stopMonitor, cleanupAllShards, getMonitorSnapshot };
