'use strict';

const WebSocket = require('ws');
const { SocksProxyAgent } = require('socks-proxy-agent');
const { MCStream } = require('./compression');
const { AuthStateMachine } = require('./auth');
const { drainMCFrames } = require('./packets');
const {
  buildEaglerVersion,
  buildEaglerLoginRequest,
  buildEaglerProfileData,
  buildEaglerFinishLogin,
  buildClientSettings,
  buildBrandPluginMessage,
  buildPositionAndLook,
  buildTeleportConfirm,
  buildChat,
  handleEaglerPacket,
  handleMCPacket,
  PACKET_KEEPALIVE,
  PACKET_KEEPALIVE_REPLY,
  PACKET_JOIN_GAME,
  PACKET_CHAT,
  PACKET_POSITION_LOOK,
  PACKET_CHUNK_DATA,
  PACKET_PLAYER_ABILITIES,
  PACKET_DISCONNECT,
  PACKET_PLUGIN_MESSAGE,
  PACKET_SET_COMPRESSION,
  buildMCFrame,
  encodeVarInt,
} = require('./codec');

const MC_PROTOCOL = 340;
const DEAD_SOCKET_MS = 20000;

const STATE = {
  IDLE: 'IDLE',
  CONNECTING: 'CONNECTING',
  EAGLER_HANDSHAKE: 'EAGLER_HANDSHAKE',
  EAGLER_LOGIN: 'EAGLER_LOGIN',
  MC_PLAY: 'MC_PLAY',
  AUTHENTICATING: 'AUTHENTICATING',
  READY: 'READY',
  CHATTING: 'CHATTING',
  DONE: 'DONE',
  STOPPED: 'STOPPED',
  FAILED: 'FAILED',
  DEAD_SOCKET: 'DEAD_SOCKET',
};

class Bot {
  constructor(opts) {
    this.id = opts.id;
    this.username = opts.username;
    this.targetUrl = opts.targetUrl;
    this.password = opts.password;
    this.proxy = opts.proxy || null;
    this.config = opts.config || {};
    this.brand = opts.brand || 'EaglercraftX';
    this.version = opts.version || '1.8';
    this.serverName = opts.serverName || 'default';
    this.profileEntries = opts.profileEntries || ['skin_v2', 'brand_uuid_v1', 'cape_v1'];
    this.handshakeStartMs = 0;
    this.state = STATE.IDLE;
    this.connectedAt = null;
    this.lastPacketAt = null;
    this.lastError = null;
    this.packetsSent = 0;
    this.packetsReceived = 0;
    this.lastKeepAliveAt = null;
    this.chatMessages = opts.chatMessages || [];
    this.stayConnected = !!opts.stayConnected;
    this._compressionThreshold = -1;
    this._serverReady = false;
    this._authDone = false;
    this._ws = null;
    this._recvBuf = Buffer.alloc(0);
    this._timeouts = [];
    this._intervals = [];
    this._authSm = new AuthStateMachine(this.config.auth_phrases, this.config.auth_timeout_ms || 2500);
    this._playerPos = { x: 0, y: 64, z: 0, yaw: 0, pitch: 0, onGround: true };
    this._reconnectAttempts = 0;
    this.maxReconnectAttempts = opts.maxReconnectAttempts || 3;
    this.eventBus = opts.eventBus || null;
    this._emittedStates = new Set();
    this._mcStream = new MCStream();
    this._lastKeepAlivePayload = null;
    this._sentClientSettings = false;
    this.log = opts.log || (() => {});
  }

  get mcStream() {
    return this._mcStream;
  }

  _setState(newState) {
    if (this.state === newState) return;
    const prev = this.state;
    this.state = newState;
    if (this._emittedStates.has(newState)) return;
    this._emittedStates.add(newState);
    if (this.eventBus) this.eventBus.emit('bot:state', { id: this.id, prev, state: newState, username: this.username });
  }

