// dashboard/js/state.js — Global state and page navigation extracted from dashboard.html

let inboxData = [];
let agentData = [];
let currentAgentId = null;
let currentTab = 'thought-process';
const DASHBOARD_TAB_ID = (function() {
  try {
    var existing = sessionStorage.getItem('minions-dashboard-tab-id');
    if (existing) return existing;
    var id = 'tab-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
    sessionStorage.setItem('minions-dashboard-tab-id', id);
    return id;
  } catch {
    return 'tab-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
  }
})();

// Sidebar page navigation — URL-routed
function getPageFromUrl() {
  const path = window.location.pathname.replace(/^\//, '') || 'home';
  if (document.querySelector('.sidebar-link[data-page="' + path + '"]')) return path;
  return 'home';
}

let currentPage = getPageFromUrl();

// W-mpgb0xa7000d90d4 (dashboard-refresh-audit.md F5) — switchPage previously
// only cleaned intervals and flipped CSS; tabs like /plans and /inbox waited
// for refresh.js's next slow-cycle (refreshKnowledgeBase + refreshPlans) before
// showing fresh data. The QA tab worked around this with its own switchPage
// monkey-patch in qa.js (__qaWrapped) — that per-tab pattern is now generalized
// into one canonical lifecycle below. (As of W-mphfb6ss000a3b9e the slow-cycle
// modulo was dropped, so refreshPlans/refreshKnowledgeBase now run every 4s
// status tick — but the page-enter loader still wins on tab-switch because it
// fires synchronously without waiting for the next interval boundary.)
//
// PAGE_LAZY_LOADERS: functions to invoke when ENTERING a page (resolved by
// name through window so the function body can live alongside its tab UI).
// PAGE_LEAVE_HOOKS: functions to invoke on EVERY switchPage call regardless
// of source/destination. Each leave hook must be safe to invoke when there
// is nothing to clean up (idempotent / no-op when inactive) — _stopPlanPoll,
// _stopMeetingPoll, _stopQaRunsPoll all clearInterval on a nullable handle;
// closeDetail / closeManagedLog short-circuit when no panel/stream is open.
const PAGE_LAZY_LOADERS = {
  qa: ['loadQaTargets', 'loadQaRunbooks', 'loadQaRunners', 'loadQaProjectsSelect', 'loadQaSessions', '_startQaSessionsPoll', '_startQaRunsPoll'],
  plans: ['refreshPlans'],
  inbox: ['refreshKnowledgeBase'],
};

const PAGE_LEAVE_HOOKS = [
  '_stopPlanPoll',
  '_stopMeetingPoll',
  'closeDetail',
  '_stopQaRunsPoll',
  '_stopQaSessionsPoll',
  'closeManagedLog',
];

function _invokePageHooks(names) {
  if (!names || !names.length) return;
  const scope = (typeof window !== 'undefined') ? window
    : (typeof globalThis !== 'undefined' ? globalThis : {});
  for (const name of names) {
    const fn = scope[name];
    if (typeof fn !== 'function') continue;
    try { fn(); } catch { /* per-hook isolation — one bad hook can't break the lifecycle */ }
  }
}

function switchPage(page, pushState) {
  _invokePageHooks(PAGE_LEAVE_HOOKS);

  currentPage = page;
  document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
  const target = document.getElementById('page-' + page);
  if (target) target.classList.add('active');
  document.querySelectorAll('.sidebar-link').forEach(l => l.classList.remove('active'));
  const link = document.querySelector('.sidebar-link[data-page="' + page + '"]');
  if (link) {
    link.classList.add('active');
    // Clear notification badge when visiting a page
    try { clearNotifBadge(link); } catch { /* clearNotifBadge may not be loaded yet */ }
  }
  if (pushState !== false) {
    const url = page === 'home' ? '/' : '/' + page;
    history.pushState({ page }, '', url);
  }

  _invokePageHooks(PAGE_LAZY_LOADERS[page]);

  // W-mphf8tkn0009f3ac — force an immediate /api/status fetch on every page
  // switch (sidebar click + popstate back/forward). Without this, visiting a
  // page just unhides a pre-rendered DOM that's whatever the last
  // setInterval(refresh, 4000) tick produced — up to 4 s stale, much worse if
  // the tab was background-throttled or a 10 MB payload is mid-parse.
  // PAGE_LAZY_LOADERS only covers qa/plans/inbox (which have side-channel
  // fetches); every other page (work, home, prs, engine, agents, pipelines,
  // meetings, watches, schedule, tools) had no per-entry refresh trigger.
  // _refreshInFlight (refresh.js) coalesces against an in-flight call so this
  // is safe even when the 4 s interval just fired; ETag/304 keeps server cost
  // ~free in steady state.
  // typeof guard: state.js evaluates before refresh.js in the assembled
  // bundle, so the very first switchPage(currentPage) call from refresh.js's
  // own bootstrap runs AFTER refresh is defined — but a future load-order
  // change or partial init must not break navigation.
  try { if (typeof refresh === 'function') refresh(); } catch { /* refresh failure must not block page navigation */ }
}

// Browser back/forward navigation
window.addEventListener('popstate', (e) => {
  switchPage(e.state?.page || getPageFromUrl(), false);
});
window._prdRequeueUi = window._prdRequeueUi || {};

function getPrdRequeueState(workItemId) {
  const map = window._prdRequeueUi || {};
  const state = map[workItemId];
  if (!state) return null;
  const now = Date.now();
  if (state.until && now > state.until) {
    delete map[workItemId];
    return null;
  }
  return state;
}

function setPrdRequeueState(workItemId, state) {
  window._prdRequeueUi = window._prdRequeueUi || {};
  window._prdRequeueUi[workItemId] = Object.assign({ updatedAt: Date.now() }, state);
}

function clearPrdRequeueState(workItemId) {
  const map = window._prdRequeueUi || {};
  delete map[workItemId];
}

function prunePrdRequeueState(workItems) {
  const map = window._prdRequeueUi || {};
  if (!Object.keys(map).length) return;
  const byId = {};
  for (const w of (workItems || [])) {
    if (w.id) byId[w.id] = w;
  }
  for (const [id, state] of Object.entries(map)) {
    if (!state) continue;
    const wi = byId[id];
    if (state.status === 'pending') {
      if (!wi || (wi.status !== 'failed' && wi.status !== 'paused')) delete map[id];
      continue;
    }
    if (state.status === 'queued') {
      if (wi && wi.status !== 'failed') delete map[id];
      continue;
    }
    if (state.status === 'error' && state.until && Date.now() > state.until) delete map[id];
  }
}

function _dashboardPresencePayload(closed) {
  return JSON.stringify({
    tabId: DASHBOARD_TAB_ID,
    closed: !!closed,
    url: location.pathname + location.search + location.hash,
    visibility: document.visibilityState || '',
  });
}

function _sendDashboardPresence(closed) {
  var payload = _dashboardPresencePayload(closed);
  try {
    if (navigator.sendBeacon) {
      var blob = new Blob([payload], { type: 'application/json' });
      if (navigator.sendBeacon('/api/browser-presence', blob)) return;
    }
  } catch {}
  try {
    fetch('/api/browser-presence', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: payload,
      keepalive: true,
    }).catch(function() {});
  } catch {}
}

