// ExitOS · global state, demo data, mock price engine.
// localStorage-backed; no backend.

const STORAGE_KEY = "exitos:v1";

const DEMO_COINS = [
  // id matches CoinGecko ids so we could swap to live later
  { id: "bitcoin",          symbol: "BTC", name: "Bitcoin",         color: "#f7931a", basePrice: 98000,  vol: 0.0035 },
  { id: "ethereum",         symbol: "ETH", name: "Ethereum",        color: "#627eea", basePrice: 3650,   vol: 0.0055 },
  { id: "solana",           symbol: "SOL", name: "Solana",          color: "#14f195", basePrice: 215,    vol: 0.009  },
  { id: "ripple",           symbol: "XRP", name: "XRP",             color: "#23292f", basePrice: 2.40,   vol: 0.012  },
  { id: "internet-computer",symbol: "ICP", name: "Internet Computer",color:"#3b00b9", basePrice: 11.80,  vol: 0.014  },
  { id: "cardano",          symbol: "ADA", name: "Cardano",         color: "#0033ad", basePrice: 0.95,   vol: 0.011  },
  { id: "chainlink",        symbol: "LINK",name: "Chainlink",       color: "#2a5ada", basePrice: 22.5,   vol: 0.010  },
  { id: "avalanche-2",      symbol: "AVAX",name: "Avalanche",       color: "#e84142", basePrice: 38.2,   vol: 0.012  },
  { id: "dogecoin",         symbol: "DOGE",name: "Dogecoin",        color: "#c2a633", basePrice: 0.34,   vol: 0.015  },
  { id: "polkadot",         symbol: "DOT", name: "Polkadot",        color: "#e6007a", basePrice: 7.2,    vol: 0.010  },
  { id: "near",             symbol: "NEAR",name: "NEAR Protocol",   color: "#00c08b", basePrice: 5.8,    vol: 0.011  },
  { id: "render-token",     symbol: "RNDR",name: "Render",          color: "#cf1f49", basePrice: 7.4,    vol: 0.013  },
  { id: "matic-network",    symbol: "POL", name: "Polygon",         color: "#8247e5", basePrice: 0.42,   vol: 0.012  },
  { id: "uniswap",          symbol: "UNI", name: "Uniswap",         color: "#ff007a", basePrice: 11.8,   vol: 0.011  },
  { id: "aptos",            symbol: "APT", name: "Aptos",           color: "#000000", basePrice: 8.6,    vol: 0.012  },
  { id: "litecoin",         symbol: "LTC", name: "Litecoin",        color: "#a6a9aa", basePrice: 108,    vol: 0.008  },
];

const FX = { USD: 1, CAD: 1.36, EUR: 0.92, GBP: 0.79 };
const CURRENCY_SYMBOLS = { USD: "$", CAD: "CA$", EUR: "€", GBP: "£" };

const DEMO_HOLDINGS = [
  // Realistic-ish: bought during cycle bottom, riding into top
  { id: "h1", coinId: "bitcoin",           amount: 0.62,    avgBuyPrice: 38500 },
  { id: "h2", coinId: "ethereum",          amount: 9.4,     avgBuyPrice: 1820 },
  { id: "h3", coinId: "solana",            amount: 95,      avgBuyPrice: 28 },
  { id: "h4", coinId: "ripple",            amount: 14500,   avgBuyPrice: 0.55 },
  { id: "h5", coinId: "internet-computer", amount: 820,     avgBuyPrice: 5.40 },
];

// Default exit ladders · meaningful starting points.
const DEMO_LADDERS = {
  "bitcoin": {
    targets: [
      { id: "t1", price: 120000, sellPercent: 15 },
      { id: "t2", price: 150000, sellPercent: 20 },
      { id: "t3", price: 200000, sellPercent: 25 },
    ],
    moonBag: 40,
  },
  "ethereum": {
    targets: [
      { id: "t1", price: 5000,  sellPercent: 20 },
      { id: "t2", price: 7500,  sellPercent: 25 },
      { id: "t3", price: 10000, sellPercent: 25 },
    ],
    moonBag: 30,
  },
  "solana": {
    targets: [
      { id: "t1", price: 300, sellPercent: 25 },
      { id: "t2", price: 450, sellPercent: 30 },
      { id: "t3", price: 600, sellPercent: 25 },
    ],
    moonBag: 20,
  },
  "ripple": {
    targets: [
      { id: "t1", price: 3,  sellPercent: 20 },
      { id: "t2", price: 6,  sellPercent: 30 },
      { id: "t3", price: 10, sellPercent: 30 },
    ],
    moonBag: 20,
  },
  "internet-computer": {
    targets: [
      { id: "t1", price: 20, sellPercent: 30 },
      { id: "t2", price: 35, sellPercent: 30 },
      { id: "t3", price: 60, sellPercent: 25 },
    ],
    moonBag: 15,
  },
};

