import { emitAfter, emitAsukaEvent, emitAsukaEventCancelable } from './events';
import type { AsukaRenderEngine } from './renderEngine';
import { useAsukaStore } from '../store/asukaStore';
import {
  elementsShareVariationFamily,
  findEquivalentElementVariationPath,
  findLinkedElementVariationForProduct,
  getElementVariationPathLabel,
  getProduct3DPartIdFromVariationPath,
  resolveElementVariationSelection,
  resolveProduct3DVariationPath,
  synchronizeStoredElementVariations,
  type AsukaElementVariationApplyInput,
  type ResolvedElementVariationSelection,
  type StoredElementVariationSyncResult,
} from './elementVariationSync';

export interface StoredElementVariationRuntimeOptions {
  includeFabric?: boolean;
  include3D?: boolean;
  skipFabricViewIds?: string[];
  skip3DViewIds?: string[];
  resolveImageGeometry?: (url: string) => Promise<{ width: number; height: number } | null>;
  syncCatalog?: boolean;
}

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

export const synchronizePhysicalCalibrationVariation = (
  source: ResolvedElementVariationSelection,
): string[] => {
  const state = useAsukaStore.getState() as any;
  const candidates = Array.from(new Set(
    [...(source.variationPath ?? [])].reverse()
      .flatMap((variation: any) => [clean(variation?.id), clean(variation?.name ?? variation?.label)])
      .filter(Boolean),
  ));
  if (!candidates.length) return [];

  const changedViewIds: string[] = [];
  const views = (Array.isArray(state?.views) ? state.views : []).map((view: any) => {
    const physicalSize = view?.physicalSize;
    const variants = physicalSize?.variants && typeof physicalSize.variants === 'object' ? physicalSize.variants : null;
    if (!variants) return view;
    const variantKey = Object.keys(variants).find((key) => candidates.some((candidate) => key.toLowerCase() === candidate.toLowerCase()));
    if (!variantKey || physicalSize.activeVariantId === variantKey) return view;
    changedViewIds.push(clean(view?.id));
    return { ...view, physicalSize: { ...physicalSize, activeVariantId: variantKey } };
  });
  if (!changedViewIds.length) return [];
  useAsukaStore.setState({ views, documentViews: views } as any);
  try { (useAsukaStore.getState() as any)?.bumpCanvasChangeTick?.(); } catch {}
  return changedViewIds.filter(Boolean);
};

export const resolveBrowserImageGeometry = async (urlInput: string): Promise<{ width: number; height: number } | null> => {
  const url = clean(urlInput);
  if (!url || typeof Image === 'undefined') return null;
  return await new Promise((resolve) => {
    const image = new Image();
    let settled = false;
    const finish = (value: { width: number; height: number } | null) => {
      if (settled) return;
      settled = true;
      resolve(value);
    };
    image.onload = () => {
      const width = Number(image.naturalWidth || image.width || 0);
      const height = Number(image.naturalHeight || image.height || 0);
      finish(width > 0 && height > 0 ? { width, height } : null);
    };
    image.onerror = () => finish(null);
    image.src = url;
    setTimeout(() => finish(null), 2500);
  });
};

const synchronizeCatalogActiveVariations = (
  source: ResolvedElementVariationSelection,
  catalogInput: any[],
): void => {
  const state = useAsukaStore.getState() as any;
  const catalog = Array.isArray(catalogInput) ? catalogInput : [];
  for (const targetElement of catalog) {
    if (!elementsShareVariationFamily(source.element, targetElement)) continue;
    const targetPath = clean(source.element?.id) === clean(targetElement?.id)
      ? source.variationPath
      : findEquivalentElementVariationPath(targetElement, source.variationPath, { sameElement: false });
    const targetVariation = targetPath?.[targetPath.length - 1] ?? null;
    const variationId = clean(targetVariation?.id);
    if (!variationId) continue;
    const elementKey = clean(targetElement?.id ?? targetElement?.name);
    const partId = getProduct3DPartIdFromVariationPath(targetPath ?? []);
    if (partId) state?.setProductPartActiveVariation?.(elementKey, partId, variationId);
    else state?.setElementActiveVariation?.(elementKey, variationId);
  }
};

export const synchronizeStoredElementVariationRuntime = async (
  input: AsukaElementVariationApplyInput,
  options: StoredElementVariationRuntimeOptions = {},
): Promise<StoredElementVariationSyncResult> => {
  const state = useAsukaStore.getState() as any;
  const result = await synchronizeStoredElementVariations({
    catalog: Array.isArray(state?.elements) ? state.elements : [],
    input,
    documentObjectsByView: state?.documentObjectsByView ?? {},
    document3dScenesByView: state?.document3dScenesByView ?? {},
    views: Array.isArray(state?.views) ? state.views : [],
    activeViewId: state?.activeViewId ?? null,
    includeFabric: options.includeFabric,
    include3D: options.include3D,
    skipFabricViewIds: options.skipFabricViewIds,
    skip3DViewIds: options.skip3DViewIds,
    resolveImageGeometry: options.resolveImageGeometry ?? resolveBrowserImageGeometry,
  });

  if (result.impactedFabricViewIds.length) {
    useAsukaStore.setState({ documentObjectsByView: result.documentObjectsByView } as any);
  }
  if (result.impacted3DViewIds.length) {
    const current = useAsukaStore.getState() as any;
    for (const viewId of result.impacted3DViewIds) {
      current?.setDocument3dSceneForView?.(viewId, result.document3dScenesByView[viewId]);
    }
  }
  if (options.syncCatalog !== false && result.source) {
    synchronizeCatalogActiveVariations(result.source, state?.elements ?? []);
  }
  if (result.impactedFabricViewIds.length || result.impacted3DViewIds.length) {
    try { (useAsukaStore.getState() as any)?.bumpCanvasChangeTick?.(); } catch {}
  }
  return result;
};

