/* global React */
// ───────────────────────────────────────────────────────────────────
// Flux Discounts — client data layer (API-backed)
// Talks to the Node/Express backend (server/) over same-origin fetch:
//   • /api/admin/*  — dashboard CRUD, gated by the Discord session cookie
//   • /api/v1/*     — the scraper ingestion API (used by the live console)
// The public surface (useFlux, fluxActions, formatters, CATEGORIES, …) is the
// same as the old localStorage version, so the view components are unchanged.
// ───────────────────────────────────────────────────────────────────
const { useState, useEffect } = React;

const CURRENCY_DEFAULT = "AUD";

// ---------- id / time helpers (kept for parity / exports) ----------
function genId(prefix) {
  const r = (crypto.getRandomValues(new Uint32Array(2)));
  return `${prefix}_${r[0].toString(36)}${r[1].toString(36)}`;
}
function genKey() {
  const bytes = crypto.getRandomValues(new Uint8Array(24));
  const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
  return `flux_live_sk_${hex}`;
}
const nowISO = () => new Date().toISOString();

// ---------- formatting ----------
function fmtMoney(v, cur = CURRENCY_DEFAULT) {
  if (v == null || isNaN(v)) return "—";
  try {
    return new Intl.NumberFormat("en-AU", { style: "currency", currency: cur, maximumFractionDigits: 2 }).format(v);
  } catch {
    return `$${Number(v).toFixed(2)}`;
  }
}
function timeAgo(iso) {
  const s = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
  if (s < 5) return "just now";
  if (s < 60) return `${s}s ago`;
  const m = Math.floor(s / 60);
  if (m < 60) return `${m}m ago`;
  const h = Math.floor(m / 60);
  if (h < 24) return `${h}h ago`;
  const d = Math.floor(h / 24);
  if (d < 7) return `${d}d ago`;
  return new Date(iso).toLocaleDateString("en-AU", { day: "numeric", month: "short" });
}
function pctOff(orig, sale) {
  if (!orig || orig <= 0 || sale == null) return 0;
  return Math.max(0, Math.round((1 - sale / orig) * 100));
}

const CATEGORIES = ["Sneakers", "Electronics", "Gaming", "Toys & Collectibles", "Streetwear", "Home", "Fashion", "Other"];
const RETAILER_LIST = [
  "Target AU", "Big W", "Kmart AU", "JB Hi-Fi", "EB Games", "Hype DC",
  "Foot Locker", "Culture Kings", "Shopify", "Above The Clouds", "Finesse", "Sneaker LAH",
];

// Discord embed builder (kept for export parity; the server builds its own).
function buildEmbed(d, event) {
  const priceLine = d.price_original
    ? `~~${fmtMoney(d.price_original, d.currency)}~~ → **${fmtMoney(d.price_sale, d.currency)}**`
    : `**${fmtMoney(d.price_sale, d.currency)}**`;
  const fields = [
    { name: "Price", value: priceLine, inline: true },
    { name: "Discount", value: `${d.discount_pct}% off`, inline: true },
    { name: "Retailer", value: d.retailer || "—", inline: true },
  ];
  if (d.code) fields.push({ name: "Code", value: "`" + d.code + "`", inline: true });
  return {
    username: "Flux Discounts",
    embeds: [{
      title: (event === "price_drop" ? "⬇ Price drop · " : "") + d.title,
      url: d.url,
      description: d.description || undefined,
      color: event === "price_drop" ? 0x2ee07a : 0x6366f1,
      fields,
      thumbnail: d.image ? { url: d.image } : undefined,
      footer: { text: `${d.category} · Flux Discounts` },
      timestamp: new Date().toISOString(),
    }],
  };
}

// ───────────────────────────────────────────────────────────────────
// Store (subscribe / in-memory cache hydrated from the server)
// ───────────────────────────────────────────────────────────────────
let state = {
  booting: true,
  discounts: [],
  apiKeys: [],
  webhooks: [],
  deliveries: [],
  session: { signedIn: false, user: { name: "", role: "" } },
  settings: { apiBase: "", currency: CURRENCY_DEFAULT },
};
const subs = new Set();
function emit() { subs.forEach((f) => f()); }

function useFlux() {
  const [, force] = useState(0);
  useEffect(() => {
    const f = () => force((x) => x + 1);
    subs.add(f);
    return () => subs.delete(f);
  }, []);
  return state;
}

// ---------- fetch helper (same-origin; sends the session cookie) ----------
async function api(path, { method = "GET", body } = {}) {
  const res = await fetch(path, {
    method,
    headers: body !== undefined ? { "Content-Type": "application/json" } : {},
    body: body !== undefined ? JSON.stringify(body) : undefined,
    credentials: "same-origin",
  });
  let json = null;
  try { json = await res.json(); } catch {}
  return { ok: res.ok, status: res.status, body: json };
}
const toastErr = (r, fallback) => {
  const msg = (r && r.body && (r.body.message || r.body.error)) || fallback;
  if (window.toast) window.toast(msg, "error");
};

// ---------- hydrate from the backend ----------
async function bootstrap() {
  const r = await api("/api/admin/bootstrap");
  if (r.ok && r.body) {
    const u = r.body.user || {};
    state = {
      booting: false,
      discounts: r.body.discounts || [],
      apiKeys: r.body.apiKeys || [],
      webhooks: r.body.webhooks || [],
      deliveries: r.body.deliveries || [],
      session: { signedIn: true, user: { name: u.username || "Owner", role: "Owner", id: u.id } },
      settings: { ...state.settings, ...(r.body.settings || {}) },
    };
  } else {
    state = { ...state, booting: false, session: { signedIn: false, user: { name: "", role: "" } } };
  }
  emit();
}
// Re-pull server-owned collections after an action that mutates them out of band
// (e.g. the live console ingest, which also fires webhooks server-side).
async function refresh() {
  const r = await api("/api/admin/bootstrap");
  if (r.ok && r.body) {
    state = {
      ...state,
      discounts: r.body.discounts || [],
      apiKeys: r.body.apiKeys || [],
      webhooks: r.body.webhooks || [],
      deliveries: r.body.deliveries || [],
      settings: { ...state.settings, ...(r.body.settings || {}) },
    };
    emit();
  }
}