const DEFAULT_STATE = {
  version: 1,
  // First-launch flow:
  //   false → WelcomeGate renders (post-license-activation)
  //   true  → user has chosen (loaded demo OR started empty); never asked again unless clearAll()
  onboarded: false,
  settings: {
    currency: "USD",
    theme: "dark",
    density: "default", // compact | default | cozy
    accentHue: 252,
    aiEnabled: false,
    priceSource: "coingecko-public", // coingecko-public | coingecko-demo | coingecko-pro | demo | manual
    coingeckoKey: "",
    aiTier: "lite", // lite | pro  (pro unlocked by license code)
    aiLicenseCode: "",
    byokAnthropicKey: "",
    taxRate: 0.30,
    stablecoinTargetPct: 25,
    notation: "auto", // auto | precise
  },
  holdings: [],
  manualPrices: {},
  // user-added coins not in DEMO_COINS (custom or CG-found)
  customCoins: [], // { id, symbol, name, color, image? }
  ladders: {},
  scenarios: {
    bullPeaks: {},
    crashPercents: [30, 50, 70, 85],
  },
  // transaction history (light)
  transactions: [],
  lastSavedAt: 0,
};

// ---------- persistence
function loadState() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return structuredClone(DEFAULT_STATE);
    const parsed = JSON.parse(raw);
    return mergeDeep(structuredClone(DEFAULT_STATE), parsed);
  } catch (e) {
    console.warn("ExitOS: load failed, using defaults", e);
    return structuredClone(DEFAULT_STATE);
  }
}
function saveState(s) {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...s, lastSavedAt: Date.now() }));
  } catch {}
}
function mergeDeep(a, b) {
  if (Array.isArray(b)) return b;
  if (b && typeof b === "object") {
    const out = { ...a };
    for (const k of Object.keys(b)) out[k] = mergeDeep(a?.[k], b[k]);
    return out;
  }
  return b === undefined ? a : b;
}

// ---------- Context
const StoreContext = React.createContext(null);

