// SPEC: PJ2026-0106050514 Workbench实时运行面 draft-2026-06-30-p0-1297-spec-first.
// Responsibility: Framework-agnostic event coalescing for Workbench SSE and refresh dispatch.

export type CoalescedQueueKey = string | null | undefined;
export type CoalescedQueueCancel = () => void;
export type CoalescedQueueScheduler = (flush: () => void) => CoalescedQueueCancel | void;

export interface CoalescedEventQueueChunkInfo {
  chunkIndex: number;
  eventCount: number;
  remainingCount: number;
  reason: string;
}

export interface CoalescedEventQueueDrainInfo {
  eventCount: number;
  deliveredCount: number;
  droppedCount: number;
  chunkCount: number;
  replacedByKey: number;
  flushDurationMs: number;
  maxItemsPerChunk: number;
  maxChunkMs: number;
  reason: string;
}

export interface CoalescedEventQueueOptions<T> {
  keyOf: (item: T) => CoalescedQueueKey;
  onFlush: (items: T[], info: CoalescedEventQueueChunkInfo) => void;
  onDrain?: (info: CoalescedEventQueueDrainInfo) => void;
  schedule?: CoalescedQueueScheduler;
  yieldSchedule?: CoalescedQueueScheduler;
  maxItemsPerChunk?: number | null;
  maxChunkMs?: number | null;
  now?: () => number;
}

interface CoalescedQueueCycle {
  startedAt: number;
  eventCount: number;
  deliveredCount: number;
  replacedByKey: number;
  chunkCount: number;
  reason: string;
}

export class CoalescedEventQueue<T> {
  private readonly keyOf: (item: T) => CoalescedQueueKey;
  private readonly onFlush: (items: T[], info: CoalescedEventQueueChunkInfo) => void;
  private readonly onDrain: ((info: CoalescedEventQueueDrainInfo) => void) | null;
  private readonly scheduleFlush: CoalescedQueueScheduler;
  private readonly scheduleYield: CoalescedQueueScheduler;
  private readonly maxItemsPerChunk: number;
  private readonly maxChunkMs: number;
  private readonly now: () => number;
  private items: T[] = [];
  private keyIndex = new Map<string, number>();
  private cancelScheduled: CoalescedQueueCancel | null = null;
  private scheduled = false;
  private cycle: CoalescedQueueCycle | null = null;

  constructor(options: CoalescedEventQueueOptions<T>) {
    this.keyOf = options.keyOf;
    this.onFlush = options.onFlush;
    this.onDrain = options.onDrain ?? null;
    this.scheduleFlush = options.schedule ?? defaultScheduler;
    this.scheduleYield = options.yieldSchedule ?? defaultYieldScheduler;
    this.maxItemsPerChunk = positiveInteger(options.maxItemsPerChunk, 16);
    this.maxChunkMs = nonNegativeNumber(options.maxChunkMs, 8);
    this.now = options.now ?? monotonicNow;
  }

  get size(): number {
    return this.items.length;
  }

  push(item: T): void {
    this.ensureCycle("scheduled");
    if (this.cycle) this.cycle.eventCount += 1;
    const key = normalizedKey(this.keyOf(item));
    if (key && this.keyIndex.has(key)) {
      this.items[this.keyIndex.get(key) ?? 0] = item;
      if (this.cycle) this.cycle.replacedByKey += 1;
    } else {
      if (key) this.keyIndex.set(key, this.items.length);
      this.items.push(item);
    }
    this.ensureScheduled();
  }

  flush(): T[] {
    this.cancelPendingSchedule();
    if (this.items.length === 0) return [];
    const items = this.items;
    this.items = [];
    this.keyIndex = new Map();
    this.ensureCycle("manual");
    if (this.cycle) {
      this.cycle.deliveredCount += items.length;
      this.cycle.chunkCount += 1;
    }
    this.onFlush(items, { chunkIndex: this.cycle?.chunkCount ?? 1, eventCount: items.length, remainingCount: 0, reason: "manual" });
    this.finishCycle("manual", 0);
    return items;
  }

  clear(reason = "clear"): void {
    this.cancelPendingSchedule();
    const droppedCount = this.items.length;
    this.items = [];
    this.keyIndex = new Map();
    this.finishCycle(reason, droppedCount);
  }

  private ensureScheduled(): void {
    if (this.scheduled) return;
    this.scheduled = true;
    const cancel = this.scheduleFlush(() => this.flushScheduledChunk("scheduled"));
    this.cancelScheduled = typeof cancel === "function" ? cancel : null;
  }

