/* ════════════════════════════════════════════════════════════════
   trend.js — 트렌드 워크스페이스 (Apache ECharts)
   - 단일 차트에 레이어를 쌓는 구조: trState + TR_LAYERS + trRender
   - P1: 멀티시리즈·dataZoom·범례표(색/강조/통계)·minmax·라이브·그룹
   - P1+: 트립/이벤트 오버레이(기존 /api/event-history 재사용)
   ════════════════════════════════════════════════════════════════ */

let trChart       = null;
let trLiveTimer   = null;
let trEditingId   = null;
let trAnalogAll   = [];
let trAnalogMap   = null;          // tag → {desc,unit,euLo,euHi}
let trBuilderSel  = new Set();

const TR_PALETTE = ['#e6194b','#3cb44b','#4363d8','#f58231','#911eb4',
                    '#42d4f4','#f032e6','#bfef45','#469990','#fabed4'];

// ── 중앙 상태 — 모든 레이어가 읽는 단일 진실원 ─────────────────
const trState = {
  group:      null,
  members:    [],                  // [{tag,color,axis,desc,unit,hidden}]
  seriesData: {},                  // { tag: [[ms,val],...] }
  window:     { t0:null, t1:null },
  selected:   null,
  live:       false,
  euAxis:     false,               // Y축: false=데이터 자동, true=메타 euLo/euHi
  yZoom:      null,                // { min, max } from Y zoom, null = auto
  normalize:  false,               // true=전체 태그를 EU 레인지 대비 %(0~100)로 환산
  liveNow:    {},                  // { tag: 현재값 } — 라이브 중 실시간 갱신
  layers:     { minmax:false, events:false, limits:false, runstate:false },
  cache:      { events:[], limits:{}, runbands:[] },
  cursor:     { a:null, b:null },
  cursorMode: false
};

// ── 레이어 레지스트리 — P2/P3는 여기 1줄 + 함수만 추가 ────────────
const TR_LAYERS = [
  { id:'minmax', perSeries: layerMinMax, when: s => s.layers.minmax },
  { id:'events', global:    layerEvents, when: s => s.layers.events },
  { id:'limits', perSeries: layerLimits, when: s => s.layers.limits },
  { id:'runstate', global:  layerRunState, when: s => s.layers.runstate },
  { id:'cursor',   global:  layerCursor,   when: s => s.cursorMode },
];

/* ── 차트 초기화 ─────────────────────────────────────────────── */
function trInitChart() {
  const el = document.getElementById('tr-chart');
  if (!el || typeof echarts === 'undefined') return;
  if (trChart) trChart.dispose();
  trChart = echarts.init(el, null, { renderer: 'canvas' });
  trChart.setOption({
    animation: false,
    grid:    { left: 58, right: 58, top: 20, bottom: 96 },
    tooltip: { trigger: 'axis', axisPointer: { type: 'cross' },
               formatter: function (ps) {                 // 헤어라인 툴팁 — 중복(markPoint/markLine/__헬퍼) 제거
                 if (!Array.isArray(ps)) ps = [ps];
                 const seen = new Set(); let head = ''; const lines = [];
                 for (const p of ps) {
                   if (!p.seriesName || p.seriesName.indexOf('__') === 0) continue;
                   const v = Array.isArray(p.value) ? p.value[1] : p.value;
                   if (v == null) continue;
                   if (seen.has(p.seriesName)) continue; seen.add(p.seriesName);
                   if (!head && p.axisValueLabel) head = p.axisValueLabel;
                   // 100% 모드면 [ms, pct, raw] → 원값(환산%) 둘 다 표시
                   const raw = (Array.isArray(p.value) && p.value.length > 2) ? p.value[2] : null;
                   const disp = (trState.normalize && raw != null)
                     ? `${(+raw).toFixed(2)} <span style="opacity:.6">(${(+v).toFixed(1)}%)</span>`
                     : (+v).toFixed(2);
                   lines.push(`${p.marker}${esc(p.seriesName)}: <b>${disp}</b>`);
                 }
                 return lines.length ? (head ? head + '<br>' : '') + lines.join('<br>') : '';
               } },
    legend:  { show: false },
    xAxis:   { type: 'time' },
    yAxis: [
      { type: 'value', scale: true },
      { type: 'value', scale: true, position: 'right' }
    ],
    dataZoom: [
      { type: 'inside', filterMode: 'none' },
      { type: 'inside', yAxisIndex: [0, 1], filterMode: 'none',
        zoomOnMouseWheel: false, moveOnMouseMove: false },
      { type: 'slider', filterMode: 'none', height: 26, bottom: 44 }
    ],
    toolbox: { right: 12, feature: {
      saveAsImage: { title: 'PNG' },
      dataView:    { readOnly: true, title: '표' },
      restore:     { title: '복원' }
    }},
    series: []
  });
  trChart.on('dataZoom', trOnZoom);
  trChart.on('restore', () => {
    trState.yZoom = null;
    trRender();
  });
  trChart.getZr().on('click', trOnCursorClick);
  window.addEventListener('resize', () => { if (trChart) trChart.resize(); });
}

