
import { useAsukaStore } from '../store/asukaStore';
import { getCanvasVisualScale } from './canvas/visualScaling';
import type { AsukaUploadModuleConfig, AsukaUploadStrategy } from '../types/options';
import { DEFAULT_ASUKA_UPLOAD_STRATEGY } from '../types/options';
import { normalizePriceInput, type AsukaPriceInput } from './pricingValue';
import { getCurrentRenderEngineKind, getRuntimeRenderEngine } from './renderEngine';


let fabricRuntimePromise: Promise<any> | null = null;

async function getFabricRuntime(): Promise<any> {
  if (!fabricRuntimePromise) {
    fabricRuntimePromise = import('./fabric').then((module) => (module as any).fabric ?? module);
  }
  return fabricRuntimePromise;
}

function applyObjectPrice(target: any, price?: AsukaPriceInput) {
  const normalized = normalizePriceInput(price);
  if (typeof normalized === 'number' && Number.isFinite(normalized) && target) {
    try { (target as any).price = normalized; } catch {}
  }
}

function applyObjectPriceDeep(target: any, price?: AsukaPriceInput) {
  applyObjectPrice(target, price);
  try {
    const children = target?.getObjects?.() ?? target?._objects ?? [];
    if (Array.isArray(children)) children.forEach((child: any) => applyObjectPriceDeep(child, price));
  } catch {}
}

function applyItemGroupMetadata(target: any, itemGroup?: string) {
  if (!target) return;
  const group = String(itemGroup ?? '').trim();
  if (!group) return;
  try { target.itemGroup = target.itemGroup ?? group; } catch {}
  if (group.toLowerCase() === 'designs' || group.toLowerCase() === 'design') {
    try { target._asukaDesign = true; } catch {}
  }
}

export type AddImageOptions = {
  /** Optional one-off upload configuration override for file-based adds. */
  upload?: AsukaUploadModuleConfig;
  left?: number;
  top?: number;
  center?: boolean;
  selectable?: boolean;
  evented?: boolean;
  /** Logical grouping for UX/features (layers filtering, colors scope, etc.) */
  itemGroup?: 'Designs' | 'Images' | 'Text' | 'Tools' | 'Other';
  /** Optional base price for the created object (used by the pricing engine). */
  price?: AsukaPriceInput;
  /** Logical upload source. When set on URL adds, the URL is also registered in the Upload module list. */
  source?: string;
  /** Optional media/input mode propagated to the Upload module list. */
  mode?: string;
  /** Optional display name for URL-based uploads. */
  name?: string;
  /** Optional preview URL for URL-based uploads. */
  previewUrl?: string;
  /** Optional metadata stored with the Upload module asset. */
  metadata?: unknown;
  /** Set to false to avoid registering URL adds in the shared Upload module list. */
  registerUploadedAsset?: boolean;
};

function canFallbackAddWithoutDesignerGuard(designer: any): boolean {
  // When the Asuka designer exposes addObject(), a null return can mean a real
  // permission denial. Do not bypass that by adding directly to Fabric.
  return typeof designer?.addObject !== 'function';
}

async function createFabricImageFromUrl(url: string): Promise<any> {
  const fabric = await getFabricRuntime();
  return await fabric.Image.fromURL(url, { crossOrigin: 'anonymous' });
}

function getDesignerDocumentCenter(designer: any, canvas: any): { x: number; y: number } {
  try {
    const center = designer?.getDocumentCenter?.();
    if (center && Number.isFinite(center.x) && Number.isFinite(center.y)) {
      return { x: Number(center.x), y: Number(center.y) };
    }
  } catch {}
  try {
    const zones = designer?.getDrawingZones?.();
    const activeId = designer?.getActiveDrawingZone?.();
    const active = Array.isArray(zones) ? (zones.find((z: any) => String(z?.id) === String(activeId)) ?? null) : null;
    if (active && Number.isFinite(Number(active.x)) && Number.isFinite(Number(active.y)) && Number.isFinite(Number(active.width)) && Number.isFinite(Number(active.height))) {
      return {
        x: Number(active.x) + Number(active.width) / 2,
        y: Number(active.y) + Number(active.height) / 2,
      };
    }
  } catch {}
  const w = Number(canvas?.getWidth?.() ?? canvas?.width ?? 0) || 0;
  const h = Number(canvas?.getHeight?.() ?? canvas?.height ?? 0) || 0;
  return { x: w / 2, y: h / 2 };
}

function positionNewObject(designer: any, canvas: any, obj: any, opts: AddImageOptions = {}) {
  const hasExplicitLeft = Number.isFinite(Number(opts?.left));
  const hasExplicitTop = Number.isFinite(Number(opts?.top));
  if (hasExplicitLeft || hasExplicitTop) {
    const left = hasExplicitLeft ? Number(opts.left) : 0;
    const top = hasExplicitTop ? Number(opts.top) : 0;
    if (opts.center !== false) {
      try { obj.set?.({ originX: 'center', originY: 'center', left, top }); } catch { obj.set?.({ left, top }); }
    } else {
      obj.set?.({ left, top });
    }
    return;
  }
  const center = getDesignerDocumentCenter(designer, canvas);
  try {
    obj.set?.({ originX: 'center', originY: 'center', left: center.x, top: center.y });
  } catch {
    obj.set?.({ left: center.x, top: center.y });
  }
}



