"use strict";

/**
 * note-store.js — v3.1.0
 * Zero-dependency note storage for Mission Barisal.
 *
 * Key improvements over v3.0.0:
 *   • Encryption (AES-256-CBC via Node.js built-in crypto)
 *   • Multi-instance locking (exclusive file create via fs.open 'wx')
 *   ws://localhost:${PORT} flushSync now drains the debounce queue before writing
 *   • Auto-clean interval uses a module-level reference (no stacking)
 *   • Backup rotation only runs when original file exists
 *
 * ─── API ─────────────────────────────────────────────────────
 *   initNoteStore()    — Initialize data dir & load notes
 *   getNote(id)        — Get a note by session/module ID
 *   setNote(id, data)  — Set/update a note
 *   listNotes()        — Get all note IDs
 *   cleanNotes(maxAge) — Remove expired notes
 *   flushSync()        — Force sync flush (drains queue first)
 * ─────────────────────────────────────────────────────────────
 */

const fs = require("fs");
const path = require("path");
const crypto = require("crypto");  // Built-in in Node.js — zero dependency

const DATA_DIR = path.resolve(process.env.DATA_DIR || "./data");
const NOTE_FILE = path.join(DATA_DIR, "notes.json");
const LOCK_FILE = path.join(DATA_DIR, ".note-store.lock");
const NOTE_TTL = parseInt(process.env.NOTE_TTL || "86400000", 10);
const MAX_NOTE_SIZE = parseInt(process.env.MAX_NOTE_SIZE || "512000", 10);
// NOTE: read at call time, not module load time, so env changes take effect.
function getEncryptionKey() {
  return process.env.NOTE_ENCRYPTION_KEY || null;
}

let notes = {};
let initialized = false;
let cleanInterval = null;  // Module-level ref to prevent interval stacking

// ─── Write Queue — serializes setNote calls ──────────────────
let writeChain = Promise.resolve();

// ─── Debounce Timer — accumulates writes ─────────────────────
let flushTimer = null;
let flushNeeded = false;

// ─── Encryption / Decryption ─────────────────────────────────

function encryptText(text) {
  const encKey = getEncryptionKey();
  if (!encKey) return text;
  const salt = crypto.randomBytes(16);
  const derivedKey = crypto.scryptSync(encKey, salt, 32);
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv("aes-256-cbc", derivedKey, iv);
  let encrypted = cipher.update(text, "utf8", "hex");
  encrypted += cipher.final("hex");
  // Format: salt:iv:ciphertext:hmac
  const hmac = crypto.createHmac("sha256", derivedKey).update(encrypted).digest("hex");
  return salt.toString("hex") + ":" + iv.toString("hex") + ":" + encrypted + ":" + hmac;
}

function decryptText(text) {
  const encKey = getEncryptionKey();
  if (!encKey) return text;
  try {
    const parts = text.split(":");
    if (parts.length < 3) return text;
    const salt = Buffer.from(parts.shift(), "hex");
    const iv = Buffer.from(parts.shift(), "hex");
    const encryptedText = parts.join(":");
    const hmacFromData = encryptedText.length > 64 ? encryptedText.slice(-64) : "";
    const actualCiphertext = hmacFromData ? encryptedText.slice(0, -64) : encryptedText;
    const derivedKey = crypto.scryptSync(encKey, salt, 32);
    if (hmacFromData) {
      const expectedHmac = crypto.createHmac("sha256", derivedKey).update(actualCiphertext).digest("hex");
      if (expectedHmac !== hmacFromData) {
        throw new Error("HMAC mismatch — data may be tampered");
      }
    }
    const decipher = crypto.createDecipheriv("aes-256-cbc", derivedKey, iv);
    let decrypted = decipher.update(actualCiphertext, "hex", "utf8");
    decrypted += decipher.final("utf8");
    return decrypted;
  } catch (err) {
    console.error("[NOTE-STORE] Decryption failed:", err.message);
    if (text.includes(":") && text.length > 64) {
      console.warn("[NOTE-STORE] WARNING: Data appears encrypted but decryption failed. Check NOTE_ENCRYPTION_KEY.");
    }
    return null;
  }
}

// ─── Multi-Instance Locking ──────────────────────────────────

function acquireLock() {
  try {
    const fd = fs.openSync(LOCK_FILE, "wx");
    fs.writeSync(fd, JSON.stringify({ pid: process.pid, started: Date.now() }));
    fs.closeSync(fd);
    return true;
  } catch (err) {
    if (err.code === "EEXIST") {
      try {
        const content = fs.readFileSync(LOCK_FILE, "utf8");
        const lockData = JSON.parse(content);
        if (Date.now() - lockData.started > 30000) {
          fs.unlinkSync(LOCK_FILE);
          return acquireLock();
        }
      } catch (_) {}
      return false;
    }
    return false;
  }
}

