// History overlay: a native <dialog> with one large chart, opened from the
// expand button on any chart card or by drag-selecting a span on a small live
// chart. Shows the median as a line and the min-max envelope as a shaded band,
// fetched once per view from the history endpoint (no SSE involved - this view
// is deliberately static and separate from the live machinery in live.js).
//
// A view is either a named range ({range: "24h"}) or a free window
// ({sinceMs, untilMs}). Drag-selecting inside the overlay drills into the
// selected window (re-fetched at finer resolution); double-click goes back.
(function () {
  "use strict";

  var dlg = document.getElementById("history-dialog");
  var cfg = document.getElementById("seed-data");
  var historyUrl = cfg && cfg.getAttribute("data-history-url");
  if (!dlg || !historyUrl || typeof uPlot === "undefined") return;

  var chartEl = document.getElementById("history-chart");
  var titleEl = document.getElementById("history-title");
  var readoutEl = document.getElementById("history-readout");
  var rangesEl = document.getElementById("history-ranges");
  var hintEl = document.getElementById("history-hint");

  var stroke = "#4f9cf9";
  var band = "rgba(79,156,249,0.18)";
  var edge = "rgba(79,156,249,0.35)";
  // Second series (tx / write) on the dual-line I/O charts, in green.
  var strokeOut = "#3fb950";
  var bandOut = "rgba(63,185,80,0.18)";
  var edgeOut = "rgba(63,185,80,0.35)";
  var grid = { stroke: "#2c313c", width: 1 };
  var axisStyle = { stroke: "#8b93a1", grid: grid, ticks: grid };

  var pad = function (n) { return n < 10 ? "0" + n : "" + n; };
  var cpuFmt = function (v) { return v.toFixed(1) + "%"; };
  // Readout precision: a container's memory often moves within a few MiB, so
  // the coarse one-decimal format of the live charts would render the whole
  // envelope as the same number.
  var memFmt = function (v) {
    return v >= 1024 ? (v / 1024).toFixed(2) + "G" : v.toFixed(1) + "M";
  };

  // Axis ticks need adaptive precision for the same reason: with a tight
  // value range every tick would round to the same label. Derive the number
  // of decimals from the tick spacing so adjacent labels always differ.
  function memTicks(vals) {
    if (!vals.length) return vals;
    var step = vals.length > 1 ? vals[1] - vals[0] : 1; // MiB
    // Sub-MiB tick spacing would need >3 decimals in G, so stay in MiB then.
    var inG = vals[vals.length - 1] >= 1024 && step >= 1;
    var div = inG ? 1024 : 1;
    var s = step / div;
    var dec = s >= 1 ? 0 : Math.min(3, Math.ceil(-Math.log10(s)));
    return vals.map(function (v) { return (v / div).toFixed(dec) + (inG ? "G" : "M"); });
  }
  function cpuTicks(vals) {
    return vals.map(function (v) { return v.toFixed(1) + "%"; });
  }
  // Bytes/second, for the network and disk-I/O history charts.
  function rateFmt(v) {
    if (v == null) return "--";
    if (v >= 1048576) return (v / 1048576).toFixed(2) + " MB/s";
    if (v >= 1024) return (v / 1024).toFixed(1) + " KB/s";
    return Math.round(v) + " B/s";
  }
  function rateTicks(vals) {
    return vals.map(rateFmt);
  }

  // Per-metric rendering config. `keys` are the HistoryPoint field prefixes
  // (each has _min/_med/_max); a second key makes the chart dual-line. `scale`
  // converts stored units to chart units (memory bytes -> MiB). `labels`
  // prefix the dual-line readout ("rx 1.2 MB/s · tx …").
  var METRICS = {
    cpu: { keys: ["cpu"], fmt: cpuFmt, ticks: cpuTicks, scale: 1 },
    mem: { keys: ["mem"], fmt: memFmt, ticks: memTicks, scale: 1 / 1048576 },
    net: { keys: ["net_rx", "net_tx"], fmt: rateFmt, ticks: rateTicks, scale: 1, labels: ["rx", "tx"] },
    disk: { keys: ["disk_read", "disk_write"], fmt: rateFmt, ticks: rateTicks, scale: 1, labels: ["read", "write"] },
  };
  // Per-series colours, indexed by key position: rx/read blue, tx/write green.
  var SERIES_COLORS = [
    { line: stroke, edge: edge, band: band },
    { line: strokeOut, edge: edgeOut, band: bandOut },
  ];

  var metric = "cpu"; // which metric the open dialog shows
  var baseTitle = "";
  var lastRange = "24h"; // most recent named range; double-click fallback
  var view = { range: lastRange }; // what the chart currently shows
  var backStack = []; // drill-down trail, popped by double-click
  var chart = null;
  var loadSeq = 0; // ignore out-of-order fetch responses after rapid clicks
  var loadAbort = null; // in-flight fetch, cancelled when superseded or closed

  // Within a day the clock alone is enough; longer windows - and windows that
  // lie further in the past - need the day to be intelligible.
  function needsDate(v) {
    if (v.range) return v.range === "7d" || v.range === "30d";
    return v.untilMs - v.sinceMs > 72e6 || Date.now() - v.sinceMs > 864e5;
  }

  function timeStr(s, withDate) {
    var d = new Date(s * 1000);
    var clock = pad(d.getHours()) + ":" + pad(d.getMinutes());
    if (!withDate) return clock;
    return pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + " " + clock;
  }

  function windowLabel(v) {
    var withDate = needsDate(v);
    return timeStr(v.sinceMs / 1000, withDate) + " – " + timeStr(v.untilMs / 1000, withDate);
  }

  // Same idea as in live.js: break the line over stretches without data
  // instead of bridging them. Threshold adapts to the series spacing (raw
  // seconds vs downsampled buckets).
  function gapFromSpacing(times) {
    if (times.length < 3) return 15;
    var deltas = [];
    for (var i = 1; i < times.length; i++) deltas.push(times[i] - times[i - 1]);
    deltas.sort(function (a, b) { return a - b; });
    return Math.max(15, deltas[Math.floor(deltas.length / 2)] * 4);
  }

  // Build uPlot series + bands for the active metric's config. Each key adds a
  // median line plus its two band edges (max, min); the band fills between them.
  // Data layout: [ts, med0, max0, min0, (med1, max1, min1)].
  function seriesAndBands(cfg) {
    var series = [{}];
    var bands = [];
    cfg.keys.forEach(function (key, k) {
      var c = SERIES_COLORS[k];
      var medIdx = series.length;
      series.push({ stroke: c.line, width: 1.5 });
      series.push({ stroke: c.edge, width: 1 });
      series.push({ stroke: c.edge, width: 1 });
      bands.push({ series: [medIdx + 1, medIdx + 2], fill: c.band });
    });
    return { series: series, bands: bands };
  }

  function chartOpts(cfg) {
    var sb = seriesAndBands(cfg);
    return {
      width: chartEl.clientWidth || 800,
      height: 420,
      // Drag drills into the selected window via a re-fetch; uPlot's own
      // zoom would only stretch the already-loaded resolution.
      cursor: { y: false, drag: { x: true, y: false, setScale: false } },
      legend: { show: false },
      scales: { x: { time: true } },
      series: sb.series,
      bands: sb.bands,
      axes: [
        Object.assign({}, axisStyle, {
          size: 30,
          values: function (u, splits) {
            var withDate = needsDate(view);
            return splits.map(function (s) { return timeStr(s, withDate); });
          },
        }),
        Object.assign({}, axisStyle, {
          // Tight memory ranges produce long labels ("1299.4M"); a fixed
          // gutter clips their leading digits, so size to the longest label.
          size: function (u, values) {
            var longest = (values || []).reduce(function (a, b) {
              return b.length > a.length ? b : a;
            }, "");
            return Math.max(56, 16 + longest.length * 8);
          },
          values: function (u, vals) { return cfg.ticks(vals); },
        }),
      ],
      hooks: {
        setCursor: [function (u) {
          var i = u.cursor.idx;
          if (i == null || u.data[0][i] == null || u.data[1][i] == null) {
            readoutEl.textContent = "";
            return;
          }
          var parts = cfg.keys.map(function (key, k) {
            var base = 1 + k * 3;
            var med = u.data[base][i], hi = u.data[base + 1][i], lo = u.data[base + 2][i];
            var spread = lo != null && hi != null && hi - lo > 1e-9
              ? " (" + cfg.fmt(lo) + " – " + cfg.fmt(hi) + ")"
              : "";
            var label = cfg.labels ? cfg.labels[k] + " " : "";
            return label + cfg.fmt(med) + spread;
          });
          readoutEl.textContent =
            timeStr(u.data[0][i], needsDate(view)) + " · " + parts.join(" · ");
        }],
        // A completed drag selection drills into that window.
        setSelect: [function (u) {
          if (u.select.width <= 0) return;
          var sinceMs = Math.round(u.posToVal(u.select.left, "x") * 1000);
          var untilMs = Math.round(u.posToVal(u.select.left + u.select.width, "x") * 1000);
          u.setSelect({ left: 0, top: 0, width: 0, height: 0 }, false);
          drill(sinceMs, untilMs);
        }],
      },
    };
  }

  function render(points) {
    var cfg = METRICS[metric] || METRICS.cpu;
    var rawTs = points.map(function (p) { return Math.floor(p.ts_ms / 1000); });
    var gapSecs = gapFromSpacing(rawTs);
    // One column triple (med/max/min) per key, sharing the timeline.
    var ts = [];
    var cols = cfg.keys.map(function () { return { med: [], hi: [], lo: [] }; });
    for (var i = 0; i < points.length; i++) {
      if (ts.length && rawTs[i] - ts[ts.length - 1] > gapSecs) {
        ts.push(ts[ts.length - 1] + 1);
        cols.forEach(function (c) { c.med.push(null); c.hi.push(null); c.lo.push(null); });
      }
      ts.push(rawTs[i]);
      var p = points[i];
      cfg.keys.forEach(function (key, k) {
        var scaled = function (v) { return v == null ? null : v * cfg.scale; };
        cols[k].med.push(scaled(p[key + "_med"]));
        cols[k].hi.push(scaled(p[key + "_max"]));
        cols[k].lo.push(scaled(p[key + "_min"]));
      });
    }
    var data = [ts];
    cols.forEach(function (c) { data.push(c.med, c.hi, c.lo); });

    if (chart) { chart.destroy(); chart = null; }
    chartEl.textContent = points.length ? "" : "No data for this range.";
    if (points.length) {
      chart = new uPlot(chartOpts(cfg), data, chartEl);
    }
  }

  // Fetch and display a view. The chrome (title, active range button, hint)
  // updates immediately; the chart follows when the data lands.
  function show(v) {
    view = v;
    if (v.range) lastRange = v.range;
    readoutEl.textContent = "";
    titleEl.textContent = v.range ? baseTitle : baseTitle + " · " + windowLabel(v);
    var btns = rangesEl.querySelectorAll("button");
    for (var i = 0; i < btns.length; i++) {
      btns[i].classList.toggle("active", btns[i].getAttribute("data-range") === v.range);
    }
    hintEl.classList.toggle("show", !v.range);

    var query = v.range
      ? "?range=" + v.range
      : "?since_ms=" + v.sinceMs + "&until_ms=" + v.untilMs;
    var seq = ++loadSeq;
    // Abort the superseded fetch instead of letting it run to completion:
    // rapid range clicks would otherwise stack requests, each occupying a
    // browser connection (loadSeq only discards their late responses). The
    // aborted fetch rejects into the catch below, where its stale seq drops it.
    if (loadAbort) loadAbort.abort();
    loadAbort = new AbortController();
    fetch(historyUrl + query, { signal: loadAbort.signal })
      .then(function (resp) {
        if (!resp.ok) throw new Error("HTTP " + resp.status);
        return resp.json();
      })
      .then(function (points) {
        if (seq === loadSeq) render(points);
      })
      .catch(function () {
        if (seq !== loadSeq) return;
        if (chart) { chart.destroy(); chart = null; }
        chartEl.textContent = "Could not load history.";
      });
  }

  function drill(sinceMs, untilMs) {
    if (untilMs - sinceMs < 10000) return; // ignore accidental nudges
    backStack.push(view);
    show({ sinceMs: sinceMs, untilMs: untilMs });
  }

  // Double-click walks the drill-down trail back; from a window opened
  // directly off a small chart it falls back to the last named range.
  chartEl.addEventListener("dblclick", function () {
    if (backStack.length) show(backStack.pop());
    else if (!view.range) show({ range: lastRange });
  });

  rangesEl.addEventListener("click", function (e) {
    var btn = e.target.closest("button[data-range]");
    if (!btn) return;
    backStack = [];
    show({ range: btn.getAttribute("data-range") });
  });

  // Put the dialog into "showing <metric> of this page's entity" state. The
  // title borrows the matching chart card's title ("Host CPU", "Memory", ...),
  // which already names the metric.
  function openFor(m) {
    metric = m;
    var btn = document.querySelector('.chart-zoom[data-metric="' + m + '"]');
    var head = btn && btn.closest(".chart-head");
    var title = head && head.querySelector(".chart-title");
    baseTitle = (title ? title.textContent : m) + " — history";
    backStack = [];
    dlg.showModal();
  }

  // Open from any chart card's expand button.
  document.body.addEventListener("click", function (e) {
    var btn = e.target.closest(".chart-zoom");
    if (!btn) return;
    openFor(btn.getAttribute("data-metric") || "cpu");
    show({ range: lastRange });
  });

  // Open from a drag selection on a small live chart (see live.js).
  document.addEventListener("dockdoe:drill", function (e) {
    var d = e.detail;
    if (d.untilMs - d.sinceMs < 10000) return;
    openFor(d.metric);
    show({ sinceMs: d.sinceMs, untilMs: d.untilMs });
  });

  document.getElementById("history-close").addEventListener("click", function () {
    dlg.close();
  });
  // Native <dialog>: a click on the backdrop registers on the dialog element
  // itself (the padded content area is covered by .history-body).
  dlg.addEventListener("click", function (e) {
    if (e.target === dlg) dlg.close();
  });
  // Covers every way the dialog closes (button, backdrop, Escape): a fetch
  // for a view nobody is looking at shouldn't keep a connection busy.
  dlg.addEventListener("close", function () {
    loadSeq++;
    if (loadAbort) { loadAbort.abort(); loadAbort = null; }
  });

  window.addEventListener("resize", function () {
    if (chart && dlg.open) {
      chart.setSize({ width: chartEl.clientWidth, height: 420 });
    }
  });

  // Deep link: #history-cpu / #history-mem-7d opens the overlay on load, so a
  // view can be bookmarked or shared (and exercised by headless UI checks).
  // The range part is validated against the server-rendered range buttons
  // rather than a second hardcoded list, so a new range is one server-side
  // change; an unknown range falls back to the default.
  var hash = /^#history-(cpu|mem|net|disk)(?:-([a-z0-9]+))?$/.exec(location.hash);
  if (hash) {
    var known = hash[2] &&
      rangesEl.querySelector('button[data-range="' + hash[2] + '"]');
    openFor(hash[1]);
    show({ range: known ? hash[2] : lastRange });
  }
})();
