import type { AsukaElement, ElementVariation } from '../types/elements';
import type { Asuka3DSceneState } from './renderEngine';
import {
  findCatalogProduct3DElementForInstance,
  product3DElementsShareVariationFamily,
  product3DInstanceMatchesElement,
} from './product3dIdentity';
import { getElementPriceBreakdown, normalizePriceInput, sumPriceInputs } from './pricingValue';
import {
  applyFabricVariationFrame,
  ensureFabricVariationFrame,
} from './canvas/fabricVariationGeometry';

export interface AsukaElementVariationApplyInput {
  scope: 'object' | 'document';
  objectId?: string;
  elementId?: string;
  familyId?: string;
  name?: string;
  variationId: string;
  variationPathIds?: string[];
  variationPathNames?: string[];
  variationSyncKey?: string;
  partId?: string;
  source?: string;
}

export interface ResolvedElementVariationSelection {
  element: AsukaElement;
  variation: ElementVariation;
  variationPath: ElementVariation[];
}

export interface StoredElementVariationSyncResult {
  source: ResolvedElementVariationSelection | null;
  documentObjectsByView: Record<string, any[]>;
  document3dScenesByView: Record<string, Asuka3DSceneState>;
  impactedFabricViewIds: string[];
  impacted3DViewIds: string[];
}

export interface StoredElementVariationSyncOptions {
  catalog: AsukaElement[];
  input: AsukaElementVariationApplyInput;
  documentObjectsByView?: Record<string, any[]>;
  document3dScenesByView?: Record<string, Asuka3DSceneState>;
  views?: Array<{ id?: string; name?: string }>;
  activeViewId?: string | null;
  includeFabric?: boolean;
  include3D?: boolean;
  skipFabricViewIds?: string[];
  skip3DViewIds?: string[];
  resolveImageGeometry?: (url: string) => Promise<{ width: number; height: number } | null>;
}

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

const clonePlain = <T,>(value: T): T => {
  try {
    return JSON.parse(JSON.stringify(value));
  } catch {
    return value;
  }
};

export const normalizeElementVariationName = (value: unknown): string => clean(value)
  .toLowerCase()
  .normalize('NFD')
  .replace(/[\u0300-\u036f]/g, '')
  .replace(/[_-]+/g, ' ')
  .replace(/\s+/g, ' ')
  .trim();

const normalizeComparableName = (value: unknown): string => normalizeElementVariationName(value)
  .replace(/\b(left|right|gauche|droite|pair|shoe|shoes|chaussure|chaussures|sneaker|sneakers|basket|baskets)\b/g, ' ')
  .replace(/\s+/g, ' ')
  .trim();

const normalizePartName = (value: unknown): string => normalizeComparableName(value)
  .replace(/\b(asuka|part|parts|surface|surfaces|mesh|meshes|material|target|zone|zones)\b/g, ' ')
  .replace(/\s+/g, ' ')
  .trim();

const getNodeName = (node: any): string => clean(node?.name ?? node?.label ?? node?.id);

export const getElementVariationSyncKey = (variation: any): string => clean(
  variation?.syncKey
  ?? variation?.metadata?.syncKey
  ?? variation?.metadata?.variationSyncKey,
);

const getExplicitPartNames = (node: any): string[] => Array.from(new Set([
  node?.product3d?.partLabel,
  node?.product3d?.label,
  node?.product3d?.partId,
  node?.product3d?.materialTarget,
  node?.metadata?.asukaProduct3DPartLabel,
  node?.metadata?.asukaProduct3DPartId,
  node?.metadata?.partLabel,
  node?.metadata?.partId,
  node?.metadata?.originalMaterialTarget,
  node?.metadata?.materialTarget,
].map(clean).filter(Boolean)));

const getPathNames = (path: any[]): string[] => (Array.isArray(path) ? path : [])
  .map((node) => normalizeElementVariationName(getNodeName(node)))
  .filter(Boolean);

