// run_state_poll.js — vanilla-JS polling for /runs/<pk>/state.json.
//
// Contract:
//  - while the run is non-terminal, each tick live-updates the
//    plain-text counter cells tagged data-poll-field="<key>" (the
//    keys match the state.json payload: members_scanned,
//    findings_count, actions_applied, actions_skipped).
//  - all DOM updates go through textContent only (anti-XSS — the
//    fields are server-controlled ints, but the rule keeps the code
//    review-able with one grep).
//  - polling pauses when document.visibilityState !== "visible".
//  - on the first server-reported terminal state, polling stops
//    (AbortController + clearInterval) and the page reloads so the
//    server re-renders the final state badge / duration and drops the
//    active-run spinner — badge markup stays server-owned, the same
//    rule run_list_state_poll.js follows.

// var (not const/let) for consistency with ES5 idioms; the real ES
// floor is set by AbortController (ES2017) and fetch (ES2015).
(function () {
  "use strict";

  var card = document.getElementById("run-state-card");
  if (!card) {
    return;
  }

  var pollUrl = card.getAttribute("data-poll-url");
  if (!pollUrl) {
    return;
  }
  var rawInterval = parseInt(
    card.getAttribute("data-poll-interval") || "5000",
    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 : 5000;
  var terminalStates = (card.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 updateField(fieldName, value) {
    var node = card.querySelector('[data-poll-field="' + fieldName + '"]');
    if (node === null) {
      return;
    }
    node.textContent = value === null || value === undefined ? "—" : value;
  }

  function applyPayload(payload) {
    // Terminal state: stop polling and reload so the server re-renders
    // the final badge / duration and removes the active-run spinner.
    // Patching the badge here instead would fork badge markup into JS.
    if (terminalStates.indexOf(payload.state) !== -1) {
      stop();
      window.location.reload();
      return;
    }
    // Non-terminal tick: live-update the counter cells in place.
    Object.keys(payload).forEach(function (key) {
      updateField(key, payload[key]);
    });
  }

  function tick() {
    if (document.visibilityState !== "visible") {
      return;
    }
    if (controller !== null) {
      controller.abort();
    }
    controller = new AbortController();
    fetch(pollUrl, {
      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_state_poll: state.json HTTP " + response.status);
          return null;
        }
        return response.json();
      })
      .then(function (payload) {
        if (payload !== null) {
          applyPayload(payload);
        }
      })
      .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_state_poll: poll failed", err);
        }
      });
  }

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