/* enterprise-landing-viz.jsx — shared visual + data primitives for the public
 * Globally landing page (#/business).
 *
 * TWO JOBS.
 *
 * 1. GEOGRAPHY. WorldMap draws a real, recognisable world silhouette from
 *    world-map.js (Natural Earth 110m via world-atlas@2 — the same source the
 *    HDI globe renders), projected equirectangularly. Markers are placed at
 *    true country centroids. Nothing on this map is decorative: every point
 *    corresponds to a country the surrounding UI is actually talking about.
 *
 * 2. THE CORPUS. Every number the page renders is derived at runtime from
 *    funding-opportunities.json — the same artefact the authenticated
 *    Enterprise workspace falls back to (MX.loadOpportunities). Records are
 *    normalised into the SAME shape MX.normOpp produces, so the landing page
 *    can hand them straight to the real matching engine (window.MX.match)
 *    rather than inventing a marketing version of it.
 *
 * If a figure cannot be computed honestly it comes back null and its UI is
 * omitted. Nothing here hard-codes a count, a coverage claim or a total.
 *
 * Read-only. No new endpoint, no Supabase call, no auth.
 *
 * Exposes window.MiraViz.
 */
(function () {
  'use strict';
  var h = React.createElement;

  // ── Geography ────────────────────────────────────────────────────────────
  // Approximate country centroids for the countries that actually appear in
  // the funding corpus and in the article corpus, including the World Bank's
  // own naming ("Kyrgyz Republic", "Egypt, Arab Republic of"). Reference
  // geography, not derived data.
  var CENTROIDS = {
    'Afghanistan': [33.9, 67.7], 'Albania': [41.2, 20.2], 'Azerbaijan': [40.1, 47.6],
    'Bangladesh': [23.7, 90.4], 'Bhutan': [27.5, 90.4], 'Bosnia and Herzegovina': [43.9, 17.7],
    'Brazil': [-14.2, -51.9], 'Cambodia': [12.6, 104.9], 'Cameroon': [7.4, 12.4],
    'China': [35.9, 104.2], 'Colombia': [4.6, -74.3], "Cote d'Ivoire": [7.5, -5.5],
    'Egypt, Arab Republic of': [26.8, 30.8], 'Egypt': [26.8, 30.8], 'Ethiopia': [9.1, 40.5],
    'Guatemala': [15.8, -90.2], 'Honduras': [15.2, -86.2], 'India': [20.6, 79.0],
    'Indonesia': [-0.8, 113.9], 'Iraq': [33.2, 43.7], 'Kazakhstan': [48.0, 66.9],
    'Kenya': [-0.02, 37.9], 'Kyrgyz Republic': [41.2, 74.8],
    "Lao People's Democratic Republic": [19.9, 102.5], 'Liberia': [6.4, -9.4],
    'Madagascar': [-18.8, 46.9], 'Malawi': [-13.3, 34.3], 'Maldives': [3.2, 73.2],
    'Mexico': [23.6, -102.6], 'Moldova': [47.4, 28.4], 'Mongolia': [46.9, 103.8],
    'Myanmar': [21.9, 95.96], 'Nepal': [28.4, 84.1], 'Nigeria': [9.1, 8.7],
    'Pakistan': [30.4, 69.3], 'Papua New Guinea': [-6.3, 143.9], 'Philippines': [12.9, 121.8],
    'Poland': [51.9, 19.1], 'Serbia': [44.0, 21.0], 'Sierra Leone': [8.5, -11.8],
    'Somalia': [5.2, 46.2], 'South Africa': [-30.6, 22.9], 'South Sudan': [7.9, 30.0],
    'Sri Lanka': [7.9, 80.8], 'Sudan': [12.9, 30.2], 'Syrian Arab Republic': [34.8, 39.0],
    'Tajikistan': [38.9, 71.3], 'Tanzania': [-6.4, 34.9], 'Thailand': [15.9, 101.0],
    'Togo': [8.6, 0.8], 'Turkiye': [38.96, 35.2], 'Turkmenistan': [38.97, 59.6],
    'Uganda': [1.4, 32.3], 'Ukraine': [48.4, 31.2], 'United States': [39.8, -98.6],
    'Uzbekistan': [41.4, 64.6], 'Viet Nam': [14.06, 108.3], 'West Bank and Gaza': [31.9, 35.2],
    'Yemen': [15.6, 48.5], 'Zambia': [-13.1, 27.8], 'Zimbabwe': [-19.0, 29.2],
    // Regional lending vehicles the World Bank publishes without a single country.
    'West Africa I': [9.0, -2.0],
  };

  // Equirectangular, matching the projection world-map.js was generated with.
  function project(lat, lng) { return [lng + 180, 90 - lat]; }
  function centroid(name) { return CENTROIDS[name] || null; }

  // ── Formatters ───────────────────────────────────────────────────────────
  function num(n) { return Number(n).toLocaleString('en-GB'); }
  function shortDate(iso) {
    try {
      return new Date(iso).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
    } catch (e) { return ''; }
  }
  function daysUntil(iso) {
    try { return Math.round((new Date(iso) - Date.now()) / 86400000); } catch (e) { return null; }
  }
  // World Bank publishes ISO; Grants.gov publishes MM/DD/YYYY.
  function parseLoose(s) {
    if (!s) return null;
    var m = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(String(s));
    var d = m ? new Date(+m[3], +m[1] - 1, +m[2]) : new Date(s);
    return isNaN(d.getTime()) ? null : d;
  }

  // ── Corpus → the shape the real matching engine expects ─────────────────
  // Deliberately mirrors MX.normOpp (enterprisex-core.jsx) field for field, so
  // window.MX.match() can score these records without a translation layer. If
  // MX ever changes its shape this is the one place the landing page follows.
  function normaliseOpp(raw, i) {
    var perMax = raw.amount_max != null ? raw.amount_max : null;
    var perMin = raw.amount_min != null ? raw.amount_min : null;
    var envelope = raw.total_budget != null ? raw.total_budget : null;
    var analytical = perMax != null ? perMax : (perMin != null ? perMin : null);
    var amountType = perMax != null ? 'per_award_max'
      : perMin != null ? 'per_award_min'
      : envelope != null ? 'total_programme_budget' : 'not_published';
    var dl = raw.deadline && !isNaN(new Date(raw.deadline)) ? new Date(raw.deadline) : null;
    var MX = window.MX;
    return {
      id: raw.id || ('opp-' + i),
      title: String(raw.display_title || raw.title || '').trim() || 'Untitled opportunity',
      funder: String(raw.funder || raw.source_name || 'Unknown funder').trim(),
      funderType: raw.funder_type || null,
      type: raw.opportunity_type || raw.record_type || 'opportunity',
      recordClass: raw.record_class || 'opportunity',
      status: raw.status || (dl && dl > new Date() ? 'verified_open' : 'status_unclear'),
      summary: raw.summary || null,
      sectors: Array.isArray(raw.sectors) ? raw.sectors : [],
      countries: Array.isArray(raw.countries) ? raw.countries : [],
      subregions: Array.isArray(raw.subregions) ? raw.subregions : [],
      regions: Array.isArray(raw.regions) ? raw.regions : [],
      eligible: Array.isArray(raw.eligible) ? raw.eligible : [],
      eligibilityText: raw.eligibility_text || null,
      deadline: dl ? dl.toISOString() : null,
      amountMin: perMin, amountMax: perMax, totalBudget: envelope,
      currency: raw.currency || null,
      analyticalGBP: MX && MX.money ? MX.money.toGBP(analytical, raw.currency) : null,
      amountType: amountType,
      url: raw.url || raw.original_url || raw.source_url || null,
      sourceName: raw.source_name || null,
      trust: raw.trust_level || 'unclear',
      evidence: raw.evidence_snippet || null,
      posted: raw.posted_date || raw.extracted_at || null,
    };
  }

  var _cache = null;
  function loadCorpus() {
    if (_cache) return _cache;
    _cache = fetch('funding-opportunities.json', { cache: 'force-cache' })
      .then(function (r) { if (!r.ok) throw new Error('corpus ' + r.status); return r.json(); })
      .catch(function () { return null; });
    return _cache;
  }

  function tally(list, key) {
    var m = Object.create(null);
    list.forEach(function (o) {
      (o[key] || []).forEach(function (v) { if (v) m[v] = (m[v] || 0) + 1; });
    });
    return Object.keys(m).map(function (k) { return { label: k, n: m[k] }; })
      .sort(function (a, b) { return b.n - a.n; });
  }

  function summarise(raw) {
    if (!raw || !raw.opportunities || !raw.opportunities.length) return null;
    var opps = raw.opportunities.map(normaliseOpp)
      .filter(function (o) { return o.recordClass !== 'source'; });
    var sources = {};
    opps.forEach(function (o) { if (o.sourceName) sources[o.sourceName] = 1; });
    return {
      total: opps.length,
      opps: opps,
      countries: tally(opps, 'countries'),
      subregions: tally(opps, 'subregions'),
      regions: tally(opps, 'regions'),
      sectors: tally(opps, 'sectors'),
      countryCount: tally(opps, 'countries').length,
      sourceCount: Object.keys(sources).length,
      lastUpdated: raw.generated_at || null,
    };
  }

  function useFundingIntel() {
    var s = React.useState(null); var intel = s[0], setIntel = s[1];
    React.useEffect(function () {
      var alive = true;
      loadCorpus().then(function (raw) { if (alive) setIntel(summarise(raw)); });
      return function () { alive = false; };
    }, []);
    return intel;
  }

  // Score the whole corpus with the REAL engine and group by its own ordering.
  // Returns null when MX has not loaded — the caller shows nothing rather than
  // a marketing approximation of the verdicts.
  function scoreCorpus(intel, org) {
    var MX = window.MX;
    if (!intel || !MX || !MX.match) return null;
    var groups = {}; (MX.ORDER || []).forEach(function (g) { groups[g] = []; });
    var scored = intel.opps.map(function (o) {
      var m = MX.match(o, org);
      (groups[m.overall] = groups[m.overall] || []).push({ opp: o, match: m });
      return { opp: o, match: m };
    });
    var shortlist = (groups['Strong candidate'] || []).concat(groups['Worth reviewing'] || []);
    return { scored: scored, groups: groups, order: MX.ORDER, shortlist: shortlist };
  }

  // ── Motion + viewport helpers ────────────────────────────────────────────
  function usePrefersReducedMotion() {
    var s = React.useState(false); var v = s[0], set = s[1];
    React.useEffect(function () {
      var mq = window.matchMedia('(prefers-reduced-motion: reduce)');
      set(mq.matches);
      var on = function () { set(mq.matches); };
      mq.addEventListener ? mq.addEventListener('change', on) : mq.addListener(on);
      return function () { mq.removeEventListener ? mq.removeEventListener('change', on) : mq.removeListener(on); };
    }, []);
    return v;
  }

  // True while the element is anywhere near the viewport, so the demo stops
  // advancing (and burning frames) when it is scrolled well away.
  function useNearViewport(ref) {
    var s = React.useState(true); var near = s[0], set = s[1];
    React.useEffect(function () {
      if (!ref.current || typeof IntersectionObserver === 'undefined') return;
      var io = new IntersectionObserver(function (es) { set(es[0].isIntersecting); },
        { rootMargin: '160px 0px' });
      io.observe(ref.current);
      return function () { io.disconnect(); };
    }, []);
    return near;
  }

  function useMediaQuery(q) {
    var s = React.useState(false); var v = s[0], set = s[1];
    React.useEffect(function () {
      var mq = window.matchMedia(q); set(mq.matches);
      var on = function () { set(mq.matches); };
      mq.addEventListener ? mq.addEventListener('change', on) : mq.addListener(on);
      return function () { mq.removeEventListener ? mq.removeEventListener('change', on) : mq.removeListener(on); };
    }, [q]);
    return v;
  }

  // ── WorldMap ─────────────────────────────────────────────────────────────
  // A real world silhouette, one <path> per country so the strokes ARE national
  // borders. The default viewBox crops the empty polar bands (roughly 80°N to
  // 52°S) so the inhabited world fills the frame.
  //
  // markers: [{ lat, lng, tone: 'quiet'|'funding'|'signal'|'home', active, label }]
  // Anything without real coordinates is dropped by the caller, never guessed.
  function WorldMap(p) {
    var W = window.WORLD_MAP;
    var markers = (p.markers || []).filter(function (m) {
      return typeof m.lat === 'number' && typeof m.lng === 'number';
    });
    return h('svg', {
      className: 'gw' + (p.className ? ' ' + p.className : ''),
      viewBox: p.viewBox || '0 12 360 130',
      preserveAspectRatio: 'xMidYMid meet',
      role: 'img', 'aria-label': p.label || 'World map',
    },
      W ? h('g', { className: 'gw-land' }, W.countries.map(function (d, i) {
        return h('path', { key: i, d: d });
      })) : null,
      markers.map(function (m, i) {
        var xy = project(m.lat, m.lng);
        var r = m.r || (m.active ? 4 : m.tone === 'quiet' ? 2.2 : 3.2);
        return h('g', {
          key: (m.label || '') + i,
          className: 'gw-m gw-m--' + (m.tone || 'quiet') + (m.active ? ' is-active' : ''),
        },
          m.active ? h('circle', { className: 'gw-ring', cx: xy[0], cy: xy[1], r: r }) : null,
          // A halo so a marker still reads where it sits on land.
          h('circle', { className: 'gw-halo', cx: xy[0], cy: xy[1], r: r + 1.6 }),
          h('circle', { className: 'gw-dot', cx: xy[0], cy: xy[1], r: r }),
          m.label ? h('title', null, m.label) : null);
      }));
  }

  // Corpus countries → plottable markers. Countries without a known centroid
  // are skipped rather than guessed at.
  function countryMarkers(names, tone, limit) {
    var out = [];
    (names || []).forEach(function (n) {
      var name = typeof n === 'string' ? n : n.label;
      var c = centroid(name);
      if (c && out.length < (limit || 40)) out.push({ label: name, lat: c[0], lng: c[1], tone: tone || 'quiet' });
    });
    return out;
  }

  window.MiraViz = {
    CENTROIDS: CENTROIDS,
    project: project,
    centroid: centroid,
    num: num,
    shortDate: shortDate,
    daysUntil: daysUntil,
    parseLoose: parseLoose,
    normaliseOpp: normaliseOpp,
    summarise: summarise,
    useFundingIntel: useFundingIntel,
    scoreCorpus: scoreCorpus,
    usePrefersReducedMotion: usePrefersReducedMotion,
    useNearViewport: useNearViewport,
    useMediaQuery: useMediaQuery,
    WorldMap: WorldMap,
    countryMarkers: countryMarkers,
  };
})();
