// src/core/Supervision.ts

import type { IExitSignal, IDownSignal, IMonitorRef } from '../engine/mailbox/IMailbox';

// Re-export mailbox types for convenience
export type { IExitSignal, IDownSignal, IMonitorRef };

/**
 * Error information passed to supervision handlers.
 */
export interface ISupervisionError {
  /** Child id that failed */
  childId: string;
  /** Error message */
  message: string;
  /** Stack trace (if available) */
  stack?: string;
  /** Original error object - use instanceof or type guards to check type */
  original?: unknown;
  /** Timestamp */
  ts: number;
  /** Number of times this child has been restarted */
  restartCount: number;
}

/**
 * Actions that can be taken in response to a child error.
 */
export type SupervisionAction =
  | 'restart'      // Restart the failed child
  | 'stop'         // Stop the failed child (remove it)
  | 'escalate'     // Propagate error to parent
  | 'ignore';      // Log and continue

/**
 * Built-in supervision strategies.
 */
export type BuiltinStrategy =
  | 'log'          // Just log errors (default)
  | 'restart'      // Always restart failed children
  | 'restart-all'  // Restart all children on any failure
  | 'stop'         // Stop failed children
  | 'escalate';    // Always escalate to parent

/**
 * Custom supervision function.
 */
export type SupervisionFn = (error: ISupervisionError) => SupervisionAction | Promise<SupervisionAction>;

/**
 * Supervision strategy - can be built-in string or custom function.
 */
export type SupervisionStrategy = BuiltinStrategy | SupervisionFn;

/**
 * Restart limits configuration.
 */
export interface IRestartLimits {
  /** Maximum restarts within time window */
  maxRestarts: number;
  /** Time window in milliseconds */
  withinMs: number;
}

/**
 * Supervision policy for a composite component.
 */
export interface ISupervisionPolicy {
  /** Strategy for handling errors */
  strategy: SupervisionStrategy;
  /** Restart limits (for 'restart' strategy) */
  restartLimits?: IRestartLimits;
  /** Delay before restart in ms */
  restartDelayMs?: number;
  /** Whether to bubble errors up via emit */
  emitErrors?: boolean;
}

/**
 * Default supervision policy.
 */
export const DEFAULT_SUPERVISION_POLICY: ISupervisionPolicy = {
  strategy: 'log',
  emitErrors: false,
};

/**
 * Supervisor - manages error handling for child components.
 *
 * ## DESIGN NOTE: Exit Signals via Topics (Joe Armstrong's Concern)
 *
 * Exit signals are delivered via pub/sub topics, not actor mailboxes:
 * ```typescript
 * // In MatrixActor._monitorChild():
 * const exitTopic = `${childInfo.mount}.$exit`;
 * transport.subscribe(exitTopic, handleExitSignal);
 * ```
 *
 * **Why topics instead of mailboxes:**
 *
 * 1. **Multiple observers** - In Erlang, multiple processes can monitor
 *    the same process. Topics enable parent + siblings + external observers
 *    to all receive the exit signal.
 *
 * 2. **System-level vs application-level** - Exit signals are runtime
 *    infrastructure, not application messages. Erlang's monitors are also
 *    delivered by the runtime, not via the actor's message queue.
 *
 * 3. **Separation of concerns** - The mailbox is for business logic messages.
 *    Supervision is orthogonal infrastructure.
 *
 * **Tradeoff:**
 * - Topics: Anyone who knows the topic can listen (no exclusivity)
 * - Mailbox: Exclusive delivery, but only one supervisor per actor
 *
 * **If mailbox-based supervision is needed**, the IDownSignal could be
 * sent to the supervisor's mailbox as a message:
 * ```typescript
 * supervisorMailbox.send({
 *   command: '$down',
 *   payload: { target: 'child.$inbox', reason: 'error' },
 *   meta: { ts: Date.now() }
 * });
 * ```
 * This would require supervisors to run processMailbox() loops.
 */
export class Supervisor {
  private readonly _policy: ISupervisionPolicy;
  private readonly _restartCounts = new Map<string, number>();
  private readonly _restartTimestamps = new Map<string, number[]>();

  constructor(policy: ISupervisionPolicy = DEFAULT_SUPERVISION_POLICY) {
    this._policy = policy;
  }

