'use strict';

/**
 * bot.js — OracleBot: mineflayer spectator bot for Mortdecai's live vision system.
 *
 * Connects to the Minecraft dev server in spectator mode and exposes
 * observation methods (players, area scan, entities, world info) for
 * the oracle-bot HTTP/WebSocket layer (server.js, Task 2).
 */

const EventEmitter = require('events');
const mineflayer = require('mineflayer');

const DEFAULT_HOST = process.env.MC_HOST || '192.168.0.244';
const DEFAULT_PORT = parseInt(process.env.MC_PORT || '25568', 10);
const DEFAULT_USERNAME = process.env.BOT_USERNAME || 'OracleBot';

const RECONNECT_BASE_MS = 1000;
const RECONNECT_MAX_MS = 30000;

class OracleBot extends EventEmitter {
  constructor(options = {}) {
    super();
    this.host = options.host || DEFAULT_HOST;
    this.port = options.port || DEFAULT_PORT;
    this.username = options.username || DEFAULT_USERNAME;

    this._bot = null;
    this._reconnectDelay = RECONNECT_BASE_MS;
    this._reconnectTimer = null;
    this._destroyed = false;
    this._spawned = false;
  }

  // ──────────────────────────────────────────────
  // Public API
  // ──────────────────────────────────────────────

  /**
   * Create and connect the mineflayer bot.
   * Safe to call multiple times — will no-op if already connected.
   */
  connect() {
    if (this._bot) return;
    if (this._destroyed) {
      console.warn('[OracleBot] Cannot connect — bot has been destroyed.');
      return;
    }

    console.log(`[OracleBot] Connecting to ${this.host}:${this.port} as ${this.username} ...`);

    const bot = mineflayer.createBot({
      host: this.host,
      port: this.port,
      username: this.username,
      auth: 'offline',
      version: '1.21.11',
      hideErrors: false,
    });

    this._bot = bot;
    this._bindEvents(bot);
    this.emit('connected');
  }

  /**
   * Teleport the bot to a player's position (spectator /tp).
   * @param {string} playerName
   */
  followPlayer(playerName) {
    this._requireSpawned('followPlayer');
    this._bot.chat(`/tp ${this.username} ${playerName}`);
  }

  /**
   * Return an array of online players with their positions.
   * @returns {Array<{name: string, x: number, y: number, z: number, health: number|null}>}
   */
  getPlayers() {
    this._requireSpawned('getPlayers');
    const players = [];
    for (const [name, playerRef] of Object.entries(this._bot.players)) {
      if (!playerRef || name === this.username) continue;
      const entity = playerRef.entity;
      players.push({
        name,
        x: entity ? parseFloat(entity.position.x.toFixed(2)) : null,
        y: entity ? parseFloat(entity.position.y.toFixed(2)) : null,
        z: entity ? parseFloat(entity.position.z.toFixed(2)) : null,
        health: playerRef.entity ? playerRef.entity.metadata : null,
        ping: playerRef.ping,
      });
    }
    return players;
  }

  /**
   * Scan blocks in a top-down horizontal slice centered at (x, y, z).
   * Returns non-air blocks within the radius on the given Y level.
   * @param {number} x
   * @param {number} y
   * @param {number} z
   * @param {number} radius  - half-width of the scan square
   * @returns {Array<{x, y, z, name}>}
   */
  scanArea(x, y, z, radius = 16) {
    this._requireSpawned('scanArea');
    const bot = this._bot;
    const results = [];
    const r = Math.min(radius, 64); // hard cap to prevent OOM

    for (let bx = x - r; bx <= x + r; bx++) {
      for (let bz = z - r; bz <= z + r; bz++) {
        const block = bot.blockAt(bot.vec3(bx, y, bz));
        if (block && block.name !== 'air' && block.name !== 'cave_air' && block.name !== 'void_air') {
          results.push({ x: bx, y, z: bz, name: block.name });
        }
      }
    }
    return results;
  }

  /**
   * Get entities near a position within a given radius.
   * @param {number} x
   * @param {number} y
   * @param {number} z
   * @param {number} radius
   * @returns {Array<{id, type, name, x, y, z, dx, dy, dz}>}
   */
  getNearbyEntities(x, y, z, radius = 32) {
    this._requireSpawned('getNearbyEntities');
    const results = [];
    const r2 = radius * radius;

    for (const entity of Object.values(this._bot.entities)) {
      if (!entity || entity === this._bot.entity) continue;
      const pos = entity.position;
      const dx = pos.x - x;
      const dy = pos.y - y;
      const dz = pos.z - z;
      const dist2 = dx * dx + dy * dy + dz * dz;
      if (dist2 <= r2) {
        results.push({
          id: entity.id,
          type: entity.type,
          name: entity.name || entity.username || entity.displayName || entity.type,
          x: parseFloat(pos.x.toFixed(2)),
          y: parseFloat(pos.y.toFixed(2)),
          z: parseFloat(pos.z.toFixed(2)),
          distance: parseFloat(Math.sqrt(dist2).toFixed(2)),
        });
      }
    }

    results.sort((a, b) => a.distance - b.distance);
    return results;
  }

