/* 即時報價 provider — 透過 Cloudflare Worker（worker/intraday-worker.js）取台美股即時報價
 *
 * window.Intraday = {
 *   getQuote(sym)              → Promise<Quote>
 *   subscribe(sym, cb, opts?)  → unsubscribe()
 *   isTradingHours(sym)        → bool
 *   isEnabled()                → bool（worker URL 已設定）
 *   getWorkerUrl/setWorkerUrl, getAuth/setAuth, getPollSeconds/setPollSeconds
 * }
 *
 * Quote 格式：
 *   { sym, price, open, high, low, prevClose, vol, ts, source }
 */

const WORKER_URL_KEY = "intraday.workerUrl";
const AUTH_KEY = "intraday.auth";
const POLL_KEY = "intraday.pollSeconds";

function getWorkerUrl() {
  return (window.TWStorage && window.TWStorage.get(WORKER_URL_KEY)) || "";
}
function setWorkerUrl(url) {
  if (window.TWStorage) window.TWStorage.set(WORKER_URL_KEY, (url || "").trim());
}
function getAuth() {
  return (window.TWStorage && window.TWStorage.get(AUTH_KEY)) || "";
}
function setAuth(secret) {
  if (window.TWStorage) window.TWStorage.set(AUTH_KEY, (secret || "").trim());
}
function getPollSeconds() {
  const v = window.TWStorage && window.TWStorage.get(POLL_KEY);
  const n = v ? parseInt(v, 10) : 10;
  return Number.isFinite(n) && n >= 3 ? n : 10;
}
function setPollSeconds(s) {
  const n = parseInt(s, 10);
  if (window.TWStorage && Number.isFinite(n) && n >= 3) {
    window.TWStorage.set(POLL_KEY, String(n));
  }
}
function isEnabled() {
  return !!getWorkerUrl();
}

async function getQuote(sym) {
  const base = getWorkerUrl();
  if (!base) throw new Error("尚未設定即時報價 Worker URL");
  const url = `${base.replace(/\/$/, "")}/quote?sym=${encodeURIComponent(sym)}`;
  const headers = {};
  const auth = getAuth();
  if (auth) headers["X-Auth"] = auth;
  const res = await fetch(url, { headers });
  const text = await res.text();
  let json;
  try { json = JSON.parse(text); }
  catch (e) { throw new Error(`Worker 回傳非 JSON：${text.slice(0, 80)}`); }
  if (!res.ok || json.error) throw new Error(json.error || `HTTP ${res.status}`);
  return json;
}

function tzNow(timeZone) {
  const now = new Date();
  return new Date(now.toLocaleString("en-US", { timeZone }));
}

function isTradingHoursTW() {
  const tw = tzNow("Asia/Taipei");
  const day = tw.getDay();
  if (day === 0 || day === 6) return false;
  const m = tw.getHours() * 60 + tw.getMinutes();
  return m >= 9 * 60 && m <= 13 * 60 + 30;
}
function isTradingHoursUS() {
  const et = tzNow("America/New_York");
  const day = et.getDay();
  if (day === 0 || day === 6) return false;
  const m = et.getHours() * 60 + et.getMinutes();
  return m >= 9 * 60 + 30 && m <= 16 * 60;
}
function isTradingHours(sym) {
  return sym.endsWith(".TW") ? isTradingHoursTW() : isTradingHoursUS();
}

/* 批次抓多檔 quote — Promise.all 並發，個別失敗不影響其他 */
async function getQuotes(syms) {
  const results = await Promise.allSettled(syms.map((s) => getQuote(s)));
  const quotes = new Map();
  const errors = new Map();
  results.forEach((r, i) => {
    const sym = syms[i];
    if (r.status === "fulfilled") quotes.set(sym, r.value);
    else errors.set(sym, r.reason?.message || String(r.reason));
  });
  return { quotes, errors };
}

/* 監聽多檔 quote — 每 N 秒批次更新 */
function subscribeMany(syms, callback, options = {}) {
  if (!getWorkerUrl() || syms.length === 0) {
    callback({ quotes: new Map(), errors: new Map() });
    return () => {};
  }
  const intervalMs = options.intervalMs ?? getPollSeconds() * 1000;
  let cancelled = false;
  let timer = null;

  const tick = async () => {
    if (cancelled) return;
    const result = await getQuotes(syms);
    if (!cancelled) callback(result);
  };

  tick();
  /* 任一檔在交易時段就 polling（混合台美股的清單常見） */
  const anyTrading = syms.some((s) => isTradingHours(s));
  if (anyTrading) {
    timer = setInterval(tick, intervalMs);
  }

  return () => {
    cancelled = true;
    if (timer) clearInterval(timer);
  };
}

function subscribe(sym, callback, options = {}) {
  if (!getWorkerUrl()) {
    callback(null, new Error("尚未設定即時報價 Worker"));
    return () => {};
  }
  const intervalMs = options.intervalMs ?? getPollSeconds() * 1000;
  let cancelled = false;
  let timer = null;

  const tick = async () => {
    if (cancelled) return;
    try {
      const q = await getQuote(sym);
      if (!cancelled) callback(q, null);
    } catch (e) {
      if (!cancelled) callback(null, e);
    }
  };

  tick();
  if (isTradingHours(sym)) {
    timer = setInterval(tick, intervalMs);
  }

  return () => {
    cancelled = true;
    if (timer) clearInterval(timer);
  };
}

window.Intraday = {
  getQuote, getQuotes, subscribe, subscribeMany, isTradingHours, isEnabled,
  isTradingHoursTW, isTradingHoursUS,
  getWorkerUrl, setWorkerUrl,
  getAuth, setAuth,
  getPollSeconds, setPollSeconds,
};
