// SPEC: PJ2026-0106050514 Workbench实时运行面 draft-2026-06-30-p0-1297-spec-first.
// Responsibility: Storage quota/fallback/cache/migration helpers. Mechanically migrated from OpenCode app/utils/persist.ts with all capacities supplied by runtime policy.

export interface StorageLike {
  readonly length?: number;
  key?(index: number): string | null;
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
  removeItem(key: string): void;
}

export interface SafeStorageRuntimePolicy {
  cacheMaxEntries?: number | null;
  cacheMaxBytes?: number | null;
  evictPrefix?: string | null;
}

export type SafeStorageStatus = "ok" | "missing" | "storage_unavailable" | "parse_error" | "write_error" | "remove_error";

export interface SafeStorageResult<T> {
  ok: boolean;
  status: SafeStorageStatus;
  value: T;
  error?: unknown;
}

export interface PersistTarget {
  storage?: string;
  legacyStorageNames?: string[];
  key: string;
  legacy?: string[];
  migrate?: (value: unknown) => unknown;
}

interface CacheEntry {
  value: string;
  bytes: number;
}

export function createSafeStorageRuntime(policy: SafeStorageRuntimePolicy = {}) {
  const cache = new Map<string, CacheEntry>();
  const cacheTotal = { bytes: 0 };
  const fallback = new Map<string, boolean>();

  const cacheDelete = (key: string) => {
    const entry = cache.get(key);
    if (!entry) return;
    cacheTotal.bytes -= entry.bytes;
    cache.delete(key);
  };

  const cachePrune = () => {
    for (;;) {
      const entryLimitOk = policy.cacheMaxEntries === undefined || policy.cacheMaxEntries === null || cache.size <= policy.cacheMaxEntries;
      const byteLimitOk = policy.cacheMaxBytes === undefined || policy.cacheMaxBytes === null || cacheTotal.bytes <= policy.cacheMaxBytes;
      if (entryLimitOk && byteLimitOk) return;
      const oldest = cache.keys().next().value as string | undefined;
      if (!oldest) return;
      cacheDelete(oldest);
    }
  };

  const cacheSet = (key: string, value: string) => {
    const bytes = value.length * 2;
    if (policy.cacheMaxBytes !== undefined && policy.cacheMaxBytes !== null && bytes > policy.cacheMaxBytes) {
      cacheDelete(key);
      return;
    }
    const entry = cache.get(key);
    if (entry) cacheTotal.bytes -= entry.bytes;
    cache.delete(key);
    cache.set(key, { value, bytes });
    cacheTotal.bytes += bytes;
    cachePrune();
  };

  const cacheGet = (key: string) => {
    const entry = cache.get(key);
    if (!entry) return undefined;
    cache.delete(key);
    cache.set(key, entry);
    return entry.value;
  };

  const fallbackDisabled = (scope: string) => fallback.get(scope) === true;
  const fallbackSet = (scope: string) => fallback.set(scope, true);

  const write = (storage: StorageLike, key: string, value: string) => {
    try {
      storage.setItem(key, value);
      cacheSet(key, value);
      return true;
    } catch (error) {
      if (!isStorageQuotaError(error)) throw error;
    }

    try {
      storage.removeItem(key);
      cacheDelete(key);
      storage.setItem(key, value);
      cacheSet(key, value);
      return true;
    } catch (error) {
      if (!isStorageQuotaError(error)) throw error;
    }

    return evict(storage, key, value, policy.evictPrefix ?? "", cacheDelete, cacheSet);
  };

  const direct = (storage: StorageLike | null | undefined, scope = "direct"): StorageLike => ({
    getItem: (key) => {
      const cached = cacheGet(key);
      if (!storage || fallbackDisabled(scope)) return cached ?? null;
      const stored = safeCall(() => storage.getItem(key), () => fallbackSet(scope));
      if (stored === null || stored === undefined) return cached ?? null;
      cacheSet(key, stored);
      return stored;
    },
    setItem: (key, value) => {
      if (!storage || fallbackDisabled(scope)) return;
      try {
        if (write(storage, key, value)) return;
      } catch {
        fallbackSet(scope);
        return;
      }
      fallbackSet(scope);
    },
    removeItem: (key) => {
      cacheDelete(key);
      if (!storage || fallbackDisabled(scope)) return;
      safeCall(() => storage.removeItem(key), () => fallbackSet(scope));
    }
  });

  const prefixed = (prefix: string, storage: StorageLike | null | undefined): StorageLike => {
    const base = `${prefix}:`;
    const item = (key: string) => base + key;
    const scoped = direct(storage, `prefix:${prefix}`);
    return {
      getItem: (key) => scoped.getItem(item(key)),
      setItem: (key, value) => scoped.setItem(item(key), value),
      removeItem: (key) => scoped.removeItem(item(key))
    };
  };

  return {
    cacheGet,
    cacheSet,
    cacheDelete,
    localStorageDirect: direct,
    localStorageWithPrefix: prefixed,
    cacheSize: () => cache.size,
    fallbackDisabled
  };
}

export function readJsonStorage<T>(storage: StorageLike | null | undefined, key: string, fallback: T): SafeStorageResult<T> {
  if (!storage) return { ok: false, status: "storage_unavailable", value: fallback };
  try {
    const raw = storage.getItem(key);
    if (raw === null) return { ok: true, status: "missing", value: fallback };
    return { ok: true, status: "ok", value: JSON.parse(raw) as T };
  } catch (error) {
    return { ok: false, status: "parse_error", value: fallback, error };
  }
}

