/**
 * Ollie Cosmos — server.
 *
 * Runs the authoritative simulation and exposes the Inhabitant API over HTTP.
 * The player viewport and the (future) Assistant "world" integration are both
 * clients of this API. Multiplayer is built in: many inhabitants can attach,
 * and when hosting (OLLIE_COSMOS_HOST=0.0.0.0) friends on your LAN can join via
 * your IP.
 */
'use strict';

const path = require('path');
const os = require('os');
const fs = require('fs');
const https = require('https');
const http = require('http');
const express = require('express');
const cors = require('cors');
const configManager = require('./config-manager');
const { World } = require('./sim/world');

const app = express();
const PORT = configManager.getPort();
const HOST = configManager.getHost();

// License verification + updates (Ollie Ecosystem standard) — see
// _support/documentation/VERIFICATION_GUIDE.md.
const PM_API_URL = process.env.OLLIE_PM_URL || 'https://pm.costa-and-associates.com';
const OLLIE_WEBPAGE_URL = process.env.OLLIE_WEBPAGE_URL || 'https://olliesystem.com';
const APP_CODE = 'ollie-cosmos';

// World is a shareable game server — permissive CORS by design.
app.use(cors());
app.use(express.json({ limit: '8mb' }));
app.use(express.static(path.join(__dirname, 'public')));

// Restore the durable world save if there is one, else build fresh from the
// configured seed/size. This is what makes "load my world back the way I left
// it" real — not just "regenerate the same starting terrain".
let world;
const savedWorld = configManager.loadWorld();
if (savedWorld) {
  try {
    world = new World({ tickRate: 10 });
    world.loadState(savedWorld); // throws on pre-infinite (format 1) saves — D1 clean break
  } catch (e) {
    console.error('[cosmos] could not restore saved world, starting fresh:', e.message);
    world = new World({ seed: Number(savedWorld && savedWorld.seed) || configManager.getSeed(), tickRate: 10 });
  }
} else {
  world = new World({ seed: configManager.getSeed(), tickRate: 10 });
}
world.start();

// Autosave so a crash or quit doesn't lose the world. Atomic + unref'd so it
// never holds the process open. Also saved explicitly on regenerate and exit.
const AUTOSAVE_MS = 20000;
const autosave = () => configManager.saveWorld(world.saveState());
const saveTimer = setInterval(autosave, AUTOSAVE_MS);
if (saveTimer.unref) saveTimer.unref();
for (const sig of ['SIGINT', 'SIGTERM']) {
  process.on(sig, () => { autosave(); process.exit(0); });
}

app.get('/api/health', (req, res) => {
  res.json({ ok: true, service: 'ollie-cosmos', pid: process.pid, port: PORT, tick: world.tick, seed: world.seed });
});

// ── License / system (verification gate + updates) ───────────────────────────
app.get('/api/system/pm-url', (req, res) => res.json({ url: PM_API_URL }));

app.get('/api/system/version', (req, res) => {
  try {
    const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
    res.json({ success: true, version: pkg.version || '0.0.0', app: APP_CODE });
  } catch (e) {
    res.status(500).json({ success: false, error: 'failed to read version' });
  }
});

function fetchOllieVersion(appCode) {
  const url = `${OLLIE_WEBPAGE_URL}/api/versions/${appCode}`;
  const lib = url.startsWith('https') ? https : http;
  return new Promise((resolve, reject) => {
    const r = lib.get(url, { timeout: 5000 }, (resp) => {
      let raw = '';
      resp.on('data', (d) => { raw += d; });
      resp.on('end', () => {
        if (resp.statusCode && resp.statusCode >= 400) return reject(new Error(`versions endpoint returned ${resp.statusCode}`));
        try {
          const data = JSON.parse(raw);
          if (data && data.success === false) return reject(new Error(data.error || 'versions endpoint returned success:false'));
          resolve(data || {});
        } catch (e) { reject(new Error('invalid JSON from versions endpoint')); }
      });
    });
    r.on('error', reject);
    r.setTimeout(5000, () => { r.destroy(); reject(new Error('versions endpoint timeout')); });
  });
}

app.post('/api/system/check-updates', async (req, res) => {
  try {
    const data = await fetchOllieVersion(APP_CODE);
    res.json({
      success: true,
      app_code: data.app_code || APP_CODE,
      name: data.name,
      version: data.version,
      released_at: data.released_at,
      download_url: data.download_url,
      release_notes_url: data.release_notes_url,
    });
  } catch (error) {
    res.json({ success: false, error: error.message });
  }
});

// ── Inhabitant API ───────────────────────────────────────────────────────────
app.post('/api/world/attach', (req, res) => res.json({ ok: true, ...world.attach(req.body || {}) }));
app.post('/api/world/detach', (req, res) => res.json({ ok: world.detach((req.body && req.body.id) || '') }));
app.get('/api/world/snapshot', (req, res) => res.json({ ok: true, snapshot: world.snapshot() }));

