#!/usr/bin/env node

const https = require('https');
const fs = require('fs');
const path = require('path');

let WebSocket;
try {
  WebSocket = require('ws');
} catch (_) {
  try {
    WebSocket = require('../backend/node_modules/ws');
  } catch (_2) {
    console.error('Error: WebSocket library (ws) not found.');
    console.error('Run: cd backend && npm install');
    process.exit(1);
  }
}

// ---------------------------------------------------------------------------
// ANSI helpers
// ---------------------------------------------------------------------------
const C = {
  reset: '\x1b[0m',
  bold: '\x1b[1m',
  dim: '\x1b[2m',
  red: '\x1b[31m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  magenta: '\x1b[35m',
  cyan: '\x1b[36m',
  white: '\x1b[37m',
};

// ---------------------------------------------------------------------------
// CLI argument parsing
// ---------------------------------------------------------------------------
function parseArgs() {
  const args = process.argv.slice(2);

  if (args.includes('--help') || args.includes('-h') || args.length === 0) {
    console.log(`
${C.bold}Jackbox API Logger${C.reset}
Connects to a Jackbox game room and logs all WebSocket events.

${C.bold}Usage:${C.reset}
  node tools/jackbox-logger.js <ROOM_CODE> [options]

${C.bold}Options:${C.reset}
  --role <shard|audience|player>   Connection role (default: shard)
  --name <name>                    Display name (default: JBLogger)
  --no-file                        Skip writing to log file
  --verbose                        Print full JSON to console
  --help, -h                       Show this help

${C.bold}Examples:${C.reset}
  node tools/jackbox-logger.js ABCD
  node tools/jackbox-logger.js ABCD --role audience
  node tools/jackbox-logger.js ABCD --verbose --no-file
`);
    process.exit(0);
  }

  const opts = {
    roomCode: null,
    role: 'shard',
    name: 'JBLogger',
    noFile: false,
    verbose: false,
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '--role') {
      opts.role = args[++i];
    } else if (arg === '--name') {
      opts.name = args[++i];
    } else if (arg === '--no-file') {
      opts.noFile = true;
    } else if (arg === '--verbose') {
      opts.verbose = true;
    } else if (!arg.startsWith('-') && !opts.roomCode) {
      opts.roomCode = arg.toUpperCase();
    }
  }

  if (!opts.roomCode) {
    console.error(`${C.red}Error: room code is required${C.reset}`);
    process.exit(1);
  }

  if (!['shard', 'audience', 'player'].includes(opts.role)) {
    console.error(`${C.red}Error: --role must be shard, audience, or player${C.reset}`);
    process.exit(1);
  }

  return opts;
}

// ---------------------------------------------------------------------------
// REST: fetch room info
// ---------------------------------------------------------------------------
function getRoomInfo(roomCode) {
  return new Promise((resolve, reject) => {
    https.get(`https://ecast.jackboxgames.com/api/v2/rooms/${roomCode}`, (res) => {
      let data = '';
      res.on('data', (chunk) => (data += chunk));
      res.on('end', () => {
        try {
          const json = JSON.parse(data);
          if (json.ok) resolve(json.body);
          else reject(new Error(json.error || 'Room not found'));
        } catch (e) {
          reject(e);
        }
      });
    }).on('error', reject);
  });
}

// ---------------------------------------------------------------------------
// Timestamp
// ---------------------------------------------------------------------------
function ts() {
  return new Date().toISOString().slice(11, 23);
}