export function writeJsonStorage<T>(storage: StorageLike | null | undefined, key: string, value: T): SafeStorageResult<T> {
  if (!storage) return { ok: false, status: "storage_unavailable", value };
  try {
    storage.setItem(key, JSON.stringify(value));
    return { ok: true, status: "ok", value };
  } catch (error) {
    return { ok: false, status: "write_error", value, error };
  }
}

export function removeStorageKey(storage: StorageLike | null | undefined, key: string): SafeStorageResult<null> {
  if (!storage) return { ok: false, status: "storage_unavailable", value: null };
  try {
    storage.removeItem(key);
    return { ok: true, status: "ok", value: null };
  } catch (error) {
    return { ok: false, status: "remove_error", value: null, error };
  }
}

export function normalizePersistedValue(defaults: unknown, raw: string, migrate?: (value: unknown) => unknown): string | undefined {
  const parsed = parseJson(raw);
  if (parsed === undefined) return undefined;
  const migrated = migrate ? migrate(parsed) : parsed;
  return JSON.stringify(mergePersistedDefaults(defaults, migrated));
}

export function migrateLegacyStorage(input: { current: StorageLike; legacyStore?: StorageLike; stores: StorageLike[]; keys: string[]; key: string; defaults: unknown; migrate?: (value: unknown) => unknown }): string | null {
  for (const store of input.stores) {
    const raw = store.getItem(input.key);
    if (raw === null) continue;
    const next = normalizePersistedValue(input.defaults, raw, input.migrate);
    if (next === undefined) {
      store.removeItem(input.key);
      continue;
    }
    input.current.setItem(input.key, next);
    store.removeItem(input.key);
    return next;
  }

  if (!input.legacyStore) return null;
  for (const key of input.keys) {
    const raw = input.legacyStore.getItem(key);
    if (raw === null) continue;
    const next = normalizePersistedValue(input.defaults, raw, input.migrate);
    if (next === undefined) {
      input.legacyStore.removeItem(key);
      continue;
    }
    input.current.setItem(input.key, next);
    input.legacyStore.removeItem(key);
    return next;
  }
  return null;
}

export function removePersistedTarget(target: { storage?: string; legacyStorageNames?: string[]; key: string }, runtime = createSafeStorageRuntime(), storage: StorageLike | null | undefined = globalStorage()): void {
  if (!target.storage) {
    runtime.localStorageDirect(storage).removeItem(target.key);
    return;
  }
  runtime.localStorageWithPrefix(target.storage, storage).removeItem(target.key);
  for (const legacy of target.legacyStorageNames ?? []) runtime.localStorageWithPrefix(legacy, storage).removeItem(target.key);
}

export function isStorageQuotaError(error: unknown): boolean {
  if (typeof DOMException !== "undefined" && error instanceof DOMException) {
    if (error.name === "QuotaExceededError") return true;
    if (error.name === "NS_ERROR_DOM_QUOTA_REACHED") return true;
    if (error.name === "QUOTA_EXCEEDED_ERR") return true;
    if (error.code === 22 || error.code === 1014) return true;
    return false;
  }
  if (!error || typeof error !== "object") return false;
  const name = (error as { name?: string }).name;
  if (name === "QuotaExceededError" || name === "NS_ERROR_DOM_QUOTA_REACHED") return true;
  if (name && /quota/iu.test(name)) return true;
  const code = (error as { code?: number }).code;
  if (code === 22 || code === 1014) return true;
  const message = (error as { message?: string }).message;
  return typeof message === "string" && /quota/iu.test(message);
}

export function mergePersistedDefaults(defaults: unknown, value: unknown): unknown {
  if (value === undefined) return defaults;
  if (value === null) return value;
  if (Array.isArray(defaults)) return Array.isArray(value) ? value : defaults;
  if (isRecord(defaults)) {
    if (!isRecord(value)) return defaults;
    const result: Record<string, unknown> = { ...defaults };
    for (const key of Object.keys(value)) {
      result[key] = key in defaults ? mergePersistedDefaults((defaults as Record<string, unknown>)[key], value[key]) : value[key];
    }
    return result;
  }
  return value;
}

function evict(storage: StorageLike, keep: string, value: string, prefix: string, cacheDelete: (key: string) => void, cacheSet: (key: string, value: string) => void): boolean {
  const total = storage.length ?? 0;
  const items: Array<{ key: string; size: number }> = [];
  for (const index of Array.from({ length: total }, (_, item) => item)) {
    const name = storage.key?.(index);
    if (!name) continue;
    if (prefix && !name.startsWith(prefix)) continue;
    if (name === keep) continue;
    const stored = storage.getItem(name);
    items.push({ key: name, size: stored?.length ?? 0 });
  }
  items.sort((a, b) => b.size - a.size);
  for (const item of items) {
    storage.removeItem(item.key);
    cacheDelete(item.key);
    try {
      storage.setItem(keep, value);
      cacheSet(keep, value);
      return true;
    } catch (error) {
      if (!isStorageQuotaError(error)) throw error;
    }
  }
  return false;
}

function parseJson(value: string): unknown | undefined {
  try {
    return JSON.parse(value);
  } catch {
    return undefined;
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function safeCall<T>(fn: () => T, onError: () => void): T | null {
  try {
    return fn();
  } catch {
    onError();
    return null;
  }
}

function globalStorage(): StorageLike | null {
  if (typeof globalThis === "undefined") return null;
  return (globalThis as typeof globalThis & { localStorage?: StorageLike }).localStorage ?? null;
}

