// Client-side API helpers — DexScreener (browser-direct) + Oracle backend (/api/analyze).

const DS_BASE = 'https://api.dexscreener.com';

// Module-level caches
let _trendingCache = { at: 0, items: null };
const TRENDING_TTL = 60_000; // 1 minute

const SOLANA_CA_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;

// --- DexScreener ----------------------------------------------------------

// Lookup token by mint address. Returns the highest-liquidity Solana pair mapped to coin shape.
async function fetchDexToken(ca) {
  const r = await fetch(`${DS_BASE}/latest/dex/tokens/${encodeURIComponent(ca)}`);
  if (!r.ok) throw new Error(`DexScreener ${r.status}`);
  const data = await r.json();
  const pairs = (data?.pairs || []).filter(p => p.chainId === 'solana');
  if (pairs.length === 0) throw new Error('No Solana pair found for this contract');
  // Pick highest USD liquidity
  pairs.sort((a, b) => (b?.liquidity?.usd || 0) - (a?.liquidity?.usd || 0));
  const best = pairs[0];
  return { coin: pairToCoin(best, ca), raw: best, allPairs: pairs };
}

// Search-as-you-type. Returns up to ~12 best Solana matches as coin-row objects.
async function searchDexTokens(query) {
  if (!query || query.length < 1) return [];
  const r = await fetch(`${DS_BASE}/latest/dex/search?q=${encodeURIComponent(query)}`);
  if (!r.ok) throw new Error(`DexScreener search ${r.status}`);
  const data = await r.json();
  const pairs = (data?.pairs || []).filter(p => p.chainId === 'solana');
  // Dedupe by token address, keep highest-liquidity per token.
  const byMint = new Map();
  for (const p of pairs) {
    const mint = p?.baseToken?.address;
    if (!mint) continue;
    const existing = byMint.get(mint);
    if (!existing || (p?.liquidity?.usd || 0) > (existing?.liquidity?.usd || 0)) {
      byMint.set(mint, p);
    }
  }
  const rows = Array.from(byMint.values())
    .sort((a, b) => (b?.liquidity?.usd || 0) - (a?.liquidity?.usd || 0))
    .slice(0, 12)
    .map(p => pairToRow(p));
  return rows;
}

// Trending Solana tokens (boosts/top + hydrate). Cached 60s.
async function fetchTrendingSolana() {
  const now = Date.now();
  if (_trendingCache.items && (now - _trendingCache.at) < TRENDING_TTL) {
    return _trendingCache.items;
  }
  const r = await fetch(`${DS_BASE}/token-boosts/top/v1`);
  if (!r.ok) throw new Error(`DexScreener boosts ${r.status}`);
  const boosts = await r.json();
  const solanaBoosts = (Array.isArray(boosts) ? boosts : [])
    .filter(b => b.chainId === 'solana')
    .slice(0, 12);
  if (solanaBoosts.length === 0) { _trendingCache = { at: now, items: [] }; return []; }
  const addrs = solanaBoosts.map(b => b.tokenAddress).join(',');
  const r2 = await fetch(`${DS_BASE}/tokens/v1/solana/${addrs}`);
  if (!r2.ok) throw new Error(`DexScreener hydrate ${r2.status}`);
  const pairs = await r2.json();
  // pairs is an array of pair objects; group by base token, pick richest per token, preserve boost order
  const byMint = new Map();
  for (const p of (Array.isArray(pairs) ? pairs : [])) {
    const mint = p?.baseToken?.address;
    if (!mint) continue;
    const existing = byMint.get(mint);
    if (!existing || (p?.liquidity?.usd || 0) > (existing?.liquidity?.usd || 0)) {
      byMint.set(mint, p);
    }
  }
  const ordered = solanaBoosts
    .map(b => byMint.get(b.tokenAddress))
    .filter(Boolean)
    .map(p => pairToRow(p));
  _trendingCache = { at: now, items: ordered };
  return ordered;
}

// --- Mappers --------------------------------------------------------------

