/**
 * Founder broadcast / user inbox store.
 * Posts: announcement | ad | enquiry | vote
 * Auth: ML-DSA-87 signatures verified via web_node wallet_crypto (same as APK).
 */
const fs = require("fs");
const path = require("path");
const { pathToFileURL } = require("url");
const { dataPath } = require("./data_dir");

const FOUNDER_ADDRESS = "SRMDBdpeTwAUKQjYsWsnsDGhNC1merWh6MV3r7uM6N4Yet";
const STORE_DIR = dataPath("broadcast_store");
const POSTS_FILE = path.join(STORE_DIR, "posts.json");
const VOTES_FILE = path.join(STORE_DIR, "votes.json");
const REPLIES_FILE = path.join(STORE_DIR, "replies.json");
const MAX_BODY = 4000;
const MAX_TITLE = 120;
const MAX_OPTION = 80;
const MAX_OPTIONS = 5;
const MAX_REPLY = 280;
const TYPES = new Set(["announcement", "ad", "enquiry", "vote"]);

/** @type {any} */
let cryptoMod = null;
/** @type {Array<any>} */
let posts = [];
/** @type {Record<string, Record<string, { optionIndex: number, at: number }>>} */
let votes = {};
/** @type {Record<string, Record<string, { text: string, optionIndex: number|null, at: number }>>} */
let replies = {};

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

function loadJson(file, fallback) {
  try {
    if (!fs.existsSync(file)) return fallback;
    return JSON.parse(fs.readFileSync(file, "utf8"));
  } catch (_) {
    return fallback;
  }
}

function saveJson(file, data) {
  ensureDir();
  // tmp+rename so a crash mid-write cannot truncate posts/votes/replies.
  const tmp = `${file}.tmp-${process.pid}`;
  fs.writeFileSync(tmp, JSON.stringify(data, null, 0));
  fs.renameSync(tmp, file);
}

function loadAll() {
  ensureDir();
  posts = loadJson(POSTS_FILE, []);
  if (!Array.isArray(posts)) posts = [];
  votes = loadJson(VOTES_FILE, {}) || {};
  replies = loadJson(REPLIES_FILE, {}) || {};
}

function persist() {
  saveJson(POSTS_FILE, posts);
  saveJson(VOTES_FILE, votes);
  saveJson(REPLIES_FILE, replies);
}

loadAll();

async function loadCrypto() {
  if (cryptoMod) return cryptoMod;
  const candidates = [
    path.join(__dirname, "web_node", "js", "wallet_crypto.js"),
    path.join(__dirname, "..", "web_node", "js", "wallet_crypto.js"),
  ];
  for (const p of candidates) {
    if (fs.existsSync(p)) {
      cryptoMod = await import(pathToFileURL(p).href);
      return cryptoMod;
    }
  }
  throw new Error("wallet_crypto.js not found");
}

/** Stable UTF-8 bytes for signing (must match Android). */
function canonicalBytes(obj) {
  return Buffer.from(stableStringify(obj), "utf8");
}

function stableStringify(value) {
  if (value === null || typeof value !== "object") {
    return JSON.stringify(value);
  }
  if (Array.isArray(value)) {
    return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
  }
  const keys = Object.keys(value).sort();
  return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k])).join(",") + "}";
}

async function verifySigned(address, publicKeyHex, signatureHex, payloadObj) {
  const crypto = await loadCrypto();
  const pk = crypto.hexToBytes(String(publicKeyHex || ""));
  const sig = crypto.hexToBytes(String(signatureHex || ""));
  if (!pk.length || !sig.length) return { ok: false, error: "missing key or signature" };
  const derived = crypto.publicKeyToAddress(pk);
  if (derived !== address) return { ok: false, error: "address/pubkey mismatch" };
  const data = new Uint8Array(canonicalBytes(payloadObj));
  const ok = crypto.verify(data, sig, pk);
  if (!ok) return { ok: false, error: "bad signature" };
  return { ok: true };
}