/* ── 레이어 함수 ─────────────────────────────────────────────── */
function layerBaseSeries(m, s) {
  return {
    name: m.tag, type: 'line', showSymbol: false, connectNulls: false,
    yAxisIndex: s.normalize ? 0 : (m.axis === 'right' ? 1 : 0),  // 100% 모드는 단일축
    lineStyle: { color: m.color, width: m.tag === s.selected ? 4 : 1.6, opacity: m.hidden ? 0 : 1 },
    itemStyle: { color: m.color },
    emphasis:  { focus: 'series', lineStyle: { width: 4 } },
    sampling:  'lttb',
    data: m.hidden ? [] : trPlotData(m),
    markPoint: { silent: true, data: [] }
  };
}

// 멤버 정규화 기준 [lo, span] — EU 레인지 우선, 없으면 데이터 min/max 폴백
function trMemberSpan(m) {
  if (m.euLo != null && m.euHi != null && m.euHi > m.euLo) return [m.euLo, m.euHi - m.euLo];
  const raw = trState.seriesData[m.tag] || [];
  let lo = Infinity, hi = -Infinity;
  for (const pt of raw) { const v = pt[1]; if (v == null) continue; if (v < lo) lo = v; if (v > hi) hi = v; }
  return (isFinite(lo) && isFinite(hi) && hi > lo) ? [lo, hi - lo] : null;
}

// 플롯용 데이터 — 100% 모드면 [ms, pct, raw], 아니면 원본 [ms, val]
function trPlotData(m) {
  const raw = trState.seriesData[m.tag] || [];
  if (!trState.normalize) return raw;
  const sp = trMemberSpan(m);
  if (!sp) return raw;
  const [lo, span] = sp;
  return raw.map(pt => pt[1] == null ? [pt[0], null, null] : [pt[0], (pt[1] - lo) / span * 100, pt[1]]);
}

function layerMinMax(m, s) {                         // 보이는 구간 최대/최소 핀
  if (m.hidden) return { markPoint: { data: [] } };
  const [t0, t1] = trVisibleWindow();
  const d = s.seriesData[m.tag] || [];               // 원값으로 min/max 산출(라벨은 원값)
  let mn = Infinity, mx = -Infinity, mnt = null, mxt = null;
  for (const [ms, v] of d) {
    if (v == null || ms < t0 || ms > t1) continue;
    if (v > mx) { mx = v; mxt = ms; }
    if (v < mn) { mn = v; mnt = ms; }
  }
  if (mxt == null) return { markPoint: { silent: true, data: [] } };
  let toY = v => v;                                   // 100% 모드는 핀 좌표도 %로 환산
  if (s.normalize) { const sp = trMemberSpan(m); if (sp) toY = v => (v - sp[0]) / sp[1] * 100; }
  const data = [
    { coord: [mxt, toY(mx)], symbol: 'pin', symbolSize: 38, itemStyle: { color: m.color },
      label: { formatter: '▲' + mx.toFixed(1), fontSize: 10, color: '#fff' } },
    { coord: [mnt, toY(mn)], symbol: 'pin', symbolRotate: 180, symbolSize: 38, itemStyle: { color: m.color },
      label: { formatter: '▼' + mn.toFixed(1), fontSize: 10, color: '#fff', offset: [0, 18] } }
  ];
  return { markPoint: { silent: true, data } };
}

function layerEvents(s) {                            // 트립/알람 세로 플래그 (전역 helper series)
  if (!s.cache.events.length) return null;
  const data = s.cache.events.map(e => ({
    xAxis: e.ts,
    lineStyle: { color: e.color, type: 'solid', width: 1.2 },
    label: { formatter: e.label, color: e.color, rotate: 90, fontSize: 9, position: 'insideEndTop' }
  }));
  return { name: '__events', type: 'line', data: [], silent: true, showSymbol: false,
           markLine: { symbol: 'none', silent: true, data } };
}

async function trLoadLimits() {
  const tags = trState.members.map(m => m.tag);
  if (!tags.length) return;
  try {
    const d = await api('GET', `/api/trend/limits?tags=${encodeURIComponent(tags.join(','))}`);
    trState.cache.limits = {};
    for (const l of (d.items || [])) trState.cache.limits[l.tag] = l;
  } catch (e) { console.error('trLoadLimits:', e); trState.cache.limits = {}; }
}

async function trLoadRunbands() {
  const from = document.getElementById('hf-trfrom').value;
  const to   = document.getElementById('hf-trto').value;
  if (!from || !to) return;
  try {
    const p = new URLSearchParams();
    p.set('from', new Date(from).toISOString());
    p.set('to', new Date(to).toISOString());
    const d = await api('GET', `/api/trend/runbands?${p}`);
    trState.cache.runbands = (d.items || []).map(b => ({
      tag: b.tag, t0: new Date(b.t0).getTime(), t1: new Date(b.t1).getTime(), state: b.state
    }));
  } catch (e) { console.error('trLoadRunbands:', e); trState.cache.runbands = []; }
}