window.addEventListener('pagehide', function() { _sendDashboardPresence(true); });
window.addEventListener('beforeunload', function() { _sendDashboardPresence(true); });
// Only ping on becoming visible — hidden->presence beacons race the pagehide close
// beacon (both fire on tab close in quick succession via sendBeacon, ordering not
// guaranteed) and can resurrect a just-deleted entry with visibility:"hidden".
// Hidden tabs naturally expire via the 45s presence window; foreground returns
// re-fire visibilitychange so we still get a fresh heartbeat when the user comes back.
//
// Also kick a refresh() so the SPA recovers from background-tab throttling.
// Chromium throttles setInterval to ~1/min on backgrounded tabs, which stretches
// our 4 s poll cadence to 60 s. A backgrounded tab that re-focuses after the
// engine paused / restarted / heartbeat advanced can otherwise sit on stale
// state (including a stuck "Engine heartbeat is stale" banner) until the next
// throttled tick fires. Triggering refresh() inline on becoming-visible
// closes that window to <1 s. refresh() is single-flight-gated by
// _refreshInFlight (see refresh.js), so concurrent calls collapse to one fetch.
document.addEventListener('visibilitychange', function() {
  if (document.visibilityState === 'visible') {
    _sendDashboardPresence(false);
    try { if (typeof refresh === 'function') refresh(); } catch {}
  }
});

function rerenderPrdFromCache() {
  // Issue #2949 — prdProgress + prd moved off /api/status to /api/prd;
  // refresh.js stashes the fresh fetch into window._lastPrdProgress +
  // window._lastPrdStatus. `!== undefined` (not a truthy check) so a
  // cache-was-null state (last PRD archived → /api/prd returned
  // {progress: null}) still triggers a re-render — renderPrd handles
  // null gracefully and paints the empty 'No PRD found' affordance.
  if (window._lastPrdProgress === undefined) return;
  renderPrdProgress(window._lastPrdProgress);
  renderPrd(window._lastPrdStatus, window._lastPrdProgress);
}

// Global fetch wrapper with timeout — prevents connection exhaustion from hung requests.
// Use for all short API calls. Long-lived streams (CC, doc-chat) manage their own AbortControllers.
function safeFetch(url, opts) {
  var timeout = (opts && opts.timeout) || 15000;
  var controller = new AbortController();
  var timer = setTimeout(function() { controller.abort(); }, timeout);
  var fetchOpts = Object.assign({}, opts, { signal: controller.signal });
  if (typeof url === 'string' && url.indexOf('/api/') === 0) {
    var headers = Object.assign({}, fetchOpts.headers || {});
    headers['X-Minions-Dashboard-Tab'] = DASHBOARD_TAB_ID;
    headers['X-Minions-Dashboard-Url'] = location.pathname + location.search + location.hash;
    headers['X-Minions-Dashboard-Visibility'] = document.visibilityState || '';
    fetchOpts.headers = headers;
  }
  delete fetchOpts.timeout;
  return fetch(url, fetchOpts).finally(function() { clearTimeout(timer); });
}

window.MinionsState = { getPageFromUrl, switchPage, getPrdRequeueState, setPrdRequeueState, clearPrdRequeueState, prunePrdRequeueState, rerenderPrdFromCache, safeFetch, PAGE_LAZY_LOADERS, PAGE_LEAVE_HOOKS };
