import type { DesignerInstance } from './designer';
import { emitBefore, emitAfter } from './events';
import { useAsukaStore } from '../store/asukaStore';
import type { AsukaPermissions } from './permissions';
import { getEffectivePermissions } from './permissions';
import { notifyModuleRegistryChanged } from './modules';
import { emitRenderEngineEvent, getActiveViewEngineKind, setCurrentRenderEngineKind, type AsukaRenderEngineKind } from './renderEngine';
import { asukaHistoryDebug, summarizeHistoryInfo, summarizeHistoryTrack } from './historyDebug';
import { getSimilarColorAsync, type PrintingColor } from './printing';
import { isRuntimeHelperRole } from './runtime/runtimeRoles';
import {
  configureFabricElementInteraction,
  isFabricElementObject,
  isFabricObjectInteractionLocked,
  isFabricObjectLayerLocked,
  lockFabricLayerInteraction,
  lockFabricObjectInteraction,
  restoreFabricElementSnapshotProperties,
} from './fabricElementInteraction';
import {
  applyFabricVariationFrame,
  ensureFabricVariationFrame,
  FABRIC_VARIATION_FRAME_PROPERTY,
} from './canvas/fabricVariationGeometry';

/**
 * View snapshots, with Zustand as the logical source of truth for `activeViewId`
 * plus an internal `currentViewId` to track which view is actually rendered.
 *
 * The module also forces `state.activeViewId` updates through `useAsukaStore.setState`
 * to keep the whole UI reactive (pagination, thumbnails, and similar surfaces).
 */

function sanitizeFabricControlsLazy(): void {
  void import('./canvas/asukaControls')
    .then((module) => {
      try { module.sanitizeFabricControls?.(); } catch {}
    })
    .catch(() => {});
}

type FabricSnapshot = any;

const VIEW_SNAPSHOTS_DEBUG = false;

// Map viewId -> JSON Fabric
const viewSnapshots: Record<string, FabricSnapshot | null> = {};

// View currently rendered on the canvas
let currentViewId: string | null = null;

// When restoring a saved state, multiple store updates can temporarily
// put the app in an inconsistent intermediate state (e.g. activeViewId
// set before snapshots are imported). During that window we must avoid
// clearing the canvas or overwriting imported snapshots.
let isStateRestoring = false;

const VIEW_SNAPSHOT_SERIALIZE_PROPS = [
  'id',
  'objectId',
  'asukaId',
  '_asukaObjectId',
  '_asukaZoneId',
  '_asukaRole',
  '_asukaLastGood',
  '_asukaElementIsInstance',
  '_asukaElementId',
  '_asukaElementFamilyId',
  '_asukaElementName',
  '_asukaElementVariationId',
  FABRIC_VARIATION_FRAME_PROPERTY,
  '_asukaElementBasePrice',
  '_asukaElementVariationPrice',
  '_asukaIgnoreDrawingZones',
  'itemGroup',
  'layerIndex',
  'price',
  'printingMethodId',
  '_asukaPricingColor',
  'selectable',
  'evented',
  'hasControls',
  'hasBorders',
  'hoverCursor',
  'perPixelTargetFind',
  'targetFindTolerance',
  '_asukaInteractionLocked',
  '_asukaLocked',
  '_asukaLockRestoreState',
  'lockMovementX',
  'lockMovementY',
  'lockScalingX',
  'lockScalingY',
  'lockRotation',
  'excludeFromExport',
  '_asukaBringToFront',
  '_asukaOrderingSeq',
  '_asukaFontId',
  '_asukaFontSource',
  '_asukaFontCssUrl',
  '_asukaFontUrl',
  '_asukaFontFormat',
  '_asukaFontWeight',
  '_asukaFontStyle',
  '_asukaFixedSlotId',
  '_asukaFixedSlotType',
  '_asukaFixedSlotConstraints',
  '_asukaFixedConstraints',
  '_asukaFixedSlotIsContent',
  '_asukaFixedSlotIsIndicator',
  '__asukaFixedLockedCenterX',
  '__asukaFixedLockedCenterY',
  '__asukaFixedLockedAngle',
];

export function setStateRestoring(v: boolean): void {
  isStateRestoring = !!v;
}

/**
 * Returns whether a full/document state import is currently replacing the
 * runtime document. Persistence listeners must not write the old mounted
 * canvas/scene back to the store during this short transition.
 */
export function isStateRestoringNow(): boolean {
  return isStateRestoring;
}

const DOCUMENT_FABRIC_HISTORY_KEY = '__document__';

function setCanvasTransientFlag(canvas: any, key: string, value: boolean): void {
  if (!canvas || !key) return;
  try {
    if (value) (canvas as any)[key] = true;
    else delete (canvas as any)[key];
  } catch {
    try { (canvas as any)[key] = value; } catch {}
  }
}


function buildScopedHistoryInfoForView(state: any, viewId: string, engine: AsukaRenderEngineKind | string): any {
  const normalizedEngine = String(engine ?? 'fabric').toLowerCase() === '3d' ? '3d' : 'fabric';
  const maxSnapshots = Math.max(1, Number(state?.documentHistoryInfo?.maxSnapshots ?? 8) || 8);
  const track = normalizedEngine === '3d'
    ? state?.document3dHistoryByView?.[viewId]
    : (state?.documentFabricHistoryByView?.[DOCUMENT_FABRIC_HISTORY_KEY] ?? state?.documentFabricHistoryByView?.[viewId]);
  const snapshots = Array.isArray(track?.snapshots) ? track.snapshots : [];
  if (!snapshots.length) {
    return {
      engine: normalizedEngine,
      scope: normalizedEngine === '3d' ? 'view' : 'document',
      viewId: normalizedEngine === '3d' ? viewId : DOCUMENT_FABRIC_HISTORY_KEY,
      index: -1,
      length: 0,
      canUndo: false,
      canRedo: false,
      maxSnapshots,
    };
  }
  const index = Math.max(0, Math.min(snapshots.length - 1, Number(track?.index ?? snapshots.length - 1) || 0));
  return {
    engine: normalizedEngine,
    scope: normalizedEngine === '3d' ? 'view' : (String(track?.scope ?? '').trim() === 'document' ? 'document' : 'view'),
    viewId: normalizedEngine === '3d' ? viewId : (String(track?.scope ?? '').trim() === 'document' ? DOCUMENT_FABRIC_HISTORY_KEY : viewId),
    index,
    length: snapshots.length,
    canUndo: index > 0,
    canRedo: index < snapshots.length - 1,
    maxSnapshots: Math.max(1, Number(track?.maxSnapshots ?? maxSnapshots) || maxSnapshots),
  };
}

