// data-loader.jsx — 把後端 admin API 拉成 mock-shape，pages 元件大致不需改。
//
// 為什麼用 mock-shape adapter 而非每 page 各自 useEffect：
//   - mock-shape 子元件（BrandsTable, ConvoListItem, KPICard...）依賴的欄位很多
//     （tier / dailyCost array / status / anomaly...），後端目前回不全
//   - 在這層補 fallback 值，pages 一行不動 → MVP 上線最快
//   - 後續逐欄補真：例如 plan_tier 從 MySQL.brand 拿到了，就在這裡換掉 fallback
//
// 暴露：
//   window.AdminDataLoader.load(range)        → 把 normalized data 寫到 window.MOCK + window.LIVE
//   window.LIVE                                = 各 endpoint 原始 JSON（給需要 delta % 的 KPI cards 用）
//   window.MOCK                                = mock-shape normalized data
//   window.AdminDataLoader.normalizeMessages(msgs) → LangChain → mock viewer shape

(function () {
  const PROVIDERS = [
    { match: /claude/i, provider: 'anthropic' },
    { match: /gpt|openai/i, provider: 'openai' },
    { match: /gemini/i, provider: 'google' },
  ];
  function providerOf(model) {
    if (!model) return 'unknown';
    for (const p of PROVIDERS) if (p.match.test(model)) return p.provider;
    return 'other';
  }

  // 後端 thread_id 是長 UUID-ish；admin viewer 想顯示成 mock-style 的 'thread_xxx'，
  // 我們直接拿 thread_id 字串。
  function statusFromConvo(c) {
    if (c.total_tool_calls > 20) return 'looped';
    if ((c.message_count || 0) > 50) return 'long';
    // 'errored' / 'abandoned' 暫無偵測訊號 — 預設 completed
    return 'completed';
  }

  function normalizeBrands(topBrands, brandSeriesMap, days, globalCacheHit) {
    const DAYS = Math.max(days.length, 1);
    return topBrands.map((b) => {
      const bid = String(b.brand_id);
      const dailyCost = (brandSeriesMap[bid] || Array(DAYS).fill(0)).map((v) => +(+v).toFixed(4));
      const totalDaily = dailyCost.reduce((a, n) => a + n, 0) || 1;
      const totalConvos = b.total_convos || 0;
      // 沒有真實 per-day 對話數 → 按費用比例分攤
      const dailyConvos = dailyCost.map((c) => Math.max(0, Math.round((c / totalDaily) * totalConvos)));

      const todayCost = dailyCost[dailyCost.length - 1] || 0;
      const recent7 = dailyCost.slice(-8, -1);
      const avg7 = recent7.length ? recent7.reduce((a, n) => a + n, 0) / recent7.length : 0;
      const anomalyRatio = avg7 > 0 ? +(todayCost / avg7).toFixed(2) : 0;
      const isAnomalous = avg7 > 0.5 && anomalyRatio > 3;

      const lastTs = b.last_activity_at ? new Date(b.last_activity_at).getTime() : null;
      const lastActivityHrs = lastTs ? Math.max(0, Math.floor((Date.now() - lastTs) / 3600000)) : 9999;
      const status = lastActivityHrs > 168 ? 'dormant' : isAnomalous ? 'anomalous' : 'active';

      return {
        id: bid,
        name: b.brand_name || bid,
        tier: 'growth',  // 未接 MySQL plan_tier，先給中性值
        contactEmail: '—',
        plan_ceiling_usd: 1500,
        activeUsers: 0,
        dailyCost,
        dailyConvos,
        totalCost: +(+(b.total_cost_usd || 0)).toFixed(4),
        totalConvos,
        tokens: b.total_tokens || 0,
        avgCostPerConvo: +(+(b.avg_cost_per_convo || 0)).toFixed(5),
        // per-brand cache_hit_rate 後端已聚合（cache_read / (input+cache_read)）；fallback 到全域避免 0
        cacheHit: typeof b.cache_hit_rate === 'number' ? b.cache_hit_rate : globalCacheHit,
        todayCost: +todayCost.toFixed(4),
        anomalyRatio,
        isAnomalous,
        lastActivityHrs,
        status,
      };
    });
  }

  function normalizeConversations(rows) {
    return (rows || []).map((c) => {
      const inputT = c.total_input_tokens || 0;
      const cacheRead = c.total_cache_read_tokens || 0;
      const denom = inputT + cacheRead;
      const brandIdStr = c.brand_id != null ? String(c.brand_id) : '';
      const adminIdStr = c.user_admin_id != null ? String(c.user_admin_id) : '';
      // brand/admin 顯示 name，沒拿到 name 才 fallback 到 ID 字串
      const brandDisplay = c.brand_name || brandIdStr || '—';
      const adminDisplay = c.user_admin_name || (adminIdStr ? `admin#${adminIdStr}` : '—');
      return {
        thread_id: c.thread_id,
        brand_id: brandIdStr,           // raw idx（給 hover tooltip）
        brand_name: c.brand_name || '', // 真實 name
        brand_display: brandDisplay,    // 預設顯示用：name → fallback id
        user_admin_id: adminIdStr,      // raw idx（給 hover tooltip）
        user_admin: adminDisplay,       // 預設顯示用：name → fallback admin#id
        user_admin_name: c.user_admin_name || '',
        user_admin_email: '',
        channel: c.channel || 'dashboard',
        model: c.last_model || '',
        title: c.title || '新對話',
        created_at: c.created_at ? new Date(c.created_at) : new Date(),
        // 軟刪標記：使用者刪了但 admin 仍可閱讀，UI 加 icon + 灰化
        deleted_at: c.deleted_at ? new Date(c.deleted_at) : null,
        msgCount: c.message_count || 0,
        toolCount: c.total_tool_calls || 0,
        tokens: c.total_tokens || 0,
        tokensInput: inputT,
        tokensOutput: c.total_output_tokens || 0,
        tokensCacheRead: cacheRead,
        tokensCacheCreate: c.total_cache_creation_tokens || 0,
        cacheHit: denom > 0 ? cacheRead / denom : 0,
        cost: +(+(c.total_cost_usd || 0)).toFixed(5),
        status: statusFromConvo(c),
      };
    });
  }

  // LangChain BaseMessage → mock viewer shape（{role, content, tool_calls, tool_result, tokens, ...}）。
  // 後端 _serialize_message 已給 plain-dict {role, content, tool_calls, tool_call_id}。
  function normalizeMessages(messages) {
    const normalized = (messages || []).map((m) => {
      const role = m.role || 'unknown';
      let content = '';
      const tool_calls = [];
      let tool_result = null;

      if (typeof m.content === 'string') {
        content = m.content;
      } else if (Array.isArray(m.content)) {
        // content blocks：只取 text 與 tool_result，**不**從 tool_use 拿 args
        // — Anthropic content_block 序列化常把 input 丟成空 {}；真正可信的 args 是 m.tool_calls[].args
        const textChunks = [];
        for (const block of m.content) {
          if (block.type === 'text' && block.text) textChunks.push(block.text);
          if (block.type === 'tool_result') {
            const txt = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
            tool_result = { name: block.tool_use_id || 'tool', preview: txt.slice(0, 4000) };
          }
        }
        content = textChunks.join('\n\n');
      } else if (m.content != null) {
        content = String(m.content);
      }

      // tool_calls：一律以 m.tool_calls (LangChain 標準) 為唯一 source
      // — content blocks 的 tool_use.input 在後端序列化階段已遺失，那條路是不可靠的 fallback
      if (Array.isArray(m.tool_calls) && m.tool_calls.length) {
        for (const tc of m.tool_calls) {
          const realArgs = (tc.args && typeof tc.args === 'object' && Object.keys(tc.args).length)
            ? tc.args
            : (tc.arguments && typeof tc.arguments === 'object' ? tc.arguments : {});
          tool_calls.push({
            id: tc.id, name: tc.name, args: realArgs, duration_ms: 0,
          });
        }
      }

      // ToolMessage（role=tool）— content 是工具回傳值。
      // 把它從 content（會在 message bubble 直接 render 一大坨）移到 tool_result（ToolResultBlock 摺疊）。
      if (role === 'tool') {
        if (tool_result == null) {
          const txt = typeof m.content === 'string' ? m.content
            : Array.isArray(m.content) ? m.content.map((b) => b.text || JSON.stringify(b)).join('\n')
            : String(m.content);
          tool_result = { name: m.tool_call_id || 'tool', preview: txt.slice(0, 4000) };
        }
        content = '';  // 不重複：tool_result 已包含同一塊內容
      }

      // 紀錄原始 tool_call_id 以便後續 merge（tool_result.name 是 fallback string，可能不是 id）
      const tool_call_id = m.tool_call_id || (tool_result && tool_result.name) || null;

      return {
        role,
        content,
        tool_calls,
        tool_result,
        tool_call_id,
        tokens: null,  // checkpoint 沒帶 per-msg token 細項；總量由 sidebar 從 conversation cost 顯示
        cost: null,
        model: null,
        latency_ms: null,
        timestamp: '',
      };
    });
    return mergeToolResultsIntoCalls(normalized);
  }

  // 把 role=tool 訊息（工具回傳值）依 tool_call_id 配對到上方 assistant message 的 tool_calls，
  // 變成 ToolCallBlock 內的 `result` 欄位。然後從訊息流移除獨立 tool 訊息——讓 viewer 看起來
  // 一個 assistant turn = 文字 + (call+回傳) 並列，不會再出現「下一個 turn 是工具結果」的視覺斷裂。
  function mergeToolResultsIntoCalls(messages) {
    const resultByCallId = {};
    for (const m of messages) {
      if (m.role === 'tool' && m.tool_result && m.tool_call_id) {
        resultByCallId[m.tool_call_id] = m.tool_result;
      }
    }
    return messages
      .filter((m) => m.role !== 'tool')
      .map((m) => {
        if (!m.tool_calls || !m.tool_calls.length) return m;
        return {
          ...m,
          tool_calls: m.tool_calls.map((tc) => ({
            ...tc,
            result: resultByCallId[tc.id] || null,
          })),
        };
      });
  }

  // 把 sparse series（{date_iso: value}）對齊到 fullDates array：缺的位置補 0
  function alignToDates(fullDates, byDate) {
    return fullDates.map((d) => +(byDate[d] || 0));
  }

  // 規則：
  //   today    → 今天 1 天
  //   7d / 30d → 滾動 N 天到今天
  //   month    → 本月 1 號到今天
  //   last     → 上月整月（1 號到上月最後一天）
  // backend cost-trend / token-mix 也跟著這個 range 算，所以 fullDates 會跟回傳資料對得上。
  function buildFullDates(range) {
    const TODAY = new Date();
    const todayMidUtc = Date.UTC(TODAY.getUTCFullYear(), TODAY.getUTCMonth(), TODAY.getUTCDate());
    const dayMs = 86400000;
    const fmt = (utcMs) => new Date(utcMs).toISOString().slice(0, 10);
    const fullDates = [];

    if (range === 'today') {
      fullDates.push(fmt(todayMidUtc));
    } else if (range === 'month') {
      const first = Date.UTC(TODAY.getUTCFullYear(), TODAY.getUTCMonth(), 1);
      for (let t = first; t <= todayMidUtc; t += dayMs) fullDates.push(fmt(t));
    } else if (range === 'last') {
      const lastMonthEnd = Date.UTC(TODAY.getUTCFullYear(), TODAY.getUTCMonth(), 0);  // 0 day = 上月最後一天
      const lastMonthFirst = Date.UTC(new Date(lastMonthEnd).getUTCFullYear(), new Date(lastMonthEnd).getUTCMonth(), 1);
      for (let t = lastMonthFirst; t <= lastMonthEnd; t += dayMs) fullDates.push(fmt(t));
    } else {
      const N = range === '7d' ? 7 : 30;
      for (let i = N - 1; i >= 0; i--) fullDates.push(fmt(todayMidUtc - i * dayMs));
    }
    return { fullDates, DAYS: fullDates.length, TODAY };
  }

  function buildBrandSeriesMap(costTrend, fullDates) {
    const costTrendDays = (costTrend && costTrend.days) || [];
    const brandSeriesMap = {};
    for (const s of (costTrend && costTrend.series) || []) {
      if (!s.key || s.key === 'other') continue;
      const byDate = {};
      (s.values || []).forEach((v, i) => {
        if (costTrendDays[i]) byDate[costTrendDays[i]] = v;
      });
      brandSeriesMap[String(s.key)] = alignToDates(fullDates, byDate);
    }
    return brandSeriesMap;
  }

  function buildDailyAgg(fullDates, brandSeriesMap) {
    return fullDates.map((date, i) => {
      const perBrand = {};
      let total = 0;
      for (const [bid, values] of Object.entries(brandSeriesMap)) {
        const v = values[i] || 0;
        perBrand[bid] = v;
        total += v;
      }
      return { date, total: +total.toFixed(4), perBrand };
    });
  }

  function buildTokenMix(tokenMix, fullDates) {
    const byDate = {};
    for (const d of (tokenMix && tokenMix.days) || []) {
      if (d.date) byDate[d.date] = d;
    }
    return fullDates.map((date) => {
      const d = byDate[date] || {};
      const fresh = d.fresh || 0;
      const cacheRead = d.cache_read || 0;
      const denom = cacheRead + fresh;
      return {
        date, fresh, cacheRead,
        cacheCreate: d.cache_creation || 0,
        output: d.output || 0,
        cacheHit: denom > 0 ? +(cacheRead / denom).toFixed(3) : 0,
      };
    });
  }

  function buildModels(byModel) {
    return ((byModel && byModel.models) || []).map((m) => [
      m.model, providerOf(m.model), 0.001, 0.005,  // prices placeholder
    ]);
  }

  function buildPerModel(byModel) {
    return ((byModel && byModel.models) || []).map((m) => ({
      model: m.model, provider: providerOf(m.model),
      requests: m.requests, tokens: m.tokens, cost: m.cost_usd,
      avgPer: m.requests > 0 ? +(m.cost_usd / m.requests).toFixed(5) : 0,
    }));
  }

  // window.MOCK / LIVE 永遠保有有效空殼，pages 在 loading 時也不會 crash。
  function ensureGlobals() {
    const { fullDates, DAYS, TODAY } = buildFullDates('30d');
    if (!window.MOCK) {
      window.MOCK = {
        TODAY, DAYS,
        BRANDS: [], DAILY_AGG: fullDates.map((d) => ({ date: d, total: 0, perBrand: {} })),
        TOKEN_MIX: fullDates.map((d) => ({ date: d, fresh: 0, cacheRead: 0, cacheCreate: 0, output: 0, cacheHit: 0 })),
        MODELS: [['claude-haiku-4-5', 'anthropic', 0.0008, 0.004]],
        PER_MODEL: [], CONVERSATIONS: [],
        STAFF: [], SAMPLE_THREAD: { messages: [] },
      };
    }
    if (!window.LIVE) {
      window.LIVE = { kpis: null, costTrend: null, topBrands: null, brandsList: null,
                      conversations: null, byModel: null, cacheRate: null, tokenMix: null };
    }
  }

  // 並行打 endpoints；任一失敗只警告不中止整批。
  async function callEndpoints(spec) {
    const keys = Object.keys(spec);
    const settled = await Promise.allSettled(keys.map((k) => spec[k]()));
    const out = {};
    keys.forEach((k, i) => {
      if (settled[i].status === 'fulfilled') {
        out[k] = settled[i].value;
      } else {
        console.warn('[AdminDataLoader]', k, 'failed:', settled[i].reason);
        out[k] = null;
      }
    });
    return out;
  }

  // ── Per-route loaders ─────────────────────────────────────
  // 共用 helper：拉 cost-trend + brands list + cache rate + (optional byModel/tokenMix)，重建 BRANDS / DAILY_AGG。
  // overview / brands / cost 都需要 BRANDS（topBar / 散布圖 / cache 排行），所以這三頁共用 builder。

  async function loadOverview(range) {
    const A = window.AdminAPI;
    const r = await callEndpoints({
      kpis: () => A.overview.kpis(range),
      costTrend: () => A.overview.costTrend(range, 5),
      topBrands: () => A.overview.topBrands(range, 30),
      cacheRate: () => A.cost.cacheRate(range),
    });
    const { fullDates, DAYS, TODAY } = buildFullDates(range);
    const brandSeriesMap = buildBrandSeriesMap(r.costTrend, fullDates);
    const globalCacheHit = (r.cacheRate && r.cacheRate.rates && r.cacheRate.rates.length)
      ? r.cacheRate.rates[r.cacheRate.rates.length - 1] : 0;
    window.MOCK.TODAY = TODAY;
    window.MOCK.DAYS = DAYS;
    const topBrandsRows = (r.topBrands && r.topBrands.brands) || [];
    window.MOCK.BRANDS = normalizeBrands(topBrandsRows, brandSeriesMap, fullDates, globalCacheHit);
    window.MOCK.DAILY_AGG = buildDailyAgg(fullDates, brandSeriesMap);
    Object.assign(window.LIVE, r);
  }

  async function loadBrands(range) {
    const A = window.AdminAPI;
    const r = await callEndpoints({
      costTrend: () => A.overview.costTrend(range, 5),
      brandsList: () => A.brands.list(range, 100),
      cacheRate: () => A.cost.cacheRate(range),
    });
    const { fullDates, DAYS, TODAY } = buildFullDates(range);
    const brandSeriesMap = buildBrandSeriesMap(r.costTrend, fullDates);
    const globalCacheHit = (r.cacheRate && r.cacheRate.rates && r.cacheRate.rates.length)
      ? r.cacheRate.rates[r.cacheRate.rates.length - 1] : 0;
    const rows = (r.brandsList && r.brandsList.brands) || [];
    window.MOCK.TODAY = TODAY;
    window.MOCK.DAYS = DAYS;
    window.MOCK.BRANDS = normalizeBrands(rows, brandSeriesMap, fullDates, globalCacheHit);
    window.MOCK.DAILY_AGG = buildDailyAgg(fullDates, brandSeriesMap);
    Object.assign(window.LIVE, r);
  }

  // conversations.list 目前不接 range 參數（後端是看全期），因此 loader 不收 range
  async function loadConversations() {
    const A = window.AdminAPI;
    const r = await callEndpoints({
      conversations: () => A.conversations.list({ limit: 200 }),
    });
    window.MOCK.CONVERSATIONS = normalizeConversations((r.conversations && r.conversations.conversations) || []);
    Object.assign(window.LIVE, r);
  }

  async function loadCost(range) {
    const A = window.AdminAPI;
    const r = await callEndpoints({
      costTrend: () => A.overview.costTrend(range, 5),
      brandsList: () => A.brands.list(range, 100),
      byModel: () => A.cost.byModel(range),
      cacheRate: () => A.cost.cacheRate(range),
      tokenMix: () => A.cost.tokenMix(range),
    });
    const { fullDates, DAYS, TODAY } = buildFullDates(range);
    const brandSeriesMap = buildBrandSeriesMap(r.costTrend, fullDates);
    const globalCacheHit = (r.cacheRate && r.cacheRate.rates && r.cacheRate.rates.length)
      ? r.cacheRate.rates[r.cacheRate.rates.length - 1] : 0;
    const rows = (r.brandsList && r.brandsList.brands) || [];
    const MODELS = buildModels(r.byModel);
    window.MOCK.TODAY = TODAY;
    window.MOCK.DAYS = DAYS;
    window.MOCK.BRANDS = normalizeBrands(rows, brandSeriesMap, fullDates, globalCacheHit);
    window.MOCK.DAILY_AGG = buildDailyAgg(fullDates, brandSeriesMap);
    window.MOCK.TOKEN_MIX = buildTokenMix(r.tokenMix, fullDates);
    window.MOCK.MODELS = MODELS.length ? MODELS : [['claude-haiku-4-5', 'anthropic', 0.0008, 0.004]];
    window.MOCK.PER_MODEL = buildPerModel(r.byModel);
    Object.assign(window.LIVE, r);
  }

  async function loadForRoute(route, range = '30d') {
    ensureGlobals();
    if (route === 'conversations') return loadConversations();
    if (route === 'brands') return loadBrands(range);
    if (route === 'cost') return loadCost(range);
    return loadOverview(range);  // overview / 預設
  }

  ensureGlobals();
  window.AdminDataLoader = { loadForRoute, loadOverview, loadBrands, loadConversations, loadCost, normalizeMessages };
})();
