/* Reezer · PriceChart
   A candlestick chart in the shape traders expect: timeframe buttons, an OHLC legend that
   follows the crosshair, a volume pane, and a dashed line at what YOU paid.

   Drawn with TradingView's Lightweight Charts (Apache-2.0, loaded from unpkg like React and
   Supabase already are) — but on OUR data. `market_candles` holds the venue's own OHLCV:
   IBKR for the swings world, Revolut X for crypto. That distinction matters. A TradingView
   *widget* would draw TradingView's feed for the ticker: a different venue, a different
   session, no idea where you bought, and a price that can disagree with the P&L printed
   beside it. Here the candles come from the same venue that holds the position.

   What we cannot copy from Revolut X's screen is the drawing toolbar and the indicator
   menu — those belong to TradingView's Advanced Charts library, which is a separate,
   application-only product. Everything else is here. */


/* touch-action does not inherit, so setting it on the wrapper alone did nothing — the finger
   lands on the chart's canvas. pan-y hands vertical swipes to the page and leaves horizontal
   ones to the chart. In fullscreen there is no page to scroll, so the chart takes everything. */
(function () {
  if (document.getElementById("nt-chart-css")) return;
  const st = document.createElement("style");
  st.id = "nt-chart-css";
  st.textContent =
    ".nt-chart, .nt-chart *, .nt-chart canvas { touch-action: pan-y; }" +
    ".nt-chart-full, .nt-chart-full *, .nt-chart-full canvas { touch-action: none; }";
  document.head.appendChild(st);
})();