function setActiveViewStateFromSwitch(targetViewId: string): AsukaRenderEngineKind {
  let targetKind: AsukaRenderEngineKind = 'fabric';
  try {
    (useAsukaStore as any).setState(
      (prev: any) => {
        const views = Array.isArray(prev?.views) ? prev.views : [];
        targetKind = getActiveViewEngineKind(views, targetViewId, prev?.defaultRenderEngineKind ?? 'fabric');
        const nextViewportByView = { ...(prev?.documentViewportByView ?? {}) };
        if (!nextViewportByView[targetViewId]) nextViewportByView[targetViewId] = { zoom: 1, panX: 0, panY: 0 };
        const historyInfo = buildScopedHistoryInfoForView(prev, targetViewId, targetKind);
        const nextHistoryInfoByView = {
          ...(prev?.documentHistoryInfoByView ?? {}),
          [targetViewId]: historyInfo,
        };
        try { setCurrentRenderEngineKind(targetKind); } catch {}
        try { notifyModuleRegistryChanged(); } catch {}
        asukaHistoryDebug('viewSnapshots.switch.setActiveViewState', {
          targetViewId,
          targetKind,
          historyInfo: summarizeHistoryInfo(historyInfo),
          fabricTrack: summarizeHistoryTrack(prev?.documentFabricHistoryByView?.[DOCUMENT_FABRIC_HISTORY_KEY] ?? prev?.documentFabricHistoryByView?.[targetViewId]),
          track3d: summarizeHistoryTrack(prev?.document3dHistoryByView?.[targetViewId]),
        });
        return {
          ...prev,
          activeViewId: targetViewId,
          documentViews: views,
          documentViewportByView: nextViewportByView,
          activeRenderEngineKind: targetKind,
          documentHistoryInfo: { ...(prev?.documentHistoryInfo ?? {}), ...historyInfo },
          documentHistoryInfoByView: nextHistoryInfoByView,
        };
      },
      false,
      'views/switchViewWithCanvas.scopedState',
    );
  } catch (err) {
    debugLog('switchViewWithCanvas setState error', err);
  }
  return targetKind;
}

function releaseCanvasRestoringGuard(canvas: any): void {
  if (!canvas) return;
  const release = () => setCanvasTransientFlag(canvas, '__asukaRestoringFromState', false);
  try {
    const raf = typeof requestAnimationFrame === 'function'
      ? requestAnimationFrame
      : ((cb: FrameRequestCallback) => setTimeout(cb, 16) as any);
    raf(() => raf(() => release()));
  } catch {
    setTimeout(release, 50);
  }
  try { setTimeout(release, 150); } catch {}
}

function markObjectRestoredFromSnapshot(serializedObject: any, obj: any): void {
  if (!obj) return;
  try { restoreFabricElementSnapshotProperties(serializedObject, obj); } catch {}
  try { (obj as any).__asukaRestoredFromState = true; } catch {}
  try { (obj as any)._asukaAutoFitDone = true; } catch {}
}

/**
 * Read-only access to a stored snapshot.
 * Used by UI (thumbnails) without requiring the view to be selected.
 */
export function getSnapshotForView(viewId: string): FabricSnapshot | null {
  const id = String(viewId ?? "").trim();
  if (!id) return null;
  return (viewSnapshots as any)[id] ?? null;
}

/**
 * Delete a stored snapshot for the given view.
 * Used when removing a view from the document.
 */
export function deleteSnapshotForView(viewId: string): void {
  const id = String(viewId ?? '').trim();
  if (!id) return;
  try {
    delete (viewSnapshots as any)[id];
  } catch {
    (viewSnapshots as any)[id] = null;
  }
  if (currentViewId === id) currentViewId = null;
}

/**
 * Ensure the given view has at least an empty snapshot.
 * This allows switching to a brand new view without relying on implicit
 * canvas clearing inside loadSnapshotForView().
 */
export function ensureEmptySnapshotForView(viewId: string): void {
  const id = String(viewId ?? '').trim();
  if (!id) return;
  if (viewSnapshots[id]) return;
  viewSnapshots[id] = { objects: [] } as any;
}


// --- Export / Import (used by state persistence) ---
export function exportViewSnapshots(): Record<string, any> {
  // Shallow clone to keep it serializable
  return { ...viewSnapshots };
}

export function importViewSnapshots(
  snapshotsByView: Record<string, any> | null | undefined,
  options: { replace?: boolean } = {},
): void {
  const replace = options.replace !== false;

  if (replace) {
    for (const k of Object.keys(viewSnapshots)) delete viewSnapshots[k];
  }

  if (!snapshotsByView) return;

  for (const [viewId, snapshot] of Object.entries(snapshotsByView)) {
    const snap = snapshot && typeof snapshot === 'object' ? snapshot as any : { objects: [] };
    viewSnapshots[viewId] = {
      ...snap,
      objects: sanitizeSnapshotObjects((snap as any)?.objects),
      drawingAreas: normalizeSnapshotDrawingAreas((snap as any)?.drawingAreas),
      activeDrawingAreaId: (snap as any)?.activeDrawingAreaId ?? null,
    } as any;
  }
}

function cloneSnapshotObjects(objects: any[] | null | undefined): any[] {
  return Array.isArray(objects) ? objects.map((obj: any) => ({ ...(obj ?? {}) })) : [];
}

function sanitizeSnapshotObjects(objects: any[] | null | undefined): any[] {
  return cloneSnapshotObjects(objects).filter((obj: any) => {
    const role = String(obj?._asukaRole ?? '').trim();
    return !isRuntimeHelperRole(role);
  });
}


function isRenderedView(viewId: string, store?: any): boolean {
  const targetViewId = String(viewId ?? '').trim();
  if (!targetViewId) return false;
  const renderedViewId = String(currentViewId ?? '').trim();
  if (renderedViewId) return targetViewId === renderedViewId;
  const activeViewId = String(store?.activeViewId ?? '').trim();
  return !!activeViewId && targetViewId === activeViewId;
}


function syncRenderedViewObjectsFromCanvas(designer: DesignerInstance | null, viewId: string, base: any = {}): void {
  const targetViewId = String(viewId ?? '').trim();
  if (!targetViewId) return;
  try {
    const live = readCanvasSnapshot(designer, base);
    const objects = sanitizeSnapshotObjects((live as any)?.objects);
    const current = (viewSnapshots as any)[targetViewId] ?? base ?? {};
    (viewSnapshots as any)[targetViewId] = {
      ...(current && typeof current === 'object' ? current : {}),
      ...(live && typeof live === 'object' ? live : {}),
      objects,
      drawingAreas: normalizeSnapshotDrawingAreas((current as any)?.drawingAreas ?? (base as any)?.drawingAreas),
      activeDrawingAreaId: (current as any)?.activeDrawingAreaId ?? (base as any)?.activeDrawingAreaId ?? null,
    } as any;
    useAsukaStore.setState((prev: any) => ({
      documentObjectsByView: {
        ...((prev?.documentObjectsByView && typeof prev.documentObjectsByView === 'object') ? prev.documentObjectsByView : {}),
        [targetViewId]: objects,
      },
    }) as any);
  } catch {}
}

function readCanvasSnapshot(designer: DesignerInstance | null, base: any = {}): FabricSnapshot | null {
  const canvas = getCanvasFromDesigner(designer);
  if (!canvas) return null;
  try {
    const json = typeof canvas.toJSON === 'function'
      ? canvas.toJSON(VIEW_SNAPSHOT_SERIALIZE_PROPS as any)
      : { objects: [] };
    const source = json && typeof json === 'object' ? json : { objects: [] };
    return {
      ...(base && typeof base === 'object' ? base : {}),
      ...(source as any),
      objects: sanitizeSnapshotObjects((source as any)?.objects),
    } as any;
  } catch {
    try {
      const objects = Array.isArray(canvas?.getObjects?.())
        ? canvas.getObjects().map((obj: any) => obj?.toObject?.(VIEW_SNAPSHOT_SERIALIZE_PROPS as any) ?? null).filter(Boolean)
        : [];
      return {
        ...(base && typeof base === 'object' ? base : {}),
        objects: sanitizeSnapshotObjects(objects),
      } as any;
    } catch {
      return null;
    }
  }
}