  async start() {
    if (this.state !== STATE.IDLE && this.state !== STATE.STOPPED && this.state !== STATE.FAILED) return;
    this._setState(STATE.CONNECTING);
    this.handshakeStartMs = Date.now();
    this._serverReady = false;
    this._authDone = false;
    this._sentClientSettings = false;
    this._authSm = new AuthStateMachine(this.config.auth_phrases, this.config.auth_timeout_ms || 2500);
    try {
      const proxyStr = this.proxy ? `${this.proxy.type}://${this.proxy.host}:${this.proxy.port}` : 'direct';
      this.log('info', `[${this.id}] Connecting as "${this.username}" via ${proxyStr}...`);

      const wsOpts = {
        headers: {
          'Origin': this.targetUrl,
          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        },
        handshakeTimeout: 15000,
        perMessageDeflate: false,
        rejectUnauthorized: false,
      };
      if (this.proxy) {
        wsOpts.agent = new SocksProxyAgent(`${this.proxy.type}://${this.proxy.host}:${this.proxy.port}`);
      }
      this._ws = new WebSocket(this.targetUrl, wsOpts);
      this._ws.on('open', () => this._onOpen());
      this._ws.on('message', (data) => this._onMessage(data));
      this._ws.on('close', (code, reason) => this._onClose(code, reason));
      this._ws.on('error', (err) => this._onError(err));
    } catch (err) {
      this.fail(`Connection error: ${err.message}`);
    }
  }

  _onOpen() {
    if (this._isStopped()) return;
    this._setState(STATE.EAGLER_HANDSHAKE);
    try {
      const buf = buildEaglerVersion(this.brand, this.version, MC_PROTOCOL, this.username);
      this._sendRaw(buf);
      this.packetsSent++;
      this.log('info', `[${this.id}] Sent Eagler version (MC proto=${MC_PROTOCOL})`);
    } catch (err) {
      this.fail(`Version packet failed: ${err.message}`);
    }
  }

  _sendRaw(buf) {
    if (this._ws && this._ws.readyState === WebSocket.OPEN) {
      this._ws.send(buf, { binary: true });
    }
  }

  _sendMCFrame(packetId, payload = Buffer.alloc(0)) {
    if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
    const compressed = this._mcStream.isEnabled() ? this._mcStream.deflate(payload) : null;
    const dataPart = compressed || Buffer.concat([encodeVarInt(packetId), payload]);
    if (this._mcStream.isEnabled()) {
      const inner = Buffer.concat([encodeVarInt(packetId), compressed]);
      const frame = Buffer.concat([encodeVarInt(inner.length), inner]);
      this._ws.send(frame, { binary: true });
    } else {
      this._ws.send(dataPart, { binary: true });
    }
    this.packetsSent++;
  }

  _sendChatMessage(message) {
    try {
      this._sendMCFrame(0x02, buildChat(message));
      this.log('info', `[${this.id}] Chat: ${message}`);
    } catch (e) {
      this.log('error', `[${this.id}] Chat failed: ${e.message}`);
    }
  }

  _onMessage(data) {
    if (this._isStopped()) return;
    const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
    this.lastPacketAt = Date.now();
    this.packetsReceived++;

    if (this.state === STATE.EAGLER_HANDSHAKE || this.state === STATE.EAGLER_LOGIN) {
      handleEaglerPacket(buf, this);
      return;
    }
    if (this.state === STATE.CONNECTING) {
      handleEaglerPacket(buf, this);
      return;
    }

    let raw = buf;
    if (this._mcStream.isEnabled()) {
      raw = this._mcStream.inflate(buf);
    }
    this._recvBuf = Buffer.concat([this._recvBuf, raw]);
    const { consumed } = drainMCFrames(this._recvBuf, (packetId, data) => {
      handleMCPacket(packetId, data, this);
    });
    if (consumed > 0) {
      this._recvBuf = this._recvBuf.slice(consumed);
    }
    if (this._recvBuf.length > 1024 * 1024) {
      this.fail('Receive buffer overflow');
    }
  }