function ensureReadableInsertedObject(canvas: any, obj: any, opts: AddImageOptions = {}) {
  if (!canvas || !obj) return;
  const hasExplicitGeometry = [opts.left, opts.top, (opts as any)?.scaleX, (opts as any)?.scaleY, (opts as any)?.width, (opts as any)?.height]
    .some((v) => typeof v === 'number' && Number.isFinite(v));
  if (hasExplicitGeometry) return;
  const visualScale = Math.max(0.01, Number(getCanvasVisualScale(canvas)) || 1);
  const lower = canvas?.getElement?.() ?? canvas?.lowerCanvasEl ?? null;
  const hostWidth = Number(lower?.clientWidth || canvas?.getWidth?.() || 0) || 0;
  const hostHeight = Number(lower?.clientHeight || canvas?.getHeight?.() || 0) || 0;
  const targetScreen = Math.min(280, Math.max(140, Math.min(hostWidth, hostHeight) * 0.22 || 180));
  try { obj.setCoords?.(); } catch {}
  const rect = obj.getBoundingRect?.(false, true) ?? null;
  const currentScreen = rect ? Math.max(Number(rect.width) || 0, Number(rect.height) || 0) * visualScale : 0;
  if (currentScreen >= targetScreen * 0.9) return;
  const ratio = Math.min(16, Math.max(1, targetScreen / Math.max(1, currentScreen || 1)));
  try { obj.scaleX = (Number(obj.scaleX) || 1) * ratio; } catch {}
  try { obj.scaleY = (Number(obj.scaleY) || 1) * ratio; } catch {}
  try { obj.setCoords?.(); } catch {}
}

type ResolvedImageSource = {
  url: string;
  revoke?: () => void;
};

function getUploadConfigFromStore(): AsukaUploadModuleConfig {
  try {
    return ((useAsukaStore.getState() as any)?.moduleConfig?.upload ?? {}) as AsukaUploadModuleConfig;
  } catch {
    return {};
  }
}

function hasRemoteUploadTarget(config: AsukaUploadModuleConfig): boolean {
  return typeof config.handler === 'function' || Boolean(String(config.endpoint ?? '').trim());
}

function getStrategyFromConfig(config: AsukaUploadModuleConfig, fallback: AsukaUploadStrategy): AsukaUploadStrategy {
  return config.strategy ?? (hasRemoteUploadTarget(config) ? 'remote' : fallback);
}

export function resolveUploadConfig(opts: AddImageOptions = {}): AsukaUploadModuleConfig {
  return {
    ...(getUploadConfigFromStore() ?? {}),
    ...(opts?.upload ?? {}),
  };
}

/** Return the strategy that will actually be used after boot config is applied. */
export function resolveUploadStrategy(
  upload?: AsukaUploadModuleConfig,
  fallback: AsukaUploadStrategy = DEFAULT_ASUKA_UPLOAD_STRATEGY,
): AsukaUploadStrategy {
  return getStrategyFromConfig(resolveUploadConfig({ upload }), fallback);
}

export function readFileAsDataUrl(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const result = reader.result;
      if (typeof result === 'string' && result) {
        resolve(result);
        return;
      }
      reject(new Error('Could not convert the uploaded file to a data URL.'));
    };
    reader.onerror = () => reject(reader.error ?? new Error('Could not read the uploaded file.'));
    reader.readAsDataURL(file);
  });
}

async function parseUploadResponse(response: Response): Promise<unknown> {
  const text = await response.text();
  if (!text.trim()) return null;
  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
}

function multipartUploadHeaders(headers: Record<string, string> | undefined): Record<string, string> | undefined {
  if (!headers) return undefined;
  const next = Object.fromEntries(
    Object.entries(headers).filter(([name]) => name.toLowerCase() !== 'content-type'),
  );
  return Object.keys(next).length ? next : undefined;
}

function uploadErrorMessage(payload: unknown, status: number): string {
  const visit = (value: unknown, depth = 0): string | null => {
    if (depth > 5 || value == null) return null;
    if (typeof value === 'string') {
      const message = value.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
      return message && message.length <= 500 ? message : null;
    }
    if (typeof value !== 'object') return null;

    const source = value as Record<string, unknown>;
    for (const key of ['message', 'detail', 'error_description']) {
      const message = visit(source[key], depth + 1);
      if (message) return message;
    }
    for (const key of ['error', 'data', 'result']) {
      const message = visit(source[key], depth + 1);
      if (message) return message;
    }
    return null;
  };

  return visit(payload) ?? `Upload failed with status ${status}.`;
}