const getPathSyncKeys = (path: any[]): string[] => (Array.isArray(path) ? path : [])
  .map(getElementVariationSyncKey)
  .filter(Boolean);

const getPathPartNames = (pathInput: any[]): string[] => {
  const path = Array.isArray(pathInput) ? pathInput.filter(Boolean) : [];
  const explicit = path.flatMap(getExplicitPartNames);
  const source = explicit.length
    ? explicit
    : path.slice(0, Math.max(0, path.length - 1)).flatMap((node) => [node?.name, node?.label, node?.id]);
  return Array.from(new Set(source.map(normalizePartName).filter(Boolean)));
};

const arraysEqual = (left: string[], right: string[]): boolean => (
  left.length === right.length && left.every((value, index) => value === right[index])
);

const partNamesCompatible = (left: unknown, right: unknown): boolean => {
  const a = normalizePartName(left);
  const b = normalizePartName(right);
  return Boolean(a && b && (a === b || a.includes(b) || b.includes(a)));
};

const pathsSharePartAndLeaf = (candidatePath: any[], sourcePath: any[]): boolean => {
  const candidateNames = getPathNames(candidatePath);
  const sourceNames = getPathNames(sourcePath);
  const candidateLeaf = candidateNames[candidateNames.length - 1] ?? '';
  const sourceLeaf = sourceNames[sourceNames.length - 1] ?? '';
  if (!candidateLeaf || candidateLeaf !== sourceLeaf) return false;

  const candidateParts = getPathPartNames(candidatePath);
  const sourceParts = getPathPartNames(sourcePath);
  return candidateParts.some((left) => sourceParts.some((right) => partNamesCompatible(left, right)));
};

export const collectElementVariationLeafPaths = (variations: any[], trail: any[] = []): ElementVariation[][] => {
  const paths: ElementVariation[][] = [];
  for (const variation of Array.isArray(variations) ? variations : []) {
    if (!variation || typeof variation !== 'object') continue;
    const nextTrail = [...trail, variation];
    const children = Array.isArray(variation.variations) ? variation.variations : [];
    if (children.length) paths.push(...collectElementVariationLeafPaths(children, nextTrail));
    else paths.push(nextTrail as ElementVariation[]);
  }
  return paths;
};

export const findElementVariationPathById = (variations: any[], variationId: string, trail: any[] = []): ElementVariation[] | null => {
  const wanted = clean(variationId);
  if (!wanted) return null;
  for (const variation of Array.isArray(variations) ? variations : []) {
    if (!variation || typeof variation !== 'object') continue;
    const nextTrail = [...trail, variation];
    if (clean(variation.id) === wanted) return nextTrail as ElementVariation[];
    const found = findElementVariationPathById(variation.variations, wanted, nextTrail);
    if (found) return found;
  }
  return null;
};

const findPathByIds = (variations: any[], pathIdsInput: string[]): ElementVariation[] | null => {
  const pathIds = pathIdsInput.map(clean).filter(Boolean);
  if (!pathIds.length) return null;
  const path: ElementVariation[] = [];
  let list = Array.isArray(variations) ? variations : [];
  for (const id of pathIds) {
    const node = list.find((variation: any) => clean(variation?.id) === id);
    if (!node) return null;
    path.push(node);
    list = Array.isArray(node?.variations) ? node.variations : [];
  }
  return path;
};

const findPathByNames = (variations: any[], pathNamesInput: string[]): ElementVariation[] | null => {
  const names = pathNamesInput.map(normalizeElementVariationName).filter(Boolean);
  if (!names.length) return null;
  const path: ElementVariation[] = [];
  let list = Array.isArray(variations) ? variations : [];
  for (const name of names) {
    const node = list.find((variation: any) => normalizeElementVariationName(getNodeName(variation)) === name);
    if (!node) return null;
    path.push(node);
    list = Array.isArray(node?.variations) ? node.variations : [];
  }
  return path;
};

