window.XiaobaiPageModules.register("sentiment", ["sentimentCycleView"], {
  enter: ["loadSentiment"],
});

/* PRESERVATION-SOURCE-BEGIN app.js:1207-1516 */
async function loadSentimentHistory(force = false) {
  if (!state.dashboard || state.sentimentLoading) return;
  const key = `${elements.tradeDate.value}:${state.sentimentRange}`;
  if (!force && state.sentimentHistoryKey === key && state.sentimentHistory) {
    renderSentimentHistory();
    return;
  }
  state.sentimentLoading = true;
  const notice = document.querySelector("#sentimentHistoryNotice");
  notice.hidden = true;
  try {
    const query = new URLSearchParams({
      trade_date: elements.tradeDate.value,
      limit: String(state.sentimentRange),
    });
    state.sentimentHistory = await apiRequest(`/api/sentiment/history?${query}`);
    state.sentimentHistoryKey = key;
    renderSentimentHistory();
  } catch (error) {
    notice.textContent = error.message || "情绪周期数据加载失败";
    notice.hidden = false;
    showToast(notice.textContent);
  } finally {
    state.sentimentLoading = false;
  }
}

function renderSentimentHistory() {
  const payload = state.sentimentHistory;
  if (!payload) return;
  const rows = payload.rows || [];
  const latest = rows[rows.length - 1];
  const body = document.querySelector("#sentimentHistoryBody");
  const empty = document.querySelector("#sentimentHistoryEmpty");
  empty.hidden = rows.length > 0;
  body.innerHTML = [...rows].reverse().map((row) => {
    return `
      <tr class="${row.trade_date === latest?.trade_date ? "latest-row" : ""}">
        <td class="sentiment-date-cell">${escapeHtml(displayCompactDate(row.trade_date))}</td>
        <td class="number sentiment-score-cell ${sentimentScoreClass(row.score)}">${number(row.score)}</td>
        <td><span class="sentiment-phase-badge ${sentimentPhaseClass(row.phase)}">${escapeHtml(row.phase)}</span></td>
        <td><span class="sentiment-direction ${trendClass(row.direction)}">${escapeHtml(row.direction)}</span></td>
        <td class="number">${number(row.limit_up_count)}</td>
        <td class="number">${number(row.first_board_count)}</td>
        <td class="number">${number(row.second_board_count)}</td>
        <td class="number">${number(row.three_plus_count)}</td>
        <td class="number">${number(row.max_height)}板</td>
        <td class="number">${number(row.broken_count)}</td>
        <td class="number">${number(row.limit_down_count)}</td>
        <td class="number">${number(row.previous_limit_count)}</td>
        <td class="number">${number(row.previous_positive_count)}</td>
        <td class="number">${formatNumber(row.previous_positive_rate, 1)}%</td>
      </tr>
    `;
  }).join("");

  if (!latest) {
    setText("sentimentHistoryDateRange", "暂无历史数据");
    return;
  }
  setText(
    "sentimentHistoryDateRange",
    `${displayCompactDate(rows[0].trade_date)} 至 ${displayCompactDate(latest.trade_date)}`,
  );
  setText("sentimentCycleScore", number(latest.score));
  setText("sentimentCycleLabel", latest.label);
  setText("sentimentCycleDate", displayCompactDate(latest.trade_date));
  setText("sentimentCyclePhase", latest.phase);
  setText("sentimentCycleDirection", latest.direction);
  const dayChange = number(latest.day_change);
  const confidence = sentimentPhaseConfidence(latest);
  setText("sentimentPhaseConfidence", `置信度 ${confidence}%`);
  setText("sentimentDayChange", `${dayChange > 0 ? "+" : ""}${formatNumber(dayChange, 1)}`);
  setText("sentimentSealRate", `${formatNumber(latest.seal_rate, 1)}%`);
  setText("sentimentLimitUp", number(latest.limit_up_count));
  setText("sentimentBroken", number(latest.broken_count));
  setText("sentimentPhaseAdvice", sentimentPhaseAdvice(latest.phase));
  setText("sentimentCurrentTag", `当前 ${number(latest.score)} · ${latest.phase}`);
  setText("sentimentComponentSummary", `五维加权 → 温度 ${number(latest.score)}`);
  setText("sentimentPeriodNote", `近 ${state.sentimentRange} 个交易日，当前展示 ${rows.length} 日`);
  const changeElement = document.querySelector("#sentimentDayChange");
  changeElement.className = changeClass(dayChange);
  setText("sentimentPreviousPositive", `${number(latest.previous_positive_count)} / ${number(latest.previous_limit_count)} 只`);
  setText("sentimentPreviousAverage", `红盘率 ${formatNumber(latest.previous_positive_rate, 1)}% · 平均 ${signed(latest.average_previous_change)}%`);
  setText("sentimentHistoryDays", `${number(payload.available_days)} 个交易日`);
  setText("sentimentNormalization", `${latest.normalization} · 当前展示 ${rows.length} 日`);
  const marker = document.querySelector("#sentimentCycleScoreMarker");
  marker.className = `sentiment-current-phase-badge ${sentimentPhaseClass(latest.phase)}`;
  document.querySelector("#sentimentComponentList").innerHTML = Object.values(latest.components || {}).map((item) => `
    <article class="sentiment-component-item">
      <div class="sentiment-component-main">
        <strong>${escapeHtml(item.label)}</strong>
        <div class="sentiment-component-track" aria-hidden="true"><i data-component-score="${clamp(item.score, 0, 100)}" style="width:0%"></i></div>
        <b>${formatNumber(item.score, 1)} <em>× ${number(item.weight)}%</em></b>
      </div>
      <small>${escapeHtml(item.summary)}</small>
    </article>
  `).join("");
  requestAnimationFrame(() => {
    animateSentimentComponents();
    animateSentimentTrendChart(rows);
    bindSentimentChartTooltip(rows);
  });
  animateRows(body);
}

