#!/usr/bin/env node
/**
 * OpenClaw Orchestration — Session State Machine
 *
 * Explicit finite state machine for session lifecycle.
 * Migrated from ECC `ecc2/src/session/mod.rs` (SessionState, can_transition_to)
 *
 * States: Pending → Running → Idle/Stale → Completed/Failed/Stopped
 *
 * Usage:
 *   const { SessionFSM, SessionState } = require('./session-fsm');
 *   const session = new SessionFSM();
 *   session.transition(SessionState.RUNNING);  // true if valid
 */

// ─── State Enum ──────────────────────────────────────────────────

const SessionState = Object.freeze({
  PENDING:   'pending',
  RUNNING:   'running',
  IDLE:      'idle',
  STALE:     'stale',
  COMPLETED: 'completed',
  FAILED:    'failed',
  STOPPED:   'stopped',
});

// ─── Transition Matrix ───────────────────────────────────────────

/**
 * Valid state transitions.
 * ECC: SessionState::can_transition_to (ecc2/src/session/mod.rs:266-290)
 */
const VALID_TRANSITIONS = {
  [SessionState.PENDING]:   [SessionState.RUNNING, SessionState.FAILED, SessionState.STOPPED],
  [SessionState.RUNNING]:   [SessionState.IDLE, SessionState.STALE, SessionState.COMPLETED, SessionState.FAILED, SessionState.STOPPED],
  [SessionState.IDLE]:      [SessionState.RUNNING, SessionState.STALE, SessionState.COMPLETED, SessionState.FAILED, SessionState.STOPPED],
  [SessionState.STALE]:     [SessionState.RUNNING, SessionState.COMPLETED, SessionState.FAILED, SessionState.STOPPED],
  // Terminal states (no outgoing transitions)
  [SessionState.COMPLETED]: [],
  [SessionState.FAILED]:    [],
  [SessionState.STOPPED]:   [],
};

// ─── State Metadata ──────────────────────────────────────────────

const STATE_META = {
  [SessionState.PENDING]:   { isActive: false, isTerminal: false, icon: '⏳', label: 'Pending' },
  [SessionState.RUNNING]:   { isActive: true,  isTerminal: false, icon: '▶️',  label: 'Running' },
  [SessionState.IDLE]:      { isActive: false, isTerminal: false, icon: '⏸️',  label: 'Idle' },
  [SessionState.STALE]:     { isActive: false, isTerminal: false, icon: '⏰',  label: 'Stale' },
  [SessionState.COMPLETED]: { isActive: false, isTerminal: true,  icon: '✅',  label: 'Completed' },
  [SessionState.FAILED]:    { isActive: false, isTerminal: true,  icon: '❌',  label: 'Failed' },
  [SessionState.STOPPED]:   { isActive: false, isTerminal: true,  icon: '🛑',  label: 'Stopped' },
};

// ─── SessionFSM Class ────────────────────────────────────────────

class SessionFSM {
  constructor(options = {}) {
    this.id = options.id || `session_${Date.now()}`;
    this.state = options.initialState || SessionState.PENDING;
    this.createdAt = options.createdAt || new Date().toISOString();
    this.updatedAt = this.createdAt;
    this.stateHistory = [{ state: this.state, timestamp: this.createdAt }];

    // Session metadata
    this.task = options.task || '';
    this.agentType = options.agentType || 'main';
    this.parentId = options.parentId || null;
    this.childIds = options.childIds || [];
    this.workDir = options.workDir || process.cwd();

    // Metrics
    this.metrics = {
      transitions: 0,
      idleTimeMs: 0,
      activeTimeMs: 0,
      lastActiveAt: this.createdAt,
    };

    // Transitions callbacks
    this._listeners = { before: [], after: [], onError: [] };
  }