const findPathBySyncKey = (variations: any[], syncKey: string): ElementVariation[] | null => {
  const wanted = clean(syncKey);
  if (!wanted) return null;
  return collectElementVariationLeafPaths(variations)
    .find((path) => getElementVariationSyncKey(path[path.length - 1]) === wanted) ?? null;
};

export const elementsShareVariationFamily = (left: any, right: any): boolean => {
  if (!left || !right) return false;
  const leftId = clean(left?.id);
  const rightId = clean(right?.id);
  if (leftId && rightId && leftId === rightId) return true;
  const leftFamily = clean(left?.familyId);
  const rightFamily = clean(right?.familyId);
  return Boolean(leftFamily && rightFamily && leftFamily === rightFamily);
};

export const findElementForVariationInput = (catalogInput: any[], input: Partial<AsukaElementVariationApplyInput>): AsukaElement | null => {
  const catalog = (Array.isArray(catalogInput) ? catalogInput : []).filter(Boolean) as AsukaElement[];
  const elementId = clean(input?.elementId);
  if (elementId) {
    const exact = catalog.find((element) => clean(element?.id) === elementId);
    if (exact) return exact;
  }

  const familyId = clean(input?.familyId);
  if (familyId) {
    const matches = catalog.filter((element) => clean(element?.familyId) === familyId);
    if (matches.length === 1) return matches[0];
    const named = matches.find((element) => clean(element?.name) === clean(input?.name));
    if (named) return named;
  }

  const name = clean(input?.name);
  if (name) {
    const matches = catalog.filter((element) => clean(element?.name) === name);
    if (matches.length === 1) return matches[0];
  }
  return null;
};

export const resolveElementVariationSelection = (
  catalogInput: any[],
  input: AsukaElementVariationApplyInput,
): ResolvedElementVariationSelection | null => {
  const element = findElementForVariationInput(catalogInput, input);
  if (!element) return null;
  const variations = Array.isArray(element.variations) ? element.variations : [];
  const path = findPathByIds(variations, input.variationPathIds ?? [])
    ?? findPathBySyncKey(variations, clean(input.variationSyncKey))
    ?? findElementVariationPathById(variations, clean(input.variationId))
    ?? findPathByNames(variations, input.variationPathNames ?? []);
  const variation = path?.[path.length - 1] ?? null;
  return variation ? { element, variation, variationPath: path! } : null;
};

export const findEquivalentElementVariationPath = (
  targetElement: any,
  sourcePathInput: any[],
  options: { sourceViewName?: string; targetViewName?: string; sameElement?: boolean } = {},
): ElementVariation[] | null => {
  const sourcePath = Array.isArray(sourcePathInput) ? sourcePathInput.filter(Boolean) : [];
  if (!sourcePath.length) return null;
  const candidates = collectElementVariationLeafPaths(targetElement?.variations);
  if (!candidates.length) return null;

  const sourceLeaf = sourcePath[sourcePath.length - 1];
  const sourceSyncKey = getElementVariationSyncKey(sourceLeaf);
  if (sourceSyncKey) {
    const syncMatch = candidates.find((path) => getElementVariationSyncKey(path[path.length - 1]) === sourceSyncKey);
    if (syncMatch) return syncMatch;
  }

  const sourceSyncPath = getPathSyncKeys(sourcePath);
  if (sourceSyncPath.length === sourcePath.length) {
    const syncPathMatch = candidates.find((path) => arraysEqual(getPathSyncKeys(path), sourceSyncPath));
    if (syncPathMatch) return syncPathMatch;
  }

  const sourceIds = sourcePath.map((node) => clean(node?.id)).filter(Boolean);
  if (options.sameElement && sourceIds.length === sourcePath.length) {
    const exactPath = candidates.find((path) => arraysEqual(path.map((node) => clean(node?.id)).filter(Boolean), sourceIds));
    if (exactPath) return exactPath;
  }

  const sourceNames = getPathNames(sourcePath);
  const exactNames = candidates.find((path) => arraysEqual(getPathNames(path), sourceNames));
  if (exactNames) return exactNames;

  const targetViewName = normalizeElementVariationName(options.targetViewName);
  if (targetViewName && sourceNames.length > 1) {
    const sourceTail = sourceNames.slice(1);
    const viewMatch = candidates.find((path) => {
      const names = getPathNames(path);
      return names[0] === targetViewName && arraysEqual(names.slice(1), sourceTail);
    });
    if (viewMatch) return viewMatch;
  }

  const partAndLeafMatch = candidates.find((path) => pathsSharePartAndLeaf(path, sourcePath));
  if (partAndLeafMatch) return partAndLeafMatch;

  const sourceLeafId = clean(sourceLeaf?.id);
  if (sourceLeafId) {
    const idMatches = candidates.filter((path) => clean(path[path.length - 1]?.id) === sourceLeafId);
    if (idMatches.length === 1) return idMatches[0];
  }

  const sourceLeafName = normalizeElementVariationName(getNodeName(sourceLeaf));
  if (sourceLeafName) {
    const nameMatches = candidates.filter((path) => normalizeElementVariationName(getNodeName(path[path.length - 1])) === sourceLeafName);
    if (nameMatches.length === 1) return nameMatches[0];
  }
  return null;
};

