// server/index.js — generic KV document store over HTTP + WebSocket.
// firebase mirror: doc-tree model. Thin REST, path-based WS push.
// Adapter (src/storage/server.js) = passthrough, no shape translation.
// TEST: cache layer rebuild check.

'use strict';

const express = require('express');
const cors = require('cors');
const http = require('http');
const crypto = require('crypto');
const { WebSocketServer } = require('ws');
const { openDb, makeStore } = require('./db');

function createServer({ dbPath, port, corsOrigin, allowDevEndpoints = false } = {}) {
  const db = openDb(dbPath || './data/tracker.sqlite');
  const app = express();
  app.use(cors({ origin: corsOrigin || '*' }));
  app.use(express.json({ limit: '1mb' }));

  // WS subscribers: path -> Set<ws>.
  // Subscribers register a path (doc or collection). On change, notify:
  //   - doc subscribers at the changed path
  //   - collection subscribers at the changed doc's parent path
  const docSubscribers = new Map();      // path -> Set<ws>
  const collSubscribers = new Map();     // collPath -> Set<ws>

  function addSub(map, key, ws) {
    if (!map.has(key)) map.set(key, new Set());
    map.get(key).add(ws);
  }
  function removeSub(map, key, ws) {
    const set = map.get(key);
    if (set) { set.delete(ws); if (set.size === 0) map.delete(key); }
  }
  function dropWs(ws) {
    for (const [key, set] of docSubscribers) { set.delete(ws); if (set.size === 0) docSubscribers.delete(key); }
    for (const [key, set] of collSubscribers) { set.delete(ws); if (set.size === 0) collSubscribers.delete(key); }
  }

  function broadcast(change) {
    const payload = JSON.stringify({ type: 'change', change });
    // doc subscriber at exact path
    const docSet = docSubscribers.get(change.path);
    if (docSet) [...docSet].forEach(ws => ws.readyState === ws.OPEN && ws.send(payload));
    // collection subscribers at parent path (collection contains this doc)
    if (change.parent) {
      const collSet = collSubscribers.get(change.parent);
      if (collSet) [...collSet].forEach(ws => ws.readyState === ws.OPEN && ws.send(payload));
    }
  }

  const store = makeStore(db, broadcast);

  // --- generic REST ---

  app.get('/health', (req, res) => res.json({ ok: true }));

  // GET /api/doc?path=campaigns/abc/encounters/xyz
  app.get('/api/doc', (req, res) => {
    const { path: p } = req.query;
    if (!p) return res.status(400).json({ error: 'path required' });
    res.json({ path: p, data: store.getDoc(p) });
  });

  // GET /api/collection?path=campaigns/abc/encounters
  app.get('/api/collection', (req, res) => {
    const { path: p, whereField, whereOp, whereValue, orderBy, dir, limit, offset } = req.query;
    if (!p) return res.status(400).json({ error: 'path required' });
    const opts = {};
    if (whereField) opts.where = { field: whereField, op: whereOp || '==', value: whereValue };
    if (orderBy) opts.orderBy = orderBy;
    if (dir) opts.dir = dir;
    if (limit) opts.limit = limit;
    if (offset) opts.offset = offset;
    res.json(store.getCollection(p, opts));
  });

  app.get('/api/collection/count', (req, res) => {
    const { path: p } = req.query;
    if (!p) return res.status(400).json({ error: 'path required' });
    res.json({ count: store.countCollection(p) });
  });

  // PUT /api/doc  body: { path, data }   (replace)
  app.put('/api/doc', (req, res) => {
    const { path: p, data } = req.body || {};
    if (!p) return res.status(400).json({ error: 'path required' });
    res.json({ path: p, data: store.setDoc(p, data) });
  });

  // PATCH /api/doc  body: { path, patch }   (shallow merge, create-on-miss)
  app.patch('/api/doc', (req, res) => {
    const { path: p, patch } = req.body || {};
    if (!p) return res.status(400).json({ error: 'path required' });
    res.json({ path: p, data: store.updateDoc(p, patch) });
  });

  // DELETE /api/doc?path=...
  app.delete('/api/doc', (req, res) => {
    const { path: p } = req.query;
    if (!p) return res.status(400).json({ error: 'path required' });
    store.deleteDoc(p);
    res.json({ ok: true });
  });

  // DELETE /api/collection?path=...&whereField=...&whereValue=...
  // Bulk delete whole collection or filtered. No fetch. SQL DELETE.
  // DEV ONLY: requires ALLOW_DEV_ENDPOINTS=1 env on server.
  app.delete('/api/collection', (req, res) => {
    if (!allowDevEndpoints && process.env.ALLOW_DEV_ENDPOINTS !== '1') {
      return res.status(403).json({ error: 'bulk delete disabled (dev only)' });
    }
    const { path: p, whereField, whereOp, whereValue } = req.query;
    if (!p) return res.status(400).json({ error: 'path required' });
    const opts = {};
    if (whereField) opts.where = { field: whereField, op: whereOp || '==', value: whereValue };
    const deleted = store.deleteCollection(p, opts);
    res.json({ ok: true, deleted });
  });

  // POST /api/collection  body: { path, data }  (addDoc: auto-id under collection)
  app.post('/api/collection', (req, res) => {
    const { path: collPath, data } = req.body || {};
    if (!collPath) return res.status(400).json({ error: 'path required' });
    const id = crypto.randomUUID();
    const docPath = `${collPath}/${id}`;
    res.json({ id, path: docPath, data: store.setDoc(docPath, data) });
  });

  // POST /api/batch  body: { ops: [{type:'set'|'update'|'delete', path, data?}] }
  app.post('/api/batch', (req, res) => {
    const { ops } = req.body || {};
    if (!Array.isArray(ops)) return res.status(400).json({ error: 'ops array required' });
    store.batchWrite(ops);
    res.json({ ok: true });
  });

  // POST /api/undo  body: { logPath, undo } where undo={encounterPath, updates, redo}
  // Transactional: apply updates + flip undone in one SQLite tx.
  // If `redo` true, applies redo patch instead + flips undone:false.
  app.post('/api/undo', (req, res) => {
    const { logPath, undo } = req.body || {};
    if (!logPath || !undo || !undo.encounterPath) {
      return res.status(400).json({ error: 'logPath + undo.encounterPath required' });
    }
    if (!store.transactionalUndo) {
      return res.status(501).json({ error: 'transactionalUndo not supported by store' });
    }
    try {
      const isRedo = !!req.body.redo;
      const updates = isRedo ? (undo.redo || undo.updates) : undo.updates;
      const result = store.transactionalUndo({
        logPath,
        encounterPath: undo.encounterPath,
        updates,
        undoneFlag: !isRedo,
      });
      res.json({ ok: true, ...result });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  // --- WebSocket: subscribe by path ---
  const server = http.createServer(app);
  const wss = new WebSocketServer({ server, path: '/ws' });

  wss.on('connection', (ws) => {
    ws.on('message', (raw) => {
      let msg;
      try { msg = JSON.parse(raw.toString()); } catch { ws.send(JSON.stringify({ type: 'error', error: 'bad json' })); return; }
      if (msg.type === 'subscribe' && msg.path) {
        if (msg.kind === 'collection') addSub(collSubscribers, msg.path, ws);
        else addSub(docSubscribers, msg.path, ws);
        ws.send(JSON.stringify({ type: 'subscribed', path: msg.path, kind: msg.kind || 'doc' }));
      } else if (msg.type === 'unsubscribe' && msg.path) {
        if (msg.kind === 'collection') removeSub(collSubscribers, msg.path, ws);
        else removeSub(docSubscribers, msg.path, ws);
      }
    });
    ws.on('close', () => dropWs(ws));
    ws.on('error', () => {});
  });

  return {
    app, server, wss, store, db,
    close(done) {
      wss.clients.forEach(c => { try { c.terminate(); } catch {} });
      wss.close();
      server.close(() => { db.close(); if (done) done(); });
    },
  };
}

// Boot standalone if run directly.
if (require.main === module) {
  const port = parseInt(process.env.PORT, 10) || 4001;
  const dbPath = process.env.DB_PATH || './data/tracker.sqlite';
  const { server } = createServer({ dbPath, port });
  server.listen(port, () => {
    console.log(`ttrpg backend listening on :${port} (db: ${dbPath})`);
  });
}

module.exports = { createServer };