function StoreProvider({ children }) {
  const [state, setState] = React.useState(() => loadState());
  const [prices, setPrices] = React.useState(() => seedPrices());
  const [tick, setTick] = React.useState(0); // for jitter animations
  const [priceStatus, setPriceStatus] = React.useState({ mode: "demo", ok: true, msg: "" });

  // keep a global registry of coin metadata so getCoinMeta resolves custom/cg-added coins
  React.useEffect(() => {
    const reg = {};
    for (const c of DEMO_COINS) reg[c.id] = c;
    for (const c of state.customCoins) reg[c.id] = c;
    window.__exosRegistry = reg;
  }, [state.customCoins]);

  // persist
  React.useEffect(() => { saveState(state); }, [state]);

  // apply theme + accent
  React.useEffect(() => {
    const root = document.documentElement;
    root.setAttribute("data-theme", state.settings.theme);
    root.setAttribute("data-density", state.settings.density);
    root.style.setProperty("--accent-h", state.settings.accentHue);
  }, [state.settings.theme, state.settings.density, state.settings.accentHue]);

  // ----- Price engine: routes by source -----
  const source = state.settings.priceSource;
  const cgKey = state.settings.coingeckoKey;

  // Mock engine: only runs when source is "demo"
  React.useEffect(() => {
    if (source !== "demo") return;
    setPriceStatus({ mode: "demo", ok: true, msg: "Simulated prices (demo)" });
    const id = setInterval(() => {
      setPrices(prev => {
        const next = { ...prev };
        for (const c of DEMO_COINS) {
          const cur = next[c.id] || { price: c.basePrice, base: c.basePrice, change24h: 0 };
          const drift = (cur.base - cur.price) / cur.base * 0.0008;
          const noise = (Math.random() - 0.5) * c.vol;
          const pct = drift + noise;
          const newPrice = Math.max(0.0001, cur.price * (1 + pct));
          const ch24 = (cur.change24h ?? 0) * 0.97 + pct * 100 * 0.6;
          next[c.id] = { ...cur, price: newPrice, change24h: ch24, lastPct: pct };
        }
        return next;
      });
      setTick(t => t + 1);
    }, 1800);
    return () => clearInterval(id);
  }, [source]);

  // CoinGecko engine
  React.useEffect(() => {
    if (!isCoinGeckoSource(source)) return;
    let cancelled = false;
    let interval;
    const coinIds = Array.from(new Set([
      ...state.holdings.map(h => h.coinId).filter(id => !id.startsWith("custom:")),
    ]));
    async function poll() {
      if (!coinIds.length) {
        setPriceStatus({ mode: source, ok: true, msg: "Connected (no coins to fetch)." });
        return;
      }
      try {
        const fetched = await cgFetchPrices(coinIds, source, cgKey);
        if (cancelled) return;
        setPrices(prev => ({ ...prev, ...fetched }));
        setPriceStatus({ mode: source, ok: true, msg: `Live · updated ${new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })}` });
      } catch (e) {
        if (cancelled) return;
        setPriceStatus({ mode: source, ok: false, msg: e?.message || "Fetch failed." });
      }
      setTick(t => t + 1);
    }
    poll();
    // public is rate-limited harder than demo/pro
    const intervalMs = source === "coingecko-public" ? 90000 : 45000;
    interval = setInterval(poll, intervalMs);
    return () => { cancelled = true; clearInterval(interval); };
  }, [source, cgKey, state.holdings.map(h => h.coinId).join(",")]);

  // Manual mode · no polling at all, prices come from state.manualPrices via effectivePrice
  React.useEffect(() => {
    if (source !== "manual") return;
    setPriceStatus({ mode: "manual", ok: true, msg: "Manual prices only · set per coin in Portfolio." });
  }, [source]);

  const api = React.useMemo(() => ({
    state, prices, tick,
    update: (mut) => setState(s => {
      const next = typeof mut === "function" ? mut(structuredClone(s)) : mut;
      return next;
    }),
    setSetting: (k, v) => setState(s => ({ ...s, settings: { ...s.settings, [k]: v } })),
    addHolding: (h) => setState(s => ({ ...s, holdings: [...s.holdings, { id: rid(), ...h }] })),
    updateHolding: (id, patch) => setState(s => ({ ...s, holdings: s.holdings.map(h => h.id === id ? { ...h, ...patch } : h) })),
    removeHolding: (id) => setState(s => ({
      ...s,
      holdings: s.holdings.filter(h => h.id !== id),
      ladders: Object.fromEntries(Object.entries(s.ladders).filter(([k]) => s.holdings.find(h => h.id === id)?.coinId !== k))
    })),
    setLadder: (coinId, ladder) => setState(s => ({ ...s, ladders: { ...s.ladders, [coinId]: ladder } })),
    addCustomCoin: (coin) => setState(s => {
      // dedupe by id
      if (s.customCoins.find(c => c.id === coin.id)) return s;
      return { ...s, customCoins: [...s.customCoins, coin] };
    }),
    rememberCoin: (coin) => setState(s => {
      // cache CG-found coins so icons / names render after a hot reload
      if (s.customCoins.find(c => c.id === coin.id)) return s;
      if (DEMO_COINS.find(c => c.id === coin.id)) return s;
      return { ...s, customCoins: [...s.customCoins, coin] };
    }),
    priceStatus,
    setBullPeak: (coinId, peak) => setState(s => ({ ...s, scenarios: { ...s.scenarios, bullPeaks: { ...s.scenarios.bullPeaks, [coinId]: peak } } })),
    setManualPrice: (coinId, price) => setState(s => ({ ...s, manualPrices: { ...s.manualPrices, [coinId]: price } })),
    clearManualPrice: (coinId) => setState(s => {
      const m = { ...s.manualPrices }; delete m[coinId]; return { ...s, manualPrices: m };
    }),
    // Welcome flow actions (called from WelcomeGate post-license-activation)
    loadDemoPortfolio: () => setState(s => ({
      ...s,
      onboarded: true,
      holdings: structuredClone(DEMO_HOLDINGS),
      ladders: structuredClone(DEMO_LADDERS),
      scenarios: {
        ...s.scenarios,
        bullPeaks: {
          "bitcoin": 250000, "ethereum": 12000, "solana": 750,
          "ripple": 12, "internet-computer": 80,
        },
      },
    })),
    startEmpty: () => setState(s => ({ ...s, onboarded: true })),
    // Settings actions
    resetDemo: () => setState(s => ({
      ...s,
      onboarded: true,
      holdings: structuredClone(DEMO_HOLDINGS),
      ladders: structuredClone(DEMO_LADDERS),
      manualPrices: {},
      transactions: [],
      scenarios: {
        ...s.scenarios,
        bullPeaks: {
          "bitcoin": 250000, "ethereum": 12000, "solana": 750,
          "ripple": 12, "internet-computer": 80,
        },
      },
    })),
    clearAll: () => setState(s => ({
      ...DEFAULT_STATE,
      settings: s.settings,           // keep prefs (theme, currency, etc.)
      onboarded: false,               // re-trigger WelcomeGate
    })),
    loadFromBackup: (data) => setState(s => mergeDeep(structuredClone(DEFAULT_STATE), data)),
  }), [state, prices, tick, priceStatus]);

  return <StoreContext.Provider value={api}>{children}</StoreContext.Provider>;
}