  /**
   * Attempt a state transition.
   * @returns {boolean} true if transition succeeded
   */
  transition(nextState) {
    if (!this.canTransitionTo(nextState)) {
      this._emit('onError', {
        from: this.state,
        to: nextState,
        reason: `Invalid transition: ${this.state} → ${nextState}`,
        timestamp: new Date().toISOString(),
      });
      return false;
    }

    // Before hooks
    this._emit('before', { from: this.state, to: nextState });

    // Execute transition
    const prevState = this.state;
    const now = new Date().toISOString();

    // Update time metrics
    const msSinceUpdate = Date.now() - new Date(this.updatedAt).getTime();
    if (STATE_META[prevState].isActive) {
      this.metrics.activeTimeMs += msSinceUpdate;
    } else {
      this.metrics.idleTimeMs += msSinceUpdate;
    }
    this.metrics.lastActiveAt = STATE_META[nextState].isActive ? now : this.metrics.lastActiveAt;

    this.state = nextState;
    this.updatedAt = now;
    this.stateHistory.push({ state: nextState, timestamp: now, previous: prevState });
    this.metrics.transitions++;

    // After hooks
    this._emit('after', { from: prevState, to: nextState, timestamp: now });

    return true;
  }

  /**
   * Check if transition is valid without executing.
   */
  canTransitionTo(nextState) {
    const allowed = VALID_TRANSITIONS[this.state] || [];
    return allowed.includes(nextState);
  }

  /**
   * Convenience transition methods.
   */
  start()   { return this.transition(SessionState.RUNNING); }
  pause()   { return this.transition(SessionState.IDLE); }
  resume()  { return this.transition(SessionState.RUNNING); }
  stale()   { return this.transition(SessionState.STALE); }
  complete() { return this.transition(SessionState.COMPLETED); }
  fail()    { return this.transition(SessionState.FAILED); }
  stop()    { return this.transition(SessionState.STOPPED); }

  /**
   * Check if session is in a terminal state.
   */
  isTerminal() {
    return STATE_META[this.state].isTerminal;
  }

  /**
   * Check if session is active (running).
   */
  isActive() {
    return STATE_META[this.state].isActive;
  }

  /**
   * Event listeners.
   */
  on(event, callback) {
    if (this._listeners[event]) this._listeners[event].push(callback);
    return this;
  }

  _emit(event, data) {
    for (const cb of this._listeners[event]) cb(data);
  }

  /**
   * Get session summary.
   */
  summary() {
    return {
      id: this.id,
      state: this.state,
      stateLabel: STATE_META[this.state].label,
      stateIcon: STATE_META[this.state].icon,
      isTerminal: this.isTerminal(),
      isActive: this.isActive(),
      task: this.task,
      agentType: this.agentType,
      parentId: this.parentId,
      childCount: this.childIds.length,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
      transitions: this.metrics.transitions,
      activeTimeMs: this.metrics.activeTimeMs,
      idleTimeMs: this.metrics.idleTimeMs,
      stateHistory: this.stateHistory,
    };
  }

  /**
   * Get valid next states from current state.
   */
  validNextStates() {
    return (VALID_TRANSITIONS[this.state] || []).map(s => ({
      state: s,
      label: STATE_META[s].label,
      icon: STATE_META[s].icon,
    }));
  }

  /**
   * Serialize for persistence.
   */
  toJSON() {
    return {
      id: this.id,
      state: this.state,
      task: this.task,
      agentType: this.agentType,
      parentId: this.parentId,
      childIds: this.childIds,
      workDir: this.workDir,
      metrics: this.metrics,
      stateHistory: this.stateHistory,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
    };
  }

  /**
   * Deserialize from persisted data.
   */
  static fromJSON(data) {
    const fsm = new SessionFSM({
      id: data.id,
      initialState: data.state,
      task: data.task,
      agentType: data.agentType,
      parentId: data.parentId,
      childIds: data.childIds,
      workDir: data.workDir,
      createdAt: data.createdAt,
    });
    fsm.metrics = data.metrics || fsm.metrics;
    fsm.stateHistory = data.stateHistory || fsm.stateHistory;
    fsm.updatedAt = data.updatedAt || fsm.updatedAt;
    return fsm;
  }
}

// ─── Session Router (Multi-Strategy Routing) ─────────────────────

/**
 * Assignment strategies for sub-agent routing.
 * ECC: AssignmentAction enum (ecc2/src/session/manager.rs)
 */
