/**
 * Throttled flush controller for streaming card updates.
 *
 * Pure scheduling primitive — manages timer-based throttling,
 * mutex-guarded flushing, and reflush-on-conflict. Contains no
 * business logic; the actual flush work is injected via callback.
 *
 * Adapted from openclaw-lark's card/flush-controller.ts.
 */

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

/** Throttle intervals by mode */
export const THROTTLE_MS = {
  /** CardKit v2 streaming: fast updates allowed */
  cardkit: 100,
  /** IM PATCH fallback: slower to avoid rate-limits */
  imPatch: 1500,
} as const

/** After a long gap (no updates for 3s), batch briefly so the
 *  first visible update contains meaningful text. */
const LONG_GAP_THRESHOLD_MS = 3000
const BATCH_AFTER_GAP_MS = 80

// ---------------------------------------------------------------------------
// FlushController
// ---------------------------------------------------------------------------

export class FlushController {
  private flushInProgress = false
  private flushResolvers: Array<() => void> = []
  private needsReflush = false
  private pendingFlushTimer: ReturnType<typeof setTimeout> | null = null
  private lastUpdateTime = 0
  private isCompleted = false
  private _cardMessageReady = false

  constructor(private readonly doFlush: () => Promise<void>) {}

  // ---------------------------------------------------------------------------
  // Public API
  // ---------------------------------------------------------------------------

  /** Mark completed — no more flushes after current one. */
  complete(): void {
    this.isCompleted = true
  }

  /** Cancel any pending deferred flush timer. */
  cancelPendingFlush(): void {
    if (this.pendingFlushTimer) {
      clearTimeout(this.pendingFlushTimer)
      this.pendingFlushTimer = null
    }
  }

  /** Wait for any in-progress flush to finish. */
  waitForFlush(): Promise<void> {
    if (!this.flushInProgress) return Promise.resolve()
    return new Promise<void>((resolve) => this.flushResolvers.push(resolve))
  }

  /** Set whether the card message is ready for streaming updates. */
  setCardMessageReady(ready: boolean): void {
    this._cardMessageReady = ready
    if (ready) {
      this.lastUpdateTime = Date.now()
    }
  }

  cardMessageReady(): boolean {
    return this._cardMessageReady
  }

  /**
   * Execute a flush (mutex-guarded, with reflush on conflict).
   * If a flush is already in progress, marks needsReflush so a
   * follow-up flush fires immediately after the current one completes.
   */
  async flush(): Promise<void> {
    if (!this._cardMessageReady || this.flushInProgress || this.isCompleted) {
      if (this.flushInProgress && !this.isCompleted) this.needsReflush = true
      return
    }

    this.flushInProgress = true
    this.needsReflush = false
    this.lastUpdateTime = Date.now()

    try {
      await this.doFlush()
      this.lastUpdateTime = Date.now()
    } finally {
      this.flushInProgress = false
      const resolvers = this.flushResolvers
      this.flushResolvers = []
      for (const resolve of resolvers) resolve()

      if (this.needsReflush && !this.isCompleted && !this.pendingFlushTimer) {
        this.needsReflush = false
        this.pendingFlushTimer = setTimeout(() => {
          this.pendingFlushTimer = null
          void this.flush()
        }, 0)
      }
    }
  }

  /**
   * Throttled update entry point.
   * @param throttleMs - Minimum interval between flushes (varies by
   *   CardKit vs IM patch mode).
   */
  async throttledUpdate(throttleMs: number): Promise<void> {
    if (!this._cardMessageReady) return

    const now = Date.now()
    const elapsed = now - this.lastUpdateTime

    if (elapsed >= throttleMs) {
      this.cancelPendingFlush()
      if (elapsed > LONG_GAP_THRESHOLD_MS) {
        this.lastUpdateTime = now
        this.pendingFlushTimer = setTimeout(() => {
          this.pendingFlushTimer = null
          void this.flush()
        }, BATCH_AFTER_GAP_MS)
      } else {
        await this.flush()
      }
    } else if (!this.pendingFlushTimer) {
      const delay = throttleMs - elapsed
      this.pendingFlushTimer = setTimeout(() => {
        this.pendingFlushTimer = null
        void this.flush()
      }, delay)
    }
  }

  /** Clean up timers. Call when the controller is discarded. */
  destroy(): void {
    this.complete()
    this.cancelPendingFlush()
    this.flushResolvers = []
  }
}