// ---------------------------------------------------------------------------
// Console summarizer — merges patterns from ws-probe.js and ws-lifecycle-test.js
// ---------------------------------------------------------------------------
function summarize(msg) {
  const r = msg.result || {};

  switch (msg.opcode) {
    case 'client/welcome': {
      const hereEntries = r.here ? Object.entries(r.here) : [];
      const players = hereEntries
        .filter(([, v]) => v.roles?.player)
        .map(([id, v]) => `${v.roles.player.name}(${id})`);
      const entityKeys = r.entities ? Object.keys(r.entities) : [];
      return (
        `id=${r.id} reconnect=${r.reconnect} secret=${r.secret}\n` +
        `           here: ${hereEntries.length} connections [${players.join(', ') || 'no players'}]\n` +
        `           entities: [${entityKeys.join(', ')}]`
      );
    }

    case 'object': {
      if (r.key === 'room' || r.key === 'bc:room') {
        const v = r.val || {};
        return `ROOM state=${v.state} lobby=${v.lobbyState} canStart=${v.gameCanStart} starting=${v.gameIsStarting} finished=${v.gameFinished} v${r.version}`;
      }
      if (r.key === 'textDescriptions') {
        const latest = r.val?.latestDescriptions;
        if (Array.isArray(latest) && latest.length > 0) {
          const last = latest[latest.length - 1];
          return `TEXT "${last.text}" (${last.category}) v${r.version}`;
        }
        return `textDescriptions v${r.version}`;
      }
      if (r.key?.startsWith('player:')) {
        const v = r.val || {};
        return `PLAYER ${r.key} state=${v.state || 'init'} name=${v.playerName || '?'} vip=${v.playerIsVIP} v${r.version}`;
      }
      const valKeys = r.val ? Object.keys(r.val).slice(0, 5).join(',') : 'null';
      return `ENTITY ${r.key} v${r.version} from=${r.from} val=[${valKeys}...]`;
    }

    case 'client/connected': {
      const roleName = r.roles ? Object.keys(r.roles)[0] : r.role;
      const playerName = r.roles?.player?.name || r.name || '';
      return `id=${r.id} userId=${r.userId} role=${roleName} name=${playerName}`;
    }

    case 'client/disconnected': {
      const roleName = r.roles ? Object.keys(r.roles)[0] : r.role;
      return `id=${r.id} role=${roleName}`;
    }

    case 'client/kicked':
      return JSON.stringify(r).slice(0, 200);

    case 'room/lock':
      return 'room locked (game starting)';

    case 'room/exit':
      return 'room closed';

    case 'room/get-audience':
      return `audience connections=${r.connections}`;

    case 'error':
      return `code=${r.code}: ${r.msg}`;

    case 'ok':
      return `seq response`;

    default:
      return JSON.stringify(r).slice(0, 200);
  }
}

function opcodeColor(opcode) {
  if (opcode === 'client/welcome') return C.green;
  if (opcode === 'error') return C.red;
  if (opcode === 'client/connected') return C.cyan;
  if (opcode === 'client/disconnected') return C.yellow;
  if (opcode === 'room/lock' || opcode === 'room/exit') return C.magenta;
  if (opcode === 'object') return C.white;
  return C.dim;
}

// ---------------------------------------------------------------------------
// File logger
// ---------------------------------------------------------------------------
class FileLogger {
  constructor(roomCode) {
    const logsDir = path.join(__dirname, '..', 'logs');
    fs.mkdirSync(logsDir, { recursive: true });

    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    this.filePath = path.join(logsDir, `jackbox-${roomCode}-${timestamp}.jsonl`);
    this.stream = fs.createWriteStream(this.filePath, { flags: 'a' });
  }

  write(entry) {
    this.stream.write(JSON.stringify(entry) + '\n');
  }

  close() {
    this.stream.end();
  }
}