export const findLinkedElementVariationForProduct = (
  catalog: any[],
  product: any,
  sourceElement: any,
  sourceVariationPath: any[],
): ResolvedElementVariationSelection | null => {
  const sourcePath = Array.isArray(sourceVariationPath) ? sourceVariationPath.filter(Boolean) : [];
  if (!sourceElement || !sourcePath.length) return null;
  if (product3DInstanceMatchesElement(product, sourceElement)) {
    return {
      element: sourceElement,
      variation: sourcePath[sourcePath.length - 1],
      variationPath: sourcePath,
    };
  }

  const targetElement = findCatalogProduct3DElementForInstance(catalog, product);
  if (!targetElement || !product3DElementsShareVariationFamily(sourceElement, targetElement)) return null;
  const variationPath = findEquivalentElementVariationPath(targetElement, sourcePath, {
    sameElement: clean(sourceElement?.id) === clean(targetElement?.id),
  });
  const variation = variationPath?.[variationPath.length - 1] ?? null;
  return variation ? { element: targetElement, variation, variationPath: variationPath! } : null;
};

export const getProduct3DPartIdFromVariationPath = (pathInput: any[]): string => {
  const path = Array.isArray(pathInput) ? pathInput : [];
  for (let index = path.length - 1; index >= 0; index -= 1) {
    const node = path[index];
    const partId = clean(
      node?.product3d?.partId
      ?? node?.metadata?.asukaProduct3DPartId
      ?? node?.metadata?.partId,
    );
    if (partId) return partId;
  }
  return '';
};

const mergeMaterials = (base: any, incoming: any): Record<string, any> => {
  const next = { ...(base && typeof base === 'object' ? base : {}) } as Record<string, any>;
  for (const [key, value] of Object.entries(incoming && typeof incoming === 'object' ? incoming : {})) {
    const id = clean(key);
    if (!id || !value || typeof value !== 'object') continue;
    next[id] = { ...(next[id] && typeof next[id] === 'object' ? next[id] : {}), ...value };
  }
  return next;
};

export const mergeProduct3DVariationPath = (pathInput: any[]): any => {
  const merged: any = {};
  for (const variation of Array.isArray(pathInput) ? pathInput : []) {
    const product3d = variation?.product3d && typeof variation.product3d === 'object' ? variation.product3d : null;
    if (!product3d) continue;
    const { materials, model, ...rest } = product3d;
    Object.assign(merged, rest);
    if (model && typeof model === 'object') merged.model = { ...(merged.model ?? {}), ...model };
    if (materials && typeof materials === 'object') merged.materials = mergeMaterials(merged.materials, materials);
  }
  const materialKeys = Object.keys(merged.materials ?? {});
  if (!clean(merged.materialTarget)) merged.materialTarget = materialKeys[0] ?? 'product';
  return merged;
};

