// data.jsx — Thrsty Meta Ads data engine
// Loads data.json (snapshot from Google Sheet 1apLPiWH...) and exposes helpers
// used by Overview / Campaigns / Creatives tabs. Pull fresh: `python3 pull_data.py`.

const RANGES = {
  '7d':  { label: 'Last 7 days',    days: 7,   buckets: { daily: 7,  weekly: 1,  monthly: 1, quarterly: 1 }, sub: 'vs previous 7 days'    },
  '30d': { label: 'Last 30 days',   days: 30,  buckets: { daily: 30, weekly: 4,  monthly: 1, quarterly: 1 }, sub: 'vs previous 30 days'   },
  '60d': { label: 'Last 60 days',   days: 60,  buckets: { daily: 60, weekly: 8,  monthly: 2, quarterly: 1 }, sub: 'vs previous 60 days'   },
  'ytd': { label: 'Since Mar 1',    days: 'ytd', buckets: { daily: 90, weekly: 12, monthly: 3, quarterly: 1 }, sub: 'full reporting period' },
  'custom': { label: 'Custom range', days: 0, buckets: { daily: 90, weekly: 26, monthly: 12, quarterly: 4 }, sub: 'vs previous period' },
};

let RAW = null;
let MIN_DATE = null;
let MAX_DATE = null;

function parseISO(s) { const [y, m, d] = s.split('-').map(Number); return new Date(Date.UTC(y, m - 1, d)); }
function fmtISO(d)   { return d.toISOString().slice(0, 10); }
function addDays(d, n) { const x = new Date(d.getTime()); x.setUTCDate(x.getUTCDate() + n); return x; }

function loadReal() {
  return fetch('data.json', { cache: 'no-cache' })
    .then(r => r.json())
    .then(d => {
      RAW = d;
      MIN_DATE = parseISO(d.meta.minDate);
      MAX_DATE = parseISO(d.meta.maxDate);
      return d;
    });
}

function rangeBounds(range, refDate = MAX_DATE, customStart = null, customEnd = null) {
  if (range === 'custom' && customStart && customEnd) {
    const start = parseISO(customStart);
    const end   = parseISO(customEnd);
    const days  = Math.round((end - start) / 86400000) + 1;
    const prevEnd   = addDays(start, -1);
    const prevStart = addDays(prevEnd, -(days - 1));
    return { start, end, prevStart, prevEnd, days };
  }
  const end = new Date((refDate || MAX_DATE).getTime());
  if (range === 'ytd') {
    // For Thrsty: "Since Mar 1" = full reporting period (min date in dataset).
    const start = new Date(MIN_DATE.getTime());
    const days = Math.round((end - start) / 86400000) + 1;
    const prevEnd   = addDays(start, -1);
    const prevStart = addDays(prevEnd, -(days - 1));
    return { start, end, prevStart, prevEnd, days };
  }
  const days = RANGES[range].days;
  const start = addDays(end, -(days - 1));
  const prevEnd = addDays(start, -1);
  const prevStart = addDays(prevEnd, -(days - 1));
  return { start, end, prevStart, prevEnd, days };
}

// ----- Aggregation helpers -----

const NUM_FIELDS = [
  'spend','impressions','reach','clicks','linkClicks','lpvs','videoViews',
  'postEng','saves','messages',
];

function emptyAgg() {
  const o = {};
  for (const f of NUM_FIELDS) o[f] = 0;
  return o;
}

function addRow(o, r) {
  for (const f of NUM_FIELDS) o[f] += r[f] || 0;
}

function deriveMetrics(o) {
  o.cpc      = o.linkClicks > 0 ? o.spend / o.linkClicks : 0;
  o.cpm      = o.impressions > 0 ? (o.spend / o.impressions) * 1000 : 0;
  o.ctr      = o.impressions > 0 ? (o.clicks / o.impressions) * 100 : 0;
  o.linkCtr  = o.impressions > 0 ? (o.linkClicks / o.impressions) * 100 : 0;
  o.frequency = o.reach > 0 ? o.impressions / o.reach : 0;
  o.costPerLpv = o.lpvs > 0 ? o.spend / o.lpvs : 0;
  o.costPerVv  = o.videoViews > 0 ? o.spend / o.videoViews : 0;
  return o;
}

function rowsBetween(start, end, campaignFilter = null) {
  const s = fmtISO(start), e = fmtISO(end);
  let arr = (RAW.daily || []).filter(r => r.d >= s && r.d <= e);
  if (campaignFilter && campaignFilter !== 'all') {
    arr = arr.filter(r => r.campaign === campaignFilter);
  }
  return arr;
}

// Aggregate (sum) all rows in a window — typically multiple rows per date (one per campaign).
function aggregate(rows) {
  const o = emptyAgg();
  for (const r of rows) addRow(o, r);
  return deriveMetrics(o);
}