function layerLimits(m, s) {
  const lim = s.cache.limits[m.tag];
  if (!lim) return {};
  let toY = v => v;
  if (s.normalize) { const sp = trMemberSpan(m); if (sp) toY = v => (v - sp[0]) / sp[1] * 100; }
  const mk = (v, txt, style) => v == null ? null : {
    yAxis: toY(v), lineStyle: style,
    label: { formatter: txt, position: 'insideEndTop', fontSize: 9 }
  };
  const data = [
    mk(lim.hi, 'HI', { color: '#ef4444', type: 'dashed' }),
    mk(lim.lo, 'LO', { color: '#ef4444', type: 'dashed' }),
    mk(lim.sp, 'SP', { color: m.color, type: 'dotted' })
  ].filter(Boolean);
  return { markLine: { silent: true, symbol: 'none', data } };
}

function layerRunState(s) {
  if (!s.cache.runbands.length) return null;
  const COL = { RUN: 'rgba(16,185,129,.10)', TRIP: 'rgba(239,68,68,.12)' };
  const data = s.cache.runbands.map(b => [
    { xAxis: b.t0, itemStyle: { color: COL[b.state] || 'rgba(148,163,184,.1)' } },
    { xAxis: b.t1 }
  ]);
  return { name: '__runstate', type: 'line', data: [], silent: true, showSymbol: false,
           markArea: { silent: true, data } };
}

function trMerge(a, b) {
  return { ...a, ...b,
    lineStyle: { ...(a.lineStyle || {}), ...(b.lineStyle || {}) },
    markPoint: b.markPoint || a.markPoint,
    markLine:  b.markLine  || a.markLine,
    markArea:  b.markArea  || a.markArea };
}

/* ── 합성 렌더 — 켜진 레이어를 모아 한 번에 setOption ─────────── */
function trRender() {
  if (!trChart) return;
  const series = trState.members.map(m => {
    let s = layerBaseSeries(m, trState);
    for (const L of TR_LAYERS) {
      if (L.when && !L.when(trState)) continue;
      if (L.perSeries) s = trMerge(s, L.perSeries(m, trState));
    }
    return s;
  });
  for (const L of TR_LAYERS) {
    if (L.when && !L.when(trState)) continue;
    if (L.global) { const g = L.global(trState); if (g) series.push(g); }
  }
  trChart.setOption({ series, yAxis: trYAxis() }, { replaceMerge: ['series'] });
  trRenderLegend();
}

/* ── Y축 — normalize → yZoom → EU 모드 → auto 순 우선 ──────────── */
function trYAxis() {
  // 100% 환산: 단일 0~100 스케일(우축 숨김). yZoom 있으면 그 %범위 사용.
  if (trState.normalize) {
    const span = trState.yZoom || { min: 0, max: 100 };
    return [
      { type:'value', scale:false, show:true,  axisLabel:{ formatter:'{value}%' }, ...span },
      { type:'value', scale:false, show:false, position:'right', axisLabel:{ formatter:'{value}%' }, ...span }
    ];
  }
  // 비-환산: show/min/max/axisLabel을 명시 리셋해 모드 전환 시 잔상 방지
  const base = pos => ({ type:'value', scale:true, show:true, min:null, max:null,
                         axisLabel:{ formatter:'{value}' }, ...(pos ? { position: pos } : {}) });
  if (trState.yZoom) return [{ ...base(), ...trState.yZoom }, { ...base('right'), ...trState.yZoom }];
  if (!trState.euAxis) return [base(), base('right')];
  const rng = right => {
    let lo = Infinity, hi = -Infinity;
    for (const m of trState.members) {
      if (m.hidden || (m.axis === 'right') !== right) continue;
      if (m.euLo != null && m.euLo < lo) lo = m.euLo;
      if (m.euHi != null && m.euHi > hi) hi = m.euHi;
    }
    return (isFinite(lo) && isFinite(hi) && hi > lo) ? { min: lo, max: hi } : { min: null, max: null };
  };
  return [{ ...base(), ...rng(false) }, { ...base('right'), ...rng(true) }];
}
function trToggleEuAxis() {
  trState.euAxis = !trState.euAxis;
  const b = document.getElementById('tr-euaxis-btn');
  if (b) b.classList.toggle('on', trState.euAxis);
  trRender();
}
function trToggleNormalize() {
  trState.normalize = !trState.normalize;
  trState.yZoom = null;                              // 단위가 바뀌므로 Y줌 리셋
  if (typeof trResetYZoom === 'function') {          // Y줌 인디케이터도 숨김(있으면)
    const ind = document.getElementById('tr-yzoom-indicator');
    if (ind) ind.classList.remove('active');
  }
  const b = document.getElementById('tr-norm-btn');
  if (b) b.classList.toggle('on', trState.normalize);
  trRender();
}

// 비-null 연속점 사이 간격이 과도하면 null 삽입해 선을 끊음(희소/집계 데이터 가짜 연결 방지)
function trBreakGaps(arr) {
  const dx = [];
  for (let i = 1; i < arr.length; i++)
    if (arr[i][1] != null && arr[i-1][1] != null) dx.push(arr[i][0] - arr[i-1][0]);
  if (dx.length < 3) return arr;
  dx.sort((a, b) => a - b);
  const lim = dx[Math.floor(dx.length / 2)] * 3;
  const out = [];
  for (let i = 0; i < arr.length; i++) {
    if (i > 0 && arr[i][1] != null && arr[i-1][1] != null && (arr[i][0] - arr[i-1][0]) > lim)
      out.push([arr[i-1][0] + 1, null]);
    out.push(arr[i]);
  }
  return out;
}

