'use strict';

/**
 * server.js — OracleBot entrypoint.
 *
 * Express HTTP server + WebSocket server providing:
 *   POST /trace    — receive tool traces from the gateway
 *   POST /command  — accept follow/scan commands
 *   GET  /health   — liveness / status check
 *   Static files from public/
 *   WS   /ws       — live push to dashboard clients
 */

const http = require('http');
const path = require('path');

const express = require('express');
const { WebSocketServer } = require('ws');

const OracleBot = require('./bot.js');
const WorldState = require('./world-state.js');

// ──────────────────────────────────────────────
// Config
// ──────────────────────────────────────────────

const PORT = parseInt(process.env.PORT || '3333', 10);
const HEARTBEAT_INTERVAL_MS = 5000;  // idle pulse
const ACTIVE_INTERVAL_MS    = 1000;  // active world-snapshot push

const MAX_WS_TOTAL    = 100;  // global WebSocket cap
const MAX_WS_PER_IP   =   5;  // per-IP WebSocket cap

// ──────────────────────────────────────────────
// Instances
// ──────────────────────────────────────────────

const bot   = new OracleBot();
const state = new WorldState(bot);

// ──────────────────────────────────────────────
// Express app
// ──────────────────────────────────────────────

const app = express();

app.use(express.json({ limit: '64kb' }));
app.use(express.static(path.join(__dirname, 'public')));

// ── POST /trace ──────────────────────────────

app.post('/trace', (req, res) => {
  const body = req.body;

  if (!body || typeof body.tool !== 'string') {
    return res.status(400).json({ ok: false, error: 'Missing required field: tool' });
  }

  const prevPlayer = state.activePlayer;

  state.onTrace(body);

  // Broadcast: trace event, mode change, then world snapshot
  broadcast(state.buildTraceEvent(body));
  broadcast(state.buildModeChange());

  const snapshot = state.buildWorldSnapshot();
  if (snapshot) broadcast(snapshot);

  // If the active player changed, follow them
  if (state.activePlayer && state.activePlayer !== prevPlayer) {
    try {
      bot.followPlayer(state.activePlayer);
    } catch (err) {
      // Bot may not be spawned yet — non-fatal
      console.warn(`[server] followPlayer failed: ${err.message}`);
    }
  }

  return res.json({ ok: true });
});

// ── POST /command ─────────────────────────────

app.post('/command', (req, res) => {
  const { action, target, center, radius } = req.body || {};

  if (!action) {
    return res.status(400).json({ ok: false, error: 'Missing required field: action' });
  }

  if (action === 'follow') {
    if (!target || typeof target !== 'string') {
      return res.status(400).json({ ok: false, error: 'follow requires target (player name)' });
    }
    try {
      bot.followPlayer(target);
      return res.json({ ok: true, action: 'follow', target });
    } catch (err) {
      return res.status(503).json({ ok: false, error: err.message });
    }
  }

  if (action === 'scan') {
    if (!center || typeof center.x !== 'number' || typeof center.y !== 'number' || typeof center.z !== 'number') {
      return res.status(400).json({ ok: false, error: 'scan requires center {x, y, z}' });
    }
    const r = typeof radius === 'number' ? radius : 16;
    try {
      const blocks   = bot.scanArea(center.x, center.y, center.z, r);
      const entities = bot.getNearbyEntities(center.x, center.y, center.z, r);
      return res.json({ ok: true, action: 'scan', center, radius: r, blocks, entities });
    } catch (err) {
      return res.status(503).json({ ok: false, error: err.message });
    }
  }

  return res.status(400).json({ ok: false, error: `Unknown action: ${action}` });
});

// ── GET /health ───────────────────────────────

app.get('/health', (_req, res) => {
  res.json({
    ok: true,
    botConnected: bot._spawned === true,
    wsClients: wss.clients.size,
    mode: state.mode,
    activePlayer: state.activePlayer,
  });
});

// ──────────────────────────────────────────────
// HTTP server
// ──────────────────────────────────────────────

const server = http.createServer(app);

// ──────────────────────────────────────────────
// WebSocket server
// ──────────────────────────────────────────────

const wss = new WebSocketServer({ server, path: '/ws' });

// Track connections per IP: Map<ip, number>
const ipCounts = new Map();

wss.on('connection', (ws, req) => {
  // Resolve client IP (trust x-forwarded-for from Caddy)
  const forwarded = req.headers['x-forwarded-for'];
  const ip = forwarded ? forwarded.split(',')[0].trim() : (req.socket.remoteAddress || 'unknown');

  // Enforce total cap
  if (wss.clients.size > MAX_WS_TOTAL) {
    ws.close(1008, 'Server at capacity');
    return;
  }

  // Enforce per-IP cap
  const currentCount = ipCounts.get(ip) || 0;
  if (currentCount >= MAX_WS_PER_IP) {
    ws.close(1008, 'Too many connections from your IP');
    return;
  }
  ipCounts.set(ip, currentCount + 1);

  // Store IP on the socket for cleanup
  ws._remoteIp = ip;

  // Send current state immediately on connect
  ws.send(JSON.stringify(state.buildHeartbeat()));

  // If we're not idle, also send current mode
  if (state.mode !== 'idle') {
    ws.send(JSON.stringify(state.buildModeChange()));
  }

  ws.on('close', () => {
    const count = ipCounts.get(ws._remoteIp) || 1;
    if (count <= 1) {
      ipCounts.delete(ws._remoteIp);
    } else {
      ipCounts.set(ws._remoteIp, count - 1);
    }
  });
});

/**
 * Broadcast a message object to all connected WebSocket clients.
 * @param {Object} msg
 */
function broadcast(msg) {
  if (wss.clients.size === 0) return;
  const payload = JSON.stringify(msg);
  for (const client of wss.clients) {
    if (client.readyState === client.OPEN) {
      client.send(payload);
    }
  }
}

// ──────────────────────────────────────────────
// Periodic updates
// ──────────────────────────────────────────────

// Heartbeat every 5s (idle mode pulse + idle timeout check)
setInterval(() => {
  if (wss.clients.size === 0) return;

  // Check if session has timed out
  const wentIdle = state.checkIdle();
  if (wentIdle) {
    broadcast(state.buildModeChange());
  }

  if (state.mode === 'idle') {
    broadcast(state.buildHeartbeat());
  }
}, HEARTBEAT_INTERVAL_MS);

// World snapshot every 1s (active mode only)
setInterval(() => {
  if (wss.clients.size === 0) return;
  if (state.mode === 'idle') return;

  const snapshot = state.buildWorldSnapshot();
  if (snapshot) broadcast(snapshot);
}, ACTIVE_INTERVAL_MS);

// ──────────────────────────────────────────────
// Bot event handlers
// ──────────────────────────────────────────────

bot.on('connected', () => {
  broadcast(state.buildStatus(true));
});

bot.on('disconnected', () => {
  broadcast(state.buildStatus(false));
});

bot.on('playerJoined', () => {
  broadcast(state.buildHeartbeat());
});

bot.on('playerLeft', () => {
  broadcast(state.buildHeartbeat());
});

// ──────────────────────────────────────────────
// Startup
// ──────────────────────────────────────────────

bot.connect();

server.listen(PORT, () => {
  console.log(`[oracle-bot] HTTP + WS server listening on port ${PORT}`);
  console.log(`[oracle-bot] WebSocket path: ws://localhost:${PORT}/ws`);
  console.log(`[oracle-bot] Health check:   http://localhost:${PORT}/health`);
  console.log(`[oracle-bot] Connecting bot to Minecraft at ${bot.host}:${bot.port} ...`);
});
