/**
 * Keep In Touch — subscribe capture service.
 *
 * A tiny dependency-light HTTP service (Node core http + better-sqlite3) that
 * receives JSON submissions from the docs site "Keep In Touch" widget and
 * upserts them into a SQLite database. Designed to run in a single container
 * on Coolify with a persistent volume, mirroring the Cusdis deployment.
 *
 * Endpoints:
 *   POST /api/subscribe   capture/upsert a subscriber  -> { ok: true }
 *   GET  /api/health      liveness probe               -> { ok: true }
 *   GET  /api/subscribers list (requires ADMIN_TOKEN)  -> [ ... ]
 *
 * A submission is keyed by whatsapp OR email — whichever is present. As the
 * widget collects more fields across submissions (email after whatsapp, then
 * name), each POST sends the full known set, and we merge into the existing
 * row rather than creating duplicates.
 *
 * Environment:
 *   PORT          listen port (default 8080)
 *   DB_PATH       SQLite file path (default ./data/subscribers.db)
 *   ALLOW_ORIGIN  CORS allow-origin (default "*"; set to your site origin)
 *   ADMIN_TOKEN   if set, required as `Authorization: Bearer <token>` for
 *                 GET /api/subscribers
 */

import http from "node:http";
import { dirname } from "node:path";
import { mkdirSync } from "node:fs";
import Database from "better-sqlite3";

const PORT = parseInt(process.env.PORT || "8080", 10);
const DB_PATH = process.env.DB_PATH || "./data/subscribers.db";
const ALLOW_ORIGIN = process.env.ALLOW_ORIGIN || "*";
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "";
const MAX_BODY = 16 * 1024; // 16 KB cap — these payloads are tiny.

// ── Database ────────────────────────────────────────────────────────────
mkdirSync(dirname(DB_PATH), { recursive: true });
const db = new Database(DB_PATH);
db.pragma("journal_mode = WAL");
db.pragma("busy_timeout = 5000");

db.exec(`
  CREATE TABLE IF NOT EXISTS subscribers (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    whatsapp    TEXT,
    email       TEXT,
    name        TEXT,
    first_page  TEXT,
    first_url   TEXT,
    user_agent  TEXT,
    ip          TEXT,
    created_at  TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at  TEXT NOT NULL DEFAULT (datetime('now'))
  );
  CREATE UNIQUE INDEX IF NOT EXISTS idx_subscribers_email
    ON subscribers(email) WHERE email IS NOT NULL AND email <> '';
  CREATE UNIQUE INDEX IF NOT EXISTS idx_subscribers_whatsapp
    ON subscribers(whatsapp) WHERE whatsapp IS NOT NULL AND whatsapp <> '';
`);

const findByEmail = db.prepare(
  "SELECT * FROM subscribers WHERE email = ? LIMIT 1"
);
const findByWhatsApp = db.prepare(
  "SELECT * FROM subscribers WHERE whatsapp = ? LIMIT 1"
);
const insertRow = db.prepare(`
  INSERT INTO subscribers (whatsapp, email, name, first_page, first_url, user_agent, ip)
  VALUES (@whatsapp, @email, @name, @first_page, @first_url, @user_agent, @ip)
`);
const updateRow = db.prepare(`
  UPDATE subscribers
     SET whatsapp   = COALESCE(NULLIF(@whatsapp, ''), whatsapp),
         email      = COALESCE(NULLIF(@email, ''), email),
         name       = COALESCE(NULLIF(@name, ''), name),
         updated_at = datetime('now')
   WHERE id = @id
`);
const listRows = db.prepare(
  "SELECT * FROM subscribers ORDER BY updated_at DESC LIMIT 1000"
);

// ── Validation / normalization ──────────────────────────────────────────
function normEmail(v) {
  return typeof v === "string" ? v.trim().toLowerCase() : "";
}
function normWhatsApp(v) {
  if (typeof v !== "string") return "";
  const cleaned = v.trim().replace(/[^\d+]/g, "");
  const digits = cleaned.replace(/[^\d]/g, "");
  if (digits.length < 7 || digits.length > 15) return "";
  return cleaned;
}
function isValidEmail(v) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
}
function clean(v, max = 200) {
  return typeof v === "string" ? v.trim().slice(0, max) : "";
}

