// ============================================================
// SocialPulse — Real API Client
// Mirrors public/js/api.js contracts from the original repo
// ============================================================

const SP_API_BASE = ''; // same-origin; set to 'https://your-api.com' for cross-origin dev

// ---- Auth state ----
let _authToken = localStorage.getItem('sp_token') || null;
let _currentUser = null;
const _authListeners = new Set();

function spGetToken() { return _authToken; }
function spGetUser() { return _currentUser; }
function spIsLoggedIn() { return !!_authToken; }
function spOnAuthChange(fn) { _authListeners.add(fn); return () => _authListeners.delete(fn); }
function _notifyAuth() { _authListeners.forEach(fn => { try { fn(_currentUser); } catch(e){} }); }

function spSetAuth(token, user) {
  _authToken = token || null;
  _currentUser = user || null;
  if (token) localStorage.setItem('sp_token', token);
  else localStorage.removeItem('sp_token');
  _notifyAuth();
}

// ---- Core fetcher ----
async function spApi(path, opts = {}) {
  const url = SP_API_BASE + (path.startsWith('/') ? path : '/' + path);
  const headers = { 'Content-Type': 'application/json', ...(opts.headers || {}) };
  if (_authToken) headers['Authorization'] = `Bearer ${_authToken}`;
  const timeout = opts.timeout || 60000;
  const ctrl = new AbortController();
  const tid = setTimeout(() => ctrl.abort(), timeout);

  let res;
  try {
    res = await fetch(url, {
      method: opts.method || 'GET',
      headers,
      body: opts.body ? JSON.stringify(opts.body) : undefined,
      signal: ctrl.signal,
    });
  } catch (e) {
    clearTimeout(tid);
    if (e.name === 'AbortError') throw new Error('請求超時，請稍後再試');
    throw new Error('網路錯誤：' + e.message);
  }
  clearTimeout(tid);

  const text = await res.text();
  let json;
  try { json = text ? JSON.parse(text) : {}; }
  catch (e) { throw new Error(res.ok ? '回應格式錯誤' : `伺服器錯誤 ${res.status}`); }

  if (res.status === 401) {
    spSetAuth(null, null);
    throw new Error('請重新登入');
  }
  if (!res.ok || json.error) {
    throw new Error(json.error || `錯誤 ${res.status}`);
  }
  return json;
}

// ---- Auth ----
// Flatten nested quota (server returns { ..user, quota:{credits:{used,limit}, brandLimit, historyDays}})
// Pages expect monthly_credits / credits_used / max_brands / history_days.
function _normalizeUser(u) {
  if (!u || typeof u !== 'object') return u;
  const q = u.quota || {};
  const credits = q.credits || {};
  // No hardcoded plan defaults — quota comes from server. Undefined until /me
  // resolves is better than lying (Pro users briefly seeing a 3-brand cap).
  return {
    ...u,
    monthly_credits: credits.limit ?? u.monthly_credits,
    credits_used: credits.used ?? u.credits_used ?? 0,
    max_brands: q.brandLimit ?? u.max_brands,
    history_days: q.historyDays ?? u.history_days,
  };
}

async function spLogin(email, password) {
  const r = await spApi('/api/auth/login', { method: 'POST', body: { email, password } });
  spSetAuth(r.token, _normalizeUser(r.user));
  spFetchMe(); // fill in quota fields that login doesn't return
  return _currentUser;
}
async function spRegister(email, password, name) {
  const r = await spApi('/api/auth/register', { method: 'POST', body: { email, password, name } });
  spSetAuth(r.token, _normalizeUser(r.user));
  spFetchMe();
  return _currentUser;
}
async function spGoogleLogin(credential) {
  const r = await spApi('/api/auth/google', { method: 'POST', body: { credential } });
  spSetAuth(r.token, _normalizeUser(r.user));
  spFetchMe();
  return _currentUser;
}
async function spFetchMe() {
  if (!_authToken) return null;
  try {
    const r = await spApi('/api/auth/me');
    _currentUser = _normalizeUser(r);
    _notifyAuth();
    return _currentUser;
  } catch (e) { spSetAuth(null, null); return null; }
}
function spLogout() { spSetAuth(null, null); }
async function spForgotPassword(email) {
  return spApi('/api/auth/forgot-password', { method: 'POST', body: { email } });
}
async function spResetPassword(token, password) {
  return spApi('/api/auth/reset-password', { method: 'POST', body: { token, password } });
}
async function spLogoutAll() {
  try { await spApi('/api/auth/logout-all', { method: 'POST' }); }
  finally { spSetAuth(null, null); }
}

