// brownie/engine.jsx — game state + tick + server-backed save/load.
// Authenticates via Telegram WebApp initData, persists to /api/* (Postgres on Neon).

const BE_TICK_HZ = 20;
const BE_SAVE_INTERVAL_MS = 30000; // 30s — keeps DB compute hours low. Hide/unload still flushes.
const BE_MIN_BOOT_MS = 3000;

function beDefaultState() {
  return {
    crumbs: 0,
    totalEarned: 0,
    totalTaps: 0,
    producers: {},
    upgrades: {},
    achievements: {},
    fudge: 0,
    lifetimeFudge: 0,
    prestigeCount: 0,
    bakeryName: null,
    lastDailyAt: null,
    dailyStreak: 0,
    boostUntil: null,
    referralCode: null,
    referralsCount: 0,
    totalStarsSpent: 0,
    lastTs: Date.now(),
  };
}

// 2× boost is active when boost_until is in the future. Cheap helper used
// from many places — receives the state and returns the multiplier (1 or 2).
function beBoostMult(state) {
  if (!state?.boostUntil) return 1;
  const t = new Date(state.boostUntil).getTime();
  return Number.isFinite(t) && t > Date.now() ? 2 : 1;
}

// ──────────────────────────────────────────────────────────────────────
// Network — auth + state load + save.
// ──────────────────────────────────────────────────────────────────────

async function beApi(path, { method = 'GET', token, body } = {}) {
  const headers = { 'Content-Type': 'application/json' };
  if (token) headers.Authorization = `Bearer ${token}`;
  const res = await fetch(path, { method, headers, body: body ? JSON.stringify(body) : undefined });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw Object.assign(new Error(err.error || `HTTP ${res.status}`), { status: res.status, ...err });
  }
  return res.json();
}

async function beLogin() {
  const tg = window.Telegram?.WebApp;
  const initData = tg?.initData || '';
  if (!initData) throw new Error('NO_TELEGRAM_INIT_DATA');
  const r = await beApi('/api/auth', { method: 'POST', body: { initData } });
  if (!r.token) throw new Error('AUTH_NO_TOKEN');
  return r.token;
}

async function beSaveState(token, state) {
  await beApi('/api/save', { method: 'POST', token, body: { state } });
}

// ──────────────────────────────────────────────────────────────────────
// Pure derived calculations — same as before.
// ──────────────────────────────────────────────────────────────────────

function bePriceOf(producer, owned) {
  return Math.ceil(producer.basePrice * Math.pow(1.15, owned));
}

function bePriceBulk(producer, owned, count) {
  const r = 1.15;
  const first = producer.basePrice * Math.pow(r, owned);
  const sum = first * (Math.pow(r, count) - 1) / (r - 1);
  return Math.ceil(sum);
}

function beGlobalMult(state) {
  let m = 1;
  for (const u of window.BE_UPGRADES) {
    if (state.upgrades[u.id] && u.globalMult) m *= u.globalMult;
  }
  m *= 1 + Object.keys(state.achievements).length * 0.01;
  m *= 1 + (state.fudge || 0) * 0.02;
  m *= beBoostMult(state);
  return m;
}

function beTapMult(state) {
  let m = 1;
  for (const u of window.BE_UPGRADES) {
    if (state.upgrades[u.id] && u.tapMult) m *= u.tapMult;
  }
  return m;
}

function beProducerBps(producer, state) {
  const owned = state.producers[producer.id] || 0;
  if (!owned) return 0;
  let m = 1;
  for (const u of window.BE_UPGRADES) {
    if (state.upgrades[u.id] && u.multiplier && u.multiplier.pid === producer.id) {
      m *= u.multiplier.value;
    }
  }
  return owned * producer.baseBps * m * beGlobalMult(state);
}

function beTotalBps(state) {
  let total = 0;
  for (const p of window.BE_PRODUCERS) total += beProducerBps(p, state);
  return total;
}

function beTapValue(state) {
  const base = 1 * beTapMult(state) * beGlobalMult(state);
  const bpsBonus = beTotalBps(state) * 0.001;
  return base + bpsBonus;
}

function beFudgeOnReset(state) {
  const earned = state.totalEarned;
  if (earned < 1e12) return 0;
  return Math.floor(Math.cbrt(earned / 1e12) * 10);
}

// ──────────────────────────────────────────────────────────────────────
// React hook — engine loop + actions.
// ──────────────────────────────────────────────────────────────────────

