import type { Asuka3DProduct, Asuka3DSceneState } from './renderEngine';

export type Asuka3DHistoryAssets = Record<string, Asuka3DProduct>;

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

const cloneModel = (model: Asuka3DProduct['model']): Asuka3DProduct['model'] => {
  if (!model || typeof model !== 'object') return undefined;
  const url = String(model.url ?? '').trim();
  if (!url) return undefined;
  return {
    url,
    ...(String(model.format ?? '').trim() ? { format: model.format } : {}),
    ...(model.metadata && typeof model.metadata === 'object' ? { metadata: cloneSmall(model.metadata) } : {}),
  } as Asuka3DProduct['model'];
};

const cloneProduct = (product: Asuka3DProduct): Asuka3DProduct => ({
  ...product,
  ...(product.model ? { model: cloneModel(product.model) } : {}),
  ...(Array.isArray(product.parts) ? { parts: cloneSmall(product.parts) } : {}),
  ...(Array.isArray(product.meshIds) ? { meshIds: [...product.meshIds] } : {}),
  ...(Array.isArray(product.variationPath) ? { variationPath: cloneSmall(product.variationPath) } : {}),
  ...(product.partVariationIds && typeof product.partVariationIds === 'object'
    ? { partVariationIds: { ...product.partVariationIds } }
    : {}),
  ...(product.transform && typeof product.transform === 'object' ? { transform: cloneSmall(product.transform) } : {}),
  ...(product.metadata && typeof product.metadata === 'object' ? { metadata: cloneSmall(product.metadata) } : {}),
});

const cloneScene = (scene: Asuka3DSceneState): Asuka3DSceneState => ({
  ...scene,
  products: (Array.isArray(scene?.products) ? scene.products : []).map(cloneProduct),
  ...(scene.camera && typeof scene.camera === 'object' ? { camera: cloneSmall(scene.camera) } : {}),
  materials: cloneSmall(scene?.materials ?? {}),
  decorations: cloneSmall(scene?.decorations ?? []),
  printAreas: cloneSmall(scene?.printAreas ?? []),
  ...(scene.assembly && typeof scene.assembly === 'object' ? { assembly: cloneSmall(scene.assembly) } : {}),
});

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

const variationToken = (product: Partial<Asuka3DProduct> | null | undefined): string => {
  const direct = normalizeToken(product?.variationId);
  if (direct) return direct;
  const path = Array.isArray(product?.variationPath)
    ? product.variationPath.map((entry: any) => normalizeToken(entry?.id ?? entry?.name)).filter(Boolean).join('/')
    : '';
  return path || 'default';
};

/** Stable key for one placed product and its model-bearing variation. */
export function get3DHistoryAssetKey(product: Partial<Asuka3DProduct> | null | undefined): string {
  const id = normalizeToken(product?.id);
  if (!id) return '';
  return `${id}::${variationToken(product)}`;
}

/** Keep large model payloads once per history track. */
export function collect3DHistoryAssets(
  products: Asuka3DProduct[] | null | undefined,
  previous: Asuka3DHistoryAssets | null | undefined = {},
): Asuka3DHistoryAssets {
  const next: Asuka3DHistoryAssets = { ...(previous ?? {}) };
  for (const product of Array.isArray(products) ? products : []) {
    if (!product || typeof product !== 'object') continue;
    const key = get3DHistoryAssetKey(product);
    if (!key) continue;
    const existing = next[key];
    const hasModel = Boolean(normalizeToken(product?.model?.url));
    if (hasModel || !existing) {
      next[key] = cloneProduct(product);
    } else {
      const incoming = cloneProduct(product);
      next[key] = {
        ...cloneProduct(existing),
        ...incoming,
        model: cloneModel(existing.model),
        parts: Array.isArray(product.parts) && product.parts.length
          ? cloneSmall(product.parts)
          : cloneSmall(existing.parts),
      } as Asuka3DProduct;
    }
  }
  return next;
}

export function normalize3DHistoryAssets(input: unknown): Asuka3DHistoryAssets {
  if (!input || typeof input !== 'object' || Array.isArray(input)) return {};
  const next: Asuka3DHistoryAssets = {};
  for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
    if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
    const product = cloneProduct(value as Asuka3DProduct);
    const normalizedKey = get3DHistoryAssetKey(product) || normalizeToken(key);
    if (!normalizedKey) continue;
    next[normalizedKey] = product;
  }
  return next;
}