function normalizeSnapshotDrawingAreas(zones: any[] | null | undefined): any[] {
  if (!Array.isArray(zones)) return [];
  const byId = new Map<string, any>();
  const order: string[] = [];
  for (const raw of zones) {
    if (!raw || typeof raw !== 'object') continue;
    const id = String(raw.id ?? '').trim();
    if (!id) continue;
    const next = {
      id,
      x: Number(raw.x ?? raw.left ?? 0) || 0,
      y: Number(raw.y ?? raw.top ?? 0) || 0,
      width: Math.max(1, Number(raw.width ?? 0) || 1),
      height: Math.max(1, Number(raw.height ?? 0) || 1),
      shape: raw.shape,
      sourceSvg: raw.sourceSvg,
      sourcePath: raw.sourcePath,
      physicalSize: raw.physicalSize && typeof raw.physicalSize === 'object' ? raw.physicalSize : undefined,
    };
    if (!byId.has(id)) order.push(id);
    byId.set(id, next);
  }
  return order.map((id) => byId.get(id)).filter(Boolean);
}

function getCurrentDocumentDrawingAreas(designer?: DesignerInstance | null): any[] {
  try {
    const runtimeZones = (designer as any)?.getDrawingZones?.();
    if (Array.isArray(runtimeZones)) return normalizeSnapshotDrawingAreas(runtimeZones);
  } catch {}
  try {
    const store = useAsukaStore.getState() as any;
    return normalizeSnapshotDrawingAreas(store?.documentDrawingAreas);
  } catch {
    return [];
  }
}

export function getSnapshotDrawingAreasForView(viewId: string): any[] {
  const id = String(viewId ?? '').trim();
  if (!id) return [];
  const snap = viewSnapshots[id];
  return normalizeSnapshotDrawingAreas((snap as any)?.drawingAreas);
}

export function setSnapshotDrawingAreasForView(viewId: string, zones: any[] | null | undefined, activeDrawingAreaId?: string | null): void {
  const id = String(viewId ?? '').trim();
  if (!id) return;
  const existing = viewSnapshots[id] ?? { objects: [] };
  const nextZones = normalizeSnapshotDrawingAreas(zones);
  viewSnapshots[id] = {
    ...(existing && typeof existing === 'object' ? existing : {}),
    drawingAreas: nextZones,
    activeDrawingAreaId: activeDrawingAreaId ?? (existing as any)?.activeDrawingAreaId ?? null,
  } as any;
}

function buildSnapshotFromDocumentView(viewId: string): FabricSnapshot | null {
  try {
    const store = useAsukaStore.getState() as any;
    const byView = store?.documentObjectsByView ?? {};
    if (!Object.prototype.hasOwnProperty.call(byView ?? {}, String(viewId))) return null;
    const base = viewSnapshots[String(viewId)] ?? {};
    const targetViewId = String(viewId);
    const activeViewId = String(store?.activeViewId ?? '').trim();
    const storeObjects = sanitizeSnapshotObjects(byView[targetViewId]);
    const existingObjects = sanitizeSnapshotObjects((base as any)?.objects);
    const shouldUseStoreObjects = storeObjects.length > 0 || existingObjects.length === 0;
    const drawingAreas = targetViewId === activeViewId
      ? getCurrentDocumentDrawingAreas()
      : normalizeSnapshotDrawingAreas((base as any)?.drawingAreas);
    return {
      ...(base && typeof base === 'object' ? base : {}),
      objects: shouldUseStoreObjects ? storeObjects : existingObjects,
      drawingAreas,
      activeDrawingAreaId: targetViewId === activeViewId
        ? (store?.documentSelection?.activeDrawingAreaId ?? (base as any)?.activeDrawingAreaId ?? null)
        : ((base as any)?.activeDrawingAreaId ?? null),
    } as any;
  } catch {
    return null;
  }
}

/**
 * Force a stored view snapshot to use the canonical document objects.
 *
 * This is intentionally different from `saveSnapshotForView`: the active
 * Fabric canvas may still contain the previous visual state while an
 * asynchronous object replacement is in progress. In that short window the
 * document store is the authoritative source and must not be overwritten by
 * the canvas that is about to be updated.
 */
export function syncSnapshotObjectsFromDocumentView(viewId: string): boolean {
  const targetViewId = String(viewId ?? '').trim();
  if (!targetViewId) return false;

  try {
    const store = useAsukaStore.getState() as any;
    const byView = store?.documentObjectsByView ?? {};
    if (!Object.prototype.hasOwnProperty.call(byView, targetViewId)) return false;

    const existing = viewSnapshots[targetViewId] ?? {};
    const activeViewId = String(store?.activeViewId ?? '').trim();
    viewSnapshots[targetViewId] = {
      ...(existing && typeof existing === 'object' ? existing : {}),
      objects: sanitizeSnapshotObjects(byView[targetViewId]),
      drawingAreas: targetViewId === activeViewId
        ? getCurrentDocumentDrawingAreas()
        : normalizeSnapshotDrawingAreas((existing as any)?.drawingAreas),
      activeDrawingAreaId: targetViewId === activeViewId
        ? (store?.documentSelection?.activeDrawingAreaId ?? (existing as any)?.activeDrawingAreaId ?? null)
        : ((existing as any)?.activeDrawingAreaId ?? null),
    } as any;
    return true;
  } catch {
    return false;
  }
}

// ---------------------------------------------------------------------------
// Elements helpers (sync element instances across views)
// ---------------------------------------------------------------------------

export function patchElementVariationInSnapshots(payload: {
  elementName: string;
  variationId: string;
  /** Optional image URL for fabric.Image objects. */
  imageUrl?: string;
  /** Optional intrinsic size of the new variation image, used to keep on-canvas box size stable across view loads. */
  intrinsicW?: number;
  intrinsicH?: number;
}): string[] {
  const { elementName, variationId, imageUrl } = payload;
  if (!elementName) return [];

  const iw = Number((payload as any).intrinsicW ?? 0);
  const ih = Number((payload as any).intrinsicH ?? 0);
  const hasIntrinsic = Number.isFinite(iw) && Number.isFinite(ih) && iw > 0 && ih > 0;

  const impactedViewIds: string[] = [];

  for (const [viewId, snap] of Object.entries(viewSnapshots)) {
    if (!snap || typeof snap !== 'object') continue;
    const objs = (snap as any).objects;
    if (!Array.isArray(objs)) continue;

    let changed = false;
    let hasElement = false;
    for (const o of objs) {
      if (!o || typeof o !== 'object') continue;
      if (String((o as any)._asukaElementName ?? '') !== String(elementName)) continue;

      hasElement = true;

      // Always keep instance metadata in sync
      (o as any)._asukaElementVariationId = String(variationId);
      (o as any)._asukaElementIsInstance = true;
      changed = true;

      // Update image source if possible.
      if (typeof imageUrl === 'string' && imageUrl.length > 0) {
        if (typeof (o as any).src === 'string') {
          // Preserve the on-canvas box size when the intrinsic image size changes.
          // The snapshot stores width/height (intrinsic) + scaleX/scaleY.
          // If we only change src, Fabric may rehydrate with mismatched dimensions across view switches.
          if (hasIntrinsic) {
            const frame = ensureFabricVariationFrame(o);
            if (frame) applyFabricVariationFrame(o, frame, { width: iw, height: ih });
          }
          (o as any).src = imageUrl;
          changed = true;
        }
      }
    }

    if (changed) {
      viewSnapshots[viewId] = { ...(snap as any), objects: objs } as any;
    }

    if (hasElement) impactedViewIds.push(String(viewId));
  }

  return impactedViewIds;
}

