'use strict';
// Session state bus — owns the per-session state machine and fires hooks on
// transitions. States: idle | busy | waiting_input.
//
// Inputs from the rest of CTM:
//   - onOutput(sessionId)        — PTY produced output → mark busy; schedule idle debounce
//   - onApprovalDetected(sessionId) — approval prompt detected → waiting_input
//   - onApprovalResolved(sessionId) — prompt handled → busy (until idle debounce fires)
//   - clearWaitingInput(sessionId) — stale/non-current wait evidence cleared → idle
//   - reset(sessionId)            — forget this session (on close)
//
// Output: fires `statechange` events via the EventEmitter interface. Consumers
// (server.js) wire these to hook-executor.
//
// States are per-session. The bus never touches PTY or DB.

const { EventEmitter } = require('events');

const VALID_STATES = new Set(['idle', 'busy', 'waiting_input']);

// Default idle debounce (content-stable ms) for providers without a detector.
// Claude/Codex override this with longer provider-specific windows because
// meaningful progress frames can be much sparser than shell output.
const DEFAULT_IDLE_DEBOUNCE_MS = 1500;

class SessionStateBus extends EventEmitter {
  constructor() {
    super();
    this._states = new Map();          // sessionId -> { state, since, providerId }
    this._idleTimers = new Map();      // sessionId -> Timeout
    this._providerDebounce = new Map(); // providerId -> ms
    this._defaultDebounce = DEFAULT_IDLE_DEBOUNCE_MS;
  }

  setProviderDebounce(providerId, ms) {
    if (!providerId) return;
    if (ms == null) this._providerDebounce.delete(providerId);
    else this._providerDebounce.set(providerId, Math.max(0, Number(ms) | 0));
  }

  setDefaultDebounce(ms) {
    this._defaultDebounce = Math.max(0, Number(ms) | 0);
  }

  getDebounceFor(providerId) {
    if (providerId && this._providerDebounce.has(providerId)) return this._providerDebounce.get(providerId);
    return this._defaultDebounce;
  }

  getState(sessionId) {
    this._reconcileExpiredBusy(sessionId);
    return this._states.get(sessionId)?.state || null;
  }

  getSnapshot(sessionId) {
    this._reconcileExpiredBusy(sessionId);
    const cur = this._states.get(sessionId);
    return cur ? { ...cur } : null;
  }

  register(sessionId, providerId) {
    if (this._states.has(sessionId)) return;
    this._states.set(sessionId, {
      state: 'idle',
      since: Date.now(),
      providerId: providerId || '',
      lastOutputAt: 0,
      idleDueAt: 0,
    });
    // No transition fired for register — consumers treat initial 'idle' as the baseline.
  }

  _transition(sessionId, newState, meta = {}) {
    if (!VALID_STATES.has(newState)) return;
    const now = Date.now();
    const cur = this._states.get(sessionId);
    if (!cur) {
      this._states.set(sessionId, {
        state: newState,
        since: now,
        providerId: meta.providerId || '',
        lastOutputAt: meta.lastOutputAt || 0,
        idleDueAt: meta.idleDueAt || 0,
      });
      this.emit('statechange', { sessionId, oldState: 'idle', newState, providerId: meta.providerId || '', ...meta });
      return;
    }
    if (cur.state === newState) return;
    const oldState = cur.state;
    cur.state = newState;
    cur.since = now;
    if (meta.providerId) cur.providerId = meta.providerId;
    if (meta.lastOutputAt) cur.lastOutputAt = meta.lastOutputAt;
    if (meta.idleDueAt !== undefined) cur.idleDueAt = meta.idleDueAt;
    if (newState !== 'busy') cur.idleDueAt = 0;
    this.emit('statechange', {
      sessionId,
      oldState,
      newState,
      providerId: cur.providerId || '',
      ...meta,
    });
  }

  _cancelIdleTimer(sessionId) {
    const t = this._idleTimers.get(sessionId);
    if (t) { clearTimeout(t); this._idleTimers.delete(sessionId); }
  }

  _reconcileExpiredBusy(sessionId, now = Date.now()) {
    const cur = this._states.get(sessionId);
    if (!cur || cur.state !== 'busy' || !cur.idleDueAt || cur.idleDueAt > now) return false;
    this._cancelIdleTimer(sessionId);
    this._transition(sessionId, 'idle', { providerId: cur.providerId || '' });
    return true;
  }

  _scheduleIdle(sessionId) {
    this._cancelIdleTimer(sessionId);
    const cur = this._states.get(sessionId);
    if (!cur) return;
    const ms = this.getDebounceFor(cur.providerId);
    cur.idleDueAt = Date.now() + ms;
    const t = setTimeout(() => {
      this._idleTimers.delete(sessionId);
      const now = this._states.get(sessionId);
      // Only flip to idle if we're still 'busy' — don't override waiting_input.
      if (now && now.state === 'busy') this._reconcileExpiredBusy(sessionId);
    }, ms);
    if (typeof t.unref === 'function') t.unref();
    this._idleTimers.set(sessionId, t);
  }

  // PTY emitted output — reset the idle clock and mark busy if not already.
  onOutput(sessionId, providerId) {
    const cur = this._states.get(sessionId);
    const now = Date.now();
    if (!cur) {
      this._states.set(sessionId, {
        state: 'busy',
        since: now,
        providerId: providerId || '',
        lastOutputAt: now,
        idleDueAt: 0,
      });
      this.emit('statechange', { sessionId, oldState: 'idle', newState: 'busy', providerId: providerId || '' });
    } else if (cur.state !== 'busy' && cur.state !== 'waiting_input') {
      this._transition(sessionId, 'busy', { providerId: providerId || cur.providerId || '', lastOutputAt: now });
    } else {
      cur.lastOutputAt = now;
      if (providerId) cur.providerId = providerId;
    }
    // Always reschedule idle — latest output wins.
    // EXCEPTION: don't debounce while waiting_input. Approval prompts sit
    // quiescent on the terminal; the user needs to act.
    const state = this._states.get(sessionId);
    if (state && state.state !== 'waiting_input') this._scheduleIdle(sessionId);
  }

  onApprovalDetected(sessionId, providerId) {
    this._cancelIdleTimer(sessionId);
    const cur = this._states.get(sessionId);
    if (!cur) this._states.set(sessionId, { state: 'idle', since: Date.now(), providerId: providerId || '' });
    this._transition(sessionId, 'waiting_input', { providerId });
  }

  onApprovalResolved(sessionId) {
    const cur = this._states.get(sessionId);
    if (!cur || cur.state !== 'waiting_input') return;
    // After approval handled, we don't know yet if Claude is busy or idle.
    // Conservatively go busy and let the debounce tick us back to idle.
    this._transition(sessionId, 'busy');
    this._scheduleIdle(sessionId);
  }

  clearWaitingInput(sessionId) {
    const cur = this._states.get(sessionId);
    if (!cur || cur.state !== 'waiting_input') return;
    this._cancelIdleTimer(sessionId);
    this._transition(sessionId, 'idle', { providerId: cur.providerId || '' });
  }

  reset(sessionId) {
    this._cancelIdleTimer(sessionId);
    this._states.delete(sessionId);
  }

  // Test/introspection helpers
  _size() { return this._states.size; }
  _all() { return Array.from(this._states.entries()); }
}

module.exports = { SessionStateBus, VALID_STATES, DEFAULT_IDLE_DEBOUNCE_MS };
