async function request(path, options = {}) {
  const headers = { ...(options.headers || {}) };
  if (!(options.body instanceof FormData)) {
    headers['Content-Type'] = headers['Content-Type'] || 'application/json';
  }

  const res = await fetch(path, {
    credentials: 'include',
    ...options,
    headers,
  });

  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    throw new Error(data.error || `Request failed (${res.status})`);
  }
  return data;
}

export const api = {
  me: () => request('/api/auth/me'),
  login: (username, password) =>
    request('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify({ username, password }),
    }),
  register: (username, password) =>
    request('/api/auth/register', {
      method: 'POST',
      body: JSON.stringify({ username, password }),
    }),
  logout: () => request('/api/auth/logout', { method: 'POST' }),
  cases: () => request('/api/cases'),
  case: (id) => request(`/api/cases/${id}`),
  catalog: () => request('/api/catalog'),
  prestige: () => request('/api/prestige'),
  doPrestige: () => request('/api/prestige', { method: 'POST', body: JSON.stringify({}) }),
  prestigeUpgrade: (skillId, count = 1) =>
    request('/api/prestige/upgrade', {
      method: 'POST',
      body: JSON.stringify({ skillId, count }),
    }),
  openCase: (id, count = 1) =>
    request(`/api/cases/${id}/open`, {
      method: 'POST',
      body: JSON.stringify({ count }),
    }),
  inventory: () => request('/api/inventory'),
  vaultStake: (inventoryItemIds) =>
    request('/api/vault/stake', {
      method: 'POST',
      body: JSON.stringify({ inventoryItemIds }),
    }),
  vaultUnstake: (inventoryItemIds) =>
    request('/api/vault/unstake', {
      method: 'POST',
      body: JSON.stringify({ inventoryItemIds }),
    }),
  sellInventory: (inventoryItemIds) =>
    request('/api/inventory/sell', {
      method: 'POST',
      body: JSON.stringify({ inventoryItemIds }),
    }),
  updateAutoSell: (body) =>
    request('/api/inventory/auto-sell', {
      method: 'PATCH',
      body: JSON.stringify(body),
    }),
  applyPendingAutoSell: (inventoryItemIds) =>
    request('/api/inventory/auto-sell/apply', {
      method: 'POST',
      body: JSON.stringify({ inventoryItemIds }),
    }),
  setInventoryFavorite: (inventoryItemIds, favorite) =>
    request('/api/inventory/favorite', {
      method: 'PATCH',
      body: JSON.stringify({ inventoryItemIds, favorite }),
    }),
  shop: () => request('/api/shop'),
  shopBuy: (packageId) =>
    request('/api/shop/buy', {
      method: 'POST',
      body: JSON.stringify({ packageId }),
    }),
  shopOsuCheckout: (packId) =>
    request('/api/shop/osu/checkout', {
      method: 'POST',
      body: JSON.stringify({ packId }),
    }),
  shopOsuConfirm: (orderId) =>
    request('/api/shop/osu/confirm', {
      method: 'POST',
      body: JSON.stringify({ orderId }),
    }),
  shopOsuOrder: (orderId) =>
    request(`/api/shop/osu/orders/${encodeURIComponent(orderId)}`),
  shopAd: () => request('/api/shop/ad', { method: 'POST' }),
  feed: () => request('/api/feed'),
  feedAnnounce: (inventoryItemIds, caseName, opts = {}) => {
    const ids = Array.isArray(inventoryItemIds)
      ? inventoryItemIds
      : inventoryItemIds != null
        ? [inventoryItemIds]
        : [];
    const body = {
      inventoryItemIds: ids.filter(Boolean),
      caseName,
      respin: Boolean(opts.respin),
    };
    if (opts.snapshotBest) {
      body.snapshotBest = opts.snapshotBest;
      body.openCount = opts.openCount;
      body.count = opts.count;
    } else if (opts.snapshotItems) {
      body.snapshotItems = opts.snapshotItems;
    }
    return request('/api/feed/announce', {
      method: 'POST',
      body: JSON.stringify(body),
    });
  },
  leaderboard: () => request('/api/leaderboard'),
  jackpot: () => request('/api/jackpot'),
  jackpotRound: (id) => request(`/api/jackpot/${id}`),
  jackpotCreateBet: (inventoryItemIds) =>
    request('/api/jackpot/bet', {
      method: 'POST',
      body: JSON.stringify({ inventoryItemIds }),
    }),
  jackpotBet: (id, inventoryItemIds) =>
    request(`/api/jackpot/${id}/bet`, {
      method: 'POST',
      body: JSON.stringify({ inventoryItemIds }),
    }),
  jackpotWithdraw: (id, inventoryItemIds) =>
    request(`/api/jackpot/${id}/withdraw`, {
      method: 'POST',
      body: JSON.stringify({ inventoryItemIds }),
    }),
  // legacy alias
  jackpotCreate: (inventoryItemIds) =>
    request('/api/jackpot/bet', {
      method: 'POST',
      body: JSON.stringify({ inventoryItemIds }),
    }),
  profile: () => request('/api/profile'),
  playerProfile: (username) =>
    request(`/api/profile/player/${encodeURIComponent(username)}`),
  achievements: () => request('/api/achievements'),
  updateProfile: (body) =>
    request('/api/profile', { method: 'PATCH', body: JSON.stringify(body) }),
  changePassword: (currentPassword, newPassword) =>
    request('/api/profile/password', {
      method: 'POST',
      body: JSON.stringify({ currentPassword, newPassword }),
    }),
  uploadAvatar: (file) => {
    const form = new FormData();
    form.append('avatar', file);
    return request('/api/profile/avatar', { method: 'POST', body: form });
  },
  adminLogin: (username, password) =>
    request('/api/admin/login', {
      method: 'POST',
      body: JSON.stringify({ username, password }),
    }),
  adminStats: () => request('/api/admin/stats'),
  adminPrestigeServerPreview: () => request('/api/admin/prestige-server'),
  adminPrestigeServer: (body) =>
    request('/api/admin/prestige-server', {
      method: 'POST',
      body: JSON.stringify(body),
    }),
  adminUsers: () => request('/api/admin/users'),
  adminUpdateUser: (id, body) =>
    request(`/api/admin/users/${id}`, {
      method: 'PATCH',
      body: JSON.stringify(body),
    }),
  adminCases: () => request('/api/admin/cases'),
  adminItems: () => request('/api/admin/items'),
  adminRarities: () => request('/api/admin/rarities'),
  adminCategories: () => request('/api/admin/categories'),
  adminShopOsuPacks: () => request('/api/admin/shop/osu-packs'),
  adminShopOsuSection: (enabled) =>
    request('/api/admin/shop/osu-section', {
      method: 'PUT',
      body: JSON.stringify({ enabled }),
    }),
  adminUpdateShopOsuPack: (id, body) =>
    request(`/api/admin/shop/osu-packs/${encodeURIComponent(id)}`, {
      method: 'PUT',
      body: JSON.stringify(body),
    }),
  createCategory: (body) =>
    request('/api/admin/categories', { method: 'POST', body: JSON.stringify(body) }),
  updateCategory: (id, body) =>
    request(`/api/admin/categories/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
  deleteCategory: (id) =>
    request(`/api/admin/categories/${id}`, { method: 'DELETE' }),
  createCase: (body) =>
    request('/api/admin/cases', { method: 'POST', body: JSON.stringify(body) }),
  updateCase: (id, body) =>
    request(`/api/admin/cases/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
  deleteCase: (id) => request(`/api/admin/cases/${id}`, { method: 'DELETE' }),
  createItem: (body) =>
    request('/api/admin/items', { method: 'POST', body: JSON.stringify(body) }),
  updateItem: (id, body) =>
    request(`/api/admin/items/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
  deleteItem: (id) => request(`/api/admin/items/${id}`, { method: 'DELETE' }),
  addCaseItem: (caseId, body) =>
    request(`/api/admin/cases/${caseId}/items`, {
      method: 'POST',
      body: JSON.stringify(body),
    }),
  updateCaseItem: (id, body) =>
    request(`/api/admin/case-items/${id}`, {
      method: 'PUT',
      body: JSON.stringify(body),
    }),
  deleteCaseItem: (id) =>
    request(`/api/admin/case-items/${id}`, { method: 'DELETE' }),
  adminUpload: (file) => {
    const form = new FormData();
    form.append('image', file);
    return request('/api/admin/upload', { method: 'POST', body: form });
  },
};

/**
 * Parse money amounts (cents) to BigInt — mirror of server `cents.js`.
 * Accepts bigint, number, or decimal/int string. Truncates toward zero.
 */
export function toCentsBigInt(value) {
  if (typeof value === 'bigint') return value;
  if (value == null || value === '') return 0n;
  if (typeof value === 'number') {
    if (!Number.isFinite(value)) return 0n;
    return BigInt(Math.trunc(value));
  }
  const s = String(value).trim();
  if (!s) return 0n;
  const m = s.match(/^([+-]?\d+)/);
  if (!m) return 0n;
  try {
    return BigInt(m[1]);
  } catch {
    return 0n;
  }
}

/** JSON-safe cents string. */
export function centsJson(value) {
  return toCentsBigInt(value).toString();
}

/** Sort/compare helper: negative if a < b, 0 if equal, positive if a > b. */
export function compareCents(a, b) {
  const aa = toCentsBigInt(a);
  const bb = toCentsBigInt(b);
  if (aa < bb) return -1;
  if (aa > bb) return 1;
  return 0;
}

/** Exact credit display (cents → "1 234.56"), for tooltips / confirmations. */
export function formatCreditsExact(cents) {
  let bi = toCentsBigInt(cents);
  const neg = bi < 0n;
  if (neg) bi = -bi;
  const whole = bi / 100n;
  const frac = (bi % 100n).toString().padStart(2, '0');
  const grouped = whole.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '\u202f');
  return `${neg ? '-' : ''}${grouped}.${frac}`;
}

/** Parse credits input (decimal string) → integer cents string. Null if invalid. */
export function creditsToCentsString(raw) {
  if (raw == null || raw === '') return null;
  const s = String(raw).trim().replace(',', '.');
  const m = s.match(/^(\d+)(?:\.(\d{1,2})\d*)?$/);
  if (!m) return null;
  try {
    const whole = BigInt(m[1]);
    const frac = m[2] ? m[2].padEnd(2, '0') : '00';
    return (whole * 100n + BigInt(frac)).toString();
  } catch {
    return null;
  }
}

/** Cents → credits decimal string without Number precision loss. */
export function centsToCreditsString(cents) {
  let bi = toCentsBigInt(cents);
  const neg = bi < 0n;
  const abs = neg ? -bi : bi;
  const whole = abs / 100n;
  const frac = (abs % 100n).toString().padStart(2, '0');
  const body =
    frac === '00' ? whole.toString() : `${whole}.${frac}`.replace(/0+$/, '').replace(/\.$/, '');
  return `${neg ? '-' : ''}${body}`;
}

/** Alphabet suffixes after T: aa, ab, … az, ba, … (each step ×1000). */
function alphabetSuffix(index) {
  let num = Math.max(0, Math.floor(index));
  let result = '';
  do {
    result = String.fromCharCode(97 + (num % 26)) + result;
    num = Math.floor(num / 26);
  } while (num > 0);
  while (result.length < 2) result = `a${result}`;
  return result;
}

function tierSuffix(tier) {
  if (tier <= 0) return '';
  if (tier === 1) return 'K';
  if (tier === 2) return 'M';
  if (tier === 3) return 'B';
  if (tier === 4) return 'T';
  return alphabetSuffix(tier - 5);
}

/**
 * Compact credit display for UI readability.
 * K / M / B / T, then aa, ab, … (×1000 each). Input is cents.
 */
export function formatCredits(cents) {
  let bi = toCentsBigInt(cents);
  const neg = bi < 0n;
  if (neg) bi = -bi;

  const creditsWhole = bi / 100n;
  const creditsFrac = bi % 100n;

  if (creditsWhole < 1000n) {
    return `${neg ? '-' : ''}${creditsWhole}.${creditsFrac.toString().padStart(2, '0')}`;
  }

  let tier = 0;
  let scaled = creditsWhole;
  let rem = 0n;
  while (scaled >= 1000n) {
    rem = scaled % 1000n;
    scaled /= 1000n;
    tier += 1;
  }

  // scaled < 1000 → safe as Number; rem gives ~3 decimal digits of precision
  const approx = Number(scaled) + Number(rem) / 1000;
  if (!Number.isFinite(approx)) return `${neg ? '-' : ''}0${tierSuffix(tier)}`;
  const shown = String(Number(approx.toFixed(2)));
  return `${neg ? '-' : ''}${shown}${tierSuffix(tier)}`;
}

export function formatChance(pct) {
  return `${Number(pct || 0).toFixed(1)}%`;
}

/** Format seconds as e.g. "2h 15m" or "45m". */
export function formatPlayTime(seconds) {
  const s = Math.max(0, Math.floor(Number(seconds) || 0));
  const h = Math.floor(s / 3600);
  const m = Math.floor((s % 3600) / 60);
  if (h <= 0 && m <= 0) return s > 0 ? `${s}s` : '0m';
  if (h <= 0) return `${m}m`;
  if (m <= 0) return `${h}h`;
  return `${h}h ${m}m`;
}

export function formatLastConnection(iso) {
  if (!iso) return 'Never';
  try {
    return new Date(iso).toLocaleString();
  } catch {
    return '—';
  }
}