/**
 * Remove all instances of an Element from every stored view snapshot.
 * This keeps Elements (structural parts) consistent across multi-views.
 */


function snapshotObjectMatchesFixedSlot(obj: any, slotId: string, objectIds?: Set<string>): boolean {
  if (!obj || !slotId) return false;
  try {
    const directSlotId = String((obj as any)?._asukaFixedSlotId ?? '').trim();
    if (directSlotId === slotId) return true;

    const objectId = String((obj as any)?._asukaObjectId ?? (obj as any)?.id ?? (obj as any)?.uuid ?? '').trim();
    if (objectIds && objectId && objectIds.has(objectId)) return true;

    const nested = [
      ...((Array.isArray((obj as any)?.objects) ? (obj as any).objects : []) as any[]),
      ...((Array.isArray((obj as any)?._objects) ? (obj as any)._objects : []) as any[]),
    ];
    return nested.some((child: any) => snapshotObjectMatchesFixedSlot(child, slotId, objectIds));
  } catch {
    return false;
  }
}

/**
 * Remove fixed-slot indicators/content from every stored view snapshot.
 * Used when deleting a marker definition so removed content cannot reappear
 * after switching views or rebuilding the active canvas from snapshots.
 */
export function removeFixedSlotObjectsFromSnapshots(slotId: string, objectIds: string[] = []): void {
  const normalizedSlotId = String(slotId ?? '').trim();
  if (!normalizedSlotId) return;
  const idSet = new Set((Array.isArray(objectIds) ? objectIds : []).map((id) => String(id ?? '').trim()).filter(Boolean));

  for (const [viewId, snap] of Object.entries(viewSnapshots)) {
    if (!snap || typeof snap !== 'object') continue;
    const objs = (snap as any).objects;
    if (!Array.isArray(objs)) continue;

    const next = objs.filter((o: any) => !snapshotObjectMatchesFixedSlot(o, normalizedSlotId, idSet));
    if (next.length === objs.length) continue;
    viewSnapshots[viewId] = { ...(snap as any), objects: next } as any;
  }
}

function snapshotObjectMatchesElement(obj: any, identifiers: Set<string>, objectIds?: Set<string>): boolean {
  if (!obj || !identifiers?.size) return false;
  try {
    const elementId = String((obj as any)?._asukaElementId ?? '').trim();
    const elementName = String((obj as any)?._asukaElementName ?? '').trim();
    const familyId = String((obj as any)?._asukaElementFamilyId ?? '').trim();
    if ((elementId && identifiers.has(elementId)) || (elementName && identifiers.has(elementName)) || (familyId && identifiers.has(familyId))) return true;

    const objectId = String((obj as any)?._asukaObjectId ?? (obj as any)?.id ?? (obj as any)?.uuid ?? '').trim();
    if (objectIds && objectId && objectIds.has(objectId)) return true;

    const nested = [
      ...((Array.isArray((obj as any)?.objects) ? (obj as any).objects : []) as any[]),
      ...((Array.isArray((obj as any)?._objects) ? (obj as any)._objects : []) as any[]),
    ];
    return nested.some((child: any) => snapshotObjectMatchesElement(child, identifiers, objectIds));
  } catch {
    return false;
  }
}

export function removeElementInstancesFromSnapshots(elementIdentifiers: string | string[], objectIds: string[] = [], viewIds?: string | string[]): void {
  const identifiers = new Set(
    (Array.isArray(elementIdentifiers) ? elementIdentifiers : [elementIdentifiers])
      .map((value) => String(value ?? '').trim())
      .filter(Boolean),
  );
  if (!identifiers.size) return;
  const idSet = new Set((Array.isArray(objectIds) ? objectIds : []).map((id) => String(id ?? '').trim()).filter(Boolean));
  const allowedViewIds = typeof viewIds === 'undefined'
    ? null
    : new Set((Array.isArray(viewIds) ? viewIds : [viewIds]).map((id) => String(id ?? '').trim()).filter(Boolean));

  for (const [viewId, snap] of Object.entries(viewSnapshots)) {
    if (allowedViewIds && !allowedViewIds.has(String(viewId))) continue;
    if (!snap || typeof snap !== 'object') continue;
    const objs = (snap as any).objects;
    if (!Array.isArray(objs)) continue;

    const next = objs.filter((o: any) => !snapshotObjectMatchesElement(o, identifiers, idSet));
    if (next.length === objs.length) continue;
    viewSnapshots[viewId] = { ...(snap as any), objects: next } as any;
  }
}

function debugLog(...args: any[]): void {
  if (!VIEW_SNAPSHOTS_DEBUG) return;
  console.debug('[AsukaDesigner:view-snapshots]', ...args);
}

export function unsetCurrentView(designer: DesignerInstance | null): void {
  // Clears the active Fabric canvas and removes the notion of "current view".
  currentViewId = null;
  try {
    (useAsukaStore as any).setState(
      (prev: any) => ({ ...prev, activeViewId: null }),
      false,
      'views/unsetCurrentView',
    );
  } catch {}

  try {
    const canvas = (designer as any)?.getCanvas?.();
    if (canvas) {
      canvas.discardActiveObject?.();
      canvas.clear();
      canvas.requestRenderAll?.();
      canvas.calcOffset?.();
    }
  } catch {}

  try { (designer as any)?.refreshAfterViewLoad?.(); } catch {}
}
export function getCurrentViewId(): string | null {
  return currentViewId;
}

/**
 * Attach a newly created Fabric runtime to the already active logical view.
 *
 * Switching between renderer kinds disposes the previous canvas. The new
 * Fabric canvas must therefore be associated with the target view before its
 * saved JSON is loaded; using switchViewWithCanvas here would incorrectly save
 * the brand-new empty canvas over the last rendered Fabric snapshot.
 */
export async function mountFabricView(designer: DesignerInstance | null, viewId: string): Promise<void> {
  const targetViewId = String(viewId ?? '').trim();
  if (!designer || !targetViewId) return;
  currentViewId = targetViewId;
  await loadSnapshotForView(designer, targetViewId);
  try {
    const state = useAsukaStore.getState() as any;
    const effective = getEffectivePermissions(
      state.permissions as AsukaPermissions,
      (state.permissionsByView?.[targetViewId] ?? null) as any,
    );
    applyCanvasEditability(designer, effective);
  } catch {}
}

function getCanvasFromDesigner(designer: DesignerInstance | null): any | null {
  if (!designer) {
    debugLog('getCanvasFromDesigner -> no designer');
    return null;
  }
  const canvas = (designer as any).getCanvas?.();
  if (!canvas) {
    debugLog('getCanvasFromDesigner -> no canvas');
  }
  return canvas ?? null;
}

function disposeFabricObjectDeep(obj: any, seen = new Set<any>()): void {
  if (!obj || seen.has(obj)) return;
  seen.add(obj);
  try {
    const children = typeof obj.getObjects === 'function'
      ? obj.getObjects()
      : Array.isArray(obj._objects)
        ? obj._objects
        : [];
    if (Array.isArray(children)) {
      children.slice().forEach((child: any) => disposeFabricObjectDeep(child, seen));
    }
  } catch {}
  try { if (obj.clipPath) disposeFabricObjectDeep(obj.clipPath, seen); } catch {}
  try { obj.off?.(); } catch {}
  try { obj.dispose?.(); } catch {}
  try { obj._cacheCanvas = undefined; obj._cacheContext = undefined; } catch {}
}