// ---- Brands ----
const spBrands = {
  list: (catId) => spApi('/api/brands' + (catId ? `?category_id=${catId}` : '')),
  get: (id) => spApi(`/api/brands/${id}`),
  create: (body) => spApi('/api/brands', { method: 'POST', body }),
  update: (id, body) => spApi(`/api/brands/${id}`, { method: 'PUT', body }),
  delete: (id) => spApi(`/api/brands/${id}`, { method: 'DELETE' }),
  fetchFbLogo: (fb_page_url) => spApi('/api/brands/fetch-fb-logo', { method: 'POST', body: { fb_page_url } }),
};

// ---- Posts ----
const spPosts = {
  list: (params = {}) => {
    const qs = new URLSearchParams();
    Object.entries(params).forEach(([k, v]) => { if (v != null && v !== '') qs.set(k, v); });
    return spApi('/api/posts?' + qs.toString());
  },
  exportCsvUrl: (params = {}) => {
    const qs = new URLSearchParams();
    Object.entries(params).forEach(([k, v]) => { if (v != null && v !== '') qs.set(k, v); });
    return SP_API_BASE + '/api/posts/export-csv?' + qs.toString();
  },
};

// ---- Stats / dashboard / categories / plans ----
const spStats = {
  dashboard: (params = {}) => {
    const qs = new URLSearchParams();
    Object.entries(params).forEach(([k, v]) => { if (v != null && v !== '') qs.set(k, v); });
    return spApi('/api/stats' + (qs.toString() ? '?' + qs.toString() : ''));
  },
  creditHistory: (limit = 50) => spApi(`/api/credits/history?limit=${limit}`),
  plans: () => spApi('/api/plans'),
  categories: () => spApi('/api/categories'),
};

// ---- Reports ----
const spReports = {
  list: (params = {}) => {
    const qs = new URLSearchParams();
    Object.entries(params).forEach(([k, v]) => { if (v != null && v !== '') qs.set(k, v); });
    return spApi('/api/reports' + (qs.toString() ? '?' + qs.toString() : ''));
  },
  get: (id) => spApi(`/api/reports/${id}`),
  generating: () => spApi('/api/reports/generating'),
  estimate: (body) => spApi('/api/reports/estimate', { method: 'POST', body }),
  // Must exceed vercel.json maxDuration (300s) so a slow-but-successful
  // generation returns through this call instead of aborting client-side.
  generate: (body) => spApi('/api/reports/generate', { method: 'POST', body, timeout: 310000 }),
  delete: (id) => spApi(`/api/reports/${id}`, { method: 'DELETE' }),
};

// ---- Plans ----
const spPlans = {
  list: () => spApi('/api/plans'),
};

// ---- Calendar ----
const spCalendar = {
  get: (year, month) => spApi(`/api/calendar?year=${year}&month=${month}`),
  events: () => spApi('/api/calendar/events'),
};

// ---- Scrape ----
// Production endpoints are per-platform async. We expose both per-platform + a
// convenience trigger that runs FB + IG in parallel for a given brand.
const spScrape = {
  fb: (brandId, body = {}) => spApi(`/api/scrape/fb/${brandId}`, { method: 'POST', body }),
  ig: (brandId, body = {}) => spApi(`/api/scrape/ig/${brandId}`, { method: 'POST', body }),
  poll: (taskId) => spApi(`/api/scrape/poll/${taskId}`),
  running: () => spApi('/api/scrape/running'),
  estimate: (body) => spApi('/api/scrape/estimate', { method: 'POST', body }),
  batchPlan: (body) => spApi('/api/scrape/all', { method: 'POST', body }),
  // Kick off scrape for whichever platforms the brand has.
  trigger: async (brand, days = 14) => {
    const newerThan = `${days} days`;
    const tasks = [];
    if (brand.fb_page_id || brand.fb_page_url) tasks.push(spScrape.fb(brand.id, { newerThan }));
    if (brand.ig_username) tasks.push(spScrape.ig(brand.id, {}));
    if (!tasks.length) throw new Error('此品牌尚未設定 FB 或 IG 帳號');
    return Promise.all(tasks);
  },
};