function NT_PriceChart({ symbol, world, entry, entryTs, ccy, height, compact, interactive }) {
  const TFS = [
    { m: 15, label: "15m" },
    { m: 60, label: "1h" },
    { m: 240, label: "4h" },
    { m: 1440, label: "D" },
    { m: 10080, label: "W" },
  ];
  // Every execution behind this holding. The dashed line is the AVERAGE; these are the real
  // buys and sells it averages, so the chart can mark each one where it happened.
  const [fills, setFills] = React.useState([]);
  React.useEffect(() => {
    const db = window.NT_CLIENT;
    if (!db || !symbol) { setFills([]); return; }
    db.from("fills").select("side,qty,price,ts").eq("symbol", symbol)
      .order("ts", { ascending: true }).limit(200)
      .then((r) => setFills((r && r.data) || []), () => setFills([]));
  }, [symbol]);

  const [tf, setTf] = React.useState(1440);
  const [rows, setRows] = React.useState(null);
  const [full, setFull] = React.useState(false);
  const holder = React.useRef(null);
  const wrap = React.useRef(null);
  const chartRef = React.useRef(null);

  const cur = (v, dp) => {
    if (v == null || !isFinite(v)) return "—";
    const s = ccy === "EUR" ? "€" : ccy === "GBP" ? "£" : "$";
    return s + Number(v).toFixed(dp != null ? dp : (Math.abs(v) < 10 ? 4 : 2));
  };

  const [pulse, setPulse] = React.useState(0);
  // Blank only when the subject changes; a background refresh must not flash the chart away.
  const fpRef = React.useRef("");
  React.useEffect(() => { setRows(null); fpRef.current = ""; }, [symbol, tf]);
  // The bot tops the candles up on its own cycle; this picks them up without a page reload.
  React.useEffect(() => {
    const h = () => setPulse((n) => n + 1);
    window.addEventListener("nt-data", h);
    return () => window.removeEventListener("nt-data", h);
  }, []);

  React.useEffect(() => {
    let dead = false;
    if (!symbol) { setRows([]); return; }
    (async () => {
      try {
        const db = window.NT_CLIENT;
        const r = await db.from("market_candles").select("ts,o,h,l,c,v")
          .eq("symbol", symbol).eq("interval_min", tf)
          .order("ts", { ascending: false }).limit(400);
        const got = ((r && r.data) || []).slice().reverse();
        // A freshly held coin can have no bars at the shown timeframe yet (the 1440/Daily
        // default needs a full day of venue history, and the bot's first candle refresh may
        // not have landed). Instead of dead-ending on "No candles … at this timeframe",
        // switch to a timeframe that DOES have candles. We only ever switch TO an interval
        // that has data, so this cannot loop.
        if (got.length < 2) {
          const a = await db.from("market_candles").select("interval_min")
            .eq("symbol", symbol).limit(1000);
          const avail = new Set(((a && a.data) || []).map((x) => x.interval_min));
          const pick = [1440, 240, 60, 15, 10080].find((m) => avail.has(m));
          if (!dead && pick != null && pick !== tf) { setTf(pick); return; }
        }
        // Only replace state when a candle actually CHANGED. Every nt-data pulse (a few
        // seconds apart) lands here, and a fresh-but-identical array used to tear down and
        // rebuild the whole chart — on a phone that read as the page refreshing and
        // jumping to the top every few seconds, on every page with one of these charts.
        const last = got[got.length - 1];
        const fp = got.length + "|" + (last ? last.ts + "|" + last.c + "|" + last.h + "|" + last.l + "|" + last.v : "");
        if (!dead && fp !== fpRef.current) { fpRef.current = fp; setRows(got); }
      } catch (e) {
        if (!dead) setRows([]);
      }
    })();
    return () => { dead = true; };
  }, [symbol, tf, pulse]);

  React.useEffect(() => {
    const LC = window.LightweightCharts;
    const el = holder.current;
    if (!LC || !el || !rows || rows.length < 2) return;

    const css = getComputedStyle(document.documentElement);
    const v = (n, f) => (css.getPropertyValue(n) || "").trim() || f;
    const phone = (window.NT_IS_MOBILE ? window.NT_IS_MOBILE()
                                       : window.matchMedia("(max-width: 900px)").matches);
    const up = v("--profit", "#21C77A"), down = v("--loss", "#F0454B");
    const grid = v("--border", "rgba(255,255,255,0.11)");
    const inset = v("--surface-inset", "#1B1B1F");

    // Lightweight Charts plots a Unix stamp as if it were UTC. Shifting each bar by its own
    // offset makes the axis read in local time and still survives a DST change mid-range.
    const local = (iso) => {
      const d = new Date(iso);
      return Math.floor((d.getTime() - d.getTimezoneOffset() * 60000) / 1000);
    };

    const seen = new Set();
    const bars = [];
    rows.forEach((r) => {
      const t = local(r.ts);
      const c = Number(r.c);
      if (!t || !isFinite(c) || seen.has(t)) return;
      seen.add(t);
      bars.push({
        time: t,
        open: Number(r.o != null ? r.o : c), high: Number(r.h != null ? r.h : c),
        low: Number(r.l != null ? r.l : c), close: c,
        volume: r.v != null ? Number(r.v) : null,
      });
    });
    if (bars.length < 2) return;

    const chart = LC.createChart(el, {
      width: el.clientWidth,
      height: full ? Math.max(320, window.innerHeight - 150) : (height || 340),
      layout: {
        background: { type: "solid", color: "transparent" },
        textColor: v("--text-tertiary", "#C9C9D2"),
        fontFamily: v("--font-sans", "system-ui"),
        fontSize: 11,
      },
      // No grid: the candles carry the shape, the lines were only noise behind them.
      grid: { vertLines: { visible: false }, horzLines: { visible: false } },
      rightPriceScale: { borderColor: grid, scaleMargins: { top: 0.10, bottom: 0.26 } },
      // A little empty space at the right so the buy-in tag is not sitting on the candles.
      // Kept in step with PAD below, which is what actually decides it.
      timeScale: { borderColor: grid, timeVisible: tf < 1440, secondsVisible: false,
                   rightOffset: 16, fixLeftEdge: true },
      crosshair: {
        mode: LC.CrosshairMode.Normal,
        vertLine: { color: v("--text-tertiary", "#C9C9D2"), width: 1, style: 2, labelBackgroundColor: inset },
        horzLine: { color: v("--text-tertiary", "#C9C9D2"), width: 1, style: 2, labelBackgroundColor: inset },
      },
      // On a phone the chart must NOT eat the swipe: a finger dragged over it has to scroll
      // the page, or the page dead-ends on the chart. Panning the chart by touch is therefore
      // only enabled in fullscreen, where there is no page to scroll.
      handleScroll: { mouseWheel: false, pressedMouseMove: true,
                      horzTouchDrag: full || !phone || interactive,
                      // Mirror of horzTouchDrag: with it off, a finger on the price axis (and any
                      // vertical drag) did nothing. On in the interactive popup, where the page
                      // behind is locked, so it can't fight the page scroll.
                      vertTouchDrag: full || !phone || interactive },
      // Dragging either axis rescales the chart, the way every trading chart behaves.
      handleScale: { mouseWheel: true, pinch: true,
                     axisPressedMouseMove: { time: true, price: true } },
      // The library defaults to the browser's language, which printed a Dutch month on the
      // axis. Reezer is in English everywhere.
      localization: { locale: "en-GB", priceFormatter: (p) => cur(p) },
    });

    const candles = chart.addCandlestickSeries({
      upColor: up, downColor: down, borderVisible: false,
      wickUpColor: up, wickDownColor: down,
      priceLineVisible: true, priceLineColor: v("--text-tertiary", "#C9C9D2"), priceLineStyle: 2,
    });
    candles.setData(bars);

    const vol = chart.addHistogramSeries({
      priceFormat: { type: "volume" }, priceScaleId: "vol", lastValueVisible: false,
    });
    vol.setData(bars.filter((b) => b.volume != null).map((b) => ({
      time: b.time, value: b.volume,
      color: b.close >= b.open ? v("--profit-bg", "rgba(33,199,122,.14)") : v("--loss-bg", "rgba(240,69,75,.14)"),
    })));
    try {
      chart.priceScale("vol").applyOptions({ scaleMargins: { top: 0.80, bottom: 0 }, visible: false });
    } catch (e) {}

    // What you paid — the one line a stock-ticker widget can never draw.
    const base = Number(entry);
    if (isFinite(base) && base > 0) {
      candles.createPriceLine({
        price: base, color: v("--violet-400", "#8676F5"), lineWidth: 1,
        lineStyle: LC.LineStyle.Dashed, axisLabelVisible: true, title: "buy-in",
      });
    }

    // A mark for every real execution. The line says what you paid on average; these say when
    // each buy actually happened and how much of it. Falls back to the position's open time
    // when we have no stored fills for it (anything bought before we started keeping them).
    {
      const nearest = (iso) => {
        const want = local(iso);
        if (!want) return null;
        let best = null, gap = Infinity;
        bars.forEach((b) => { const g = Math.abs(b.time - want); if (g < gap) { gap = g; best = b.time; } });
        return best;
      };
      // Just "buy" / "sell". The size used to ride along ("bought 0.001") and on a busy
      // chart those labels overlapped each other and the price scale.
      let marks = (fills || []).map((f) => {
        const t = nearest(f.ts);
        if (t == null) return null;
        const sell = String(f.side).toLowerCase() === "sell";
        return { time: t, position: sell ? "aboveBar" : "belowBar",
                 shape: sell ? "arrowDown" : "arrowUp",
                 color: sell ? v("--loss", "#F0454B") : v("--violet-400", "#8676F5"),
                 text: sell ? "sell" : "buy" };
      }).filter(Boolean);
      if (!marks.length && entryTs) {
        const t = nearest(entryTs);
        if (t != null) marks = [{ time: t, position: "belowBar", shape: "arrowUp",
                                  color: v("--violet-400", "#8676F5"), text: "buy" }];
      }
      // Several fills can land on one bar at a coarse timeframe; the library wants them sorted
      // and will only show one per bar, so merge those into a single mark.
      const byBar = {};
      marks.forEach((m) => { byBar[m.time] = byBar[m.time] ? { ...m, text: m.text + " +" } : m; });
      const merged = Object.values(byBar).sort((a, b) => a.time - b.time);
      if (merged.length) candles.setMarkers(merged);
    }

    chartRef.current = chart;
    // 400 daily candles squeezed into one panel reads as a flat line. Open on the recent
    // stretch instead; scrolling and pinching still reach the whole history.
    //
    // NOTE: this sets the visible range EXPLICITLY, which overrides rightOffset — asking for
    // "to: bars.length + 2" pins two bars of empty space at the right no matter what rightOffset
    // says. The gap the buy-in label needs has to be requested here.
    const PAD = 16;     // enough for the buy-in tag to clear the last candles, not a third of the panel
    const WINDOW = full ? 260 : 130;
    if (bars.length > WINDOW) {
      try {
        chart.timeScale().setVisibleLogicalRange({ from: bars.length - WINDOW, to: bars.length + PAD });
      } catch (e) {}
    } else {
      try {
        chart.timeScale().setVisibleLogicalRange({ from: 0, to: bars.length + PAD });
      } catch (e) {}
    }

    const onResize = () => { try { chart.applyOptions({ width: el.clientWidth }); } catch (e) {} };
    window.addEventListener("resize", onResize);
    return () => {
      window.removeEventListener("resize", onResize);
      try { chart.remove(); } catch (e) {}
      chartRef.current = null;
    };
  }, [rows, entry, ccy, height, tf, full, fills]);

  // Fullscreen. Three things can happen and the button has to work in all of them:
  // the standard API, Safari's webkit-prefixed one, or a browser that refuses outright —
  // in which case we maximise inside the page instead, so the button is never dead.
  const [overlay, setOverlay] = React.useState(false);

  React.useEffect(() => {
    const onChange = () => {
      const el = document.fullscreenElement || document.webkitFullscreenElement || null;
      setFull(!!el && el === wrap.current);
    };
    document.addEventListener("fullscreenchange", onChange);
    document.addEventListener("webkitfullscreenchange", onChange);
    return () => {
      document.removeEventListener("fullscreenchange", onChange);
      document.removeEventListener("webkitfullscreenchange", onChange);
    };
  }, []);

  // Esc leaves the in-page fallback too, the way it leaves real fullscreen.
  React.useEffect(() => {
    if (!overlay) return;
    const onKey = (e) => { if (e.key === "Escape") { setOverlay(false); setFull(false); } };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [overlay]);

  const toggleFull = () => {
    const el = wrap.current;
    if (!el) return;
    const isFull = (document.fullscreenElement || document.webkitFullscreenElement) === el;
    if (isFull || overlay) {
      try {
        if (document.exitFullscreen) document.exitFullscreen();
        else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
      } catch (e) {}
      setOverlay(false);
      setFull(false);
      return;
    }
    const req = el.requestFullscreen || el.webkitRequestFullscreen;
    if (!req) { setOverlay(true); setFull(true); return; }
    try {
      const p = req.call(el);
      if (p && p.catch) p.catch(() => { setOverlay(true); setFull(true); });
    } catch (e) {
      setOverlay(true);
      setFull(true);
    }
  };


  const tfBtn = (t) => (
    <button key={t.m} type="button" onClick={() => setTf(t.m)}
      style={{ height: 26, minWidth: 34, padding: "0 9px", borderRadius: "var(--radius-xs)", cursor: "pointer",
               border: "1px solid " + (tf === t.m ? "var(--violet-line)" : "transparent"),
               background: tf === t.m ? "var(--violet-soft)" : "transparent",
               color: tf === t.m ? "var(--violet-400)" : "var(--text-tertiary)",
               font: "var(--w-medium) var(--t-2xs)/1 var(--font-sans)" }}>
      {t.label}
    </button>
  );

  const note = (text) => (
    <div style={{ height: (height || 340), display: "flex", alignItems: "center", justifyContent: "center",
                  font: "var(--w-regular) var(--t-xs)/1 var(--font-sans)", color: "var(--text-tertiary)" }}>{text}</div>
  );

  return (
    <div ref={wrap}
      style={overlay ? { position: "fixed", inset: 0, zIndex: 2000, background: "var(--bg-app)", height: "100dvh", overflow: "hidden",
                         padding: "calc(env(safe-area-inset-top,0px) + 16px) calc(env(safe-area-inset-right,0px) + 16px) calc(env(safe-area-inset-bottom,0px) + 16px) calc(env(safe-area-inset-left,0px) + 16px)" }
             : full ? { background: "var(--bg-app)", padding: 24, height: "100%", overflow: "hidden" } : null}>
      <div style={{ display: "flex", alignItems: "center", gap: 4, flexWrap: "wrap", marginBottom: 10 }}>
        {TFS.map(tfBtn)}
        <span style={{ marginLeft: "auto", font: "var(--w-regular) var(--t-2xs)/1 var(--font-sans)", color: "var(--text-tertiary)" }}>
          {world === "crypto" ? "Revolut X" : "IBKR"}
        </span>
        <button type="button" onClick={toggleFull} title={full ? "Leave fullscreen (Esc)" : "Fullscreen"}
          style={{ height: 26, width: 28, display: "inline-flex", alignItems: "center", justifyContent: "center",
                   marginLeft: 4, borderRadius: "var(--radius-xs)", cursor: "pointer",
                   border: "1px solid var(--border)", background: "transparent", color: "var(--text-tertiary)" }}>
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
            {full
              ? <g><path d="M9 3v6H3" /><path d="M15 3v6h6" /><path d="M9 21v-6H3" /><path d="M15 21v-6h6" /></g>
              : <g><path d="M3 9V3h6" /><path d="M21 9V3h-6" /><path d="M3 15v6h6" /><path d="M21 15v6h-6" /></g>}
          </svg>
        </button>
      </div>

      {/* The OHLC + volume readout used to sit here. Removed at Gianni's request: it was a
          line of numbers above every chart, and the crosshair already labels the price. */}

      {rows === null ? note("Loading candles…")
        : rows.length < 2 ? note("No candles stored for " + symbol + " yet at this timeframe.")
        : !window.LightweightCharts ? note("Chart library did not load.")
        : <div ref={holder} className={full ? "nt-chart nt-chart-full" : "nt-chart"}
               style={{ width: "100%" }} />}

      {!compact && rows && rows.length > 1 && (
        <div style={{ marginTop: 8, display: "flex", gap: 14, flexWrap: "wrap",
                      font: "var(--w-regular) var(--t-2xs)/1 var(--font-sans)", color: "var(--text-tertiary)" }}>
          <span>{rows.length} candles</span>
          {isFinite(Number(entry)) && Number(entry) > 0 && (
            <span style={{ color: "var(--violet-400)" }}>dashed line = your buy-in {cur(Number(entry))}</span>
          )}
          <span style={{ marginLeft: "auto" }}>times shown in your local timezone</span>
        </div>
      )}
    </div>
  );
}

window.NT_PriceChart = NT_PriceChart;