  /**
   * Return general world / server info.
   * @returns {{timeOfDay: number, isDay: boolean, raining: boolean, thundering: boolean, gameMode: string}}
   */
  getWorldInfo() {
    this._requireSpawned('getWorldInfo');
    const bot = this._bot;
    return {
      timeOfDay: bot.time.timeOfDay,
      isDay: bot.time.isDay,
      raining: bot.isRaining,
      thundering: bot.thunderState > 0,
      gameMode: bot.game.gameMode,
      dimension: bot.game.dimension,
    };
  }

  /**
   * Cleanly disconnect and prevent further reconnects.
   */
  destroy() {
    this._destroyed = true;
    if (this._reconnectTimer) {
      clearTimeout(this._reconnectTimer);
      this._reconnectTimer = null;
    }
    if (this._bot) {
      try {
        this._bot.quit('OracleBot shutting down');
      } catch (_) {
        // ignore errors during shutdown
      }
      this._bot = null;
    }
    this._spawned = false;
    console.log('[OracleBot] Destroyed.');
  }

  // ──────────────────────────────────────────────
  // Internal helpers
  // ──────────────────────────────────────────────

  _requireSpawned(methodName) {
    if (!this._bot || !this._spawned) {
      throw new Error(`[OracleBot] ${methodName}() called before bot spawned`);
    }
  }

  _bindEvents(bot) {
    bot.once('login', () => {
      console.log(`[OracleBot] Logged in as ${bot.username}`);
    });

    bot.once('spawn', () => {
      this._spawned = true;
      this._reconnectDelay = RECONNECT_BASE_MS; // reset backoff on successful spawn

      console.log(
        `[OracleBot] Spawned at x=${bot.entity.position.x.toFixed(1)} ` +
        `y=${bot.entity.position.y.toFixed(1)} ` +
        `z=${bot.entity.position.z.toFixed(1)} ` +
        `gamemode=${bot.game.gameMode}`
      );

      // If not already spectator, become spectator via server command
      if (bot.game.gameMode !== 'spectator') {
        console.log('[OracleBot] Not in spectator mode — requesting /gamemode spectator');
        bot.chat('/gamemode spectator');
      }

      this.emit('spawned', {
        position: bot.entity.position,
        gameMode: bot.game.gameMode,
      });
    });

    bot.on('playerJoined', (player) => {
      console.log(`[OracleBot] Player joined: ${player.username}`);
      this.emit('playerJoined', player.username);
    });

    bot.on('playerLeft', (player) => {
      console.log(`[OracleBot] Player left: ${player.username}`);
      this.emit('playerLeft', player.username);
    });

    bot.on('error', (err) => {
      console.error(`[OracleBot] Error: ${err.message}`);
      // Don't schedule reconnect here — 'end' will fire afterward
    });

    bot.on('kicked', (reason) => {
      let reasonText = reason;
      try {
        // reason may be a JSON chat component
        const parsed = JSON.parse(reason);
        reasonText = parsed.text || reason;
      } catch (_) {
        // use raw string
      }
      console.warn(`[OracleBot] Kicked: ${reasonText}`);
      this.emit('disconnected', { reason: 'kicked', message: reasonText });
    });

    bot.on('end', (reason) => {
      console.log(`[OracleBot] Connection ended: ${reason}`);
      this._bot = null;
      this._spawned = false;
      this.emit('disconnected', { reason: 'end', message: reason });
      this._scheduleReconnect();
    });
  }

  _scheduleReconnect() {
    if (this._destroyed) return;

    const delay = this._reconnectDelay;
    console.log(`[OracleBot] Reconnecting in ${delay}ms ...`);

    this._reconnectTimer = setTimeout(() => {
      this._reconnectTimer = null;
      // Exponential backoff: double delay up to max
      this._reconnectDelay = Math.min(this._reconnectDelay * 2, RECONNECT_MAX_MS);
      this.connect();
    }, delay);
  }
}

module.exports = OracleBot;
