import type { HeartbeatStatusInput } from './platformClients.js';
import type { NodeStatusSetResult } from './runtimeClient.js';

export interface HeartbeatPublisher {
  publishHeartbeat(input?: HeartbeatStatusInput): Promise<NodeStatusSetResult>;
}

export interface CompanionHeartbeatLoopOptions {
  intervalMs?: number;
  jitterRatio?: number;
  buildHeartbeat?: () => HeartbeatStatusInput | Promise<HeartbeatStatusInput>;
  onSuccess?: (result: NodeStatusSetResult) => void;
  onError?: (error: Error) => void;
  maxConsecutiveFailures?: number;
  onFailureLimitReached?: (error: Error, consecutiveFailures: number) => void;
  randomFn?: () => number;
}

export interface CompanionHeartbeatLoopState {
  running: boolean;
  successCount: number;
  lastSuccessAt: number | null;
  failureCount: number;
  lastFailure: Error | null;
}

/**
 * Lightweight periodic heartbeat loop for companion runtimes.
 * Calls `publishHeartbeat` on a fixed interval with optional dynamic payload generation.
 */
export class CompanionHeartbeatLoop {
  private readonly publisher: HeartbeatPublisher;
  private readonly intervalMs: number;
  private readonly jitterRatio: number;
  private readonly buildHeartbeat?: () => HeartbeatStatusInput | Promise<HeartbeatStatusInput>;
  private readonly onSuccess?: (result: NodeStatusSetResult) => void;
  private readonly onError?: (error: Error) => void;
  private readonly maxConsecutiveFailures: number;
  private readonly onFailureLimitReached?: (error: Error, consecutiveFailures: number) => void;
  private readonly randomFn: () => number;

  private timer: NodeJS.Timeout | null = null;
  private started = false;
  private inFlight = false;
  private successfulHeartbeats = 0;
  private lastSuccessAtMs: number | null = null;
  private consecutiveFailures = 0;
  private lastError: Error | null = null;

  constructor(publisher: HeartbeatPublisher, options: CompanionHeartbeatLoopOptions = {}) {
    const intervalMs = options.intervalMs ?? 30_000;
    if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
      throw new Error('intervalMs must be a positive number');
    }
    const jitterRatio = options.jitterRatio ?? 0;
    if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1) {
      throw new Error('jitterRatio must be between 0 and 1');
    }
    const maxConsecutiveFailures = options.maxConsecutiveFailures ?? Number.POSITIVE_INFINITY;
    if (!Number.isFinite(maxConsecutiveFailures) && maxConsecutiveFailures !== Number.POSITIVE_INFINITY) {
      throw new Error('maxConsecutiveFailures must be a positive number or Infinity');
    }
    if (Number.isFinite(maxConsecutiveFailures) && maxConsecutiveFailures < 1) {
      throw new Error('maxConsecutiveFailures must be >= 1 when specified');
    }

    this.publisher = publisher;
    this.intervalMs = intervalMs;
    this.jitterRatio = jitterRatio;
    this.buildHeartbeat = options.buildHeartbeat;
    this.onSuccess = options.onSuccess;
    this.onError = options.onError;
    this.maxConsecutiveFailures = maxConsecutiveFailures;
    this.onFailureLimitReached = options.onFailureLimitReached;
    this.randomFn = options.randomFn ?? Math.random;
  }

  get running(): boolean {
    return this.started;
  }

  get failureCount(): number {
    return this.consecutiveFailures;
  }

  get successCount(): number {
    return this.successfulHeartbeats;
  }

  get lastSuccessAt(): number | null {
    return this.lastSuccessAtMs;
  }

  get lastFailure(): Error | null {
    return this.lastError;
  }

  getState(): CompanionHeartbeatLoopState {
    return {
      running: this.running,
      successCount: this.successCount,
      lastSuccessAt: this.lastSuccessAt,
      failureCount: this.failureCount,
      lastFailure: this.lastFailure,
    };
  }

  start(runImmediately = true): void {
    if (this.started) {
      return;
    }
    this.started = true;
    this.successfulHeartbeats = 0;
    this.lastSuccessAtMs = null;
    this.consecutiveFailures = 0;
    this.lastError = null;

    if (runImmediately) {
      void this.tick();
      return;
    }
    this.scheduleNext();
  }

  stop(): void {
    this.started = false;
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
  }

  async tickNow(): Promise<void> {
    await this.tick(true);
  }

  private scheduleNext(): void {
    if (!this.started) {
      return;
    }
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
    const delay = this.computeDelayMs();
    this.timer = setTimeout(() => {
      void this.tick();
    }, delay);
  }

  private computeDelayMs(): number {
    if (this.jitterRatio === 0) {
      return this.intervalMs;
    }
    const minFactor = 1 - this.jitterRatio;
    const maxFactor = 1 + this.jitterRatio;
    const random = this.randomFn();
    const factor = minFactor + (maxFactor - minFactor) * random;
    return Math.max(1, Math.round(this.intervalMs * factor));
  }

  private async tick(force = false): Promise<void> {
    if ((!this.started && !force) || this.inFlight) {
      return;
    }
    this.inFlight = true;
    try {
      const payload = this.buildHeartbeat ? await this.buildHeartbeat() : undefined;
      const result = await this.publisher.publishHeartbeat(payload);
      this.successfulHeartbeats += 1;
      this.lastSuccessAtMs = Date.now();
      this.consecutiveFailures = 0;
      this.lastError = null;
      this.onSuccess?.(result);
    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error));
      this.consecutiveFailures += 1;
      this.lastError = err;
      this.onError?.(err);
      if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
        this.onFailureLimitReached?.(err, this.consecutiveFailures);
        this.stop();
        return;
      }
    } finally {
      this.inFlight = false;
      this.scheduleNext();
    }
  }
}
