// analytics-engine.jsx — Analytics data model
// ────────────────────────────────────────────────────────────────
// Pure data layer: derive period-filtered metrics across 7 dimensions
// (Market / Artist / Work / Recording / Release / Video / Platform).
// Cross-filter store: a single { dim, key } selection that all tabs respect.
// Anomaly overlays: pull from window.__ANOMALIES (Leaks engine) and
// flag z-score outliers from internal series.
// ────────────────────────────────────────────────────────────────
(function () {
  if (typeof window === 'undefined') return;
  if (window.ANALYTICS_ENGINE) return;

  // ─── seeded RNG so the synthetic series is stable across renders ───
  function mulberry(seed) {
    let t = seed >>> 0;
    return function () {
      t |= 0; t = (t + 0x6D2B79F5) | 0;
      let r = Math.imul(t ^ (t >>> 15), 1 | t);
      r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r;
      return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
    };
  }
  function hash(s) { let h = 2166136261 >>> 0; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; }

  // ─── period helpers ────────────────────────────────────────────────
  const NOW = Date.now();
  const DAY = 86_400_000;
  function periodToDays(p) {
    if (p === '30d') return 30;
    if (p === '90d') return 90;
    if (p === 'ytd') {
      const d = new Date();
      const start = new Date(d.getFullYear(), 0, 1).getTime();
      return Math.max(1, Math.round((NOW - start) / DAY));
    }
    if (p === 'ttm') return 365;
    if (typeof p === 'number') return p;
    return 90;
  }

  // ─── platforms (with stable colors that read against bg + bg-2) ────
  const PLATFORMS = [
    { k: 'spotify',     l: 'Spotify',     c: '#1DB954', share: 0.41 },
    { k: 'apple',       l: 'Apple Music', c: '#FA243C', share: 0.18 },
    { k: 'youtube',     l: 'YouTube',     c: '#FF0033', share: 0.11 },
    { k: 'amazon',      l: 'Amazon',      c: '#FF9900', share: 0.07 },
    { k: 'tidal',       l: 'Tidal',       c: '#00C2D7', share: 0.04 },
    { k: 'tiktok',      l: 'TikTok',      c: '#69C9D0', share: 0.05 },
    { k: 'pandora',     l: 'Pandora',     c: '#3668FF', share: 0.03 },
    { k: 'deezer',      l: 'Deezer',      c: '#A238FF', share: 0.025 },
    { k: 'soundcloud',  l: 'SoundCloud',  c: '#FF5500', share: 0.02 },
    { k: 'sync',        l: 'Sync · Film/TV', c: '#6e6a60', share: 0.06 },
    { k: 'pro',         l: 'PRO · Performance', c: '#1a4ed8', share: 0.045 },
    { k: 'mech',        l: 'Mechanical · MLC/HFA', c: '#0a8754', share: 0.02 },
  ];

  // ─── markets / territories with ISO codes for the choropleth ─────
  const MARKETS = [
    { k: 'US', l: 'United States', code: 'US', region: 'NA', share: 0.34, lat: 39, lng: -97 },
    { k: 'GB', l: 'United Kingdom', code: 'GB', region: 'EU', share: 0.09, lat: 54, lng: -2 },
    { k: 'DE', l: 'Germany',        code: 'DE', region: 'EU', share: 0.07, lat: 51, lng: 10 },
    { k: 'FR', l: 'France',         code: 'FR', region: 'EU', share: 0.05, lat: 46, lng: 2 },
    { k: 'JP', l: 'Japan',          code: 'JP', region: 'AS', share: 0.06, lat: 36, lng: 138 },
    { k: 'BR', l: 'Brazil',         code: 'BR', region: 'SA', share: 0.05, lat: -10, lng: -55 },
    { k: 'MX', l: 'Mexico',         code: 'MX', region: 'NA', share: 0.04, lat: 23, lng: -102 },
    { k: 'AU', l: 'Australia',      code: 'AU', region: 'OC', share: 0.03, lat: -25, lng: 134 },
    { k: 'CA', l: 'Canada',         code: 'CA', region: 'NA', share: 0.04, lat: 56, lng: -106 },
    { k: 'KR', l: 'South Korea',    code: 'KR', region: 'AS', share: 0.025, lat: 36, lng: 128 },
    { k: 'IN', l: 'India',          code: 'IN', region: 'AS', share: 0.025, lat: 21, lng: 79 },
    { k: 'NL', l: 'Netherlands',    code: 'NL', region: 'EU', share: 0.02, lat: 52, lng: 5 },
    { k: 'SE', l: 'Sweden',         code: 'SE', region: 'EU', share: 0.018, lat: 60, lng: 18 },
    { k: 'IT', l: 'Italy',          code: 'IT', region: 'EU', share: 0.018, lat: 42, lng: 12 },
    { k: 'ES', l: 'Spain',          code: 'ES', region: 'EU', share: 0.022, lat: 40, lng: -3 },
    { k: 'AR', l: 'Argentina',      code: 'AR', region: 'SA', share: 0.012, lat: -34, lng: -64 },
    { k: 'CO', l: 'Colombia',       code: 'CO', region: 'SA', share: 0.011, lat: 4, lng: -74 },
    { k: 'NG', l: 'Nigeria',        code: 'NG', region: 'AF', share: 0.01, lat: 9, lng: 8 },
    { k: 'ZA', l: 'South Africa',   code: 'ZA', region: 'AF', share: 0.008, lat: -29, lng: 24 },
    { k: 'PH', l: 'Philippines',    code: 'PH', region: 'AS', share: 0.012, lat: 13, lng: 122 },
  ];

  function pf(k) { return PLATFORMS.find(p => p.k === k); }
  function mk(k) { return MARKETS.find(m => m.k === k); }

  // ─── source datasets ───────────────────────────────────────────────
  const sourceWorks       = () => (window.WORKS || []).slice(0, 200);
  const sourceRecordings  = () => (window.RECORDINGS || []).slice(0, 250);
  const sourceReleases    = () => (window.RELEASES || []).slice(0, 80);
  const sourceArtists     = () => (window.ARTISTS || []).filter(a => a.type === 'Person' || a.type === 'Group').slice(0, 60);
  const sourceVideos      = () => (window.VIDEOS || window.__VIDEOS || []).slice(0, 50);

  // ─── synthetic earnings/streams generators ────────────────────────
  // Each entity gets a deterministic earnings curve over `days` days.
  function dailySeries(seedKey, days, opts = {}) {
    const rng = mulberry(hash(seedKey));
    const baseDaily = (opts.baseDaily || 200);
    const trend = opts.trend != null ? opts.trend : (rng() - 0.45) * 0.004;
    const weekly = 0.18;       // weekend lift
    const monthly = 0.10;      // payout cadence
    const noise = 0.22;
    const out = [];
    for (let i = 0; i < days; i++) {
      const t = NOW - (days - 1 - i) * DAY;
      const dow = new Date(t).getDay();
      const dom = new Date(t).getDate();
      const wkLift = (dow === 5 || dow === 6) ? (1 + weekly) : 1;
      const moLift = (dom <= 5) ? (1 + monthly) : 1;
      const trendLift = 1 + trend * (i - days / 2);
      const noiseLift = 1 + (rng() - 0.5) * noise;
      const v = Math.max(0, baseDaily * wkLift * moLift * trendLift * noiseLift);
      out.push({ t, v, streams: v * (4_500 + rng() * 2_500) });
    }
    return out;
  }

  // Allocate a daily total across markets according to share (with per-entity deviation)
  function allocByMarket(seedKey, total, opts = {}) {
    const rng = mulberry(hash(seedKey + ':mkt'));
    const dev = opts.dev || 0.5;
    const skew = MARKETS.map(m => m.share * (1 + (rng() - 0.5) * dev));
    const sum = skew.reduce((s, v) => s + v, 0);
    return MARKETS.map((m, i) => ({ market: m.k, value: total * skew[i] / sum }));
  }
  function allocByPlatform(seedKey, total, opts = {}) {
    const rng = mulberry(hash(seedKey + ':plat'));
    const dev = opts.dev || 0.5;
    const skew = PLATFORMS.map(p => p.share * (1 + (rng() - 0.5) * dev));
    const sum = skew.reduce((s, v) => s + v, 0);
    return PLATFORMS.map((p, i) => ({ platform: p.k, value: total * skew[i] / sum }));
  }

  // ─── KPI for a series (with PoP if comparePeriod === true) ────────
  function kpiFromSeries(series, opts = {}) {
    const total = series.reduce((s, p) => s + p.v, 0);
    const totalStreams = series.reduce((s, p) => s + p.streams, 0);
    const half = Math.floor(series.length / 2);
    const cur = series.slice(half).reduce((s, p) => s + p.v, 0);
    const prev = series.slice(0, half).reduce((s, p) => s + p.v, 0);
    const pop = prev === 0 ? 0 : (cur - prev) / prev;
    return { total, totalStreams, pop, cur, prev };
  }

  // Map daily value series → 7-day moving average for line charts
  function smooth(series, w = 7) {
    return series.map((p, i) => {
      const lo = Math.max(0, i - w + 1);
      const win = series.slice(lo, i + 1);
      return { t: p.t, v: win.reduce((s, x) => s + x.v, 0) / win.length };
    });
  }

  // ─── per-dimension data construction ────────────────────────────────
  function buildDim(dim, ctx) {
    const days = ctx.days;
    if (dim === 'market') {
      const rows = MARKETS.map((m, idx) => {
        const ser = dailySeries('mkt:' + m.k, days, { baseDaily: 7000 * m.share + 50, trend: idx % 3 === 0 ? 0.002 : -0.001 });
        const k = kpiFromSeries(ser);
        return { id: m.k, label: m.l, code: m.code, region: m.region, lat: m.lat, lng: m.lng, share: m.share,
                 total: k.total, streams: k.totalStreams, pop: k.pop, series: ser };
      });
      return rows.sort((a, b) => b.total - a.total);
    }
    if (dim === 'platform') {
      return PLATFORMS.map(p => {
        const ser = dailySeries('plat:' + p.k, days, { baseDaily: 5000 * p.share + 50 });
        const k = kpiFromSeries(ser);
        return { id: p.k, label: p.l, color: p.c, share: p.share, total: k.total, streams: k.totalStreams, pop: k.pop, series: ser };
      }).sort((a, b) => b.total - a.total);
    }
    if (dim === 'artist') {
      return sourceArtists().slice(0, 36).map((a, i) => {
        const ser = dailySeries('art:' + a.id, days, { baseDaily: 60 + (36 - i) * 12 });
        const k = kpiFromSeries(ser);
        return { id: a.id, label: a.name, role: a.role || a.type, country: a.country, total: k.total, streams: k.totalStreams, pop: k.pop, series: ser };
      }).sort((a, b) => b.total - a.total);
    }
    if (dim === 'work') {
      return sourceWorks().slice(0, 40).map((w, i) => {
        const ser = dailySeries('wrk:' + (w.id || w.iswc || i), days, { baseDaily: 30 + (40 - i) * 8 });
        const k = kpiFromSeries(ser);
        return { id: w.id || ('w_' + i), label: w.title, iswc: w.iswc, writers: w.writers, total: k.total, streams: k.totalStreams, pop: k.pop, series: ser, releaseDate: w.year };
      }).sort((a, b) => b.total - a.total);
    }
    if (dim === 'recording') {
      return sourceRecordings().slice(0, 40).map((r, i) => {
        const ser = dailySeries('rec:' + (r.id || r.isrc || i), days, { baseDaily: 25 + (40 - i) * 7 });
        const k = kpiFromSeries(ser);
        return { id: r.id || ('r_' + i), label: r.title, isrc: r.isrc, artist: r.artist, total: k.total, streams: k.totalStreams, pop: k.pop, series: ser, releaseDate: r.releaseDate };
      }).sort((a, b) => b.total - a.total);
    }
    if (dim === 'release') {
      return sourceReleases().slice(0, 30).map((rel, i) => {
        const ser = dailySeries('rel:' + (rel.id || rel.upc || i), days, { baseDaily: 80 + (30 - i) * 18 });
        const k = kpiFromSeries(ser);
        return { id: rel.id || ('rel_' + i), label: rel.title, upc: rel.upc, artist: rel.artist, type: rel.type, releaseDate: rel.releaseDate, total: k.total, streams: k.totalStreams, pop: k.pop, series: ser };
      }).sort((a, b) => b.total - a.total);
    }
    if (dim === 'video') {
      return sourceVideos().slice(0, 24).map((v, i) => {
        const ser = dailySeries('vid:' + (v.id || i), days, { baseDaily: 18 + (24 - i) * 5 });
        const k = kpiFromSeries(ser);
        return { id: v.id || ('v_' + i), label: v.title, kind: v.kind, recId: v.recordingId || v.recId, total: k.total, streams: k.totalStreams * 1.4, pop: k.pop, series: ser, releaseDate: v.publishedAt };
      }).sort((a, b) => b.total - a.total);
    }
    return [];
  }

  // ─── flow: market → DSP for sankey ────────────────────────────────
  function buildSankey(ctx) {
    const total = 1_500_000 * ctx.days / 90;
    const flows = [];
    MARKETS.forEach(m => {
      const mTotal = total * m.share;
      const skew = mulberry(hash('flow:' + m.k));
      PLATFORMS.forEach(p => {
        const dev = (skew() - 0.5) * 0.6;
        flows.push({ from: m.k, to: p.k, value: mTotal * p.share * (1 + dev) });
      });
    });
    return flows;
  }

  // ─── cohort: release year × age (in months) → earnings ───────────
  function buildCohort(ctx) {
    const releases = sourceReleases().slice(0, 30);
    const cohorts = {};
    releases.forEach(r => {
      const year = r.releaseDate ? new Date(r.releaseDate).getFullYear() : 2022;
      const ageMonths = Math.max(0, Math.round((NOW - new Date(r.releaseDate || NOW).getTime()) / (30 * DAY)));
      if (!cohorts[year]) cohorts[year] = { year, monthly: {} };
    });
    Object.values(cohorts).forEach(c => {
      const rng = mulberry(hash('cohort:' + c.year));
      const decay = c.year < 2020 ? 0.985 : c.year < 2023 ? 0.97 : 0.94;
      let v = (2026 - c.year) < 4 ? 80_000 : 30_000;
      for (let m = 0; m <= 36; m++) {
        c.monthly[m] = v * (1 + (rng() - 0.5) * 0.2);
        v *= decay;
      }
    });
    return Object.values(cohorts).sort((a, b) => b.year - a.year);
  }

  // ─── funnel (Release → Save → Playlist Add → Stream → Repeat) ──
  function buildFunnel(ctx) {
    const stages = [
      { k: 'release',    l: 'Release impression', n: 1_400_000 },
      { k: 'preview',    l: 'Track preview',      n:   720_000 },
      { k: 'save',       l: 'Library save',       n:   180_000 },
      { k: 'playlist',   l: 'Playlist add',       n:    62_000 },
      { k: 'stream',     l: 'Full stream',        n:   810_000 },
      { k: 'repeat',     l: 'Repeat (3+ plays)',  n:   240_000 },
      { k: 'follow',     l: 'Artist follow',      n:    18_400 },
    ];
    return stages;
  }

  // ─── concentration / pareto for any dim ──────────────────────────
  function paretoCurve(rows) {
    const total = rows.reduce((s, r) => s + r.total, 0);
    let acc = 0;
    return rows.map((r, i) => {
      acc += r.total;
      return { i, label: r.label, share: r.total / total, cumShare: acc / total };
    });
  }

  // ─── anomalies overlay ────────────────────────────────────────────
  function getAnomaliesFor(dim, id) {
    const all = window.__ANOMALIES || [];
    return all.filter(a => a.dim === dim && (a.id === id || a.entity === id));
  }
  function flagSeries(series) {
    const vals = series.map(p => p.v);
    const mean = vals.reduce((s, v) => s + v, 0) / vals.length;
    const sd = Math.sqrt(vals.reduce((s, v) => s + (v - mean) * (v - mean), 0) / vals.length);
    return series.map(p => ({ ...p, anomaly: sd > 0 && Math.abs(p.v - mean) > 2.5 * sd ? Math.sign(p.v - mean) : 0 }));
  }

  // ─── cross-filter store (subscribers re-render on change) ────────
  const xfilter = { dim: null, id: null, label: null };
  const subs = new Set();
  function setXFilter(next) {
    if (!next || next.dim == null) {
      xfilter.dim = null; xfilter.id = null; xfilter.label = null;
    } else {
      xfilter.dim = next.dim; xfilter.id = next.id; xfilter.label = next.label;
    }
    subs.forEach(fn => { try { fn(); } catch (e) { console.warn(e); } });
  }
  function subscribeXFilter(fn) { subs.add(fn); return () => subs.delete(fn); }

  // ─── apply cross-filter to a row list (decimates totals based on the
  // selected entity's share with this dim) ───────────────────────────
  function applyXFilter(rows, dim) {
    if (!xfilter.dim || xfilter.dim === dim) return rows;
    // Heuristic decimation — represents "what share of <selected entity>
    // was driven by <this row>". Hash-based, deterministic.
    const seedBase = `xf:${xfilter.dim}:${xfilter.id}:${dim}:`;
    return rows.map(r => {
      const rng = mulberry(hash(seedBase + r.id));
      const factor = 0.04 + rng() * 0.18;
      const series = r.series.map(p => ({ ...p, v: p.v * factor, streams: p.streams * factor }));
      const k = kpiFromSeries(series);
      return { ...r, series, total: k.total, streams: k.totalStreams, pop: k.pop, _xf: true };
    });
  }

  // ─── public API ───────────────────────────────────────────────────
  window.ANALYTICS_ENGINE = {
    PLATFORMS, MARKETS, pf, mk,
    periodToDays,
    buildDim,
    buildSankey, buildCohort, buildFunnel, paretoCurve,
    kpiFromSeries, smooth, flagSeries,
    getAnomaliesFor,
    xfilter, setXFilter, subscribeXFilter, applyXFilter,
    dailySeries, allocByMarket, allocByPlatform,
  };
})();