function resolveRemoteUrlFromPayload(payload: unknown, config: AsukaUploadModuleConfig): string | null {
  try {
    if (typeof config.mapResponseToUrl === 'function') {
      const mapped = config.mapResponseToUrl(payload);
      return typeof mapped === 'string' && mapped ? mapped : null;
    }
  } catch {
    return null;
  }
  if (typeof payload === 'string' && payload) return payload;
  if (payload && typeof payload === 'object') {
    const candidate =
      (payload as any).url ??
      (payload as any).dataUrl ??
      (payload as any).dataURL ??
      (payload as any).src ??
      (payload as any).value ??
      (payload as any).fileUrl ??
      (payload as any).downloadUrl;
    if (typeof candidate === 'string' && candidate) return candidate;
    const nested = (payload as any).data ?? (payload as any).result ?? null;
    if (nested && nested !== payload) return resolveRemoteUrlFromPayload(nested, config);
  }
  return null;
}


function isFileLike(value: unknown): value is File {
  return typeof File !== 'undefined' && value instanceof File;
}

function normalizeUploadString(value: unknown): string | undefined {
  if (typeof value !== 'string') return undefined;
  const trimmed = value.trim();
  return trimmed || undefined;
}

function getFileNameFromUrl(url: string): string {
  try {
    const clean = String(url || '').split('?')[0].split('#')[0];
    const name = decodeURIComponent(clean.slice(clean.lastIndexOf('/') + 1));
    return name || 'Selected image';
  } catch {
    return 'Selected image';
  }
}

export type ResolvedUploadSelectionItem = {
  id?: string;
  file?: File;
  url?: string;
  previewUrl?: string;
  name?: string;
  mimeType?: string;
  size?: number;
  lastModified?: number;
  source?: string;
  mode?: string;
  strategy?: string;
  metadata?: unknown;
};

export function hasConfiguredUploadPicker(upload?: AsukaUploadModuleConfig): boolean {
  const config = { ...(getUploadConfigFromStore() ?? {}), ...(upload ?? {}) } as AsukaUploadModuleConfig;
  return typeof config.selectHandler === 'function';
}

function normalizeUploadSelectionItem(item: unknown, config: AsukaUploadModuleConfig = {}, fallback: { source?: string; mode?: string } = {}): ResolvedUploadSelectionItem | null {
  if (!item) return null;

  if (isFileLike(item)) {
    return {
      file: item,
      name: item.name,
      mimeType: item.type,
      size: item.size,
      lastModified: item.lastModified,
      source: fallback.source,
      mode: fallback.mode,
    };
  }

  if (typeof item === 'string') {
    const url = normalizeUploadString(item);
    return url ? { url, previewUrl: url, name: getFileNameFromUrl(url), source: fallback.source, mode: fallback.mode, strategy: 'selected-url' } : null;
  }

  if (typeof item !== 'object') return null;

  const raw = item as any;
  const directUrl = resolveRemoteUrlFromPayload(raw, config) ?? undefined;
  const previewUrl = normalizeUploadString(raw.previewUrl) ?? directUrl;
  const file = isFileLike(raw.file) ? raw.file : undefined;
  const url = directUrl ?? normalizeUploadString(raw.url) ?? normalizeUploadString(raw.src) ?? normalizeUploadString(raw.value);
  const name =
    normalizeUploadString(raw.name) ??
    normalizeUploadString(raw.filename) ??
    normalizeUploadString(file?.name) ??
    (url ? getFileNameFromUrl(url) : undefined) ??
    'Selected image';

  if (!file && !url && !previewUrl) return null;

  const metadata = raw.metadata ?? (raw.data && raw.data !== raw ? raw.data : undefined) ?? raw;
  return {
    id: normalizeUploadString(raw.id),
    file,
    url,
    previewUrl,
    name,
    mimeType: normalizeUploadString(raw.mimeType) ?? normalizeUploadString(raw.type) ?? normalizeUploadString(file?.type),
    size: Number.isFinite(Number(raw.size)) ? Number(raw.size) : (Number.isFinite(Number(file?.size)) ? Number(file?.size) : undefined),
    lastModified: Number.isFinite(Number(raw.lastModified)) ? Number(raw.lastModified) : (Number.isFinite(Number(file?.lastModified)) ? Number(file.lastModified) : undefined),
    source: normalizeUploadString(raw.source) ?? fallback.source,
    mode: normalizeUploadString(raw.mode) ?? fallback.mode,
    strategy: normalizeUploadString(raw.strategy) ?? (url ? 'selected-url' : undefined),
    metadata,
  };
}