function useStore() {
  const ctx = React.useContext(StoreContext);
  if (!ctx) throw new Error("useStore must be inside StoreProvider");
  return ctx;
}

function seedPrices() {
  const obj = {};
  for (const c of DEMO_COINS) {
    obj[c.id] = { price: c.basePrice, base: c.basePrice, change24h: (Math.random() - 0.3) * 4, lastPct: 0 };
  }
  return obj;
}

// ---------- helpers
function rid() { return Math.random().toString(36).slice(2, 10); }

function getCoinMeta(coinId) {
  const reg = window.__exosRegistry;
  if (reg && reg[coinId]) return reg[coinId];
  // fallback: search DEMO_COINS directly (in case registry hasn't initialized)
  const demo = DEMO_COINS.find(c => c.id === coinId);
  if (demo) return demo;
  // last-resort generic stub
  const sym = (coinId || "").replace(/^custom:/, "").slice(0, 5).toUpperCase();
  return { id: coinId, symbol: sym || "?", name: coinId, color: typeof colorFromId === "function" ? colorFromId(coinId) : "#777" };
}

function effectivePrice(coinId, prices, manualPrices) {
  if (manualPrices && manualPrices[coinId] != null) return manualPrices[coinId];
  return prices?.[coinId]?.price ?? getCoinMeta(coinId).basePrice;
}

function fmtCurrency(n, currency = "USD", opts = {}) {
  if (n == null || isNaN(n)) return "·";
  const cur = currency in FX ? currency : "USD";
  const val = n * FX[cur];
  const sym = CURRENCY_SYMBOLS[cur];
  const abs = Math.abs(val);
  let digits = 2;
  if (opts.precise) digits = abs < 1 ? 6 : abs < 10 ? 4 : 2;
  else if (opts.compact && abs >= 1000) {
    return (val < 0 ? "-" : "") + sym + compact(abs);
  }
  else if (abs < 1) digits = 4;
  else if (abs >= 10000) digits = 0;
  const s = abs.toLocaleString(undefined, { minimumFractionDigits: digits, maximumFractionDigits: digits });
  return (val < 0 ? "-" : "") + sym + s;
}
function compact(n) {
  if (n >= 1e9) return (n/1e9).toFixed(2).replace(/\.?0+$/, "") + "B";
  if (n >= 1e6) return (n/1e6).toFixed(2).replace(/\.?0+$/, "") + "M";
  if (n >= 1e3) return (n/1e3).toFixed(2).replace(/\.?0+$/, "") + "k";
  return n.toFixed(2);
}
function fmtPct(n, opts = {}) {
  if (n == null || isNaN(n)) return "·";
  const digits = opts.digits ?? (Math.abs(n) < 1 ? 2 : 1);
  return (n > 0 ? "+" : "") + n.toFixed(digits) + "%";
}
function fmtNum(n, digits = 4) {
  if (n == null || isNaN(n)) return "·";
  if (Math.abs(n) >= 1000) return n.toLocaleString(undefined, { maximumFractionDigits: 2 });
  return n.toLocaleString(undefined, { maximumFractionDigits: digits });
}

// portfolio math
function computePortfolio(state, prices) {
  const rows = state.holdings.map(h => {
    const meta = getCoinMeta(h.coinId);
    const price = effectivePrice(h.coinId, prices, state.manualPrices);
    const value = h.amount * price;
    const cost = h.amount * h.avgBuyPrice;
    const pnl = value - cost;
    const pnlPct = cost > 0 ? (pnl / cost) * 100 : 0;
    return { ...h, meta, price, value, cost, pnl, pnlPct };
  });
  const total = rows.reduce((a, r) => a + r.value, 0);
  const totalCost = rows.reduce((a, r) => a + r.cost, 0);
  const pnl = total - totalCost;
  const pnlPct = totalCost > 0 ? (pnl / totalCost) * 100 : 0;
  rows.forEach(r => r.alloc = total > 0 ? (r.value / total) * 100 : 0);
  rows.sort((a, b) => b.value - a.value);
  return { rows, total, totalCost, pnl, pnlPct };
}

