// api.jsx — Lightweight fetch client + React hooks for /api/*.
//
// Conventions:
//   - All requests use credentials: 'same-origin' (the session cookie).
//   - Errors are normalized to { code, message, details? } regardless of source
//     (HTTP failure, JSON parse error, server-side ok=false envelope).
//   - A 401 response broadcasts a "lstl:unauthenticated" CustomEvent so the
//     app shell can show the login screen without polling /api/auth/session.

const API = {};

async function _fetchJson(path, init = {}) {
  let resp;
  try {
    resp = await fetch(path, {
      credentials: 'same-origin',
      headers: { Accept: 'application/json' },
      ...init,
    });
  } catch (e) {
    return { ok: false, error: { code: 'NETWORK_ERROR', message: String(e && e.message || e) } };
  }

  if (resp.status === 401) {
    try { window.dispatchEvent(new CustomEvent('lstl:unauthenticated')); } catch (_) {}
    return { ok: false, error: { code: 'AUTH_REQUIRED', message: '인증이 필요합니다' } };
  }

  let body = null;
  try { body = await resp.json(); }
  catch (e) {
    return { ok: false, error: { code: 'BAD_JSON', message: 'invalid JSON response', status: resp.status } };
  }

  if (!resp.ok) {
    const err = (body && body.error) || { code: 'HTTP_' + resp.status, message: resp.statusText };
    return { ok: false, error: err };
  }

  // Server-side envelope: if body has explicit `ok=false`, surface it.
  if (body && body.ok === false) {
    return { ok: false, error: body.error || { code: 'UNKNOWN', message: '알 수 없는 오류' }, raw: body };
  }
  return { ok: true, data: body };
}

function _qs(params) {
  if (!params) return '';
  const u = new URLSearchParams();
  for (const [k, v] of Object.entries(params)) {
    if (v === undefined || v === null || v === '') continue;
    u.set(k, String(v));
  }
  const s = u.toString();
  return s ? '?' + s : '';
}

// ─── public surface ────────────────────────────────────────────────────────

API.health = () => _fetchJson('/api/health');

API.auth = {
  session: () => _fetchJson('/api/auth/session'),
  login: (id, password) => _fetchJson('/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
    body: JSON.stringify({ id, password }),
  }),
  logout: () => _fetchJson('/api/auth/logout', { method: 'POST' }),
};

API.portfolio = {
  summary: () => _fetchJson('/api/portfolio/summary'),
  positions: () => _fetchJson('/api/portfolio/positions'),
};

API.goals = {
  list: () => _fetchJson('/api/goals'),
  // config: { market, pct?, target_equity?, anchor_equity?, reset_anchor? }
  // POST /api/goals/config — operator-only; Codex never calls this.
  configSet: (body) => _fetchJson('/api/goals/config', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
    body: JSON.stringify(body),
  }),
};

API.problems = {
  list: (params = {}) => _fetchJson('/api/problems' + _qs(params)),
};

API.memory = {
  read: (params) => _fetchJson('/api/memory/read' + _qs(params)),
  stocks: () => _fetchJson('/api/memory/stocks'),
};

API.news = {
  market: (params = {}) => _fetchJson('/api/news/market' + _qs(params)),
  stock: (params) => _fetchJson('/api/news/stock' + _qs(params)),
  search: (params) => _fetchJson('/api/news/search' + _qs(params)),
  get: (news_id) => _fetchJson('/api/news/get' + _qs({ news_id })),
  reads: (params = {}) => _fetchJson('/api/news/reads' + _qs(params)),
};

API.backtests = {
  list: (params = {}) => _fetchJson('/api/backtests' + _qs(params)),
  get: (month) => _fetchJson('/api/backtests/' + encodeURIComponent(month)),
};

API.orders = {
  intents: (params = {}) => _fetchJson('/api/orders/intents' + _qs(params)),
  reservations: (market) => _fetchJson('/api/orders/reservations' + _qs({ market })),
};

API.cycles = {
  list: (params = {}) => _fetchJson('/api/cycles/list' + _qs(params)),
  timeline: (cycle_id) => _fetchJson('/api/cycles/' + encodeURIComponent(cycle_id) + '/timeline'),
};

API.schedule = {
  read: () => _fetchJson('/api/schedule'),
};

API.system = {
  state: () => _fetchJson('/api/system/state'),
  codexUsage: () => _fetchJson('/api/system/codex-usage'),
};