function disposeCanvasObjectsForReload(canvas: any): void {
  try { canvas.discardActiveObject?.(); } catch {}
  const objects = (() => {
    try { return Array.isArray(canvas?.getObjects?.()) ? canvas.getObjects().slice() : []; } catch { return []; }
  })();
  for (const obj of objects) {
    try { canvas.remove?.(obj); } catch {}
    try { disposeFabricObjectDeep(obj); } catch {}
  }
  try {
    if (Array.isArray(canvas?._objects)) canvas._objects = canvas._objects.filter((obj: any) => obj && typeof obj.render === 'function');
  } catch {}
}

function createCanvasLoadCover(canvas: any): () => void {
  const noop = () => {};
  if (!canvas) return noop;

  const createRemover = (element: HTMLElement) => {
    let removed = false;
    return () => {
      if (removed) return;
      removed = true;
      try { element.remove(); } catch {}
      try {
        if ((canvas as any).__asukaLoadCoverElement === element) {
          delete (canvas as any).__asukaLoadCoverElement;
        }
      } catch {}
    };
  };

  try {
    const previous = (canvas as any).__asukaLoadCoverElement as HTMLElement | undefined;
    if (previous?.isConnected) return createRemover(previous);
    previous?.remove?.();
    delete (canvas as any).__asukaLoadCoverElement;
  } catch {}

  const lowerCanvas = (canvas as any).lowerCanvasEl ?? (canvas as any).getElement?.();
  const wrapper = (canvas as any).wrapperEl ?? lowerCanvas?.parentElement;
  if (!lowerCanvas || !wrapper) return noop;

  const objectCount = (() => {
    try { return Array.isArray(canvas.getObjects?.()) ? canvas.getObjects().length : 0; } catch { return 0; }
  })();
  if (objectCount === 0) return noop;

  try {
    const ownerDocument = lowerCanvas.ownerDocument ?? document;
    const cover = ownerDocument.createElement('canvas');
    const sourceWidth = Math.max(1, Math.floor(Number(lowerCanvas.width) || 1));
    const sourceHeight = Math.max(1, Math.floor(Number(lowerCanvas.height) || 1));
    cover.width = sourceWidth;
    cover.height = sourceHeight;
    const context = cover.getContext('2d');
    if (!context) return noop;
    context.drawImage(lowerCanvas, 0, 0, sourceWidth, sourceHeight);

    const canvasRect = lowerCanvas.getBoundingClientRect();
    const wrapperRect = wrapper.getBoundingClientRect();
    cover.setAttribute('aria-hidden', 'true');
    cover.dataset.asukaCanvasLoadCover = 'true';
    Object.assign(cover.style, {
      position: 'absolute',
      left: `${canvasRect.left - wrapperRect.left}px`,
      top: `${canvasRect.top - wrapperRect.top}px`,
      width: `${canvasRect.width}px`,
      height: `${canvasRect.height}px`,
      display: 'block',
      pointerEvents: 'none',
      userSelect: 'none',
      zIndex: '20',
    });
    wrapper.appendChild(cover);
    (canvas as any).__asukaLoadCoverElement = cover;
    return createRemover(cover);
  } catch {
    // Restoration still proceeds if the browser cannot clone the current frame.
    return noop;
  }
}

/**
 * Save the current canvas into the snapshot of a given view.
 */
export function saveSnapshotForView(
  designer: DesignerInstance | null,
  viewId: string,
): void {
  debugLog('saveSnapshotForView called', { viewId });

  try {
    const store = useAsukaStore.getState() as any;
    const targetViewId = String(viewId);
    const byView = store?.documentObjectsByView ?? {};
    const base = viewSnapshots[targetViewId] ?? {};
    const activeViewId = String(store?.activeViewId ?? '').trim();
    const isActiveRenderedView = isRenderedView(targetViewId, store);
    const canvas = getCanvasFromDesigner(designer) as any;
    const canvasIsRestoring = Boolean(
      canvas?.__asukaSnapshotLoading
      || canvas?.__asukaViewRebuilding
      || canvas?.__asukaStateImporting
      || canvas?.__asukaRestoringFromState,
    );
    const storeObjects = Array.isArray(byView?.[targetViewId]) ? sanitizeSnapshotObjects(byView[targetViewId]) : null;
    const drawingAreas = targetViewId === activeViewId
      ? getCurrentDocumentDrawingAreas(designer)
      : normalizeSnapshotDrawingAreas((base as any)?.drawingAreas);
    const activeDrawingAreaId = targetViewId === activeViewId
      ? (store?.documentSelection?.activeDrawingAreaId ?? (base as any)?.activeDrawingAreaId ?? null)
      : ((base as any)?.activeDrawingAreaId ?? null);

    if (isActiveRenderedView && !canvasIsRestoring) {
      const live = readCanvasSnapshot(designer, base);
      if (live) {
        const json = {
          ...live,
          drawingAreas,
          activeDrawingAreaId,
        };
        debugLog('saveSnapshotForView success (live-canvas)', { viewId, objects: json.objects.length });
        viewSnapshots[targetViewId] = json;
        try {
          useAsukaStore.setState((prev: any) => ({
            documentObjectsByView: {
              ...((prev?.documentObjectsByView && typeof prev.documentObjectsByView === 'object') ? prev.documentObjectsByView : {}),
              [targetViewId]: sanitizeSnapshotObjects(json.objects),
            },
          }) as any);
        } catch {}
        return;
      }
    }

    if (storeObjects && storeObjects.length > 0) {
      const json = {
        ...(base && typeof base === 'object' ? base : {}),
        objects: storeObjects,
        drawingAreas,
        activeDrawingAreaId,
      };
      debugLog('saveSnapshotForView success (document-store)', { viewId, objects: json.objects.length });
      viewSnapshots[targetViewId] = json;
      return;
    }
  } catch {}

  const rebuilt = buildSnapshotFromDocumentView(String(viewId));
  if (rebuilt) {
    viewSnapshots[String(viewId)] = rebuilt;
    debugLog('saveSnapshotForView success (document-rebuilt)', {
      viewId,
      objects: Array.isArray((rebuilt as any)?.objects) ? (rebuilt as any).objects.length : 0,
    });
    return;
  }

  const existing = viewSnapshots[String(viewId)] ?? { objects: [] };
  viewSnapshots[String(viewId)] = {
    ...(existing && typeof existing === 'object' ? existing : {}),
    objects: sanitizeSnapshotObjects((existing as any)?.objects),
    drawingAreas: normalizeSnapshotDrawingAreas((existing as any)?.drawingAreas),
    activeDrawingAreaId: (existing as any)?.activeDrawingAreaId ?? null,
  } as any;
  debugLog('saveSnapshotForView fallback (existing snapshot only)', { viewId });
}

/**
 * Save the current canvas into the snapshot of the active view
 * (based on `state.activeViewId` or `currentViewId`).
 */