function looksLikeUploadSelectionItem(payload: unknown, config: AsukaUploadModuleConfig = {}): boolean {
  if (!payload) return false;
  if (isFileLike(payload)) return true;
  if (typeof payload === 'string') return Boolean(normalizeUploadString(payload));
  if (typeof payload !== 'object') return false;

  const raw = payload as any;
  if (isFileLike(raw.file)) return true;
  if (resolveRemoteUrlFromPayload(raw, config)) return true;
  return Boolean(normalizeUploadString(raw.previewUrl) || normalizeUploadString(raw.name) || normalizeUploadString(raw.filename));
}

function collectUploadSelectionPayloadItems(payload: unknown, config: AsukaUploadModuleConfig = {}, depth = 0): unknown[] {
  if (!payload || depth > 8) return [];
  if (Array.isArray(payload)) return payload.flatMap((item) => collectUploadSelectionPayloadItems(item, config, depth + 1));
  if (looksLikeUploadSelectionItem(payload, config)) return [payload];
  if (typeof payload !== 'object') return [payload];

  const raw = payload as any;
  const containerKeys = ['items', 'media', 'attachments', 'selection', 'files', 'assets'];
  for (const key of containerKeys) {
    if (raw[key] !== undefined) {
      const nested = collectUploadSelectionPayloadItems(raw[key], config, depth + 1);
      if (nested.length) return nested;
    }
  }

  const responseKeys = ['data', 'result'];
  for (const key of responseKeys) {
    if (raw[key] !== undefined && raw[key] !== payload) {
      const nested = collectUploadSelectionPayloadItems(raw[key], config, depth + 1);
      if (nested.length) return nested;
    }
  }

  return [payload];
}

function normalizeUploadSelectionResult(payload: unknown, config: AsukaUploadModuleConfig = {}, fallback: { source?: string; mode?: string } = {}): ResolvedUploadSelectionItem[] {
  const rawItems = collectUploadSelectionPayloadItems(payload, config);
  const items: ResolvedUploadSelectionItem[] = [];
  for (const raw of rawItems) {
    const normalized = normalizeUploadSelectionItem(raw, config, fallback);
    if (normalized) items.push(normalized);
  }
  return items;
}

export async function openConfiguredUploadPicker(upload: AsukaUploadModuleConfig | undefined, context: { accept?: string; multiple?: boolean; source?: string; mode?: string; kind?: string } = {}): Promise<ResolvedUploadSelectionItem[] | null> {
  const config = { ...(getUploadConfigFromStore() ?? {}), ...(upload ?? {}) } as AsukaUploadModuleConfig;
  const fallback = { source: context.source, mode: context.mode };

  if (typeof config.selectHandler === 'function') {
    const payload = await config.selectHandler({
      config,
      accept: context.accept,
      multiple: context.multiple,
      source: context.source,
      mode: context.mode,
      kind: context.kind,
    });
    return normalizeUploadSelectionResult(payload, config, fallback);
  }

  return null;
}

export function registerUploadSelection(selection: ResolvedUploadSelectionItem, fallback: { source?: string; mode?: string; strategy?: string } = {}) {
  try {
    return (useAsukaStore.getState() as any)?.registerUploadedAsset?.({
      id: selection.id,
      file: selection.file,
      url: selection.url,
      previewUrl: selection.previewUrl,
      name: selection.name,
      mimeType: selection.mimeType,
      size: selection.size,
      lastModified: selection.lastModified,
      source: selection.source ?? fallback.source,
      mode: selection.mode ?? fallback.mode,
      strategy: selection.strategy ?? fallback.strategy,
      status: 'ready',
      metadata: selection.metadata,
    });
  } catch {
    return null;
  }
}

export async function resolveUploadSelectionUrl(selection: ResolvedUploadSelectionItem, opts: ResolveUploadedFileUrlOptions = {}): Promise<string> {
  if (selection.file) {
    return await resolveUploadedFileUrl(selection.file, opts);
  }
  const url = normalizeUploadString(selection.url) ?? normalizeUploadString(selection.previewUrl);
  if (!url) throw new Error('Selected media did not expose a usable URL.');
  registerUploadSelection(selection, { source: opts.source, mode: opts.mode, strategy: 'selected-url' });
  return url;
}

function maybeRegisterUrlAsUploadedAsset(url: string, opts: AddImageOptions = {}) {
  if ((opts as any)?.registerUploadedAsset === false) return;
  const source = normalizeUploadString((opts as any)?.source);
  if (!source) return;
  registerUploadSelection({
    url,
    previewUrl: normalizeUploadString((opts as any)?.previewUrl) ?? url,
    name: normalizeUploadString((opts as any)?.name) ?? getFileNameFromUrl(url),
    source,
    mode: normalizeUploadString((opts as any)?.mode),
    strategy: 'selected-url',
    metadata: (opts as any)?.metadata,
  }, { source, mode: normalizeUploadString((opts as any)?.mode), strategy: 'selected-url' });
}