function animateSentimentComponents() {
  document.querySelectorAll("#sentimentComponentList [data-component-score]").forEach((bar, index) => {
    const width = `${number(bar.dataset.componentScore)}%`;
    if (!motionEnabled()) {
      bar.style.width = width;
      return;
    }
    setTimeout(() => { bar.style.width = width; }, index * 70);
  });
}

function animateSentimentTrendChart(rows) {
  if (sentimentChartAnimationFrame) cancelAnimationFrame(sentimentChartAnimationFrame);
  if (!motionEnabled()) {
    drawSentimentTrendChart(rows, 1);
    return;
  }
  const startedAt = performance.now();
  const duration = 780;
  const frame = (now) => {
    const rawProgress = Math.min(1, (now - startedAt) / duration);
    const progress = 1 - (1 - rawProgress) ** 3;
    drawSentimentTrendChart(rows, progress);
    if (rawProgress < 1) sentimentChartAnimationFrame = requestAnimationFrame(frame);
    else sentimentChartAnimationFrame = null;
  };
  sentimentChartAnimationFrame = requestAnimationFrame(frame);
}

function drawSentimentTrendChart(rows, progress = 1) {
  const canvas = document.querySelector("#sentimentTrendChart");
  if (!canvas || !rows.length || state.activeView !== "sentimentCycleView") return;
  const rect = canvas.getBoundingClientRect();
  if (!rect.width) return;
  const width = Math.max(320, rect.width);
  const height = Math.max(220, rect.height);
  const ratio = window.devicePixelRatio || 1;
  canvas.width = Math.round(width * ratio);
  canvas.height = Math.round(height * ratio);
  const context = canvas.getContext("2d");
  const palette = currentChartPalette();
  context.setTransform(ratio, 0, 0, ratio, 0, 0);
  context.clearRect(0, 0, width, height);
  context.fillStyle = palette.background;
  context.fillRect(0, 0, width, height);
  const padding = { top: 18, right: 18, bottom: 34, left: 42 };
  const chartWidth = width - padding.left - padding.right;
  const chartHeight = height - padding.top - padding.bottom;
  const x = (index) => padding.left + (rows.length === 1 ? chartWidth / 2 : index / (rows.length - 1) * chartWidth);
  const y = (score) => padding.top + (100 - clamp(score, 0, 100)) / 100 * chartHeight;

  context.font = '10px "Microsoft YaHei UI", sans-serif';
  context.textAlign = "right";
  context.textBaseline = "middle";
  for (let score = 0; score <= 100; score += 20) {
    const lineY = y(score);
    context.strokeStyle = score === 40 || score === 80 ? palette.zero : palette.grid;
    context.lineWidth = 1;
    context.beginPath();
    context.moveTo(padding.left, lineY);
    context.lineTo(width - padding.right, lineY);
    context.stroke();
    context.fillStyle = palette.axis;
    context.fillText(String(score), padding.left - 8, lineY);
  }

  context.save();
  context.beginPath();
  context.rect(padding.left - 6, padding.top - 8, (chartWidth + 12) * clamp(progress, 0, 1), chartHeight + 18);
  context.clip();

  const finalPhase = rows[rows.length - 1]?.phase;
  let phaseStart = rows.length - 1;
  while (phaseStart > 0 && rows[phaseStart - 1]?.phase === finalPhase) phaseStart -= 1;
  if (["退潮", "冰点"].includes(finalPhase)) {
    const startX = phaseStart === 0 ? padding.left : (x(phaseStart - 1) + x(phaseStart)) / 2;
    context.fillStyle = palette.alertArea;
    context.fillRect(startX, padding.top, width - padding.right - startX, chartHeight);
    context.fillStyle = palette.up;
    context.font = '10px "Microsoft YaHei UI", sans-serif';
    context.textAlign = "center";
    context.textBaseline = "top";
    context.fillText(finalPhase, (startX + width - padding.right) / 2, padding.top + 4);
  }

  const movingAverage = rows.map((_row, index) => {
    const start = Math.max(0, index - 4);
    const sample = rows.slice(start, index + 1);
    return sample.reduce((sum, item) => sum + number(item.score), 0) / sample.length;
  });
  context.beginPath();
  movingAverage.forEach((score, index) => {
    if (index === 0) context.moveTo(x(index), y(score));
    else context.lineTo(x(index), y(score));
  });
  context.strokeStyle = palette.movingAverage;
  context.lineWidth = 1.5;
  context.setLineDash([5, 4]);
  context.stroke();
  context.setLineDash([]);

  context.beginPath();
  rows.forEach((row, index) => {
    const pointX = x(index);
    const pointY = y(row.score);
    if (index === 0) context.moveTo(pointX, pointY);
    else context.lineTo(pointX, pointY);
  });
  context.lineTo(x(rows.length - 1), padding.top + chartHeight);
  context.lineTo(x(0), padding.top + chartHeight);
  context.closePath();
  context.fillStyle = palette.area;
  context.fill();

  context.beginPath();
  rows.forEach((row, index) => {
    const pointX = x(index);
    const pointY = y(row.score);
    if (index === 0) context.moveTo(pointX, pointY);
    else context.lineTo(pointX, pointY);
  });
  context.strokeStyle = palette.line;
  context.lineWidth = 2.5;
  context.lineJoin = "round";
  context.lineCap = "round";
  context.stroke();

  rows.forEach((row, index) => {
    context.beginPath();
    context.arc(x(index), y(row.score), index === rows.length - 1 ? 4.5 : 3, 0, Math.PI * 2);
    context.fillStyle = ["退潮", "冰点"].includes(row.phase) ? palette.up : row.phase === "修复" ? palette.repair : palette.line;
    context.fill();
    context.strokeStyle = palette.background;
    context.lineWidth = 1.5;
    context.stroke();
  });
  context.restore();

  const labelStep = Math.max(1, Math.ceil(rows.length / 6));
  context.textAlign = "center";
  context.textBaseline = "top";
  context.fillStyle = palette.axis;
  rows.forEach((row, index) => {
    if (index % labelStep !== 0 && index !== rows.length - 1) return;
    const dateText = displayCompactDate(row.trade_date).slice(5);
    context.fillText(dateText, x(index), height - padding.bottom + 10);
  });
}