export function saveSnapshotForActiveView(designer: DesignerInstance | null): void {
  const state = useAsukaStore.getState() as any;
  const storeActiveViewId: string | null = state.activeViewId ?? null;
  const viewIdToUse = storeActiveViewId || currentViewId;

  debugLog('saveSnapshotForActiveView', {
    storeActiveViewId,
    currentViewId,
    viewIdToUse,
  });

  if (!viewIdToUse) {
    debugLog('saveSnapshotForActiveView aborted: no view id to use');
    return;
  }

  saveSnapshotForView(designer, viewIdToUse);
}

/**
 * Load a view snapshot into the canvas.
 *
 * - Si aucun snapshot n'existe encore -> canvas VIERGE
 */
export async function loadSnapshotForView(
  designer: DesignerInstance | null,
  viewId: string,
): Promise<void> {
  debugLog('loadSnapshotForView called', { viewId });
  const canvas = getCanvasFromDesigner(designer);
  if (!canvas) {
    debugLog('loadSnapshotForView aborted: no canvas');
    return;
  }

  const snapshot = viewSnapshots[String(viewId)] ?? null;

  const hasSnapshot = Boolean(snapshot);
  let objectsCount = 0;
  if (snapshot && Array.isArray((snapshot as any).objects)) {
    objectsCount = (snapshot as any).objects.length;
  }
  debugLog('loadSnapshotForView snapshot info', {
    viewId,
    hasSnapshot,
    objectsCount,
  });

  const applySnapshotDrawingAreas = (snap: any | null) => {
    const zones = normalizeSnapshotDrawingAreas((snap as any)?.drawingAreas);
    const activeAreaId = String((snap as any)?.activeDrawingAreaId ?? '').trim() || null;
    try { (designer as any)?.setDrawingZones?.(zones as any, { bypassPermissions: true, source: 'snapshot.loadDrawingAreas' }); } catch {}
    try { designer?.setActiveDrawingZone?.(activeAreaId && zones.some((z: any) => String(z.id) === activeAreaId) ? activeAreaId : null); } catch {}
    try {
      const store = useAsukaStore.getState() as any;
      store?.setDocumentDrawingAreas?.(zones as any);
      store?.setDocumentSelection?.({ activeDrawingAreaId: activeAreaId && zones.some((z: any) => String(z.id) === activeAreaId) ? activeAreaId : null });
    } catch {}
  };

  // Drawing-area restore can rebuild helper Fabric objects before the real
  // canvas JSON is loaded. Guard that phase too, otherwise generic object-added
  // listeners may treat helper/recreated objects as fresh user additions.
  setCanvasTransientFlag(canvas, '__asukaRestoringFromState', true);
  applySnapshotDrawingAreas(snapshot);

  // IMPORTANT:
  // Never wipe the current canvas when the target view has no snapshot.
  // Transient states (like JSON import) can briefly set activeViewId while
  // snapshots are not yet ready; clearing here causes "flash then wipe".
  //
  // Views that truly need an empty canvas should explicitly clear at creation
  // time (or store an empty snapshot), not by implicit navigation.
  if (!snapshot) {
    debugLog('loadSnapshotForView -> skip (no snapshot)');
    releaseCanvasRestoringGuard(canvas);
    const currentState = useAsukaStore.getState() as any;
    if (
      String(currentState?.activeViewId ?? '').trim() === String(viewId)
      && getActiveViewEngineKind(
        currentState?.views ?? [],
        currentState?.activeViewId ?? null,
        currentState?.defaultRenderEngineKind ?? 'fabric',
      ) === 'fabric'
    ) {
      emitRenderEngineEvent({
        type: 'ready',
        action: 'view.activate',
        engine: 'fabric',
        viewId,
      });
    }
    return;
  }

  // fabric.Canvas#loadFromJSON clears internally, but repeated load/rebuild cycles
  // can leave object caches/listeners alive until GC. Mark the canvas as loading
  // before removing old objects so object:removed handlers ignore these transient
  // removals, then dispose old Fabric objects explicitly.
  const removeLoadCover = createCanvasLoadCover(canvas);
  let finalized = false;
  const markLoading = (value: boolean) => {
    setCanvasTransientFlag(canvas, '__asukaSnapshotLoading', value);
    if (value) setCanvasTransientFlag(canvas, '__asukaRestoringFromState', true);
  };

  const finalizeLoad = () => {
    if (finalized) return;
    finalized = true;
    setCanvasTransientFlag(canvas, '__asukaSnapshotLoading', false);
    releaseCanvasRestoringGuard(canvas);

    debugLog('loadSnapshotForView -> loadFromJSON completed', {
      viewId,
    });

    // loadFromJSON clears the canvas after the first drawing-area restore pass.
    // Re-apply the per-view drawing areas now that the target view objects are
    // loaded, so runtime zone indexes/placeholders/clips are rebuilt for the
    // restored canvas instead of being lost after a view switch.
    applySnapshotDrawingAreas(snapshot);

    try { sanitizeFabricControlsLazy(); } catch {}
    // Defensive: remove any runtime/helpers that accidentally got serialized in old snapshots.
    try {
      const objs = canvas.getObjects ? canvas.getObjects() : [];
      objs.forEach((o: any) => {
        const role = (o as any)?._asukaRole;
        if (isRuntimeHelperRole(role)) {
          try { canvas.remove(o); } catch {}
        }
      });
    } catch {}
    try { canvas.discardActiveObject?.(); } catch {}
    // Keep the logical document store in sync with the objects Fabric just
    // rehydrated. Some UI modules (Elements, Layers, export targets) read from
    // documentObjectsByView rather than directly from the live canvas; without
    // this, a restored element can be visible on the canvas but absent from the
    // Elements list after a canonical document-part reload.
    try { syncRenderedViewObjectsFromCanvas(designer, viewId, snapshot); } catch {}
    try { designer?.refreshAfterViewLoad?.(); } catch {}

    // Re-apply the persisted per-view Fabric history after Fabric has finished
    // loading/reviving objects. Some Fabric object-added/loaded listeners can
    // touch the local runtime track during load; if that happens after the first
    // import in switchViewWithCanvas(), the History UI is briefly correct and then
    // falls back to a one-snapshot baseline. Importing the stored track here makes
    // the loaded view the final source of truth.
    try {
      const storedFabricTrack = (useAsukaStore.getState() as any)?.documentFabricHistoryByView?.[DOCUMENT_FABRIC_HISTORY_KEY] ?? (useAsukaStore.getState() as any)?.documentFabricHistoryByView?.[viewId] ?? null;
      const storedLength = Array.isArray((storedFabricTrack as any)?.snapshots) ? (storedFabricTrack as any).snapshots.length : 0;
      if (storedLength > 0 && typeof (designer as any)?.importHistoryTrack === 'function') {
        asukaHistoryDebug('viewSnapshots.load.finalize.importStoredFabricTrack', {
          viewId,
          storedFabricTrack: summarizeHistoryTrack(storedFabricTrack),
        });
        (designer as any).importHistoryTrack(viewId, storedFabricTrack);
      }
    } catch (error) {
      asukaHistoryDebug('viewSnapshots.load.finalize.importStoredFabricTrack.error', {
        viewId,
        error: String((error as any)?.message ?? error),
      });
    }

    // Render strategy:
    // - `loadFromJSON` may finish before all image elements are fully decoded.
    // - Some environments only paint images on the next interaction (click/move),
    //   which looks like objects "disappear" after a view switch.
    // We force a few scheduled re-renders and also attach onload handlers to any
    // not-yet-loaded image elements.
    try {
      const objs = canvas.getObjects ? canvas.getObjects() : [];
      for (const o of objs) {
        if (!o) continue;
        const isImg = o.type === 'image' || o.isType?.('image');
        if (!isImg) continue;
        const el = (o as any)?._originalElement || (o as any)?._element || (o as any)?.getElement?.();
        if (el && typeof el === 'object' && 'complete' in el && !(el as any).complete) {
          try {
            const prev = (el as any).onload;
            (el as any).onload = (...args: any[]) => {
              try { prev?.(...args); } catch {}
              try { canvas.requestRenderAll?.(); } catch {}
              try { canvas.renderAll?.(); } catch {}
            };
          } catch {}
        }
      }
    } catch {}

    const scheduleRender = () => {
      try { canvas.requestRenderAll?.(); } catch {}
      try { canvas.renderAll?.(); } catch {}
    };

    // Immediate + a couple of delayed renders to catch late image loads.
    scheduleRender();
    try {
      requestAnimationFrame(() => scheduleRender());
      requestAnimationFrame(() => requestAnimationFrame(() => scheduleRender()));
    } catch {
      setTimeout(scheduleRender, 0);
      setTimeout(scheduleRender, 50);
    }
    setTimeout(scheduleRender, 150);

    const revealLoadedCanvas = () => {
      scheduleRender();
      removeLoadCover();
    };
    try {
      requestAnimationFrame(() => requestAnimationFrame(revealLoadedCanvas));
    } catch {
      setTimeout(revealLoadedCanvas, 50);
    }
    // Defensive fallback for background tabs where requestAnimationFrame can be
    // throttled indefinitely.
    setTimeout(revealLoadedCanvas, 350);
    const currentState = useAsukaStore.getState() as any;
    if (
      String(currentState?.activeViewId ?? '').trim() === String(viewId)
      && getActiveViewEngineKind(
        currentState?.views ?? [],
        currentState?.activeViewId ?? null,
        currentState?.defaultRenderEngineKind ?? 'fabric',
      ) === 'fabric'
    ) {
      emitRenderEngineEvent({
        type: 'ready',
        action: 'view.activate',
        engine: 'fabric',
        viewId,
      });
    }
  };

  markLoading(true);
  try { disposeCanvasObjectsForReload(canvas); } catch {}
  try {
    await canvas.loadFromJSON(snapshot, (serializedObject: any, fabricObject: any) => {
      markObjectRestoredFromSnapshot(serializedObject, fabricObject);
    });
    finalizeLoad();
  } catch (err) {
    setCanvasTransientFlag(canvas, '__asukaSnapshotLoading', false);
    releaseCanvasRestoringGuard(canvas);
    removeLoadCover();
    debugLog('loadSnapshotForView error on loadFromJSON', err);
    try { canvas.requestRenderAll?.(); } catch {}
    try { canvas.renderAll?.(); } catch {}
    emitRenderEngineEvent({
      type: 'error',
      action: 'view.activate',
      engine: 'fabric',
      viewId,
      payload: err,
    });
  }
}