async function uploadFileWithHandler(file: File, config: AsukaUploadModuleConfig, context: { source?: string; mode?: string } = {}): Promise<string> {
  if (typeof config.handler !== 'function') {
    throw new Error('Upload handler is not configured.');
  }

  const payload = await config.handler(file, {
    config,
    source: context.source,
    mode: context.mode,
  });

  const resolvedUrl = resolveRemoteUrlFromPayload(payload, config);
  if (!resolvedUrl) {
    throw new Error('The upload handler response could not be mapped to a usable image URL.');
  }
  return resolvedUrl;
}

async function uploadFileToRemote(file: File, config: AsukaUploadModuleConfig, context: { source?: string; mode?: string } = {}): Promise<string> {
  if (typeof config.handler === 'function') {
    return await uploadFileWithHandler(file, config, context);
  }

  const endpoint = String(config.endpoint ?? '').trim();
  if (!endpoint) {
    throw new Error('Upload strategy "remote" requires an upload.endpoint value or upload.handler.');
  }

  const formData = new FormData();
  formData.append(config.fieldName || 'file', file, file.name);
  if (context.source) formData.append('asuka_source', context.source);
  if (context.mode) formData.append('asuka_mode', context.mode);

  const response = await fetch(endpoint, {
    method: config.method || 'POST',
    body: formData,
    // FormData must own Content-Type so the browser can include its multipart boundary.
    headers: multipartUploadHeaders(config.headers),
    credentials: config.credentials,
  });

  const payload = await parseUploadResponse(response);
  if (!response.ok) {
    throw new Error(uploadErrorMessage(payload, response.status));
  }

  const resolvedUrl = resolveRemoteUrlFromPayload(payload, config);
  if (!resolvedUrl) {
    throw new Error('The upload response could not be mapped to a usable image URL.');
  }
  return resolvedUrl;
}

export type ResolveUploadedFileUrlOptions = {
  /** One-off upload policy override. */
  upload?: AsukaUploadModuleConfig;
  /** Fallback strategy when neither strategy, endpoint nor handler is configured. */
  defaultStrategy?: AsukaUploadStrategy;
  /** Caller identifier used by custom upload handlers. */
  source?: string;
  /** Media input mode used by custom upload handlers. */
  mode?: string;
  /** When false, the resolved file is not registered in the shared Upload module list. */
  registerUploadedAsset?: boolean;
};

function registerResolvedUploadedAsset(file: File, url: string, opts: ResolveUploadedFileUrlOptions = {}, strategy?: AsukaUploadStrategy) {
  if ((opts as any)?.registerUploadedAsset === false) return;
  try {
    (useAsukaStore.getState() as any)?.registerUploadedAsset?.({
      file,
      url,
      source: opts.source ?? 'upload-pipeline',
      mode: opts.mode,
      strategy,
      status: 'ready',
    });
  } catch {}
}

async function resolveImageSourceFromFile(file: File, opts: AddImageOptions & ResolveUploadedFileUrlOptions = {}): Promise<ResolvedImageSource> {
  const uploadConfig = resolveUploadConfig(opts);
  const strategy = getStrategyFromConfig(uploadConfig, opts.defaultStrategy ?? DEFAULT_ASUKA_UPLOAD_STRATEGY);

  if (strategy === 'base64') {
    const dataUrl = await readFileAsDataUrl(file);
    registerResolvedUploadedAsset(file, dataUrl, opts, 'base64');
    return { url: dataUrl };
  }

  if (strategy === 'remote') {
    const remoteUrl = await uploadFileToRemote(file, uploadConfig, { source: opts.source, mode: opts.mode });
    registerResolvedUploadedAsset(file, remoteUrl, opts, 'remote');
    return { url: remoteUrl };
  }

  const blobUrl = URL.createObjectURL(file);
  registerResolvedUploadedAsset(file, blobUrl, opts, 'objectUrl');
  return {
    url: blobUrl,
    revoke: () => {
      setTimeout(() => {
        try {
          URL.revokeObjectURL(blobUrl);
        } catch {}
      }, 5_000);
    },
  };
}

export async function resolveUploadedFileUrl(file: File, opts: ResolveUploadedFileUrlOptions = {}): Promise<string> {
  const source = await resolveImageSourceFromFile(file, opts as AddImageOptions & ResolveUploadedFileUrlOptions);
  // This helper is used by persisted form fields, so never schedule revocation here.
  // When objectUrl is explicitly selected, the caller owns the blob URL lifecycle.
  return source.url;
}

async function createFileFromRemoteUrl(url: string): Promise<File> {
  const response = await fetch(url, { mode: 'cors' });
  if (!response.ok) {
    throw new Error(`Could not download the selected image (status ${response.status}).`);
  }

  const blob = await response.blob();
  const contentType = blob.type || response.headers.get('content-type') || 'image/*';
  const safeContentType = typeof contentType === 'string' && contentType ? contentType : 'image/*';

  const extensionByType: Record<string, string> = {
    'image/jpeg': 'jpg',
    'image/jpg': 'jpg',
    'image/png': 'png',
    'image/webp': 'webp',
    'image/gif': 'gif',
    'image/svg+xml': 'svg',
    'image/avif': 'avif',
  };

  const extension = extensionByType[safeContentType] || 'img';
  return new File([blob], `asuka-remote-image.${extension}`, { type: safeContentType });
}