function releaseLock() {
  try {
    if (fs.existsSync(LOCK_FILE)) fs.unlinkSync(LOCK_FILE);
  } catch (_) {}
}

// ─── Helpers ─────────────────────────────────────────────────

function now() {
  return Date.now();
}

function ensureDir(dirPath) {
  if (!fs.existsSync(dirPath)) {
    fs.mkdirSync(dirPath, { recursive: true });
  }
}

function atomicWriteSync(filePath, content) {
  const tmpPath = filePath + ".tmp";
  fs.writeFileSync(tmpPath, content, "utf8");
  fs.renameSync(tmpPath, filePath);
}

function rotateBackup(filePath) {
  if (!fs.existsSync(filePath)) return;
  const bak1 = filePath + ".bak1";
  const bak2 = filePath + ".bak2";
  const bak3 = filePath + ".bak3";
  if (fs.existsSync(bak2)) fs.renameSync(bak2, bak3);
  if (fs.existsSync(bak1)) fs.renameSync(bak1, bak2);
  fs.renameSync(filePath, bak1);
}

function validateNoteData(id, data) {
  if (!id || typeof id !== "string" || id.trim() === "") {
    return { valid: false, reason: "id must be a non-empty string" };
  }
  if (id.startsWith("_")) {
    return { valid: false, reason: "id starting with '_' is reserved (e.g. _meta)" };
  }
  if (!data || typeof data !== "object" || Array.isArray(data)) {
    return { valid: false, reason: "data must be a plain object" };
  }
  if (Object.keys(data).length === 0) {
    return { valid: false, reason: "data cannot be empty" };
  }
  const sizeEstimate = JSON.stringify(data).length;
  if (sizeEstimate > MAX_NOTE_SIZE) {
    return { valid: false, reason: "data exceeds " + MAX_NOTE_SIZE + " byte limit (got " + sizeEstimate + ")" };
  }
  return { valid: true };
}

/**
 * Read file content with decryption support.
 * Handles: plain JSON, old format (iv:ciphertext), new format (salt:iv:ciphertext:hmac)
 * Returns parsed object or throws on failure.
 */
function readNotesFile(filePath) {
  const raw = fs.readFileSync(filePath, "utf8");
  // Try plain JSON first (backward compat with v2/v3 without encryption)
  try {
    const parsed = JSON.parse(raw);
    if (typeof parsed === "object" && parsed !== null) return parsed;
  } catch (_) { /* not plain JSON — try decryption */ }
  // Try decryption
  const decrypted = decryptText(raw);
  if (decrypted === null) {
    throw new Error("Decryption failed — check NOTE_ENCRYPTION_KEY or data may be corrupted");
  }
  return JSON.parse(decrypted);
}

/**
 * Serialized write to disk via write queue.
 * Encrypts content if encryption key is set.
 * Also encrypts backup files to prevent plaintext leak.
 */
function performWrite() {
  try {
    ensureDir(DATA_DIR);
    let content = JSON.stringify(notes, null, 2);
    const encKey = getEncryptionKey();
    if (encKey) {
      content = encryptText(content);
    }
    rotateBackup(NOTE_FILE);
    atomicWriteSync(NOTE_FILE, content);
    // If encryption is on, encrypt backup files too (prevents plaintext leak)
    if (encKey) {
      const bak1 = NOTE_FILE + ".bak1";
      if (fs.existsSync(bak1)) {
        try {
          const bakRaw = fs.readFileSync(bak1, "utf8");
          if (bakRaw.trim().startsWith("{")) {
            atomicWriteSync(bak1, encryptText(bakRaw));
          }
        } catch (_) {}
      }
    }
  } catch (err) {
    try {
      let content = JSON.stringify(notes, null, 2);
      if (getEncryptionKey()) content = encryptText(content);
      fs.writeFileSync(NOTE_FILE, content, "utf8");
    } catch (fatal) {
      console.error("[NOTE-STORE] Flush error:", fatal.message);
    }
  }
}

// ─── Flush Functions ─────────────────────────────────────────

function scheduleFlush() {
  flushNeeded = true;
  if (flushTimer) return;
  flushTimer = setTimeout(() => {
    flushTimer = null;
    if (!flushNeeded) return;
    flushNeeded = false;
    writeChain = writeChain.then(() => performWrite());
  }, 150);
}