/**
 * Switch to another view:
 *  - Save the current view (based on `currentViewId` or the store `activeViewId`)
 *  - Update `currentViewId`
 *  - Update `state.activeViewId` via `useAsukaStore.setState` to keep the UI reactive
 *  - Load the target view snapshot (or an empty canvas)
 */

export function applyCanvasEditability(designer: DesignerInstance | null, perms: AsukaPermissions): void {
  const canvas = getCanvasFromDesigner(designer);
  if (!canvas) return;

  const canEdit =
    (perms as any).canAddRect !== false ||
    (perms as any).canAddText !== false ||
    (perms as any).canAddImage !== false ||
    (perms as any).canAddShape !== false ||
    (perms as any).canDeleteObject !== false ||
    (perms as any).canDuplicateObject !== false ||
    (perms as any).canMoveObject !== false ||
    (perms as any).canResizeObject !== false ||
    (perms as any).canRotateObject !== false ||
    (perms as any).canEditObjectColors !== false ||
    (perms as any).canEditObjectText !== false ||
    (perms as any).canEditObjectStudio !== false ||
    (perms as any).canClearCanvas !== false;

  try {
    canvas.selection = !!canEdit;
    canvas.defaultCursor = canEdit ? 'default' : 'default';
  } catch {}

  try {
    const objects = canvas.getObjects ? canvas.getObjects() : [];
    objects.forEach((obj: any) => {
      const role = (obj as any)?._asukaRole;
      // Runtime helpers own their interaction contract. Applying end-user
      // permissions to them can turn overlays into invisible pointer targets.
      if (isRuntimeHelperRole(role)) return;
      if (isFabricObjectLayerLocked(obj)) {
        lockFabricLayerInteraction(obj);
        try { obj.setCoords?.(); } catch {}
        return;
      }
      if (isFabricObjectInteractionLocked(obj)) {
        lockFabricObjectInteraction(obj);
        try { obj.setCoords?.(); } catch {}
        return;
      }
      const isElementInstance = isFabricElementObject(obj);
      if (isElementInstance) {
        // Element instances keep selectable and evented independent.
        configureFabricElementInteraction(obj, {
          selectable: Boolean((obj as any).selectable === true),
          evented: Boolean((obj as any).evented !== false),
        });
        try { obj.setCoords?.(); } catch {}
        return;
      }
      const canSelect = !!((perms as any).canMoveObject || (perms as any).canResizeObject || (perms as any).canRotateObject || (perms as any).canDeleteObject || (perms as any).canDuplicateObject || (perms as any).canEditObjectColors || (perms as any).canEditObjectText || (perms as any).canEditObjectStudio);
      obj.selectable = !!canSelect;
      obj.hasControls = !!((perms as any).canResizeObject || (perms as any).canRotateObject);
      obj.hasBorders = !!canSelect;
      obj.lockMovementX = !((perms as any).canMoveObject !== false);
      obj.lockMovementY = !((perms as any).canMoveObject !== false);
      obj.lockScalingX = !((perms as any).canResizeObject !== false);
      obj.lockScalingY = !((perms as any).canResizeObject !== false);
      obj.lockRotation = !((perms as any).canRotateObject !== false);
      // keep evented true so hover tools (like magnifier) still work
      obj.evented = true;
    });
    canvas.requestRenderAll?.();
  } catch {}
}