const AssignmentStrategy = Object.freeze({
  SPAWNED:           'spawned',            // Create new session
  REUSED_IDLE:       'reused_idle',        // Attach to existing idle session
  REUSED_ACTIVE:     'reused_active',      // Route to active session
  DEFERRED_SATURATED:'deferred_saturated', // Leave in inbox (queue full)
});

class SessionRouter {
  constructor(options = {}) {
    this.maxActivePerAgent = options.maxActivePerAgent || 5;
    this.maxIdlePerAgent = options.maxIdlePerAgent || 10;
    this.sessions = new Map(); // id → SessionFSM
    this.inbox = [];           // deferred tasks
  }

  /**
   * Register a session with the router.
   */
  register(session) {
    this.sessions.set(session.id, session);
  }

  /**
   * Determine best routing strategy for a new task.
   *
   * ECC: assign_session → AssignmentAction
   */
  route(task, agentType = 'default') {
    // Count active/idle sessions for this agent type
    const agentSessions = [...this.sessions.values()]
      .filter(s => s.agentType === agentType);

    const activeCount = agentSessions.filter(s => s.isActive()).length;
    const idleSessions = agentSessions.filter(s => s.state === SessionState.IDLE);

    // Check saturation
    if (activeCount >= this.maxActivePerAgent) {
      return {
        strategy: AssignmentStrategy.DEFERRED_SATURATED,
        reason: `Agent type '${agentType}' saturated: ${activeCount}/${this.maxActivePerAgent} active`,
        task,
      };
    }

    // Try to reuse idle session
    if (idleSessions.length > 0) {
      return {
        strategy: AssignmentStrategy.REUSED_IDLE,
        session: idleSessions[0],
        reason: `Reusing idle session ${idleSessions[0].id}`,
        task,
      };
    }

    // Check if should reuse active session (similar task)
    const activeSessions = agentSessions.filter(s => s.isActive());
    const similarSession = activeSessions.find(s =>
      s.task && this._similarity(s.task, task) > 0.5
    );

    if (similarSession) {
      return {
        strategy: AssignmentStrategy.REUSED_ACTIVE,
        session: similarSession,
        reason: `Similar task in progress: ${similarSession.id}`,
        task,
      };
    }

    // Default: spawn new
    return {
      strategy: AssignmentStrategy.SPAWNED,
      reason: 'Spawning new session',
      task,
    };
  }

  /**
   * Rebalance: move deferred tasks to available sessions.
   * ECC: rebalance_all_teams (ecc2/src/session/manager.rs)
   */
  rebalance() {
    const dispatched = [];

    for (let i = this.inbox.length - 1; i >= 0; i--) {
      const task = this.inbox[i];
      const result = this.route(task.task, task.agentType);

      if (result.strategy !== AssignmentStrategy.DEFERRED_SATURATED) {
        this.inbox.splice(i, 1);
        dispatched.push(result);
      }
    }

    return dispatched;
  }

  /**
   * Simple string similarity (Jaccard on word sets).
   */
  _similarity(a, b) {
    const wordsA = new Set((a || '').toLowerCase().split(/\W+/).filter(Boolean));
    const wordsB = new Set((b || '').toLowerCase().split(/\W+/).filter(Boolean));
    const intersection = [...wordsA].filter(w => wordsB.has(w)).length;
    const union = new Set([...wordsA, ...wordsB]).size;
    return union === 0 ? 0 : intersection / union;
  }

  /**
   * Get router stats.
   */
  stats() {
    const sessions = [...this.sessions.values()];
    return {
      total: sessions.length,
      byState: sessions.reduce((acc, s) => { acc[s.state] = (acc[s.state]||0)+1; return acc; }, {}),
      inboxSize: this.inbox.length,
      activeCount: sessions.filter(s => s.isActive()).length,
      terminalCount: sessions.filter(s => s.isTerminal()).length,
    };
  }
}

// ─── Module exports ──────────────────────────────────────────────

module.exports = {
  SessionFSM,
  SessionState,
  STATE_META,
  VALID_TRANSITIONS,
  SessionRouter,
  AssignmentStrategy,
};