export const resolveProduct3DVariationPath = (pathInput: any[]): any | null => {
  const path = Array.isArray(pathInput) ? pathInput.filter(Boolean) : [];
  const leaf = path[path.length - 1];
  if (!leaf) return null;
  return {
    ...leaf,
    product3d: mergeProduct3DVariationPath(path),
    metadata: {
      ...(leaf?.metadata ?? {}),
      variationPath: path.map((node) => ({ id: clean(node?.id), name: getNodeName(node) })),
    },
  };
};

export const getElementVariationPathLabel = (pathInput: any[]): string => (Array.isArray(pathInput) ? pathInput : [])
  .map(getNodeName)
  .filter(Boolean)
  .join(' / ');

const getMaterialPatch = (path: any[], fallbackTarget?: string): { target: string; material: any } | null => {
  const merged = mergeProduct3DVariationPath(path);
  const entries = Object.entries(merged?.materials && typeof merged.materials === 'object' ? merged.materials : {})
    .filter(([, value]) => value && typeof value === 'object') as Array<[string, any]>;
  if (!entries.length) return null;
  const preferredTarget = clean(merged?.materialTarget ?? fallbackTarget);
  const entry = entries.find(([key]) => clean(key) === preferredTarget) ?? entries[0];
  return { target: clean(entry[0] ?? preferredTarget) || 'product', material: clonePlain(entry[1]) };
};

const stripInstancePrefix = (value: unknown): string => {
  const raw = clean(value);
  const index = raw.indexOf('::');
  return index >= 0 ? raw.slice(index + 2) : raw;
};

const getMaterialAliases = (material: any, fallbackTarget: string): Set<string> => new Set([
  fallbackTarget,
  stripInstancePrefix(fallbackTarget),
  material?.materialTarget,
  material?.metadata?.materialTarget,
  material?.metadata?.originalMaterialTarget,
].map(stripInstancePrefix).filter(Boolean));

const chooseSceneMaterialKey = (scene: any, product: any, sourceTarget: string): string => {
  const productId = clean(product?.id);
  const target = stripInstancePrefix(sourceTarget) || 'product';
  const materials = {
    ...(scene?.materials && typeof scene.materials === 'object' ? scene.materials : {}),
    ...(product?.materials && typeof product.materials === 'object' ? product.materials : {}),
  };
  const keys = Object.keys(materials);
  const scoped = productId ? `${productId}::${target}` : '';
  if (scoped && keys.includes(scoped)) return scoped;
  if (keys.includes(target)) return target;
  for (const key of keys) {
    if (productId && key.startsWith(`${productId}::`) && getMaterialAliases((materials as any)[key], target).has(target)) return key;
  }
  for (const key of keys) {
    if (getMaterialAliases((materials as any)[key], target).has(target)) return key;
  }
  return scoped || target;
};

const mergeStoredMaterial = (previous: any, incoming: any, materialKey: string, productId: string, originalTarget: string): any => {
  const previousMetadata = previous?.metadata && typeof previous.metadata === 'object' ? previous.metadata : {};
  const incomingMetadata = incoming?.metadata && typeof incoming.metadata === 'object' ? incoming.metadata : {};
  const targetMeshIds = incoming?.targetMeshIds
    ?? incomingMetadata?.targetMeshIds
    ?? previous?.targetMeshIds
    ?? previousMetadata?.targetMeshIds;
  return {
    ...(previous && typeof previous === 'object' ? clonePlain(previous) : {}),
    ...(incoming && typeof incoming === 'object' ? clonePlain(incoming) : {}),
    label: clean(incoming?.label ?? previous?.label ?? originalTarget ?? materialKey) || materialKey,
    materialTarget: materialKey,
    ...(targetMeshIds ? { targetMeshIds: clonePlain(targetMeshIds) } : {}),
    metadata: {
      ...clonePlain(previousMetadata),
      ...clonePlain(incomingMetadata),
      ...(productId ? { asukaProductId: productId } : {}),
      originalMaterialTarget: originalTarget,
      materialTarget: materialKey,
      ...(targetMeshIds ? { targetMeshIds: clonePlain(targetMeshIds) } : {}),
    },
  };
};