API.fx = {
  rates: () => _fetchJson('/api/fx/rates'),
};

window.API = API;

// ─── React hooks (deps: React must already be loaded) ──────────────────────

const { useState: _useApiState, useEffect: _useApiEffect, useCallback: _useApiCallback, useRef: _useApiRef } = React;

/**
 * useApi(fetcher, deps, opts) — runs fetcher() on mount + dep change.
 * Returns { data, error, loading, reload }.
 * opts.refreshMs (optional): auto-reload interval.
 * opts.skip (optional bool): skip the initial fetch.
 */
function useApi(fetcher, deps = [], opts = {}) {
  const [state, setState] = _useApiState({ data: null, error: null, loading: !opts.skip });
  const aliveRef = _useApiRef(true);
  const fetcherRef = _useApiRef(fetcher);
  fetcherRef.current = fetcher;

  const run = _useApiCallback(async () => {
    setState(s => ({ ...s, loading: true }));
    const res = await fetcherRef.current();
    if (!aliveRef.current) return;
    if (res.ok) setState({ data: res.data, error: null, loading: false });
    else        setState({ data: null,     error: res.error, loading: false });
  }, []);

  _useApiEffect(() => {
    aliveRef.current = true;
    // opts.skip is in the dep list so a skip:true→false transition (e.g. a
    // lazy tab becoming active) actually re-triggers the fetch. Without it the
    // gated query would never run once unskipped.
    if (!opts.skip) run();
    else setState(s => ({ ...s, loading: false }));
    return () => { aliveRef.current = false; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [opts.skip, ...deps]);

  _useApiEffect(() => {
    if (!opts.refreshMs || opts.skip) return;
    const id = setInterval(() => { run(); }, opts.refreshMs);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [opts.refreshMs, opts.skip, ...deps]);

  return { ...state, reload: run };
}

window.useApi = useApi;

// Small util for compact KRW / USD formatting reused by widgets.
function fmtKRW(n) {
  if (n == null || isNaN(n)) return '—';
  const v = Number(n);
  if (Math.abs(v) >= 100000000) return (v / 100000000).toFixed(2) + '억';
  if (Math.abs(v) >= 10000)     return (v / 10000).toFixed(1) + '만';
  return Math.round(v).toLocaleString();
}
function fmtUSD(n) {
  if (n == null || isNaN(n)) return '—';
  const v = Number(n);
  return '$' + v.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function fmtPct(n, digits = 2) {
  if (n == null || isNaN(n)) return '—';
  return Number(n).toFixed(digits) + '%';
}
function fmtBytes(n) {
  if (n == null || isNaN(n)) return '—';
  const v = Number(n);
  if (v >= 1e9) return (v / 1e9).toFixed(2) + ' GB';
  if (v >= 1e6) return (v / 1e6).toFixed(1) + ' MB';
  if (v >= 1e3) return (v / 1e3).toFixed(0) + ' KB';
  return v + ' B';
}
function fmtRelativeTime(isoUtc) {
  // Promotes anything < 1 minute to "1m" so the UI never shows raw seconds
  // (which look noisy on a ticking page, and read as nonsense for stamps
  // that are stale by 12s). Handles future timestamps too (Codex window
  // resets_at, scheduled jobs) — returns "in 2h", "in 7d" instead of the
  // old bug "-8284s ago".
  if (!isoUtc) return '—';
  try {
    const t = new Date(isoUtc);
    if (isNaN(t.getTime())) return '—';
    const diffSec = (Date.now() - t.getTime()) / 1000;
    const future = diffSec < 0;
    const abs = Math.abs(diffSec);
    let val, unit;
    if (abs < 60)         { val = 1;                       unit = 'm'; }
    else if (abs < 3600)  { val = Math.floor(abs / 60);    unit = 'm'; }
    else if (abs < 86400) { val = Math.floor(abs / 3600);  unit = 'h'; }
    else                  { val = Math.floor(abs / 86400); unit = 'd'; }
    return future ? `in ${val}${unit}` : `${val}${unit} ago`;
  } catch (_) { return '—'; }
}
window.fmtKRW = fmtKRW;
window.fmtUSD = fmtUSD;
window.fmtPct = fmtPct;
window.fmtBytes = fmtBytes;
window.fmtRelativeTime = fmtRelativeTime;