/**
 * Resolve a remote image through the active upload policy.
 *
 * A direct URL is kept for the explicit `objectUrl` strategy. Base64 and remote
 * strategies first download the image as a File. Browser download/CORS failures
 * fall back to the original URL, while configured server upload failures remain
 * visible to the caller.
 */
export async function resolveRemoteImageUrlWithUpload(url: string, opts: AddImageOptions = {}): Promise<string> {
  const normalizedUrl = String(url ?? '').trim();
  if (!normalizedUrl) throw new Error('A remote image URL is required.');

  const uploadConfig = resolveUploadConfig(opts);
  const strategy = getStrategyFromConfig(uploadConfig, DEFAULT_ASUKA_UPLOAD_STRATEGY);
  if (strategy === 'objectUrl') return normalizedUrl;

  let file: File;
  try {
    file = await createFileFromRemoteUrl(normalizedUrl);
  } catch {
    return normalizedUrl;
  }

  return await resolveUploadedFileUrl(file, {
    upload: uploadConfig,
    defaultStrategy: DEFAULT_ASUKA_UPLOAD_STRATEGY,
    source: opts.source ?? 'remote-image',
    mode: opts.mode,
    registerUploadedAsset: opts.registerUploadedAsset,
  });
}

/**
 * Add an image from a remote URL while attempting to reuse the configured file upload strategy.
 *
 * This is primarily used by the built-in Images module (Pixabay, Pexels, Facebook). When the
 * active upload strategy is `base64` or `remote`, the URL is downloaded as a File first so the
 * same upload pipeline can be reused. If the remote download fails (for example because of CORS),
 * the function falls back to a direct URL add.
 */
export async function addImageFromRemoteUrlWithUpload(designer: any, url: string, opts: AddImageOptions = {}) {
  const resolvedUrl = await resolveRemoteImageUrlWithUpload(url, opts);
  await addImageFromUrl(designer, resolvedUrl, opts);
}

/**
 * Add an image to the current canvas from an URL.
 */
export async function addImageFromUrl(designer: any, url: string, opts: AddImageOptions = {}) {
  if (getCurrentRenderEngineKind() === '3d') {
    maybeRegisterUrlAsUploadedAsset(url, opts);
    const engine3d = getRuntimeRenderEngine('3d');
    if (engine3d) {
      await engine3d.objects.addImage({
        url,
        name: opts.name,
        price: opts.price,
        metadata: { ...(opts.metadata && typeof opts.metadata === 'object' ? opts.metadata as Record<string, unknown> : {}), source: opts.source, mode: opts.mode, itemGroup: opts.itemGroup ?? 'Images' },
      });
    }
    return;
  }

  const canvas: any = designer?.getCanvas?.();
  if (!canvas) return;

  maybeRegisterUrlAsUploadedAsset(url, opts);

  const img = await createFabricImageFromUrl(url);
  if (!img) return;

  img.set?.({
    selectable: opts.selectable ?? true,
    evented: opts.evented ?? true,
  });
  positionNewObject(designer, canvas, img, opts);
  ensureReadableInsertedObject(canvas, img, opts);
  applyItemGroupMetadata(img, opts.itemGroup ?? 'Images');

  // Ensure images behave like other Asuka objects (zone assignment + clip + custom controls)
  try {
    (designer as any)?._asukaPrepareExternalObject?.(img);
  } catch {}

  (img as any).itemGroup = (img as any).itemGroup ?? (opts.itemGroup ?? 'Images');

  // Optional pricing metadata (used by pricing engine)
  applyObjectPrice(img, opts.price);

  const added = (designer as any)?.addObject?.(img, {
    setActive: true,
    commitHistory: false,
    itemGroup: (img as any).itemGroup ?? (opts.itemGroup ?? 'Images'),
    price: opts.price,
  });

  if (!added && canFallbackAddWithoutDesignerGuard(designer)) {
    canvas.add?.(img);
    canvas.setActiveObject?.(img);
  }

  try { (added ?? img).setCoords?.(); } catch {}

  canvas.requestRenderAll?.();
}

/**
 * Add an image to the current canvas from a File object.
 *
 * The effective strategy comes from `opts.upload` first, then from
 * `moduleConfig.upload`, and defaults to `base64`.
 */
export async function addImageFromFile(designer: any, file: File, opts: AddImageOptions = {}) {
  if (getCurrentRenderEngineKind() === '3d') {
    const uploadConfig = resolveUploadConfig(opts);
    const source = await resolveImageSourceFromFile(file, { source: 'canvas', ...(opts as any) });
    try {
      await addImageFromUrl(designer, source.url, { ...opts, name: opts.name ?? file.name, upload: uploadConfig });
    } finally {
      try { source.revoke?.(); } catch {}
    }
    return;
  }

  const source = await resolveImageSourceFromFile(file, { source: 'canvas', ...(opts as any) });
  try {
    await addImageFromUrl(designer, source.url, opts);
  } finally {
    try {
      source.revoke?.();
    } catch {}
  }
}