export async function switchViewWithCanvas(
  designer: DesignerInstance | null,
  targetViewId: string,
): Promise<void> {
  const initialState = useAsukaStore.getState() as any;
  const previousActiveViewId: string | null = initialState.activeViewId ?? null;
  const previousActiveKind = getActiveViewEngineKind(initialState.views ?? [], previousActiveViewId, initialState.defaultRenderEngineKind ?? 'fabric');
  const renderedFabricViewId = currentViewId || (previousActiveKind !== '3d' ? previousActiveViewId : null);
  const saveFromViewId = renderedFabricViewId;

  debugLog('switchViewWithCanvas called', {
    previousActiveViewId,
    previousActiveKind,
    renderedFabricViewId,
    currentViewId,
    to: targetViewId,
    hasDesigner: !!designer,
  });
  asukaHistoryDebug('viewSnapshots.switch.start', {
    previousActiveViewId,
    previousActiveKind,
    renderedFabricViewId,
    targetViewId,
    currentHistoryInfo: summarizeHistoryInfo(initialState?.documentHistoryInfo),
    currentFabricTrack: summarizeHistoryTrack(saveFromViewId ? initialState?.documentFabricHistoryByView?.[saveFromViewId] : null),
    targetFabricTrack: summarizeHistoryTrack(targetViewId ? initialState?.documentFabricHistoryByView?.[targetViewId] : null),
    target3dTrack: summarizeHistoryTrack(targetViewId ? initialState?.document3dHistoryByView?.[targetViewId] : null),
  });

  emitBefore('views.switch', { from: previousActiveViewId, to: targetViewId, renderedFrom: renderedFabricViewId });

  if (!targetViewId) {
    debugLog('switchViewWithCanvas aborted: no targetViewId');
    return;
  }

  // 1) Save the Fabric view that is actually rendered on the Fabric canvas.
  // Do not save using the previous active view blindly: when leaving a 3D view,
  // the previous active view is 3D, while the hidden Fabric canvas still contains
  // the last rendered Fabric view. Saving that canvas under the 3D view corrupts
  // per-view history and makes the History module look reset/mismatched.
  if (!isStateRestoring && designer && saveFromViewId && saveFromViewId !== targetViewId) {
    debugLog('switchViewWithCanvas -> saving rendered Fabric view', saveFromViewId);
    saveSnapshotForView(designer, saveFromViewId);
  } else {
    debugLog(
      'switchViewWithCanvas -> skip save (no rendered Fabric view, no designer, same view, or restoring)',
    );
  }

  // 2) Update the store with the target view, target engine and the target view
  // history track in one atomic state update. The old code only forced
  // activeViewId through raw setState, bypassing activeRenderEngineKind and the
  // per-view documentHistoryInfo. That is the real reason History looked reset
  // immediately after changing views.
  const targetKind = setActiveViewStateFromSwitch(targetViewId);
  asukaHistoryDebug('viewSnapshots.switch.afterScopedState', {
    targetViewId,
    targetKind,
    documentHistoryInfo: summarizeHistoryInfo((useAsukaStore.getState() as any)?.documentHistoryInfo),
  });

  // 3) 3D views are rendered by the Babylon engine. Never load a Fabric snapshot
  // for a 3D target view and never mark the Fabric canvas currentViewId as a 3D
  // view; doing so causes the next switch to save Fabric JSON under the 3D id.
  if (targetKind === '3d') {
    debugLog('switchViewWithCanvas -> target is 3D, skip Fabric snapshot load');
    emitAfter('views.switch', { from: previousActiveViewId, to: targetViewId, renderedFrom: renderedFabricViewId, engine: targetKind });
    return;
  }

  // 4) Update currentViewId (the Fabric view effectively rendered on canvas)
  currentViewId = targetViewId;

  // 5) Load the target Fabric view.
  // Capture the rendered source frame before setDocumentSize()/resizeToHost().
  // The loader below reuses this cover, including when source and target views
  // have different intrinsic sizes.
  try {
    if (viewSnapshots[String(targetViewId)]) {
      createCanvasLoadCover(getCanvasFromDesigner(designer));
    }
  } catch {}
  const state = useAsukaStore.getState() as any;
  try {
    const views = (state.views ?? []) as any[];
    const v = Array.isArray(views) ? views.find((x) => x?.id === targetViewId) : null;
    const w = Math.max(1, Math.floor(Number(v?.width ?? 800) || 800));
    const h = Math.max(1, Math.floor(Number(v?.height ?? 600) || 600));

    try {
      try { (designer as any)?.setDocumentSize?.(w, h); } catch {}
      try { (designer as any)?.resizeToHost?.(); } catch {}
    } catch {}

    try {
      const unit = (v?.unit as any) || 'px';
      const dpi = Math.max(10, Math.floor(Number(v?.dpi ?? 96) || 96));
      (state.setRulerUnit as any)?.(unit);
      (state.setRulerDpi as any)?.(dpi);
    } catch {}
  } catch {}

  try {
    const storedFabricTrack = (useAsukaStore.getState() as any)?.documentFabricHistoryByView?.[DOCUMENT_FABRIC_HISTORY_KEY] ?? (useAsukaStore.getState() as any)?.documentFabricHistoryByView?.[targetViewId] ?? null;
    if (storedFabricTrack && typeof (designer as any)?.importHistoryTrack === 'function') {
      asukaHistoryDebug('viewSnapshots.switch.importTargetFabricTrack.beforeLoad', {
        targetViewId,
        storedFabricTrack: summarizeHistoryTrack(storedFabricTrack),
      });
      (designer as any).importHistoryTrack(targetViewId, storedFabricTrack);
    }
  } catch (error) {
    asukaHistoryDebug('viewSnapshots.switch.importTargetFabricTrack.error', { targetViewId, error: String((error as any)?.message ?? error) });
  }

  await loadSnapshotForView(designer, targetViewId);

  // 5b) Apply per-view permissions to the canvas (readonly/editable)
  try {
    const s = useAsukaStore.getState() as any;
    const effective = getEffectivePermissions(
      s.permissions as AsukaPermissions,
      (s.permissionsByView?.[targetViewId] ?? null) as any,
    );
    applyCanvasEditability(designer, effective);
  } catch {}

  // 6) Log the resulting store state
  const stateAfter = useAsukaStore.getState() as any;
  debugLog('switchViewWithCanvas end state', {
    activeViewIdAfter: stateAfter.activeViewId ?? null,
    activeRenderEngineKindAfter: stateAfter.activeRenderEngineKind ?? null,
    currentViewId,
  });
  asukaHistoryDebug('viewSnapshots.switch.end', {
    targetViewId,
    targetKind,
    activeViewIdAfter: stateAfter.activeViewId ?? null,
    activeRenderEngineKindAfter: stateAfter.activeRenderEngineKind ?? null,
    documentHistoryInfo: summarizeHistoryInfo(stateAfter.documentHistoryInfo),
    targetFabricTrack: summarizeHistoryTrack(stateAfter.documentFabricHistoryByView?.[targetViewId]),
  });

  emitAfter('views.switch', { from: previousActiveViewId, to: targetViewId, renderedFrom: renderedFabricViewId, engine: targetKind });
}

/**
 * Harmonize colors across all existing snapshots using a given palette
 * (hex/rgb colors and URL-based patterns are both supported).
 */
export async function harmonizeSnapshotsAcrossDocument(
  palette: PrintingColor[],
): Promise<void> {
  debugLog('harmonizeSnapshotsAcrossDocument called', {
    paletteSize: palette?.length ?? 0,
  });

  if (!palette || palette.length === 0) return;

  const ids = Object.keys(viewSnapshots);
  debugLog('harmonizeSnapshotsAcrossDocument view ids', ids);
  if (ids.length === 0) return;

  const harmonizeObjects = async (objects: any[]): Promise<void> => {
    for (const obj of objects) {
      if (!obj) continue;

      if (Array.isArray(obj.objects)) {
        await harmonizeObjects(obj.objects);
      }

      const fillVal = obj.fill;
      if (typeof fillVal === 'string') {
        const similar = await getSimilarColorAsync(fillVal, palette);
        if (similar) {
          obj.fill = similar.value;
        }
      }
    }
  };

  for (const viewId of ids) {
    const snapshot = viewSnapshots[viewId];
    if (!snapshot || !Array.isArray((snapshot as any).objects)) continue;
    debugLog('harmonizeSnapshotsAcrossDocument -> harmonizing view', viewId);
    await harmonizeObjects((snapshot as any).objects);
  }
}