  /**
   * Handle an error from a child.
   * Returns the action to take.
   */
  async handleError(error: ISupervisionError): Promise<SupervisionAction> {
    const { strategy } = this._policy;

    // Update restart count
    const currentCount = this._restartCounts.get(error.childId) ?? 0;
    error.restartCount = currentCount;

    // Check restart limits
    if (this._isOverRestartLimit(error.childId)) {
      // eslint-disable-next-line no-console -- supervisor restart-limit violations need direct kernel diagnostics
      console.error(
        `[Supervisor] Child '${error.childId}' exceeded restart limits, stopping`
      );
      return 'stop';
    }

    // Determine action
    let action: SupervisionAction;

    if (typeof strategy === 'function') {
      action = await strategy(error);
    } else {
      switch (strategy) {
        case 'log':
          // eslint-disable-next-line no-console -- log supervision strategy intentionally writes child failures to console
          console.error(`[Supervisor] Child '${error.childId}' error:`, error.message);
          action = 'ignore';
          break;
        case 'restart':
          action = 'restart';
          break;
        case 'restart-all':
          action = 'restart'; // Caller handles 'all' logic
          break;
        case 'stop':
          action = 'stop';
          break;
        case 'escalate':
          action = 'escalate';
          break;
        default:
          action = 'ignore';
      }
    }

    // Track restart if that's the action
    if (action === 'restart') {
      this._recordRestart(error.childId);
    }

    return action;
  }

  /**
   * Check if child is over restart limit.
   */
  private _isOverRestartLimit(childId: string): boolean {
    const limits = this._policy.restartLimits;
    if (!limits) return false;

    const timestamps = this._restartTimestamps.get(childId) ?? [];
    const now = Date.now();
    const windowStart = now - limits.withinMs;

    // Count restarts within window
    const recentRestarts = timestamps.filter(ts => ts > windowStart).length;
    return recentRestarts >= limits.maxRestarts;
  }

  /**
   * Record a restart for tracking limits.
   */
  private _recordRestart(childId: string): void {
    const count = (this._restartCounts.get(childId) ?? 0) + 1;
    this._restartCounts.set(childId, count);

    const timestamps = this._restartTimestamps.get(childId) ?? [];
    timestamps.push(Date.now());

    // Clean old timestamps
    const limits = this._policy.restartLimits;
    if (limits) {
      const windowStart = Date.now() - limits.withinMs;
      const recent = timestamps.filter(ts => ts > windowStart);
      this._restartTimestamps.set(childId, recent);
    } else {
      this._restartTimestamps.set(childId, timestamps);
    }
  }

  /**
   * Get restart delay in ms.
   */
  getRestartDelayMs(): number {
    return this._policy.restartDelayMs ?? 100;
  }

  /**
   * Check if errors should be emitted.
   */
  shouldEmitErrors(): boolean {
    return this._policy.emitErrors ?? false;
  }

  /**
   * Check if strategy is 'restart-all'.
   */
  isRestartAllStrategy(): boolean {
    return this._policy.strategy === 'restart-all';
  }

  /**
   * Reset restart tracking for a child.
   */
  resetChild(childId: string): void {
    this._restartCounts.delete(childId);
    this._restartTimestamps.delete(childId);
  }

  /**
   * Reset all tracking.
   */
  reset(): void {
    this._restartCounts.clear();
    this._restartTimestamps.clear();
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Actor Exit Signal Integration
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Convert an actor IExitSignal to a SupervisionError.
   *
   * This bridges the actor mailbox system (IExitSignal) with the
   * supervision system (SupervisionError), enabling unified error handling.
   *
   * @param signal - Exit signal from actor termination
   * @returns SupervisionError for processing by handleError()
   */
  exitSignalToError(signal: IExitSignal): ISupervisionError {
    // Extract child id from mount path (last segment)
    const childId = signal.from.split('.').pop() ?? signal.from;

    return {
      childId,
      message: signal.error?.message ?? `Actor terminated with reason: ${signal.reason}`,
      stack: signal.error?.stack,
      original: signal,
      ts: Date.now(),
      restartCount: this._restartCounts.get(childId) ?? 0,
    };
  }

  /**
   * Handle an actor exit signal.
   *
   * Convenience method that converts the signal and determines action.
   *
   * @param signal - Exit signal from actor termination
   * @returns Action to take
   */
  async handleExitSignal(signal: IExitSignal): Promise<SupervisionAction> {
    // Normal termination doesn't need supervision action
    if (signal.reason === 'normal') {
      return 'ignore';
    }

    const error = this.exitSignalToError(signal);
    return this.handleError(error);
  }
}

/**
 * Create a IDownSignal from an IExitSignal.
 *
 * Used by supervisors to notify monitors about child termination.
 *
 * @param exit - Exit signal from terminated actor
 * @param observerMount - Mount path of the observer
 * @returns IDownSignal to send to observers
 */
export function createDownSignal(exit: IExitSignal, observerMount: string): IDownSignal {
  return {
    type: 'down',
    target: exit.from,
    reason: exit.reason,
    error: exit.error,
  };
}

/**
 * Create a IMonitorRef for tracking monitors.
 *
 * @param target - Mount path being monitored
 * @param observer - Mount path of observer
 * @returns IMonitorRef for tracking
 */
export function createMonitorRef(target: string, observer: string): IMonitorRef {
  return {
    id: `${observer}->${target}-${Date.now()}`,
    target,
    observer,
  };
}