async function loadSvgFromUrl(url: string): Promise<{ objects: any[]; options: any } | null> {
  const fabric = await getFabricRuntime();
  if (typeof (fabric as any).loadSVGFromURL !== 'function') return null;
  try {
    const result = await (fabric as any).loadSVGFromURL(url, { crossOrigin: 'anonymous' });
    return {
      objects: Array.isArray(result?.objects) ? result.objects : [],
      options: result?.options && typeof result.options === 'object' ? result.options : {},
    };
  } catch {
    return null;
  }
}

async function loadSvgFromString(svgText: string): Promise<{ objects: any[]; options: any } | null> {
  const fabric = await getFabricRuntime();
  if (typeof (fabric as any).loadSVGFromString !== 'function') return null;
  try {
    const result = await (fabric as any).loadSVGFromString(svgText);
    return {
      objects: Array.isArray(result?.objects) ? result.objects : [],
      options: result?.options && typeof result.options === 'object' ? result.options : {},
    };
  } catch {
    return null;
  }
}

/**
 * Add a SVG to the current canvas from a raw SVG string.
 *
 * This groups SVG objects into a single Fabric object when possible.
 */
export async function addSvgFromString(designer: any, svgText: string, opts: AddImageOptions = {}) {
  const canvas: any = designer?.getCanvas?.();
  if (!canvas) return;

  const res = await loadSvgFromString(svgText);
  if (!res) return;

  const objects = res.objects ?? [];
  const options = res.options ?? {};
  if (!objects || objects.length === 0) return;

  let svgObj: any = null;
  const fabric = await getFabricRuntime();

  // Try Fabric util helper
  try {
    const groupFn = (fabric as any).util?.groupSVGElements;
    if (typeof groupFn === 'function') {
      svgObj = groupFn(objects, options);
    }
  } catch {
    // ignore
  }

  // Fallback: create a Group
  if (!svgObj) {
    try {
      const GroupCtor = (fabric as any).Group;
      if (typeof GroupCtor === 'function') svgObj = new GroupCtor(objects, options);
    } catch {
      svgObj = null;
    }
  }

  // Last fallback: add all objects individually.
  // Apply itemGroup first so design SVG pieces are eligible for scoped auto-fit.
  if (!svgObj) {
    for (const o of objects) {
      applyItemGroupMetadata(o, opts.itemGroup ?? 'Images');
      try {
        (designer as any)?._asukaPrepareExternalObject?.(o);
      } catch {}
    }
    for (const o of objects) {
      applyObjectPriceDeep(o, opts.price);
      const added = (designer as any)?.addObject?.(o, { setActive: false, commitHistory: false, itemGroup: (o as any).itemGroup ?? (opts.itemGroup ?? 'Images'), price: opts.price });
      if (!added && canFallbackAddWithoutDesignerGuard(designer)) canvas.add?.(o);
    }
    canvas.requestRenderAll?.();
    return;
  }

  try {
    if (typeof svgObj._updateObjectsCoords === 'function') svgObj._updateObjectsCoords();
  } catch {}

  svgObj.set?.({
    selectable: opts.selectable ?? true,
    evented: opts.evented ?? true,
  });
  positionNewObject(designer, canvas, svgObj, opts);
  ensureReadableInsertedObject(canvas, svgObj, opts);
  applyItemGroupMetadata(svgObj, opts.itemGroup ?? 'Images');

  try {
    (designer as any)?._asukaPrepareExternalObject?.(svgObj);
  } catch {}

  (svgObj as any).itemGroup = (svgObj as any).itemGroup ?? (opts.itemGroup ?? 'Images');

  // Optional pricing metadata (used by pricing engine)
  applyObjectPriceDeep(svgObj, opts.price);

  try {
    svgObj.setCoords?.();
  } catch {}

  const added = (designer as any)?.addObject?.(svgObj, {
    setActive: true,
    commitHistory: false,
    itemGroup: (svgObj as any).itemGroup ?? (opts.itemGroup ?? 'Images'),
    price: opts.price,
  });

  if (!added && canFallbackAddWithoutDesignerGuard(designer)) {
    canvas.add?.(svgObj);
    canvas.setActiveObject?.(svgObj);
  }

  try {
    if (typeof (added ?? svgObj).setCoords === 'function') (added ?? svgObj).setCoords();
  } catch {}

  canvas.requestRenderAll?.();
}

/**
 * Add a SVG to the current canvas from an URL.
 *
 * This groups SVG objects into a single Fabric object when possible.
 */