// Group rows by date, return one merged row per date (sum across campaigns).
function mergeByDate(rows) {
  const acc = {};
  for (const r of rows) {
    const t = acc[r.d] = acc[r.d] || Object.assign({ d: r.d }, emptyAgg());
    addRow(t, r);
  }
  return Object.values(acc).map(deriveMetrics).sort((a,b) => a.d.localeCompare(b.d));
}

// Bucket merged-by-date rows into N buckets by granularity.
function bucketRows(rows, granularity, start, end) {
  if (!rows.length) return { labels: [], rows: [] };
  let bucketSize;
  const dayCount = Math.round((end - start) / 86400000) + 1;
  if (granularity === 'daily') bucketSize = 1;
  else if (granularity === 'weekly') bucketSize = 7;
  else if (granularity === 'monthly') bucketSize = Math.max(28, Math.round(dayCount / 12));
  else if (granularity === 'quarterly') bucketSize = Math.max(60, Math.round(dayCount / 4));
  else bucketSize = 1;

  const buckets = [];
  for (let cursor = new Date(start.getTime()); cursor <= end; ) {
    const bEnd = addDays(cursor, bucketSize - 1);
    const stop = bEnd > end ? end : bEnd;
    const inB = rows.filter(r => r.d >= fmtISO(cursor) && r.d <= fmtISO(stop));
    const agg = emptyAgg();
    for (const r of inB) addRow(agg, r);
    deriveMetrics(agg);
    buckets.push({ start: new Date(cursor.getTime()), end: stop, agg });
    cursor = addDays(stop, 1);
  }
  return { labels: buckets.map(b => labelFor(b.start, b.end, granularity)), rows: buckets.map(b => b.agg) };
}

function labelFor(start, end, gran) {
  const M = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  if (gran === 'daily')      return `${start.getUTCDate()}/${start.getUTCMonth() + 1}`;
  if (gran === 'weekly')     return `${start.getUTCDate()}/${start.getUTCMonth() + 1}`;
  if (gran === 'monthly')    return `${M[start.getUTCMonth()]} ${String(start.getUTCFullYear()).slice(-2)}`;
  if (gran === 'quarterly')  return `Q${Math.floor(start.getUTCMonth() / 3) + 1} ${String(start.getUTCFullYear()).slice(-2)}`;
  return fmtISO(start);
}

// ----- Public API -----

function realKPIs(campaign, range, customStart = null, customEnd = null) {
  const b = rangeBounds(range, undefined, customStart, customEnd);
  const cur  = aggregate(rowsBetween(b.start,    b.end,    campaign));
  const prev = aggregate(rowsBetween(b.prevStart, b.prevEnd, campaign));
  return { cur, prev, bounds: b };
}

function realSeries(campaign, range, granularity, customStart = null, customEnd = null) {
  const b = rangeBounds(range, undefined, customStart, customEnd);
  const merged = mergeByDate(rowsBetween(b.start, b.end, campaign));
  const { labels, rows: bk } = bucketRows(merged, granularity, b.start, b.end);
  return {
    labels,
    spend:        bk.map(r => Math.round(r.spend)),
    impressions:  bk.map(r => r.impressions),
    clicks:       bk.map(r => r.clicks),
    linkClicks:   bk.map(r => r.linkClicks),
    lpvs:         bk.map(r => r.lpvs),
    videoViews:   bk.map(r => r.videoViews),
    cpc:          bk.map(r => +r.cpc.toFixed(2)),
    cpm:          bk.map(r => +r.cpm.toFixed(2)),
    ctr:          bk.map(r => +r.ctr.toFixed(2)),
    costPerLpv:   bk.map(r => +r.costPerLpv.toFixed(2)),
  };
}

function realSeriesPrev(campaign, range, granularity, customStart = null, customEnd = null) {
  const b = rangeBounds(range, undefined, customStart, customEnd);
  const merged = mergeByDate(rowsBetween(b.prevStart, b.prevEnd, campaign));
  const { rows: bk } = bucketRows(merged, granularity, b.prevStart, b.prevEnd);
  return {
    spend:      bk.map(r => Math.round(r.spend)),
    linkClicks: bk.map(r => r.linkClicks),
    lpvs:       bk.map(r => r.lpvs),
    cpc:        bk.map(r => +r.cpc.toFixed(2)),
  };
}