  onEaglerServerVersion(eaglerProto, mcProto) {
    this._setState(STATE.EAGLER_LOGIN);
    try {
      const buf = buildEaglerLoginRequest(this.username, this.serverName, this.password);
      this._sendRaw(buf);
      this.packetsSent++;
      this.log('info', `[${this.id}] Sent login request`);
    } catch (err) {
      this.fail(`Login request failed: ${err.message}`);
    }
  }

  onEaglerAllowLogin() {
    try {
      const profile = buildEaglerProfileData(this.profileEntries);
      this._sendRaw(profile);
      this.packetsSent++;
      this._sendRaw(buildEaglerFinishLogin());
      this.packetsSent++;
      this.log('info', `[${this.id}] Sent Profile Data + finish login`);
    } catch (err) {
      this.fail(`Profile/finish failed: ${err.message}`);
    }
  }

  onEaglerFinishLogin() {
    this._setState(STATE.MC_PLAY);
    if (this.proxy && this.proxy.key) {
      this.log('info', `[${this.id}] Handshake complete via ${this.proxy.key}`);
    }
    if (!this._sentClientSettings) {
      this._sendMCFrame(0x04, buildClientSettings('en_US', 8, 0, true, 0x7F, 1));
      this._sendMCFrame(0x09, buildBrandPluginMessage('vanilla'));
      this._sentClientSettings = true;
      this.log('info', `[${this.id}] Sent Client Settings + Brand`);
    }
    this._setTimeout(() => {
      if (!this._serverReady && !this._isStopped() && this.state === STATE.MC_PLAY) {
        this.log('warn', `[${this.id}] No chunks after 3s — triggering auth anyway`);
        this._onServerReady();
      }
    }, 3000);
    this._startDeadCheck();
  }

  onKeepAlive(replyPayload) {
    this.lastKeepAliveAt = Date.now();
    if (this._mcStream.isEnabled()) {
      this._sendMCFrame(PACKET_KEEPALIVE_REPLY, replyPayload);
    } else {
      this._sendMCFrame(PACKET_KEEPALIVE_REPLY, replyPayload);
    }
    if (this.eventBus) this.eventBus.emit('bot:keepalive', { id: this.id });
    if (!this._serverReady) this._onServerReady();
  }

  onJoinGame(info) {
    if (!this._serverReady) this._onServerReady();
  }

  onPositionLook(info) {
    this._playerPos = {
      x: info.x, y: info.y, z: info.z,
      yaw: info.yaw, pitch: info.pitch, onGround: true,
    };
    const tpId = info.teleportId || 0;
    this._sendMCFrame(0x00, encodeVarInt(tpId));
    this._sendMCFrame(0x06, buildPositionAndLook(info.x, info.y, info.z, info.yaw, info.pitch, true));
    if (!this._serverReady) this._onServerReady();
  }

  onChunkData() {
    if (!this._serverReady) this._onServerReady();
  }

  onPlayerAbilities() {
    if (!this._serverReady) this._onServerReady();
  }

  onPluginMessage(pm) {
    if (pm.channel === 'minecraft:brand') {
      this.log('info', `[${this.id}] Server brand: ${pm.body}`);
    }
  }

  onChat(parsed) {
    if (parsed.text) {
      this.log('info', `[${this.id}] Chat: ${parsed.text}`);
    }
    const action = this._authSm.consume({ text: parsed.text, translate: parsed.translate });
    if (action === 'login_sent') {
      this._onAlreadyRegistered();
    } else if (action === 'auth_done') {
      this._onAuthComplete();
    }
  }