// ---------------------------------------------------------------------------
// Main logger
// ---------------------------------------------------------------------------
async function main() {
  const opts = parseArgs();
  const { roomCode, role, name, noFile, verbose } = opts;

  console.log(`${C.bold}Jackbox API Logger${C.reset}`);
  console.log(`${C.cyan}Room:${C.reset} ${roomCode}  ${C.cyan}Role:${C.reset} ${role}  ${C.cyan}Name:${C.reset} ${name}`);
  console.log();

  // Fetch room info
  console.log(`${C.dim}[${ts()}]${C.reset} Fetching room info...`);
  let roomInfo;
  try {
    roomInfo = await getRoomInfo(roomCode);
  } catch (e) {
    console.error(`${C.red}Failed to fetch room info: ${e.message}${C.reset}`);
    process.exit(1);
  }

  console.log(`${C.dim}[${ts()}]${C.reset} ${C.green}Room found${C.reset}`);
  console.log(`  ${C.cyan}Game:${C.reset}       ${roomInfo.appTag}`);
  console.log(`  ${C.cyan}Host:${C.reset}       ${roomInfo.host}`);
  console.log(`  ${C.cyan}Players:${C.reset}    max ${roomInfo.maxPlayers}`);
  console.log(`  ${C.cyan}Locked:${C.reset}     ${roomInfo.locked}`);
  console.log(`  ${C.cyan}Full:${C.reset}       ${roomInfo.full}`);
  console.log(`  ${C.cyan}Audience:${C.reset}   ${roomInfo.audienceEnabled ? 'enabled' : 'disabled'}`);
  console.log();

  // File logger
  let fileLogger = null;
  if (!noFile) {
    fileLogger = new FileLogger(roomCode);
    console.log(`${C.dim}[${ts()}]${C.reset} Logging to ${C.bold}${fileLogger.filePath}${C.reset}`);
    fileLogger.write({
      ts: new Date().toISOString(),
      direction: 'meta',
      type: 'session_start',
      roomCode,
      role,
      name,
      host: roomInfo.host,
      appTag: roomInfo.appTag,
      maxPlayers: roomInfo.maxPlayers,
      locked: roomInfo.locked,
      full: roomInfo.full,
      audienceEnabled: roomInfo.audienceEnabled,
    });
  }

  // State for reconnection
  let shardId = null;
  let secret = null;
  let msgCount = 0;
  let reconnecting = false;
  let manuallyStopped = false;
  const startTime = Date.now();

  function buildWsUrl(reconnect) {
    if (role === 'audience') {
      return `wss://${roomInfo.audienceHost || roomInfo.host}/api/v2/audience/${roomCode}/play`;
    }
    const base = `wss://${roomInfo.host}/api/v2/rooms/${roomCode}/play`;
    if (reconnect && secret && shardId) {
      return `${base}?role=${role}&name=${encodeURIComponent(name)}&format=json&secret=${secret}&id=${shardId}`;
    }
    return `${base}?role=${role}&name=${encodeURIComponent(name)}&userId=${name}-${Date.now()}&format=json`;
  }

  function connect(isReconnect) {
    const url = buildWsUrl(isReconnect);
    console.log(`${C.dim}[${ts()}]${C.reset} ${isReconnect ? 'Reconnecting' : 'Connecting'}: ${C.dim}${url}${C.reset}`);

    const ws = new WebSocket(url, ['ecast-v0'], {
      headers: {
        Origin: 'https://jackbox.tv',
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
      },
      handshakeTimeout: 10000,
    });

    ws.on('open', () => {
      console.log(`${C.dim}[${ts()}]${C.reset} ${C.green}${C.bold}CONNECTED${C.reset}`);
      console.log();
    });

    ws.on('message', (raw) => {
      msgCount++;
      try {
        const msg = JSON.parse(raw.toString());

        // Capture credentials from welcome for reconnection
        if (msg.opcode === 'client/welcome' && msg.result) {
          shardId = msg.result.id;
          secret = msg.result.secret;
        }

        // Console output
        const color = opcodeColor(msg.opcode);
        const summary = summarize(msg);
        console.log(
          `${C.dim}[${ts()}]${C.reset} ${C.dim}#${msgCount} pc:${msg.pc ?? '-'}${C.reset} ${color}${C.bold}${msg.opcode}${C.reset} ${summary}`
        );
        if (verbose) {
          console.log(JSON.stringify(msg, null, 2));
        }

        // File output
        if (fileLogger) {
          fileLogger.write({
            ts: new Date().toISOString(),
            direction: 'recv',
            msgNum: msgCount,
            pc: msg.pc,
            opcode: msg.opcode,
            raw: msg,
          });
        }
      } catch (e) {
        console.log(`${C.dim}[${ts()}]${C.reset} ${C.yellow}UNPARSEABLE${C.reset} ${raw.toString().slice(0, 200)}`);
        if (fileLogger) {
          fileLogger.write({
            ts: new Date().toISOString(),
            direction: 'recv',
            msgNum: msgCount,
            parseError: true,
            raw: raw.toString().slice(0, 2000),
          });
        }
      }
    });

    ws.on('close', (code, reason) => {
      console.log(`${C.dim}[${ts()}]${C.reset} ${C.yellow}DISCONNECTED${C.reset} code=${code} reason=${reason}`);
      if (fileLogger) {
        fileLogger.write({
          ts: new Date().toISOString(),
          direction: 'meta',
          type: 'disconnected',
          code,
          reason: reason?.toString(),
        });
      }
      if (!manuallyStopped && secret != null) {
        reconnectWithBackoff(ws);
      }
    });

    ws.on('error', (err) => {
      console.error(`${C.dim}[${ts()}]${C.reset} ${C.red}WS ERROR: ${err.message}${C.reset}`);
      if (fileLogger) {
        fileLogger.write({
          ts: new Date().toISOString(),
          direction: 'meta',
          type: 'error',
          message: err.message,
        });
      }
    });

    // SIGINT handler
    const onSigint = () => {
      manuallyStopped = true;
      const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
      console.log();
      console.log(`${C.bold}--- Session Summary ---${C.reset}`);
      console.log(`  ${C.cyan}Messages:${C.reset}  ${msgCount}`);
      console.log(`  ${C.cyan}Duration:${C.reset}  ${elapsed}s`);
      if (fileLogger) {
        console.log(`  ${C.cyan}Log file:${C.reset}  ${fileLogger.filePath}`);
        fileLogger.write({
          ts: new Date().toISOString(),
          direction: 'meta',
          type: 'session_end',
          msgCount,
          durationMs: Date.now() - startTime,
        });
        fileLogger.close();
      }
      console.log();
      try {
        ws.close(1000, 'Logger stopped');
      } catch (_) {}
      setTimeout(() => process.exit(0), 500);
    };

    process.removeAllListeners('SIGINT');
    process.on('SIGINT', onSigint);

    return ws;
  }

  async function reconnectWithBackoff() {
    if (reconnecting || manuallyStopped) return;
    reconnecting = true;
    const delays = [2000, 4000, 8000];

    for (let i = 0; i < delays.length; i++) {
      console.log(`${C.dim}[${ts()}]${C.reset} ${C.yellow}Reconnect attempt ${i + 1}/${delays.length} in ${delays[i] / 1000}s...${C.reset}`);
      await new Promise((r) => setTimeout(r, delays[i]));

      if (manuallyStopped) {
        reconnecting = false;
        return;
      }

      try {
        const freshRoom = await getRoomInfo(roomCode);
        if (!freshRoom) {
          console.log(`${C.dim}[${ts()}]${C.reset} ${C.red}Room no longer exists${C.reset}`);
          reconnecting = false;
          shutdown();
          return;
        }
        roomInfo.host = freshRoom.host;
        connect(true);
        reconnecting = false;
        return;
      } catch (e) {
        console.error(`${C.dim}[${ts()}]${C.reset} ${C.red}Reconnect attempt ${i + 1} failed: ${e.message}${C.reset}`);
      }
    }

    console.error(`${C.dim}[${ts()}]${C.reset} ${C.red}${C.bold}All reconnect attempts failed. Exiting.${C.reset}`);
    reconnecting = false;
    shutdown();
  }

  function shutdown() {
    const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
    console.log();
    console.log(`${C.bold}--- Session Summary ---${C.reset}`);
    console.log(`  ${C.cyan}Messages:${C.reset}  ${msgCount}`);
    console.log(`  ${C.cyan}Duration:${C.reset}  ${elapsed}s`);
    if (fileLogger) {
      console.log(`  ${C.cyan}Log file:${C.reset}  ${fileLogger.filePath}`);
      fileLogger.write({
        ts: new Date().toISOString(),
        direction: 'meta',
        type: 'session_end',
        msgCount,
        durationMs: Date.now() - startTime,
      });
      fileLogger.close();
    }
    console.log();
    process.exit(1);
  }

  connect(false);
}

main().catch((e) => {
  console.error(`${C.red}Fatal: ${e.message}${C.reset}`);
  process.exit(1);
});
