const fs = require('fs');
const path = require('path');
const os = require('os');

const DEFAULT_PORT = 19013; // next free Ollie port after Settings (19012)

class ConfigManager {
  constructor() {
    this.configDir = path.join(os.homedir(), '.ollie-cosmos');
    this.stateFile = path.join(this.configDir, 'state.json');
    this.worldDir = path.join(this.configDir, 'world'); // per-seed durable saves (WP4)
    if (!fs.existsSync(this.configDir)) fs.mkdirSync(this.configDir, { recursive: true });
    this.config = this._load();
    // Content last written per file (by path) — lets saveWorld() skip chunks
    // that haven't changed since the last autosave instead of rewriting the
    // whole explored world every tick. Keyed by absolute path, not chunk key,
    // so it stays correct across a seed switch (see resetWorldDir).
    this._writeCache = new Map();
  }

  _load() {
    let state = {};
    if (fs.existsSync(this.stateFile)) {
      try {
        state = JSON.parse(fs.readFileSync(this.stateFile, 'utf8'));
      } catch (e) {
        console.error('[world] failed to read state.json:', e.message);
      }
    }
    if (!state.seed) {
      state.seed = (Date.now() >>> 0) || 1337;
      this._save(state);
    }
    return state;
  }

  _save(state) {
    try {
      fs.writeFileSync(this.stateFile, JSON.stringify(state, null, 2), 'utf8');
    } catch (e) {
      console.error('[world] failed to save state.json:', e.message);
    }
  }

  getPort() {
    return Number(process.env.PORT) || this.config.port || DEFAULT_PORT;
  }
  getSeed() {
    return this.config.seed >>> 0;
  }
  /**
   * World edge length (cells). Set OLLIE_COSMOS_SIZE or `size` in
   * ~/.ollie-cosmos/state.json. Bounded so the terrain mesh stays sane — a
   * truly *infinite* (chunk-streamed) world is a future item (see VISION.md).
   */
  getSize() {
    const s = Number(process.env.OLLIE_COSMOS_SIZE) || this.config.size || 512;
    // Upper bound keeps the single terrain mesh + load snapshot sane on modest
    // GPUs. 768² ≈ 590k verts is about the ceiling before a chunk-streamed world
    // (see VISION.md) is the right move instead of a bigger monolithic grid.
    return Math.max(96, Math.min(768, Math.round(s)));
  }
  saveSeed(seed) {
    this.config.seed = seed >>> 0;
    this._save(this.config);
  }

  /**
   * Bind host. World is the ONE Ollie app meant to be *shared*: default to
   * loopback (private), but set OLLIE_COSMOS_HOST=0.0.0.0 (or `host` in
   * state.json) to host a server friends on your LAN can join.
   */
  getHost() {
    return process.env.OLLIE_COSMOS_HOST || this.config.host || '127.0.0.1';
  }
  isHosting() {
    return this.getHost() === '0.0.0.0';
  }

  // ── durable world save (WP4: per-chunk files under world/<seed>/) ───────────
  // One directory per seed (~/.ollie-cosmos/world/<seed>/): a small meta.json
  // (seed/name/tick/versions/types/dormant bodies) plus chunks/<cx>_<cz>.json
  // for every chunk World.saveState() reports as touched (edits and/or a
  // populated entity record) — untouched chunks store nothing. World.js and
  // ChunkStore are unchanged: they still produce/consume the same in-memory
  // shape (chunkEdits/chunkEntities as Map-entry arrays); this file only
  // decides how that shape is laid out on disk.
  _seedDir(seed) { return path.join(this.worldDir, String(seed >>> 0)); }
  _chunkFile(dir, key) { return path.join(dir, 'chunks', key.replace(',', '_') + '.json'); } // "3,-5" -> "3_-5.json"
  _keyFromChunkFile(file) { return file.replace(/\.json$/, '').replace('_', ','); } // reverse of the above

  /** Atomic write, but only if `data` actually differs from what's already
   *  there — this is the "lazy" in lazy write-back: an autosave over a world
   *  you've mostly stopped touching costs ~zero disk I/O. */
  _writeJsonIfChanged(file, data) {
    const str = JSON.stringify(data);
    if (this._writeCache.get(file) === str) return;
    fs.mkdirSync(path.dirname(file), { recursive: true });
    const tmp = file + '.tmp';
    fs.writeFileSync(tmp, str, 'utf8');
    fs.renameSync(tmp, file);
    this._writeCache.set(file, str);
  }

  /** The last saved world for `seed` (defaults to the configured seed) in the
   *  same shape World.loadState() has always expected, or null if none/corrupt. */
  loadWorld(seed) {
    const dir = this._seedDir(seed != null ? seed : this.getSeed());
    const metaFile = path.join(dir, 'meta.json');
    if (!fs.existsSync(metaFile)) return null;
    try {
      const meta = JSON.parse(fs.readFileSync(metaFile, 'utf8'));
      const chunkEdits = [], chunkEntities = [];
      const chunksDir = path.join(dir, 'chunks');
      if (fs.existsSync(chunksDir)) {
        for (const file of fs.readdirSync(chunksDir)) {
          if (!file.endsWith('.json')) continue;
          let data;
          try { data = JSON.parse(fs.readFileSync(path.join(chunksDir, file), 'utf8')); } catch (e) { continue; } // skip a corrupt chunk, don't lose the rest of the world
          const key = this._keyFromChunkFile(file);
          if (data.edits) chunkEdits.push([key, data.edits]);
          if (data.entities) chunkEntities.push([key, data.entities]);
        }
      }
      return { ...meta, chunkEdits, chunkEntities };
    } catch (e) {
      console.error('[cosmos] failed to read world meta:', e.message);
      return null;
    }
  }

  /** Persist a World.saveState() snapshot: meta.json + one file per touched
   *  chunk, each written only if it changed since the last save. */
  saveWorld(state) {
    try {
      const dir = this._seedDir(state.seed);
      const { chunkEdits, chunkEntities, ...meta } = state;
      this._writeJsonIfChanged(path.join(dir, 'meta.json'), meta);
      const perChunk = new Map();
      for (const [k, edits] of chunkEdits || []) { if (!perChunk.has(k)) perChunk.set(k, {}); perChunk.get(k).edits = edits; }
      for (const [k, rec] of chunkEntities || []) { if (!perChunk.has(k)) perChunk.set(k, {}); perChunk.get(k).entities = rec; }
      for (const [k, data] of perChunk) this._writeJsonIfChanged(this._chunkFile(dir, k), data);
      if (this.config.seed !== state.seed) this.saveSeed(state.seed); // so the next launch's getSeed() finds this directory
      return true;
    } catch (e) {
      console.error('[cosmos] failed to save world:', e.message);
      return false;
    }
  }

  /** Wipe a seed's saved directory — used before regenerating that seed fresh
   *  (D1-style clean break, but per-seed: "new/load world" always means
   *  "deterministic regeneration", never "restore what I previously did to
   *  this seed"; only the durable-save path means that). */
  resetWorldDir(seed) {
    const dir = this._seedDir(seed);
    try { fs.rmSync(dir, { recursive: true, force: true }); } catch (e) { /* nothing to remove */ }
    for (const key of this._writeCache.keys()) if (key.startsWith(dir)) this._writeCache.delete(key);
  }
}

module.exports = new ConfigManager();