const getViewName = (views: Array<{ id?: string; name?: string }>, viewId: string): string => clean(
  views.find((view) => clean(view?.id) === clean(viewId))?.name,
);

const findSourceViewId = (objectsByView: Record<string, any[]>, objectId: string): string => {
  const wanted = clean(objectId).replace(/^product:/i, '');
  if (!wanted) return '';
  for (const [viewId, objects] of Object.entries(objectsByView)) {
    if ((Array.isArray(objects) ? objects : []).some((object) => clean(object?._asukaObjectId ?? object?.id) === wanted)) return viewId;
  }
  return '';
};

const findCatalogElementForFabricObject = (catalog: AsukaElement[], object: any): AsukaElement | null => {
  const elementId = clean(object?._asukaElementId);
  if (elementId) {
    const exact = catalog.find((element) => clean(element?.id) === elementId);
    if (exact) return exact;
  }
  const familyId = clean(object?._asukaElementFamilyId);
  if (familyId) {
    const matches = catalog.filter((element) => clean(element?.familyId) === familyId);
    if (matches.length === 1) return matches[0];
    const named = matches.find((element) => clean(element?.name) === clean(object?._asukaElementName));
    if (named) return named;
  }
  return null;
};

const applySerializedImageGeometry = (object: any, geometry: { width: number; height: number } | null): void => {
  if (!geometry) return;
  const frame = ensureFabricVariationFrame(object);
  if (!frame) return;
  applyFabricVariationFrame(object, frame, geometry);
};

const patchStoredFabricViews = async (options: {
  catalog: AsukaElement[];
  input: AsukaElementVariationApplyInput;
  source: ResolvedElementVariationSelection;
  objectsByView: Record<string, any[]>;
  views: Array<{ id?: string; name?: string }>;
  sourceViewName: string;
  skipViewIds: Set<string>;
  resolveImageGeometry?: StoredElementVariationSyncOptions['resolveImageGeometry'];
}): Promise<{ byView: Record<string, any[]>; impacted: string[] }> => {
  const nextByView = { ...options.objectsByView };
  const impacted: string[] = [];
  const geometryCache = new Map<string, Promise<{ width: number; height: number } | null>>();
  const resolveGeometry = async (url: string) => {
    if (!options.resolveImageGeometry || !url) return null;
    if (!geometryCache.has(url)) geometryCache.set(url, options.resolveImageGeometry(url));
    try { return await geometryCache.get(url)!; } catch { return null; }
  };
  const wantedObjectId = clean(options.input.objectId);

  for (const [viewId, objectsInput] of Object.entries(options.objectsByView)) {
    if (options.skipViewIds.has(clean(viewId))) continue;
    const objects = Array.isArray(objectsInput) ? objectsInput : [];
    let changed = false;
    const targetViewName = getViewName(options.views, viewId);
    const nextObjects: any[] = [];
    for (const object of objects) {
      if (!object || typeof object !== 'object') {
        nextObjects.push(object);
        continue;
      }
      const objectId = clean(object?._asukaObjectId ?? object?.id);
      if (options.input.scope === 'object' && (!wantedObjectId || objectId !== wantedObjectId)) {
        nextObjects.push(object);
        continue;
      }
      const targetElement = findCatalogElementForFabricObject(options.catalog, object);
      if (!targetElement || !elementsShareVariationFamily(options.source.element, targetElement)) {
        nextObjects.push(object);
        continue;
      }
      const targetPath = findEquivalentElementVariationPath(targetElement, options.source.variationPath, {
        sourceViewName: options.sourceViewName,
        targetViewName,
        sameElement: clean(options.source.element?.id) === clean(targetElement?.id),
      });
      const targetVariation = targetPath?.[targetPath.length - 1] ?? null;
      if (!targetVariation) {
        nextObjects.push(object);
        continue;
      }

      const nextObject = { ...object };
      const variationId = clean(targetVariation.id);
      const imageUrl = clean(targetVariation.image ?? targetElement.image);
      const price = getElementPriceBreakdown(targetElement, targetVariation, targetPath);
      nextObject._asukaElementVariationId = variationId;
      nextObject._asukaElementIsInstance = true;
      nextObject._asukaElementId = clean(targetElement.id);
      nextObject._asukaElementFamilyId = clean(targetElement.familyId ?? targetElement.id);
      nextObject._asukaElementName = clean(targetElement.name);
      nextObject._asukaElementBasePrice = price.base;
      nextObject._asukaElementVariationPrice = price.variation;
      nextObject.price = price.total;
      if (imageUrl && (typeof nextObject.src === 'string' || Object.prototype.hasOwnProperty.call(nextObject, 'src'))) {
        nextObject.src = imageUrl;
        applySerializedImageGeometry(nextObject, await resolveGeometry(imageUrl));
      }
      nextObjects.push(nextObject);
      changed = true;
    }
    if (changed) {
      nextByView[viewId] = nextObjects;
      impacted.push(viewId);
    }
  }
  return { byView: nextByView, impacted };
};

