// SPEC: PJ2026-0106050514 Workbench实时运行面 draft-2026-06-30-p0-1297-spec-first; PJ2026-01060505 Workbench性能 draft-2026-06-19-p0.
// Responsibility: Health probe retry/cache helpers. Mechanically migrated from OpenCode app/utils/server-health.ts with all timing supplied by runtime policy.

import { createScopedCache } from "@/utils/scoped-cache";

export type WorkbenchHealthState = "ok" | "degraded" | "unavailable" | "blocked";

export interface WorkbenchHealthSnapshot<T = unknown> {
  key: string;
  state: WorkbenchHealthState;
  value: T | null;
  error: unknown | null;
  observedAt: string;
}

export interface CheckWorkbenchHealthOptions<T> {
  key: string;
  fetcher: (signal: AbortSignal | undefined) => Promise<T>;
  classify?: (value: T) => WorkbenchHealthState;
  timeoutMs?: number | null;
  signal?: AbortSignal | null;
  retryCount?: number | null;
  retryDelayMs?: number | null;
}

export interface WorkbenchHealthCacheOptions {
  cacheMs?: number | null;
  now?: () => number;
}

interface HealthCacheEntry<T> {
  at: number;
  done: boolean;
  promise: Promise<WorkbenchHealthSnapshot<T>>;
}

interface HealthLatestEntry {
  snapshot: WorkbenchHealthSnapshot | null;
}

export class WorkbenchHealthProbeCache {
  private readonly latest: ReturnType<typeof createScopedCache<HealthLatestEntry>>;
  private readonly inflight = new Map<string, HealthCacheEntry<unknown>>();
  private readonly now: () => number;
  private readonly cacheMs: number | null;

  constructor(options: WorkbenchHealthCacheOptions = {}) {
    this.now = options.now ?? Date.now;
    this.cacheMs = positiveIntegerOrNull(options.cacheMs);
    this.latest = createScopedCache<HealthLatestEntry>(() => ({ snapshot: null }), { ttlMs: this.cacheMs, now: () => this.now() });
  }

  get(key: string): WorkbenchHealthSnapshot | null {
    return this.latest.peek(normalizeKey(key))?.snapshot ?? null;
  }

  probe<T>(input: CheckWorkbenchHealthOptions<T>): Promise<WorkbenchHealthSnapshot<T>> {
    const key = normalizeKey(input.key);
    const hit = this.inflight.get(key) as HealthCacheEntry<T> | undefined;
    const now = this.now();
    if (hit && (!hit.done || (this.cacheMs !== null && now - hit.at < this.cacheMs))) return hit.promise;
    const promise = checkWorkbenchHealth(input).then((snapshot) => {
      this.latest.get(key).snapshot = snapshot;
      return snapshot;
    }).finally(() => {
      const current = this.inflight.get(key);
      if (!current || current.promise !== promise) return;
      current.done = true;
      current.at = this.now();
    });
    this.inflight.set(key, { at: now, done: false, promise });
    return promise;
  }

  clear(key?: string): void {
    if (key) {
      const normalized = normalizeKey(key);
      this.latest.delete(normalized);
      this.inflight.delete(normalized);
    } else {
      this.latest.clear();
      this.inflight.clear();
    }
  }
}

export function createWorkbenchHealthProbeCache(options: WorkbenchHealthCacheOptions = {}): WorkbenchHealthProbeCache {
  return new WorkbenchHealthProbeCache(options);
}

export async function checkWorkbenchHealth<T>(input: CheckWorkbenchHealthOptions<T>): Promise<WorkbenchHealthSnapshot<T>> {
  const timeout = input.signal ? undefined : timeoutSignal(input.timeoutMs);
  const signal = input.signal ?? timeout?.signal;
  const retryCount = positiveIntegerOrNull(input.retryCount) ?? 0;
  const retryDelayMs = positiveIntegerOrNull(input.retryDelayMs) ?? 0;
  const startedAt = new Date().toISOString();
  const next = (count: number, error: unknown): Promise<WorkbenchHealthSnapshot<T>> => {
    if (count >= retryCount || !retryableHealthError(error, signal)) return Promise.resolve(unavailableSnapshot(input.key, error, startedAt));
    return wait(retryDelayMs * (count + 1), signal).then(() => attempt(count + 1)).catch((waitError) => unavailableSnapshot(input.key, waitError, startedAt));
  };
  const attempt = async (count: number): Promise<WorkbenchHealthSnapshot<T>> => {
    try {
      const value = await input.fetcher(signal);
      return {
        key: normalizeKey(input.key),
        state: input.classify?.(value) ?? "ok",
        value,
        error: null,
        observedAt: new Date().toISOString()
      };
    } catch (error) {
      return next(count, error);
    }
  };
  return attempt(0).finally(() => timeout?.clear?.());
}

function timeoutSignal(timeoutMs: number | null | undefined): { signal: AbortSignal; clear?: () => void } | undefined {
  const parsed = positiveIntegerOrNull(timeoutMs);
  if (parsed === null) return undefined;
  const nativeTimeout = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout;
  if (nativeTimeout) {
    try {
      return { signal: nativeTimeout.call(AbortSignal, parsed) };
    } catch {}
  }
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), parsed);
  return { signal: controller.signal, clear: () => clearTimeout(timer) };
}

function wait(ms: number, signal?: AbortSignal | null): Promise<void> {
  return new Promise<void>((resolve, reject) => {
    if (signal?.aborted) {
      reject(new DOMException("Aborted", "AbortError"));
      return;
    }
    const timer = setTimeout(() => {
      signal?.removeEventListener("abort", onAbort);
      resolve();
    }, ms);
    const onAbort = () => {
      clearTimeout(timer);
      reject(new DOMException("Aborted", "AbortError"));
    };
    signal?.addEventListener("abort", onAbort, { once: true });
  });
}

function retryableHealthError(error: unknown, signal?: AbortSignal | null): boolean {
  if (signal?.aborted) return false;
  if (!(error instanceof Error)) return false;
  if (error.name === "AbortError" || error.name === "TimeoutError") return false;
  if (error instanceof TypeError) return true;
  return /network|fetch|econnreset|econnrefused|enotfound|timedout/iu.test(error.message);
}

function unavailableSnapshot<T>(key: string, error: unknown, observedAt: string): WorkbenchHealthSnapshot<T> {
  return { key: normalizeKey(key), state: "unavailable", value: null, error, observedAt };
}

function positiveIntegerOrNull(value: number | null | undefined): number | null {
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
  return Math.trunc(value);
}

function normalizeKey(key: string): string {
  const text = key.trim();
  if (!text) throw new Error("health probe key is required");
  return text;
}

