// run_list_state_poll.js — bulk polling for the runs list.
//
// Contract:
//  - reads the active-run pks from data-row-pk + data-row-state on
//    every <tr>; only non-terminal rows are sent to the endpoint.
//  - on any state transition (server-reported state differs from the
//    initially-rendered data-row-state), reload the full page so the
//    server-side rendered badges / duration / pagination stay the
//    single source of truth.
//  - polling pauses when document.visibilityState !== "visible" and
//    stops once no active rows remain.

(function () {
  "use strict";

  var table = document.querySelector("[data-active-runs]");
  if (!table) {
    return;
  }
  var pollUrl = table.getAttribute("data-poll-url");
  if (!pollUrl) {
    return;
  }
  var rawInterval = parseInt(
    table.getAttribute("data-poll-interval") || "10000",
    10
  );
  // Floor against NaN / 0 / negative: a malformed data-poll-interval must
  // not coerce setInterval into a 0ms request flood. The config comes
  // from a data-* attribute, so the JS owns this policy decision.
  var pollInterval =
    isFinite(rawInterval) && rawInterval >= 1000 ? rawInterval : 10000;
  var terminalStates = (table.getAttribute("data-terminal-states") || "")
    .split(",")
    .filter(function (s) {
      return s.length > 0;
    });

  var controller = null;
  var timerId = null;

  function stop() {
    if (timerId !== null) {
      clearInterval(timerId);
      timerId = null;
    }
    if (controller !== null) {
      controller.abort();
      controller = null;
    }
  }

  function activeRowPks() {
    var rows = table.querySelectorAll("tr[data-row-pk][data-row-state]");
    var pks = [];
    for (var i = 0; i < rows.length; i++) {
      var state = rows[i].getAttribute("data-row-state");
      if (terminalStates.indexOf(state) === -1) {
        pks.push(rows[i].getAttribute("data-row-pk"));
      }
    }
    return pks;
  }

  function applyPayload(payload) {
    if (!payload || !payload.runs) {
      return;
    }
    for (var i = 0; i < payload.runs.length; i++) {
      var run = payload.runs[i];
      var row = table.querySelector('tr[data-row-pk="' + run.pk + '"]');
      if (row === null) {
        continue;
      }
      var initial = row.getAttribute("data-row-state");
      if (initial !== run.state) {
        stop();
        window.location.reload();
        return;
      }
    }
  }

  function tick() {
    if (document.visibilityState !== "visible") {
      return;
    }
    var pks = activeRowPks();
    if (pks.length === 0) {
      stop();
      return;
    }
    if (controller !== null) {
      controller.abort();
    }
    controller = new AbortController();
    fetch(pollUrl + "?ids=" + pks.join(","), {
      credentials: "same-origin",
      signal: controller.signal,
    })
      .then(function (response) {
        if (!response.ok) {
          // Breadcrumb for a persistent failure (403 after a permission
          // revoke, or a session-expiry redirect to an HTML login).
          // Keep polling — the server stays the source of truth — but an
          // audit tool must not go blind silently.
          console.warn(
            "run_list_state_poll: runs/state.json HTTP " + response.status
          );
          return null;
        }
        return response.json();
      })
      .then(applyPayload)
      .catch(function (err) {
        // Abort is the normal teardown path; warn only on real errors
        // (network failure, or a non-JSON body from a login redirect).
        if (err && err.name !== "AbortError") {
          console.warn("run_list_state_poll: poll failed", err);
        }
      });
  }

  timerId = setInterval(tick, pollInterval);
  document.addEventListener("pagehide", stop);
})();