// Upsert: find an existing row by email or whatsapp, merge; else insert.
const upsert = db.transaction((rec) => {
  let existing = null;
  if (rec.email) existing = findByEmail.get(rec.email);
  if (!existing && rec.whatsapp) existing = findByWhatsApp.get(rec.whatsapp);

  if (existing) {
    updateRow.run({
      id: existing.id,
      whatsapp: rec.whatsapp,
      email: rec.email,
      name: rec.name,
    });
    return { id: existing.id, created: false };
  }
  const info = insertRow.run(rec);
  return { id: info.lastInsertRowid, created: true };
});

// ── HTTP helpers ──────────────────────────────────────────────────────────
function cors(res) {
  res.setHeader("Access-Control-Allow-Origin", ALLOW_ORIGIN);
  res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
  res.setHeader("Vary", "Origin");
}
function json(res, status, obj) {
  cors(res);
  res.writeHead(status, { "Content-Type": "application/json" });
  res.end(JSON.stringify(obj));
}
function clientIp(req) {
  const xff = req.headers["x-forwarded-for"];
  if (typeof xff === "string" && xff.length) return xff.split(",")[0].trim();
  return req.socket.remoteAddress || "";
}

function readBody(req) {
  return new Promise((resolve, reject) => {
    let size = 0;
    const chunks = [];
    req.on("data", (c) => {
      size += c.length;
      if (size > MAX_BODY) {
        reject(new Error("payload too large"));
        req.destroy();
        return;
      }
      chunks.push(c);
    });
    req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
    req.on("error", reject);
  });
}

// ── Server ─────────────────────────────────────────────────────────────
const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);

  if (req.method === "OPTIONS") {
    cors(res);
    res.writeHead(204);
    res.end();
    return;
  }

  if (req.method === "GET" && url.pathname === "/api/health") {
    return json(res, 200, { ok: true });
  }

  if (req.method === "GET" && url.pathname === "/api/subscribers") {
    if (ADMIN_TOKEN) {
      const auth = req.headers["authorization"] || "";
      if (auth !== `Bearer ${ADMIN_TOKEN}`) {
        return json(res, 401, { ok: false, error: "unauthorized" });
      }
    } else {
      // No admin token configured — refuse rather than leak data.
      return json(res, 403, { ok: false, error: "admin token not configured" });
    }
    return json(res, 200, { ok: true, subscribers: listRows.all() });
  }

  if (req.method === "POST" && url.pathname === "/api/subscribe") {
    let raw;
    try {
      raw = await readBody(req);
    } catch (e) {
      return json(res, 413, { ok: false, error: "payload too large" });
    }

    let data;
    try {
      data = JSON.parse(raw || "{}");
    } catch (e) {
      return json(res, 400, { ok: false, error: "invalid JSON" });
    }

    const email = normEmail(data.email);
    const whatsapp = normWhatsApp(data.whatsapp);

    if (email && !isValidEmail(email)) {
      return json(res, 422, { ok: false, error: "invalid email" });
    }
    if (!email && !whatsapp) {
      return json(res, 422, {
        ok: false,
        error: "provide a valid email or whatsapp number",
      });
    }

    const rec = {
      whatsapp,
      email,
      name: clean(data.name, 120),
      first_page: clean(data.page, 300),
      first_url: clean(data.url, 500),
      user_agent: clean(req.headers["user-agent"], 400),
      ip: clientIp(req),
    };

    try {
      const result = upsert(rec);
      return json(res, 200, { ok: true, id: result.id, created: result.created });
    } catch (e) {
      console.error("upsert failed:", e);
      return json(res, 500, { ok: false, error: "server error" });
    }
  }

  json(res, 404, { ok: false, error: "not found" });
});

server.listen(PORT, () => {
  console.log(`[subscribe] listening on :${PORT} (db: ${DB_PATH})`);
});

// Graceful shutdown so SQLite WAL checkpoints cleanly.
function shutdown() {
  server.close(() => {
    try { db.close(); } catch {}
    process.exit(0);
  });
}
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
