/* Klipzy.lv — Firestore-backed data hooks (campaigns, submissions, transactions) */

function fbReady() { return window.__firebaseReady || Promise.resolve(window.KlipzyFirebase); }

/* ---- Platform canonicalization ----
   Submissions store a lowercase canonical platform enum. Campaigns keep
   human labels for their allowed-platform list, so we map between them. */
const PLATFORM_LABEL = { youtube: 'YouTube', tiktok: 'TikTok', instagram: 'Instagram' };
function canonPlatform(label) {
  const s = String(label || '').toLowerCase();
  if (s.includes('youtube') || s.includes('shorts')) return 'youtube';
  if (s.includes('tiktok')) return 'tiktok';
  if (s.includes('insta') || s.includes('reels')) return 'instagram';
  return s || 'youtube';
}
/* Extract a clean video ID from a submitted URL (YouTube → 11-char id;
   TikTok/Instagram → trailing path id, else the raw URL as a fallback). */
function extractVideoId(url, platform) {
  const u = String(url || '').trim();
  if (!u) return '';
  const p = platform || canonPlatform(u);
  if (p === 'youtube') {
    const pats = [/[?&]v=([A-Za-z0-9_-]{11})/, /youtu\.be\/([A-Za-z0-9_-]{11})/, /\/shorts\/([A-Za-z0-9_-]{11})/, /\/embed\/([A-Za-z0-9_-]{11})/];
    for (const re of pats) { const m = u.match(re); if (m) return m[1]; }
    if (/^[A-Za-z0-9_-]{11}$/.test(u)) return u;
    return '';
  }
  // tiktok /video/<digits>, instagram /reel|/p/<code>
  const m = u.match(/\/(?:video|reel|reels|p)\/([A-Za-z0-9_-]+)/);
  if (m) return m[1];
  const tail = u.split(/[?#]/)[0].split('/').filter(Boolean).pop();
  return tail || u;
}
/* Normalize a raw Firestore submission into the shape the UI expects.
   `viewCount` is canonical; `views` is kept as an alias for legacy reads.
   `review` is the moderation state; `status` stays the tracking lifecycle. */
function mapSubmission(s) {
  const viewCount = Number(s.viewCount != null ? s.viewCount : (s.views || 0));
  const platform = s.platform || '';
  return {
    ...s,
    platform,
    platformLabel: PLATFORM_LABEL[platform] || platform || '',
    videoId: s.videoId || '',
    url: s.url || '',
    viewCount,
    views: viewCount,               // alias for existing UI references
    startViews: Number(s.startViews) || 0,
    paidAmount: Number(s.paidAmount) || 0,
    withdrawnAmount: Number(s.withdrawnAmount) || 0,
    review: s.reviewStatus || 'pending',
    status: s.status || 'active',   // tracking lifecycle
  };
}

function daysUntil(end) {
  if (!end) return null;
  const d = new Date(end);
  if (isNaN(d)) return null;
  return Math.max(0, Math.ceil((d - new Date()) / 86400000));
}
function fmtDeadline(end) {
  if (!end) return '—';
  const months = ['jan.', 'feb.', 'mar.', 'apr.', 'maijs', 'jūn.', 'jūl.', 'aug.', 'sep.', 'okt.', 'nov.', 'dec.'];
  const d = new Date(end);
  if (isNaN(d)) return end;
  return d.getDate() + '. ' + months[d.getMonth()];
}

/* Map a raw Firestore campaign to the shape the cards/detail expect. */
function mapCampaign(c) {
  return {
    id: c.id,
    docId: c.id,
    brandUid: c.brandUid,
    brand: c.brandName || c.name || 'Zīmols',
    initials: c.initials || (c.brandName || '?').slice(0, 2).toUpperCase(),
    brandLogo: c.brandLogo || '',
    grad: c.grad || ['#2563EB', '#3B82F6'],
    name: c.name || '',
    product: c.name || c.niche || '',
    tagline: c.niche || '',
    niche: c.niche || '',
    niche2: '',
    desc: c.desc || '',
    rate: Number(c.rate) || 0,         // € per 1 000 views
    budget: Number(c.budget) || 0,
    minViews: Number(c.minViews) || 0,
    maxPerClip: Number(c.maxPerClip) || 0,
    spent: Number(c.spent) || 0,
    views: Number(c.views) || 0,
    clippers: Number(c.clippers) || 0,
    clipperUids: Array.isArray(c.clipperUids) ? c.clipperUids : [],
    platforms: c.platforms || [],
    dos: c.dos || [],
    donts: c.donts || [],
    start: c.start || '',
    end: c.end || '',
    deadline: fmtDeadline(c.end),
    daysLeft: daysUntil(c.end),
    assetsLink: c.assetsLink || '',
    assetsLinks: Array.isArray(c.assetsLinks) ? c.assetsLinks : (c.assetsLink ? [c.assetsLink] : []),
    assets: Array.isArray(c.assets) ? c.assets : [],
    exampleClips: Array.isArray(c.exampleClips) ? c.exampleClips : [],
    status: c.status || 'active',
    // Budget lifecycle fields (added by the refresh function; derived here
    // when a campaign predates them so old docs still render sensibly).
    budgetTotal: Number(c.budgetTotal != null ? c.budgetTotal : c.budget) || 0,
    budgetRemaining: Number(c.budgetRemaining != null ? c.budgetRemaining : ((Number(c.budget) || 0) - (Number(c.spent) || 0))) || 0,
    payoutRate: Number(c.payoutRate != null ? c.payoutRate : (Number(c.rate) || 0) / 1000),
    isActive: c.isActive != null ? c.isActive : (c.status || 'active') === 'active',
    isOpenForSubmissions: c.isOpenForSubmissions != null ? c.isOpenForSubmissions : ((c.status || 'active') === 'active'),
    isLowBudget: !!c.isLowBudget,
  };
}

function useCampaigns() {
  const [state, setState] = React.useState({ campaigns: [], loading: true });
  const [nonce, setNonce] = React.useState(0);
  React.useEffect(() => {
    let active = true;
    setState((s) => ({ ...s, loading: true }));
    fbReady().then((fb) => {
      if (!fb) return;
      // Public listings only show active campaigns (paused/deleted are hidden).
      fb.listCampaigns().then((rows) => { if (active) setState({ campaigns: rows.map(mapCampaign).filter((c) => c.status === 'active'), loading: false }); })
        .catch(() => { if (active) setState({ campaigns: [], loading: false }); });
    });
    return () => { active = false; };
  }, [nonce]);
  return { ...state, reload: () => setNonce((n) => n + 1) };
}

function useBrandCampaigns(uid) {
  const [state, setState] = React.useState({ campaigns: [], loading: true });
  const [nonce, setNonce] = React.useState(0);
  React.useEffect(() => {
    if (!uid) { setState({ campaigns: [], loading: false }); return; }
    let active = true;
    setState((s) => ({ ...s, loading: true }));
    fbReady().then((fb) => {
      if (!fb) return;
      fb.listBrandCampaigns(uid).then((rows) => { if (active) setState({ campaigns: rows.map(mapCampaign), loading: false }); })
        .catch(() => { if (active) setState({ campaigns: [], loading: false }); });
    });
    return () => { active = false; };
  }, [uid, nonce]);
  return { ...state, reload: () => setNonce((n) => n + 1) };
}

function useBrandSubmissions(uid) {
  const [state, setState] = React.useState({ subs: [], loading: true });
  const [nonce, setNonce] = React.useState(0);
  React.useEffect(() => {
    if (!uid) { setState({ subs: [], loading: false }); return; }
    let active = true;
    setState((s) => ({ ...s, loading: true }));
    fbReady().then((fb) => {
      if (!fb) return;
      fb.listBrandSubmissions(uid).then((rows) => {
        const mapped = rows.map(mapSubmission);
        mapped.sort((a, b) => (b.createdAt && b.createdAt.seconds || 0) - (a.createdAt && a.createdAt.seconds || 0));
        if (active) setState({ subs: mapped, loading: false });
      }).catch(() => { if (active) setState({ subs: [], loading: false }); });
    });
    return () => { active = false; };
  }, [uid, nonce]);
  return { ...state, reload: () => setNonce((n) => n + 1) };
}

function useTransactions(uid) {
  const [state, setState] = React.useState({ txns: [], loading: true });
  const [nonce, setNonce] = React.useState(0);
  React.useEffect(() => {
    if (!uid) { setState({ txns: [], loading: false }); return; }
    let active = true;
    setState((s) => ({ ...s, loading: true }));
    fbReady().then((fb) => {
      if (!fb) return;
      fb.listTransactions(uid).then((rows) => { if (active) setState({ txns: rows, loading: false }); })
        .catch(() => { if (active) setState({ txns: [], loading: false }); });
    });
    return () => { active = false; };
  }, [uid, nonce]);
  return { ...state, reload: () => setNonce((n) => n + 1) };
}

/* Notifications addressed to one user (info requests from admin). */
function useUserNotifications(uid) {
  const [state, setState] = React.useState({ notes: [], loading: true });
  const [nonce, setNonce] = React.useState(0);
  React.useEffect(() => {
    if (!uid) { setState({ notes: [], loading: false }); return; }
    let active = true;
    fbReady().then((fb) => {
      if (!fb) return;
      fb.listUserNotifications(uid).then((rows) => { if (active) setState({ notes: rows, loading: false }); })
        .catch(() => { if (active) setState({ notes: [], loading: false }); });
    });
    return () => { active = false; };
  }, [uid, nonce]);
  return { ...state, reload: () => setNonce((n) => n + 1) };
}

/* All active notifications (admin overview). */
function useAllNotifications() {
  const [state, setState] = React.useState({ notes: [], loading: true });
  const [nonce, setNonce] = React.useState(0);
  React.useEffect(() => {
    let active = true;
    fbReady().then((fb) => {
      if (!fb) return;
      fb.listAllNotifications().then((rows) => { if (active) setState({ notes: rows, loading: false }); })
        .catch(() => { if (active) setState({ notes: [], loading: false }); });
    });
    return () => { active = false; };
  }, [nonce]);
  return { ...state, reload: () => setNonce((n) => n + 1) };
}

/* Derive a brand's participating clippers from its submissions. */
function clippersFromSubmissions(subs) {
  const map = new Map();
  subs.forEach((s) => {
    const key = s.clipperUid || s.clipper;
    if (!key) return;
    const cur = map.get(key) || { name: s.clipper || 'Klipotājs', uid: s.clipperUid, clips: 0, views: 0, earned: 0, approved: 0 };
    cur.clips += 1;
    cur.views += Number(s.viewCount) || 0;
    if (s.review === 'approved') { cur.approved += 1; cur.earned += payableForSub(s, null); }
    map.set(key, cur);
  });
  return [...map.values()].map((c) => ({ ...c, earned: round2(c.earned) })).sort((a, b) => b.views - a.views);
}

const round2 = (n) => Math.round((n + Number.EPSILON) * 100) / 100;
function tsToDate(ts) {
  if (!ts || !ts.seconds) return '—';
  return new Date(ts.seconds * 1000).toLocaleDateString('lv-LV', { day: 'numeric', month: 'short' });
}

const DAY = 86400000;
const PLATEAU_DAYS = 7;
/* A clip auto-finishes once PLATEAU_DAYS pass without a ≥1 000-view gain.
   We track the last growth moment on the submission (lastGrowthAt); without
   live view tracking this defaults to creation time, so a clip that stops
   growing is considered "done" after the plateau window. */
function isPlateaued(sub) {
  const ref = (sub.lastGrowthAt && sub.lastGrowthAt.seconds) || (sub.createdAt && sub.createdAt.seconds);
  if (!ref) return false;
  return (Date.now() - ref * 1000) > PLATEAU_DAYS * DAY;
}
/* Effective status shown to the brand, driven by the moderation state
   (`review`): rejected/pending pass through; approved clips read as
   "finished" once they plateau, otherwise "active". */
function effectiveSubStatus(sub) {
  if (sub.review === 'rejected') return 'rejected';
  if (sub.review === 'pending') return 'pending';
  return isPlateaued(sub) ? 'finished' : 'active'; // approved
}
/* Amount owed to a clipper for a submission, honouring the campaign's optional
   minimum-views payout threshold. */
function payableForSub(sub, campaign) {
  // Fall back to the submission's own rate/minViews snapshot when the campaign
  // no longer exists (e.g. brand deleted it) so earnings still resolve.
  const rate = campaign ? campaign.rate : (Number(sub.rate) || 0);
  const min = campaign ? (campaign.minViews || 0) : (Number(sub.minViews) || 0);
  const maxClip = campaign ? (campaign.maxPerClip || 0) : (Number(sub.maxPerClip) || 0);
  const start = Number(sub.startViews) || 0;
  const v = Number(sub.viewCount != null ? sub.viewCount : sub.views) || 0;
  // Earnings count only views GAINED above the submission baseline.
  let gained = Math.max(0, v - start);
  if (min > 0 && v < min) gained = 0; // total hasn't crossed the payout threshold yet
  // Clippers are paid per FULL 1,000 views only — floor to the nearest 1,000.
  const paidThousands = Math.floor(gained / 1000);
  let amount = round2(paidThousands * rate);
  // Per-clip earnings cap: once a clip hits it, it stops earning further.
  if (maxClip > 0) amount = Math.min(amount, maxClip);
  return amount;
}
/* Whether a clip has hit its per-clip earnings cap (shown as "Pabeigts"). */
function clipMaxedOut(sub, campaign) {
  const maxClip = campaign ? (campaign.maxPerClip || 0) : (Number(sub.maxPerClip) || 0);
  if (maxClip <= 0) return false;
  return payableForSub(sub, campaign) >= maxClip;
}

/* FIFO budget allocation across a campaign's clips.
   The campaign budget is a hard ceiling: clips are paid in submission order
   (oldest first) until the budget runs out. Later clips get whatever remains
   — possibly nothing. Returns a Map of submissionId → payable amount. */
function allocateCampaignPayouts(subs, campaign) {
  const budget = campaign
    ? Number(campaign.budgetTotal != null ? campaign.budgetTotal : campaign.budget) || 0
    : 0;
  const order = [...(subs || [])].sort((a, b) => {
    const ta = (a.createdAt && a.createdAt.seconds) || 0;
    const tb = (b.createdAt && b.createdAt.seconds) || 0;
    return ta - tb; // oldest submission paid first
  });
  const out = new Map();
  let left = budget > 0 ? budget : Infinity;
  for (const s of order) {
    if (s.reviewStatus === 'rejected' || s.review === 'rejected') { out.set(s.id, 0); continue; }
    const want = payableForSub(s, campaign);
    const give = Math.max(0, Math.min(want, round2(left)));
    out.set(s.id, round2(give));
    left = round2(left - give);
    if (left <= 0) left = 0;
  }
  return out;
}
/* Whether a campaign's budget is fully committed (no room for new clips). */
function campaignBudgetExhausted(subs, campaign) {
  const budget = campaign
    ? Number(campaign.budgetTotal != null ? campaign.budgetTotal : campaign.budget) || 0
    : 0;
  if (budget <= 0) return false;
  const committed = [...(subs || [])].reduce((t, s) => t + payableForSub(s, campaign), 0);
  return round2(committed) >= budget;
}

Object.assign(window, { round2, isPlateaued, effectiveSubStatus, payableForSub, clipMaxedOut, allocateCampaignPayouts, campaignBudgetExhausted, mapSubmission, canonPlatform, extractVideoId, PLATFORM_LABEL });

/* Clipper dashboard data: joined campaigns + own submissions → enriched view. */
function useClipperData(uid) {
  const [state, setState] = React.useState({ campaigns: [], clips: [], loading: true });
  const [nonce, setNonce] = React.useState(0);
  React.useEffect(() => {
    if (!uid) { setState({ campaigns: [], clips: [], loading: false }); return; }
    let active = true;
    setState((s) => ({ ...s, loading: true }));
    fbReady().then(async (fb) => {
      if (!fb) return;
      try {
        const [allRaw, mySubsRaw, joinedIds] = await Promise.all([
          fb.listCampaigns(),
          fb.listMySubmissions(uid),
          fb.getJoinedCampaigns(uid).catch(() => []),
        ]);
        if (!active) return;
        const mySubs = mySubsRaw.map(mapSubmission);
        const all = allRaw.map(mapCampaign);
        const byId = new Map(all.map((c) => [c.id, c]));

        // clips derived from my submissions
        const clips = mySubs.map((s) => {
          const camp = byId.get(s.campaignId);
          // earned survives campaign deletion via the submission's rate snapshot
          const earned = payableForSub(s, camp);
          return {
            id: s.id, grad: camp ? camp.grad : ['#2563EB', '#3B82F6'],
            date: tsToDate(s.createdAt), campaign: s.campaignName || (camp && camp.name) || '—',
            platform: s.platformLabel, views: s.viewCount, status: s.review, earned, url: s.url || '',
            paidAmount: Number(s.paidAmount) || 0,
            withdrawnAmount: Number(s.withdrawnAmount) || 0,
            cid: s.campaignId,
            // extra fields for dispute + explicit clip lifecycle state
            disputed: !!s.disputed, frozen: !!s.frozen, fraudReview: !!s.fraudReview, disputeStatus: s.disputeStatus,
            reviewStatus: s.review, campaignDeleted: !camp,
            clipState: window.clipState ? window.clipState({ ...s, reviewStatus: s.review }, camp) : s.review,
            brandUid: s.brandUid, clipperUid: s.clipperUid, campaignName: s.campaignName,
          };
        }).sort((a, b) => (b.id > a.id ? 1 : -1));

        // participating campaigns = explicit joins ∪ campaigns I've submitted to.
        // Paused/deleted campaigns are kept (with a status) so the clipper sees
        // that the campaign was stopped, rather than silently disappearing.
        const nameById = {};
        mySubs.forEach((s) => { if (s.campaignId && s.campaignName) nameById[s.campaignId] = s.campaignName; });
        const partIds = new Set([...joinedIds, ...mySubs.map((s) => s.campaignId)].filter(Boolean));
        const campaigns = [...partIds]
          // Archived (removed-from-list) campaigns leave the list but their
          // clips still count toward lifetime totals below.
          .filter((id) => mySubs.some((s) => s.campaignId === id && !s.archived) || joinedIds.includes(id))
          .map((id) => {
          const camp = byId.get(id);
          const mine = clips.filter((c) => c.cid === id);
          const subIds = mySubs.filter((s) => s.campaignId === id).map((s) => s.id);
          const base = {
            cid: id, id, clips: mine.length, subIds,
            views: mine.reduce((s, c) => s + c.views, 0),
            earned: round2(mine.reduce((s, c) => s + (c.status === 'approved' ? c.earned : 0), 0)),
          };
          if (!camp) {
            // campaign doc no longer exists → brand deleted it
            return { ...base, brand: nameById[id] || 'Kampaņa', initials: (nameById[id] || 'K').slice(0, 2).toUpperCase(), grad: ['#94A3B8', '#CBD5E1'], rate: 0, status: 'deleted', ended: true };
          }
          // ended = explicitly inactive, or the end date has passed
          const past = camp.end ? (new Date(camp.end).getTime() < Date.now()) : false;
          return { ...base, brand: camp.brand, initials: camp.initials, grad: camp.grad, rate: camp.rate, end: camp.end, status: camp.status || 'active', ended: camp.status !== 'active' || past, isOpenForSubmissions: camp.isOpenForSubmissions !== false, isLowBudget: !!camp.isLowBudget };
        });

        setState({ campaigns, clips, loading: false });
      } catch (e) { if (active) setState({ campaigns: [], clips: [], loading: false }); }
    });
    return () => { active = false; };
  }, [uid, nonce]);
  return { ...state, reload: () => setNonce((n) => n + 1) };
}

/* Admin: all users enriched with derived stats from campaigns + submissions. */
function useAdminData() {
  const [state, setState] = React.useState({ users: [], campaigns: [], submissions: [], loading: true });
  const [nonce, setNonce] = React.useState(0);
  React.useEffect(() => {
    let active = true;
    setState((s) => ({ ...s, loading: true }));
    fbReady().then(async (fb) => {
      if (!fb) return;
      try {
        const [usersRaw, campsRaw, subsRaw] = await Promise.all([
          fb.listUsers(), fb.listCampaigns(), fb.listAllSubmissions(),
        ]);
        if (!active) return;
        const subs = subsRaw.map(mapSubmission);
        const rawCampaigns = campsRaw.map(mapCampaign);

        // ---- Per-campaign aggregation (single source of truth) ----
        // Roll submissions up per campaign so Admin/Brand/Clipper all read the
        // same figures: total/verified/pending views, distinct clippers.
        const agg = {};
        subs.forEach((s) => {
          const id = s.campaignId; if (!id) return;
          const a = agg[id] || (agg[id] = { views: 0, verifiedViews: 0, pendingViews: 0, clipperSet: new Set() });
          const v = s.viewCount || 0;
          a.views += v;
          if (s.review === 'approved') a.verifiedViews += v;
          else if (s.review !== 'rejected') a.pendingViews += v;
          if (s.clipperUid) a.clipperSet.add(s.clipperUid);
        });
        // Merge aggregates onto each campaign (override the stored `views`/
        // `clippers` with the aggregated counts so tables stay consistent).
        const campaigns = rawCampaigns.map((c) => {
          const a = agg[c.id] || { views: 0, verifiedViews: 0, pendingViews: 0, clipperSet: new Set() };
          return {
            ...c,
            views: a.views,
            verifiedViews: a.verifiedViews,
            pendingViews: a.pendingViews,
            clippers: a.clipperSet.size,
          };
        });
        const byId = new Map(campaigns.map((c) => [c.id, c]));

        // ---- Daily activity aggregation (last 30 days) ----
        // Two event series from stored submissions: clips submitted per day
        // and views generated per day (bucketed by submission date).
        const DAYS = 30;
        const dayMs = 86400000;
        const today = new Date(); today.setHours(0, 0, 0, 0);
        const clipsPerDay = new Array(DAYS).fill(0);
        const viewsPerDay = new Array(DAYS).fill(0);
        subs.forEach((s) => {
          const secs = s.createdAt && s.createdAt.seconds;
          if (!secs) return;
          const d = new Date(secs * 1000); d.setHours(0, 0, 0, 0);
          const idx = DAYS - 1 - Math.round((today - d) / dayMs);
          if (idx >= 0 && idx < DAYS) { clipsPerDay[idx] += 1; viewsPerDay[idx] += (s.viewCount || 0); }
        });
        const activity = { clipsPerDay, viewsPerDay };

        const users = usersRaw.map((u) => {
          const initials = (u.displayName || u.email || '?').trim().split(/\s+/).map((w) => w[0]).slice(0, 2).join('').toUpperCase();
          const grad = gradForName(u.displayName || u.email || '?');
          const joined = u.createdAt && u.createdAt.seconds ? new Date(u.createdAt.seconds * 1000).toLocaleDateString('lv-LV') : '—';
          const base = { name: u.displayName || u.email || 'Lietotājs', email: u.email || '', role: u.role || 'clipper', initials, grad, joined, uid: u.uid, joinedTs: (u.createdAt && u.createdAt.seconds) || 0, photoURL: u.photoURL || null, photoPath: u.photoPath || null };
          if (u.role === 'brand') {
            const mine = campaigns.filter((c) => c.brandUid === u.uid);
            // Per-brand total views = sum of viewCount across the brand's submissions.
            const brandViews = subs.filter((s) => s.brandUid === u.uid).reduce((sum, s) => sum + (s.viewCount || 0), 0);
            return { ...base, approved: u.approved !== false, spent: mine.reduce((s, c) => s + (c.spent || 0), 0), totalViews: brandViews, activeCampaigns: mine.filter((c) => c.status === 'active').length, lastCampaign: mine[0] ? mine[0].name : '—' };
          }
          if (u.role === 'clipper') {
            const mySubs = subs.filter((s) => s.clipperUid === u.uid);
            const enrich = (s) => { const c = byId.get(s.campaignId); return { name: s.campaignName || (c && c.name) || '—', views: s.viewCount, earned: payableForSub(s, c), status: s.review }; };
            const all = mySubs.map(enrich);
            return { ...base, totalPayouts: round2(all.filter((x) => x.status === 'approved').reduce((s, x) => s + x.earned, 0)), totalViews: all.reduce((s, x) => s + x.views, 0), active: all.filter((x) => x.status !== 'rejected'), finished: all.filter((x) => x.status === 'approved') };
          }
          return base; // admin
        });

        // Total platform views = sum of viewCount across ALL submissions.
        const totalViews = subs.reduce((sum, s) => sum + (s.viewCount || 0), 0);
        // Self-heal the public clipper count from the real users list.
        try { const n = users.filter((u) => u.role === 'clipper').length; if (window.KlipzyFirebase && window.KlipzyFirebase.setPublicClipperCount) window.KlipzyFirebase.setPublicClipperCount(n); } catch (_) {}
        setState({ users, campaigns, submissions: subs, totalViews, activity, loading: false });
      } catch (e) { if (active) setState({ users: [], campaigns: [], submissions: [], totalViews: 0, activity: { clipsPerDay: [], viewsPerDay: [] }, loading: false }); }
    });
    return () => { active = false; };
  }, [nonce]);
  return { ...state, reload: () => setNonce((n) => n + 1) };
}

const ADMIN_GRADS = [['#F43F5E', '#FB7185'], ['#2563EB', '#3B82F6'], ['#7C3AED', '#A78BFA'], ['#0EA5E9', '#38BDF8'], ['#16A34A', '#4ADE80'], ['#F59E0B', '#FBBF24']];
function gradForName(s) { let h = 0; for (let i = 0; i < (s || '').length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; return ADMIN_GRADS[h % ADMIN_GRADS.length]; }

/* Public platform-wide stats for the landing-page hero card.
   Reads active campaigns + all submissions and rolls them up into:
   totalViews (verified clip views), paidOut (€ actually paid to clippers),
   activeCampaigns (count), plus a per-campaign feed (views + payout) and the
   most recent payout for the floating badge. Best-effort: any read failure
   leaves `ready:false` so the caller falls back to its static mock. */
function usePlatformStats() {
  const [state, setState] = React.useState({ ready: false, loading: true, totalViews: 0, paidOut: 0, activeCampaigns: 0, feed: [], lastPayout: null });
  React.useEffect(() => {
    let active = true;
    fbReady().then(async (fb) => {
      if (!fb) { if (active) setState((s) => ({ ...s, loading: false })); return; }
      try {
        const [campsRaw, subsRaw] = await Promise.all([fb.listCampaigns(), fb.listAllSubmissions()]);
        if (!active) return;
        const subs = subsRaw.map(mapSubmission);
        const campaigns = campsRaw.map(mapCampaign);
        const activeCamps = campaigns.filter((c) => c.status === 'active');

        // Roll submissions up per campaign.
        const agg = {};
        let totalViews = 0, paidOut = 0;
        subs.forEach((s) => {
          const v = s.viewCount || 0;
          if (s.review === 'approved') totalViews += v;
          paidOut += Number(s.paidAmount) || 0;
          const id = s.campaignId; if (!id) return;
          const a = agg[id] || (agg[id] = { views: 0, paid: 0 });
          if (s.review === 'approved') a.views += v;
          a.paid += Number(s.paidAmount) || 0;
        });

        const feed = activeCamps.map((c) => ({
          cid: c.id, brand: c.brand, initials: c.initials, grad: c.grad,
          rate: c.rate, views: (agg[c.id] && agg[c.id].views) || 0, paid: (agg[c.id] && agg[c.id].paid) || 0,
        })).sort((a, b) => b.views - a.views);

        // Distinct clippers who have submitted at least one clip.
        const clipperCount = new Set(subs.map((s) => s.clipperUid).filter(Boolean)).size;
        // Prefer the true registered-clipper count from the public stats doc.
        let regClipperCount = clipperCount;
        try { const ps = window.KlipzyFirebase && await window.KlipzyFirebase.getPublicStats(); if (ps && ps.clipperCount != null) regClipperCount = ps.clipperCount; } catch (_) {}

        // Most recent payout across all submissions (for the floating badge).
        let lastPayout = null;
        subs.forEach((s) => {
          const amt = Number(s.paidAmount) || 0; if (amt <= 0) return;
          const ts = (s.lastPaidAt && s.lastPaidAt.seconds) || (s.createdAt && s.createdAt.seconds) || 0;
          if (!lastPayout || ts > lastPayout.ts) lastPayout = { ts, amount: amt, campaign: s.campaignName || '' };
        });

        setState({ ready: true, loading: false, totalViews, paidOut: round2(paidOut), activeCampaigns: activeCamps.length, clipperCount: regClipperCount, feed, lastPayout });
      } catch (e) { if (active) setState((s) => ({ ...s, loading: false })); }
    });
    return () => { active = false; };
  }, []);
  return state;
}

/* Admin: all clipper + brand cash-out requests, enriched with requester name. */
function useAdminCashouts(users) {
  const [state, setState] = React.useState({ rows: [], loading: true });
  const [nonce, setNonce] = React.useState(0);
  React.useEffect(() => {
    let active = true;
    setState((s) => ({ ...s, loading: true }));
    fbReady().then(async (fb) => {
      if (!fb || !fb.listAllCashouts) { if (active) setState({ rows: [], loading: false }); return; }
      try {
        const rows = await fb.listAllCashouts();
        if (!active) return;
        setState({ rows, loading: false });
      } catch (_) { if (active) setState({ rows: [], loading: false }); }
    });
    return () => { active = false; };
  }, [nonce]);
  return { ...state, reload: () => setNonce((n) => n + 1) };
}

Object.assign(window, {
  useCampaigns, useBrandCampaigns, useBrandSubmissions, useTransactions,
  useClipperData, useAdminData, useAdminCashouts, useUserNotifications, useAllNotifications, usePlatformStats,
  mapCampaign, clippersFromSubmissions, daysUntil, fmtDeadline,
});