  _onServerReady() {
    if (this._serverReady) return;
    this._serverReady = true;
    this._setState(STATE.AUTHENTICATING);
    this.log('info', `[${this.id}] Server ready — sending /register`);
    const pwd = this.password;
    this._authSm.markRegisterSent();
    this._setTimeout(() => {
      if (this._isStopped() || this.state !== STATE.AUTHENTICATING) return;
      this._sendChatMessage(`/register ${pwd} ${pwd}`);
      this._authSm.markAwaitRegister();
    }, 500);
    this._setTimeout(() => {
      if (this._isStopped() || this.state !== STATE.AUTHENTICATING) return;
      if (!this._authSm.isDone()) {
        this._onAuthComplete();
      }
    }, this.config.auth_timeout_ms || 2500);
  }

  _onAlreadyRegistered() {
    if (this._authDone) return;
    const pwd = this.password;
    this._authSm.markLoginSent();
    this._setTimeout(() => {
      if (this._isStopped() || this.state !== STATE.AUTHENTICATING) return;
      this._sendChatMessage(`/login ${pwd}`);
      this._authSm.markAwaitLogin();
    }, 500);
    this._setTimeout(() => {
      if (this._isStopped() || this.state !== STATE.AUTHENTICATING) return;
      if (!this._authSm.isDone()) {
        this._onAuthComplete();
      }
    }, (this.config.auth_timeout_ms || 2500) + 500);
  }

  _onAuthComplete() {
    if (this._authDone) return;
    this._authDone = true;
    this._authSm.state = 'DONE';
    this._setState(STATE.READY);
    this.log('success', `[${this.id}] Auth complete`);
    if (this.proxy && this.proxy.key) {
      const latency = Date.now() - this.handshakeStartMs;
      if (this.eventBus) this.eventBus.emit('proxy:success', { key: this.proxy.key, latencyMs: latency });
    }
    if (this.eventBus) this.eventBus.emit('bot:handshake', { id: this.id });
    this._scheduleChatMessages();
  }

  _scheduleChatMessages() {
    const messages = this.chatMessages;
    if (!messages || messages.length === 0) {
      if (this.stayConnected) {
        this.log('info', `[${this.id}] No messages — staying connected`);
        this._setState(STATE.DONE);
      } else {
        this.log('info', `[${this.id}] No messages — disconnecting`);
        this._setTimeout(() => {
          if (!this._isStopped()) this.stop('done');
        }, 500);
      }
      return;
    }
    this._setState(STATE.CHATTING);
    messages.forEach((msg, i) => {
      this._setTimeout(() => {
        if (this._isStopped() || this.state !== STATE.CHATTING) return;
        this._sendChatMessage(msg);
      }, 1000 + (i * 1500));
    });
    if (this.stayConnected) {
      this._setTimeout(() => {
        if (!this._isStopped()) this.log('info', `[${this.id}] Messages sent — staying connected`);
      }, 1000 + (messages.length * 1500) + 500);
    } else {
      this._setTimeout(() => {
        if (!this._isStopped()) this.stop('done');
      }, 1000 + (messages.length * 1500) + 500);
    }
  }

  sendChat(message) {
    if (this._isStopped()) return false;
    if (this.state !== STATE.READY && this.state !== STATE.CHATTING && this.state !== STATE.DONE) return false;
    this._sendChatMessage(message);
    return true;
  }

  _startDeadCheck() {
    if (this._intervals.length > 0) return;
    const t = setInterval(() => {
      if (this._isStopped()) return;
      const elapsed = this.lastPacketAt ? Date.now() - this.lastPacketAt : 0;
      if (elapsed > DEAD_SOCKET_MS) {
        this.log('warn', `[${this.id}] No data for ${Math.round(elapsed / 1000)}s — dead socket`);
        this._setState(STATE.DEAD_SOCKET);
        this._cleanup();
        if (this.eventBus) this.eventBus.emit('bot:dead', { id: this.id, reason: 'dead-socket' });
        if (this.proxy && this.proxy.key) {
          if (this.eventBus) this.eventBus.emit('proxy:failure', { key: this.proxy.key, reason: 'dead-socket' });
        }
      }
    }, 5000);
    this._intervals.push(t);
  }

