/* Supabase client + shared hooks — loaded first, before all other scripts */

(function () {
  const SB_URL = "https://ohjgkdeczzkmojuorxlf.supabase.co";
  const SB_ANON =
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
    "eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9oamdrZGVjenprbW9qdW9yeGxmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc4NDE1NDMsImV4cCI6MjA5MzQxNzU0M30." +
    "tLsJ4Sl5dPmEYHCbm0nXyW-Va-An7X6o3hdELrqKw4U";

  window.SB_URL = SB_URL;
  window.SB_ANON = SB_ANON;
  window.sb = window.supabase.createClient(SB_URL, SB_ANON);

  // Anonymous cart ID — persisted across sessions
  if (!localStorage.getItem("sanaa_anon_id")) {
    const bytes = crypto.getRandomValues(new Uint8Array(16));
    const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
    localStorage.setItem("sanaa_anon_id", hex);
  }
  window.getAnonId = () => localStorage.getItem("sanaa_anon_id");

  // ——— Data transform helpers ——————————————————————

  const CAT_SHAPE = { macrame: "arch", ceramics: "horseshoe", candles: "pointed", resin: "keel" };

  // Local hero images for products. Keyed by slug — drop a file at
  // assets/products/<slug>.{png,jpg} and add the entry here.
  // Anything not listed falls back to the gradient Placeholder.
  const PRODUCT_IMAGES = {
    "oasis-knot": "assets/products/oasis-knot.png",
    "fustat-bowl": "assets/products/fustat-bowl.png",
    "ward-el-nil": "assets/products/ward-el-nil.png",
    "aswan-stone": "assets/products/aswan-stone.png",
    "palm-shadow": "assets/products/palm-shadow.png",
    "midan-lantern": "assets/products/midan-lantern.png",
  };
  window.productImage = function (slug) {
    return PRODUCT_IMAGES[slug] || null;
  };

  window.transformProduct = function (row) {
    return {
      id: row.id,
      slug: row.slug,
      num: row.sku,
      name: row.name_en,
      ar: row.name_ar,
      cat: row.categories?.slug || "macrame",
      type: row.subtitle_en || row.categories?.name_en || "",
      maker: row.makers?.name_en || "",
      makerSlug: row.makers?.slug || "",
      price: Math.round((row.price_piastres || 0) / 100),
      tone: row.makers?.tone || "sand",
      shape: CAT_SHAPE[row.categories?.slug] || "arch",
      edition: row.edition_size ? `1 of ${row.edition_size}` : (row.edition_kind === "unique" ? "1 of 1" : "Open"),
      region: row.region_en || "",
      color: row.color || "",
      material: row.materials_en || "",
      dims: row.dimensions_en || "",
      made: row.made_days ? `${row.made_days} days` : "",
      care: [],
      steps: [],
      gallery: [],
      stock: row.stock || 0,
      is_featured: row.is_featured || false,
      featured_rank: row.featured_rank,
      image: PRODUCT_IMAGES[row.slug] || null,
    };
  };

  window.transformMaker = function (row, allPieces) {
    const myPieces = allPieces ? allPieces.filter(p => p.makerSlug === row.slug) : [];
    return {
      id: row.id,
      slug: row.slug,
      name: row.name_en,
      ar: row.name_ar,
      city: row.city_en,
      trade: row.trade_en,
      since: row.since_year,
      tone: row.tone || "sand",
      pieces: myPieces.length,
      commission_pct: row.commission_pct || 18,
    };
  };

  // ——— Promise-based loaders (cached) ——————————————

  window._piecesCache = null;
  window._makersCache = null;

  window.loadPieces = function () {
    if (window._piecesCache) return Promise.resolve(window._piecesCache);
    if (window._piecesPromise) return window._piecesPromise;
    window._piecesPromise = window.sb
      .from("products")
      .select("*, categories(slug, name_en, name_ar), makers(id, slug, name_en, name_ar, tone, city_en, trade_en, since_year, commission_pct)")
      .eq("is_published", true)
      .order("featured_rank", { ascending: true, nullsFirst: false })
      .then(({ data, error }) => {
        if (error) { console.warn("loadPieces:", error.message); return []; }
        window._piecesCache = (data || []).map(window.transformProduct);
        return window._piecesCache;
      });
    return window._piecesPromise;
  };

  window.loadMakers = function () {
    if (window._makersCache) return Promise.resolve(window._makersCache);
    if (window._makersPromise) return window._makersPromise;
    window._makersPromise = window.sb
      .from("makers")
      // Explicit public column list (NOT select("*")) so the anon listing never
      // requests maker bank/tax PII. transformMaker only reads these fields.
      // See TODO_HUMAN_4 for the matching server-side column lockdown.
      .select("id, slug, name_en, name_ar, city_en, trade_en, since_year, tone, commission_pct, is_published, featured_rank")
      .eq("is_published", true)
      .order("featured_rank", { ascending: true, nullsFirst: false })
      .then(({ data, error }) => {
        if (error) { console.warn("loadMakers:", error.message); return []; }
        return window.loadPieces().then(pieces => {
          window._makersCache = (data || []).map(m => window.transformMaker(m, pieces));
          return window._makersCache;
        });
      });
    return window._makersPromise;
  };

  // ——— React hooks ——————————————————————————————————

  window.usePieces = function (cat) {
    const [all, setAll] = React.useState(window._piecesCache || []);
    React.useEffect(() => {
      window.loadPieces().then(pieces => setAll(pieces));
    }, []);
    return cat ? all.filter(p => p.cat === cat) : all;
  };

  window.useMakers = function () {
    const [data, setData] = React.useState(window._makersCache || []);
    React.useEffect(() => {
      window.loadMakers().then(m => setData(m));
    }, []);
    return data;
  };

  // ──── Workshops + Journal: live-first, hardcoded fallback ────
  // Same pattern as usePieces/useMakers. Public pages show whichever exists.
  window._workshopsCache = null;
  window.loadWorkshops = function () {
    if (window._workshopsCache) return Promise.resolve(window._workshopsCache);
    if (window._workshopsPromise) return window._workshopsPromise;
    window._workshopsPromise = window.sb
      .from("workshops")
      .select("*, makers:host_maker_id(name_en, name_ar, slug, city_en, trade_en, maker_code)")
      .eq("is_published", true)
      .order("starts_at", { ascending: true })
      .then(({ data, error }) => {
        if (error) { console.warn("loadWorkshops:", error.message); return []; }
        window._workshopsCache = data || [];
        return window._workshopsCache;
      });
    return window._workshopsPromise;
  };
  window.useWorkshops = function () {
    const [data, setData] = React.useState(window._workshopsCache || []);
    React.useEffect(() => { window.loadWorkshops().then(setData); }, []);
    return data;
  };

  window._journalCache = null;
  window.loadJournal = function () {
    if (window._journalCache) return Promise.resolve(window._journalCache);
    if (window._journalPromise) return window._journalPromise;
    window._journalPromise = window.sb
      .from("journal_posts")
      .select("id, slug, title_en, title_ar, dek_en, dek_ar, body_en, body_ar, tag, cover_image, read_minutes, published_at, profiles:author_id(full_name)")
      .eq("is_published", true)
      .order("published_at", { ascending: false, nullsFirst: false })
      .then(({ data, error }) => {
        if (error) { console.warn("loadJournal:", error.message); return []; }
        window._journalCache = data || [];
        return window._journalCache;
      });
    return window._journalPromise;
  };
  window.useJournal = function () {
    const [data, setData] = React.useState(window._journalCache || []);
    React.useEffect(() => { window.loadJournal().then(setData); }, []);
    return data;
  };

  window.useAuth = function () {
    const [session, setSession] = React.useState(null);
    const [loading, setLoading] = React.useState(true);
    React.useEffect(() => {
      // Initial session load — wrap in try/catch so a broken refresh token
      // never bubbles up as an unhandled rejection.
      window.sb.auth.getSession()
        .then(({ data: { session } }) => {
          setSession(session);
          setLoading(false);
        })
        .catch((err) => {
          // Likely a corrupted refresh token in localStorage. Clear it and
          // continue as guest — cart in localStorage is untouched.
          window._handleAuthFailure?.(err);
          setSession(null);
          setLoading(false);
        });
      const { data: { subscription } } = window.sb.auth.onAuthStateChange((event, s) => {
        // SIGNED_OUT or USER_DELETED → null session is correct.
        // TOKEN_REFRESHED → s carries the fresh token; just update.
        // Any other event with s=null after we had one means refresh failed.
        setSession(s);
      });
      return () => subscription.unsubscribe();
    }, []);
    return { user: session?.user ?? null, session, loading };
  };

  // ──────── Auth-failure handler (refresh token corruption etc.) ────────
  // Called from useAuth's .catch and from the global unhandledrejection
  // listener. Does the silent cleanup so the customer never sees the SDK's
  // raw "AuthApiError: Invalid Refresh Token" string.
  let _authFailureNoticeShown = false;
  window._handleAuthFailure = async function (err) {
    const msg = ((err && err.message) || "").toString().toLowerCase();
    const looksLikeAuth =
      msg.includes("refresh token") ||
      msg.includes("invalid_grant") ||
      msg.includes("invalid jwt") ||
      msg.includes("jwt expired") ||
      msg.includes("authapierror");
    if (!looksLikeAuth) return false;
    try {
      // Clear the stored session so subsequent calls don't keep retrying with
      // the bad token. signOut({scope:'local'}) only purges localStorage and
      // doesn't hit the server, so it can't fail.
      await window.sb.auth.signOut({ scope: "local" }).catch(() => {});
    } catch (_) { /* swallow */ }
    if (!_authFailureNoticeShown) {
      _authFailureNoticeShown = true;
      // Reset the notice flag after a minute so the same user can see it
      // again on the next failure cycle.
      setTimeout(() => { _authFailureNoticeShown = false; }, 60_000);
      // Surface a calm one-time toast via the window event bus. The shell
      // listens for sanaa:notice and renders a SANAA-tone banner.
      try {
        window.dispatchEvent(new CustomEvent("sanaa:notice", {
          detail: {
            kind: "auth_expired",
            en: "Your session needs to be refreshed to continue securely.",
            ar: "تحتاج جلستك إلى تحديث للمتابعة بأمان.",
          },
        }));
      } catch (_) { /* older browsers */ }
    }
    return true;
  };

  // ──────── Customer-safe error translator ────────
  // Maps backend error codes (and a few free-text fingerprints) to
  // SANAA-tone bilingual copy. Anything not in the map falls back to a
  // restrained generic line — we never echo raw Supabase / Postgres /
  // Edge-Function strings to the customer.
  const _ERROR_COPY = {
    stock_unavailable:    { en: "This piece is no longer available in the quantity you selected.", ar: "هذه القطعة غير متاحة بالكميّة التي اخترتها." },
    stock_race:           { en: "A piece just sold out while we were preparing your order.",       ar: "نفدت قطعة بينما كنّا نُعدّ طلبك." },
    item_unavailable:     { en: "One of your pieces is no longer available.",                      ar: "إحدى قطعك لم تعد متوفّرة." },
    cart_empty:           { en: "Your bag is empty. Add a piece to continue.",                     ar: "حقيبتك فارغة. أضف قطعة للمتابعة." },
    address_required:     { en: "Please complete the delivery address before placing the order.", ar: "يرجى إكمال عنوان التوصيل قبل تأكيد الطلب." },
    invalid_phone:        { en: "That phone number doesn't look right. Please double-check.",      ar: "رقم الهاتف لا يبدو صحيحًا." },
    invalid_request:      { en: "We couldn't read that request. Please try again.",                 ar: "تعذّر قراءة الطلب. حاول مرّة أخرى." },
    email_required:       { en: "Please add a valid email so we can send your order confirmation.",ar: "يرجى إضافة بريد إلكتروني صحيح لإرسال تأكيد الطلب." },
    account_blocked:      { en: "We can't place this order from your account. Please contact SANAA support.", ar: "لا يمكننا إتمام الطلب من حسابك. يرجى التواصل مع فريق صَنعة." },
    auth_required:        { en: "Your session has expired. Please sign in again to continue.",     ar: "انتهت صلاحيّة الجلسة. يرجى تسجيل الدخول مجدّدًا." },
    network:              { en: "We couldn't reach SANAA. Please check your connection and try again.", ar: "تعذّر الوصول إلى صَنعة. تأكّد من اتصالك وحاول مرة أخرى." },
    generic_5xx:          { en: "Something went wrong on our side. Please try again in a moment.", ar: "حدث خلل عندنا. حاول مرّة أخرى بعد قليل." },
    duplicate_submit:     { en: "We're already placing this order — one moment.",                  ar: "نُعالج هذا الطلب بالفعل — لحظة." },
  };
  // Lightweight fingerprinting of common Supabase / Auth raw strings so a
  // single integration point catches every legacy source.
  function _fingerprint(raw) {
    const t = ((raw && (raw.code || raw.message || raw.error || raw)) || "").toString().toLowerCase();
    if (!t) return "generic_5xx";
    if (t.includes("refresh token") || t.includes("invalid jwt") || t.includes("jwt expired") || t.includes("authapierror")) return "auth_required";
    if (t.includes("failed to fetch") || t.includes("network")) return "network";
    if (t.includes("insufficient stock") || t.includes("just sold out")) return "stock_race";
    if (t.includes("no longer available")) return "item_unavailable";
    if (t.includes("cart is empty") || t.includes("bag is empty")) return "cart_empty";
    return null;
  }
  // Returns { code, en, ar, details } given anything thrown / returned.
  window.translateError = function (raw, fallbackCode) {
    if (!raw) return { code: fallbackCode || "generic_5xx", ..._ERROR_COPY[fallbackCode || "generic_5xx"], details: null };
    // Already-coded: response body shape { code, friendly_en, friendly_ar }
    if (raw.code && _ERROR_COPY[raw.code]) {
      return { code: raw.code, en: raw.friendly_en || _ERROR_COPY[raw.code].en, ar: raw.friendly_ar || _ERROR_COPY[raw.code].ar, details: raw.details || null };
    }
    const fp = _fingerprint(raw);
    if (fp && _ERROR_COPY[fp]) return { code: fp, ..._ERROR_COPY[fp], details: null };
    // Unknown — never echo raw text. Fall back to a calm generic.
    return { code: fallbackCode || "generic_5xx", ..._ERROR_COPY[fallbackCode || "generic_5xx"], details: null };
  };

  // Localised getter for templates that already know their lang prop.
  window.errorCopy = function (raw, lang, fallbackCode) {
    const t = window.translateError(raw, fallbackCode);
    return lang === "ar" ? (t.ar || t.en) : (t.en || t.ar);
  };

  // ——— Admin data loader (writes into ADMIN_* globals) ————

  window.loadAdminData = async function () {
    try {
      // All admin reads are bounded.
      // Aggregations (per-order qty, per-maker totals, per-product sales) come from
      // SECURITY DEFINER RPCs — no full table scans on the client.
      const [
        approvals, pieces, makers, orders, auditRows, profiles,
        orderSummariesR, makerTotalsR, productSalesR, dashStatsR,
        payouts, workshops, journal,
        financeSummaryR, statsDeltasR, applicationsR,
      ] = await Promise.all([
        window.sb.from("approval_requests")
          .select("*, makers(name_en, slug, tone, city_en, trade_en, rubric_score)")
          .order("created_at", { ascending: false })
          .limit(200),
        window.sb.from("products")
          .select("*, categories(slug, name_en), makers(name_en, slug, tone)")
          .order("created_at", { ascending: false })
          .limit(200),
        window.sb.from("makers").select("*"),
        window.sb.from("orders")
          .select("id, order_number, status, payment_status, total_piastres, created_at, guest_name, guest_email, guest_phone, shipping_address_json, profiles(full_name, email)")
          .order("created_at", { ascending: false })
          .limit(200),
        window.sb.from("audit_log")
          .select("*")
          .order("created_at", { ascending: false })
          .limit(100),
        window.sb.from("profiles").select("id, email, full_name, role, created_at").order("created_at", { ascending: false }).limit(500),
        // Server-side aggregations (replaces unbounded order_items.select('*'))
        window.sb.rpc("admin_order_summaries", { p_limit: 200 }),
        window.sb.rpc("admin_maker_totals"),
        window.sb.rpc("admin_product_sales"),
        window.sb.rpc("admin_dashboard_stats"),
        window.sb.from("payouts").select("*, makers(name_en, maker_payout_details(iban_full, bank_name))").order("period_start", { ascending: false }).limit(50),
        window.sb.from("workshops").select("*, makers:host_maker_id(name_en, slug, maker_code)").order("starts_at", { ascending: false }).limit(80),
        window.sb.from("journal_posts").select("id, slug, title_en, tag, author_id, is_published, published_at, created_at").order("created_at", { ascending: false }).limit(50),
        // Real finance + stats deltas — admin-gated RPCs. EGP major units,
        // current calendar year, per-maker commission (not flat 18%), no 200 cap.
        // Wrapped so a single failure leaves the cards at a safe zero/empty state
        // rather than throwing out of loadAdminData.
        window.sb.rpc("admin_finance_summary").then(r => r).catch(e => ({ error: e })),
        window.sb.rpc("admin_stats_deltas").then(r => r).catch(e => ({ error: e })),
        // Maker onboarding applications — admin-gated RPC. Fail-soft to [] so a
        // single failure (or a backend not yet migrated) leaves the Applications
        // section empty rather than throwing out of loadAdminData.
        window.sb.rpc("admin_list_maker_applications").then(r => r).catch(e => ({ error: e })),
      ]);

      // ───── Maker applications (onboarding review queue) ─────
      // RPC admin_list_maker_applications returns the full row set; we store it
      // verbatim on window.ADMIN_APPLICATIONS. Open/pending = received|shortlisted.
      // Fail-soft to [] so the Applications section degrades to its empty state.
      if (applicationsR && applicationsR.error) {
        console.warn("admin_list_maker_applications:", applicationsR.error.message || applicationsR.error);
        window.ADMIN_APPLICATIONS = [];
      } else {
        window.ADMIN_APPLICATIONS = applicationsR && applicationsR.data ? applicationsR.data : [];
      }

      if (approvals.data) {
        window.ADMIN_APPROVALS = approvals.data
          .filter(a => a.status === "pending")
          .map(a => ({
            id: a.id,
            type: a.kind,
            status: a.status,
            priority: a.priority || "normal",
            maker: a.makers?.slug || "",
            makerName: a.makers?.name_en || "Unknown",
            submitted: a.created_at,
            age: a.created_at,  // raw ISO; localized at render via window.fmtAge(age, lang)
            title: a.title || a.kind,
            summary: a.summary_en || "",
            diff: a.diff_json
              ? Object.entries(a.diff_json).map(([k, v]) => ({
                  k, old: v?.old ?? null, new: v?.new ?? null,
                }))
              : [],
            note: a.requester_note || "",
          }));
      }

      if (pieces.data) {
        window.ADMIN_PIECES = pieces.data.map(p => ({
          num: p.sku,
          name: p.name_en,
          // maker slug is kept ONLY for internal joins (piece-count per maker,
          // search). It is never rendered — the UI shows makerName.
          maker: p.makers?.slug,
          makerName: p.makers?.name_en || "—",
          trade: p.categories?.name_en || "—",
          categorySlug: p.categories?.slug || "",
          price: Math.round((p.price_piastres || 0) / 100),
          stock: p.stock || 0,
          sold: 0,
          status: p.is_published ? "Live" : "Draft",
          tone: p.makers?.tone || "sand",
        }));
      }

      if (makers.data && window.ADMIN_PIECES) {
        window.ADMIN_MAKERS = makers.data.map(m => ({
          slug: m.slug,
          name: m.name_en,
          ar: m.name_ar,
          city: m.city_en,
          trade: m.trade_en,
          since: m.since_year,
          tone: m.tone || "sand",
          pieces: window.ADMIN_PIECES.filter(p => p.maker === m.slug).length,
          tier: null,
          rubric: null,
          split: 100 - (m.commission_pct || 18),
          status: "active",
          lifetime: 0,
          gmv: 0,
        }));
      }

      // Build aggregations from server-side RPC results (no full table scans).
      // admin_order_summaries returns: { order_id, qty, maker_ids[], maker_names[] }
      const itemsByOrder = {};
      for (const row of (orderSummariesR.data || [])) {
        itemsByOrder[row.order_id] = {
          qty: row.qty || 0,
          makerIds: row.maker_ids || [],
          makerNames: row.maker_names || [],
          makerCodes: (row.maker_codes || []).filter(Boolean),
        };
      }
      // admin_maker_totals returns: { maker_id, gross_piastres, qty, items }
      const itemsByMaker = {};
      for (const row of (makerTotalsR.data || [])) {
        itemsByMaker[row.maker_id] = {
          gross: Number(row.gross_piastres) || 0,
          qty: row.qty || 0,
          items: row.items || 0,
        };
      }
      // admin_product_sales returns: { product_id, sold }
      const productSold = {};
      for (const row of (productSalesR.data || [])) {
        if (row.product_id) productSold[row.product_id] = row.sold || 0;
      }
      window._itemsByOrder = itemsByOrder;

      // ───── Maker name lookup for orders
      const makerById = {};
      for (const m of (makers.data || [])) makerById[m.id] = m;

      if (orders.data) {
        window.ADMIN_ORDERS = orders.data.map(o => {
          const agg = itemsByOrder[o.id] || { qty: 1, makerIds: [], makerNames: [], makerCodes: [] };
          const makerNames = (agg.makerNames || []).filter(Boolean);
          const makerCodes = (agg.makerCodes || []).filter(Boolean);
          const total = Math.round((o.total_piastres || 0) / 100);
          const fees = Math.round((o.total_piastres || 0) * 0.18 / 100);
          return {
            id: o.order_number,
            _uuid: o.id,
            // Raw ISO — formatted locale-aware at render time (F10). Keeping
            // the raw timestamp lets the orders table / drawer localize to AR.
            date: o.created_at,
            buyer: o.profiles?.full_name || o.guest_name || "Guest",
            email: o.profiles?.email || o.guest_email || "—",
            phone: (o.shipping_address_json && o.shipping_address_json.phone) || o.guest_phone || "—",
            city: (o.shipping_address_json && o.shipping_address_json.city) || "—",
            makerName: makerNames.length > 1 ? `${makerNames[0]} +${makerNames.length - 1}` : (makerNames[0] || "—"),
            makerCodes,
            makerCodeDisplay: makerCodes.length > 1 ? `${makerCodes[0]} +${makerCodes.length - 1}` : (makerCodes[0] || "—"),
            qty: agg.qty || 1,
            gross: total,
            fees: fees,
            payout: total - fees,
            total: total,
            // Store the RAW lowercase DB enum so the order-status tabs and the
            // shared ORDER_STATUS_LABEL map can match it (e.g. pending_confirmation).
            status: (o.status || "pending").toLowerCase(),
            payment_status: o.payment_status,
            shipping_address_json: o.shipping_address_json,
          };
        });
      }

      // Profile lookup for audit "actor"
      const profileByActor = {};
      for (const p of (profiles.data || [])) profileByActor[p.id] = p;

      if (auditRows.data) {
        window.ADMIN_AUDIT = auditRows.data.map(e => {
          const actor = profileByActor[e.actor_id]?.full_name || profileByActor[e.actor_id]?.email || e.actor_role || "system";
          const action = e.action;
          const kind = action.includes("approved") ? "approve"
             : action.includes("rejected") ? "reject"
             : action.includes("login") ? "login"
             : action.includes("created") || action.includes("submitted") ? "create"
             : action.includes("payout") ? "payout"
             : action.includes("suspend") ? "suspend"
             : "edit";
          return {
            at: new Date(e.created_at).toLocaleString("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }),
            actor,
            // raw enum, e.g. "approval_approved" — resolved to a bilingual
            // label at render time via window.auditActionLabel (admin-data.jsx).
            action,
            ref: e.target_id ? String(e.target_id).slice(0, 8) : "—",
            ip: "—",
            kind,
            age: e.created_at,  // raw ISO; localized at render via window.fmtAge(age, lang)
            meta: e.meta_json || {},
          };
        });
      }

      // Update maker rows with REAL gmv from order_items aggregation
      if (window.ADMIN_MAKERS) {
        window.ADMIN_MAKERS = window.ADMIN_MAKERS.map(m => {
          const realMaker = makers.data?.find(x => x.slug === m.slug);
          const agg = realMaker ? itemsByMaker[realMaker.id] || { gross: 0, qty: 0 } : { gross: 0, qty: 0 };
          // rubric_score is the only real quality signal. When it's absent we
          // show no score and no inferred tier ("Unrated") — never a fabricated 87.
          const rubric = (realMaker && realMaker.rubric_score != null) ? realMaker.rubric_score : null;
          const tier = realMaker?.is_published === false
            ? "Suspended"
            : (rubric == null ? "Unrated"
               : rubric >= 90 ? "Senior Master"
               : rubric >= 80 ? "Verified House"
               : "Emerging");
          return {
            ...m,
            tier,
            rubric,
            gmv: Math.round(agg.gross / 100),
            sold: agg.qty,
            lifetime: Math.round(agg.gross / 100),
            id: realMaker?.id,
          };
        });
      }

      // Update pieces rows with REAL sold counts
      if (window.ADMIN_PIECES) {
        window.ADMIN_PIECES = window.ADMIN_PIECES.map((p, i) => {
          const dbPiece = pieces.data?.[i];
          const sold = dbPiece ? (productSold[dbPiece.id] || 0) : 0;
          return { ...p, sold, id: dbPiece?.id };
        });
      }

      // ───── Live customers from profiles + orders aggregation
      const customerSpend = {};
      for (const o of (orders.data || [])) {
        const key = o.user_id || o.guest_email;
        if (!key) continue;
        if (!customerSpend[key]) customerSpend[key] = { orders: 0, spend: 0, lastOrder: o.created_at, name: o.profiles?.full_name || o.guest_name || "Guest", email: o.profiles?.email || o.guest_email, city: o.shipping_address_json?.city || "—", user_id: o.user_id };
        customerSpend[key].orders += 1;
        customerSpend[key].spend += Math.round((o.total_piastres || 0) / 100);
        if (new Date(o.created_at) > new Date(customerSpend[key].lastOrder)) customerSpend[key].lastOrder = o.created_at;
      }
      window.ADMIN_CUSTOMERS = Object.values(customerSpend).map(c => ({
        name: c.name,
        email: c.email,
        city: c.city,
        orders: c.orders,
        spend: c.spend,
        lifetime: c.spend,
        // Raw ISO — formatted locale-aware at render time (F10).
        lastOrder: c.lastOrder,
        joined: c.lastOrder,
        loyalty: c.orders >= 5 ? "vip" : c.orders >= 2 ? "returning" : "new",
        tier: c.orders >= 5 ? "VIP" : c.orders >= 2 ? "Returning" : "New",
      })).sort((a, b) => b.spend - a.spend);

      // ───── Live team — admin/superadmin profiles
      window.ADMIN_TEAM = (profiles.data || []).filter(p => p.role === "admin" || p.role === "superadmin").map(p => ({
        id: p.id,
        name: p.full_name || p.email.split("@")[0],
        email: p.email,
        role: p.role === "superadmin" ? "Owner" : "Approver",
        since: new Date(p.created_at).toLocaleDateString("en-GB", { month: "short", year: "numeric" }),
      }));

      // Map payouts to live data — the pending-payout LIST stays sourced from the
      // payouts table (admin_finance_summary returns only the YTD payouts NUMBER).
      const payoutRows = (payouts.data || []).map(p => {
        // maker_payout_details is embedded under makers; PostgREST may return it
        // as an object or a single-element array depending on the relationship.
        const _mpd = Array.isArray(p.makers?.maker_payout_details)
          ? p.makers.maker_payout_details[0]
          : p.makers?.maker_payout_details;
        return {
          maker: p.makers?.name_en || "—",
          // Raw ISO — formatted locale-aware at render time (F10), month+year.
          period: p.period_start,
          gross: Math.round((p.gross_piastres || 0) / 100),
          split: Math.round((p.payout_piastres || 0) / 100),
          bank: _mpd?.bank_name ? `${_mpd.bank_name} ${_mpd.iban_full ? "····" + _mpd.iban_full.slice(-4) : ""}` : "—",
          status: (p.status || "pending").charAt(0).toUpperCase() + (p.status || "pending").slice(1),
        };
      });

      // ───── Real finance — admin_finance_summary RPC (EGP major units, current
      // calendar year, all qualifying orders, REAL per-maker commission).
      // The render (AdminFinancePage) divides each YTD number by 1000 and prints
      // `returnRate + "% rate"` — so returnRate must be a PERCENT, and the RPC's
      // return_rate is a FRACTION (0..1), hence ×100. Monthly is mapped to the
      // chart's shape: { d: Date, gmv } (the axis is localized via fmtDate(m.d)).
      // On RPC failure, fall back to a safe zero/empty state (cards show "No data").
      const fin = (financeSummaryR && !financeSummaryR.error && financeSummaryR.data) || null;
      if (fin && typeof fin === "object") {
        const finYear = Number(fin.year) || new Date().getFullYear();
        window.ADMIN_FINANCE = {
          ytdGmv: Math.round(Number(fin.ytd_gmv) || 0),
          ytdFees: Math.round(Number(fin.ytd_fees) || 0),
          ytdPayouts: Math.round(Number(fin.ytd_payouts) || 0),
          ytdReturns: Math.round(Number(fin.ytd_returns) || 0),
          // return_rate is a fraction (e.g. 0.0312); the card renders `+ "% rate"`.
          returnRate: +((Number(fin.return_rate) || 0) * 100).toFixed(1),
          monthly: (Array.isArray(fin.monthly) ? fin.monthly : []).map(mo => ({
            d: new Date(finYear, (Number(mo.month) || 1) - 1, 1),
            gmv: Number(mo.gmv) || 0,
            fees: Number(mo.fees) || 0,
          })),
          payouts: payoutRows,
        };
      } else {
        if (financeSummaryR && financeSummaryR.error) {
          console.warn("admin_finance_summary:", financeSummaryR.error.message || financeSummaryR.error);
        }
        window.ADMIN_FINANCE = {
          ytdGmv: 0, ytdFees: 0, ytdPayouts: 0, ytdReturns: 0,
          returnRate: 0, monthly: [], payouts: payoutRows,
        };
      }

      // ───── Live workshops with host name + occupancy + lifecycle status
      window.ADMIN_WORKSHOPS = (workshops.data || []).map(w => {
        const seatsTotal = w.seats_total || 0;
        const seatsTaken = w.seats_taken || 0;
        const occupancyPct = seatsTotal ? Math.round(100 * seatsTaken / seatsTotal) : 0;
        const statusLabel = ({
          draft: "Draft",
          published: "Live",
          fully_booked: "Full",
          in_progress: "In progress",
          completed: "Completed",
          cancelled: "Cancelled",
          archived: "Archived",
        })[w.status] || (w.is_published ? "Live" : "Draft");
        return {
          id: w.id,
          slug: w.slug,
          t: w.title_en,
          title_ar: w.title_ar,
          host: w.makers?.name_en || "—",
          host_slug: w.makers?.slug,
          host_code: w.makers?.maker_code,
          starts_at: w.starts_at,
          // Raw ISO — formatted locale-aware at render time (F10),
          // weekday+day+month. starts_at above carries the same value.
          date: w.starts_at,
          seats: `${seatsTaken} / ${seatsTotal}`,
          seats_total: seatsTotal,
          seats_taken: seatsTaken,
          occupancy_pct: occupancyPct,
          rev: Math.round((seatsTaken * (w.price_piastres || 0)) / 100),
          price_piastres: w.price_piastres,
          status: statusLabel,
          status_key: w.status || (w.is_published ? "published" : "draft"),
          is_published: w.is_published,
          city_en: w.city_en,
        };
      });

      // ───── Live content (journal posts)
      window.ADMIN_CONTENT = (journal.data || []).map(j => ({
        id: j.id,
        kind: "Article",
        title: j.title_en,
        author: profileByActor[j.author_id]?.full_name || profileByActor[j.author_id]?.email || "SANAA editorial",
        // Raw ISO — formatted locale-aware at render time (F10), day+month.
        date: j.published_at || j.created_at,
        status: j.is_published ? "Published" : "Draft",
      }));

      // ───── Open returns count (for the nav badge). Cheap because the
      // return_requests table is small. We don't bring the rows themselves
      // — the AdminReturnsPage loads them on mount via admin_returns_list.
      window.sb.from("return_requests")
        .select("id", { count: "exact", head: true })
        .in("status", ["requested","approved","received","refund_failed"])
        .then(({ count }) => {
          window.ADMIN_OPEN_RETURNS = count || 0;
          if (window._adminRefreshCallback) window._adminRefreshCallback();
        });

      // ───── Stats deltas — admin_stats_deltas RPC.
      //   makers_prev      = published makers ~30d ago  → makersDelta = live − prev (OV-2)
      //   new_customers_7d = customers whose FIRST order is in the last 7 days (OV-3)
      // Wrapped so a failure leaves the deltas at a safe 0 (cards still render).
      const deltas = (statsDeltasR && !statsDeltasR.error && statsDeltasR.data) || null;
      if (statsDeltasR && statsDeltasR.error) {
        console.warn("admin_stats_deltas:", statsDeltasR.error.message || statsDeltasR.error);
      }
      const makersPrev = deltas ? Number(deltas.makers_prev) || 0 : null;
      const newCustomers7d = deltas ? Number(deltas.new_customers_7d) || 0 : 0;

      // ───── Live overview stats — server-aggregated (admin_dashboard_stats RPC)
      // Falls back to in-JS aggregates only if the RPC call failed.
      const ds = (dashStatsR && dashStatsR.data) || null;
      if (ds && typeof ds === "object") {
        const prev = Number(ds.gmv_prev || 0);
        const cur = Number(ds.gmv || 0);
        const gmvDelta = prev > 0 ? `+${Math.round((cur - prev) / prev * 100)}%` : (cur > 0 ? "new" : "0");
        const liveMakers = ds.makers || 0;
        window.ADMIN_STATS = {
          gmv: cur,
          gmvDelta,
          makers: liveMakers,
          // Real net change vs ~30d ago (OV-2 — no longer hardcoded +0).
          makersDelta: makersPrev != null ? liveMakers - makersPrev : 0,
          applicants: ds.applicants || 0,
          piecesLive: ds.pieces_live || 0,
          piecesDraft: ds.pieces_draft || 0,
          customers: ds.customers || 0,
          // Genuinely-new customers in the last 7 days (OV-3), not "recent orders".
          customerDelta: newCustomers7d,
          pendingApprovals: ds.pending_approvals || 0,
          highApprovals: ds.high_approvals || 0,
          todayApprovals: ds.today_approvals || 0,
        };
      } else {
        // Fallback (non-admin or RPC error) — empty defaults
        window.ADMIN_STATS = {
          gmv: 0, gmvDelta: "0",
          makers: (makers.data || []).filter(m => m.is_published).length,
          makersDelta: 0, applicants: 0,
          piecesLive: (pieces.data || []).filter(p => p.is_published).length,
          piecesDraft: (pieces.data || []).filter(p => !p.is_published).length,
          customers: 0, customerDelta: 0,
          pendingApprovals: (window.ADMIN_APPROVALS || []).length,
          highApprovals: 0, todayApprovals: 0,
        };
      }

      if (window._adminRefreshCallback) window._adminRefreshCallback();
    } catch (err) {
      console.warn("loadAdminData:", err.message);
    }
  };

  // ──────── Realtime helpers ────────
  // Each subscription gets a unique channel name so React Strict-Mode double-invoke
  // (and rapid re-mounts) cannot collide on the same channel.
  // The latest onInsert callback is captured via a ref so it always sees fresh state.
  let _channelSeq = 0;
  function _nextChannel(prefix) { return prefix + "-" + (++_channelSeq) + "-" + Math.random().toString(36).slice(2, 8); }

  window.useRealtimeMessages = function (conversationId, onInsert) {
    const cbRef = React.useRef(onInsert);
    React.useEffect(() => { cbRef.current = onInsert; });
    React.useEffect(() => {
      if (!conversationId) return;
      const channelName = _nextChannel("conv-" + conversationId);
      const channel = window.sb
        .channel(channelName)
        .on("postgres_changes",
          { event: "INSERT", schema: "public", table: "messages", filter: "conversation_id=eq." + conversationId },
          (payload) => { try { cbRef.current && cbRef.current(payload.new); } catch (_) {} })
        .subscribe();
      return () => { window.sb.removeChannel(channel); };
    }, [conversationId]);
  };

  window.useRealtimeConversations = function (filterColumn, filterValue, onChange) {
    const cbRef = React.useRef(onChange);
    React.useEffect(() => { cbRef.current = onChange; });
    React.useEffect(() => {
      if (!filterValue) return;
      const channelName = _nextChannel("convs-" + filterColumn + "-" + filterValue);
      const channel = window.sb
        .channel(channelName)
        .on("postgres_changes",
          { event: "*", schema: "public", table: "conversations", filter: filterColumn + "=eq." + filterValue },
          (payload) => { try { cbRef.current && cbRef.current(payload); } catch (_) {} })
        .subscribe();
      return () => { window.sb.removeChannel(channel); };
    }, [filterColumn, filterValue]);
  };

  // ──────── Global client error logger ────────
  // Captures uncaught errors + unhandled promise rejections and ships them to client_errors.
  // Throttled at the DB layer (10 per fingerprint/minute) so an infinite loop can't spam.
  function _hashFingerprint(s) {
    let h = 0; for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
    return "fp_" + Math.abs(h).toString(36);
  }
  let _lastError = "";
  async function _logError(message, stack, extra) {
    try {
      const fp = _hashFingerprint((message || "") + "|" + (stack || "").split("\n")[0]);
      // De-dupe consecutive identical errors within the same render cycle
      if (fp === _lastError) return;
      _lastError = fp;
      setTimeout(() => { _lastError = ""; }, 2000);
      const { data: { user } } = await window.sb.auth.getUser();
      await window.sb.from("client_errors").insert({
        user_id: user?.id || null,
        url: window.location.href.slice(0, 1000),
        ua: navigator.userAgent.slice(0, 500),
        message: (message || "").toString().slice(0, 2000),
        stack: (stack || "").toString().slice(0, 8000),
        extra_json: extra || null,
        fingerprint: fp,
      });
    } catch (_) { /* never let logging break the app */ }
  }
  window.addEventListener("error", (e) => {
    _logError(e.message, e.error?.stack, { type: "error", filename: e.filename, lineno: e.lineno, colno: e.colno });
  });
  window.addEventListener("unhandledrejection", (e) => {
    const reason = e.reason;
    // First: try to handle auth failures silently. If we did, suppress the
    // default browser logging so "AuthApiError: Invalid Refresh Token" never
    // ends up in the customer's console / DevTools surface.
    const handled = window._handleAuthFailure && window._handleAuthFailure(reason);
    if (handled) { try { e.preventDefault(); } catch (_) {} return; }
    _logError(reason?.message || String(reason), reason?.stack, { type: "unhandledrejection" });
  });
  window._logClientError = _logError;  // for manual reporting from try/catch sites

  // Kick off product + maker prefetch on load
  window.loadPieces();
  window.loadMakers();
})();