// Exit ladder math for one holding
function computeLadder(holding, ladder, currentPrice, taxRate) {
  if (!ladder || !ladder.targets?.length) {
    return { targets: [], totalProceeds: 0, totalNet: 0, totalCoinsSold: 0, sumPct: 0, remainingPct: 100, moonBag: ladder?.moonBag ?? 0 };
  }
  let totalProceeds = 0, totalNet = 0, totalCoinsSold = 0;
  const targets = ladder.targets.map(t => {
    const coinsSold = holding.amount * (t.sellPercent / 100);
    const proceeds = coinsSold * t.price;
    const costBasis = coinsSold * holding.avgBuyPrice;
    const gain = proceeds - costBasis;
    const tax = Math.max(0, gain) * taxRate;
    const net = proceeds - tax;
    totalProceeds += proceeds;
    totalNet += net;
    totalCoinsSold += coinsSold;
    return { ...t, coinsSold, proceeds, gain, tax, net, reached: currentPrice >= t.price };
  });
  const sumPct = ladder.targets.reduce((a, t) => a + t.sellPercent, 0);
  const remainingPct = Math.max(0, 100 - sumPct);
  return { targets, totalProceeds, totalNet, totalCoinsSold, sumPct, remainingPct, moonBag: ladder.moonBag ?? remainingPct };
}

// Capital recovery · for "You Already Won"
// Find: at the first ladder target, what % must we sell to recover full initial cost?
function capitalRecovery(holding, currentPrice) {
  const cost = holding.amount * holding.avgBuyPrice;
  if (currentPrice <= 0) return null;
  const coinsNeeded = cost / currentPrice;
  if (coinsNeeded >= holding.amount) return { possible: false, percentNeeded: 100 };
  const pct = (coinsNeeded / holding.amount) * 100;
  return { possible: true, percentNeeded: pct, coinsNeeded, proceeds: cost, remainingCoins: holding.amount - coinsNeeded };
}

// Round-trip risk score 0..100
function roundTripRisk(state, prices) {
  const { rows, total, totalCost, pnl } = computePortfolio(state, prices);
  if (!rows.length || total === 0) return { score: 0, label: "·", reasons: ["No holdings yet"], pnl, total };
  let score = 0;
  const reasons = [];
  // 1) Unrealized gains as % of cost
  const upPct = totalCost > 0 ? (pnl / totalCost) * 100 : 0;
  if (upPct > 0) score += Math.min(50, upPct / 8); // big gains = more to lose
  if (upPct > 50) reasons.push(`You're up ${Math.round(upPct)}% · a lot to give back`);
  // 2) Plan coverage: weighted % of holdings sold by ladders
  let plannedSell = 0, weight = 0;
  for (const r of rows) {
    const l = state.ladders[r.coinId];
    const sumPct = l?.targets?.reduce((a,t)=>a+t.sellPercent,0) || 0;
    plannedSell += sumPct * r.value;
    weight += r.value;
  }
  const avgPlan = weight > 0 ? plannedSell / weight : 0;
  // less plan = more risk
  score += (100 - Math.min(100, avgPlan)) * 0.35;
  if (avgPlan < 30) reasons.push("Most holdings have no exit plan");
  // 3) Concentration
  const topAlloc = rows[0]?.alloc ?? 0;
  if (topAlloc > 60) { score += 10; reasons.push(`${rows[0].meta.symbol} is ${Math.round(topAlloc)}% of portfolio`); }
  score = Math.max(0, Math.min(100, Math.round(score)));
  let label = "Low";
  if (score >= 70) label = "High";
  else if (score >= 40) label = "Elevated";
  else if (score >= 20) label = "Moderate";
  return { score, label, reasons, pnl, total, totalCost, upPct, avgPlan };
}

// Cycle-peak portfolio if all bull-peak prices hit
function peakValuation(state) {
  let peak = 0;
  for (const h of state.holdings) {
    const p = state.scenarios.bullPeaks[h.coinId] ?? 0;
    peak += h.amount * p;
  }
  return peak;
}

// expose globals
Object.assign(window, {
  DEMO_COINS, FX, CURRENCY_SYMBOLS, STORAGE_KEY,
  StoreProvider, useStore,
  rid, getCoinMeta, effectivePrice,
  fmtCurrency, fmtPct, fmtNum, compact,
  computePortfolio, computeLadder, capitalRecovery, roundTripRisk, peakValuation,
});
