type RuntimeModelAsset = {
  runtimeUrl: string;
  owned: boolean;
  touchedAt: number;
};

const REGISTRY_KEY = '__ASUKA_3D_MODEL_RUNTIME_ASSETS__';
const MAX_RUNTIME_MODEL_ASSETS = 24;

const getRegistry = (): Map<string, RuntimeModelAsset> => {
  const root = globalThis as any;
  if (!(root[REGISTRY_KEY] instanceof Map)) root[REGISTRY_KEY] = new Map<string, RuntimeModelAsset>();
  return root[REGISTRY_KEY] as Map<string, RuntimeModelAsset>;
};

const normalizeSource = (value: unknown): string => String(value ?? '').trim();

const pruneRegistry = (): void => {
  const registry = getRegistry();
  if (registry.size <= MAX_RUNTIME_MODEL_ASSETS) return;
  const entries = Array.from(registry.entries()).sort((a, b) => a[1].touchedAt - b[1].touchedAt);
  for (const [source, asset] of entries.slice(0, Math.max(0, entries.length - MAX_RUNTIME_MODEL_ASSETS))) {
    if (asset.owned && /^blob:/i.test(asset.runtimeUrl)) {
      try { URL.revokeObjectURL(asset.runtimeUrl); } catch {}
    }
    registry.delete(source);
  }
};

export const register3DModelRuntimeUrl = (sourceInput: unknown, runtimeUrlInput: unknown, options: { owned?: boolean } = {}): string => {
  const source = normalizeSource(sourceInput);
  const runtimeUrl = normalizeSource(runtimeUrlInput);
  if (!source || !runtimeUrl) return runtimeUrl;
  const registry = getRegistry();
  const previous = registry.get(source);
  if (previous?.owned && previous.runtimeUrl !== runtimeUrl && /^blob:/i.test(previous.runtimeUrl)) {
    try { URL.revokeObjectURL(previous.runtimeUrl); } catch {}
  }
  registry.set(source, { runtimeUrl, owned: options.owned === true, touchedAt: Date.now() });
  pruneRegistry();
  return runtimeUrl;
};

export const register3DModelFileRuntimeUrl = (sourceInput: unknown, file: File | Blob | null | undefined): string => {
  const source = normalizeSource(sourceInput);
  if (!source || !file || typeof URL === 'undefined' || typeof URL.createObjectURL !== 'function') return '';
  try { return register3DModelRuntimeUrl(source, URL.createObjectURL(file), { owned: true }); } catch { return ''; }
};

export const resolve3DModelRuntimeUrl = (sourceInput: unknown): string => {
  const source = normalizeSource(sourceInput);
  if (!source) return '';
  const asset = getRegistry().get(source);
  if (!asset) return source;
  asset.touchedAt = Date.now();
  return asset.runtimeUrl || source;
};

const dataUrlToBlobFallback = async (source: string, mimeType: string): Promise<Blob> => {
  const comma = source.indexOf(',');
  if (comma < 0) throw new Error('Invalid data URL');
  const header = source.slice(0, comma);
  const body = source.slice(comma + 1);
  if (!/;base64/i.test(header)) return new Blob([decodeURIComponent(body)], { type: mimeType });
  const binary = atob(body);
  const chunks: Uint8Array[] = [];
  const chunkSize = 256 * 1024;
  for (let offset = 0; offset < binary.length; offset += chunkSize) {
    const end = Math.min(binary.length, offset + chunkSize);
    const bytes = new Uint8Array(end - offset);
    for (let index = offset; index < end; index += 1) bytes[index - offset] = binary.charCodeAt(index);
    chunks.push(bytes);
    if (end < binary.length) await new Promise<void>((resolve) => setTimeout(resolve, 0));
  }
  return new Blob(chunks, { type: mimeType });
};

export const resolveOrCreate3DModelRuntimeUrl = async (sourceInput: unknown, mimeType = 'application/octet-stream'): Promise<string> => {
  const source = normalizeSource(sourceInput);
  if (!source || !/^data:/i.test(source)) return resolve3DModelRuntimeUrl(source);
  const existing = getRegistry().get(source);
  if (existing?.runtimeUrl) {
    existing.touchedAt = Date.now();
    return existing.runtimeUrl;
  }
  try {
    const response = await fetch(source);
    const blob = await response.blob();
    return register3DModelRuntimeUrl(source, URL.createObjectURL(blob), { owned: true });
  } catch {
    try {
      const blob = await dataUrlToBlobFallback(source, mimeType);
      return register3DModelRuntimeUrl(source, URL.createObjectURL(blob), { owned: true });
    } catch {
      return source;
    }
  }
};

export const clear3DModelRuntimeUrls = (): void => {
  const registry = getRegistry();
  for (const asset of registry.values()) {
    if (asset.owned && /^blob:/i.test(asset.runtimeUrl)) {
      try { URL.revokeObjectURL(asset.runtimeUrl); } catch {}
    }
  }
  registry.clear();
};