function useBrownieEngine() {
  const { useState, useEffect, useRef, useCallback } = React;
  const [state, setState] = useState(beDefaultState);
  const [status, setStatus] = useState('booting'); // booting | ready | error
  const [errorMsg, setErrorMsg] = useState(null);
  const [offlineGain, setOfflineGain] = useState(0);
  const stateRef = useRef(state);
  stateRef.current = state;
  const tokenRef = useRef(null);
  const dirtyRef = useRef(false);

  // Boot: Telegram init → auth → load server state. Hold the loading screen
  // for at least BE_MIN_BOOT_MS so the splash always reads.
  useEffect(() => {
    let cancelled = false;
    const bootStarted = Date.now();
    (async () => {
      try {
        // Telegram WebApp stubs exist even when the page is opened in a regular
        // browser, but calling most methods throws "WebAppMethodUnsupported".
        // Each call is wrapped so the "open in Telegram" screen can still render.
        const tg = window.Telegram?.WebApp;
        if (tg) {
          const safe = (fn) => { try { fn(); } catch {} };
          safe(() => tg.ready());
          safe(() => tg.expand?.());
          safe(() => tg.requestFullscreen?.());
          safe(() => tg.disableVerticalSwipes?.());
          safe(() => tg.setHeaderColor?.('#14080a'));
          safe(() => tg.setBackgroundColor?.('#14080a'));

          const syncFs = () => {
            try { document.body.classList.toggle('tg-fullscreen', !!tg.isFullscreen); } catch {}
          };
          syncFs();
          safe(() => tg.onEvent?.('fullscreenChanged', syncFs));
          safe(() => tg.onEvent?.('fullscreenFailed', syncFs));
        }
        const token = await beLogin();
        if (cancelled) return;
        tokenRef.current = token;

        const r = await beApi('/api/state', { token });
        if (cancelled) return;
        const loaded = { ...beDefaultState(), ...r.state, lastTs: Date.now() };

        const remaining = BE_MIN_BOOT_MS - (Date.now() - bootStarted);
        if (remaining > 0) await new Promise(res => setTimeout(res, remaining));
        if (cancelled) return;

        setState(loaded);
        setOfflineGain(Number(r.offlineGain) || 0);
        setStatus('ready');
      } catch (e) {
        console.error('[engine] boot failed', e);
        const remaining = BE_MIN_BOOT_MS - (Date.now() - bootStarted);
        if (remaining > 0) await new Promise(res => setTimeout(res, remaining));
        if (!cancelled) {
          setErrorMsg(e.message || 'boot_failed');
          setStatus('error');
        }
      }
    })();
    return () => { cancelled = true; };
  }, []);

  // Tick loop — only after boot.
  useEffect(() => {
    if (status !== 'ready') return;
    const interval = setInterval(() => {
      setState(prev => {
        const now = Date.now();
        const dt = Math.min(5, (now - prev.lastTs) / 1000);
        const bps = beTotalBps(prev);
        const gain = bps * dt;
        const next = {
          ...prev,
          crumbs: prev.crumbs + gain,
          totalEarned: prev.totalEarned + gain,
          lastTs: now,
        };
        let achDirty = false;
        const achievements = { ...next.achievements };
        for (const a of window.BE_ACHIEVEMENTS) {
          if (!achievements[a.id] && a.test(next)) {
            achievements[a.id] = true;
            achDirty = true;
          }
        }
        if (achDirty) next.achievements = achievements;
        return next;
      });
    }, 1000 / BE_TICK_HZ);
    return () => clearInterval(interval);
  }, [status]);

  // Autosave every 5s when dirty + on hide.
  useEffect(() => {
    if (status !== 'ready') return;
    const flush = () => {
      const token = tokenRef.current;
      if (!token || !dirtyRef.current) return;
      dirtyRef.current = false;
      beSaveState(token, stateRef.current).catch((e) => {
        dirtyRef.current = true; // retry next tick
        console.warn('[engine] save failed', e);
      });
    };
    const id = setInterval(flush, BE_SAVE_INTERVAL_MS);
    const onHide = () => {
      flush();
      const token = tokenRef.current;
      if (token && navigator.sendBeacon) {
        // Best-effort save on backgrounding.
        const blob = new Blob([JSON.stringify({ state: stateRef.current })], { type: 'application/json' });
        // sendBeacon can't set Authorization header — fall back to fetch keepalive.
        fetch('/api/save', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
          body: JSON.stringify({ state: stateRef.current }),
          keepalive: true,
        }).catch(() => {});
      }
    };
    window.addEventListener('beforeunload', onHide);
    document.addEventListener('visibilitychange', onHide);
    return () => {
      clearInterval(id);
      window.removeEventListener('beforeunload', onHide);
      document.removeEventListener('visibilitychange', onHide);
    };
  }, [status]);

  const markDirty = () => { dirtyRef.current = true; };

  const tap = useCallback(() => {
    setState(prev => {
      const v = beTapValue(prev);
      return {
        ...prev,
        crumbs: prev.crumbs + v,
        totalEarned: prev.totalEarned + v,
        totalTaps: prev.totalTaps + 1,
      };
    });
    markDirty();
  }, []);

  const buy = useCallback((producerId, count) => {
    setState(prev => {
      const p = window.BE_PRODUCERS.find(x => x.id === producerId);
      if (!p) return prev;
      const owned = prev.producers[producerId] || 0;
      const cost = bePriceBulk(p, owned, count);
      if (prev.crumbs < cost) return prev;
      return {
        ...prev,
        crumbs: prev.crumbs - cost,
        producers: { ...prev.producers, [producerId]: owned + count },
      };
    });
    markDirty();
  }, []);

  const buyUpgrade = useCallback((upgradeId) => {
    setState(prev => {
      const u = window.BE_UPGRADES.find(x => x.id === upgradeId);
      if (!u || prev.upgrades[upgradeId]) return prev;
      if (prev.crumbs < u.price) return prev;
      return {
        ...prev,
        crumbs: prev.crumbs - u.price,
        upgrades: { ...prev.upgrades, [upgradeId]: true },
      };
    });
    markDirty();
  }, []);

  const prestige = useCallback(() => {
    setState(prev => {
      const gained = beFudgeOnReset(prev);
      if (gained <= 0) return prev;
      const fresh = beDefaultState();
      fresh.fudge = (prev.fudge || 0) + gained;
      fresh.lifetimeFudge = (prev.lifetimeFudge || 0) + gained;
      fresh.prestigeCount = (prev.prestigeCount || 0) + 1;
      fresh.achievements = prev.achievements;
      fresh.lastTs = Date.now();
      return fresh;
    });
    markDirty();
  }, []);

  const dismissOfflineGain = useCallback(() => setOfflineGain(0), []);

  // Re-fetch state from the server (used after async events like a Stars purchase).
  const refreshState = useCallback(async () => {
    const token = tokenRef.current;
    if (!token) return null;
    try {
      const r = await beApi('/api/state', { token });
      const loaded = { ...beDefaultState(), ...r.state, lastTs: Date.now() };
      setState(loaded);
      return loaded;
    } catch (e) {
      console.warn('[engine] refreshState failed', e);
      return null;
    }
  }, []);

  const claimDaily = useCallback(async () => {
    const token = tokenRef.current;
    if (!token) return null;
    try {
      const r = await beApi('/api/daily', { method: 'POST', token, body: {} });
      // Server already added the crumbs and bumped the streak; refresh to mirror it.
      await refreshState();
      return r;
    } catch (e) {
      if (e.status === 409) return { error: 'ALREADY_CLAIMED', nextAt: e.nextAt };
      console.warn('[engine] claimDaily failed', e);
      return { error: e.message || 'failed' };
    }
  }, [refreshState]);

  // Trigger a Telegram Stars purchase.
  // productId ∈ { 'boost_2x_30m', 'boost_2x_4h', 'boost_2x_24h' }
  // Returns { status: 'paid' | 'cancelled' | 'failed' | 'pending' }.
  const buyBoost = useCallback(async (productId) => {
    const token = tokenRef.current;
    const tg = window.Telegram?.WebApp;
    if (!token || !tg?.openInvoice) return { status: 'unsupported' };

    let invoice;
    try {
      invoice = await beApi('/api/shop/boost', { method: 'POST', token, body: { productId } });
    } catch (e) {
      return { status: 'failed', reason: e.message };
    }

    return new Promise((resolve) => {
      tg.openInvoice(invoice.invoiceUrl, async (status) => {
        if (status === 'paid') {
          // Webhook applies the boost — give it a moment, then refresh.
          await new Promise(r => setTimeout(r, 800));
          await refreshState();
        }
        resolve({ status });
      });
    });
  }, [refreshState]);

  const setBakeryName = useCallback((name) => {
    const cleaned = (name || '').replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, 30);
    if (!cleaned) return;
    setState(prev => ({ ...prev, bakeryName: cleaned }));
    markDirty();
  }, []);

  const hardReset = useCallback(async () => {
    if (!confirm('Wipe all progress? This cannot be undone.')) return;
    const token = tokenRef.current;
    const fresh = beDefaultState();
    setState(fresh);
    if (token) {
      try { await beSaveState(token, fresh); } catch (e) { console.warn('[engine] reset save failed', e); }
    }
  }, []);

  return {
    state,
    status,
    errorMsg,
    offlineGain,
    tap, buy, buyUpgrade, prestige, hardReset, dismissOfflineGain, setBakeryName,
    claimDaily, buyBoost, refreshState,
    boostMult: beBoostMult(state),
    bps: beTotalBps(state),
    tapValue: beTapValue(state),
    globalMult: beGlobalMult(state),
    fudgeOnReset: beFudgeOnReset(state),
  };
}

Object.assign(window, {
  bePriceOf, bePriceBulk, beGlobalMult, beTapMult,
  beProducerBps, beTotalBps, beTapValue, beFudgeOnReset, beBoostMult,
  useBrownieEngine,
});