// ---- React hooks ----
const { useState: _useState, useEffect: _useEffect, useCallback: _useCallback } = React;

function useApi(fn, deps = []) {
  const [data, setData] = _useState(null);
  const [loading, setLoading] = _useState(true);
  const [error, setError] = _useState(null);
  const reload = _useCallback(() => {
    setLoading(true); setError(null);
    fn().then(d => { setData(d); setLoading(false); })
        .catch(e => { setError(e.message || String(e)); setLoading(false); });
  }, deps);
  _useEffect(() => { reload(); }, deps);
  return { data, loading, error, reload };
}

function useAuth() {
  const [user, setUser] = _useState(spGetUser());
  _useEffect(() => {
    const off = spOnAuthChange(setUser);
    if (spIsLoggedIn() && !spGetUser()) spFetchMe();
    return off;
  }, []);
  return { user, isLoggedIn: !!user, logout: spLogout };
}

// ============================================================
// Scrape task tracker — survives tab switches + page refresh.
// Module-level so multiple pages can show the same in-flight state.
// ============================================================
const _scrapeTasks = new Map(); // taskId -> { taskId, brandId, brandName, pct, status, error }
const _scrapeListeners = new Set();
function _notifyScrape() { _scrapeListeners.forEach(fn => { try { fn(); } catch {} }); }

function _pollScrapeTask(taskId) {
  const tick = async () => {
    if (!_scrapeTasks.has(taskId)) return;
    try {
      const r = await spScrape.poll(taskId);
      const t = _scrapeTasks.get(taskId);
      if (!t) return;
      if (r.status === 'completed') {
        _scrapeTasks.delete(taskId);
        _notifyScrape();
        spFetchMe();
      } else if (r.status === 'failed') {
        _scrapeTasks.set(taskId, { ...t, status: 'failed', error: r.error || '更新失敗' });
        _notifyScrape();
        setTimeout(() => { _scrapeTasks.delete(taskId); _notifyScrape(); }, 6000);
      } else {
        _scrapeTasks.set(taskId, { ...t, pct: Math.min((t.pct || 10) + 2, 90) });
        _notifyScrape();
        setTimeout(tick, 3000);
      }
    } catch (e) {
      setTimeout(tick, 5000);
    }
  };
  tick();
}

// platforms: undefined (both) | ['fb'] | ['ig']. Caller usually passes only one
// platform to target the FB or IG button specifically.
async function spStartBrandScrape(brand, days = 14, platforms) {
  const newerThan = `${days} days`;
  const want = platforms && platforms.length ? platforms : ['fb','ig'];
  const launches = [];
  if (want.includes('fb') && (brand.fb_page_id || brand.fb_page_url)) {
    launches.push(spScrape.fb(brand.id, { newerThan }).catch(e => ({ _err: e })));
  }
  if (want.includes('ig') && brand.ig_username) {
    launches.push(spScrape.ig(brand.id, {}).catch(e => ({ _err: e })));
  }
  if (!launches.length) throw new Error('此品牌尚未設定 ' + want.map(p => p.toUpperCase()).join('/') + ' 帳號');
  const results = await Promise.all(launches);
  const errs = [];
  for (const r of results) {
    if (r?._err) { errs.push(r._err.message); continue; }
    if (r?.taskId) {
      _scrapeTasks.set(r.taskId, {
        taskId: r.taskId, brandId: brand.id, brandName: brand.name,
        pct: 10, status: 'running', startedAt: Date.now(),
      });
      _pollScrapeTask(r.taskId);
    }
  }
  _notifyScrape();
  if (!_scrapeTasks.size && errs.length) throw new Error(errs.join(' / '));
  if (errs.length) return { partial: true, errors: errs };
  return { ok: true };
}