function newId() {
  return `b_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
}

function tally(postId) {
  const map = votes[postId] || {};
  const counts = {};
  for (const v of Object.values(map)) {
    const i = String(v.optionIndex);
    counts[i] = (counts[i] || 0) + 1;
  }
  return counts;
}

function viewPost(post, viewerAddress, isFounder) {
  const id = post.id;
  const myVote = viewerAddress && votes[id] ? votes[id][viewerAddress] : null;
  const myReply = viewerAddress && replies[id] ? replies[id][viewerAddress] : null;
  const showResults =
    isFounder ||
    post.resultsVisibility === "public" ||
    post.type !== "vote";
  const out = {
    id: post.id,
    type: post.type,
    title: post.title,
    body: post.body,
    createdAt: post.createdAt,
    options: post.options || [],
    allowFreeText: !!post.allowFreeText,
    resultsVisibility: post.resultsVisibility || "public",
    closed: !!post.closed,
    myVoteOptionIndex: myVote ? myVote.optionIndex : null,
    myReplyText: myReply ? myReply.text : null,
    myReplyOptionIndex: myReply && myReply.optionIndex != null ? myReply.optionIndex : null,
  };
  if (post.type === "vote" || post.type === "enquiry") {
    if (showResults || isFounder) {
      out.tally = tally(id);
      out.voteCount = Object.keys(votes[id] || {}).length;
    } else {
      out.tally = null;
      out.voteCount = null;
    }
  }
  if (isFounder && (post.type === "enquiry" || post.type === "vote")) {
    out.replies = Object.entries(replies[id] || {}).map(([addr, r]) => ({
      address: addr,
      text: r.text || "",
      optionIndex: r.optionIndex,
      at: r.at,
    }));
  }
  return out;
}

function listForViewer(viewerAddress, isFounder) {
  const sorted = posts.slice().sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
  return sorted.map((p) => viewPost(p, viewerAddress || "", !!isFounder));
}

function getForViewer(id, viewerAddress, isFounder) {
  const post = posts.find((p) => p.id === id);
  if (!post) return null;
  return viewPost(post, viewerAddress || "", !!isFounder);
}

/**
 * Founder publish. Body must include address, publicKey, signature, and fields.
 * Signature covers: { op:"publish", type, title, body, options, allowFreeText, resultsVisibility, createdAt }
 */
async function publish(body) {
  const address = String(body.address || "");
  const publicKey = String(body.publicKey || "");
  const signature = String(body.signature || "");
  if (address !== FOUNDER_ADDRESS) return { ok: false, error: "founder only", code: 403 };

  const type = String(body.type || "").toLowerCase();
  if (!TYPES.has(type)) return { ok: false, error: "invalid type", code: 400 };

  const title = String(body.title || "").trim().slice(0, MAX_TITLE);
  const postBody = String(body.body || "").trim().slice(0, MAX_BODY);
  if (!title) return { ok: false, error: "title required", code: 400 };

  let options = Array.isArray(body.options)
    ? body.options.map((o) => String(o || "").trim().slice(0, MAX_OPTION)).filter(Boolean)
    : [];
  if (options.length > MAX_OPTIONS) options = options.slice(0, MAX_OPTIONS);

  const allowFreeText = !!body.allowFreeText;
  let resultsVisibility = String(body.resultsVisibility || "public").toLowerCase();
  if (resultsVisibility !== "founder_only") resultsVisibility = "public";

  if (type === "vote") {
    if (options.length < 2) return { ok: false, error: "vote needs 2–5 options", code: 400 };
  }
  if (type === "enquiry") {
    if (!allowFreeText && options.length < 1) {
      return { ok: false, error: "enquiry needs options or free text", code: 400 };
    }
  }
  if (type === "announcement" || type === "ad") {
    options = [];
  }

  const createdAt = Number(body.createdAt) || Date.now();
  const payload = {
    op: "publish",
    type,
    title,
    body: postBody,
    options,
    allowFreeText,
    resultsVisibility,
    createdAt,
  };
  const auth = await verifySigned(address, publicKey, signature, payload);
  if (!auth.ok) return { ok: false, error: auth.error || "auth failed", code: 403 };

  const post = {
    id: newId(),
    type,
    title,
    body: postBody,
    options,
    allowFreeText,
    resultsVisibility: type === "vote" ? resultsVisibility : "public",
    closed: false,
    createdAt,
    founderAddress: address,
  };
  posts.push(post);
  votes[post.id] = {};
  replies[post.id] = {};
  persist();
  return { ok: true, post: viewPost(post, address, true) };
}

async function closePost(id, body) {
  const address = String(body.address || "");
  if (address !== FOUNDER_ADDRESS) return { ok: false, error: "founder only", code: 403 };
  const post = posts.find((p) => p.id === id);
  if (!post) return { ok: false, error: "not found", code: 404 };
  const closedAt = Number(body.closedAt) || Date.now();
  const payload = { op: "close", id, closedAt };
  const auth = await verifySigned(
    address,
    String(body.publicKey || ""),
    String(body.signature || ""),
    payload
  );
  if (!auth.ok) return { ok: false, error: auth.error || "auth failed", code: 403 };
  post.closed = true;
  post.closedAt = closedAt;
  persist();
  return { ok: true, post: viewPost(post, address, true) };
}

async function vote(id, body) {
  const address = String(body.address || "");
  const publicKey = String(body.publicKey || "");
  const signature = String(body.signature || "");
  const optionIndex = Number(body.optionIndex);
  const post = posts.find((p) => p.id === id);
  if (!post) return { ok: false, error: "not found", code: 404 };
  if (post.closed) return { ok: false, error: "closed", code: 400 };
  if (post.type !== "vote" && post.type !== "enquiry") {
    return { ok: false, error: "not votable", code: 400 };
  }
  if (!Number.isInteger(optionIndex) || optionIndex < 0 || optionIndex >= (post.options || []).length) {
    return { ok: false, error: "bad option", code: 400 };
  }
  if (!address.startsWith("SRMD")) return { ok: false, error: "bad address", code: 400 };
  if (votes[id] && votes[id][address]) return { ok: false, error: "already voted", code: 409 };

  const at = Number(body.at) || Date.now();
  const payload = { op: "vote", id, optionIndex, at };
  const auth = await verifySigned(address, publicKey, signature, payload);
  if (!auth.ok) return { ok: false, error: auth.error || "auth failed", code: 403 };

  if (!votes[id]) votes[id] = {};
  votes[id][address] = { optionIndex, at };
  persist();
  const isFounder = address === FOUNDER_ADDRESS;
  return { ok: true, post: viewPost(post, address, isFounder) };
}

async function reply(id, body) {
  const address = String(body.address || "");
  const publicKey = String(body.publicKey || "");
  const signature = String(body.signature || "");
  const text = String(body.text || "").trim().slice(0, MAX_REPLY);
  let optionIndex =
    body.optionIndex === null || body.optionIndex === undefined || body.optionIndex === ""
      ? null
      : Number(body.optionIndex);
  const post = posts.find((p) => p.id === id);
  if (!post) return { ok: false, error: "not found", code: 404 };
  if (post.closed) return { ok: false, error: "closed", code: 400 };
  if (post.type !== "enquiry") return { ok: false, error: "not enquiry", code: 400 };
  if (!address.startsWith("SRMD")) return { ok: false, error: "bad address", code: 400 };

  if (optionIndex != null) {
    if (
      !Number.isInteger(optionIndex) ||
      optionIndex < 0 ||
      optionIndex >= (post.options || []).length
    ) {
      return { ok: false, error: "bad option", code: 400 };
    }
  } else if (!post.allowFreeText || !text) {
    if ((post.options || []).length > 0 && optionIndex == null && !text) {
      return { ok: false, error: "choose an option or write a reply", code: 400 };
    }
    if (!post.allowFreeText) return { ok: false, error: "free text not allowed", code: 400 };
    if (!text) return { ok: false, error: "text required", code: 400 };
  }

  if (replies[id] && replies[id][address]) return { ok: false, error: "already replied", code: 409 };
  // Also block if they already voted via vote endpoint for enquiry options
  if (votes[id] && votes[id][address] && optionIndex != null) {
    return { ok: false, error: "already voted", code: 409 };
  }

  const at = Number(body.at) || Date.now();
  const payload = {
    op: "reply",
    id,
    text,
    optionIndex: optionIndex == null ? null : optionIndex,
    at,
  };
  const auth = await verifySigned(address, publicKey, signature, payload);
  if (!auth.ok) return { ok: false, error: auth.error || "auth failed", code: 403 };

  if (!replies[id]) replies[id] = {};
  replies[id][address] = { text, optionIndex, at };
  if (optionIndex != null) {
    if (!votes[id]) votes[id] = {};
    if (!votes[id][address]) votes[id][address] = { optionIndex, at };
  }
  persist();
  const isFounder = address === FOUNDER_ADDRESS;
  return { ok: true, post: viewPost(post, address, isFounder) };
}

function statusFields() {
  const latest = posts.reduce((m, p) => Math.max(m, p.createdAt || 0), 0);
  return {
    broadcastCount: posts.length,
    latestBroadcastAt: latest || 0,
  };
}

function resetBroadcasts() {
  posts = [];
  votes = {};
  replies = {};
  persist();
}

module.exports = {
  FOUNDER_ADDRESS,
  listForViewer,
  getForViewer,
  publish,
  closePost,
  vote,
  reply,
  statusFields,
  resetBroadcasts,
  canonicalBytes,
  stableStringify,
};