// Aggregate per campaign over the range (campaign performance card).
function realCampaignSplit(range, customStart = null, customEnd = null) {
  const b = rangeBounds(range, undefined, customStart, customEnd);
  const rows = rowsBetween(b.start, b.end);
  const prevRows = rowsBetween(b.prevStart, b.prevEnd);
  const byName = {};
  for (const r of rows) {
    const t = byName[r.campaign] = byName[r.campaign] || { name: r.campaign, agg: emptyAgg(), aggPrev: emptyAgg() };
    addRow(t.agg, r);
  }
  for (const r of prevRows) {
    const t = byName[r.campaign] = byName[r.campaign] || { name: r.campaign, agg: emptyAgg(), aggPrev: emptyAgg() };
    addRow(t.aggPrev, r);
  }
  return Object.values(byName).map(t => ({
    name: t.name,
    agg: deriveMetrics(t.agg),
    aggPrev: deriveMetrics(t.aggPrev),
  })).sort((a,b) => b.agg.spend - a.agg.spend);
}

function realCreatives() { return RAW.creatives || []; }

// Aggregate weekly breakdown rows over a date range, grouped by `key` (region/age/gender).
// Each row has shape { d, <key>, spend, impressions, clicks, linkClicks, ctr, cpc }.
// The week-start date `d` qualifies the row as "in range" if it falls in [start, end].
function aggregateBreakdown(rows, key, start, end) {
  const s = fmtISO(start), e = fmtISO(end);
  const inRange = rows.filter(r => r.d >= s && r.d <= e);
  const byKey = {};
  for (const r of inRange) {
    const k = r[key];
    const t = byKey[k] = byKey[k] || { [key]: k, spend: 0, impressions: 0, clicks: 0, linkClicks: 0 };
    t.spend       += r.spend       || 0;
    t.impressions += r.impressions || 0;
    t.clicks      += r.clicks      || 0;
    t.linkClicks  += r.linkClicks  || 0;
  }
  // Derive CTR + CPC from sums (more accurate than averaging per-week ratios)
  return Object.values(byKey).map(t => ({
    ...t,
    ctr: t.impressions > 0 ? (t.clicks / t.impressions) * 100 : 0,
    cpc: t.linkClicks  > 0 ? t.spend / t.linkClicks            : 0,
  }));
}

function realRegion(range, customStart, customEnd) {
  const b = rangeBounds(range, undefined, customStart, customEnd);
  return aggregateBreakdown(RAW.region || [], 'region', b.start, b.end);
}
function realAge(range, customStart, customEnd) {
  const b = rangeBounds(range, undefined, customStart, customEnd);
  return aggregateBreakdown(RAW.age || [], 'age', b.start, b.end);
}
function realGender(range, customStart, customEnd) {
  const b = rangeBounds(range, undefined, customStart, customEnd);
  return aggregateBreakdown(RAW.gender || [], 'gender', b.start, b.end);
}

function listCampaigns() {
  return Array.from(new Set((RAW.daily || []).map(r => r.campaign))).sort();
}

function realSparkline(campaign, metric, days = 14) {
  const end = MAX_DATE;
  const start = addDays(end, -(days - 1));
  const merged = mergeByDate(rowsBetween(start, end, campaign));
  return merged.map(r => {
    if (metric === 'spend')      return Math.round(r.spend);
    if (metric === 'linkClicks') return r.linkClicks;
    if (metric === 'lpvs')       return r.lpvs;
    if (metric === 'cpc')        return +r.cpc.toFixed(2);
    if (metric === 'ctr')        return +r.ctr.toFixed(2);
    if (metric === 'videoViews') return r.videoViews;
    if (metric === 'impressions') return r.impressions;
    return r[metric] || 0;
  });
}

window.ThrstyData = {
  RANGES,
  loadReal,
  rangeBounds,
  realKPIs,
  realSeries,
  realSeriesPrev,
  realCampaignSplit,
  realCreatives,
  realRegion,
  realAge,
  realGender,
  realSparkline,
  listCampaigns,
  RAW_META: () => (RAW && RAW.meta) || null,
  MIN_DATE: () => MIN_DATE,
  MAX_DATE: () => MAX_DATE,
};

// Pseudo-random fallback series (used by KPI when no real spark data passed).
function buildSeries(seed, base, vol, n, trend = 0.02) {
  let s = seed;
  const r = () => { s = (s * 9301 + 49297) % 233280; return s / 233280; };
  const out = []; let v = base;
  for (let i = 0; i < n; i++) {
    const noise = (r() - 0.5) * vol;
    const seasonal = Math.sin(i / Math.max(2, n / 6)) * vol * 0.4;
    v = base * (1 + trend * (i / n)) + noise + seasonal;
    out.push(Math.max(0, Math.round(v)));
  }
  return out;
}

// Compatibility shim — many shared components reference SilsalData.
// Mirror the API surface used by KPI / shell so we don't fork those files.
window.SilsalData = {
  RANGES,
  COUNTRIES: [],
  COUNTRY_KEY: {},
  rangeBounds,
  buildSeries,
  RAW_META: () => (RAW && RAW.meta) || null,
};
