/* ============================================================
   $LAND — LIVE data layer (NO fake data)
   window.LandData = { rarities, holders, buildings, rounds, map, stats, ... }
   ------------------------------------------------------------
   Everything here is REAL, fetched from the worker-backed API:
     /api/holders   — wallets holding >= 100k $LAND (map is built from BALANCE)
     /api/buildings — live MC + holder count of the 8 building tokens (pump.fun)
     /api/rounds    — SOL payout round history
     /api/stats     — headline numbers
   Before the token launches these return empty/zero, so the site shows honest
   empty states (no holders → "land not distributed yet"), and fills in by
   itself the moment the indexer sees the live mint — no redeploy.

   LandData is a STABLE object reference, mutated in place on refresh; a version
   counter + subscribe() let React re-render. Components keep reading D.holders
   etc. synchronously; they just start empty and update on the first poll.
   ============================================================ */
(function () {
  // ---- rarity tiers (whole-token balance -> rarity). SOURCE OF TRUTH:
  //      config/launch.toml [rarity] (mirrored in worker/lib/holders.mjs). ----
  const rarities = [
    { key: 'common',    label: 'Common',    min: 100000,    accent: '#69a255', ring: 'transparent', glow: 0, badge: '#69a255' },
    { key: 'uncommon',  label: 'Uncommon',  min: 300000,    accent: '#4f9f7a', ring: '#4f9f7a',     glow: 0, badge: '#4f9f7a' },
    { key: 'rare',      label: 'Rare',      min: 1000000,   accent: '#3f86b8', ring: '#3f86b8',     glow: 1, badge: '#3f86b8' },
    { key: 'epic',      label: 'Epic',      min: 3000000,   accent: '#7a6bd0', ring: '#7a6bd0',     glow: 2, badge: '#7a6bd0' },
    { key: 'legendary', label: 'Legendary', min: 10000000,  accent: '#d9b24a', ring: '#d9b24a',     glow: 3, badge: '#d9b24a' },
  ];
  function rarityOf(vol) { let r = rarities[0]; for (const t of rarities) if (vol >= t.min) r = t; return r; }
  const rarIndex = (key) => rarities.findIndex(r => r.key === key);
  const rarityKeyFromName = (name) => (name || '').toLowerCase();

  const TOTAL_SUPPLY = 1_000_000_000;
  const DECIMALS = 6;
  const PROGRAM_ID = 'CkKDCLu4g71eiD2eSr8wNcJeUHgkfvBSUSrgLPbS4Ewj';
  const LAND_MINT = 'HYoMirye3VjqMi4gogCueZcvdJCUAoeXuLdmBUMFpump';

  // Static building presentation (live MC/holders merged from /api/buildings).
  const BUILDING_STATIC = [
    { ticker: 'HALL',   key: 'HALL',   name: 'Town Hall', mint: 'EsH1pk5RJgczEY7ufAFzHp3ywVarzMkjmADzdc3Ppump', accent: '#d9b24a', hook: 'grand facade, flag, clock', narr: 'Heart of the settlement. $HALL holders set the agenda and call the rounds.' },
    { ticker: 'KEEP',   key: 'KEEP',   name: 'Keep',      mint: '3PjkRXzBvpfBV1vYC4SniveKms3f1bdgbFZoz8Nfpump', accent: '#8a8f98', hook: 'stone tower, battlements', narr: 'Defense of the territory. $KEEP guards an owner’s share from dilution.' },
    { ticker: 'HOME',   key: 'HOME',   name: 'House',     mint: 'GH9mV2pucpKzZbRLRAcpw3Lh94uVDV5CaPgMn6XCpump', accent: '#c25b3f', hook: 'simple cottage, pitched roof', narr: 'The basic unit of living. The most widely held building token.' },
    { ticker: 'MART',   key: 'MART',   name: 'Market',    mint: 'C6rDGBue6QjBqqnBd5USEG7Fo2n8BWAbEDX7dUVpump', accent: '#cf7b3a', hook: 'awnings, market stalls', narr: 'Trade hub. $MART boosts a parcel’s liquidity on the secondary market.' },
    { ticker: 'FORGE',  key: 'FORGE',  name: 'Forge',     mint: '2GpcojZ739Td1QQxkoQ9a7fyuMtt7sr89GoAboLYpump', accent: '#d4542a', hook: 'chimney, smoke, fire', narr: 'Production. $FORGE forges upgrades for the other buildings.' },
    { ticker: 'TEMPLE', key: 'TEMPLE', name: 'Temple',    mint: '6LjVkDjuc2kza1F3TpdfXmVm1qAfifGXoiBR1Q74pump', accent: '#7a6bd0', hook: 'spire, stained glass', narr: 'Cult of scarcity. $TEMPLE is about faith in the finiteness of land.' },
    { ticker: 'PORT',   key: 'PORT',   name: 'Harbor',    mint: '2mAadKnUiLXdCHCTMkxa6JKZJq5etgzpg1xp724Xpump', accent: '#3f86b8', hook: 'dock, boat, water', narr: 'Gateway to the world. $PORT opens bridges to other maps.' },
    { ticker: 'FARM',   key: 'FARM',   name: 'Farmstead', mint: '6npdnSGZUWz2sATSUCRpARASMpBJmPiXoEyDrdQppump', accent: '#c8a23a', hook: 'barn, fields', narr: 'Harvest of SOL. $FARM passively compounds your share of daily payouts.' },
  ];

  // ---- map packer: grow each holder a contiguous blob of `area` cells on a
  //      MAP_W×MAP_H grid. Deterministic per refresh (seeded by holder index),
  //      identical algorithm to the original — only the INPUT is now real
  //      holder areaPx instead of fabricated areas. ----
  const MAP_W = 40, MAP_H = 22;
  function rng(seed) { return function () { seed |= 0; seed = (seed + 0x9E3779B9) | 0; let t = Math.imul(seed ^ (seed >>> 16), 0x21f0aaad); t = Math.imul(t ^ (t >>> 15), 0x735a2d97); return ((t ^ (t >>> 15)) >>> 0) / 4294967296; }; }

  function buildMap(holders) {
    const cells = new Array(MAP_W * MAP_H).fill(null);
    const idx = (x, y) => y * MAP_W + x;
    const R = rng(0x1A4D5EED); // fixed seed → stable layout per holder set
    function growBlob(h) {
      const target = Math.max(1, Math.min(h.area, MAP_W * MAP_H));
      let sx, sy, tries = 0;
      do { sx = Math.floor(R() * MAP_W); sy = Math.floor(R() * MAP_H); tries++; } while (cells[idx(sx, sy)] !== null && tries < 600);
      if (cells[idx(sx, sy)] !== null) return;
      const frontier = [[sx, sy]]; const got = [];
      while (got.length < target && frontier.length) {
        const fi = Math.floor(R() * frontier.length);
        const [x, y] = frontier.splice(fi, 1)[0];
        if (x < 0 || y < 0 || x >= MAP_W || y >= MAP_H) continue;
        if (cells[idx(x, y)] !== null) continue;
        cells[idx(x, y)] = h.id; got.push([x, y]);
        const nb = [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]];
        for (const n of nb) if (R() < 0.85) frontier.push(n);
        if (frontier.length === 0) { const g = got[Math.floor(R() * got.length)]; frontier.push([g[0] + 1, g[1]], [g[0], g[1] + 1]); }
      }
      h.cells = got;
    }
    // big holders first so their territory stays contiguous
    [...holders].sort((a, b) => b.area - a.area).forEach(growBlob);
    return cells;
  }

  // ---- shape API rows into the holder objects the UI expects ----
  function shapeHolders(apiHolders) {
    return apiHolders.map((h, i) => {
      const whole = Number(BigInt(h.balance) / 10n ** BigInt(DECIMALS));
      return {
        id: i,
        handle: shortHandle(h.wallet),
        address: h.wallet,
        vol: whole,             // whole $LAND held
        rarity: rarityKeyFromName(h.rarity),
        area: h.areaPx,         // pixels = floor(whole/100k) — from BALANCE
        rank: h.rank,
        buildingsCount: h.buildings,
        buildingMult: h.buildingMult,
        builds: [],             // which building tickers held (filled from buildings API if exposed)
        income: 0,
        cells: [],
      };
    });
  }

  // Display handle from a wallet (no fake @names): short base58.
  function shortHandle(addr) { return addr.slice(0, 4) + '…' + addr.slice(-4); }

  // ---- the stable LandData object (mutated in place on refresh) ----
  const fmt = {
    sol: (n) => (n || 0).toLocaleString('en-US', { maximumFractionDigits: 2 }) + ' SOL',
    land: (n) => n >= 1e6 ? (n / 1e6).toFixed(n >= 1e7 ? 0 : 1) + 'M' : n >= 1e3 ? (n / 1e3).toFixed(0) + 'K' : '' + (n || 0),
    usd: (n) => '$' + ((n || 0) >= 1e6 ? (n / 1e6).toFixed(2) + 'M' : (n || 0) >= 1e3 ? (n / 1e3).toFixed(1) + 'K' : (n || 0).toFixed(2)),
    addr: (a, n = 4) => a ? a.slice(0, n) + '…' + a.slice(-n) : '',
  };

  const subs = new Set();
  const LandData = {
    rarities, rarityOf, rarIndex, fmt,
    PROGRAM_ID, LAND_MINT,
    MAP_W, MAP_H, idx: (x, y) => y * MAP_W + x,
    // live (start empty — honest, no fake)
    buildingDefs: BUILDING_STATIC.map(b => ({ ...b, marketCapUsd: 0, holderCount: 0, priceSol: 0, stage: 'unknown' })),
    holders: [],
    holderById: {},
    cells: new Array(MAP_W * MAP_H).fill(null),
    rounds: [],
    stats: emptyStats(),
    loaded: false,            // false until first successful refresh
    version: 0,               // bumps on each refresh → React re-renders
    subscribe(fn) { subs.add(fn); return () => subs.delete(fn); },
    async refresh() { return doRefresh(); },
  };

  function emptyStats() {
    return {
      marketcap: 0, mcChange: 0, price: 0, priceChange: 0,
      holders: 0, totalSupply: TOTAL_SUPPLY, wrappedSupply: 0,
      currentPool: 0, totalPaid: 0, nextRoundTs: 0,
      mapW: MAP_W, mapH: MAP_H, totalCells: MAP_W * MAP_H, ownedCells: 0, claimedPct: 0,
    };
  }

  function notify() { LandData.version++; subs.forEach(fn => { try { fn(); } catch (e) {} }); }

  async function getJson(url) {
    const r = await fetch(url, { cache: 'no-store' });
    if (!r.ok) throw new Error(url + ' ' + r.status);
    return r.json();
  }

  async function doRefresh() {
    try {
      const [hRes, bRes, rRes, sRes] = await Promise.all([
        getJson('/api/holders').catch(() => ({ holders: [] })),
        getJson('/api/buildings').catch(() => ({ buildings: [] })),
        getJson('/api/rounds').catch(() => ({ rounds: [], totalPaidLamports: '0' })),
        getJson('/api/stats').catch(() => ({})),
      ]);

      // --- buildings (live MC/holders merged onto static presentation) ---
      const liveB = new Map((bRes.buildings || []).map(b => [b.ticker, b]));
      LandData.buildingDefs = BUILDING_STATIC.map(b => {
        const l = liveB.get(b.ticker) || {};
        return { ...b, marketCapUsd: l.marketCapUsd || 0, holderCount: l.holderCount || 0, priceSol: l.priceSol || 0, stage: l.stage || 'unknown' };
      });

      // --- holders + map ---
      const holders = shapeHolders(hRes.holders || []);
      const cells = buildMap(holders);
      const holderById = {}; holders.forEach(h => { h.cells = h.cells || []; holderById[h.id] = h; });
      holders.forEach(h => { h.area = h.cells.length; });

      // payout weight = areaPx × buildingMult (same formula the distributor uses,
      // SPEC §3.5). share = weight / Σweight → real proportional payout share.
      let totalWeight = 0;
      holders.forEach(h => { h.weight = h.areaPx * (h.buildingMult || 1); totalWeight += h.weight; });
      holders.forEach(h => { h.share = totalWeight > 0 ? h.weight / totalWeight : 0; });

      // --- rounds ---
      const LAMPORTS = 1e9;
      const rounds = (rRes.rounds || []).map(r => ({
        round: r.round,
        amount: Number(BigInt(r.amountLamports)) / LAMPORTS,
        ts: r.ts || 0,
        tx: r.claimTxSig || '',
        wallets: r.recipients || 0,
      }));

      // --- stats ---
      const s = sRes || {};
      const ownedCells = cells.filter(c => c !== null).length;
      const totalPaid = Number(BigInt(rRes.totalPaidLamports || '0')) / LAMPORTS;
      LandData.stats = {
        marketcap: s.landMarketCapUsd || 0, mcChange: 0,
        price: 0, priceChange: 0,
        holders: s.holderCount || holders.length, totalSupply: TOTAL_SUPPLY,
        wrappedSupply: holders.reduce((a, h) => a + h.vol, 0),
        currentPool: 0, totalPaid, nextRoundTs: 0,
        mapW: MAP_W, mapH: MAP_H, totalCells: MAP_W * MAP_H, ownedCells,
        claimedPct: Math.round(1000 * ownedCells / (MAP_W * MAP_H)) / 10,
        solPriceUsd: s.solPriceUsd || 0,
        lastIndexMs: s.lastIndexMs || 0,
      };

      LandData.holders = holders;
      LandData.holderById = holderById;
      LandData.cells = cells;
      LandData.rounds = rounds;
      LandData.loaded = true;
      notify();
    } catch (e) {
      // keep last good (or empty) data; mark loaded so UI leaves the spinner.
      LandData.loaded = true;
      notify();
    }
  }

  window.LandData = LandData;

  // React hook: re-render on every refresh. Components call useLandData() to
  // subscribe; returns the version (changes → re-render).
  window.useLandData = function useLandData() {
    const [, force] = React.useReducer(x => x + 1, 0);
    React.useEffect(() => LandData.subscribe(force), []);
    return LandData;
  };

  // kick off first load + poll every 20s (live without being chatty).
  doRefresh();
  setInterval(doRefresh, 20000);
})();