function pairToCoin(p, fallbackCa) {
  const t = p.baseToken || {};
  const created = p.pairCreatedAt ? new Date(p.pairCreatedAt) : null;
  return {
    ticker: (t.symbol || '???').toUpperCase(),
    name: t.name || t.symbol || 'Unknown',
    ca: t.address || fallbackCa,
    chain: 'Solana',
    age: ageString(created),
    pairCreatedAt: p.pairCreatedAt || null,
    pairAddress: p.pairAddress || null,
    dexId: p.dexId || null,
    imageUrl: p.info?.imageUrl || null,
    priceUsd: numOrNull(p.priceUsd),
    marketCap: numOrNull(p.marketCap),
    fdv: numOrNull(p.fdv),
    liquidity: numOrNull(p?.liquidity?.usd),
    volume24h: numOrNull(p?.volume?.h24),
    volume6h:  numOrNull(p?.volume?.h6),
    volume1h:  numOrNull(p?.volume?.h1),
    buys24h:  intOrNull(p?.txns?.h24?.buys),
    sells24h: intOrNull(p?.txns?.h24?.sells),
    buys1h:   intOrNull(p?.txns?.h1?.buys),
    sells1h:  intOrNull(p?.txns?.h1?.sells),
    change5m:  numOrNull(p?.priceChange?.m5),
    change1h:  numOrNull(p?.priceChange?.h1),
    change6h:  numOrNull(p?.priceChange?.h6),
    change24h: numOrNull(p?.priceChange?.h24),
    holders: null, // not provided by DexScreener
    // Live OHLCV is not provided cheaply by DexScreener; reports synthesize a sparkline
    // from priceChange windows. Real candles come in v2.
  };
}

function pairToRow(p) {
  const t = p.baseToken || {};
  return {
    ticker: (t.symbol || '???').toUpperCase(),
    name: t.name || t.symbol || 'Unknown',
    ca: t.address,
    imageUrl: p.info?.imageUrl || null,
    priceUsd: numOrNull(p.priceUsd),
    marketCap: numOrNull(p.marketCap),
    liquidity: numOrNull(p?.liquidity?.usd),
    change24h: numOrNull(p?.priceChange?.h24),
    dexId: p.dexId || null,
  };
}

function numOrNull(v) {
  if (v === null || v === undefined || v === '') return null;
  const n = Number(v);
  return Number.isFinite(n) ? n : null;
}
function intOrNull(v) {
  if (v === null || v === undefined || v === '') return null;
  const n = Number(v);
  return Number.isFinite(n) ? Math.round(n) : null;
}

function ageString(date) {
  if (!date) return '—';
  const ms = Date.now() - date.getTime();
  const m = Math.floor(ms / 60000);
  if (m < 60) return `${m}m`;
  const h = Math.floor(m / 60);
  if (h < 24) return `${h}h ${m % 60}m`;
  const d = Math.floor(h / 24);
  return `${d}d ${h % 24}h`;
}

// --- Synthesized chart from priceChange windows --------------------------
// DexScreener doesn't give us OHLCV. We approximate by drawing a curve that
// lands at the right percentage move for the requested timeframe.
function synthChart(coin, tf) {
  const map = {
    '1H':  { points: 60,  changePct: coin.change1h  ?? 0, vSeed: 17 },
    '4H':  { points: 80,  changePct: ((coin.change6h ?? coin.change1h ?? 0) * 4 / 6), vSeed: 23 },
    '1D':  { points: 96,  changePct: coin.change24h ?? 0, vSeed: 31 },
    'ALL': { points: 120, changePct: coin.change24h ?? 0, vSeed: 73 },
  };
  const cfg = map[tf] || map['4H'];
  const { points, changePct, vSeed } = cfg;
  const startPrice = 100;
  const endPrice = startPrice * (1 + (changePct / 100));
  // Deterministic noise via LCG seeded by ticker + tf
  let s = (vSeed + (coin.ticker || '').split('').reduce((a, c) => a + c.charCodeAt(0), 0)) || 7;
  const rand = () => { s = (s * 9301 + 49297) % 233280; return s / 233280; };
  const chart = [];
  for (let i = 0; i < points; i++) {
    const t = i / (points - 1);
    const trend = startPrice + (endPrice - startPrice) * t;
    const noise = (Math.sin(i * 0.7) * 4) + (Math.cos(i * 1.3) * 3) + (rand() - 0.5) * 5;
    chart.push(Math.max(1, trend + noise));
  }
  const volume = Array.from({ length: points }, (_, i) => 14 + Math.abs(Math.sin(i * 0.5)) * 30 + rand() * 18 + (i / points) * 12);
  return { chart, volume };
}

// --- Oracle backend (Gemini via /api/analyze) ----------------------------

async function callOracleReport({ ca, dexData, personality }) {
  const r = await fetch('/api/analyze', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ mode: 'report', ca, dexData, personality }),
  });
  if (!r.ok) {
    const err = await r.json().catch(() => ({}));
    throw new Error(err.error || `AI report failed (${r.status})`);
  }
  return await r.json();
}

async function callOracleChat({ ca, coinContext, history, message, personality }) {
  const r = await fetch('/api/analyze', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ mode: 'chat', ca, coinContext, history, message, personality }),
  });
  if (!r.ok) {
    const err = await r.json().catch(() => ({}));
    throw new Error(err.error || `AI chat failed (${r.status})`);
  }
  const data = await r.json();
  return data.text || '';
}

Object.assign(window, {
  SOLANA_CA_RE,
  fetchDexToken, searchDexTokens, fetchTrendingSolana,
  synthChart,
  callOracleReport, callOracleChat,
});