  _setTimeout(fn, ms) {
    if (this._isStopped()) return null;
    const t = setTimeout(() => {
      const idx = this._timeouts.indexOf(t);
      if (idx !== -1) this._timeouts.splice(idx, 1);
      try { fn(); } catch (_) {}
    }, ms);
    this._timeouts.push(t);
    return t;
  }

  _isStopped() {
    return this.state === STATE.STOPPED || this.state === STATE.FAILED || this.state === STATE.DEAD_SOCKET;
  }

  _onClose(code, reason) {
    if (this._isStopped()) return;
    if (this.state === STATE.EAGLER_HANDSHAKE || this.state === STATE.EAGLER_LOGIN || this.state === STATE.CONNECTING) {
      this.log('warn', `[${this.id}] Disconnected during handshake (code=${code})`);
      if (this.proxy && this.proxy.key && this.eventBus) {
        this.eventBus.emit('proxy:failure', { key: this.proxy.key, reason: 'handshake-failed' });
      }
      this._setState(STATE.FAILED);
      this.lastError = `disconnect-code-${code}`;
      if (this.eventBus) this.eventBus.emit('bot:dead', { id: this.id, reason: `disconnect-code-${code}` });
    } else {
      this.log('warn', `[${this.id}] Disconnected (code=${code})`);
      this._setState(STATE.DEAD_SOCKET);
      if (this.eventBus) this.eventBus.emit('bot:dead', { id: this.id, reason: `disconnect-code-${code}` });
    }
    this._cleanup();
  }

  _onError(err) {
    if (this._isStopped()) return;
    const msg = err.message || String(err);
    this.lastError = msg;
    this.log('error', `[${this.id}] Error: ${msg}`);
    if (this.proxy && this.proxy.key && this.eventBus && (
      msg.includes('certificate') || msg.includes('ECONNREFUSED') ||
      msg.includes('ETIMEDOUT') || msg.includes('ENOTFOUND') ||
      msg.includes('Proxy') || msg.includes('SOCKS')
    )) {
      this.eventBus.emit('proxy:failure', { key: this.proxy.key, reason: 'connection-error' });
    }
  }

  fail(message) {
    if (this._isStopped()) return;
    this.lastError = message;
    this.log('error', `[${this.id}] FAILED: ${message}`);
    this._setState(STATE.FAILED);
    if (this.eventBus) this.eventBus.emit('bot:dead', { id: this.id, reason: message });
    this._cleanup();
  }

  stop(reason = 'manual') {
    if (this._isStopped()) return;
    this._setState(STATE.STOPPED);
    this.log('info', `[${this.id}] Stopped (${reason})`);
    if (this.eventBus) this.eventBus.emit('bot:dead', { id: this.id, reason });
    this._cleanup();
  }

  _cleanup() {
    for (const t of this._timeouts) clearTimeout(t);
    this._timeouts = [];
    for (const t of this._intervals) clearInterval(t);
    this._intervals = [];
    if (this._ws) {
      try { if (this._ws.readyState <= 1) this._ws.close(1000, 'Done'); } catch (_) {}
      this._ws = null;
    }
    if (this.proxy && this.proxy.key && this.eventBus) {
      this.eventBus.emit('proxy:release', { key: this.proxy.key, botId: this.id });
    }
  }

  toJSON() {
    return {
      id: this.id,
      username: this.username,
      proxy: this.proxy ? `${this.proxy.type}://${this.proxy.host}:${this.proxy.port}` : 'direct',
      proxyKey: this.proxy?.key || null,
      state: this.state,
      packetsSent: this.packetsSent,
      packetsReceived: this.packetsReceived,
      connectedAt: this.connectedAt,
      lastPacketAt: this.lastPacketAt,
      lastError: this.lastError,
      lastKeepAliveAt: this.lastKeepAliveAt,
    };
  }
}

module.exports = { Bot, STATE };