/* ── 데이터 조회 ─────────────────────────────────────────────── */
function trAutoInterval(from, to) {
  const h = (to - from) / 3600000;
  if (h <= 6) return '1 minute';
  if (h <= 48) return '5 minutes';
  if (h <= 14 * 24) return '1 hour';
  return '1 day';
}

async function trQuery() {
  trStopLive();
  if (!trChart) trInitChart();
  const tags = trState.members.map(m => m.tag);
  if (!tags.length) { trState.seriesData = {}; trRender(); return; }

  const from = document.getElementById('hf-trfrom').value;
  const to   = document.getElementById('hf-trto').value;
  const intervalSel = document.getElementById('tr-interval');
  let interval = intervalSel ? intervalSel.value : '1 minute';
  if (interval === 'auto') {
    const fromD = from ? new Date(from) : new Date();
    const toD   = to   ? new Date(to)   : new Date();
    interval = trAutoInterval(fromD.getTime(), toD.getTime());
  }

  // 원시 조회 (집계 실패/0행 시 폴백으로도 사용)
  const rawQuery = async () => {
    const p = new URLSearchParams();
    tags.forEach(t => p.append('tagNames', t));
    if (from) p.set('from', new Date(from).toISOString());
    if (to)   p.set('to',   new Date(to).toISOString());
    p.set('limit', '5000');
    const d = await api('GET', `/api/history/query?${p}`);
    return { rows: d.rows || [], tk: 'recordedAt' };
  };

  let rows = [], tk = 'recordedAt';
  try {
    if (interval !== '1 minute') {
      // 집계 경로 — 엔드포인트가 [FromBody]라 반드시 JSON body로 전송
      const d = await api('POST', '/api/text-to-sql/query-history-interval', {
        tagNames: tags,
        from: from ? new Date(from).toISOString() : null,
        to:   to   ? new Date(to).toISOString()   : null,
        interval, limit: 5000
      });
      rows = (d && d.success !== false) ? (d.rows || []) : [];
      tk = rows[0]?.timeBucket != null ? 'timeBucket' : 'recordedAt';
      // 집계가 0행이면(엔드포인트 이슈 등) 원시로 폴백 → 차트 공백 방지
      if (!rows.length) { const r = await rawQuery(); rows = r.rows; tk = r.tk; }
    } else {
      const r = await rawQuery(); rows = r.rows; tk = r.tk;
    }
  } catch (e) {
    console.error('trQuery:', e);
    try { const r = await rawQuery(); rows = r.rows; tk = r.tk; }   // 집계 예외도 원시 폴백
    catch (e2) { setGlobal('err', '트렌드 조회 실패'); alert('조회 실패: ' + e2.message); return; }
  }

  trState.seriesData = {};
  for (const m of trState.members) trState.seriesData[m.tag] = [];
  for (const r of rows) {
    const ms = new Date(r[tk]).getTime();
    if (!isFinite(ms)) continue;                          // 잘못된 타임스탬프 스킵(축 왜곡 방지)
    for (const m of trState.members) {
      const v = r.values?.[m.tag];
      trState.seriesData[m.tag].push([ms, (v == null || v === '') ? null : +v]);
    }
  }
  for (const m of trState.members)
    trState.seriesData[m.tag] = trBreakGaps(trState.seriesData[m.tag]);

  if (trState.layers.events) await trLoadEvents();
  if (trState.layers.limits) await trLoadLimits();
  if (trState.layers.runstate) await trLoadRunbands();
  trState.yZoom = null;
  trChart.setOption({ dataZoom: [
    { start: 0, end: 100, startValue: null, endValue: null },
    { start: 0, end: 100, startValue: null, endValue: null },
    { start: 0, end: 100, startValue: null, endValue: null }
  ]});
  trRender();
  setGlobal('ok', `${rows.length}건`);
}

/* ── 퀵 레인지 / 날짜 ─────────────────────────────────────────── */
function trSetDt(target, date) {
  const p = n => String(n).padStart(2, '0');
  const v = `${date.getFullYear()}-${p(date.getMonth()+1)}-${p(date.getDate())}T${p(date.getHours())}:${p(date.getMinutes())}`;
  document.getElementById('hf-' + target).value = v;
  document.getElementById('dtp-' + target + '-display').textContent = v.replace('T', ' ');
}

function trShiftStart(now) {                          // 교대 06/14/22
  const h = now.getHours();
  let s = h >= 22 ? 22 : h >= 14 ? 14 : h >= 6 ? 6 : 22;
  const d = new Date(now); d.setMinutes(0, 0, 0);
  if (s === 22 && h < 6) d.setDate(d.getDate() - 1);
  d.setHours(s);
  return d;
}