// ───────────────────────────────────────────────────────────────────
// The ingest contract — real POST to the scraper API (live console)
// ───────────────────────────────────────────────────────────────────
async function ingest(payload, rawKey) {
  const res = await fetch("/api/v1/discounts", {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-API-Key": rawKey || "" },
    body: JSON.stringify(payload),
  });
  let body = null;
  try { body = await res.json(); } catch { body = { error: "bad_response" }; }
  if (res.ok) refresh(); // reflect the new/updated discount + deliveries in the UI
  return { ok: res.ok, status: res.status, body };
}

// ───────────────────────────────────────────────────────────────────
// Actions
// ───────────────────────────────────────────────────────────────────
const actions = {
  signIn() { window.location.href = "/auth/discord/login"; },
  signOut() { window.location.href = "/auth/discord/logout"; },

  async upsertDiscount(d) {
    const payload = { ...d };
    const r = d.id
      ? await api(`/api/admin/discounts/${d.id}`, { method: "PUT", body: payload })
      : await api("/api/admin/discounts", { method: "POST", body: payload });
    if (!r.ok || !r.body || !r.body.discount) { toastErr(r, "Could not save discount"); return null; }
    const saved = r.body.discount;
    const i = state.discounts.findIndex((x) => x.id === saved.id);
    if (i >= 0) state.discounts[i] = saved; else state.discounts.unshift(saved);
    emit();
    return saved.id;
  },
  async deleteDiscount(id) {
    const r = await api(`/api/admin/discounts/${id}`, { method: "DELETE" });
    if (!r.ok) return toastErr(r, "Could not delete discount");
    state.discounts = state.discounts.filter((x) => x.id !== id);
    emit();
  },

  async createKey(name) {
    const r = await api("/api/admin/keys", { method: "POST", body: { name: name || "new-scraper" } });
    if (!r.ok || !r.body || !r.body.key) { toastErr(r, "Could not create key"); return null; }
    state.apiKeys.unshift(r.body.key);
    emit();
    return r.body.key;
  },
  async revokeKey(id) {
    const r = await api(`/api/admin/keys/${id}/revoke`, { method: "POST" });
    if (!r.ok) return toastErr(r, "Could not revoke key");
    const k = state.apiKeys.find((x) => x.id === id); if (k) k.revoked = true;
    emit();
  },
  async deleteKey(id) {
    const r = await api(`/api/admin/keys/${id}`, { method: "DELETE" });
    if (!r.ok) return toastErr(r, "Could not delete key");
    state.apiKeys = state.apiKeys.filter((x) => x.id !== id);
    emit();
  },

  async upsertWebhook(w) {
    const r = w.id
      ? await api(`/api/admin/webhooks/${w.id}`, { method: "PATCH", body: w })
      : await api("/api/admin/webhooks", { method: "POST", body: w });
    if (!r.ok || !r.body || !r.body.webhook) return toastErr(r, "Could not save webhook");
    const saved = r.body.webhook;
    const i = state.webhooks.findIndex((x) => x.id === saved.id);
    if (i >= 0) state.webhooks[i] = saved; else state.webhooks.unshift(saved);
    emit();
  },
  async deleteWebhook(id) {
    const r = await api(`/api/admin/webhooks/${id}`, { method: "DELETE" });
    if (!r.ok) return toastErr(r, "Could not delete webhook");
    state.webhooks = state.webhooks.filter((x) => x.id !== id);
    emit();
  },
  async toggleWebhook(id) {
    const r = await api(`/api/admin/webhooks/${id}/toggle`, { method: "POST" });
    if (!r.ok || !r.body || !r.body.webhook) return toastErr(r, "Could not toggle webhook");
    const i = state.webhooks.findIndex((x) => x.id === id);
    if (i >= 0) state.webhooks[i] = r.body.webhook;
    emit();
  },
  async testWebhook(id) {
    const r = await api(`/api/admin/webhooks/${id}/test`, { method: "POST" });
    if (!r.ok || !r.body) { toastErr(r, "Test failed"); return { ok: false }; }
    if (r.body.delivery) state.deliveries.unshift(r.body.delivery);
    emit();
    const res = r.body.result || {};
    return { ok: !!res.ok, code: res.code, simulated: res.simulated };
  },
  async clearDeliveries() {
    const r = await api("/api/admin/deliveries", { method: "DELETE" });
    if (!r.ok) return toastErr(r, "Could not clear log");
    state.deliveries = [];
    emit();
  },

  async setSetting(k, v) {
    const r = await api("/api/admin/settings", { method: "PUT", body: { [k]: v } });
    if (r.ok && r.body && r.body.settings) state.settings = r.body.settings;
    else state.settings = { ...state.settings, [k]: v };
    emit();
  },
  // No demo seed to restore anymore — just re-pull live data from the server.
  resetAll() { refresh(); },

  ingest,
};

// Kick off hydration as soon as the data layer loads.
bootstrap();

Object.assign(window, {
  useFlux, fluxActions: actions, fmtMoney, timeAgo, pctOff, buildEmbed,
  CATEGORIES, RETAILER_LIST, genKey, genId,
});