function flushSync() {
  if (flushTimer) {
    clearTimeout(flushTimer);
    flushTimer = null;
  }
  if (flushNeeded) {
    flushNeeded = false;
    performWrite();
  }
  return true;
}

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

function initNoteStore() {
  ensureDir(DATA_DIR);

  if (!acquireLock()) {
    console.warn("[NOTE-STORE] WARNING: Another instance is using " + DATA_DIR + " (lock file exists). Data corruption possible if both write simultaneously.");
  }

  if (fs.existsSync(NOTE_FILE)) {
    try {
      notes = readNotesFile(NOTE_FILE);
      console.log(
        "[NOTE-STORE] Loaded: " +
          Object.keys(notes).length +
          " notes — " +
          NOTE_FILE +
          (getEncryptionKey() ? " [ENCRYPTED]" : " [PLAIN]"),
      );
    } catch (err) {
      console.log("[NOTE-STORE] Main file corrupt, trying .bak1: " + err.message);
      const bak1 = NOTE_FILE + ".bak1";
      if (fs.existsSync(bak1)) {
        try {
          notes = readNotesFile(bak1);
          console.log("[NOTE-STORE] Recovered from .bak1: " + Object.keys(notes).length + " notes");
          atomicWriteSync(NOTE_FILE, JSON.stringify(notes, null, 2));
        } catch (bakErr) {
          console.log("[NOTE-STORE] .bak1 also corrupt, starting fresh:", bakErr.message);
          notes = {};
        }
      } else {
        console.log("[NOTE-STORE] No backup, starting fresh:", err.message);
        notes = {};
      }
    }
  } else {
    // notes.json missing — check if .bak1 exists (crash recovery during write)
    const bak1 = NOTE_FILE + ".bak1";
    if (fs.existsSync(bak1)) {
      try {
        notes = readNotesFile(bak1);
        console.log("[NOTE-STORE] Recovered from .bak1 (" + Object.keys(notes).length + " notes) — notes.json was missing (crash during write)");
        atomicWriteSync(NOTE_FILE, JSON.stringify(notes, null, 2));
      } catch (bakErr) {
        console.log("[NOTE-STORE] .bak1 also corrupt, starting fresh:", bakErr.message);
        notes = {};
      }
    } else {
      notes = {
        _meta: {
          created: now(),
          version: "3.1.0",
          domain: process.env.DOMAIN || "localhost",
        },
      };
      flushSync();
      console.log("[NOTE-STORE] Created fresh — " + NOTE_FILE);
    }
  }

  // Clear previous interval on re-init to prevent stacking
  if (cleanInterval) {
    clearInterval(cleanInterval);
    console.log("[NOTE-STORE] Cleared previous auto-clean interval (re-init detected)");
  }
  const CLEAN_INTERVAL = parseInt(process.env.NOTE_CLEAN_INTERVAL || "3600000", 10);
  cleanInterval = setInterval(() => {
    const removed = cleanNotes(NOTE_TTL);
    if (removed > 0) {
      console.log("[NOTE-STORE] Auto-cleaned " + removed + " expired notes");
    }
  }, CLEAN_INTERVAL);

  initialized = true;
  return { ok: true, noteCount: Object.keys(notes).length };
}

function getNote(id) {
  if (!initialized) return null;
  if (!id || typeof id !== "string") return null;
  return notes[id] || null;
}

function setNote(id, data) {
  if (!initialized) throw new Error("note-store not initialized");
  const validation = validateNoteData(id, data);
  if (!validation.valid) {
    throw new Error("[NOTE-STORE] setNote rejected: " + validation.reason);
  }
  notes[id] = {
    ...(notes[id] || {}),
    ...data,
    updated: now(),
  };
  scheduleFlush();
  return notes[id];
}

function listNotes() {
  if (!initialized) return [];
  return Object.keys(notes).filter((k) => !k.startsWith("_"));
}

function cleanNotes(maxAgeMs) {
  if (!initialized) return 0;
  const cutoff = now() - maxAgeMs;
  let removed = 0;
  for (const [id, note] of Object.entries(notes)) {
    if (id.startsWith("_")) continue;
    if ((note.created || note.updated || 0) < cutoff) {
      delete notes[id];
      removed++;
    }
  }
  if (removed > 0) flushSync();
  return removed;
}

// Clean up lock file on exit
process.on("exit", releaseLock);
process.on("SIGINT", () => { releaseLock(); process.exit(0); });
process.on("SIGTERM", () => { releaseLock(); process.exit(0); });

module.exports = {
  initNoteStore,
  getNote,
  setNote,
  listNotes,
  cleanNotes,
  flushSync,
};