function trQuickRange(r) {
  const now = new Date(); let from = new Date(now);
  if      (r === '1h')  from.setHours(now.getHours() - 1);
  else if (r === '8h')  from.setHours(now.getHours() - 8);
  else if (r === '24h') from.setDate(now.getDate() - 1);
  else if (r === '7d')  from.setDate(now.getDate() - 7);
  else if (r === 'shift') from = trShiftStart(now);
  trSetDt('trfrom', from); trSetDt('trto', now);
  trQuery();
}

/* ── 보이는 윈도우 / 통계 ─────────────────────────────────────── */
function trVisibleWindow() {
  let lo = Infinity, hi = -Infinity;
  for (const k in trState.seriesData)
    for (const pt of trState.seriesData[k]) { if (pt[0] < lo) lo = pt[0]; if (pt[0] > hi) hi = pt[0]; }
  if (!isFinite(lo)) return [0, 0];
  if (!trChart) return [lo, hi];
  const dz = (trChart.getOption().dataZoom || [])[0] || {};
  if (dz.startValue != null && dz.endValue != null) return [dz.startValue, dz.endValue];
  const s = (dz.start ?? 0) / 100, e = (dz.end ?? 100) / 100;
  return [lo + (hi - lo) * s, lo + (hi - lo) * e];
}

function trStats(d, t0, t1) {
  if (!d) return {};
  let mn = Infinity, mx = -Infinity, sum = 0, n = 0, last = null;
  for (const [ms, v] of d) {
    if (v == null || ms < t0 || ms > t1) continue;
    if (v < mn) mn = v; if (v > mx) mx = v; sum += v; n++; last = v;
  }
  if (!n) return {};
  const avg = sum / n; let sq = 0;
  for (const [ms, v] of d) { if (v == null || ms < t0 || ms > t1) continue; sq += (v - avg) ** 2; }
  return { min: mn.toFixed(2), max: mx.toFixed(2), avg: avg.toFixed(2),
           sd: Math.sqrt(sq / n).toFixed(2), last: last.toFixed(2) };
}

/* ── 범례 / 통계 표 ──────────────────────────────────────────── */
function trRenderLegend() {
  const tb = document.getElementById('tr-legend-body');
  if (!tb) return;
  if (!trState.members.length) {
    tb.innerHTML = '<tr><td colspan="12" class="tr-empty">그룹을 불러오거나 ＋그룹으로 멤버를 추가하세요.</td></tr>';
    return;
  }
  const [t0, t1] = trVisibleWindow();
  tb.innerHTML = trState.members.map(m => {
    const st = trStats(trState.seriesData[m.tag], t0, t1);
    // 현재: 라이브 중이면 실시간 현재값, 아니면 보이는 구간의 마지막 표본
    const live = (trState.live && trState.liveNow[m.tag] != null) ? trState.liveNow[m.tag] : null;
    const cur = live != null ? live.toFixed(2) : (st.last ?? '—');
    return `<tr class="${m.tag === trState.selected ? 'tr-sel' : ''}" onclick="trHighlight('${esc(m.tag)}')">
      <td><input type="color" value="${m.color}" onclick="event.stopPropagation()" onchange="trSetColor('${esc(m.tag)}',this.value)"></td>
      <td class="tr-tag">${esc(m.tag)}</td>
      <td>${esc(m.desc || '')}</td>
      <td class="val${live != null ? ' tr-live-cur' : ''}">${cur}</td>
      <td class="val">${st.min ?? '—'}</td>
      <td class="val">${st.max ?? '—'}</td>
      <td class="val">${st.avg ?? '—'}</td>
      <td class="val mut">${st.sd ?? '—'}</td>
      <td>${esc(m.unit || '')}</td>
      <td><select onclick="event.stopPropagation()" onchange="trSetAxis('${esc(m.tag)}',this.value)">
        <option value="left"${m.axis !== 'right' ? ' selected' : ''}>좌</option>
        <option value="right"${m.axis === 'right' ? ' selected' : ''}>우</option></select></td>
      <td><input type="checkbox" ${m.hidden ? '' : 'checked'} onclick="event.stopPropagation();trToggleVis('${esc(m.tag)}',this.checked)"></td>
      <td><button class="tr-rm" onclick="event.stopPropagation();trRemoveMember('${esc(m.tag)}')">×</button></td>
    </tr>`;
  }).join('');
}

function trHighlight(tag) {
  trState.selected = (trState.selected === tag) ? null : tag;
  trRender();
  trChart.dispatchAction({ type: 'downplay' });
  if (trState.selected) trChart.dispatchAction({ type: 'highlight', seriesName: trState.selected });
}
function trSetColor(tag, c) { const m = trState.members.find(x => x.tag === tag); if (!m) return; m.color = c; trRender(); trPersist(); }
function trSetAxis(tag, a)  { const m = trState.members.find(x => x.tag === tag); if (!m) return; m.axis = a; trRender(); trPersist(); }
function trToggleVis(tag, vis) { const m = trState.members.find(x => x.tag === tag); if (!m) return; m.hidden = !vis; trRender(); }
function trRemoveMember(tag) { trState.members = trState.members.filter(m => m.tag !== tag); delete trState.seriesData[tag]; trRender(); trPersist(); }