function bindSentimentChartTooltip(rows) {
  const canvas = document.querySelector("#sentimentTrendChart");
  const tooltip = document.querySelector("#sentimentChartTooltip");
  if (!canvas || !tooltip || !rows.length) return;
  canvas.onmousemove = (event) => {
    const rect = canvas.getBoundingClientRect();
    const padding = { left: 42, right: 18 };
    const chartWidth = Math.max(1, rect.width - padding.left - padding.right);
    const relativeX = clamp(event.clientX - rect.left - padding.left, 0, chartWidth);
    const index = rows.length === 1 ? 0 : Math.round(relativeX / chartWidth * (rows.length - 1));
    const row = rows[index];
    tooltip.innerHTML = `${escapeHtml(displayCompactDate(row.trade_date))} · 温度 <b>${number(row.score)}</b> · ${escapeHtml(row.phase)}`;
    tooltip.hidden = false;
    const targetLeft = padding.left + (rows.length === 1 ? chartWidth / 2 : index / (rows.length - 1) * chartWidth);
    tooltip.style.left = `${clamp(targetLeft + 10, 8, rect.width - tooltip.offsetWidth - 8)}px`;
    tooltip.style.top = `${clamp(event.clientY - rect.top - 34, 8, rect.height - 34)}px`;
  };
  canvas.onmouseleave = () => { tooltip.hidden = true; };
}

function sentimentScoreClass(score) {
  const value = number(score);
  return value >= 60 ? "score-strong" : value < 40 ? "score-weak" : "score-neutral";
}

function sentimentPhaseClass(phase) {
  return {
    "冰点": "phase-ice",
    "修复": "phase-repair",
    "发酵": "phase-fermentation",
    "高潮": "phase-climax",
    "分化": "phase-divergence",
    "退潮": "phase-retreat",
  }[phase] || "phase-divergence";
}

function sentimentPhaseConfidence(row) {
  const explicit = number(row?.confidence || row?.phase_confidence);
  if (explicit > 0) return Math.round(clamp(explicit, 0, 100));
  const historyEvidence = Math.min(12, number(row?.history_days) * 0.6);
  const movementEvidence = Math.min(18, Math.abs(number(row?.day_change)) * 0.8);
  return Math.round(clamp(62 + historyEvidence + movementEvidence, 60, 92));
}

function sentimentPhaseAdvice(phase) {
  return {
    "冰点": "情绪处于极弱区，先观察风险释放，允许没有候选结果。",
    "修复": "风险开始收敛，关注率先转强的核心，小仓验证修复强度。",
    "发酵": "主线与梯队正在形成，优先跟随核心，避免偏离主线。",
    "高潮": "情绪与一致性已处高位，聚焦核心并主动降低后排暴露。",
    "分化": "强弱开始分层，关注承接与回流，淘汰失去辨识度的方向。",
    "退潮": "情绪指标继续走弱。",
  }[phase] || "市场结构尚未形成清晰阶段，保持观察并等待确认。";
}

/* PRESERVATION-SOURCE-END app.js:1207-1516 */
