/* Observability chart rendering and page actions.
 * Chart.js is loaded lazily through analysis-chart-theme.js so observability
 * pages do not block initial HTML parsing on a chart library.
 */
(function () {
  'use strict';

  const providerLabelMap = {
  gcp_ollama: '主力建議路徑',
  ollama_secondary: '備援建議路徑',
  ollama_111: '第三建議路徑',
  gemini: '雲端備援',
    claude: '雲端審查備援',
    nim: '快速雲端建議',
    openrouter: '外部備援路徑',
    nim_via_elephant: '雲端加速備援'
  };

  function readJson(id, fallback) {
    const node = document.getElementById(id);
    if (!node) return fallback;
    try {
      return JSON.parse(node.textContent || 'null') ?? fallback;
    } catch (error) {
      console.error(`[observability] JSON parse failed: ${id}`, error);
      return fallback;
    }
  }

  function loadChartJs() {
    if (window.EwoooCChartTheme && window.EwoooCChartTheme.loadChartJs) {
      return window.EwoooCChartTheme.loadChartJs();
    }
    if (window.Chart) return Promise.resolve(window.Chart);
    return Promise.reject(new Error('Chart.js loader unavailable'));
  }

  function runWhenVisible(selector, render) {
    const targets = Array.from(document.querySelectorAll(selector));
    if (!targets.length) return;
    if (!('IntersectionObserver' in window)) {
      render();
      return;
    }
    const observer = new IntersectionObserver(entries => {
      if (!entries.some(entry => entry.isIntersecting)) return;
      observer.disconnect();
      render();
    }, { rootMargin: '240px 0px' });
    targets.forEach(target => observer.observe(target));
  }

  function mountChart(selector, render) {
    runWhenVisible(selector, () => {
      loadChartJs()
        .then(render)
        .catch(error => console.error('[observability] Chart.js 載入失敗:', error));
    });
  }

  function renderOverviewHostSparklines() {
    const data = readJson('obs-host-sparkline-data', {});
    mountChart('canvas[data-host-sparkline]', () => {
      document.querySelectorAll('canvas[data-host-sparkline]').forEach(el => {
        if (el.dataset.chartReady === '1') return;
        const label = el.getAttribute('data-host-sparkline');
        const sp = data[label];
        if (!sp || !sp.hours || !sp.hours.length) return;
        el.dataset.chartReady = '1';
        new Chart(el, {
          type: 'line',
          data: {
            labels: sp.hours,
            datasets: [{
              data: sp.uptime_pct,
              borderColor: '#c96442',
              backgroundColor: 'rgba(201, 100, 66, 0.14)',
              borderWidth: 1.8,
              fill: true,
              tension: 0.42,
              pointRadius: 0
            }]
          },
          options: {
            responsive: true,
            maintainAspectRatio: false,
            plugins: {
              legend: { display: false },
              tooltip: {
                displayColors: false,
                callbacks: { label: c => `${c.label}: ${c.parsed.y.toFixed(0)}% 可用率` }
              }
            },
            scales: { x: { display: false }, y: { display: false, min: 0, max: 100 } }
          }
        });
      });
    });
  }

  function renderBusinessVerdict() {
    const verdictRows = readJson('obs-business-verdict-data', []);
    const canvas = document.getElementById('verdictPieChart');
    if (!canvas || !verdictRows.length) return;
    const verdictLabelMap = {
      effective: '有效',
      success: '成功',
      positive: '正向',
      backfired: '反效果',
      negative: '負向',
      failed: '失敗',
      neutral: '中性',
      pending: '待回收',
      inconclusive: '尚未定論',
      no_data: '無資料'
    };
    mountChart('#verdictPieChart', () => {
      new Chart(canvas, {
        type: 'doughnut',
        data: {
          labels: verdictRows.map(row => verdictLabelMap[row.verdict] || row.verdict || '未分類'),
          datasets: [{
            data: verdictRows.map(row => row.count || 0),
            backgroundColor: ['#2f8f6b', '#c96442', '#f1b45a', '#6d4b3f', '#d9a06f'],
            borderColor: '#fff8ee',
            borderWidth: 4,
            hoverOffset: 10
          }]
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          plugins: {
            legend: {
              position: 'bottom',
              labels: { usePointStyle: true, boxWidth: 8, color: '#6f564b', font: { weight: 700 } }
            }
          },
          cutout: '66%'
        }
      });
    });
  }

  function renderAiCalls() {
    const data = readJson('obs-ai-calls-data', {});
    const labels = data.labels || [];
    const calls = data.calls || [];
    const costs = data.costs || [];
    const errors = data.errors || [];
    if (!labels.length) return;
    mountChart('#hourlyTrendChart, canvas[data-spark]', () => {
      const sparkColors = { calls: '#c96442', cost: '#b8792f', errors: '#b94b45' };
      const sparkData = { calls, cost: costs, errors };
      document.querySelectorAll('canvas[data-spark]').forEach(el => {
        if (el.dataset.chartReady === '1') return;
        const key = el.getAttribute('data-spark');
        const series = sparkData[key];
        if (!series || !series.length) return;
        el.dataset.chartReady = '1';
        new Chart(el, {
          type: 'line',
          data: {
            labels,
            datasets: [{
              data: series,
              borderColor: sparkColors[key],
              backgroundColor: `${sparkColors[key]}24`,
              borderWidth: 1.4,
              fill: true,
              tension: 0.42,
              pointRadius: 0
            }]
          },
          options: {
            responsive: true,
            maintainAspectRatio: false,
            plugins: { legend: { display: false }, tooltip: { enabled: false } },
            scales: { x: { display: false }, y: { display: false, beginAtZero: true } }
          }
        });
      });

      const el = document.getElementById('hourlyTrendChart');
      if (!el || el.dataset.chartReady === '1') return;
      el.dataset.chartReady = '1';
      new Chart(el, {
        data: {
          labels,
          datasets: [
            { type: 'line', label: '呼叫數', data: calls, borderColor: '#c96442', backgroundColor: 'rgba(201,100,66,.12)', tension: 0.35, fill: true, yAxisID: 'y' },
            { type: 'line', label: '錯誤', data: errors, borderColor: '#b94b45', backgroundColor: 'rgba(185,75,69,.1)', tension: 0.35, yAxisID: 'y' },
            { type: 'bar', label: '成本 USD', data: costs, backgroundColor: 'rgba(184,121,47,.38)', borderColor: '#b8792f', yAxisID: 'y1' }
          ]
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          interaction: { mode: 'index', intersect: false },
          scales: {
            y: { beginAtZero: true, title: { display: true, text: '次數' } },
            y1: { position: 'right', beginAtZero: true, grid: { drawOnChartArea: false }, title: { display: true, text: 'USD' } }
          }
        }
      });
    });
  }

  function renderBudgetCharts() {
    const data = readJson('obs-budget-data', {});
    const providerCostMonth = data.providerCostMonth || [];
    const costTrend30d = data.costTrend30d || [];
    if (providerCostMonth.length) {
      mountChart('#providerCostPieChart', () => {
        const el = document.getElementById('providerCostPieChart');
        if (!el || el.dataset.chartReady === '1') return;
        el.dataset.chartReady = '1';
        const colors = {
          gcp_ollama: '#4f8a5b',
          ollama_secondary: '#7aaa82',
          ollama_111: '#a3cfa8',
          gemini: '#b8792f',
          claude: '#4f6f8f',
          nim: '#6aa6a6',
          openrouter: '#8b8077',
          nim_via_elephant: '#c96442'
        };
        new Chart(el, {
          type: 'doughnut',
          data: {
            labels: providerCostMonth.map(d => providerLabelMap[d.provider] || d.provider),
            datasets: [{
              data: providerCostMonth.map(d => d.cost),
              backgroundColor: providerCostMonth.map((d, i) => colors[d.provider] || `hsl(${(i * 47) % 360},55%,55%)`),
              borderWidth: 1,
              borderColor: '#fff'
            }]
          },
          options: {
            responsive: true,
            maintainAspectRatio: false,
            plugins: { legend: { position: 'bottom', labels: { font: { size: 11 } } } }
          }
        });
      });
    }
    if (costTrend30d.length) {
      mountChart('#costTrend30dChart', () => {
        const el = document.getElementById('costTrend30dChart');
        if (!el || el.dataset.chartReady === '1') return;
        el.dataset.chartReady = '1';
        const dateSet = [...new Set(costTrend30d.map(row => row.date))].sort();
        const providerSet = [...new Set(costTrend30d.map(row => row.provider))];
        const palette = ['#c96442', '#b8792f', '#4f8a5b', '#4f6f8f', '#6aa6a6', '#8b8077', '#a66a4a'];
        const datasets = providerSet.map((provider, index) => ({
          label: providerLabelMap[provider] || provider,
          data: dateSet.map(date => {
            const row = costTrend30d.find(item => item.date === date && item.provider === provider);
            return row ? row.cost : 0;
          }),
          backgroundColor: palette[index % palette.length]
        }));
        new Chart(el, {
          type: 'bar',
          data: { labels: dateSet, datasets },
          options: {
            responsive: true,
            maintainAspectRatio: false,
            interaction: { mode: 'index', intersect: false },
            scales: { x: { stacked: true }, y: { stacked: true, beginAtZero: true, title: { display: true, text: 'USD' } } }
          }
        });
      });
    }
  }

  function renderHostHealth() {
    const data = readJson('obs-host-health-data', {});
    const healSparkline = data.healSparkline || [];
    if (!healSparkline.length) return;
    mountChart('#healSparkline', () => {
      const el = document.getElementById('healSparkline');
      if (!el || el.dataset.chartReady === '1') return;
      el.dataset.chartReady = '1';
      new Chart(el, {
        type: 'line',
        data: {
          labels: healSparkline.map(d => d.date),
          datasets: [{
            data: healSparkline.map(d => d.rate),
            borderColor: '#c96442',
            backgroundColor: 'rgba(201,100,66,.14)',
            borderWidth: 2,
            fill: true,
            tension: 0.35,
            pointRadius: 2
          }]
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          plugins: { legend: { display: false } },
          scales: { x: { display: false }, y: { min: 0, max: 100, ticks: { callback: v => `${v}%` } } }
        }
      });
    });
  }

  function renderPromotionReview() {
    const dist = readJson('obs-promotion-review-data', {});
    const keys = Object.keys(dist || {});
    if (!keys.length) return;
    mountChart('#episodeDistChart', () => {
      const el = document.getElementById('episodeDistChart');
      if (!el || el.dataset.chartReady === '1') return;
      el.dataset.chartReady = '1';
      const colorMap = {
        pending: '#8b8077',
        awaiting_review: '#b8792f',
        approved: '#4f8a5b',
        rejected_quality: '#b94b45',
        rejected_hallucination: '#9f3330',
        rejected_duplicate: '#c96442',
        rejected_human: '#8f2925',
        expired: '#b8aea5'
      };
      const labelMap = {
        pending: '待處理',
        awaiting_review: '待審核',
        approved: '已晉升',
        rejected_quality: '品質拒',
        rejected_hallucination: '幻覺拒',
        rejected_duplicate: '重複拒',
        rejected_human: '人工拒',
        expired: '已過期'
      };
      new Chart(el, {
        type: 'doughnut',
        data: {
          labels: keys.map(k => labelMap[k] || k),
          datasets: [{ data: keys.map(k => dist[k]), backgroundColor: keys.map(k => colorMap[k] || '#999'), borderWidth: 1, borderColor: '#fff' }]
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          plugins: { legend: { position: 'bottom', labels: { font: { size: 11 } } } }
        }
      });
    });
  }

  function renderQualityTrend() {
    const rows = readJson('obs-quality-trend-data', []);
    if (!rows.length) return;
    mountChart('#ragFeedbackPieChart', () => {
      const el = document.getElementById('ragFeedbackPieChart');
      if (!el || el.dataset.chartReady === '1') return;
      el.dataset.chartReady = '1';
      const colorMap = { 1: '#b94b45', 2: '#c96442', 3: '#b8792f', 4: '#7aaa82', 5: '#4f8a5b' };
      new Chart(el, {
        type: 'doughnut',
        data: {
          labels: rows.map(row => `${row.score} 星`),
          datasets: [{ data: rows.map(row => row.count), backgroundColor: rows.map(row => colorMap[row.score] || '#8b8077'), borderWidth: 1, borderColor: '#fff' }]
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          plugins: { legend: { position: 'bottom', labels: { font: { size: 12 } } } }
        }
      });
    });
  }

  function renderPptAudit() {
    const stats = readJson('obs-ppt-audit-data', {});
    if (!stats.total) return;
    mountChart('#pptAuditPieChart', () => {
      const el = document.getElementById('pptAuditPieChart');
      if (!el || el.dataset.chartReady === '1') return;
      el.dataset.chartReady = '1';
      const data = [
        { label: '通過', value: stats.passed || 0, color: '#4f8a5b' },
        { label: '失敗', value: stats.failed || 0, color: '#b8792f' },
        { label: '錯誤', value: stats.error || 0, color: '#b94b45' },
        { label: '跳過', value: stats.skipped || 0, color: '#8b8077' }
      ].filter(item => item.value > 0);
      if (!data.length) return;
      new Chart(el, {
        type: 'doughnut',
        data: {
          labels: data.map(item => item.label),
          datasets: [{ data: data.map(item => item.value), backgroundColor: data.map(item => item.color), borderWidth: 1, borderColor: '#fff' }]
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          plugins: { legend: { position: 'bottom', labels: { font: { size: 12 } } } }
        }
      });
    });
  }

  async function postJson(url, options) {
    const fetcher = window.fetchWithCSRF || window.fetch.bind(window);
    return fetcher(url, options || {});
  }

  window.triggerCodeReview = async function triggerCodeReview() {
    if (!confirm('啟動部署檢查流程？\n\n會在背景檢查最新更新。')) return;
    try {
      const response = await postJson('/observability/ai_calls/trigger_code_review', { method: 'POST' });
      const data = await response.json();
      if (data.ok) {
        alert('部署檢查已排入背景處理，完成後可回到觀測台查看結果。');
      } else {
        alert(`❌ ${data.error || '觸發失敗'}`);
      }
    } catch (error) {
      console.warn('code_review_trigger_failed', error);
      alert('操作暫時無法完成，請稍後再試或查看處理狀態。');
    }
  };

  window.forceThrottle = async function forceThrottle() {
    if (!confirm('立即重算所有供應商的節流狀態？')) return;
    try {
      const response = await postJson('/observability/budget/force_throttle', { method: 'POST' });
      const data = await response.json();
      if (data.ok) {
        const providers = data.throttled_providers && data.throttled_providers.length > 0
          ? data.throttled_providers.join(', ')
          : '（無）';
        alert(`✅ 已重算：被節流的供應商 = ${providers}`);
        window.location.reload();
      } else {
        alert(`❌ ${data.error || '重算失敗'}`);
      }
    } catch (error) {
      console.warn('budget_force_throttle_failed', error);
      alert('操作暫時無法完成，請稍後再試或查看處理狀態。');
    }
  };

  window.saveBudget = async function saveBudget(id) {
    const budgetInput = document.querySelector(`.budget-input[data-budget-id="${id}"]`);
    const alertInput = document.querySelector(`.alert-input[data-budget-id="${id}"]`);
    const button = document.querySelector(`.save-budget-btn[data-budget-id="${id}"]`);
    if (!budgetInput || !alertInput || !button) return;
    button.disabled = true;
    button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
    try {
      const response = await postJson(`/observability/budget/update/${id}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          budget_usd: parseFloat(budgetInput.value),
          alert_pct: parseInt(alertInput.value, 10)
        })
      });
      const data = await response.json();
      if (data.ok) {
        button.innerHTML = '<i class="fas fa-check"></i> 已儲存';
        setTimeout(() => {
          button.innerHTML = '<i class="fas fa-save me-1"></i>儲存';
          button.disabled = false;
        }, 1500);
      } else {
        alert(`更新失敗：${data.error || '請稍後再試'}`);
        button.disabled = false;
        button.innerHTML = '<i class="fas fa-save me-1"></i>儲存';
      }
    } catch (error) {
      console.warn('budget_save_failed', error);
      alert('操作暫時無法完成，請稍後再試或查看處理狀態。');
      button.disabled = false;
      button.innerHTML = '<i class="fas fa-save me-1"></i>儲存';
    }
  };

  window.togglePlaybook = async function togglePlaybook(id, name) {
    if (!confirm(`切換 Playbook 「${name}」狀態？`)) return;
    try {
      const response = await postJson(`/observability/playbooks/toggle/${id}`, { method: 'POST' });
      const data = await response.json();
      if (data.ok) {
        alert(`✅ ${data.message}`);
        window.location.reload();
      } else {
        alert(`❌ ${data.error || '切換失敗'}`);
      }
    } catch (error) {
      console.warn('playbook_toggle_failed', error);
      alert('操作暫時無法完成，請稍後再試或查看處理狀態。');
    }
  };

  window.triggerAutoHeal = async function triggerAutoHeal(hostLabel) {
    if (!confirm(`觸發 AutoHeal？\n\n主機：${hostLabel}`)) return;
    try {
      const response = await postJson('/observability/host_health/trigger_autoheal', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ host_label: hostLabel })
      });
      const data = await response.json();
      if (data.ok) {
        alert(`✅ AutoHeal 已派出\n動作：${data.action || '—'}\n訊息：${data.message || ''}`);
        window.location.reload();
      } else {
        alert(`❌ ${data.error || data.message || '觸發失敗'}`);
      }
    } catch (error) {
      console.warn('host_autoheal_failed', error);
      alert('操作暫時無法完成，請稍後再試或查看處理狀態。');
    }
  };

  window.approveEpisode = async function approveEpisode(id, button) {
    button.disabled = true;
    button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 處理中...';
    try {
      const response = await postJson(`/observability/promotion_review/approve/${id}`, { method: 'POST' });
      const data = await response.json();
      if (data.ok) {
        const card = document.querySelector(`.episode-card[data-episode-id="${id}"]`);
        const footer = card ? card.querySelector('.card-footer') : null;
        if (card && footer) {
          card.classList.add('border-success');
          footer.innerHTML = `<span class="text-success"><i class="fas fa-check me-1"></i>已晉升 → ai_insights #${data.insight_id}（審核者：${data.approver}）</span>`;
        } else {
          window.location.reload();
        }
      } else {
        alert(`晉升失敗：${data.error || '請稍後再試'}`);
        button.disabled = false;
        button.innerHTML = '<i class="fas fa-check me-1"></i>通過晉升';
      }
    } catch (error) {
      console.warn('promotion_approve_failed', error);
      alert('操作暫時無法完成，請稍後再試或查看處理狀態。');
      button.disabled = false;
      button.innerHTML = '<i class="fas fa-check me-1"></i>通過晉升';
    }
  };

  window.rejectEpisode = async function rejectEpisode(id, button) {
    if (!confirm(`拒絕學習片段 #${id}？此筆將永不晉升。`)) return;
    button.disabled = true;
    button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 處理中...';
    try {
      const response = await postJson(`/observability/promotion_review/reject/${id}`, { method: 'POST' });
      const data = await response.json();
      if (data.ok) {
        const card = document.querySelector(`.episode-card[data-episode-id="${id}"]`);
        const footer = card ? card.querySelector('.card-footer') : null;
        if (card && footer) {
          card.classList.add('border-danger');
          footer.innerHTML = '<span class="text-danger"><i class="fas fa-times me-1"></i>已拒絕（人工拒絕）</span>';
        } else {
          window.location.reload();
        }
      } else {
        alert(`拒絕失敗：${data.error || '請稍後再試'}`);
        button.disabled = false;
        button.innerHTML = '<i class="fas fa-times me-1"></i>拒絕';
      }
    } catch (error) {
      console.warn('promotion_reject_failed', error);
      alert('操作暫時無法完成，請稍後再試或查看處理狀態。');
      button.disabled = false;
      button.innerHTML = '<i class="fas fa-times me-1"></i>拒絕';
    }
  };

  window.triggerAiderHeal = async function triggerAiderHeal(pptxFilename, errorMsg, triggerButton) {
    if (!confirm(`觸發修復流程？\n\n檔案：${pptxFilename}\n問題：${(errorMsg || '').substring(0, 200)}`)) return;
    const statusNode = document.querySelector('[data-ppt-auto-status]');
    const originalHtml = triggerButton ? triggerButton.innerHTML : '';
    if (triggerButton) {
      triggerButton.disabled = true;
      triggerButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>派工中';
    }
    if (statusNode) {
      statusNode.classList.add('is-working');
      statusNode.textContent = `${pptxFilename || 'PPT'} 正在排入背景修復流程。`;
    }
    try {
      const response = await postJson('/observability/ppt_audit/trigger_aider_heal', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ pptx_filename: pptxFilename, issue_summary: errorMsg || '' })
      });
      const data = await response.json();
      if (!response.ok || !data.ok) {
        throw new Error(data.error || data.message || '觸發失敗');
      }
      if (triggerButton) {
        triggerButton.innerHTML = data.status === 'already_running'
          ? '<i class="fas fa-clock me-1"></i>執行中'
          : '<i class="fas fa-check me-1"></i>已排入';
      }
      if (statusNode) {
        statusNode.classList.remove('is-working');
        statusNode.textContent = data.message || '修復流程已排入背景執行，完成後會回報處理結果。';
      }
      if (window.refreshPptAiderHealStatus) {
        window.refreshPptAiderHealStatus();
      }
    } catch (error) {
      console.warn('ppt_audit_trigger_aider_heal_failed', error);
      if (triggerButton) {
        triggerButton.disabled = false;
        triggerButton.innerHTML = originalHtml;
      }
      if (statusNode) {
        statusNode.classList.remove('is-working');
        statusNode.textContent = '修復派工失敗，請稍後再試或查看處理狀態。';
      }
    }
  };

  function renderPptAiderHealStatus(payload) {
    const panel = document.querySelector('[data-ppt-aider-status]');
    if (!panel) return;
    const jobs = Array.isArray(payload && payload.jobs) ? payload.jobs : [];
    const count = Number((payload && payload.active_count) || jobs.length || 0);
    panel.classList.toggle('is-empty', count <= 0);
    const title = panel.querySelector('[data-ppt-aider-status-title]');
    const meta = panel.querySelector('[data-ppt-aider-status-meta]');
    const list = panel.querySelector('[data-ppt-aider-job-list]');
    if (title) title.textContent = count > 0 ? `修復流程執行中 · ${count}` : '修復流程待命';
    if (meta) {
      meta.textContent = count > 0
        ? '修復完成後會回報處理結果。'
        : '有問題的審核紀錄可直接一鍵派工。';
    }
    if (list) {
      list.innerHTML = jobs.slice(0, 3).map(job => `
        <div class="ppt-aider-job">
          <code>${escapeHtml(job.pptx_filename || 'manual')}</code>
          <span>${escapeHtml(job.queued_at || '')}</span>
          <small>${escapeHtml(job.diagnosis || '')}</small>
        </div>
      `).join('');
    }
  }

  window.refreshPptAiderHealStatus = async function refreshPptAiderHealStatus() {
    try {
      const response = await fetch('/observability/ppt_audit/aider_heal_status', {
        headers: { 'Accept': 'application/json' }
      });
      const data = await response.json();
      if (!response.ok || !data.ok) return;
      renderPptAiderHealStatus(data);
    } catch (error) {
      console.warn('ppt_audit_aider_heal_status_failed', error);
    }
  };

  function renderPptVisionStatus(payload) {
    const panel = document.querySelector('[data-ppt-vision-status]');
    if (!panel) return;
    const status = (payload && payload.status) || 'unknown';
    panel.className = panel.className.replace(/\bis-[a-z_]+\b/g, '').trim();
    panel.classList.add(`is-${status}`);
    const title = panel.querySelector('[data-ppt-vision-status-title]');
    const meta = panel.querySelector('[data-ppt-vision-status-meta]');
    const list = panel.querySelector('[data-ppt-vision-status-list]');
    if (title) title.textContent = (payload && payload.status_label) || '狀態未知';
    if (meta) meta.textContent = (payload && payload.message) || '最近視覺 QA 狀態無法讀取。';
    if (!list) return;
    const lastRun = payload && payload.last_run;
    if (!lastRun) {
      list.innerHTML = `
        <div class="ppt-vision-job">
          <span>尚無紀錄</span>
          <strong>待命</strong>
          <small>按下「立即視覺 QA」後會在這裡顯示背景任務狀態。</small>
        </div>
      `;
      return;
    }
    const summary = lastRun.summary || {};
    const timestamp = lastRun.finished_at || lastRun.started_at || lastRun.queued_at || '';
    const issueText = `${Number(summary.audited_count || 0)} 份 / ${Number(summary.total_issues || 0)} 問題`;
    const errorText = Number(summary.error_count || 0) > 0
      ? `錯誤 ${Number(summary.error_count || 0)}`
      : '無執行錯誤';
    list.innerHTML = `
      <div class="ppt-vision-job">
        <span>${escapeHtml(timestamp)}</span>
        <strong>${escapeHtml(issueText)}</strong>
        <small>${escapeHtml(errorText)}</small>
      </div>
    `;
  }

  window.refreshPptVisionStatus = async function refreshPptVisionStatus() {
    try {
      const response = await fetch('/observability/ppt_audit/vision_status', {
        headers: { 'Accept': 'application/json' }
      });
      const data = await response.json();
      if (!response.ok || !data.ok) return;
      renderPptVisionStatus(data);
    } catch (error) {
      console.warn('ppt_vision_status_failed', error);
    }
  };

  function initPptAutoGeneration() {
    const panel = document.querySelector('[data-ppt-auto-generation]');
    const pageStatus = document.querySelector('[data-ppt-auto-status]');
    const previewModal = document.querySelector('[data-ppt-preview-modal]');
    const previewFrame = previewModal ? previewModal.querySelector('[data-ppt-preview-frame]') : null;
    const previewTitle = previewModal ? previewModal.querySelector('[data-ppt-preview-modal-title]') : null;
    const previewFilename = previewModal ? previewModal.querySelector('[data-ppt-preview-filename]') : null;
    const previewOpenPage = previewModal ? previewModal.querySelector('[data-ppt-preview-open-page]') : null;
    const previewDownload = previewModal ? previewModal.querySelector('[data-ppt-preview-download]') : null;
    const previewLoading = previewModal ? previewModal.querySelector('[data-ppt-preview-loading]') : null;
    const previewFrameWrap = previewModal ? previewModal.querySelector('.ppt-preview-frame-wrap') : null;
    const visionAuditFilenames = readJson('obs-ppt-audit-filenames', []);
    if (document.querySelector('[data-ppt-vision-status]') && window.refreshPptVisionStatus) {
      window.refreshPptVisionStatus();
    }

    function closePreviewModal() {
      if (!previewModal) return;
      previewModal.hidden = true;
      previewModal.setAttribute('aria-hidden', 'true');
      document.body.classList.remove('ppt-preview-open');
      if (previewFrame) {
        previewFrame.removeAttribute('src');
      }
    }

    function openPreviewModal(trigger) {
      if (!previewModal || !previewFrame) return false;
      const pdfUrl = trigger.dataset.pptPreviewPdf || '';
      if (!pdfUrl) return false;
      const title = trigger.dataset.pptPreviewTitle || '簡報線上預覽';
      const filename = trigger.dataset.pptFilename || '';
      if (previewTitle) previewTitle.textContent = title;
      if (previewFilename) previewFilename.textContent = filename;
      if (previewOpenPage) previewOpenPage.href = trigger.dataset.pptPreviewPage || trigger.href || pdfUrl;
      if (previewDownload) previewDownload.href = trigger.dataset.pptDownloadUrl || '#';
      if (previewLoading) previewLoading.textContent = filename ? `${filename} 正在載入 PDF 預覽` : '正在載入 PDF 預覽';
      previewModal.hidden = false;
      previewModal.setAttribute('aria-hidden', 'false');
      document.body.classList.add('ppt-preview-open');
      if (previewFrameWrap) previewFrameWrap.classList.remove('is-loaded');
      previewFrame.src = pdfUrl;
      return true;
    }

    if (previewModal) {
      if (previewFrame && previewFrame.dataset.boundLoad !== '1') {
        previewFrame.dataset.boundLoad = '1';
        previewFrame.addEventListener('load', () => {
          if (previewFrameWrap) previewFrameWrap.classList.add('is-loaded');
        });
      }
      previewModal.querySelectorAll('[data-ppt-preview-close]').forEach(button => {
        if (button.dataset.bound === '1') return;
        button.dataset.bound = '1';
        button.addEventListener('click', closePreviewModal);
      });
      document.addEventListener('keydown', event => {
        if (event.key === 'Escape' && !previewModal.hidden) closePreviewModal();
      });
    }

    document.querySelectorAll('[data-ppt-open-preview]').forEach(link => {
      if (link.dataset.bound === '1') return;
      link.dataset.bound = '1';
      link.addEventListener('click', event => {
        if (openPreviewModal(link)) {
          event.preventDefault();
        }
      });
    });

    document.querySelectorAll('[data-ppt-aider-heal]').forEach(button => {
      if (button.dataset.bound === '1') return;
      button.dataset.bound = '1';
      button.addEventListener('click', () => {
        window.triggerAiderHeal(button.dataset.pptFilename || '', button.dataset.pptError || '', button);
      });
    });

    async function triggerVisionAudit(button) {
      const filenames = Array.isArray(visionAuditFilenames) ? visionAuditFilenames.filter(Boolean) : [];
      if (!filenames.length) {
        if (pageStatus) pageStatus.textContent = '目前沒有可送進視覺 QA 的 PPTX 檔案。';
        return;
      }
      const buttons = Array.from(document.querySelectorAll('[data-ppt-run-vision]'));
      const originalHtml = button ? button.innerHTML : '';
      buttons.forEach(item => {
        item.disabled = true;
        item.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>QA 排入中';
      });
      if (pageStatus) {
        pageStatus.classList.add('is-working');
        pageStatus.textContent = `已準備送出 ${filenames.length} 份簡報進行視覺 QA，完成後會更新審核紀錄。`;
      }
      try {
        const response = await postJson('/observability/ppt_audit/run_vision', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ filenames, max_files: filenames.length })
        });
        const data = await response.json();
        if (!response.ok || !data.ok) {
          throw new Error(data.error || data.message || '視覺 QA 送出失敗');
        }
        buttons.forEach(item => {
          item.innerHTML = data.status === 'already_running'
            ? '<i class="fas fa-clock me-1"></i>QA 執行中'
            : '<i class="fas fa-check me-1"></i>QA 已排入';
        });
        if (pageStatus) {
          pageStatus.textContent = data.status === 'already_running'
            ? '視覺 QA 已在執行中，請稍後重新整理查看審核結果。'
            : `視覺 QA 已排入 ${filenames.length} 份簡報；完成後會更新審核結果。`;
        }
        if (window.refreshPptVisionStatus) {
          window.refreshPptVisionStatus();
        }
      } catch (error) {
        console.warn('ppt_vision_audit_queue_failed', error);
        buttons.forEach(item => {
          item.disabled = false;
          item.innerHTML = item === button && originalHtml ? originalHtml : '<i class="fas fa-eye me-1"></i>立即視覺 QA';
        });
        if (pageStatus) {
          pageStatus.classList.remove('is-working');
          pageStatus.textContent = '視覺 QA 送出失敗，請稍後再試或查看處理狀態。';
        }
      }
    }

    document.querySelectorAll('[data-ppt-run-vision]').forEach(button => {
      if (button.dataset.bound === '1') return;
      button.dataset.bound = '1';
      button.addEventListener('click', () => triggerVisionAudit(button));
    });

    function markPreviewCacheReady(filename) {
      document.querySelectorAll('[data-ppt-preview-state]').forEach(node => {
        if (node.dataset.pptPreviewState !== filename) return;
        node.classList.remove('status-warn');
        node.classList.add('status-blue');
        node.textContent = 'PDF 預覽快取已建立';
      });
      document.querySelectorAll('[data-ppt-prewarm-preview]').forEach(node => {
        if (node.dataset.pptFilename !== filename) return;
        node.innerHTML = '<i class="fas fa-check me-1"></i>已快取';
        node.disabled = true;
      });
    }

    async function prewarmPreview(button, options = {}) {
      const filename = button.dataset.pptFilename || '';
      if (!filename) return false;
      const originalHtml = button.innerHTML;
      button.disabled = true;
      button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>預熱中';
      if (!options.quiet && pageStatus) {
        pageStatus.classList.add('is-working');
        pageStatus.textContent = `${filename} 正在建立 PDF 預覽快取。`;
      }
      try {
        const response = await postJson(`/observability/ppt_audit_file/${encodeURIComponent(filename)}/prewarm`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' }
        });
        const data = await response.json();
        if (!response.ok || !data.ok) {
          throw new Error(data.error || '預熱失敗');
        }
        markPreviewCacheReady(filename);
        if (!options.quiet && pageStatus) {
          pageStatus.classList.remove('is-working');
          pageStatus.textContent = data.message || `${filename} 的 PDF 預覽快取已建立。`;
        }
        return true;
      } catch (error) {
        console.warn('ppt_preview_prewarm_failed', error);
        button.disabled = false;
        button.innerHTML = originalHtml;
        if (!options.quiet && pageStatus) {
          pageStatus.classList.remove('is-working');
          pageStatus.textContent = 'PDF 預覽預熱失敗，請稍後再試或直接開啟線上預覽。';
        }
        return false;
      }
    }

    document.querySelectorAll('[data-ppt-prewarm-preview]').forEach(button => {
      if (button.dataset.bound === '1') return;
      button.dataset.bound = '1';
      button.addEventListener('click', () => prewarmPreview(button));
    });
    document.querySelectorAll('[data-ppt-prewarm-all]').forEach(button => {
      if (button.dataset.bound === '1') return;
      button.dataset.bound = '1';
      button.addEventListener('click', async () => {
        const targets = [];
        const seen = new Set();
        document.querySelectorAll('[data-ppt-prewarm-preview]').forEach(item => {
          const filename = item.dataset.pptFilename || '';
          if (!filename || item.disabled || seen.has(filename)) return;
          seen.add(filename);
          targets.push(item);
        });
        if (!targets.length) {
          button.disabled = true;
          button.innerHTML = '<i class="fas fa-check me-1"></i>本頁已快取';
          if (pageStatus) {
            pageStatus.textContent = '本頁可預覽簡報的 PDF 快取都已建立。';
          }
          return;
        }
        const originalHtml = button.innerHTML;
        const countBadge = button.querySelector('[data-ppt-prewarm-count]');
        button.disabled = true;
        let successCount = 0;
        let failedCount = 0;
        for (let index = 0; index < targets.length; index += 1) {
          button.innerHTML = `<i class="fas fa-spinner fa-spin me-1"></i>預熱 ${index + 1}/${targets.length}`;
          if (pageStatus) {
            pageStatus.classList.add('is-working');
            pageStatus.textContent = `正在預熱本頁 PDF 快取：${index + 1}/${targets.length}`;
          }
          const ok = await prewarmPreview(targets[index], { quiet: true });
          if (ok) successCount += 1;
          else failedCount += 1;
          if (countBadge) {
            countBadge.textContent = String(Math.max(targets.length - successCount, 0));
          }
        }
        if (pageStatus) {
          pageStatus.classList.remove('is-working');
          pageStatus.textContent = failedCount
            ? `PDF 預熱完成 ${successCount}/${targets.length}，仍有 ${failedCount} 份失敗。`
            : `本頁 ${successCount} 份 PDF 預覽快取已建立。`;
        }
        if (failedCount) {
          button.disabled = false;
          button.innerHTML = originalHtml;
        } else {
          button.innerHTML = '<i class="fas fa-check me-1"></i>本頁已快取';
        }
      });
    });

    if (!panel) return;

    const button = panel.querySelector('[data-ppt-generate-missing]');
    const status = panel.querySelector('[data-ppt-auto-status]');
    const reportTypes = (panel.dataset.reportTypes || '')
      .split(',')
      .map(value => value.trim())
      .filter(Boolean);

    async function triggerGeneration(isAuto, selectedReportTypes, triggerButton, labelText, force = false) {
      const targetButton = triggerButton || button;
      const targetReportTypes = selectedReportTypes || reportTypes;
      const originalHtml = targetButton ? targetButton.innerHTML : '';
      if (targetButton) {
        targetButton.disabled = true;
        targetButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>排入中';
      }
      if (status) {
        status.classList.add('is-working');
        status.textContent = isAuto
          ? '偵測到本月定義簡報缺漏，已排入背景補齊。'
          : `${labelText || '簡報補齊'} 已排入背景產線，完成後重新整理即可看到最新狀態。`;
      }

      try {
        const response = await postJson('/observability/ppt_audit/generate_missing', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ report_types: targetReportTypes, force: Boolean(force) })
        });
        const data = await response.json();
        if (status) {
          status.textContent = data.message || (data.status === 'queued'
            ? '已排入背景補齊，請稍後重新整理。'
            : `狀態：${data.status || '已送出'}`);
        }
        if (targetButton) {
          targetButton.innerHTML = '<i class="fas fa-check me-1"></i>已排入';
        }
      } catch (error) {
        console.warn('ppt_auto_generation_failed', error);
        if (status) {
          status.classList.remove('is-working');
          status.textContent = '補齊任務送出失敗，請稍後再試或查看處理狀態。';
        }
        if (targetButton) {
          targetButton.disabled = false;
          targetButton.innerHTML = originalHtml;
        }
      }
    }

    if (button) {
      button.addEventListener('click', () => triggerGeneration(false));
    }
    document.querySelectorAll('[data-ppt-generate-one]').forEach(singleButton => {
      if (singleButton.dataset.bound === '1') return;
      singleButton.dataset.bound = '1';
      singleButton.addEventListener('click', () => {
        const reportType = singleButton.dataset.reportType || '';
        if (!reportType) return;
        const label = singleButton.dataset.reportLabel || reportType;
        triggerGeneration(false, [reportType], singleButton, label, true);
      });
    });

    if (panel.dataset.autoStart === 'true') {
      const key = `ppt-auto-generation:${new Date().toISOString().slice(0, 10)}`;
      let last = 0;
      const now = Date.now();
      try {
        last = Number(window.localStorage.getItem(key) || 0);
      } catch (_error) {
        last = 0;
      }
      if (reportTypes.length && (!last || now - last > 6 * 60 * 60 * 1000)) {
        try {
          window.localStorage.setItem(key, String(now));
        } catch (_error) {
          // Ignore storage failures; the server-side generation lock still protects the job.
        }
        triggerGeneration(true);
      }
    }
  }

  function escapeHtml(value) {
    if (!value) return '';
    return String(value).replace(/[&<>"']/g, char => ({
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#39;'
    })[char]);
  }

  function insightLabel(value) {
    const insightLabelMap = {
      product_pick: '選品攻擊',
      price_recommendation: '價格建議',
      competitor_price: '競品價格',
      sales_anomaly: '業績異常',
      budget_strategy: '預算策略',
      rag_feedback: '知識反饋',
      ppt_audit: 'PPT 審核',
      quality_issue: '品質問題',
      promotion: '活動促銷',
      market_signal: '市場訊號',
      strategy: '策略洞察'
    };
    return insightLabelMap[value] || String(value || '未分類洞察').replaceAll('_', ' ');
  }

  window.showHits = async function showHits(queryId) {
    const modalEl = document.getElementById('hitsModal');
    const body = document.getElementById('hitsModalBody');
    if (!modalEl || !body) return;
    body.innerHTML = '<div class="text-center"><i class="fas fa-spinner fa-spin"></i> 載入中...</div>';
    const modal = new bootstrap.Modal(modalEl);
    modal.show();
    try {
      const response = await fetch(`/observability/rag_queries/${queryId}/hits`);
      const data = await response.json();
      if (!data.ok) {
        body.innerHTML = `<div class="alert alert-danger">❌ ${escapeHtml(data.error || '載入失敗')}</div>`;
        return;
      }

      let html = `<div class="mb-3"><small class="text-muted">查詢 #${data.query_id} · 門檻 ${data.threshold} · 命中 ${data.hit_count}</small><div class="p-2 mt-1 obs-modal-preview"><small><strong>查詢內容：</strong></small><br><code>${escapeHtml(data.query_text || '')}</code></div></div>`;
      if (!data.hits || data.hits.length === 0) {
        html += '<div class="alert alert-warning">無命中詳細資料</div>';
      } else {
        html += '<h6 class="mb-2">主要命中內容預覽：</h6>';
        data.hits.forEach(hit => {
          html += `<div class="mb-2 p-2 obs-modal-preview"><div class="mb-1"><span class="badge bg-light text-dark me-1">#${hit.id}</span><span class="badge bg-info me-1">${escapeHtml(insightLabel(hit.insight_type))}</span>${hit.period ? `<span class="badge bg-secondary me-1">${escapeHtml(hit.period)}</span>` : ''}${hit.product_sku ? `<small class="text-muted me-1">SKU: ${escapeHtml(hit.product_sku)}</small>` : ''}<small class="text-muted">${escapeHtml(hit.created_at || '')}</small></div><small>${escapeHtml(hit.content || '')}${hit.content && hit.content.length >= 300 ? '…' : ''}</small></div>`;
        });
      }
      body.innerHTML = html;
    } catch (error) {
      console.warn('rag_query_hits_load_failed', error);
      body.innerHTML = '<div class="alert alert-danger">❌ 召回詳情暫時無法載入，請稍後再試或查看處理狀態。</div>';
    }
  };

  function boot() {
    renderOverviewHostSparklines();
    renderBusinessVerdict();
    renderAiCalls();
    renderBudgetCharts();
    renderHostHealth();
    renderPromotionReview();
    renderQualityTrend();
    renderPptAudit();
    initPptAutoGeneration();
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot, { once: true });
  } else {
    boot();
  }
})();