function trOnZoom() {
  const [t0, t1] = trVisibleWindow();
  trState.window = { t0, t1 };
  trRenderLegend();
  if (trState.layers.minmax) trRender();
}

/* ── Shift+Y축 zoom 모드 ────────────────────────────────────── */
let trShiftDown = false;

function trSetYZoomMode(active) {
  if (!trChart) return;
  const dz = (trChart.getOption().dataZoom || [{},{},{}]).map(d => ({ ...d }));
  dz[0].zoomOnMouseWheel = !active;
  dz[0].moveOnMouseMove = !active;
  dz[1].zoomOnMouseWheel = active;
  dz[1].moveOnMouseMove = active;
  trChart.setOption({ dataZoom: dz }, { replaceMerge: ['dataZoom'] });
  document.getElementById('tr-yzoom-indicator')?.classList.toggle('active', active);
}

document.addEventListener('keydown', e => {
  if (e.key === 'Shift' && !trShiftDown) {
    trShiftDown = true;
    trSetYZoomMode(true);
  }
});
document.addEventListener('keyup', e => {
  if (e.key === 'Shift' && trShiftDown) {
    trShiftDown = false;
    trSetYZoomMode(false);
  }
});

/* ── Y축 zoom 원위치 ─────────────────────────────────────────── */
function trResetYZoom() {
  if (!trChart) return;
  const dz = (trChart.getOption().dataZoom || [{},{},{}]).map(d => ({ ...d }));
  dz[1].start = 0;
  dz[1].end = 100;
  dz[1].startValue = null;
  dz[1].endValue = null;
  trChart.setOption({ dataZoom: dz }, { replaceMerge: ['dataZoom'] });
}

/* ── 듀얼 커서 Δ ───────────────────────────────────────────── */
function trToggleCursorMode() {
  trState.cursorMode = !trState.cursorMode;
  const btn = document.getElementById('tr-cursor-btn');
  if (btn) btn.classList.toggle('on', trState.cursorMode);
  if (!trState.cursorMode) { trState.cursor = { a: null, b: null }; trRenderCursor(); }
}

function trRenderCursor() { trRender(); }   // 커서는 레이어(layerCursor)로 그린다

// 듀얼 커서 레이어 (global) — A/B 점 + 연결선 + Δ 라벨. 데이터좌표를 ECharts가 자동 픽셀변환.
function layerCursor(s) {
  const { a, b } = s.cursor;
  if (!a && !b) return null;
  const mp = [];
  if (a) mp.push({ coord: [a.ms, a.val], symbol: 'circle', symbolSize: 10, itemStyle: { color: '#f59e0b' },
                   label: { show: true, formatter: 'A', position: 'top', color: '#f59e0b', fontSize: 11, fontWeight: 'bold' } });
  if (b) mp.push({ coord: [b.ms, b.val], symbol: 'circle', symbolSize: 10, itemStyle: { color: '#3b82f6' },
                   label: { show: true, formatter: 'B', position: 'top', color: '#3b82f6', fontSize: 11, fontWeight: 'bold' } });
  let ml;
  if (a && b) {
    const dtS  = (b.ms - a.ms) / 1000;
    const dy   = (a.val != null && b.val != null) ? b.val - a.val : null;
    const slope = (dy != null && dtS !== 0) ? dy / (dtS / 60) : null;   // 분당 변화율
    const txt = [`Δt=${Math.abs(dtS).toFixed(0)}s`,
                 dy != null ? `Δy=${dy.toFixed(2)}` : null,
                 slope != null ? `기울기=${slope.toFixed(2)}/min` : null].filter(Boolean).join('   ');
    ml = { symbol: 'none', silent: true, lineStyle: { color: '#8b5cf6', type: 'dashed', width: 1.5 },
           data: [[{ coord: [a.ms, a.val] },
                   { coord: [b.ms, b.val],
                     label: { show: true, formatter: txt, fontSize: 11, color: '#fff',
                              backgroundColor: 'rgba(124,58,237,.9)', padding: [4, 7], borderRadius: 4 } }]] };
  }
  return { name: '__cursor', type: 'line', data: [], silent: true, showSymbol: false, z: 20,
           markPoint: { silent: true, data: mp }, markLine: ml };
}

function trOnCursorClick(e) {
  if (!trState.cursorMode || !trChart) return;
  const pt = trChart.convertFromPixel({ xAxisIndex: 0, yAxisIndex: 0 }, [e.offsetX, e.offsetY]);
  if (!pt || !isFinite(pt[0])) return;
  const ms = pt[0];
  // 보이는 데이터에서 가장 가까운 값 찾기
  let minDist = Infinity, bestVal = null;
  for (const tag in trState.seriesData) {
    for (const [t, v] of trState.seriesData[tag]) {
      if (v == null) continue;
      const dist = Math.abs(t - ms);
      if (dist < minDist) { minDist = dist; bestVal = v; }
    }
  }
  if (bestVal == null) return;
  if (!trState.cursor.a) {
    trState.cursor.a = { ms, val: bestVal };
  } else if (!trState.cursor.b) {
    trState.cursor.b = { ms, val: bestVal };
  } else {
    trState.cursor = { a: null, b: null };
  }
  trRenderCursor();
}

