'use strict';

(function initSessionStatusPrecedence(root, factory) {
  if (typeof module === 'object' && module.exports) {
    module.exports = factory();
  } else {
    root.SessionStatusPrecedence = factory();
  }
})(typeof globalThis !== 'undefined' ? globalThis : this, function buildSessionStatusPrecedence() {
  const AUTHORITATIVE_STATUS_TTL_MS = 120000;
  const SERVER_LIVE_STATUS_TTL_MS = 10000;
  const STREAM_STATUS_TTL_MS = 60000;
  const SERVER_WORKING_TTL_MS = 10000;
  const RECENT_OUTPUT_TTL_MS = 5000;
  const RECENT_INPUT_TTL_MS = 3000;
  const ACTIVE_TURN_MAX_QUIET_MS = 45 * 60 * 1000;
  const OBSERVED_TIME_FUTURE_SKEW_MS = 60 * 1000;
  const WAITING_INPUT_STALE_MS = 30 * 60 * 1000;

  const STATUS_TEXT = {
    running: 'Running',
    resuming: 'Resuming',
    waiting: 'Needs You',
    idle: 'Idle',
    exited: 'Exited',
  };

  function normalizeLiveSessionStatus(status) {
    const text = String(status || '').toLowerCase().replace(/[-\s]+/g, '_');
    if (!text) return '';
    if (text === 'busy' || text === 'active' || text === 'thinking') return 'running';
    if (text === 'restoring' || text === 'resuming' || text === 'restore_starting' || text === 'restarting') return 'resuming';
    if (text === 'waiting_input' || text === 'waiting_for_input') return 'waiting';
    if (Object.prototype.hasOwnProperty.call(STATUS_TEXT, text)) return text;
    return '';
  }

  function result(status, source) {
    const normalized = normalizeLiveSessionStatus(status);
    if (!normalized) return null;
    return {
      cls: normalized,
      text: STATUS_TEXT[normalized] || 'Idle',
      source: source || normalized,
    };
  }

  function parseTimeMs(value, fallback = 0, parser) {
    if (value == null || value === '') return fallback || 0;
    if (typeof parser === 'function') {
      const parsed = parser(value);
      if (parsed) return parsed;
    }
    if (typeof value === 'number' && Number.isFinite(value)) return value;
    const parsed = Date.parse(String(value));
    return Number.isFinite(parsed) ? parsed : (fallback || 0);
  }

  function normalizeObservedTimeMs(value, fallback = 0, parser, now = Date.now()) {
    const parsed = parseTimeMs(value, fallback, parser);
    if (!parsed) return 0;
    if (parsed > now + OBSERVED_TIME_FUTURE_SKEW_MS) return 0;
    return parsed > now ? now : parsed;
  }

  function isFresh(timestamp, now, ttl) {
    return Boolean(timestamp && timestamp <= now && (now - timestamp) < ttl);
  }

  function isBlockingWaitingReason(reason) {
    const text = String(reason || '').toLowerCase();
    return text === 'approval' || text === 'choice' || /\bapproval\b/.test(text) || /\bpermission\b/.test(text);
  }

  function resolveSessionStatus(input = {}, options = {}) {
    const now = options.now || Date.now();
    const parser = options.parseTimeMs;
    if (input.exited) return result('exited', 'exited');
    if (input.metaType === 'walle') {
      return input.walleGenerating
        ? result('running', 'walle-generating')
        : result('idle', 'walle-idle');
    }

    const observedTime = (value, fallback = 0) => normalizeObservedTimeMs(value, fallback, parser, now);
    const serverLiveStatusAt = observedTime(input.serverLiveStatusAt);
    const serverLiveStatusEventAt = observedTime(input.serverLiveStatusEventAt, serverLiveStatusAt);
    const serverLiveStatusMaxEventAgeMs = Math.max(0, Number(input.serverLiveStatusMaxEventAgeMs) || 0);
    const codexRunningEvidenceAt = observedTime(input.codexRunningEvidenceAt);
    const providerRunningEvidenceAt = observedTime(input.providerRunningEvidenceAt);
    const terminalBusyStatus = Boolean(input.terminalBusyStatus);
    const terminalBusyStatusAt = observedTime(input.terminalBusyStatusAt);
    const authoritativeStatusAt = observedTime(input.authoritativeStatusAt);
    const codexRunningHold = Boolean(input.codexRunningHold);
    const providerRunningHold = Boolean(input.providerRunningHold);
    const pendingNonRunningStatus = result(input.codexPendingNonRunningStatus, 'codex-pending-demotion');
    const pendingNonRunningEventAt = observedTime(input.codexPendingNonRunningEventAt);
    const activeTurnStartedAt = observedTime(input.activeTurnStartedAt);
    const activeTurnLastEvidenceAt = observedTime(input.activeTurnLastEvidenceAt);
    const activeTurnMaxQuietMs = Math.max(0, Number(input.activeTurnMaxQuietMs) || ACTIVE_TURN_MAX_QUIET_MS);
    const activeTurnEvidenceAt = Math.max(activeTurnStartedAt, activeTurnLastEvidenceAt);
    const activeTurnOpen = Boolean(input.activeTurnOpen) &&
      activeTurnEvidenceAt &&
      activeTurnMaxQuietMs &&
      (now - activeTurnEvidenceAt) <= activeTurnMaxQuietMs;
    const restoreStatus = normalizeLiveSessionStatus(input.restoreStatus);
    const restoreResuming = Boolean(input.restoreStarting || input.restoreResuming || restoreStatus === 'resuming');
    const activationStatus = normalizeLiveSessionStatus(input.activationPreservedStatus);
    const activationStatusCapturedAt = observedTime(input.activationPreservedStatusCapturedAt);
    const activationStatusUntil = parseTimeMs(input.activationPreservedStatusUntil, 0, parser);
    const activationPreservedRunning = activationStatus === 'running' &&
      activationStatusUntil > now;
    const streamStatusAt = observedTime(input.streamStatusAt);
    const streamStatus = normalizeLiveSessionStatus(input.streamStatus);
    const streamFresh = Boolean(streamStatus && isFresh(streamStatusAt, now, STREAM_STATUS_TTL_MS));
    const streamRunning = streamFresh && streamStatus === 'running';
    const waitingAt = observedTime(input.waitingForInputAt);
    const waitingReason = String(input.waitingReason || '').toLowerCase();
    const waitingMaxAgeMs = Math.max(0, Number(input.waitingInputStaleMs) || WAITING_INPUT_STALE_MS);
    const waitingForInputRaw = Boolean(input.waitingForInput);
    const waitingIsStale = Boolean(
      waitingForInputRaw &&
      waitingAt &&
      waitingMaxAgeMs &&
      (now - waitingAt) > waitingMaxAgeMs &&
      !isBlockingWaitingReason(waitingReason) &&
      !input.waitingHasUnsubmittedInput
    );
    const waitingForInput = waitingForInputRaw && !waitingIsStale;

    const serverLive = result(input.serverLiveStatus, 'server-live');
    if (serverLive && isFresh(serverLiveStatusAt, now, SERVER_LIVE_STATUS_TTL_MS)) {
      if (serverLive.cls === 'resuming') return serverLive;
      const serverLiveRunningIsStale = serverLive.cls === 'running' &&
        serverLiveStatusMaxEventAgeMs > 0 &&
        serverLiveStatusEventAt &&
        (now - serverLiveStatusEventAt) > serverLiveStatusMaxEventAgeMs;
      const serverLiveWaitingIsStale = serverLive.cls === 'waiting' && waitingIsStale;
      const serverLiveIdleShouldYieldToWaiting = serverLive.cls === 'idle' && waitingForInput;
      const terminalBusyIsCurrent =
        terminalBusyStatus &&
        terminalBusyStatusAt &&
        (!input.authoritativeSource || !authoritativeStatusAt || terminalBusyStatusAt >= authoritativeStatusAt);
      const serverLiveIsStaleNonRunning = serverLive.cls !== 'running' &&
        (
          terminalBusyIsCurrent ||
          (streamRunning && !input.authoritativeSource &&
            streamStatusAt &&
            (!serverLiveStatusEventAt || serverLiveStatusEventAt <= streamStatusAt)) ||
          codexRunningHold ||
          providerRunningHold ||
          (activationPreservedRunning &&
            activationStatusCapturedAt &&
            serverLiveStatusEventAt &&
            serverLiveStatusEventAt <= activationStatusCapturedAt)
        );
      if (!serverLiveRunningIsStale && !serverLiveWaitingIsStale && !serverLiveIsStaleNonRunning && !serverLiveIdleShouldYieldToWaiting) {
        return serverLive;
      }
      // Heartbeats are received "now", but their event timestamp is the
      // last server-observed activity. Do not let an old idle/waiting
      // heartbeat mask newer stream/capture or local Codex "Working" evidence.
    }

    const serverWorkingAt = observedTime(input.serverWorkingAt);
    const serverWorkingEventAt = observedTime(input.serverWorkingEventAt, serverWorkingAt);
    const serverWorking = Boolean(serverWorkingAt &&
      (now - serverWorkingAt) < SERVER_WORKING_TTL_MS &&
      (!waitingForInput || !waitingAt || serverWorkingEventAt >= waitingAt));
    const streamIsStaleNonRunning = streamFresh &&
      streamStatus !== 'running' &&
      activationPreservedRunning &&
      activationStatusCapturedAt &&
      streamStatusAt <= activationStatusCapturedAt;
    const streamSettlesLocalBusy = streamFresh &&
      streamStatus !== 'running' &&
      !streamIsStaleNonRunning &&
      !codexRunningHold &&
      !providerRunningHold &&
      (!terminalBusyStatus || !terminalBusyStatusAt || streamStatusAt >= terminalBusyStatusAt) &&
      (!codexRunningEvidenceAt || streamStatusAt >= codexRunningEvidenceAt) &&
      (!providerRunningEvidenceAt || streamStatusAt >= providerRunningEvidenceAt);
    const lastOutputAt = Math.max(
      observedTime(input.lastOutputAt),
      observedTime(input.metaLastPtyActivity)
    );
    const recentOutput = Boolean(lastOutputAt && (now - lastOutputAt) < RECENT_OUTPUT_TTL_MS);

    if (input.authoritativeSource) {
      const authAt = authoritativeStatusAt;
      if (isFresh(authAt, now, AUTHORITATIVE_STATUS_TTL_MS)) {
        if (input.working) return result('running', 'authoritative-working');
        if (waitingForInput) return result('waiting', 'authoritative-waiting');
        if (codexRunningHold) return result('running', 'codex-running-hold');
        if (providerRunningHold) return result('running', 'provider-running-hold');
        const newerTerminalBusy =
          terminalBusyStatus &&
          terminalBusyStatusAt &&
          (!authAt || terminalBusyStatusAt >= authAt);
        if (newerTerminalBusy) return result('running', 'codex-terminal-busy');
        const newerRunningEvidence =
          serverWorking ||
          (activeTurnOpen && (!authAt || activeTurnEvidenceAt >= authAt)) ||
          (streamRunning && (!authAt || streamStatusAt >= authAt)) ||
          (recentOutput && (!authAt || lastOutputAt >= authAt));
        if (newerRunningEvidence) return result('running', 'newer-running-evidence');
        return result('idle', 'authoritative-idle');
      }
    }

    if (serverWorking) return result('running', 'server-working');
    if (waitingForInput) return result('waiting', 'waiting-for-input');
    if (activeTurnOpen) return result('running', 'active-turn');
    if (streamSettlesLocalBusy) return result(streamStatus, 'stream-status');
    if (terminalBusyStatus) return result('running', 'codex-terminal-busy');
    if (codexRunningHold) return result('running', 'codex-running-hold');
    if (providerRunningHold) return result('running', 'provider-running-hold');
    if (pendingNonRunningStatus && pendingNonRunningStatus.cls !== 'running' && pendingNonRunningStatus.cls !== 'exited') {
      const pendingIsNotObsolete = !codexRunningEvidenceAt ||
        !pendingNonRunningEventAt ||
        pendingNonRunningEventAt >= codexRunningEvidenceAt;
      if (pendingIsNotObsolete) return pendingNonRunningStatus;
    }
    if (streamFresh && !streamIsStaleNonRunning) return result(streamStatus, 'stream-status');
    if (recentOutput) return result('running', 'recent-output');
    if (restoreResuming) return result('resuming', 'restore');
    if (activationPreservedRunning) return result('running', 'activation-preserved-running');
    if (waitingForInput && !recentOutput) return result('waiting', 'waiting-fallback');
    const lastInputAt = observedTime(input.lastInputAt);
    if (lastInputAt && (now - lastInputAt) < RECENT_INPUT_TTL_MS) return result('running', 'recent-input');
    return result('idle', 'default-idle');
  }

  return {
    AUTHORITATIVE_STATUS_TTL_MS,
    SERVER_LIVE_STATUS_TTL_MS,
    ACTIVE_TURN_MAX_QUIET_MS,
    OBSERVED_TIME_FUTURE_SKEW_MS,
    WAITING_INPUT_STALE_MS,
    normalizeLiveSessionStatus,
    isBlockingWaitingReason,
    liveStatusResult: (status) => result(status, 'live-status'),
    normalizeObservedTimeMs,
    parseTimeMs,
    resolveSessionStatus,
  };
});