  private ensureYieldScheduled(): void {
    if (this.scheduled) return;
    this.scheduled = true;
    const cancel = this.scheduleYield(() => this.flushScheduledChunk("yield"));
    this.cancelScheduled = typeof cancel === "function" ? cancel : null;
  }

  private flushScheduledChunk(reason: string): void {
    this.scheduled = false;
    this.cancelScheduled = null;
    if (this.items.length === 0) {
      this.finishCycle(reason, 0);
      return;
    }
    this.ensureCycle(reason);
    const chunkIndex = (this.cycle?.chunkCount ?? 0) + 1;
    if (this.cycle) this.cycle.chunkCount += 1;
    const chunkStartedAt = this.now();
    let deliveredInChunk = 0;
    while (this.items.length > 0 && deliveredInChunk < this.maxItemsPerChunk) {
      const item = this.takeNextItem();
      this.onFlush([item], { chunkIndex, eventCount: 1, remainingCount: this.items.length, reason });
      if (this.cycle) this.cycle.deliveredCount += 1;
      deliveredInChunk += 1;
      if (this.items.length > 0 && this.now() - chunkStartedAt >= this.maxChunkMs) break;
    }
    if (this.items.length > 0) {
      this.ensureYieldScheduled();
      return;
    }
    this.finishCycle(reason, 0);
  }

  private takeNextItem(): T {
    const item = this.items[0];
    this.items = this.items.slice(1);
    this.rebuildKeyIndex();
    if (item === undefined) throw new Error("coalesced queue item missing");
    return item;
  }

  private rebuildKeyIndex(): void {
    this.keyIndex = new Map();
    for (let index = 0; index < this.items.length; index += 1) {
      const item = this.items[index];
      const key = item === undefined ? null : normalizedKey(this.keyOf(item));
      if (key) this.keyIndex.set(key, index);
    }
  }

  private ensureCycle(reason: string): void {
    if (this.cycle) return;
    this.cycle = { startedAt: this.now(), eventCount: 0, deliveredCount: 0, replacedByKey: 0, chunkCount: 0, reason };
  }

  private finishCycle(reason: string, droppedCount: number): void {
    const cycle = this.cycle;
    if (!cycle) return;
    this.cycle = null;
    const dropped = Math.max(droppedCount, cycle.eventCount - cycle.deliveredCount);
    this.onDrain?.({
      eventCount: cycle.eventCount,
      deliveredCount: cycle.deliveredCount,
      droppedCount: dropped,
      chunkCount: cycle.chunkCount,
      replacedByKey: cycle.replacedByKey,
      flushDurationMs: Math.max(0, this.now() - cycle.startedAt),
      maxItemsPerChunk: this.maxItemsPerChunk,
      maxChunkMs: this.maxChunkMs,
      reason: reason || cycle.reason
    });
  }

  private cancelPendingSchedule(): void {
    if (this.cancelScheduled) this.cancelScheduled();
    this.cancelScheduled = null;
    this.scheduled = false;
  }
}

export function createCoalescedEventQueue<T>(options: CoalescedEventQueueOptions<T>): CoalescedEventQueue<T> {
  return new CoalescedEventQueue(options);
}

function defaultScheduler(flush: () => void): CoalescedQueueCancel {
  if (typeof queueMicrotask === "function") {
    let canceled = false;
    queueMicrotask(() => {
      if (!canceled) flush();
    });
    return () => {
      canceled = true;
    };
  }
  let canceled = false;
  void Promise.resolve().then(() => {
    if (!canceled) flush();
  });
  return () => {
    canceled = true;
  };
}

function defaultYieldScheduler(flush: () => void): CoalescedQueueCancel {
  if (typeof requestAnimationFrame === "function" && typeof cancelAnimationFrame === "function") {
    const id = requestAnimationFrame(() => flush());
    return () => cancelAnimationFrame(id);
  }
  if (typeof setTimeout === "function" && typeof clearTimeout === "function") {
    const id = setTimeout(() => flush(), 0);
    return () => clearTimeout(id);
  }
  return defaultScheduler(flush);
}

function normalizedKey(value: CoalescedQueueKey): string | null {
  if (typeof value !== "string") return null;
  const text = value.trim();
  return text ? text : null;
}

function positiveInteger(value: number | null | undefined, fallback: number): number {
  const parsed = Number(value);
  return Number.isFinite(parsed) && parsed > 0 ? Math.max(1, Math.trunc(parsed)) : fallback;
}

function nonNegativeNumber(value: number | null | undefined, fallback: number): number {
  const parsed = Number(value);
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
}

function monotonicNow(): number {
  return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
}