const findAsset = (product: Asuka3DProduct, assets: Asuka3DHistoryAssets): Asuka3DProduct | null => {
  const exact = assets[get3DHistoryAssetKey(product)];
  if (exact) return exact;
  const prefix = `${normalizeToken(product?.id)}::`;
  if (!prefix || prefix === '::') return null;
  const fallbackKey = Object.keys(assets).find((key) => key.startsWith(prefix));
  return fallbackKey ? assets[fallbackKey] : null;
};

const hydrateProductStaticPayload = (
  product: Asuka3DProduct,
  source: Asuka3DProduct,
): Asuka3DProduct => {
  const snapshot = cloneProduct(product);
  return {
    ...snapshot,
    model: normalizeToken(snapshot?.model?.url) ? cloneModel(snapshot.model) : cloneModel(source.model),
    parts: Array.isArray(snapshot?.parts) && snapshot.parts.length
      ? cloneSmall(snapshot.parts)
      : cloneSmall(source.parts),
    metadata: {
      ...(source.metadata && typeof source.metadata === 'object' ? cloneSmall(source.metadata) : {}),
      ...(snapshot.metadata && typeof snapshot.metadata === 'object' ? cloneSmall(snapshot.metadata) : {}),
    },
  } as Asuka3DProduct;
};

export function hydrate3DHistoryScene(
  scene: Asuka3DSceneState,
  assets: Asuka3DHistoryAssets | null | undefined,
): Asuka3DSceneState {
  const registry = normalize3DHistoryAssets(assets);
  const products = (Array.isArray(scene?.products) ? scene.products : []).map((product) => {
    const asset = findAsset(product, registry);
    if (!asset) return cloneProduct(product);
    return hydrateProductStaticPayload(product, asset);
  });
  return { ...cloneScene(scene), products };
}

export function hydrate3DHistorySceneFromRuntime(
  scene: Asuka3DSceneState,
  runtimeScene: Asuka3DSceneState | null | undefined,
): Asuka3DSceneState {
  const runtimeProducts = Array.isArray(runtimeScene?.products) ? runtimeScene.products : [];
  if (!runtimeProducts.length || !Array.isArray(scene?.products) || !scene.products.length) return cloneScene(scene);

  const byId = new Map<string, Asuka3DProduct>();
  const byCatalogId = new Map<string, Asuka3DProduct>();
  const byName = new Map<string, Asuka3DProduct>();
  for (const product of runtimeProducts) {
    const id = normalizeToken(product?.id);
    const catalogId = normalizeToken(product?.catalogId);
    const name = normalizeToken(product?.name);
    if (id) byId.set(id, product);
    if (catalogId && !byCatalogId.has(catalogId)) byCatalogId.set(catalogId, product);
    if (name && !byName.has(name)) byName.set(name, product);
  }

  const products = scene.products.map((product) => {
    const runtime = byId.get(normalizeToken(product?.id))
      ?? byCatalogId.get(normalizeToken(product?.catalogId))
      ?? byName.get(normalizeToken(product?.name))
      ?? null;
    if (!runtime) return cloneProduct(product);
    return hydrateProductStaticPayload(product, runtime);
  });

  return { ...cloneScene(scene), products };
}

const getLargeStringSignature = (value: unknown): string => {
  const raw = typeof value === 'string' ? value : String(value ?? '');
  const length = raw.length;
  if (!length) return '0';
  const sampleSize = 24;
  const head = raw.slice(0, Math.min(sampleSize, length));
  const tail = length > sampleSize ? raw.slice(Math.max(sampleSize, length - sampleSize)) : '';
  return `${length}:${head}:${tail}`;
};

export function get3DHistoryAssetsSignature(input: unknown): string {
  if (!input || typeof input !== 'object' || Array.isArray(input)) return '';
  return Object.entries(input as Record<string, unknown>)
    .filter((entry): entry is [string, Asuka3DProduct] => Boolean(entry[1] && typeof entry[1] === 'object' && !Array.isArray(entry[1])))
    .sort(([left], [right]) => left.localeCompare(right))
    .map(([key, product]) => {
      const normalizedKey = get3DHistoryAssetKey(product) || normalizeToken(key);
      return `${normalizedKey}:${getLargeStringSignature(product?.model?.url)}:${Array.isArray(product?.parts) ? product.parts.length : 0}`;
    })
    .join('|');
}
