// =================================================================
// pw-api.jsx — switchable data layer for the Operator Dashboard.
//
//   window.__api  — the singleton client.
//
//   Mock mode  → operates on the in-memory store (the App's `data`
//                React state, bound via api.bindStore()).
//   Live mode  → real fetch() against the active environment's BFF,
//                with Bearer session auth, X-Acting-Role, Idempotency-Key
//                on writes, error-envelope parsing, and 401 → sign-out.
//
//   Reads:   api.read(name, {params, query})         → Promise<data>
//   Writes:  api.write(name, optimistic, {body,...})  → Promise<res>
//            (optimistic updater is applied to the store immediately;
//             on a live error it's rolled back and the envelope toasted.)
//   Boot:    api.bootstrap()  → refresh every collection from the endpoint.
//
//   Switching mode / environment / baseUrl is runtime — call
//   api.setConfig({...}); the app re-bootstraps and the UI "goes live".
// =================================================================

(() => {
  const CFG = window.PAYWALL_API;
  const CONN_KEY = "krafts.paywall.conn.v1";

  // ---- tiny event emitter ----
  const listeners = { change: [], log: [], authError: [], status: [] };
  const emit = (ev, payload) => (listeners[ev] || []).forEach(fn => { try { fn(payload); } catch (e) {} });
  const on = (ev, fn) => { (listeners[ev] = listeners[ev] || []).push(fn); return () => { listeners[ev] = listeners[ev].filter(f => f !== fn); }; };

  const uuid = () => (crypto.randomUUID ? crypto.randomUUID() : "id-" + Date.now() + "-" + Math.random().toString(16).slice(2));

  // ---- connection config (persisted) ----
  const loadConn = () => {
    const def = { mode: CFG.mode, environment: CFG.defaultEnvironment, overrideUrl: "" };
    try { const r = localStorage.getItem(CONN_KEY); return r ? { ...def, ...JSON.parse(r) } : def; } catch (e) { return def; }
  };
  let conn = loadConn();
  const saveConn = () => { try { localStorage.setItem(CONN_KEY, JSON.stringify(conn)); } catch (e) {} };
  const resolvedBaseUrl = () => conn.overrideUrl?.trim() || (CFG.environments[conn.environment]?.baseUrl) || "";
  const getConfig = () => ({ ...conn, baseUrl: resolvedBaseUrl(), live: conn.mode === "live" });

  let connectionStatus = { state: conn.mode === "live" ? "unknown" : "mock", detail: "" };
  const setStatus = (s) => { connectionStatus = s; emit("status", s); };

  // ---- store binding (the App's data state) ----
  let store = { get: () => (window.__pwData || {}), set: () => {} };
  const bindStore = (s) => { store = s; };

  // ---- request log (ring buffer) ----
  const log = [];
  const pushLog = (entry) => { log.unshift(entry); if (log.length > 60) log.pop(); emit("log", entry); };
  const getLog = () => log.slice();

  // ---- endpoint resolution across endpoints + session maps ----
  const findEndpoint = (name) => CFG.endpoints[name] || CFG.session[name] || null;
  const interpolate = (url, params) => url.replace(/\{(\w+)\}/g, (m, k) => (params && params[k] != null ? encodeURIComponent(params[k]) : m));
  const isWrite = (method) => ["POST", "PUT", "PATCH", "DELETE"].includes(method);

  class PaywallApiError extends Error {
    constructor(message, status, code, requestId) { super(message); this.name = "PaywallApiError"; this.status = status; this.code = code; this.requestId = requestId; }
  }

  const normalizeRequestBody = (body) => {
    if (!body || typeof body !== "object" || Array.isArray(body)) return body;
    const out = { ...body };
    for (const key of ["valid_from", "valid_until"]) {
      if (/^\d{4}-\d{2}-\d{2}$/.test(String(out[key] || ""))) out[key] = `${out[key]}T00:00:00.000Z`;
    }
    if (typeof out.eligible_policy === "string") out.eligible_policy = { type: out.eligible_policy };
    return out;
  };

  // ---- low-level fetch (live) ----
  const fetchLive = async (def, { params, query, body, idempotencyKey } = {}) => {
    const base = resolvedBaseUrl();
    let url = base + interpolate(def.url, params);
    if (query && Object.keys(query).length) url += (url.includes("?") ? "&" : "?") + new URLSearchParams(query).toString();

    const session = window.__session?.get?.();
    const role = window.__actingRole || "publisher";
    const headers = { ...CFG.defaults.headers };
    if (session?.token) headers[CFG.auth.header] = `${CFG.auth.scheme} ${session.token}`;
    headers[CFG.auth.roleHeader] = role;
    headers["X-Environment"] = conn.environment;
    if (isWrite(def.method)) headers["Idempotency-Key"] = idempotencyKey || uuid();

    const ctrl = new AbortController();
    const timer = setTimeout(() => ctrl.abort(), CFG.defaults.timeoutMs);
    const started = performance.now();
    let res, payload = null;
    try {
      res = await fetch(url, {
        method: def.method, headers, credentials: CFG.defaults.credentials, signal: ctrl.signal,
        body: body != null && def.method !== "GET" ? JSON.stringify(normalizeRequestBody(body)) : undefined,
      });
    } catch (e) {
      clearTimeout(timer);
      pushLog({ id: uuid(), t: Date.now(), mode: "live", method: def.method, url, status: 0, ok: false, ms: Math.round(performance.now() - started), error: e.name === "AbortError" ? "timeout" : "network" });
      setStatus({ state: "error", detail: e.name === "AbortError" ? "Request timed out" : "Network unreachable" });
      throw new PaywallApiError(e.name === "AbortError" ? "Request timed out" : "Network error — endpoint unreachable", 0, "network");
    }
    clearTimeout(timer);
    try { payload = await res.json(); } catch (e) {}
    pushLog({ id: uuid(), t: Date.now(), mode: "live", method: def.method, url, status: res.status, ok: res.ok, ms: Math.round(performance.now() - started), requestId: payload?.request_id });

    if (res.status === 401 || res.status === 403) { emit("authError", { status: res.status }); }
    if (!res.ok) {
      const env = payload?.error || {};
      throw new PaywallApiError(env.message || res.statusText || "Request failed", res.status, env.code, payload?.request_id);
    }
    setStatus({ state: "live", detail: `Connected · ${conn.environment}` });
    return payload;
  };

  // ---- mock latency ----
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));
  const mockLatency = () => 90 + Math.random() * 230;

  // ---- which store key each list/read endpoint maps to ----
  const titleize = (s) => String(s || "").replace(/[_-]/g, " ").replace(/\b\w/g, c => c.toUpperCase());
  const featureList = (features) => Array.isArray(features) ? features : Object.values(features || {}).map(String);
  const normalizeCode = (c) => ({ ...c, brand: c.brand || c.advertiser_name || c.advertisers?.name || "Brand", publisher_ids: c.publisher_ids || [] });
  const normalizeOffer = (o) => ({ ...o, total_limit: o.total_limit ?? 0, code_id: o.code_id || "", claimed_count: o.claimed_count ?? 0 });
  const normalizeUser = (u) => ({ ...u, id: u.id || u.user_id, ledger: u.ledger || [] });
  const normalizeSubscriber = (s) => ({
    ...s,
    id: s.id || s.subscription_id,
    user: s.user || s.user_email || s.email || s.user_id,
    plan: s.plan || s.plan_name || s.plan_id || "Plan",
  });
  const normalizeRule = (r) => ({ ...r, label: r.label || titleize(r.event_type), daily_cap: r.daily_cap ?? 0 });
  const normalizePublisher = (p) => {
    const onboarding = p.onboarding || {};
    return {
      ...p,
      id: p.id || p.publisher_id,
      plan_code: p.plan_code || "starter",
      subscribers: Number(p.subscribers || 0),
      onboarding: {
        enabled: Boolean(onboarding.enabled || false),
        welcome_coins_amount: Number(onboarding.welcome_coins_amount || 0),
        welcome_coins_reason: onboarding.welcome_coins_reason || "Welcome bonus",
        eligible_policy: typeof onboarding.eligible_policy === "string" ? onboarding.eligible_policy : (onboarding.eligible_policy?.type || "new_users"),
      },
      gateways: p.gateways || [],
    };
  };

  const READ_MAP = {
    getPublisherSubscription: { key: "publisherSubscription", pick: p => (p.subscriptions || [])[0] || p },
    listPlatformPlans: { key: "platformPlans", pick: p => (p.plans || []).map(x => ({ ...x, features: featureList(x.features) })) },
    listPlans: { key: "consumerPlans", pick: p => p.plans || [] },
    listSubscribers: { key: "subscribers", pick: p => (p.subscribers || []).map(normalizeSubscriber) },
    listEngagementRules: { key: "engagementRules", pick: p => (p.rules || []).map(normalizeRule) },
    listUsers: { key: "users", pick: p => (p.users || []).map(normalizeUser) },
    listCouponOffers: { key: "couponOffers", pick: p => (p.offers || []).map(normalizeOffer) },
    listAdTiers: { key: "adTiers", pick: p => p.tiers || p.ad_tiers || [] },
    getAdConfig: { key: "adConfig", pick: p => p.config || p.ad_config || p },
    listStoreDiscountCodes: { key: "storeDiscountCodes", pick: p => (p.codes || []).map(normalizeCode) },
    listAdvertisers: { key: "advertisers", pick: p => p.advertisers || [] },
    listDiscountCodes: { key: "discountTemplates", pick: p => (p.codes || []).map(normalizeCode) },
    listPublishers: { key: "publishers", pick: p => (p.publishers || []).map(normalizePublisher) },
  };

  // ---- core: read ----
  const read = async (name, opts = {}) => {
    const def = findEndpoint(name);
    if (!def) throw new Error(`[api] Unknown endpoint "${name}"`);
    if (conn.mode === "live") {
      const payload = await fetchLive(def, opts);
      const map = READ_MAP[name];
      return map?.pick ? map.pick(payload || {}) : payload;
    }
    // mock
    await sleep(mockLatency());
    pushLog({ id: uuid(), t: Date.now(), mode: "mock", method: def.method, url: interpolate(def.url, opts.params), status: 200, ok: true, ms: 0 });
    const map = READ_MAP[name];
    if (map) return store.get()[map.key];
    return null;
  };

  // ---- core: call (generic, no optimism) ----
  const call = async (name, opts = {}) => {
    const def = findEndpoint(name);
    if (!def) throw new Error(`[api] Unknown endpoint "${name}"`);
    if (conn.mode === "live") return fetchLive(def, opts);
    await sleep(mockLatency());
    pushLog({ id: uuid(), t: Date.now(), mode: "mock", method: def.method, url: interpolate(def.url, opts.params), status: 200, ok: true, ms: 0 });
    return { ok: true, mock: true };
  };

  // ---- core: write (optimistic + live) ----
  // optimistic: function(prevData) => nextData   (applied immediately)
  // opts: { params, body, ok, err, idempotencyKey }
  const write = async (name, optimistic, opts = {}) => {
    const def = findEndpoint(name);
    if (!def) throw new Error(`[api] Unknown endpoint "${name}"`);
    const prev = store.get();
    if (optimistic) store.set(optimistic);              // instant UI in both modes
    try {
      const res = await call(name, opts);
      if (opts.ok) window.__toast(opts.ok, "good");
      return res;
    } catch (e) {
      if (optimistic) store.set(() => prev);            // rollback on live failure
      const msg = e.code ? `${opts.err || "Request failed"} — ${e.message}` : (opts.err || e.message);
      window.__toast(msg, "warn");
      throw e;
    }
  };

  // ---- bootstrap: refresh every collection from the endpoint ----
  const bootstrap = async () => {
    if (conn.mode !== "live") { setStatus({ state: "mock", detail: "Local fixtures" }); return { ok: true, mode: "mock" }; }
    setStatus({ state: "connecting", detail: `Connecting to ${conn.environment}…` });
    const names = Object.keys(READ_MAP);
    const patch = {};
    const results = await Promise.allSettled(names.map(async (n) => {
      const v = await read(n);
      const map = READ_MAP[n];
      if (v != null) patch[map.key] = v;
    }));
    const failed = results.filter(r => r.status === "rejected").length;
    if (failed === names.length) {
      setStatus({ state: "error", detail: "All endpoints unreachable" });
      return { ok: false, failed };
    }
    if (Object.keys(patch).length) store.set(d => ({ ...d, ...patch }));
    setStatus({ state: "live", detail: `Connected · ${conn.environment}${failed ? ` · ${failed} not-built` : ""}` });
    emit("change", getConfig());
    return { ok: true, failed };
  };

  const testConnection = async () => {
    if (conn.mode !== "live") return { ok: true, mode: "mock", detail: "Mock mode — no network" };
    try { await call("health"); return { ok: true, detail: "Endpoint reachable" }; }
    catch (e) { return { ok: false, detail: e.message }; }
  };

  // ---- runtime config setters ----
  const setConfig = async (patch) => {
    conn = { ...conn, ...patch }; saveConn();
    setStatus({ state: conn.mode === "live" ? "connecting" : "mock", detail: "" });
    emit("change", getConfig());
    return bootstrap();
  };

  window.__api = {
    read, call, write, bootstrap, testConnection,
    setConfig, getConfig, getStatus: () => connectionStatus,
    bindStore, getLog, clearLog: () => { log.length = 0; emit("log", null); },
    on, PaywallApiError, environments: CFG.environments,
  };

  // React hook: bind the App's data state to the client store.
  window.useBoundStore = (initial) => {
    const [data, setData] = React.useState(initial);
    const ref = React.useRef(data); ref.current = data;
    React.useEffect(() => { window.__pwData = data; ref.current = data; }, [data]);
    React.useEffect(() => { window.__api.bindStore({ get: () => ref.current, set: setData }); }, []);
    return [data, setData];
  };

  // React hook: subscribe to connection status.
  window.useConnStatus = () => {
    const [s, setS] = React.useState(window.__api.getStatus());
    React.useEffect(() => window.__api.on("status", setS), []);
    return s;
  };
})();