// The ground, one chunk at a time (heights as centi-units, materials as
// bytes). Clients keep a ring of these around their inhabitant and ask again
// when a chunk's version moves (terrain edits bump it).
app.get('/api/world/chunk', (req, res) => {
  const cx = parseInt(req.query.cx, 10), cz = parseInt(req.query.cz, 10);
  if (!Number.isFinite(cx) || !Number.isFinite(cz)) return res.json({ ok: false, error: 'cx and cz required' });
  res.json({ ok: true, chunk: world.chunkData(cx, cz) });
});
app.get('/api/world/types', (req, res) => res.json({ ok: true, typesVersion: world.typesVersion, types: world.serializeTypes() }));
app.get('/api/world/perceive', (req, res) => res.json({ ok: true, ...world.perceive(String(req.query.id || '')) }));
app.get('/api/world/grid', (req, res) => {
  const v = world.gridView(String(req.query.id || ''), { radius: req.query.radius, step: req.query.step });
  if (!v) return res.json({ ok: false, error: 'unknown inhabitant' });
  res.json({ ok: true, ...v });
});
app.post('/api/world/say', (req, res) => {
  const { id, text } = req.body || {};
  res.json(world.say(String(id || ''), text));
});
app.get('/api/world/chat', (req, res) => res.json({ ok: true, chat: world.recentChat(Number(req.query.n) || 30) }));
app.post('/api/world/act', (req, res) => {
  const { id, action } = req.body || {};
  res.json(world.act(String(id || ''), action));
});
app.post('/api/world/author', (req, res) => {
  const { id, spec } = req.body || {};
  res.json(world.author(String(id || ''), spec || {}));
});

// Direct manipulation — edit / delete / fork an existing entity by id. The same
// authoring power as author(), but aimed at what's already in the world: this is
// "click a thing and recode it on the go".
// `actor` (an inhabitant id) says who is doing the editing, so the world can
// apply its per-inhabitant god-verb grants (players full CRUD, agents
// create-only by default). Requests without an actor get full access — policy
// for well-behaved clients, not a security boundary (see grants in world.js).
app.post('/api/world/edit', (req, res) => {
  const body = req.body || {};
  const id = String(body.id || '');
  const actor = body.actor ? String(body.actor) : null;
  if (body.op === 'delete') return res.json(world.deleteEntity(id, actor));
  if (body.op === 'fork') return res.json(world.forkEntity(id, actor));
  res.json(world.editEntity(id, body.props || {}, actor));
});

// Sculpt / paint the ground (raise, lower, paint a material) in a radius.
app.post('/api/world/terrain', (req, res) => res.json(world.editTerrain(req.body || {}, req.body && req.body.actor ? String(req.body.actor) : null)));

// New world from a seed (or random). Persists the seed AND the fresh world so a
// restart brings back the new one; clients reload when the worldVersion changes.
app.post('/api/world/regenerate', (req, res) => {
  const result = world.regenerate(req.body && req.body.seed);
  configManager.saveSeed(result.seed);
  configManager.resetWorldDir(result.seed); // "new/load world" is fresh regeneration, not restoring old saved deviations for that seed
  configManager.saveWorld(world.saveState());
  res.json({ ok: true, ...result });
});

// Force an immediate save (the HUD's explicit "save" — autosave covers the rest).
app.post('/api/world/save', (req, res) => {
  const ok = configManager.saveWorld(world.saveState());
  res.json({ ok, tick: world.tick });
});

app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'public', 'index.html')));

function lanAddresses() {
  const out = [];
  const ifaces = os.networkInterfaces();
  for (const name of Object.keys(ifaces)) {
    for (const ni of ifaces[name] || []) {
      if (ni.family === 'IPv4' && !ni.internal) out.push(ni.address);
    }
  }
  return out;
}

if (require.main === module) {
  app.listen(PORT, HOST, () => {
    console.log('\n  Ollie Cosmos');
    console.log('  ════════════════════════════════════════════');
    console.log(`  URL:    http://localhost:${PORT}`);
    console.log(`  Seed:   ${world.seed}   World: "${world.name}" (infinite)   Tick: ${world.tickRate}Hz`);
    if (configManager.isHosting()) {
      console.log('  Hosting (LAN) — friends can join at:');
      for (const ip of lanAddresses()) console.log(`     http://${ip}:${PORT}`);
    } else {
      console.log('  Private (loopback). To host for friends: OLLIE_COSMOS_HOST=0.0.0.0 npm start');
    }
    console.log('  ════════════════════════════════════════════\n');
  });
}

module.exports = app;
