'use strict';

/**
 * world-state.js — WorldState: aggregates OracleBot perception + gateway traces
 * into a unified, versioned state object for the oracle-bot WebSocket layer.
 *
 * All outbound messages carry `v: 1` for protocol versioning.
 */

const ACTIVE_TIMEOUT = 10000; // ms before reverting to idle
const MAX_TRACES = 50;
const SCAN_RADIUS = 16;
const ENTITY_RADIUS = 30;

class WorldState {
  /**
   * @param {import('./bot.js')} bot - Connected OracleBot instance
   */
  constructor(bot) {
    this.bot = bot;

    // Public constants (readable by consumers)
    this.ACTIVE_TIMEOUT = ACTIVE_TIMEOUT;
    this.MAX_TRACES = MAX_TRACES;

    // Mode tracking
    this.mode = 'idle';            // 'idle' | 'god' | 'sudo'
    this.activePlayer = null;      // player name or null
    this.activeSessionId = null;   // current session ID or null

    // Trace ring buffer
    this.traces = [];
    this.lastActivity = null;      // Date.now() timestamp or null
  }

  // ──────────────────────────────────────────────
  // Trace ingestion
  // ──────────────────────────────────────────────

  /**
   * Called when the gateway POSTs a trace event.
   * Updates mode, activePlayer, sessionId and appends to ring buffer.
   *
   * @param {Object} traceData - Arbitrary trace payload from the gateway
   */
  onTrace(traceData) {
    // Update mode if provided and valid
    if (traceData.mode && ['idle', 'god', 'sudo'].includes(traceData.mode)) {
      this.mode = traceData.mode;
    }

    // Update active player if provided
    if (traceData.player !== undefined) {
      this.activePlayer = traceData.player || null;
    }

    // Update session ID if provided
    if (traceData.session_id !== undefined) {
      this.activeSessionId = traceData.session_id || null;
    }

    this.lastActivity = Date.now();

    // Append to ring buffer, evict oldest if at capacity
    this.traces.push({ ...traceData, ts: this.lastActivity });
    if (this.traces.length > MAX_TRACES) {
      this.traces.shift();
    }
  }

  // ──────────────────────────────────────────────
  // Idle detection
  // ──────────────────────────────────────────────

  /**
   * Check if session has gone idle due to inactivity.
   * If mode != idle and lastActivity is older than ACTIVE_TIMEOUT, revert to idle.
   *
   * @returns {boolean} true if mode changed to idle
   */
  checkIdle() {
    if (this.mode === 'idle') return false;
    if (this.lastActivity === null) {
      this.mode = 'idle';
      this.activePlayer = null;
      this.activeSessionId = null;
      return true;
    }
    if (Date.now() - this.lastActivity > ACTIVE_TIMEOUT) {
      this.mode = 'idle';
      this.activePlayer = null;
      this.activeSessionId = null;
      return true;
    }
    return false;
  }

  // ──────────────────────────────────────────────
  // Message builders
  // ──────────────────────────────────────────────

  /**
   * Heartbeat: lightweight pulse with player list and world time.
   * Safe to call even when bot is not spawned — returns empty players on error.
   *
   * @returns {{v:number, type:string, mode:string, activePlayer:string|null, players:Array, world:Object, ts:number}}
   */
  buildHeartbeat() {
    let players = [];
    let world = { time: null, isRaining: null };

    try {
      players = this.bot.getPlayers();
    } catch (_) {
      // Bot not yet spawned — return empty array
    }

    try {
      const info = this.bot.getWorldInfo();
      world = {
        time: info.timeOfDay,
        isRaining: info.raining,
      };
    } catch (_) {
      // Bot not yet spawned — leave defaults
    }

    return {
      v: 1,
      type: 'heartbeat',
      mode: this.mode,
      activePlayer: this.activePlayer,
      players,
      world,
      ts: Date.now(),
    };
  }

  /**
   * World snapshot: full spatial state centered on the active player.
   * Returns null if no activePlayer or player position is unknown.
   *
   * @returns {Object|null}
   */
  buildWorldSnapshot() {
    if (!this.activePlayer) return null;

    // Find the active player's position from the bot's player list
    let center = null;
    try {
      const players = this.bot.getPlayers();
      const match = players.find((p) => p.name === this.activePlayer);
      if (!match || match.x === null || match.y === null || match.z === null) {
        // Player entity not loaded yet
        return null;
      }
      center = { x: match.x, y: match.y, z: match.z };
    } catch (_) {
      return null;
    }

    let blocks = [];
    let entities = [];
    let allPlayers = [];

    try {
      blocks = this.bot.scanArea(center.x, center.y, center.z, SCAN_RADIUS);
    } catch (_) {
      // Bot not spawned or other error — leave empty
    }

    try {
      entities = this.bot.getNearbyEntities(center.x, center.y, center.z, ENTITY_RADIUS);
    } catch (_) {
      // Bot not spawned or other error — leave empty
    }

    try {
      allPlayers = this.bot.getPlayers();
    } catch (_) {
      // Bot not spawned or other error — leave empty
    }

    return {
      v: 1,
      type: 'world',
      mode: this.mode,
      activePlayer: this.activePlayer,
      center,
      blocks,
      entities,
      players: allPlayers,
      ts: Date.now(),
    };
  }

  /**
   * Wrap a raw trace payload into a versioned trace event message.
   *
   * @param {Object} traceData
   * @returns {{v:number, type:string, ts:number, ...traceData}}
   */
  buildTraceEvent(traceData) {
    return {
      v: 1,
      type: 'trace',
      ...traceData,
      ts: Date.now(),
    };
  }

  /**
   * Mode change notification. Emit after onTrace() updates mode.
   *
   * @returns {{v:number, type:string, mode:string, player:string|null, ts:number}}
   */
  buildModeChange() {
    return {
      v: 1,
      type: 'mode',
      mode: this.mode,
      player: this.activePlayer,
      ts: Date.now(),
    };
  }

  /**
   * Bot connection status message.
   *
   * @param {boolean} connected
   * @returns {{v:number, type:string, connected:boolean, ts:number}}
   */
  buildStatus(connected) {
    return {
      v: 1,
      type: 'status',
      connected,
      ts: Date.now(),
    };
  }
}

module.exports = WorldState;