const patchStored3DViews = (options: {
  catalog: AsukaElement[];
  input: AsukaElementVariationApplyInput;
  source: ResolvedElementVariationSelection;
  scenesByView: Record<string, Asuka3DSceneState>;
  skipViewIds: Set<string>;
}): { byView: Record<string, Asuka3DSceneState>; impacted: string[] } => {
  const nextByView = { ...options.scenesByView };
  const impacted: string[] = [];
  const wantedProductId = clean(options.input.objectId).replace(/^product:/i, '');

  for (const [viewId, rawScene] of Object.entries(options.scenesByView)) {
    if (options.skipViewIds.has(clean(viewId)) || !rawScene || typeof rawScene !== 'object') continue;
    const scene = clonePlain(rawScene);
    const products = Array.isArray(scene.products) ? scene.products : [];
    if (!products.length) continue;
    let changed = false;
    let selectedPartId = clean(scene.selectedPartId);
    const nextMaterials = { ...(scene.materials && typeof scene.materials === 'object' ? scene.materials : {}) } as Record<string, any>;
    const nextProducts = products.map((rawProduct) => {
      const product = rawProduct && typeof rawProduct === 'object' ? clonePlain(rawProduct) : rawProduct;
      const productId = clean(product?.id);
      if (options.input.scope === 'object' && (!wantedProductId || productId !== wantedProductId)) return rawProduct;
      const target = findLinkedElementVariationForProduct(options.catalog, product, options.source.element, options.source.variationPath);
      if (!target) return rawProduct;
      const partId = getProduct3DPartIdFromVariationPath(target.variationPath) || clean(options.input.partId);
      const variation = resolveProduct3DVariationPath(target.variationPath) ?? target.variation;
      const variationId = clean(variation?.id);
      if (!variationId) return rawProduct;

      const pathRecords = target.variationPath.map((node) => ({ id: clean(node?.id), name: getNodeName(node) }));
      const partVariationIds = {
        ...(product?.partVariationIds && typeof product.partVariationIds === 'object' ? product.partVariationIds : {}),
        ...(partId ? { [partId]: variationId } : {}),
      };
      const partVariationPrices: Record<string, number> = {};
      for (const [key, value] of Object.entries(product?.partVariationPrices && typeof product.partVariationPrices === 'object' ? product.partVariationPrices : {})) {
        const normalized = normalizePriceInput(value);
        if (clean(key) && normalized !== undefined) partVariationPrices[clean(key)] = normalized;
      }
      if (partId) partVariationPrices[partId] = sumPriceInputs(target.variationPath.map((node) => node?.price));
      const price = getElementPriceBreakdown(target.element, target.variation, target.variationPath);
      const basePrice = normalizePriceInput(product?.basePrice) ?? price.base;
      const variationPrice = partId ? sumPriceInputs(Object.values(partVariationPrices)) : price.variation;
      let nextProduct: any = {
        ...product,
        catalogId: clean(target.element?.id) || undefined,
        name: clean(target.element?.name) || undefined,
        variationId,
        variationName: clean(variation?.name ?? variationId),
        variationPath: pathRecords,
        variationPathLabel: getElementVariationPathLabel(target.variationPath),
        partVariationIds,
        partVariationPrices,
        basePrice,
        variationPrice,
        price: basePrice + variationPrice,
        metadata: {
          ...(product?.metadata && typeof product.metadata === 'object' ? product.metadata : {}),
          elementId: clean(target.element?.id) || undefined,
          elementFamilyId: clean(target.element?.familyId) || undefined,
          elementName: clean(target.element?.name) || undefined,
        },
      };
      if (partId) selectedPartId = partId;

      const materialPatch = getMaterialPatch(target.variationPath, partId || undefined);
      if (materialPatch?.material) {
        const originalTarget = stripInstancePrefix(materialPatch.target || partId || 'product') || 'product';
        const materialKey = chooseSceneMaterialKey(scene, nextProduct, originalTarget);
        nextMaterials[materialKey] = mergeStoredMaterial(nextMaterials[materialKey], materialPatch.material, materialKey, productId, originalTarget);
      }
      changed = true;
      return nextProduct;
    });
    if (!changed) continue;
    nextByView[viewId] = {
      ...scene,
      products: nextProducts,
      materials: nextMaterials,
      selectedPartId: selectedPartId || null,
    };
    impacted.push(viewId);
  }
  return { byView: nextByView, impacted };
};