// Batch: launch scrape for every brand the user owns that has the given
// platform. The backend /scrape/all endpoint only builds the plan + checks
// credits; actual per-platform starts still happen client-side so each task
// shows up in the in-flight tracker.
//
// We register an optimistic placeholder task for each planned launch BEFORE
// firing the HTTP requests. This makes the RunningBanner appear instantly
// (the per-brand backend calls can take 2-5s each). Real taskIds replace
// placeholders on success; failures remove them and bubble up.
async function spStartBatchScrape({ days = 7, platform = 'all' } = {}) {
  const plan = await spScrape.batchPlan({
    newerThan: `${days} days`, platform,
  });
  const tasks = plan.tasks || [];
  if (!tasks.length) throw new Error('沒有可爬取的品牌');

  // Backend already reserved a pendingId per task atomically. We forward each
  // pendingId to the per-platform scrape call so it doesn't double-reserve.
  // If a launch fails before the scrape route runs, release the orphan reservation.
  const launches = tasks.map(t => {
    const placeholderId = `_pending_${t.brandId}_${t.platform}_${Date.now()}_${Math.random().toString(36).slice(2,6)}`;
    _scrapeTasks.set(placeholderId, {
      taskId: placeholderId, brandId: t.brandId, brandName: `${t.brandName}（${t.platform.toUpperCase()}）`,
      pct: 5, status: 'starting', startedAt: Date.now(),
    });
    const body = t.platform === 'fb'
      ? { newerThan: `${days} days`, pendingId: t.pendingId }
      : { pendingId: t.pendingId };
    const call = t.platform === 'fb' ? spScrape.fb(t.brandId, body) : spScrape.ig(t.brandId, body);
    return call.then(
      r => ({ ...r, brandId: t.brandId, brandName: t.brandName, placeholderId, pendingId: t.pendingId }),
      e => ({ _err: e, brandName: t.brandName, placeholderId, pendingId: t.pendingId })
    );
  });
  _notifyScrape();

  const results = await Promise.all(launches);
  const errs = [];
  let ok = 0;
  for (const r of results) {
    _scrapeTasks.delete(r.placeholderId);
    if (r?._err) {
      errs.push(`${r.brandName}: ${r._err.message}`);
      // Release the per-task reservation so it doesn't sit orphan until cron reaps.
      if (r.pendingId) {
        spApi('/api/scrape/release-pending', { method: 'POST', body: { pendingId: r.pendingId } }).catch(() => {});
      }
      continue;
    }
    if (r?.taskId) {
      _scrapeTasks.set(r.taskId, {
        taskId: r.taskId, brandId: r.brandId, brandName: r.brandName,
        pct: 10, status: 'running', startedAt: Date.now(),
      });
      _pollScrapeTask(r.taskId);
      ok++;
    }
  }
  _notifyScrape();
  return { ok, errors: errs, estimateMin: plan.estimateMin, estimateMax: plan.estimateMax };
}

async function spRestoreScrapeTasks() {
  try {
    const running = await spScrape.running();
    for (const t of (running || [])) {
      if (_scrapeTasks.has(t.id)) continue;
      _scrapeTasks.set(t.id, {
        taskId: t.id, brandId: t.brand_id, brandName: t.brand_name,
        pct: 20, status: 'running', startedAt: Date.parse(t.created_at) || Date.now(),
      });
      _pollScrapeTask(t.id);
    }
    _notifyScrape();
  } catch {}
}

function useScrapeTasks(brandId) {
  const [, setTick] = _useState(0);
  _useEffect(() => {
    const fn = () => setTick(v => v + 1);
    _scrapeListeners.add(fn);
    return () => _scrapeListeners.delete(fn);
  }, []);
  const all = Array.from(_scrapeTasks.values());
  return brandId ? all.filter(t => t.brandId === brandId) : all;
}

Object.assign(window, {
  SP_API_BASE, spApi, spGetToken, spGetUser, spIsLoggedIn, spOnAuthChange, spSetAuth,
  spLogin, spRegister, spGoogleLogin, spFetchMe, spLogout,
  spForgotPassword, spResetPassword, spLogoutAll,
  spBrands, spPosts, spStats, spReports, spPlans, spCalendar, spScrape,
  spStartBrandScrape, spStartBatchScrape, spRestoreScrapeTasks, useScrapeTasks,
  useApi, useAuth,
});