export async function addSvgFromUrl(designer: any, url: string, opts: AddImageOptions = {}) {
  const canvas: any = designer?.getCanvas?.();
  if (!canvas) return;

  // Prefer fetching ourselves and using loadSVGFromString.
  // This avoids edge cases where loadSVGFromURL returns empty objects (CORS/proxy/html responses, etc.).
  let res: { objects: any[]; options: any } | null = null;
  try {
    const resp = await fetch(url, { mode: 'cors' });
    const text = await resp.text();
    if (text && text.includes('<svg')) {
      res = await loadSvgFromString(text);
    }
  } catch {
    // ignore; fall back to loadSVGFromURL
  }

  if (!res) res = await loadSvgFromUrl(url);
  if (!res) return;

  const objects = res.objects ?? [];
  // Some SVGs (especially those relying on viewBox) may yield options.width/height = 0.
  // Passing these through to grouping can create a 0x0 object. Sanitize them.
  const options = { ...(res.options ?? {}) } as any;
  if (!options.width) delete options.width;
  if (!options.height) delete options.height;

  if (!objects.length) {
    // Nothing parsed: do not add an invisible 0x0 object.
    return;
  }

  let svgObj: any = null;
  const fabric = await getFabricRuntime();
  // Try Fabric util helper
  try {
    const groupFn = (fabric as any).util?.groupSVGElements;
    if (typeof groupFn === 'function') {
      svgObj = groupFn(objects, options);
    }
  } catch {
    // ignore
  }

  // Fallback: create a Group
  if (!svgObj) {
    try {
      const GroupCtor = (fabric as any).Group;
      if (typeof GroupCtor === 'function') svgObj = new GroupCtor(objects, options);
    } catch {
      svgObj = null;
    }
  }

  // Last fallback: add all objects individually
  if (!svgObj) {
    // Ensure each object gets zone/clip/controls when adding individually.
    // Apply itemGroup first so design SVG pieces are eligible for scoped auto-fit.
    for (const o of objects) {
      applyItemGroupMetadata(o, opts.itemGroup ?? 'Images');
      try {
        (designer as any)?._asukaPrepareExternalObject?.(o);
      } catch {}
    }
    for (const o of objects) {
      applyObjectPriceDeep(o, opts.price);
      const added = (designer as any)?.addObject?.(o, { setActive: false, commitHistory: false, itemGroup: (o as any).itemGroup ?? (opts.itemGroup ?? 'Images'), price: opts.price });
      if (!added && canFallbackAddWithoutDesignerGuard(designer)) canvas.add?.(o);
    }
    canvas.requestRenderAll?.();
    return;
  }

  // If the resulting object has no measurable bounds (can happen with odd SVGs),
  // attempt to recalc before we place it.
  try {
    if (typeof svgObj._calcBounds === 'function') svgObj._calcBounds();
    if (typeof svgObj._updateObjectsCoords === 'function') svgObj._updateObjectsCoords();
  } catch {
    // ignore
  }

  svgObj.set?.({
    selectable: opts.selectable ?? true,
    evented: opts.evented ?? true,
  });
  positionNewObject(designer, canvas, svgObj, opts);
  applyItemGroupMetadata(svgObj, opts.itemGroup ?? 'Images');

  // Ensure SVG groups behave like other Asuka objects (zone assignment + clip + custom controls)
  try {
    (designer as any)?._asukaPrepareExternalObject?.(svgObj);
  } catch {}

  // Ensure grouped SVG behaves like other Asuka objects (zone assignment + clip + custom controls)
  try {
    (designer as any)?._asukaPrepareExternalObject?.(svgObj);
  } catch {}

  (svgObj as any).itemGroup = (svgObj as any).itemGroup ?? (opts.itemGroup ?? 'Images');

  // Optional pricing metadata (used by pricing engine)
  applyObjectPriceDeep(svgObj, opts.price);

  // Ensure bounds are computed (some Fabric builds require it after grouping).
  try {
    svgObj.setCoords?.();
  } catch {
    // ignore
  }

  // If we still end up with 0x0 bounds, force a recalculation.
  try {
    const br = svgObj.getBoundingRect?.(true, true);
    if (br && (!br.width || !br.height)) {
      svgObj._calcBounds?.();
      svgObj._updateObjectsCoords?.();
      svgObj.setCoords?.();
    }
  } catch {
    // ignore
  }

  const added = (designer as any)?.addObject?.(svgObj, {
    setActive: true,
    commitHistory: false,
    itemGroup: (svgObj as any).itemGroup ?? (opts.itemGroup ?? 'Images'),
    price: opts.price,
  });

  if (!added && canFallbackAddWithoutDesignerGuard(designer)) {
    canvas.add?.(svgObj);
    canvas.setActiveObject?.(svgObj);
  }

  // Ensure coords/bounds are updated so selection box is correct.
  try {
    if (typeof (added ?? svgObj).setCoords === 'function') (added ?? svgObj).setCoords();
  } catch {
    // ignore
  }
  canvas.requestRenderAll?.();
}