/* ── 레이어 토글 ─────────────────────────────────────────────── */
async function trToggleLayer(id) {
  trState.layers[id] = !trState.layers[id];
  const btn = document.querySelector(`.tr-tog[data-layer="${id}"]`);
  if (btn) btn.classList.toggle('on', trState.layers[id]);
  if (id === 'events' && trState.layers.events) await trLoadEvents();
  if (id === 'limits' && trState.layers.limits) await trLoadLimits();
  if (id === 'runstate' && trState.layers.runstate) await trLoadRunbands();
  trRender();
}

async function trLoadEvents() {
  const from = document.getElementById('hf-trfrom').value;
  const to   = document.getElementById('hf-trto').value;
  const body = { limit: 1000 };
  if (from) body.from = new Date(from).toISOString();
  if (to)   body.to   = new Date(to).toISOString();
  const COL = { TRIP:'#ef4444', ALARM:'#f59e0b', RUN:'#10b981', NORMAL:'#64748b', CHANGE:'#60a5fa' };
  try {
    const d = await api('POST', '/api/events/query', body);
    trState.cache.events = (d || [])
      .filter(r => r.EventType === 'TRIP' || r.EventType === 'ALARM')
      .map(r => ({ ts: new Date(r.EventTime).getTime(), type: r.EventType,
                   color: COL[r.EventType] || '#888',
                   label: `${(r.TagName || '').toUpperCase()} ${r.EventType}` }));
  } catch (e) { console.error('trLoadEvents:', e); trState.cache.events = []; }
}

/* ── 실시간 ──────────────────────────────────────────────────── */
function trToggleLive() {
  if (trLiveTimer) { trStopLive(); return; }
  if (!trState.members.length) { alert('먼저 그룹을 불러오세요.'); return; }
  const b = document.getElementById('tr-live-btn');
  b.textContent = '⏸ 정지'; b.classList.add('live-on');
  trState.live = true;
  trLiveTimer = setInterval(trLiveTick, 5000);
  trLiveTick();
}
function trStopLive() {
  if (trLiveTimer) { clearInterval(trLiveTimer); trLiveTimer = null; }
  trState.live = false;
  const b = document.getElementById('tr-live-btn');
  if (b) { b.textContent = '▶ 라이브'; b.classList.remove('live-on'); }
}
async function trLiveTick() {
  const tags = trState.members.map(m => m.tag);
  if (!tags.length) return;
  try {
    const d = await api('GET', `/api/trend/live?tags=${encodeURIComponent(tags.join(','))}`);
    let changed = false;
    for (const pt of (d.items || [])) {
      if (pt.value == null) continue;
      trState.liveNow[pt.tag] = +pt.value;            // 현재값은 매 틱 갱신(타임스탬프 동일해도)
      const arr = trState.seriesData[pt.tag]; if (!arr) continue;
      const ms = new Date(pt.ts).getTime();
      if (!isFinite(ms)) continue;
      if (!arr.length || arr[arr.length - 1][0] !== ms) { arr.push([ms, +pt.value]); changed = true; }
    }
    trRenderLegend();             // 현재값은 항상 먼저 갱신 (차트 렌더와 분리 — 렌더 에러가 표를 막지 않게)
    if (changed) { try { trRender(); } catch (e) { console.error('trLiveTick render:', e); } }
  } catch (e) { console.error('trLiveTick:', e); }
}

/* ── Insights (P3 자리) ──────────────────────────────────────── */
function trToggleInsights() { document.getElementById('tr-insights').classList.toggle('hidden'); }
function trAnalyze() { document.getElementById('tr-ins-body').textContent = 'P3 단계에서 LLM 분석이 연결됩니다.'; }

/* ── 그룹 빌더 (아날로그 클릭 선택) ──────────────────────────── */
async function trEnsureAnalogMap() {
  if (trAnalogMap) return trAnalogMap;
  const d = await api('GET', '/api/trend/analog-points');
  trAnalogAll = d.items || [];
  trAnalogMap = {};
  for (const p of trAnalogAll) trAnalogMap[p.tagName] = { desc: p.description, unit: p.unit, euLo: p.euLo, euHi: p.euHi };
  return trAnalogMap;
}

async function trOpenBuilder() {
  trEditingId = null;
  document.getElementById('tr-builder-name').value = '';
  document.getElementById('tr-builder-search').value = '';
  await trOpenBuilderWith(new Set(trState.members.map(m => m.tag)));
}
async function trOpenBuilderWith(preSel) {
  document.getElementById('tr-builder').classList.remove('hidden');
  trBuilderSel = preSel;
  await trEnsureAnalogMap();
  trRenderAnalog();
}
function trCloseBuilder() { document.getElementById('tr-builder').classList.add('hidden'); }

