"""Ground Truth 차트 HTML 생성 (전체 기간 지원)."""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import pandas as pd

from deepcoin.data.candle_loader import load_candles

# 0이면 제한 없이 전체 봉 표시
DEFAULT_MAX_CANDLES = 0


def _data_js_path(html_path: Path) -> Path:
    """HTML과 짝을 이루는 데이터 JS 경로 (file:// 프로토콜 호환).

    예: ground_truth_chart_v3.html → ground_truth_chart_v3_data.js
    """
    return html_path.with_name(f"{html_path.stem}_data.js")


def render_ground_truth_chart(
    db_path: Path,
    symbol: str,
    gt_result: dict[str, Any],
    output_path: Path,
    chart_lookback_days: int | None = None,
    max_candles: int = DEFAULT_MAX_CANDLES,
) -> Path:
    """GT 타점이 표시된 HTML 차트를 생성한다.

    대용량(3분봉 2년 등)은 종가 라인으로 전체 기간을 표시하고,
    JSON 데이터는 별도 파일로 분리한다.

    Args:
        db_path: SQLite 경로.
        symbol: 코인 심볼.
        gt_result: build_ground_truth 결과.
        output_path: HTML 출력 경로.
        chart_lookback_days: 차트에 표시할 일수. None이면 GT lookback과 동일.
        max_candles: 0이면 전체, 양수면 최근 N봉만.

    Returns:
        HTML 저장 경로.
    """
    interval_min = gt_result["meta"]["interval_min"]
    gt_lookback_days = gt_result["meta"]["lookback_days"]
    chart_days = chart_lookback_days if chart_lookback_days is not None else gt_lookback_days

    df = load_candles(db_path, symbol, interval_min, lookback_days=chart_days)
    if max_candles > 0 and len(df) > max_candles:
        df = df.iloc[-max_candles:].reset_index(drop=True)

    times = (pd.to_datetime(df["datetime"]).astype("int64") // 10**9).astype(int).tolist()

    buy_markers = []
    sell_markers = []
    for sig in gt_result.get("signals") or []:
        ts = int(pd.Timestamp(sig["datetime"]).timestamp())
        marker = {
            "time": ts,
            "price": sig["price"],
            "marker_id": sig.get("marker_id", sig.get("leg_id")),
            "signal_type": sig.get("signal_type", "swing_low" if sig["side"] == "buy" else "swing_high"),
        }
        if sig["side"] == "buy":
            buy_markers.append(marker)
        else:
            sell_markers.append(marker)

    chart_meta = {
        **gt_result["meta"],
        "chart_lookback_days": chart_days,
        "gt_lookback_days": gt_lookback_days,
        "chart_data_from": str(df["datetime"].min()),
        "chart_data_to": str(df["datetime"].max()),
        "gt_marker_count": len(buy_markers),
    }
    payload = {
        "times": times,
        "open": df["open"].astype(float).tolist(),
        "high": df["high"].astype(float).tolist(),
        "low": df["low"].astype(float).tolist(),
        "close": df["close"].astype(float).tolist(),
        "buy_markers": buy_markers,
        "sell_markers": sell_markers,
        "meta": chart_meta,
        "bar_count": len(df),
    }

    output_path.parent.mkdir(parents=True, exist_ok=True)
    data_path = _data_js_path(output_path)
    with data_path.open("w", encoding="utf-8") as fp:
        fp.write("window.CHART_DATA=")
        json.dump(payload, fp, ensure_ascii=False, separators=(",", ":"))
        fp.write(";")

    data_js_name = data_path.name
    output_path.write_text(_html_template(data_js_name), encoding="utf-8")
    return output_path


_HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8" />
  <title>Ground Truth Chart</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.min.css" />
  <script src="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.iife.min.js"></script>
  <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
  <script src="__DATA_JS_NAME__"></script>
  <style>
    body { font-family: "Malgun Gothic", Arial, sans-serif; margin: 0; background: #f5f5f5; color: #333; }
    header { padding: 16px 24px; background: #fff; border-bottom: 1px solid #ddd; }
    h1 { margin: 0 0 6px; font-size: 20px; }
    .meta { font-size: 13px; color: #666; }
    .toolbar { padding: 10px 24px; background: #fff; border-bottom: 1px solid #eee; display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
    .toolbar-group { display: flex; gap: 6px; align-items: center; padding-right: 12px; border-right: 1px solid #e0e0e0; }
    .toolbar-group:last-of-type { border-right: none; }
    .toolbar button { padding: 6px 12px; border: 1px solid #bbb; background: #fff; cursor: pointer; border-radius: 4px; font-size: 13px; white-space: nowrap; }
    .toolbar button:hover { background: #f0f4f8; }
    .toolbar button.active { background: #1565c0; color: #fff; border-color: #1565c0; }
    .toolbar button.home { background: #2e7d32; color: #fff; border-color: #2e7d32; font-weight: bold; }
    .toolbar button.home:hover { background: #1b5e20; }
    .toolbar .leg-info { font-size: 12px; color: #555; min-width: 90px; }
    #status { font-size: 12px; color: #888; margin-left: auto; }
    #overview { height: 480px; margin: 12px 24px; background: #fff; border: 1px solid #ddd; overflow: visible; }
    #overview .u-wrap, #overview .uplot { overflow: visible !important; }
    #detail-wrap { margin: 0 24px 12px; display: none; }
    #detail-wrap h2 { font-size: 15px; margin: 0 0 8px; }
    #detail { height: 360px; background: #fff; border: 1px solid #ddd; overflow: visible; }
  </style>
</head>
<body>
  <header>
    <h1 id="title">Ground Truth Chart</h1>
    <div class="meta" id="meta"></div>
  </header>
  <div class="toolbar">
    <div class="toolbar-group">
      <button id="btn-home" class="home" title="전체 2년 화면으로 복귀">홈</button>
      <button id="btn-prev-leg" title="이전 매수·매도 타점">◀ 이전</button>
      <button id="btn-next-leg" title="다음 매수·매도 타점">다음 ▶</button>
      <span class="leg-info" id="leg-info">타점 - / -</span>
    </div>
    <div class="toolbar-group">
      <button id="btn-all" class="btn-period active">전체</button>
      <button id="btn-365d" class="btn-period">1년</button>
      <button id="btn-30d" class="btn-period">30일</button>
      <button id="btn-7d" class="btn-period">7일</button>
      <button id="btn-3d" class="btn-period">3일</button>
    </div>
    <div class="toolbar-group">
      <button id="btn-zoom-in" title="확대">+ 확대</button>
      <button id="btn-zoom-out" title="축소">− 축소</button>
      <button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
    </div>
    <div class="toolbar-group">
      <button id="btn-markers" title="매수·매도 마커 표시/숨김">마커 숨김</button>
      <button id="btn-toggle-detail" title="상세 캔들 패널 표시/숨김">상세 패널</button>
    </div>
    <span id="status">데이터 로딩 중…</span>
  </div>
  <div id="overview"></div>
  <div id="detail-wrap">
    <h2 id="detail-title">상세 캔들</h2>
    <div id="detail"></div>
  </div>
  <script>
    let DATA = null;
    let overviewPlot = null;
    let detailChart = null;
    let detailSeries = null;
    let currentMode = "overview";
    let currentLegIdx = 0;
    let showMarkers = true;
    let detailVisible = false;
    let lastDetailStart = 0;

    const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
    let axisMeasureCtx = null;

    function fmtPrice(v) {
      return Math.round(v).toLocaleString("ko-KR");
    }

    function measureTextWidth(text, font) {
      if (!axisMeasureCtx) {
        const c = document.createElement("canvas");
        axisMeasureCtx = c.getContext("2d");
      }
      axisMeasureCtx.font = font;
      return axisMeasureCtx.measureText(text).width;
    }

    function yAxisLabelWidth() {
      const vals = DATA.close;
      if (!vals || !vals.length) return 88;
      const samples = new Set([vals[0], vals[vals.length - 1]]);
      let lo = vals[0], hi = vals[0];
      for (let i = 1; i < vals.length; i++) {
        if (vals[i] < lo) lo = vals[i];
        if (vals[i] > hi) hi = vals[i];
      }
      samples.add(lo);
      samples.add(hi);
      samples.add((lo + hi) / 2);
      let maxW = 0;
      samples.forEach(v => {
        maxW = Math.max(maxW, measureTextWidth(fmtPrice(v), AXIS_FONT));
      });
      return Math.ceil(maxW) + 20;
    }

    function updateLegInfo() {
      const total = DATA.buy_markers.length;
      const el = document.getElementById("leg-info");
      if (!total) { el.textContent = "타점 없음"; return; }
      el.textContent = `타점 ${currentLegIdx + 1} / ${total}`;
    }

    const MARKER_FONT = "bold 18px Malgun Gothic, Arial, sans-serif";

    function markerSuffix(signalType) {
      if (signalType === "pullback") return "*";
      if (signalType === "breakout") return "^";
      if (signalType === "div_bull" || signalType === "div_bear") return "d";
      return "";
    }

    function drawMarkerLabel(ctx, label, x, y, color, up) {
      ctx.font = MARKER_FONT;
      const lx = x + 10;
      const ly = y + (up ? 28 : -20);
      ctx.lineWidth = 3;
      ctx.lineJoin = "round";
      ctx.strokeStyle = "rgba(255,255,255,0.85)";
      ctx.strokeText(label, lx, ly);
      ctx.fillStyle = color;
      ctx.fillText(label, lx, ly);
    }

    function drawMarkers(u, buys, sells) {
      if (!showMarkers) return;
      const ctx = u.ctx;
      const drawOne = (m, color, up) => {
        const x = u.valToPos(m.time, "x", true);
        const y = u.valToPos(m.price, "y", true);
        if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
        ctx.fillStyle = color;
        ctx.beginPath();
        const s = 8;
        const gap = 12;
        if (up) {
          ctx.moveTo(x, y + gap); ctx.lineTo(x - s, y + gap + 16); ctx.lineTo(x + s, y + gap + 16);
        } else {
          ctx.moveTo(x, y - gap); ctx.lineTo(x - s, y - gap - 16); ctx.lineTo(x + s, y - gap - 16);
        }
        ctx.closePath(); ctx.fill();
        const label = (up ? "B" : "S") + m.marker_id + markerSuffix(m.signal_type);
        drawMarkerLabel(ctx, label, x, y, color, up);
      };
      buys.forEach(m => {
        let color = "#2e7d32";
        if (m.signal_type === "breakout") color = "#ef6c00";
        else if (m.signal_type === "div_bull") color = "#7b1fa2";
        drawOne(m, color, true);
      });
      sells.forEach(m => {
        const color = m.signal_type === "div_bear" ? "#7b1fa2" : "#c62828";
        drawOne(m, color, false);
      });
    }

    function overviewXRange() {
      if (!overviewPlot) return { min: DATA.times[0], max: DATA.times[DATA.times.length - 1] };
      const s = overviewPlot.scales.x;
      return { min: s.min, max: s.max };
    }

    function setOverviewXRange(min, max) {
      const t0 = DATA.times[0];
      const t1 = DATA.times[DATA.times.length - 1];
      overviewPlot.setScale("x", {
        min: Math.max(t0, min),
        max: Math.min(t1, max),
      });
    }

    function fitOverview() {
      setOverviewXRange(DATA.times[0], DATA.times[DATA.times.length - 1]);
    }

    function zoomOverview(factor) {
      const { min, max } = overviewXRange();
      const mid = (min + max) / 2;
      const half = Math.max((max - min) * factor / 2, 3600);
      setOverviewXRange(mid - half, mid + half);
    }

    function buildOverview(keepRange) {
      const prev = keepRange ? overviewXRange() : null;
      if (overviewPlot) { overviewPlot.destroy(); overviewPlot = null; }
      const yAxisW = yAxisLabelWidth();
      const opts = {
        width: document.getElementById("overview").clientWidth,
        height: 480,
        padding: [14, 10, 14, 10],
        scales: { x: { time: true } },
        axes: [
          { gap: 6 },
          {
            side: 3,
            size: yAxisW,
            gap: 10,
            font: AXIS_FONT,
            values: (u, vals) => vals.map(v => fmtPrice(v)),
          },
        ],
        series: [
          {},
          { label: "종가", stroke: "#1565c0", width: 1 },
        ],
        cursor: { drag: { x: true, y: false, setScale: true } },
        hooks: {
          draw: [(u) => drawMarkers(u, DATA.buy_markers, DATA.sell_markers)],
        },
      };
      overviewPlot = new uPlot(opts, [DATA.times, DATA.close], document.getElementById("overview"));
      if (prev && keepRange) setOverviewXRange(prev.min, prev.max);
      else fitOverview();
    }

    function sliceLastDays(days) {
      const cutoff = DATA.times[DATA.times.length - 1] - days * 86400;
      let start = 0;
      for (let i = DATA.times.length - 1; i >= 0; i--) {
        if (DATA.times[i] < cutoff) { start = i + 1; break; }
      }
      return { start, end: DATA.times.length };
    }

    function buildDetailCandles(startIdx, endIdx) {
      lastDetailStart = startIdx;
      const end = endIdx || DATA.times.length;
      document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
      const wrap = document.getElementById("detail");
      wrap.innerHTML = "";
      const priceAxisW = yAxisLabelWidth();
      detailChart = LightweightCharts.createChart(wrap, {
        layout: { background: { color: "#fff" }, textColor: "#333", fontSize: 14 },
        grid: { vertLines: { color: "#eee" }, horzLines: { color: "#eee" } },
        rightPriceScale: { visible: false },
        leftPriceScale: {
          borderVisible: true,
          minimumWidth: priceAxisW,
          scaleMargins: { top: 0.08, bottom: 0.08 },
        },
        timeScale: { timeVisible: true, secondsVisible: false },
        width: wrap.clientWidth,
        height: 360,
      });
      detailSeries = detailChart.addCandlestickSeries({
        priceScaleId: "left",
        upColor: "#c62828", downColor: "#1565c0",
        borderUpColor: "#c62828", borderDownColor: "#1565c0",
        wickUpColor: "#c62828", wickDownColor: "#1565c0",
      });
      const candles = [];
      for (let i = startIdx; i < end; i++) {
        candles.push({
          time: DATA.times[i],
          open: DATA.open[i], high: DATA.high[i],
          low: DATA.low[i], close: DATA.close[i],
        });
      }
      detailSeries.setData(candles);
      const t0 = DATA.times[startIdx];
      const t1 = DATA.times[end - 1];
      const markers = [];
      if (showMarkers) {
        DATA.buy_markers.forEach(m => {
          if (m.time >= t0 && m.time <= t1) markers.push({
            time: m.time, position: "belowBar",
            color: m.signal_type === "breakout" ? "#ef6c00"
              : m.signal_type === "div_bull" ? "#7b1fa2" : "#2e7d32",
            shape: "arrowUp", size: 3,
            text: "B" + m.marker_id + markerSuffix(m.signal_type),
          });
        });
        DATA.sell_markers.forEach(m => {
          if (m.time >= t0 && m.time <= t1) markers.push({
            time: m.time, position: "aboveBar",
            color: m.signal_type === "div_bear" ? "#7b1fa2" : "#c62828",
            shape: "arrowDown", size: 3,
            text: "S" + m.marker_id + markerSuffix(m.signal_type),
          });
        });
      }
      markers.sort((a, b) => a.time - b.time);
      detailSeries.setMarkers(markers);
      detailChart.timeScale().fitContent();
    }

    function setActive(btnId) {
      document.querySelectorAll(".btn-period").forEach(b => b.classList.remove("active"));
      const el = document.getElementById(btnId);
      if (el) el.classList.add("active");
    }

    function goHome() {
      currentMode = "overview";
      setActive("btn-all");
      document.getElementById("overview").style.display = "block";
      if (!overviewPlot) buildOverview(false);
      else fitOverview();
      document.getElementById("status").textContent =
        `전체 ${DATA.bar_count.toLocaleString()}봉 | 드래그=줌, 더블클릭=리셋`;
    }

    function nearestSellAfter(buyTime) {
      let best = null;
      for (const s of DATA.sell_markers) {
        if (s.time >= buyTime && (!best || s.time < best.time)) best = s;
      }
      return best || DATA.sell_markers[DATA.sell_markers.length - 1];
    }

    function jumpToLeg(idx) {
      const total = DATA.buy_markers.length;
      if (!total) return;
      currentLegIdx = Math.max(0, Math.min(idx, total - 1));
      updateLegInfo();
      const buy = DATA.buy_markers[currentLegIdx];
      const sell = nearestSellAfter(buy.time);
      const span = sell ? Math.max(sell.time - buy.time, 86400) : 86400 * 3;
      const pad = span * 0.4;
      const vmin = buy.time - pad;
      const vmax = (sell ? sell.time : buy.time) + pad;

      currentMode = "overview";
      setActive("btn-all");
      document.getElementById("overview").style.display = "block";
      if (!overviewPlot) buildOverview(false);
      setOverviewXRange(vmin, vmax);

      let start = 0;
      for (let i = 0; i < DATA.times.length; i++) {
        if (DATA.times[i] >= vmin) { start = i; break; }
      }
      let end = DATA.times.length;
      for (let i = DATA.times.length - 1; i >= 0; i--) {
        if (DATA.times[i] <= vmax) { end = i + 1; break; }
      }
      const buyLabel = buy.signal_type === "pullback" ? "눌림목 매수"
        : buy.signal_type === "breakout" ? "돌파 매수"
        : buy.signal_type === "div_bull" ? "다이버전스 매수" : "스윙 매수";
      document.getElementById("detail-title").textContent =
        `B${buy.marker_id} ${buyLabel} — ${new Date(buy.time * 1000).toLocaleString("ko-KR")}`;
      buildDetailCandles(start, end);
      const sellText = sell ? ` → 매도 ${fmtPrice(sell.price)}` : "";
      document.getElementById("status").textContent =
        `B${buy.marker_id} ${buyLabel} ${fmtPrice(buy.price)}${sellText}`;
    }

    function showPeriod(days, btnId, label) {
      currentMode = "detail";
      setActive(btnId);
      detailVisible = true;
      document.getElementById("btn-toggle-detail").textContent = "상세 숨김";
      const { start } = sliceLastDays(days);
      document.getElementById("detail-title").textContent =
        `${label} 3분봉 캔들 (${(DATA.times.length - start).toLocaleString()}봉)`;
      buildDetailCandles(start);
      document.getElementById("overview").style.display = "block";
      if (!overviewPlot) buildOverview(false);
      const t0 = DATA.times[start];
      setOverviewXRange(t0, DATA.times[DATA.times.length - 1]);
      document.getElementById("status").textContent = `${label} 구간 표시`;
    }

    function applyZoom(factor) {
      if (currentMode === "detail" && detailChart) {
        const ts = detailChart.timeScale();
        const r = ts.getVisibleLogicalRange();
        if (!r) return;
        const mid = (r.from + r.to) / 2;
        const half = Math.max((r.to - r.from) * factor / 2, 10);
        ts.setVisibleLogicalRange({ from: mid - half, to: mid + half });
      } else if (overviewPlot) {
        zoomOverview(factor);
      }
    }

    function applyFit() {
      if (currentMode === "detail" && detailChart) {
        detailChart.timeScale().fitContent();
      } else if (overviewPlot) {
        fitOverview();
      }
    }

    function init() {
      DATA = window.CHART_DATA;
      if (!DATA) throw new Error("차트 데이터 JS 없음");
      const m = DATA.meta;
      const chartDays = m.chart_lookback_days || m.lookback_days;
      const gtDays = m.gt_lookback_days || m.lookback_days;
      const chartLabel = chartDays >= 365 ? `${Math.round(chartDays / 365)}년` : `${chartDays}일`;
      const gtLabel = gtDays >= 365 ? `${Math.round(gtDays / 365)}년` : `${gtDays}일`;
      const tier = m.chart_tier ? ` ${m.chart_tier.toUpperCase()}` : "";
      document.getElementById("title").textContent =
        `${m.symbol} Ground Truth${tier} (${m.interval_label}) — 차트 ${chartLabel} / GT ${gtLabel}`;
      document.getElementById("btn-all").textContent = `전체 ${chartLabel}`;
      const chartFrom = m.chart_data_from || m.data_from;
      const chartTo = m.chart_data_to || m.data_to;
      const tierKey = (m.chart_tier || "v3").toLowerCase();
      const legend = tierKey === "v1"
        ? "B=스윙매수 S=스윙매도"
        : tierKey === "v2"
          ? "B/S=스윙 B*=눌림목"
          : "B/S=스윙 B*=눌림 B^=돌파 Bd/Sd=다이버전스";
      document.getElementById("meta").textContent =
        `차트 ${chartFrom} ~ ${chartTo} (${DATA.bar_count.toLocaleString()}봉) | 매수 ${DATA.buy_markers.length} / 매도 ${DATA.sell_markers.length} (${gtLabel}) | ${legend}`;
      updateLegInfo();
      document.getElementById("status").textContent =
        `전체 ${DATA.bar_count.toLocaleString()}봉 | 드래그=줌, 더블클릭=리셋`;

      buildOverview(false);

      document.getElementById("btn-home").onclick = goHome;
      document.getElementById("btn-prev-leg").onclick = () => jumpToLeg(currentLegIdx - 1);
      document.getElementById("btn-next-leg").onclick = () => jumpToLeg(currentLegIdx + 1);
      document.getElementById("btn-all").onclick = goHome;
      document.getElementById("btn-365d").onclick = () => showPeriod(365, "btn-365d", "최근 1년");
      document.getElementById("btn-30d").onclick = () => showPeriod(30, "btn-30d", "최근 30일");
      document.getElementById("btn-7d").onclick = () => showPeriod(7, "btn-7d", "최근 7일");
      document.getElementById("btn-3d").onclick = () => showPeriod(3, "btn-3d", "최근 3일");
      document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
      document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
      document.getElementById("btn-fit").onclick = applyFit;
      document.getElementById("btn-markers").onclick = () => {
        showMarkers = !showMarkers;
        document.getElementById("btn-markers").textContent = showMarkers ? "마커 숨김" : "마커 표시";
        if (overviewPlot) buildOverview(true);
        if (detailChart) buildDetailCandles(lastDetailStart);
      };
      document.getElementById("btn-toggle-detail").onclick = () => {
        detailVisible = !detailVisible;
        document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
        document.getElementById("btn-toggle-detail").textContent = detailVisible ? "상세 숨김" : "상세 패널";
        if (detailVisible && !detailChart) {
          const { start } = sliceLastDays(7);
          buildDetailCandles(start);
        }
      };

      document.getElementById("overview").addEventListener("dblclick", () => {
        if (currentMode === "overview") fitOverview();
      });

      window.addEventListener("resize", () => {
        if (overviewPlot) buildOverview(true);
      });
    }
    try { init(); } catch (err) {
      document.getElementById("status").textContent = "데이터 로드 실패: " + err;
    }
  </script>
</body>
</html>"""


def _html_template(data_js_name: str) -> str:
    """차트 HTML 템플릿을 생성한다."""
    return _HTML_TEMPLATE.replace("__DATA_JS_NAME__", data_js_name)