const exportActive3DScene = async (engine: AsukaRenderEngine, viewId: string): Promise<any | null> => {
  if (!viewId || !engine?.lifecycle?.exportView) return null;
  try {
    return await Promise.resolve(engine.lifecycle.exportView(viewId));
  } catch {
    return null;
  }
};

/**
 * Apply an element variation through the active renderer and synchronize every
 * inactive view through canonical serialized state. No native renderer object
 * crosses this boundary.
 */
export const applyElementVariationAcrossRenderers = async (
  engine: AsukaRenderEngine | null | undefined,
  input: AsukaElementVariationApplyInput,
): Promise<boolean> => {
  const state = useAsukaStore.getState() as any;
  const catalog = Array.isArray(state?.elements) ? state.elements : [];
  const source = resolveElementVariationSelection(catalog, input);
  if (!source) return false;

  const kind = clean(engine?.kind ?? state?.activeRenderEngineKind).toLowerCase();
  if (kind !== '3d' && kind !== 'babylon') {
    const designer = state?.designerInstance as any;
    if (!designer?.applyElementVariation) return false;
    const applied = (await designer.applyElementVariation(input)) !== false;
    if (applied) synchronizePhysicalCalibrationVariation(source);
    return applied;
  }

  if (!engine?.parts?.applyVariation) return false;
  const payload = { ...input, engine: '3d' };
  if (!emitAsukaEventCancelable('before:elements.applyVariation', payload)) return false;

  const activeViewId = clean(state?.activeViewId);
  const scene = await exportActive3DScene(engine, activeViewId);
  const products = Array.isArray(scene?.products)
    ? scene.products
    : (Array.isArray(state?.document3dScenesByView?.[activeViewId]?.products)
        ? state.document3dScenesByView[activeViewId].products
        : []);
  const wantedProductId = clean(input.objectId).replace(/^product:/i, '');
  const targets = products
    .filter((product: any) => input.scope !== 'object' || (wantedProductId && clean(product?.id) === wantedProductId))
    .map((product: any) => {
      const target = findLinkedElementVariationForProduct(catalog, product, source.element, source.variationPath);
      if (!target) return null;
      const partId = getProduct3DPartIdFromVariationPath(target.variationPath) || clean(input.partId);
      const variation = resolveProduct3DVariationPath(target.variationPath) ?? target.variation;
      const variationId = clean(variation?.id);
      if (!partId || !variationId) return null;
      return {
        productId: clean(product?.id),
        element: target.element,
        partId,
        variation,
        variationId,
        variationPath: target.variationPath,
      };
    })
    .filter(Boolean) as Array<{
      productId: string;
      element: any;
      partId: string;
      variation: any;
      variationId: string;
      variationPath: any[];
    }>;

  if (!targets.length) return false;

  try {
    for (let index = 0; index < targets.length; index += 1) {
      const target = targets[index];
      const isFinalTarget = index === targets.length - 1;
      await engine.parts.applyVariation(target.partId, target.variationId, {
        element: target.element,
        variation: target.variation,
        variationPath: target.variationPath,
        productId: target.productId,
        source: clean(input.source) || 'api.element-variation',
        focus: false,
        commitHistory: isFinalTarget,
        emitChange: isFinalTarget,
        metadata: {
          variationPath: target.variationPath.map((node) => ({ id: clean(node?.id), name: clean(node?.name ?? node?.label) })),
          variationPathLabel: getElementVariationPathLabel(target.variationPath),
        },
      });
    }

    const activeScene = await exportActive3DScene(engine, activeViewId);
    if (activeScene && activeViewId) {
      (useAsukaStore.getState() as any)?.setDocument3dSceneForView?.(activeViewId, activeScene);
    }
    const syncResult = await synchronizeStoredElementVariationRuntime(input, {
      includeFabric: true,
      include3D: true,
      skip3DViewIds: activeViewId ? [activeViewId] : [],
      syncCatalog: true,
    });
    const physicalViewIds = synchronizePhysicalCalibrationVariation(source);
    emitAfter('elements.applyVariation', {
      ...payload,
      result: true,
      impactedFabricViewIds: syncResult.impactedFabricViewIds,
      impacted3DViewIds: syncResult.impacted3DViewIds,
      physicalViewIds,
    });
    return true;
  } catch (error: any) {
    emitAsukaEvent('elements.applyVariation.error', { ...payload, error: clean(error?.message ?? error) });
    return false;
  }
};