export const synchronizeStoredElementVariations = async (
  options: StoredElementVariationSyncOptions,
): Promise<StoredElementVariationSyncResult> => {
  const catalog = (Array.isArray(options.catalog) ? options.catalog : []).filter(Boolean);
  const objectsByView = options.documentObjectsByView && typeof options.documentObjectsByView === 'object'
    ? options.documentObjectsByView
    : {};
  const scenesByView = options.document3dScenesByView && typeof options.document3dScenesByView === 'object'
    ? options.document3dScenesByView
    : {};
  const source = resolveElementVariationSelection(catalog, options.input);
  if (!source) {
    return {
      source: null,
      documentObjectsByView: objectsByView,
      document3dScenesByView: scenesByView,
      impactedFabricViewIds: [],
      impacted3DViewIds: [],
    };
  }

  const views = Array.isArray(options.views) ? options.views : [];
  const sourceViewId = findSourceViewId(objectsByView, clean(options.input.objectId)) || clean(options.activeViewId);
  const sourceViewName = getViewName(views, sourceViewId);
  let nextObjects = objectsByView;
  let nextScenes = scenesByView;
  let impactedFabricViewIds: string[] = [];
  let impacted3DViewIds: string[] = [];

  if (options.includeFabric !== false) {
    const patched = await patchStoredFabricViews({
      catalog,
      input: options.input,
      source,
      objectsByView,
      views,
      sourceViewName,
      skipViewIds: new Set((options.skipFabricViewIds ?? []).map(clean).filter(Boolean)),
      resolveImageGeometry: options.resolveImageGeometry,
    });
    nextObjects = patched.byView;
    impactedFabricViewIds = patched.impacted;
  }

  if (options.include3D !== false) {
    const patched = patchStored3DViews({
      catalog,
      input: options.input,
      source,
      scenesByView,
      skipViewIds: new Set((options.skip3DViewIds ?? []).map(clean).filter(Boolean)),
    });
    nextScenes = patched.byView;
    impacted3DViewIds = patched.impacted;
  }

  return {
    source,
    documentObjectsByView: nextObjects,
    document3dScenesByView: nextScenes,
    impactedFabricViewIds,
    impacted3DViewIds,
  };
};