function trRenderAnalog() {
  const q = (document.getElementById('tr-builder-search').value || '').toLowerCase();
  const body = document.getElementById('tr-analog-body');
  body.innerHTML = trAnalogAll
    .filter(p => !q || p.tagName.toLowerCase().includes(q) || (p.description || '').toLowerCase().includes(q))
    .map(p => `<tr class="${trBuilderSel.has(p.tagName) ? 'tr-pick' : ''}" onclick="trTogglePick('${esc(p.tagName)}')">
      <td>${trBuilderSel.has(p.tagName) ? '✔' : ''}</td>
      <td class="tr-tag">${esc(p.tagName)}</td>
      <td>${esc(p.description || '')}</td>
      <td>${esc(parseEnumPv(p.area) || '')}</td>
      <td class="val">${p.value ?? '—'}</td>
      <td>${esc(p.unit || '')}</td>
      <td class="val">${p.euLo ?? '—'}</td>
      <td class="val">${p.euHi ?? '—'}</td>
    </tr>`).join('');
  document.getElementById('tr-builder-count').textContent = `${trBuilderSel.size}개 선택`;
}
function trTogglePick(tag) {
  if (trBuilderSel.has(tag)) trBuilderSel.delete(tag); else trBuilderSel.add(tag);
  trRenderAnalog();
}

async function trSaveGroup() {
  const name = document.getElementById('tr-builder-name').value.trim();
  if (!name) { alert('그룹 이름을 입력하세요.'); return; }
  if (!trBuilderSel.size) { alert('태그를 최소 1개 선택하세요.'); return; }
  const members = [...trBuilderSel].map((t, i) => {
    const ex = trState.members.find(m => m.tag === t);
    return { tag: t, color: ex?.color || TR_PALETTE[i % TR_PALETTE.length], axis: ex?.axis || 'left' };
  });
  const body = { name, members };
  try {
    const saved = trEditingId
      ? await api('PUT', `/api/trend/groups/${trEditingId}`, body)
      : await api('POST', '/api/trend/groups', body);
    trCloseBuilder();
    await trLoadGroupList();
    document.getElementById('tr-group-select').value = saved.id;
    await trApplyGroup(saved);
  } catch (e) { console.error('trSaveGroup:', e); alert('저장 실패: ' + e.message); }
}

/* ── 그룹 목록 / 로드 / 삭제 / 편집 ──────────────────────────── */
async function trLoadGroupList() {
  try {
    const d = await api('GET', '/api/trend/groups');
    const sel = document.getElementById('tr-group-select');
    const cur = sel.value;
    sel.innerHTML = '<option value="">— 그룹 선택 —</option>' +
      (d.items || []).map(g => `<option value="${g.id}">${esc(g.name)} (${(g.members || []).length})</option>`).join('');
    if (cur) sel.value = cur;
  } catch (e) { console.error('trLoadGroupList:', e); }
}

async function trApplyGroup(g) {
  trState.group = g;
  await trEnsureAnalogMap();
  trState.members = (g.members || []).map((m, i) => ({
    tag:   m.tag,
    color: m.color || TR_PALETTE[i % TR_PALETTE.length],
    axis:  m.axis || 'left',
    desc:  trAnalogMap[m.tag]?.desc || '',
    unit:  trAnalogMap[m.tag]?.unit || '',
    euLo:  trAnalogMap[m.tag]?.euLo,
    euHi:  trAnalogMap[m.tag]?.euHi,
    hidden: false
  }));
  await trQuery();
}

async function trLoadGroup() {
  const id = document.getElementById('tr-group-select').value;
  if (!id) { alert('그룹을 선택하세요.'); return; }
  try { await trApplyGroup(await api('GET', `/api/trend/groups/${id}`)); }
  catch (e) { console.error('trLoadGroup:', e); alert('불러오기 실패: ' + e.message); }
}

async function trDeleteGroup() {
  const id = document.getElementById('tr-group-select').value;
  if (!id) { alert('삭제할 그룹을 선택하세요.'); return; }
  if (!confirm('이 그룹을 삭제하시겠습니까?')) return;
  try {
    await api('DELETE', `/api/trend/groups/${id}`);
    trState.group = null;
    await trLoadGroupList();
    document.getElementById('tr-group-select').value = '';
  } catch (e) { console.error('trDeleteGroup:', e); alert('삭제 실패: ' + e.message); }
}

async function trEditGroup() {
  const id = document.getElementById('tr-group-select').value;
  if (!id) { alert('편집할 그룹을 선택하세요.'); return; }
  try {
    const g = await api('GET', `/api/trend/groups/${id}`);
    trEditingId = g.id;
    document.getElementById('tr-builder-name').value = g.name;
    document.getElementById('tr-builder-search').value = '';
    await trOpenBuilderWith(new Set((g.members || []).map(m => m.tag)));
  } catch (e) { console.error('trEditGroup:', e); }
}

async function trPersist() {                          // 색상/축 변경을 현재 그룹에 저장
  if (!trState.group) return;
  try {
    await api('PUT', `/api/trend/groups/${trState.group.id}`, {
      name: trState.group.name,
      description: trState.group.description,
      members: trState.members.map(m => ({ tag: m.tag, color: m.color, axis: m.axis }))
    });
  } catch (e) { console.error('trPersist:', e); }
}

/* ── 탭 진입 ─────────────────────────────────────────────────── */
paneInit.trend = function () {
  if (!trChart) trInitChart(); else trChart.resize();
  trLoadGroupList();
};
