import { create } from 'zustand';

import {
  DEFAULT_PERMISSIONS,
  canOpenModuleId,
  getEffectivePermissions,
  mergePermissions,
  permissionsForRole,
  type AsukaRole,
  type AsukaPermissions,
} from '../core/permissions';

import type { PrintingMethod, ColorScope } from '../core/printing';
import type { ModuleId } from '../types/moduleIds';
import type { LayoutZoneId } from '../types/layout';
import type { DesignerInstance } from '../core/designer';
import { ensureModuleAvailable, getModule, notifyModuleRegistryChanged, type ModuleDisplayMode } from '../core/modules';
import { emitAsukaEvent, emitAsukaEventCancelable } from '../core/events';
import { removeElementInstancesFromSnapshots, removeFixedSlotObjectsFromSnapshots, ensureEmptySnapshotForView, switchViewWithCanvas } from '../core/viewSnapshots';

import type { AsukaElement, ElementVariation } from '../types/elements';
import type { AsukaElementVariationApplyInput } from '../core/elementVariationSync';
import type { AsukaCameraView } from '../types/camera';
import { setVariationCamera } from '../core/camera';
import type { AsukaCustomFontConfig, AsukaGoogleFontConfig, AsukaRegisteredFont, AsukaSystemFontConfig, AsukaUploadModuleConfig, AsukaUploadStrategy } from '../types/options';
import { dedupeFonts, getDefaultSystemFonts, normalizeCustomFont, normalizeGoogleFont, normalizeSystemFont } from '../core/fonts';
import { getVanillaUiHelpers } from '../core/moduleUi';
import { DEFAULT_LICENSE, DEFAULT_LICENSE_RESOLUTION } from '../core/license/license.defaults';
import { canUseFeature } from '../core/license/feature-gates';
import type { AsukaLicenseEntitlements, AsukaLicenseFeature, AsukaLicenseResolutionResult, AsukaLicenseTokenInput } from '../core/license/license.types';
import { DEFAULT_RESPONSIVE_CONFIG, getResponsiveBreakpoint, normalizeResponsiveConfig, type AsukaResponsiveBreakpoint, type AsukaResponsiveConfig, type NormalizedAsukaResponsiveConfig } from '../core/responsive';
import { DEFAULT_ASUKA_UI_OPTIONS, normalizeAsukaUiOptions, type AsukaUiOptions, type NormalizedAsukaUiOptions } from '../core/uiOptions';
import { normalizePriceInput, type AsukaPriceInput } from '../core/pricingValue';
import { asuka2DPerfDebug, asukaHistoryDebug, getAsukaDebugPayloadSize, summarizeHistoryInfo, summarizeHistoryTrack } from '../core/historyDebug';
import type { AsukaPhysicalSize } from '../core/physicalMeasurement';
import { getActiveViewEngineKind, normalizeRenderEngineCapabilityList, normalizeRenderEngineKind, setCurrentRenderEngineKind, type Asuka3DHistoryTrackState, type Asuka3DSceneState, type AsukaRenderEngineCapabilityKey, type AsukaRenderEngineKind, type AsukaRenderEngineOptionsByKind } from '../core/renderEngine';
import { get3DHistoryAssetsSignature } from '../core/history3d';
import { logAsuka3DPerf } from '../core/perf3dDebug';
import { ensureProduct3DPartGroups } from '../core/product3dParts';
import { resolveModuleLayoutPlacement } from '../core/modulePlacement';
import { setRuntimeStoreBridge } from '../core/runtimeStoreBridge';



// Monotonic token used to prevent stale async openModule calls from reopening
// an overlay after it has already been closed. This can happen with lazy modules:
// openModule(id) starts loading, the module closes, then the original promise resolves
// and would otherwise set activeModuleId again with empty module state.
const DEFAULT_HISTORY_MAX_SNAPSHOTS = 8;
const DOCUMENT_FABRIC_HISTORY_KEY = '__document__';


const isHeavy3DStoreDataUrl = (value: unknown): boolean => {
  const raw = String(value ?? '').trim();
  if (!raw) return false;
  if (/^data:(?:model\/|application\/(?:octet-stream|gltf|glb|x-binary))/i.test(raw)) return true;
  return /^data:/i.test(raw) && raw.length > 524_288;
};

const compact3DModelForStoreHistory = (model: any): any | undefined => {
  if (!model || typeof model !== 'object') return undefined;
  const url = String(model?.url ?? '').trim();
  if (!url) return undefined;
  // History snapshots remain valid canonical scenes. Heavy model payloads live
  // once in the track-level assets registry and are omitted from snapshots.
  if (isHeavy3DStoreDataUrl(url) || url.length > 4096) return undefined;
  return { ...(model ?? {}) };
};

const stripHeavy3DModelsFromHistorySnapshot = (snapshot: any): any => {
  if (!snapshot || typeof snapshot !== 'object') return snapshot;
  const next: any = { ...(snapshot as any) };
  if (Array.isArray(next.products)) {
    next.products = next.products.map((instance: any) => {
      if (!instance || typeof instance !== 'object') return instance;
      const out: any = { ...(instance as any) };
      if (out.model) {
        const compactModel = compact3DModelForStoreHistory(out.model);
        if (compactModel) out.model = compactModel;
        else delete out.model;
      }
      return out;
    });
  }
  return next;
};

const compactElementForStoreFabricHistory = (element: any): any => {
  if (!element || typeof element !== 'object') return element;
  const isProduct3D = String(element?.type ?? '').trim() === 'product-3d' || !!element?.product3d;
  if (!isProduct3D) return element;
  const compactConfig = (config: any): any => {
    if (!config || typeof config !== 'object') return config;
    const out: any = { ...(config as any) };
    // Catalog elements are not 3D scene snapshots: keep their canonical model
    // descriptor so the product can still be added after a Fabric undo/redo.
    return out;
  };
  const walkVariation = (variation: any): any => {
    if (!variation || typeof variation !== 'object') return variation;
    const out: any = { ...(variation as any) };
    if (out.product3d) out.product3d = compactConfig(out.product3d);
    if (typeof out.image === 'string' && (isHeavy3DStoreDataUrl(out.image) || out.image.length > 524_288)) delete out.image;
    if (typeof out.icon === 'string' && (isHeavy3DStoreDataUrl(out.icon) || out.icon.length > 524_288)) delete out.icon;
    if (Array.isArray(out.variations)) out.variations = out.variations.map(walkVariation);
    return out;
  };
  const out: any = { ...(element as any) };
  if (out.product3d) out.product3d = compactConfig(out.product3d);
  if (Array.isArray(out.variations)) out.variations = out.variations.map(walkVariation);
  return out;
};

const compactElementsForStoreFabricHistory = (elements: any): any[] => (Array.isArray(elements) ? elements : []).map(compactElementForStoreFabricHistory);


const compact3DSceneForStore = (scene: any): any => {
  if (!scene || typeof scene !== 'object') return scene;
  return {
    ...(scene as any),
    products: Array.isArray(scene.products)
      ? scene.products.map((product: any) => product && typeof product === 'object' ? { ...product } : product)
      : [],
  };
};


const strip3DScenesFromFabricHistorySnapshot = (snapshot: any): any => {
  if (!snapshot || typeof snapshot !== 'object') return snapshot;
  const next = { ...(snapshot as any) };
  // Fabric history tracks 2D document changes across views. 3D scenes/history are
  // persisted separately; keeping them in every Fabric snapshot duplicates model
  // payloads and can exhaust browser memory when switching from 3D to 2D.
  try { delete next.scenes3dByView; } catch {}
  try { delete next.document3dScenesByView; } catch {}
  try { delete next.history3dByView; } catch {}
  try { delete next.document3dHistoryByView; } catch {}
  try { delete next.historyFabricByView; } catch {}
  try { delete next.documentFabricHistoryByView; } catch {}
  if (Array.isArray((next as any).elements)) {
    try { (next as any).elements = compactElementsForStoreFabricHistory((next as any).elements); } catch {}
  }
  return next;
};

const trimHistorySnapshots = (snapshotsInput: any[], indexInput: any, maxSnapshots = DEFAULT_HISTORY_MAX_SNAPSHOTS) => {
  const snapshots = (Array.isArray(snapshotsInput) ? snapshotsInput : []).filter((snapshot: any) => snapshot && typeof snapshot === 'object');
  const max = Math.max(2, Number(maxSnapshots) || DEFAULT_HISTORY_MAX_SNAPSHOTS);
  const safeIndex = Math.max(0, Math.min(Math.max(0, snapshots.length - 1), Number(indexInput ?? snapshots.length - 1) || 0));
  if (snapshots.length <= max) return { snapshots, index: snapshots.length ? safeIndex : -1 };
  const tailStart = Math.max(1, snapshots.length - (max - 1));
  const trimmed = [snapshots[0], ...snapshots.slice(tailStart)];
  const index = safeIndex === 0 ? 0 : (safeIndex >= tailStart ? 1 + (safeIndex - tailStart) : 0);
  return { snapshots: trimmed, index: Math.max(0, Math.min(trimmed.length - 1, index)) };
};

let moduleOpenRequestSeq = 0;

const invalidatePendingModuleOpen = () => {
  moduleOpenRequestSeq += 1;
};

const buildModuleOpenState = (state: any, id: ModuleId, resolved: any): any => {
  const prevId =
    (state.moduleOverlayStack && state.moduleOverlayStack.length > 0
      ? state.moduleOverlayStack[state.moduleOverlayStack.length - 1]
      : state.activeModuleId) ?? null;

  let nextHidden = state.hiddenModules;
  let nextExiting = state.exitingModules;
  if (prevId && prevId !== id) {
    nextExiting = { ...(state.exitingModules ?? {}), [prevId]: true } as any;
    const next = { ...(nextHidden ?? {}) } as any;
    delete next[prevId];
    nextHidden = next;
  }

  const nextType = (resolved?.displayMode ?? 'floating') as any;
  const mounted = resolved?.keepMounted ? { ...(state.mountedModules ?? {}), [id]: true } : state.mountedModules;
  const hidden = resolved?.keepMounted
    ? (() => {
        const next = { ...(nextHidden ?? {}) } as any;
        delete next[id];
        return next;
      })()
    : nextHidden;

  return {
    activeModuleId: id,
    moduleOverlayStack: [id],
    moduleOverlayType: nextType,
    mountedModules: mounted,
    hiddenModules: hidden,
    exitingModules: nextExiting,
  } as any;
};


const makeUploadedAssetId = () => `upload-${Date.now()}-${Math.random().toString(16).slice(2)}`;

function isBlobUrl(value: any): boolean {
  return typeof value === 'string' && /^blob:/i.test(value);
}

function revokeUploadPreviewUrl(value: any) {
  if (!isBlobUrl(value)) return;
  try { URL.revokeObjectURL(value); } catch {}
}

function getUploadFileSignature(file: any): string {
  if (!file) return '';
  const name = String(file?.name ?? '').trim();
  const size = Number(file?.size ?? 0) || 0;
  const lastModified = Number(file?.lastModified ?? 0) || 0;
  const type = String(file?.type ?? '').trim();
  return [name, size, lastModified, type].join('::');
}

function normalizeUploadSource(value: any): string | undefined {
  const normalized = String(value ?? '').trim();
  return normalized || undefined;
}

function normalizePriceField<T extends Record<string, any>>(item: T): T {
  if (!item || typeof item !== 'object') return item;
  const normalized = normalizePriceInput((item as any).price);
  const next: any = { ...item };
  if (typeof normalized === 'number') next.price = normalized;
  else if ('price' in next) delete next.price;
  return next as T;
}

function normalizeElementVariationPrices(variation: any): any {
  if (!variation || typeof variation !== 'object') return variation;
  const next: any = normalizePriceField(variation);
  if (Array.isArray(next.variations)) next.variations = next.variations.map(normalizeElementVariationPrices);
  return next;
}

function normalizeElementPrices(element: any): any {
  if (!element || typeof element !== 'object') return element;
  const next: any = normalizePriceField(element);
  if (Array.isArray(next.variations)) next.variations = next.variations.map(normalizeElementVariationPrices);
  if (Array.isArray(next.children)) next.children = next.children.map(normalizeElementPrices);
  return ensureProduct3DPartGroups(next);
}

function normalizeModuleConfigPatch(patch: any): any {
  if (!patch || typeof patch !== 'object') return patch;
  const next: any = { ...patch };
  if (next.designs && typeof next.designs === 'object' && Array.isArray(next.designs.items)) {
    next.designs = {
      ...next.designs,
      items: next.designs.items.map((item: any) => normalizePriceField(item)),
    };
  }
  return next;
}

function getSerializedObjectId(obj: any): string {
  return String(obj?._asukaObjectId ?? obj?.id ?? obj?.uuid ?? '').trim();
}

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

    const objectId = getSerializedObjectId(obj);
    if (objectIds && objectId && objectIds.has(objectId)) return true;

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

function collectElementDeleteIdentifiers(elements: AsukaElement[], identifier: string): Set<string> {
  const wanted = String(identifier ?? '').trim();
  const identifiers = new Set<string>();
  if (wanted) identifiers.add(wanted);

  const add = (value: any) => {
    const normalized = String(value ?? '').trim();
    if (normalized) identifiers.add(normalized);
  };

  const walk = (list: AsukaElement[]) => {
    for (const el of Array.isArray(list) ? list : []) {
      const id = String((el as any)?.id ?? '').trim();
      const name = String((el as any)?.name ?? '').trim();
      const familyId = String((el as any)?.familyId ?? '').trim();
      if (wanted && (id === wanted || name === wanted || familyId === wanted)) {
        add(id);
        add(name);
        add(familyId);
      }
      walk(Array.isArray((el as any)?.children) ? (el as any).children : []);
    }
  };

  walk(elements);
  return identifiers;
}

function isElementCatalogEntryMatched(el: any, identifiers: Set<string>): boolean {
  if (!el || !identifiers?.size) return false;
  const id = String(el?.id ?? '').trim();
  const name = String(el?.name ?? '').trim();
  const familyId = String(el?.familyId ?? '').trim();
  return (!!id && identifiers.has(id)) || (!!name && identifiers.has(name)) || (!!familyId && identifiers.has(familyId));
}

function isElementCatalogEntryStillUsed(el: any, byView: Record<string, any>): boolean {
  if (!el || !byView || typeof byView !== 'object') return false;
  const identifiers = new Set<string>();
  const add = (value: any) => {
    const normalized = String(value ?? '').trim();
    if (normalized) identifiers.add(normalized);
  };
  add(el?.id);
  add(el?.name);
  add(el?.familyId);
  if (!identifiers.size) return false;

  for (const objects of Object.values(byView)) {
    const list = Array.isArray(objects) ? objects : [];
    if (list.some((o: any) => objectMatchesElement(o, identifiers))) return true;
  }
  return false;
}

function removeUnusedElementsFromCatalog(elements: AsukaElement[], identifiers: Set<string>, byViewAfterDelete: Record<string, any>): AsukaElement[] {
  return (Array.isArray(elements) ? elements : [])
    .filter((el: any) => !isElementCatalogEntryMatched(el, identifiers) || isElementCatalogEntryStillUsed(el, byViewAfterDelete))
    .map((el: any) => {
      const children = Array.isArray(el?.children) ? removeUnusedElementsFromCatalog(el.children, identifiers, byViewAfterDelete) : el?.children;
      return children === el?.children ? el : { ...el, children };
    });
}

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

    const objectId = getSerializedObjectId(obj);
    if (objectIds && objectId && objectIds.has(objectId)) return true;

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

/**
 * Represents a draggable menu block.
 */
export interface LayoutBlock {
  id: string;
  kind: ModuleId;
  label: string;
  icon: string;
  order: number;

  /** Responsive priority. Lower values stay visible first on compact mobile navigation. */
  priority?: number;

  /** Optional display mode override for this block (floating/modal/drawer/panel...). */
  type?: string;

  /** Optional menu-level engine filter. When omitted, the module definition controls compatibility. */
  supportedEngines?: AsukaRenderEngineKind[];

  /** Optional menu-level capability filter. When omitted, the module definition controls capability compatibility. */
  requiredCapabilities?: AsukaRenderEngineCapabilityKey[];

  /** Optional DOM selector used as the portal/mount target for this block's module overlay. */
  mountTarget?: string;

  /** Optional collapsed state (used for inline zones to collapse individual panels). */
  collapsed?: boolean;

  /** Optional render mode override for this block. */
  renderMode?: 'inline' | 'portal';
}

/**
 * Vues (front, back, etc.).
 * For now we only keep an id + a label.
 * Later we can attach the canvas state for each view.
 */
export interface AsukaView {
  id: string;
  name: string;

  /** Virtual document size for this view (world units / px). */
  width: number;
  height: number;

  /** Pixels-per-inch used when the user enters sizes in cm/mm/in. */
  dpi: number;

  /** Last unit used in the UI for this view. */
  unit: 'px' | 'cm' | 'mm' | 'in';

  /** Real-world extent represented by this view when no narrower drawing-area calibration exists. */
  physicalSize?: AsukaPhysicalSize;

  /** Rendering engine for this view. Defaults to the global Fabric engine. */
  engine?: AsukaRenderEngineKind;

  /** Initial or persisted 3D scene state for 3D views. */
  scene3d?: Asuka3DSceneState;

  /** Convenience model URL for 3D views. */
}

/**
 * Slice layout.
 */
export interface LayoutState {
  permissions: AsukaPermissions;

  /** Per-view permission overrides. */
  permissionsByView: Record<string, Partial<AsukaPermissions>>;
  setPermissionsForView: (viewId: string, patch: Partial<AsukaPermissions>) => void;
  clearPermissionsForView: (viewId: string) => void;

  /** Current role (dev/editor/viewer). */
  role: AsukaRole;
  setRole: (role: AsukaRole) => void;

  setPermissions: (patch: Partial<AsukaPermissions>) => void;

  blocksByZone: Record<LayoutZoneId, LayoutBlock[]>;

  /** Whether a zone is currently collapsed. */
  zoneCollapsed: Record<LayoutZoneId, boolean>;

  /** Whether a zone can be collapsed. */
  zoneCollapsible: Record<LayoutZoneId, boolean>;

  /**
   * Display style of each layout zone.
   * - 'default': current sidebar style (expanded list + header)
   * - 'inline': compact icon bar (Figma-like)
   */
  zoneType: Record<LayoutZoneId, 'default' | 'panels' | 'inline'>;

  /** Update the collapsed state of a zone. */
  setZoneCollapsed: (zone: LayoutZoneId, value: boolean) => void;

  /** Toggle the collapsed state of a zone. */
  toggleZoneCollapsed: (zone: LayoutZoneId) => void;

  /** Update the type of a zone (default / panels / inline). */
  setZoneType: (zone: LayoutZoneId, type: 'default' | 'panels' | 'inline') => void;

  /** Update whether a zone can be collapsed. */
  setZoneCollapsible: (zone: LayoutZoneId, value: boolean) => void;

  /** Move a menu block to another zone / position. */
  moveBlock: (blockId: string, targetZone: LayoutZoneId, targetIndex?: number) => void;

  /** Update the collapsed state of a block (mainly used in inline mode). */
  setBlockCollapsed: (blockId: string, value: boolean) => void;

  /** Toggle the collapsed state of a block (mainly used in inline mode). */
  toggleBlockCollapsed: (blockId: string) => void;

  /** Update whether a block can be collapsed. */
  setBlockCollapsible: (blockId: string, value: boolean) => void;

  /** Id du bloc actuellement en cours de drag & drop. */
  draggedBlockId: string | null;
  startDraggingBlock: (blockId: string) => void;
  endDraggingBlock: () => void;

  /** Initialize the layout from the configuration passed to createAsukaDesigner. */
  initializeLayoutFromConfig: (config: any) => void;
}

/** Global UI behavior configuration. */
export interface UiConfigState {
  uiConfig: NormalizedAsukaUiOptions;
  setUiConfig: (config: AsukaUiOptions | null | undefined) => void;
}

/** Responsive layout metrics and configuration. */
export interface ResponsiveState {
  responsiveConfig: NormalizedAsukaResponsiveConfig;
  responsiveBreakpoint: AsukaResponsiveBreakpoint;
  responsiveWidth: number;
  setResponsiveConfig: (config: AsukaResponsiveConfig | null | undefined) => void;
  setResponsiveMetrics: (width: number) => void;
}

/**
 * Slice modules.
 */
export interface ModuleState {
  activeModuleId: ModuleId | null;

  /** Runtime overrides for module display mode (e.g. panel-right, modal...). */
  moduleDisplayModeOverrides: Partial<Record<ModuleId, ModuleDisplayMode>>;
  setModuleDisplayModeOverride: (id: ModuleId, mode: ModuleDisplayMode | null) => void;

  /** Runtime overrides for module mount target (CSS selector). */
  moduleMountTargetOverrides: Partial<Record<ModuleId, string>>;
  setModuleMountTargetOverride: (id: ModuleId, target: string | null) => void;

  /**
   * Modules currently mounted in the DOM.
   * Used to support `keepMounted`.
   */
  mountedModules: Partial<Record<ModuleId, true>>;

  /**
   * Visibility flag for mounted modules.
   * - true  => the module is mounted but hidden
   * - false => mounted and visible
   */
  hiddenModules: Partial<Record<ModuleId, true>>;

  /** Modules currently playing an exit animation. */
  exitingModules: Partial<Record<ModuleId, true>>;

  /** Called once an exit animation has finished. */
  finalizeModuleExit: (id: ModuleId) => void;

  openModule: (id: ModuleId) => void;
  closeModule: () => void;
  openMenu: (id: ModuleId) => void;
  closeMenu: () => void;
  toggleMenu: (id: ModuleId) => void;

  /**
   * Internal navigation (F8) for module overlays.
   */
  moduleOverlayStack: ModuleId[];
  moduleOverlayType: 'floating' | 'modal' | 'drawer' | 'panel-left' | 'panel-right' | 'panel-bottom' | 'panel-top' | 'popover';
  moduleOverlayAnchorBox: { left: number; top: number; width: number; height: number } | null;
  pendingRestoreSelectionMenu: boolean;

  pushModuleOverlay: (id: ModuleId) => void;
  backModuleOverlay: () => void;
  setModuleOverlayAnchorBox: (
    box: { left: number; top: number; width: number; height: number } | null,
  ) => void;
  setPendingRestoreSelectionMenu: (value: boolean) => void;

  /**
   * When true, the Views module should open its "Create view" form on mount.
   * Used by the empty-state CTA (no views) to reuse the existing add-view form.
   */
  pendingOpenCreateViewModal: boolean;
  requestOpenCreateViewModal: () => void;
  clearPendingOpenCreateViewModal: () => void;
}

export interface ModuleConfig {
  upload: AsukaUploadModuleConfig;

  /** Static designs catalog (SVGs or images) for the Designs module. */
  designs?: {
    items: Array<{
      url: string;
      name: string;
      price?: AsukaPriceInput;
      category?: string;
    }>;
  };

  /** Optional Shapes presets available at boot. */
  shapes?: {
    svgAssets?: Array<{
      id?: string;
      name: string;
      svgText: string;
    }>;
    style?: {
      fill?: string;
      stroke?: string;
      strokeWidth?: number;
      opacity?: number;
    };
    editDefaults?: {
      rectRadius?: number;
      starSpikes?: number;
      starInnerRatio?: number;
    };
  };

  modelOptimizer?: {
    endpoint?: string;
    headers?: Record<string, string>;
    credentials?: RequestCredentials;
    maxFileSizeMb?: number;
  };
}


export type UploadedAssetStatus = 'ready' | 'uploading' | 'error';

export interface UploadedAsset {
  id: string;
  name: string;
  url: string;
  previewUrl?: string;
  mimeType?: string;
  size?: number;
  lastModified?: number;
  fileSignature?: string;
  file?: File;
  createdAt: number;
  updatedAt: number;
  source?: string;
  mode?: string;
  strategy?: AsukaUploadStrategy | string;
  status: UploadedAssetStatus;
  error?: string;
  metadata?: unknown;
}

export interface RegisterUploadedAssetInput {
  id?: string;
  file?: File;
  url?: string;
  previewUrl?: string;
  name?: string;
  mimeType?: string;
  size?: number;
  lastModified?: number;
  source?: string;
  mode?: string;
  strategy?: AsukaUploadStrategy | string;
  status?: UploadedAssetStatus;
  error?: string;
  metadata?: unknown;
}

export interface UploadedAssetsState {
  uploadedAssets: UploadedAsset[];
  registerUploadedAsset: (input: RegisterUploadedAssetInput) => UploadedAsset;
  updateUploadedAsset: (id: string, patch: Partial<UploadedAsset>) => void;
  removeUploadedAsset: (id: string) => void;
  clearUploadedAssets: () => void;
}

export interface ModuleConfigState {
  moduleConfig: ModuleConfig;
  setModuleConfig: (patch: Partial<ModuleConfig>) => void;
}

export interface IntegrationsConfig {
  pixabay?: {
    apiKey?: string;
  };
  pexels?: {
    apiKey?: string;
  };
  facebook?: {
    appId?: string;
    apiVersion?: string;
    scopes?: string;
  };
}

export interface IntegrationsState {
  integrations: IntegrationsConfig;
  setIntegrations: (patch: Partial<IntegrationsConfig>) => void;
}

export interface LicenseState {
  license: AsukaLicenseEntitlements;
  licenseResolution: AsukaLicenseResolutionResult;
  licenseToken: AsukaLicenseTokenInput | null;
  setLicenseState: (patch: {
    license?: AsukaLicenseEntitlements;
    resolution?: AsukaLicenseResolutionResult;
    token?: AsukaLicenseTokenInput | null;
  }) => void;
  canUseLicenseFeature: (feature: AsukaLicenseFeature | string) => boolean;
}

export interface DesignerState {
  /** @internal Native Fabric runtime retained only by the internal store. */
  designerInstance: DesignerInstance | null;
  /** @internal Native Fabric runtime setter retained only by the internal store. */
  setDesignerInstance: (instance: DesignerInstance | null) => void;
  defaultRenderEngineKind: AsukaRenderEngineKind;
  activeRenderEngineKind: AsukaRenderEngineKind;
  renderEngineOptionsByKind: AsukaRenderEngineOptionsByKind;
  setRenderEngineOptionsByKind: (options: AsukaRenderEngineOptionsByKind) => void;
  setDefaultRenderEngineKind: (kind: AsukaRenderEngineKind) => void;
  setActiveRenderEngineKind: (kind: AsukaRenderEngineKind) => void;
}

export interface AiRuntimeState {
  aiRuntime: any | null;
  setAiRuntime: (runtime: any | null) => void;
}

export interface ExportRuntimeState {
  exportRuntime: any | null;
  setExportRuntime: (runtime: any | null) => void;
}

/**
 * Slice vues.
 */
export interface ViewState {
  views: AsukaView[];
  activeViewId: string | null;
  addView: (payload?: string | Partial<Pick<AsukaView, 'name' | 'width' | 'height' | 'dpi' | 'unit' | 'physicalSize' | 'engine' | 'scene3d'>>) => string;
  updateView: (id: string, patch: Partial<Pick<AsukaView, 'name' | 'width' | 'height' | 'dpi' | 'unit' | 'physicalSize' | 'engine' | 'scene3d'>>) => void;
  setActiveView: (id: string) => void;
  removeView: (id: string) => void;

  /** Optional thumbnails per view (data URLs). Used by the pagination UI. */
  viewThumbnails: Record<string, string>;
  setViewThumbnail: (viewId: string, dataUrl: string) => void;
}


export interface DocumentDrawingAreaState {
  id: string;
  x: number;
  y: number;
  width: number;
  height: number;
  shape?: string;
  sourceSvg?: string;
  sourcePath?: string;
  physicalSize?: AsukaPhysicalSize;
}

export interface DocumentViewportState {
  zoom: number;
  panX: number;
  panY: number;
}

export interface DocumentSelectionState {
  activeObjectIds: string[];
  activeDrawingAreaId: string | null;
}

export interface DocumentHistoryInfoState {
  index: number;
  length: number;
  canUndo: boolean;
  canRedo: boolean;
  maxSnapshots: number;
}

export interface DocumentState {
  documentViews: AsukaView[];
  documentDrawingAreas: DocumentDrawingAreaState[];
  documentViewportByView: Record<string, DocumentViewportState>;
  documentSelection: DocumentSelectionState;
  documentObjectsByView: Record<string, any[]>;
  document3dScenesByView: Record<string, Asuka3DSceneState>;
  document3dHistoryByView: Record<string, Asuka3DHistoryTrackState>;
  documentFabricHistoryByView: Record<string, Asuka3DHistoryTrackState>;
  documentHistoryInfo: DocumentHistoryInfoState;
  documentHistoryInfoByView: Record<string, DocumentHistoryInfoState>;
  setDocumentViews: (views: AsukaView[]) => void;
  setDocumentDrawingAreas: (zones: DocumentDrawingAreaState[]) => void;
  setDocumentViewportForView: (viewId: string, viewport: Partial<DocumentViewportState>) => void;
  setDocumentSelection: (selection: Partial<DocumentSelectionState>) => void;
  setDocumentObjectsForView: (viewId: string, objects: any[]) => void;
  setDocument3dSceneForView: (viewId: string, scene: Asuka3DSceneState | null | undefined) => void;
  setDocument3dCameraForView: (viewId: string, camera: Asuka3DSceneState['camera'] | null | undefined) => void;
  setDocument3dHistoryForView: (viewId: string, history: Asuka3DHistoryTrackState | null | undefined) => void;
  setDocumentFabricHistoryForView: (viewId: string, history: Asuka3DHistoryTrackState | null | undefined) => void;
  setDocumentHistoryInfo: (info: Partial<DocumentHistoryInfoState>) => void;
}

/**
 * State global Asuka.
 */
export type SelectionMenuType = 'floating' | 'popover' | 'panel-left' | 'panel-right' | 'panel-bottom' | 'panel-top' | 'modal' | 'drawer';

export interface SelectionMenuState {
  selectionMenuType: SelectionMenuType;
  selectionMenuRootType: SelectionMenuType;
  selectionMenuItems: ModuleId[];
  selectionMenuOpen: boolean;
  selectionMenuStack: ModuleId[];
  selectionMenuAnchorBox: { left: number; top: number; width: number; height: number } | null;

  setSelectionMenuItems: (items: ModuleId[]) => void;
  setSelectionMenuType: (type: SelectionMenuType) => void;
  setSelectionMenuAnchorBox: (box: { left: number; top: number; width: number; height: number } | null) => void;
  openSelectionMenu: () => void;
  closeSelectionMenu: () => void;
  selectionMenuGoTo: (id: ModuleId, options?: { type?: SelectionMenuType }) => void;
  selectionMenuBack: () => void;
  resetSelectionMenuNav: () => void;
}

export interface ColorScopeState {
  colorScope: ColorScope;
  setColorScope: (scope: ColorScope) => void;
}

// --- Printing methods / colors (global config) -----------------------------

export interface PrintingConfigState {
  printingMethods: PrintingMethod[];
  setPrintingMethods: (methods: PrintingMethod[]) => void;
}

export interface DefaultsState {
  defaults: {
    uiScaling: {
      keepControlsReadable: boolean,
      keepHelpersReadable: boolean,
      baseControlSize: number,
      baseTouchControlSize: number,
      minHelperStrokeWidth: number,
      maxCompensationMultiplier: number,
    },
    priceFormat: {
      code: string;
      symbol: string;
      priceFormat: string;
      decimalSeparator: string;
      thousandSeparator: string;
      precision: number;
      trimZeros: boolean;
    };
  };
  setDefaults: (patch: Partial<DefaultsState['defaults']>) => void;
}

export interface ColorsState {
  colors: Array<any>;
  setColors: (colors: Array<any>) => void;
}

export interface PricingState {
  price: number;
  setPrice: (price: number | string) => void;

  pricingRules: Array<any>;
  setPricingRules: (rules: Array<any>) => void;

  addPricingRule: (rule?: any) => string;
  updatePricingRule: (id: string, patch: any) => void;
  removePricingRule: (id: string) => void;
  duplicatePricingRule: (id: string) => string;
  movePricingRule: (id: string, delta: number) => void;
  togglePricingRule: (id: string) => void;

  pricingTotals: {
    base: number;
    standard: number;
    rules: number;
    total: number;
    updatedAt: number;
  };
  setPricingTotals: (totals: Partial<PricingState['pricingTotals']> & { updatedAt?: number }) => void;
}

export interface I18nState {
  i18nVersion: number;
  bumpI18nVersion: () => void;
}


export interface FontsState {
  fontsCatalog: AsukaRegisteredFont[];
  setFontsCatalog: (fonts: AsukaRegisteredFont[]) => void;
  addSystemFont: (payload: string | AsukaSystemFontConfig) => string;
  addGoogleFont: (payload: string | AsukaGoogleFontConfig) => string;
  addCustomFont: (payload: AsukaCustomFontConfig) => string;
  removeFont: (id: string) => void;
  markFontState: (id: string, patch: Partial<Pick<AsukaRegisteredFont, 'loaded' | 'loading' | 'error'>>) => void;
}

// --- Fixed slots (placeholders) ---

export type FixedSlotType = 'text' | 'image' | 'design';

export type FixedSlotConstraints = {
  lockX?: boolean;
  lockY?: boolean;
  lockRotation?: boolean;
  minWidth?: number;
  minHeight?: number;
  maxWidth?: number;
  maxHeight?: number;
  allowedColors?: string[];
  minChars?: number;
  maxChars?: number;
  maxLines?: number;
  allowedFonts?: string[];
};

export type FixedSlot = {
  id: string;
  type: FixedSlotType;
  x: number;
  y: number;
  constraints: FixedSlotConstraints;
  label?: string;
  /** Visual color used by the marker indicator on the canvas. */
  color?: string;
  /** Optional target width applied to the object that replaces this marker. */
  targetWidth?: number;
  /** Optional target height applied to the object that replaces this marker. */
  targetHeight?: number;
  /** Optional ordering priority. When true, the marker wins over bring-to-front elements. */
  bringToFront?: boolean;
  /** 3D surface anchor set when the marker is placed in a 3D view. */
  surface3d?: {
    meshId: string;
    position: { x: number; y: number; z: number };
    normal: { x: number; y: number; z: number };
  };
};

export interface FixedSlotsState {
  fixedSlots: FixedSlot[];
  fixedSlotsPlacing: null | {
    type: FixedSlotType;
    constraints: FixedSlotConstraints;
    label?: string;
    color?: string;
    targetWidth?: number;
    targetHeight?: number;
    bringToFront?: boolean;
  };
  fixedSlotActiveId: string | null;
  /** Marker editor modal state (create/edit). */
  markerEditorState: null | { mode: 'create' | 'edit'; slotId: string | null };
  setMarkerEditorState: (state: null | { mode: 'create' | 'edit'; slotId: string | null }) => void;

  beginPlaceFixedSlot: (payload: {
    type: FixedSlotType;
    constraints?: FixedSlotConstraints;
    label?: string;
    color?: string;
    targetWidth?: number;
    targetHeight?: number;
    bringToFront?: boolean;
  }) => void;
  cancelPlaceFixedSlot: () => void;

  addFixedSlot: (slot: FixedSlot) => void;
  removeFixedSlot: (slotId: string) => void;

  /** Merge/update slot + constraints. Also attempts to update already-created canvas objects. */
  updateFixedSlot: (slotId: string, patch: Partial<FixedSlot>) => void;

  setFixedSlotActive: (slotId: string | null) => void;
  setFixedSlotPosition: (slotId: string, x: number, y: number) => void;
}

// --- Elements (product parts / variants) ---
export type ElementSelection = {
  /** Element name (global key). */
  name: string;
  /** Optional viewId where the selection happened. */
  viewId?: string | null;
};

export interface ElementsState {
  /** Global elements catalog (can be recursive). */
  elements: AsukaElement[];
  /** Currently selected element on canvas (if any). */
  elementSelection: ElementSelection | null;

  /** Element editor modal state (opened from Elements module). */
  elementEditor: null | { mode: 'add' | 'edit'; initialValues?: Partial<AsukaElement> };
  openElementEditor: (mode: 'add' | 'edit', initialValues?: Partial<AsukaElement>) => void;
  closeElementEditor: () => void;

  /** Overlay state for per-instance variation picker (opened when clicking a non-selectable element on canvas). */
  elementVariationPicker: null | { objectId: string; elementId?: string; elementName: string; variationId?: string; objectSrc?: string; partId?: string; partLabel?: string; partMeshId?: string; partMaterialId?: string; focusOnOpen?: boolean; source?: string };
  openElementVariationPicker: (payload: { objectId: string; elementId?: string; elementName: string; variationId?: string; objectSrc?: string; partId?: string; partLabel?: string; partMeshId?: string; partMaterialId?: string; focusOnOpen?: boolean; source?: string }) => void;
  closeElementVariationPicker: () => void;

  setElementSelection: (sel: ElementSelection | null) => void;

  addElement: (name: string, element: AsukaElement) => void;
  editElement: (name: string, patch: Partial<AsukaElement>) => void;
  /** Save or clear the camera preset attached to one recursive variation. */
  setElementVariationCamera: (elementId: string, variationId: string, camera: Partial<AsukaCameraView> | null) => boolean;
  removeElement: (name: string) => void;

  /** Update which variation is active for the element definition. */
  setElementActiveVariation: (name: string, variationId: string) => void;
  /** Update one 3D product-part variation group without resetting other active part groups. */
  setProductPartActiveVariation: (name: string, partId: string, variationId: string) => void;

  /**
   * Apply a variation through the active designer runtime. Product-part variation calls may include partId.
   */
  applyElementVariation: (args: AsukaElementVariationApplyInput) => Promise<boolean>;

  /**
   * Apply a variation to all instances on the current canvas, and update other view snapshots
   * when similar instances exist.
   */

  /** Convenience: find element by name (deep). */
  findElement: (name: string) => AsukaElement | null;
  /** Convenience: find variation by id in an element. */
  findElementVariation: (name: string, variationId: string) => ElementVariation | null;
}

// --- Magnifier ---

export interface MagnifierState {
  magnifierEnabled: boolean;
  magnifierActive: boolean;
  magnifierX: number;
  magnifierY: number;
  magnifierSize: number;
  magnifierZoom: number;
  setMagnifierEnabled: (enabled: boolean) => void;
  setMagnifierActive: (active: boolean) => void;
  setMagnifierPosition: (x: number, y: number) => void;
  setMagnifierSize: (size: number) => void;
  setMagnifierZoom: (zoom: number) => void;
}

// --- Ruler (action module + overlay) ---

export type RulerUnit = 'px' | 'mm' | 'cm' | 'product';

export interface RulerProductConfig {
  /** Label displayed in the ruler (e.g. 'cm', 'mm', 'in'). */
  unitLabel: string;
  /** How many Fabric/world units represent 1 product unit. */
  worldUnitsPerUnit: number;
}

export interface RulerState {
  rulerEnabled: boolean;
  rulerUnit: RulerUnit;
  /** Pixels per inch used when rulerUnit is 'mm'/'cm'. Default: 96. */
  rulerDpi: number;
  /** Product-specific unit mapping (used when rulerUnit is 'product'). */
  rulerProduct: RulerProductConfig;

  setRulerEnabled: (enabled: boolean) => void;
  toggleRuler: () => void;
  setRulerUnit: (unit: RulerUnit) => void;
  setRulerDpi: (dpi: number) => void;
  setRulerProduct: (patch: Partial<RulerProductConfig>) => void;
}

// --- Shapes (basic + custom SVG library) ---

export type ShapePlaceKind =
  | { type: 'basic'; kind: 'rect' | 'circle' | 'triangle' | 'line' | 'star' }
  | { type: 'svg'; svgId: string };

export interface SvgShapeAsset {
  id: string;
  name: string;
  svgText: string;
}

export interface ShapesState {
  /** Library of custom SVG shapes available in the Shapes module. */
  shapeSvgAssets: SvgShapeAsset[];

  /** Default style applied to newly created shapes. */
  shapeStyle: {
    /** Fill color. Use 'transparent' for none. */
    fill: string;
    /** Stroke color. Use 'transparent' for none. */
    stroke: string;
    /** Stroke width in px. */
    strokeWidth: number;
    /** Opacity (0..1). */
    opacity: number;
  };

  /** Editing defaults for parameterized shapes. */
  shapeEditDefaults: {
    /** Rectangle corner radius (px). */
    rectRadius: number;
    /** Star spikes count. */
    starSpikes: number;
    /** Star inner radius ratio (0..1). */
    starInnerRatio: number;
  };

  setShapeStyle: (patch: Partial<ShapesState['shapeStyle']>) => void;
  setShapeEditDefaults: (patch: Partial<ShapesState['shapeEditDefaults']>) => void;

  /** Current placement mode for shapes (click-drag on canvas). */
  shapePlacing: null | ShapePlaceKind;

  beginPlaceShape: (payload: ShapePlaceKind) => void;
  cancelPlaceShape: () => void;

  addShapeSvgAsset: (payload: { name: string; svgText: string; id?: string }) => string;
  removeShapeSvgAsset: (id: string) => void;
}

export interface AsukaState
  extends LayoutState,
    UiConfigState,
    ResponsiveState,
    ModuleState,
    IntegrationsState,
    LicenseState,
    ModuleConfigState,
    UploadedAssetsState,
    DesignerState,
    AiRuntimeState,
    ExportRuntimeState,
    I18nState,
    ListenersState,
    FixedSlotsState,
    ElementsState,
    ViewState,
    DocumentState,
    SelectionMenuState,
    MagnifierState,
    RulerState,
    ShapesState,
    FontsState,
    DefaultsState,
    ColorScopeState,
    ColorsState,
    PricingState,
    PrintingConfigState {
  bootReady: boolean;
  bootStatus: string;
  setBootReady: (ready: boolean) => void;
  setBootStatus: (status: string) => void;
}

// ---------------------------------------------------------------------------
// Listeners (behaviour flags) — enables/disables core designer behaviours.
// ---------------------------------------------------------------------------

export type AsukaListenerId =
  | 'constraints'
  | 'preventBoundingBoxExit'
  | 'preventBoundingBoxOvertaking'
  | 'snapObjects'
  | 'snapRotation'
  | 'snapCenter'
  | 'clickElement'
  | 'adjustBoundingBoxColors'
  | 'boundingBoxesBorders';

export interface AsukaListenerSettings {
  // Snapping
  snapTolerance: number;
  snapRelease: number;
  snapRotationStep: number;

  /** 0..1 — how strongly objects are attracted to guides (1 = strong snap, 0 = very soft). */
  magnetStrength: number;

  /** Enable dynamic snap tuning based on movement speed (slow = stronger snap, fast = weaker). */
  smartSnap: boolean;
  /** Minimum multiplier applied to snap tolerance when moving fast. */
  smartSnapMinFactor: number;
  /** Maximum multiplier applied to snap tolerance when moving slow. */
  smartSnapMaxFactor: number;

  // Constraints feel
  /** 0..1 — how strongly objects are pushed back inside boundaries when they try to exceed them. */
  edgeResistance: number;

  // Inertia
  inertia: boolean;
  /** Friction (0..1). Lower stops faster; higher glides longer. */
  inertiaFriction: number;
  /** Minimum speed (px/ms) to start inertia. */
  inertiaMinSpeed: number;
  /** Safety cap. */
  inertiaMaxMs: number;

  // Elastic edges + spring physics (soft boundary feel)
  elasticEdges: boolean;
  /** Maximum overflow allowed beyond boundary in soft mode (px). */
  elasticOverflowLimit: number;
  /** Spring stiffness (higher = snappier). */
  springK: number;
  /** Spring damping (0..1). Higher = less oscillation. */
  springDamping: number;

  // Snap priority weights (lower = higher priority)
  snapPriorityWeights: {
    zoneEdge: number;
    zoneCenter: number;
    canvasEdge: number;
    canvasCenter: number;
    objectEdge: number;
    objectCenter: number;
  };

  // Magnetic stacking (soft align-to-other-objects)
  magneticStacking: boolean;
  magneticStackingStrength: number; // 0..1
  magneticStackingSpacing: number; // px

  // Collision layers
  collisionEnabled: boolean;
  collisionMode: 'soft' | 'hard';
  collisionPadding: number; // px
}

export interface ListenersState {
  /** Enabled flags for each behaviour. Defaults are applied at init. */
  listenersEnabled: Record<AsukaListenerId, boolean>;
  /** Tuning values for some behaviours (snap tolerance/step...). */
  listenerSettings: AsukaListenerSettings;

  setListenerEnabled: (id: AsukaListenerId, enabled: boolean) => void;
  setListenersEnabled: (patch: Partial<Record<AsukaListenerId, boolean>>) => void;
  isListenerEnabled: (id: AsukaListenerId) => boolean;
  setListenerSettings: (patch: Partial<AsukaListenerSettings>) => void;
}

// Printing methods are empty by default and only become available when provided at init/API.
const DEFAULT_PRINTING_METHODS: PrintingMethod[] = [];

/**
 * Default blocks.
 */
const LAYOUT_ZONES: LayoutZoneId[] = ['top', 'bottom', 'left', 'right'];

const initialBlocksByZone: Record<LayoutZoneId, LayoutBlock[]> = {
  top: [
    { id: 'block-magnifier', kind: 'magnifier', label: 'Magnifier', icon: 'magnifying-glass', order: 0, type: 'floating' },
    { id: 'block-ruler', kind: 'ruler', label: 'Ruler', icon: 'ruler', order: 1, type: 'floating' },
    { id: 'block-zoom', kind: 'zoom', label: 'Zoom', icon: 'zoom', order: 2, type: 'floating' },
    { id: 'block-download', kind: 'download', label: 'Download', icon: 'download', order: 3, type: 'floating' },
  ],
  bottom: [],
  left: [
    { id: 'block-views', kind: 'views', label: 'Views', icon: 'views', order: 0, type: 'floating' },
    { id: 'block-designs', kind: 'designs', label: 'Designs', icon: 'brush', order: 1, type: 'floating' },
    { id: 'block-text', kind: 'text', label: 'Text', icon: 'text', order: 2, type: 'floating' },
    { id: 'block-images', kind: 'image', label: 'Images', icon: 'images', order: 3, type: 'floating' },
    { id: 'block-shapes', kind: 'shapes', label: 'Shapes', icon: 'customize', order: 4, type: 'floating' },
    { id: 'block-layers', kind: 'layers', label: 'Layers', icon: 'layers', order: 5, type: 'floating' },
  ],
  right: [],
};

/**
 * Initial views are intentionally empty.
 *
 * When the designer is created without imported state or explicit view creation,
 * the UI should start in the empty-state flow instead of forcing a default
 * "Front" view. The first view is created explicitly by the user or by an
 * imported document/configuration.
 */
const initialViews: AsukaView[] = [];

// Default listener flags are enabled unless explicitly disabled.
const DEFAULT_LISTENERS_ENABLED: Record<AsukaListenerId, boolean> = {
  constraints: true,
  // Default UX is permissive but safe:
  // - partial overflow is allowed
  // - full exit from the drawing area/container reverts to the last valid position
  preventBoundingBoxExit: true,
  preventBoundingBoxOvertaking: false,
  snapObjects: true,
  snapRotation: true,
  snapCenter: true,
  adjustBoundingBoxColors: true,
  boundingBoxesBorders: true,
  clickElement: true,
};

const DEFAULT_LISTENER_SETTINGS: AsukaListenerSettings = {
  snapTolerance: 5,
  snapRelease: 10,
  snapRotationStep: 45,

  magnetStrength: 0.85,

  smartSnap: true,
  smartSnapMinFactor: 0.6,
  smartSnapMaxFactor: 1.6,

  edgeResistance: 0,

  inertia: true,
  inertiaFriction: 0.92,
  inertiaMinSpeed: 0.25,
  inertiaMaxMs: 450,

  elasticEdges: false,
  elasticOverflowLimit: 0,
  springK: 0.22,
  springDamping: 0.78,

  snapPriorityWeights: {
    zoneEdge: 0.8,
    zoneCenter: 1.05,
    canvasEdge: 0.95,
    canvasCenter: 1.15,
    objectEdge: 1.0,
    objectCenter: 1.2,
  },

  magneticStacking: true,
  magneticStackingStrength: 0.75,
  magneticStackingSpacing: 0,

  collisionEnabled: false,
  collisionMode: 'soft',
  collisionPadding: 0,
};


function normalizeLayoutItemSupportedEngines(value: any): AsukaRenderEngineKind[] | undefined {
  if (!Array.isArray(value)) return undefined;
  const engines = value
    .map((item: any) => String(item ?? '').trim().toLowerCase())
    .filter((item: string): item is AsukaRenderEngineKind => item === 'fabric' || item === '3d');
  return engines.length > 0 ? Array.from(new Set(engines)) : undefined;
}

function normalizeLayoutItemRequiredCapabilities(value: any): AsukaRenderEngineCapabilityKey[] | undefined {
  return normalizeRenderEngineCapabilityList(value);
}

function normalizeBlocksForZone(blocks: any[], zone: LayoutZoneId): LayoutBlock[] {
  return (Array.isArray(blocks) ? blocks : [])
    .filter((block: any) =>
      block
      && typeof block === 'object'
      && typeof block.kind === 'string',
    )
    .map((block: any, index: number) => ({
      id: typeof block.id === 'string' && block.id.trim() ? block.id.trim() : `block-${zone}-${String(block.kind)}-${index}`,
      kind: block.kind as ModuleId,
      label: typeof block.label === 'string' && block.label.trim() ? block.label.trim() : String(block.kind),
      icon: typeof block.icon === 'string' && block.icon.trim() ? block.icon.trim() : 'views',
      order: typeof block.order === 'number' && Number.isFinite(block.order) ? block.order : index,
      priority: typeof block.priority === 'number' && Number.isFinite(block.priority) ? block.priority : undefined,
      type: typeof block.type === 'string' && block.type.trim() ? block.type.trim() : undefined,
      supportedEngines: normalizeLayoutItemSupportedEngines((block as any).supportedEngines),
      requiredCapabilities: normalizeLayoutItemRequiredCapabilities((block as any).requiredCapabilities),
      mountTarget: typeof block.mountTarget === 'string' && block.mountTarget.trim() ? block.mountTarget.trim() : undefined,
      collapsed: typeof block.collapsed === 'boolean' ? block.collapsed : undefined,
      renderMode: block.renderMode === 'inline' || block.renderMode === 'portal' ? block.renderMode : undefined,
    }))
    .sort((a, b) => {
      const orderDelta = Number(a.order ?? 0) - Number(b.order ?? 0);
      return orderDelta !== 0 ? orderDelta : String(a.id ?? '').localeCompare(String(b.id ?? ''));
    })
    .map((block, order) => ({ ...block, order }));
}

function normalizeBlocksByZone(blocksByZone: Partial<Record<LayoutZoneId, any[]>>): Record<LayoutZoneId, LayoutBlock[]> {
  return {
    top: normalizeBlocksForZone(blocksByZone.top ?? [], 'top'),
    bottom: normalizeBlocksForZone(blocksByZone.bottom ?? [], 'bottom'),
    left: normalizeBlocksForZone(blocksByZone.left ?? [], 'left'),
    right: normalizeBlocksForZone(blocksByZone.right ?? [], 'right'),
  };
}

function mapBlocksByZone(
  blocksByZone: Record<LayoutZoneId, LayoutBlock[]>,
  mapper: (block: LayoutBlock, zone: LayoutZoneId, index: number) => LayoutBlock,
): Record<LayoutZoneId, LayoutBlock[]> {
  const next: Partial<Record<LayoutZoneId, LayoutBlock[]>> = {};
  LAYOUT_ZONES.forEach((zone) => {
    next[zone] = (blocksByZone?.[zone] ?? []).map((block, index) => mapper({ ...block }, zone, index));
  });
  return normalizeBlocksByZone(next);
}

// ---------------------------------------------------------------------------
// Permission helper (store-level)
// ---------------------------------------------------------------------------

function getEffectivePermissionsFromStoreState(state: any): AsukaPermissions {
  return getEffectivePermissions(
    (state?.permissions ?? DEFAULT_PERMISSIONS) as AsukaPermissions,
    (state?.permissionsByView?.[state?.activeViewId ?? ''] ?? null) as any,
  );
}

function isDeniedFromStore(getFn: () => any, key: keyof AsukaPermissions, payload: any = {}): boolean {
  const state = getFn?.() ?? {};
  const perms = getEffectivePermissionsFromStoreState(state);

  if (key === 'canOpenModule' && payload?.id) {
    try {
      if (canOpenModuleId(perms, payload.id as any)) return false;
    } catch {}
  } else if ((perms as any)[key]) {
    return false;
  }

  const reason = payload?.reason ?? `Permission denied: ${String(key)}`;
  const ok = emitAsukaEventCancelable('before:permission.denied', { key, reason, ...payload });
  if (!ok) return false; // listener override
  emitAsukaEvent('after:permission.denied', { key, reason, ...payload });
  return true;
}

/**
 * Store Zustand principal.
 */
export const useAsukaStore = create<AsukaState>((set, get) => ({
  // i18n reactive tick (bumped on locale/translations changes so UI can re-render)
  i18nVersion: 0,
  bumpI18nVersion: () => set((s: any) => ({ i18nVersion: (s.i18nVersion ?? 0) + 1 })),

  // Behaviour toggles (listeners)
  listenersEnabled: { ...DEFAULT_LISTENERS_ENABLED },
  listenerSettings: { ...DEFAULT_LISTENER_SETTINGS },
  setListenerEnabled: (id, enabled) =>
    set((s: any) => ({
      listenersEnabled: { ...(s.listenersEnabled ?? {}), [id]: !!enabled },
    })),
  setListenersEnabled: (patch) =>
    set((s: any) => ({
      listenersEnabled: { ...(s.listenersEnabled ?? {}), ...(patch ?? {}) },
    })),
  isListenerEnabled: (id) => {
    const val = (get() as any).listenersEnabled?.[id];
    return val !== false;
  },
  setListenerSettings: (patch) =>
    set((s: any) => ({
      listenerSettings: { ...(s.listenerSettings ?? DEFAULT_LISTENER_SETTINGS), ...(patch ?? {}) },
    })),

  // Misc defaults (global)
  defaults: {
    uiScaling: {
      keepControlsReadable: true,
      keepHelpersReadable: true,
      baseControlSize: 20,
      baseTouchControlSize: 28,
      minHelperStrokeWidth: 1,
      maxCompensationMultiplier: 4,
    },
    priceFormat: {
      code: '',
      symbol: '',
      priceFormat: '%2$s',
      decimalSeparator: '.',
      thousandSeparator: '',
      precision: 2,
      trimZeros: false,
    },
  },
  setDefaults: (patch: any) => set((s: any) => ({ defaults: { ...(s.defaults ?? {}), ...(patch ?? {}) } })),

// Magnifier action module state
  magnifierEnabled: false,
  magnifierActive: false,
  magnifierX: 0,
  magnifierY: 0,
  magnifierSize: 140,
  magnifierZoom: 2,
  setMagnifierEnabled: (enabled) => {
    if (enabled && isDeniedFromStore(get, 'canUseMagnifier', { action: 'store.setMagnifierEnabled', enabled })) return;
    set({ magnifierEnabled: Boolean(enabled), magnifierActive: enabled ? (get() as any).magnifierActive : false });
  },
  setMagnifierActive: (active) => {
    if (active && isDeniedFromStore(get, 'canUseMagnifier', { action: 'store.setMagnifierActive', active })) return;
    set({ magnifierActive: Boolean(active) });
  },
  setMagnifierPosition: (x, y) => {
    if (isDeniedFromStore(get, 'canUseMagnifier', { action: 'store.setMagnifierPosition', x, y })) return;
    set({ magnifierX: Number(x) || 0, magnifierY: Number(y) || 0 });
  },
  setMagnifierSize: (size) => {
    if (isDeniedFromStore(get, 'canUseMagnifier', { action: 'store.setMagnifierSize', size })) return;
    set({ magnifierSize: Math.max(60, Number(size) || 140) });
  },
  setMagnifierZoom: (zoom) => {
    if (isDeniedFromStore(get, 'canUseMagnifier', { action: 'store.setMagnifierZoom', zoom })) return;
    set({ magnifierZoom: Math.max(1, Number(zoom) || 2) });
  },

  // Ruler (action module + overlay)
  rulerEnabled: false,
  rulerUnit: 'px',
  rulerDpi: 96,
  rulerProduct: { unitLabel: 'cm', worldUnitsPerUnit: 10 },
  setRulerEnabled: (enabled) => set({ rulerEnabled: Boolean(enabled) }),
  toggleRuler: () => set((s: any) => ({ rulerEnabled: !(s as any).rulerEnabled })),
  setRulerUnit: (unit) =>
    set({ rulerUnit: (unit as any) ?? 'px' }),
  setRulerDpi: (dpi) => set({ rulerDpi: Math.max(10, Number(dpi) || 96) }),
  setRulerProduct: (patch) =>
    set((s: any) => ({
      rulerProduct: {
        ...(s.rulerProduct ?? { unitLabel: 'cm', worldUnitsPerUnit: 10 }),
        ...(patch ?? {}),
      },
    })),

  // Shapes (basic + custom SVG library)
  shapeSvgAssets: [],
  shapeStyle: {
    fill: 'transparent',
    stroke: '#111827',
    strokeWidth: 2,
    opacity: 1,
  },
  shapeEditDefaults: {
    rectRadius: 0,
    starSpikes: 5,
    starInnerRatio: 0.45,
  },
  setShapeStyle: (patch) => {
    if (isDeniedFromStore(get, 'canAddShape', { action: 'store.setShapeStyle', patch })) return;
    set((s: any) => ({
      shapeStyle: {
        ...(s.shapeStyle ?? { fill: 'transparent', stroke: '#111827', strokeWidth: 2, opacity: 1 }),
        ...(patch ?? {}),
      },
    }));
  },
  setShapeEditDefaults: (patch) => {
    if (isDeniedFromStore(get, 'canAddShape', { action: 'store.setShapeEditDefaults', patch })) return;
    set((s: any) => ({
      shapeEditDefaults: {
        ...(s.shapeEditDefaults ?? { rectRadius: 0, starSpikes: 5, starInnerRatio: 0.45 }),
        ...(patch ?? {}),
      },
    }));
  },
  shapePlacing: null,
  beginPlaceShape: (payload) => {
    if (isDeniedFromStore(get, 'canAddShape', { action: 'store.beginPlaceShape', payload })) return;
    if ((payload as any)?.type === 'basic' && isDeniedFromStore(get, 'canDrawShape', { action: 'store.beginPlaceShape', payload })) return;
    set({ shapePlacing: payload as any });
  },
  cancelPlaceShape: () => set({ shapePlacing: null }),
  addShapeSvgAsset: (payload) => {
    if (isDeniedFromStore(get, 'canAddShape', { action: 'store.addShapeSvgAsset', payload })) return '';
    const id = String(payload?.id ?? `shape-${Math.random().toString(36).slice(2)}-${Date.now()}`);
    const name = String(payload?.name ?? '').trim() || 'Shape';
    const svgText = String(payload?.svgText ?? '').trim();
    if (!svgText) return '';
    set((s: any) => ({ shapeSvgAssets: [...(s.shapeSvgAssets ?? []), { id, name, svgText }] }));
    return id;
  },
  removeShapeSvgAsset: (id) => {
    if (isDeniedFromStore(get, 'canAddShape', { action: 'store.removeShapeSvgAsset', id })) return;
    set((s: any) => ({
      shapeSvgAssets: (s.shapeSvgAssets ?? []).filter((x: any) => String(x?.id) !== String(id)),
    }));
  },

  // Fixed slots (placeholders)
  fixedSlots: [],
  fixedSlotsPlacing: null,
  fixedSlotActiveId: null,
  markerEditorState: null,
  setMarkerEditorState: (state) => {
    const mode = (state as any)?.mode;
    if (mode === 'create' && isDeniedFromStore(get, 'canCreateFixedSlots', { action: 'store.setMarkerEditorState', state })) return;
    if (mode === 'edit' && isDeniedFromStore(get, 'canEditFixedSlots', { action: 'store.setMarkerEditorState', state })) return;
    set({ markerEditorState: state });
  },

  beginPlaceFixedSlot: ({ type, constraints, label, color, targetWidth, targetHeight, bringToFront }) => {
    if (isDeniedFromStore(get, 'canCreateFixedSlots', { action: 'store.beginPlaceFixedSlot', type })) return;
    set({
      fixedSlotsPlacing: {
        type,
        constraints: constraints ?? {},
        label,
        color,
        targetWidth: Number.isFinite(Number(targetWidth)) && Number(targetWidth) > 0 ? Number(targetWidth) : undefined,
        targetHeight: Number.isFinite(Number(targetHeight)) && Number(targetHeight) > 0 ? Number(targetHeight) : undefined,
        bringToFront: Boolean(bringToFront),
      },
    });
  },
  cancelPlaceFixedSlot: () => set({ fixedSlotsPlacing: null }),
  addFixedSlot: (slot) => {
    if (isDeniedFromStore(get, 'canCreateFixedSlots', { action: 'store.addFixedSlot', slot })) return;
    set((s: any) => ({ fixedSlots: [...(s.fixedSlots ?? []), slot], fixedSlotsPlacing: null }));
    try { (get() as any)?.designerInstance?.__asukaScheduleHistorySnapshot?.({ force: true }); } catch {}
  },
  removeFixedSlot: (slotId) => {
    if (isDeniedFromStore(get, 'canDeleteFixedSlots', { action: 'store.removeFixedSlot', slotId })) return;
    const normalizedSlotId = String(slotId ?? '').trim();
    if (!normalizedSlotId) return;

    const removedObjectIds = new Set<string>();
    let activeViewId = '';

    set((s: any) => {
      activeViewId = String(s?.activeViewId ?? '').trim();
      const nextFixedSlots = (s.fixedSlots ?? []).filter((x: any) => String(x?.id ?? '').trim() !== normalizedSlotId);
      const nextActiveId = String(s.fixedSlotActiveId ?? '').trim() === normalizedSlotId ? null : (s.fixedSlotActiveId ?? null);
      const nextMarkerEditorState = String((s.markerEditorState as any)?.slotId ?? '').trim() === normalizedSlotId ? null : (s.markerEditorState ?? null);
      const byView = (s?.documentObjectsByView && typeof s.documentObjectsByView === 'object') ? s.documentObjectsByView : {};
      const nextByView = Object.fromEntries(
        Object.entries(byView).map(([viewId, list]) => {
          const nextList = (Array.isArray(list) ? list : []).filter((o: any) => {
            const match = objectMatchesFixedSlot(o, normalizedSlotId);
            if (match) {
              const objectId = getSerializedObjectId(o);
              if (objectId) removedObjectIds.add(objectId);
            }
            return !match;
          });
          return [viewId, nextList];
        }),
      );

      return {
        fixedSlots: nextFixedSlots,
        fixedSlotActiveId: nextActiveId,
        markerEditorState: nextMarkerEditorState,
        documentObjectsByView: nextByView,
      } as any;
    });

    try {
      removeFixedSlotObjectsFromSnapshots(normalizedSlotId, Array.from(removedObjectIds));
    } catch {}

    try {
      const inst = (get() as any)?.designerInstance as any;
      const canvas = inst?.getCanvas?.();
      if (canvas?.getObjects) {
        const idSet = new Set(removedObjectIds);
        const related = (canvas.getObjects?.() ?? []).filter((o: any) => objectMatchesFixedSlot(o, normalizedSlotId, idSet));
        if (related.length > 0) {
          const activeObject = canvas.getActiveObject?.();
          if (activeObject && objectMatchesFixedSlot(activeObject, normalizedSlotId, idSet)) {
            try { canvas.discardActiveObject?.(); } catch {}
          }
          for (const obj of related) {
            try {
              obj.visible = false;
              obj.evented = false;
              obj.selectable = false;
              obj.excludeFromExport = true;
              obj.setCoords?.();
            } catch {}
            try { canvas.remove?.(obj); } catch {}
          }
          try { canvas.requestRenderAll?.(); } catch {}
        }
      }

      try { inst?.runtime?.fixedSlots?.ensureIndicatorsForSlots?.(); } catch {}
      try { inst?.__asukaScheduleHistorySnapshot?.({ force: true }); } catch {}
      // Safety net: if a live Fabric object did not carry the marker metadata,
      // reload the active view from the already-filtered document state/snapshot.
      try { if (inst && activeViewId) switchViewWithCanvas(inst, activeViewId); } catch {}
    } catch {}
  },

  updateFixedSlot: (slotId, patch) => {
    if (isDeniedFromStore(get, 'canEditFixedSlots', { action: 'store.updateFixedSlot', slotId, patch })) return;
    let runtimeToRefresh: any = null;
    set((s: any) => {
      const normalizedSlotId = String(slotId ?? '');
      const nextSlots = (s.fixedSlots ?? []).map((fs: any) => {
        if (String(fs?.id) !== normalizedSlotId) return fs;
        const mergedConstraints = { ...(fs.constraints ?? {}), ...(((patch as any).constraints ?? {}) as any) };
        const safePatch: any = { ...(patch as any) };
        // Editing type/label/constraints must never accidentally move the marker.
        // Some form/state paths can pass x/y as undefined; spreading that over the
        // existing slot makes the indicator rebuild at 0,0 or NaN and corrupts history.
        if (!Number.isFinite(Number(safePatch.x))) delete safePatch.x;
        if (!Number.isFinite(Number(safePatch.y))) delete safePatch.y;
        return { ...fs, ...safePatch, constraints: mergedConstraints };
      });

      // Apply constraints & metadata to already-created canvas objects when possible.
      try {
        const inst = (s.designerInstance ?? null) as any;
        runtimeToRefresh = inst?.runtime?.fixedSlots ?? null;
        const canvas = inst?.getCanvas?.();
        if (canvas?.getObjects) {
          const slot = nextSlots.find((x: any) => String(x?.id) === normalizedSlotId);
          const objs = canvas.getObjects?.() ?? [];
          for (const o of objs) {
            if (String((o as any)?._asukaFixedSlotId) !== normalizedSlotId) continue;
            (o as any)._asukaFixedSlotType = String(slot?.type ?? (o as any)._asukaFixedSlotType ?? 'text');
            (o as any)._asukaFixedSlotColor = typeof slot?.color === 'string' ? slot.color : undefined;
            (o as any)._asukaFixedSlotTargetWidth = Number.isFinite(Number(slot?.targetWidth)) && Number(slot?.targetWidth) > 0 ? Number(slot.targetWidth) : undefined;
            (o as any)._asukaFixedSlotTargetHeight = Number.isFinite(Number(slot?.targetHeight)) && Number(slot?.targetHeight) > 0 ? Number(slot.targetHeight) : undefined;
            (o as any)._asukaBringToFront = Boolean(slot?.bringToFront);
            (o as any).bringToFront = Boolean(slot?.bringToFront);
            (o as any)._asukaFixedSlotConstraints = slot?.constraints ?? {};
            try {
              if (!(o as any)._asukaFixedSlotIsIndicator) inst?.applyFixedSlotTargetSizeToObject?.(o, slot);
              inst?.applyFixedConstraintsToObject?.(o, slot?.constraints ?? {});
            } catch {}
          }
          try {
            canvas.requestRenderAll?.();
          } catch {}
        }
      } catch {}

      return { fixedSlots: nextSlots };
    });

    // The visual indicator must be rebuilt *after* Zustand has committed the
    // new slot definition. This keeps the marker position stable while updating
    // the icon, label and constraints immediately after an edit.
    try {
      const run = () => {
        try {
          const inst = (get() as any)?.designerInstance as any;
          const runtime = runtimeToRefresh ?? (inst?.runtime?.fixedSlots ?? null);
          runtime?.ensureIndicatorsForSlots?.();
          inst?.__asukaScheduleHistorySnapshot?.({ force: true });
        } catch {}
      };
      const scheduler = (globalThis as any)?.queueMicrotask;
      if (typeof scheduler === 'function') scheduler(run);
      else setTimeout(run, 0);
    } catch {}
  },

  setFixedSlotActive: (slotId) => set({ fixedSlotActiveId: slotId ?? null }),
  setFixedSlotPosition: (slotId, x, y) => {
    if (isDeniedFromStore(get, 'canEditFixedSlots', { action: 'store.setFixedSlotPosition', slotId, x, y })) return;
    set((s: any) => ({
      fixedSlots: (s.fixedSlots ?? []).map((fs: any) =>
        String(fs?.id) === String(slotId) ? { ...fs, x: Number(x) || 0, y: Number(y) || 0 } : fs,
      ),
    }));
    try {
      const inst = (get() as any)?.designerInstance as any;
      inst?.runtime?.fixedSlots?.ensureIndicatorsForSlots?.();
      inst?.__asukaScheduleHistorySnapshot?.({ force: true });
    } catch {}
  },

  // --- Elements (catalog + instances sync) ---
  elements: [],
  elementSelection: null,
  elementEditor: null,
  openElementEditor: (mode, initialValues) => {
    const action = mode === 'add' ? 'store.openElementEditor.add' : 'store.openElementEditor.edit';
    if (mode === 'add' && isDeniedFromStore(get, 'canAddElements', { action, initialValues })) return;
    if (mode === 'edit') {
      if (isDeniedFromStore(get, 'canEditElements', { action, initialValues })) return;
      if (isDeniedFromStore(get, 'canEditElementVariations', { action, initialValues })) return;
    }
    set({ elementEditor: { mode, initialValues: initialValues ?? {} } as any });
  },
  closeElementEditor: () => set({ elementEditor: null }),

  elementVariationPicker: null,
  openElementVariationPicker: (payload) => {
    if (isDeniedFromStore(get, 'canViewElements', { action: 'store.openElementVariationPicker', payload })) return;
    const startedAt = typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();
    const moduleId = 'element-variation-picker' as ModuleId;

    // Contextual canvas navigation follows the module's actual layout placement.
    // Inline and panel sidebars already own the module body, so reveal that body
    // (including collapsed zones/blocks) instead of opening a duplicate overlay.
    // Default menu zones remain triggers and therefore use the regular overlay path.
    const placement = resolveModuleLayoutPlacement(get(), moduleId);
    if (placement?.kind === 'embedded') {
      moduleOpenRequestSeq += 1;
      set((state: any) => {
        const stack = Array.isArray(state.moduleOverlayStack) ? state.moduleOverlayStack : [];
        const filteredStack = stack.filter((entry: any) => String(entry ?? '') !== String(moduleId));
        const pickerOwnsOverlay = String(state.activeModuleId ?? '') === String(moduleId);

        const nextZoneCollapsed = placement.zoneCollapsed
          ? { ...(state.zoneCollapsed ?? {}), [placement.zone]: false }
          : state.zoneCollapsed;

        const nextBlocksByZone = placement.blockCollapsed
          ? {
              ...(state.blocksByZone ?? {}),
              [placement.zone]: (Array.isArray(state.blocksByZone?.[placement.zone])
                ? state.blocksByZone[placement.zone]
                : []).map((block: any) =>
                  String(block?.id ?? '') === String(placement.blockId)
                    ? { ...block, collapsed: false }
                    : block,
                ),
            }
          : state.blocksByZone;

        return {
          elementVariationPicker: payload,
          zoneCollapsed: nextZoneCollapsed,
          blocksByZone: nextBlocksByZone,
          ...(pickerOwnsOverlay || filteredStack.length !== stack.length
            ? {
                activeModuleId: pickerOwnsOverlay ? (filteredStack[filteredStack.length - 1] ?? null) : state.activeModuleId,
                moduleOverlayStack: filteredStack,
              }
            : {}),
        } as any;
      });
      logAsuka3DPerf('ui-module', 'variation-picker-revealed-in-layout', {
        moduleId,
        zone: placement.zone,
        zoneType: placement.zoneType,
        blockId: placement.blockId,
        expandedZone: placement.zoneCollapsed,
        expandedBlock: placement.blockCollapsed,
        elementId: payload?.elementId ?? null,
        objectId: payload?.objectId ?? null,
        partId: payload?.partId ?? null,
      }, { durationMs: (typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now()) - startedAt, console: false });
      return;
    }

    const resolved = getModule(moduleId);
    if (resolved && !resolved.action) {
      // Canvas product-part clicks used to perform two independent store writes:
      // first the picker payload, then (after an async module lookup) the overlay.
      // Besides rendering twice, the guard in ModuleHost could briefly observe an
      // active picker without its payload. Open both atomically when the built-in
      // module is already registered.
      moduleOpenRequestSeq += 1;
      set((state: any) => ({
        ...buildModuleOpenState(state, moduleId, resolved),
        elementVariationPicker: payload,
      }));
      logAsuka3DPerf('ui-module', 'variation-picker-opened-atomic', {
        moduleId,
        elementId: payload?.elementId ?? null,
        objectId: payload?.objectId ?? null,
        partId: payload?.partId ?? null,
      }, { durationMs: (typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now()) - startedAt, console: false });
      return;
    }
    set({ elementVariationPicker: payload });
    get().openModule(moduleId);
  },
  closeElementVariationPicker: () => set({ elementVariationPicker: null }),
  setElementSelection: (sel) => set({ elementSelection: sel }),

  findElement: (identifier) => {
    const wanted = String(identifier ?? '').trim();
    const walk = (list: AsukaElement[]): AsukaElement | null => {
      for (const el of list) {
        if (String((el as any)?.id ?? '').trim() === wanted) return el;
        if (String(el?.name ?? '').trim() === wanted) return el;
        const children = Array.isArray(el.children) ? el.children : [];
        const found = walk(children);
        if (found) return found;
      }
      return null;
    };
    return walk((get() as any).elements ?? []);
  },
  findElementVariation: (identifier, variationId) => {
    const el = (get() as any).findElement?.(identifier) as AsukaElement | null;
    if (!el) return null;
    const walk = (list: any[]): any => {
      for (const v of Array.isArray(list) ? list : []) {
        if (!v) continue;
        if (String((v as any)?.id ?? '') === String(variationId)) return v;
        const found = walk((v as any)?.variations ?? []);
        if (found) return found;
      }
      return null;
    };
    return walk((el as any).variations ?? []) ?? null;
  },

  addElement: (identifier, element) =>
    set((s: any) => {
      if (isDeniedFromStore(get, 'canAddElements', { action: 'store.addElement', identifier })) return {} as any;
      const list: AsukaElement[] = Array.isArray(s.elements) ? s.elements : [];
      const wanted = String((element as any)?.id ?? identifier ?? '').trim();
      const exists = list.some((e) => String((e as any)?.id ?? '').trim() === wanted);
      const normalizedElement = normalizeElementPrices(element);
      const next = exists ? list.map((e) => (String((e as any)?.id ?? '').trim() === wanted ? normalizedElement : e)) : [...list, normalizedElement];
      return { elements: next };
    }),
  editElement: (identifier, patch) =>
    set((s: any) => {
      if (isDeniedFromStore(get, 'canEditElements', { action: 'store.editElement', identifier })) return {} as any;
      if (isDeniedFromStore(get, 'canEditElementVariations', { action: 'store.editElement', identifier })) return {} as any;
      const list: AsukaElement[] = Array.isArray(s.elements) ? s.elements : [];
      const wanted = String(identifier ?? '').trim();
      const normalizedPatch = normalizeElementPrices(patch ?? {});
      const next = list.map((e) => (String((e as any)?.id ?? '').trim() === wanted || String((e as any)?.name ?? '').trim() === wanted ? normalizeElementPrices({ ...e, ...normalizedPatch }) : e));
      return { elements: next };
    }),
  setElementVariationCamera: (elementIdentifier, variationId, camera) => {
    if (isDeniedFromStore(get, 'canEditElements', { action: 'store.setElementVariationCamera', elementIdentifier, variationId })) return false;
    if (isDeniedFromStore(get, 'canEditElementVariations', { action: 'store.setElementVariationCamera', elementIdentifier, variationId })) return false;
    const wanted = String(elementIdentifier ?? '').trim();
    let changed = false;
    set((s: any) => {
      const list: AsukaElement[] = Array.isArray(s.elements) ? s.elements : [];
      const walk = (items: AsukaElement[]): AsukaElement[] => items.map((element) => {
        const matches = String((element as any)?.id ?? '').trim() === wanted || String((element as any)?.name ?? '').trim() === wanted;
        let nextElement = element;
        if (matches) {
          const updated = setVariationCamera(element, variationId, camera);
          if (updated !== element) changed = true;
          nextElement = updated;
        }
        const children = Array.isArray((nextElement as any)?.children) ? (nextElement as any).children as AsukaElement[] : [];
        if (!children.length) return nextElement;
        const nextChildren = walk(children);
        return nextChildren.some((child, index) => child !== children[index])
          ? { ...nextElement, children: nextChildren }
          : nextElement;
      });
      const next = walk(list);
      return changed ? { elements: next } : {} as any;
    });
    return changed;
  },
  removeElement: (identifier) => {
    if (isDeniedFromStore(get, 'canDeleteElements', { action: 'store.removeElement', identifier })) return;
    const normalizedIdentifier = String(identifier ?? '').trim();
    if (!normalizedIdentifier) return;

    const removedObjectIds = new Set<string>();
    let activeViewId = '';
    let deleteIdentifiers = new Set<string>([normalizedIdentifier]);

    set((s: any) => {
      const list: AsukaElement[] = Array.isArray(s.elements) ? s.elements : [];
      activeViewId = String(s?.activeViewId ?? '').trim();
      deleteIdentifiers = collectElementDeleteIdentifiers(list, normalizedIdentifier);
      const byView = (s?.documentObjectsByView && typeof s.documentObjectsByView === 'object') ? s.documentObjectsByView : {};
      const currentObjects = activeViewId && Array.isArray(byView?.[activeViewId]) ? byView[activeViewId] : [];
      const nextCurrentObjects = currentObjects.filter((o: any) => {
        const match = objectMatchesElement(o, deleteIdentifiers);
        if (match) {
          const objectId = getSerializedObjectId(o);
          if (objectId) removedObjectIds.add(objectId);
        }
        return !match;
      });
      const nextByView = activeViewId
        ? { ...byView, [activeViewId]: nextCurrentObjects }
        : byView;

      const scenesByView = (s?.document3dScenesByView && typeof s.document3dScenesByView === 'object') ? s.document3dScenesByView : {};
      let next3dScenesByView = scenesByView;
      const historyByView = (s?.document3dHistoryByView && typeof s.document3dHistoryByView === 'object') ? s.document3dHistoryByView : {};
      let next3dHistoryByView = historyByView;
      for (const [viewId, scene] of Object.entries(scenesByView as Record<string, any>)) {
        const products = Array.isArray(scene?.products) ? scene.products : [];
        const removedProductIds = new Set<string>();
        const remainingProducts = products.filter((product: any) => {
          const keys = [product?.id, product?.catalogId, product?.name].map((value) => String(value ?? '').trim()).filter(Boolean);
          const remove = keys.some((key) => deleteIdentifiers.has(key));
          if (remove && product?.id) removedProductIds.add(String(product.id));
          return !remove;
        });
        if (remainingProducts.length === products.length) continue;
        if (next3dScenesByView === scenesByView) next3dScenesByView = { ...(scenesByView as any) };
        const remainingPrintAreas = (Array.isArray(scene?.printAreas) ? scene.printAreas : []).filter((area: any) => {
          const productId = String(area?.metadata?.asukaProductId ?? '').trim();
          return !productId || !removedProductIds.has(productId);
        });
        const remainingDecorations = (Array.isArray(scene?.decorations) ? scene.decorations : []).filter((decoration: any) => {
          const productId = String(decoration?.metadata?.asukaProductId ?? '').trim();
          return !productId || !removedProductIds.has(productId);
        });
        const selectedProductId = remainingProducts.some((product: any) => String(product?.id ?? '') === String(scene?.selectedProductId ?? ''))
          ? scene.selectedProductId
          : (String(remainingProducts[remainingProducts.length - 1]?.id ?? '').trim() || null);
        (next3dScenesByView as any)[viewId] = {
          ...scene,
          products: remainingProducts,
          selectedProductId,
          selectedPartId: selectedProductId ? scene?.selectedPartId ?? null : null,
          printAreas: remainingPrintAreas,
          activePrintAreaId: remainingPrintAreas.some((area: any) => String(area?.id ?? '') === String(scene?.activePrintAreaId ?? ''))
            ? scene.activePrintAreaId
            : (String(remainingPrintAreas[0]?.id ?? '').trim() || null),
          decorations: remainingDecorations,
        };
        if (next3dHistoryByView === historyByView) next3dHistoryByView = { ...(historyByView as any) };
        delete (next3dHistoryByView as any)[viewId];
      }

      return {
        elements: removeUnusedElementsFromCatalog(list, deleteIdentifiers, nextByView),
        elementSelection: s?.elementSelection && deleteIdentifiers.has(String((s.elementSelection as any)?.name ?? '').trim()) ? null : s?.elementSelection,
        documentObjectsByView: nextByView,
        document3dScenesByView: next3dScenesByView,
        document3dHistoryByView: next3dHistoryByView,
      };
    });

    try {
      removeElementInstancesFromSnapshots(Array.from(deleteIdentifiers), Array.from(removedObjectIds), activeViewId);
    } catch {}

    try {
      const inst = (get() as any)?.designerInstance as any;
      const canvas = inst?.getCanvas?.();
      if (canvas?.getObjects) {
        const idSet = new Set(removedObjectIds);
        const related = (canvas.getObjects?.() ?? []).filter((o: any) => objectMatchesElement(o, deleteIdentifiers, idSet));
        if (related.length > 0) {
          const activeObject = canvas.getActiveObject?.();
          if (activeObject && objectMatchesElement(activeObject, deleteIdentifiers, idSet)) {
            try { canvas.discardActiveObject?.(); } catch {}
          }
          for (const obj of related) {
            try {
              obj.visible = false;
              obj.evented = false;
              obj.selectable = false;
              obj.excludeFromExport = true;
              obj.setCoords?.();
            } catch {}
            try { canvas.remove?.(obj); } catch {}
          }
          try { canvas.requestRenderAll?.(); } catch {}
        }
      }

      try { inst?.__asukaScheduleHistorySnapshot?.({ force: true }); } catch {}
      // Safety net: if a live Fabric object did not carry the expected metadata,
      // reload the active view from the already-filtered document state/snapshots.
      try { if (inst && activeViewId) switchViewWithCanvas(inst, activeViewId); } catch {}
    } catch {}
  },

  setElementActiveVariation: (identifier, variationId) => {
    if (isDeniedFromStore(get, 'canViewElements', { action: 'store.setElementActiveVariation', identifier, variationId })) return;
    set((s: any) => {
      const list: AsukaElement[] = Array.isArray(s.elements) ? s.elements : [];
      const wanted = String(identifier ?? '').trim();
      const targetVariationId = String(variationId ?? '').trim();
      const markActiveDeep = (variations: any[]): any[] => (Array.isArray(variations) ? variations : []).map((variation: any) => ({
        ...(variation ?? {}),
        active: String((variation as any)?.id ?? '').trim() === targetVariationId,
        variations: markActiveDeep(Array.isArray((variation as any)?.variations) ? (variation as any).variations : []),
      }));
      const next = list.map((e) => {
        if (String((e as any)?.id ?? '').trim() !== wanted && String((e as any)?.name ?? '').trim() !== wanted) return e;
        return { ...e, variations: markActiveDeep((e as any)?.variations ?? []) };
      });
      return { elements: next };
    });
  },

  setProductPartActiveVariation: (identifier, partId, variationId) => {
    if (isDeniedFromStore(get, 'canViewElements', { action: 'store.setProductPartActiveVariation', identifier, partId, variationId })) return;
    set((s: any) => {
      const list: AsukaElement[] = Array.isArray(s.elements) ? s.elements : [];
      const wanted = String(identifier ?? '').trim();
      const targetPartId = String(partId ?? '').trim();
      const targetVariationId = String(variationId ?? '').trim();
      const isPartGroup = (variation: any): boolean => Boolean(
        variation?.metadata?.asukaProduct3DPartGroup === true
        || variation?.product3d?.partGroup === true,
      );
      const getPartId = (variation: any): string => String(
        variation?.product3d?.partId
        ?? variation?.metadata?.asukaProduct3DPartId
        ?? variation?.metadata?.partId
        ?? '',
      ).trim();
      const samePart = (a: string, b: string): boolean => {
        const left = String(a ?? '').trim().toLowerCase();
        const right = String(b ?? '').trim().toLowerCase();
        return !!left && !!right && (left === right || left.includes(right) || right.includes(left));
      };
      let changed = false;
      const markPartActiveDeep = (variations: any[]): any[] => (Array.isArray(variations) ? variations : []).map((variation: any) => {
        if (!variation || typeof variation !== 'object') return variation;
        if (isPartGroup(variation) && samePart(getPartId(variation), targetPartId)) {
          const children = Array.isArray(variation.variations) ? variation.variations : [];
          let groupChanged = variation.active !== false;
          const nextChildren = children.map((child: any) => {
            const nextActive = String(child?.id ?? '').trim() === targetVariationId;
            if (child?.active !== nextActive) groupChanged = true;
            return groupChanged
              ? { ...(child ?? {}), active: nextActive, variations: Array.isArray(child?.variations) ? child.variations : [] }
              : child;
          });
          if (!groupChanged) return variation;
          changed = true;
          return {
            ...variation,
            active: false,
            variations: nextChildren,
          };
        }
        const children = Array.isArray(variation.variations) ? variation.variations : [];
        if (!children.length) return variation;
        const nextChildren = markPartActiveDeep(children);
        return nextChildren === children ? variation : { ...variation, variations: nextChildren };
      });
      const next = list.map((e) => {
        if (String((e as any)?.id ?? '').trim() !== wanted && String((e as any)?.name ?? '').trim() !== wanted) return e;
        const variations = (e as any)?.variations ?? [];
        const changedBeforeGroups = changed;
        const groupedVariations = markPartActiveDeep(variations);
        if (changed !== changedBeforeGroups) return { ...e, variations: groupedVariations };

        // Single-mesh products deliberately keep a flat variation tree. When the
        // sole logical part is targeted, update that normal tree instead of
        // requiring a synthetic part-group/sub-variation wrapper.
        const parts = Array.isArray((e as any)?.product3d?.parts) ? (e as any).product3d.parts : [];
        const onlyPart = parts.length === 1 ? parts[0] : null;
        if (!onlyPart || !samePart(String(onlyPart?.id ?? onlyPart?.materialTarget ?? ''), targetPartId)) return e;
        let flatChanged = false;
        const markFlatActiveDeep = (items: any[]): any[] => (Array.isArray(items) ? items : []).map((variation: any) => {
          if (!variation || typeof variation !== 'object') return variation;
          const children = Array.isArray(variation.variations) ? variation.variations : [];
          const nextChildren = children.length ? markFlatActiveDeep(children) : children;
          const nextActive = children.length === 0 && String(variation?.id ?? '').trim() === targetVariationId;
          if (variation?.active !== nextActive || nextChildren !== children) flatChanged = true;
          return {
            ...variation,
            active: nextActive,
            variations: nextChildren,
          };
        });
        const nextVariations = markFlatActiveDeep(variations);
        if (!flatChanged) return e;
        changed = true;
        return { ...e, variations: nextVariations };
      });
      return changed ? { elements: next } : s;
    });
  },

  applyElementVariation: async (args: AsukaElementVariationApplyInput) => {
    if (isDeniedFromStore(get, 'canViewElements', { action: 'store.applyElementVariation', args })) return false;
    const designer = (get() as any).designerInstance as any;
    if (!designer?.applyElementVariation) return false;
    // Keep catalog state in sync when user applies from UI (document scope).
    if (args.scope === 'document') {
      if (args.partId) (get() as any).setProductPartActiveVariation?.(args.elementId || args.name || '', args.partId, args.variationId);
      else (get() as any).setElementActiveVariation?.(args.elementId || args.name || '', args.variationId);
    }
    return await designer.applyElementVariation(args);
  },


  // Printing methods / colors (global, editable)
  printingMethods: DEFAULT_PRINTING_METHODS,
  setPrintingMethods: (methods) => set({ printingMethods: Array.isArray(methods) ? methods : [] }),

  // Colors list (global, editable)
  colors: [],
  setColors: (colors) => {
    if (isDeniedFromStore(get, 'canEditColors', { action: 'store.setColors', colors })) return;
    set({ colors: Array.isArray(colors) ? colors : [] });
  },

  // Pricing
  price: 0,
  setPrice: (price) => {
    if (get().designerInstance && isDeniedFromStore(get, 'canEditPricingBasePrice', { action: 'store.setPrice' })) return;
    set({ price: normalizePriceInput(price) ?? 0 });
  },

  pricingRules: [],
  setPricingRules: (rules) => {
    if (get().designerInstance && isDeniedFromStore(get, 'canEditPricingRules', { action: 'store.setPricingRules' })) return;
    set({ pricingRules: Array.isArray(rules) ? rules : [] });
  },

  addPricingRule: (rule) => {
    if (isDeniedFromStore(get, 'canEditPricingRules', { action: 'store.addPricingRule' })) return '';
    if (isDeniedFromStore(get, 'canAddPricingRule', { action: 'store.addPricingRule' })) return '';
    const id = String((rule as any)?.id ?? `pricing-${Math.random().toString(36).slice(2)}-${Date.now()}`);
    const nextRule = {
      enabled: true,
      handler: 'layerSize',
      scope: 'all',
      match: 'all',
      conditionAggregation: 'sum',
      target: { types: [], view: [] },
      conditions: [],
      ...(rule ?? {}),
      id,
    };
    set((s: any) => ({ pricingRules: [...(s.pricingRules ?? []), nextRule] }));
    return id;
  },

  updatePricingRule: (id, patch) => {
    if (isDeniedFromStore(get, 'canEditPricingRules', { action: 'store.updatePricingRule', id })) return;
    set((s: any) => ({
      pricingRules: (Array.isArray(s.pricingRules) ? s.pricingRules : []).map((r: any) =>
        String(r?.id) === String(id) ? { ...r, ...(patch ?? {}) } : r,
      ),
    }));
  },

  removePricingRule: (id) => {
    if (isDeniedFromStore(get, 'canEditPricingRules', { action: 'store.removePricingRule', id })) return;
    if (isDeniedFromStore(get, 'canRemovePricingRule', { action: 'store.removePricingRule', id })) return;
    set((s: any) => ({
      pricingRules: (Array.isArray(s.pricingRules) ? s.pricingRules : []).filter((r: any) => String(r?.id) !== String(id)),
    }));
  },

  duplicatePricingRule: (id) => {
    if (isDeniedFromStore(get, 'canEditPricingRules', { action: 'store.duplicatePricingRule', id })) return '';
    if (isDeniedFromStore(get, 'canAddPricingRule', { action: 'store.duplicatePricingRule', id })) return '';
    const rules = (get() as any).pricingRules ?? [];
    const idx = rules.findIndex((r: any) => String(r?.id) === String(id));
    if (idx < 0) return '';
    const base = rules[idx];
    const newId = `pricing-${Math.random().toString(36).slice(2)}-${Date.now()}`;
    const copy = { ...base, id: newId, label: base?.label ? `${base.label} (copy)` : '' };
    const next = [...rules];
    next.splice(idx + 1, 0, copy);
    set({ pricingRules: next });
    return newId;
  },

  movePricingRule: (id, delta) => {
    if (isDeniedFromStore(get, 'canEditPricingRules', { action: 'store.movePricingRule', id, delta })) return;
    const rules = (get() as any).pricingRules ?? [];
    const idx = rules.findIndex((r: any) => String(r?.id) === String(id));
    if (idx < 0) return;
    const nextIdx = idx + (delta ?? 0);
    if (nextIdx < 0 || nextIdx >= rules.length) return;
    const next = [...rules];
    const [item] = next.splice(idx, 1);
    next.splice(nextIdx, 0, item);
    set({ pricingRules: next });
  },

  togglePricingRule: (id) => {
    if (isDeniedFromStore(get, 'canEditPricingRules', { action: 'store.togglePricingRule', id })) return;
    set((s: any) => ({
      pricingRules: (Array.isArray(s.pricingRules) ? s.pricingRules : []).map((r: any) =>
        String(r?.id) === String(id) ? { ...r, enabled: r?.enabled === false } : r,
      ),
    }));
  },

  pricingTotals: { base: 0, standard: 0, rules: 0, total: 0, updatedAt: 0 },
  setPricingTotals: (totals) =>
    set((s: any) => ({
      pricingTotals: {
        ...(s.pricingTotals ?? {}),
        ...(totals ?? {}),
        updatedAt: typeof (totals as any)?.updatedAt === 'number' ? (totals as any).updatedAt : Date.now(),
      },
    })),

  // Global color scope
  colorScope: 'object',
  setColorScope: (scope) => set({ colorScope: scope }),

  // Integrations
  integrations: {},
  setIntegrations: (patch) => set((s: any) => ({ integrations: { ...(s.integrations ?? {}), ...(patch ?? {}) } })),

  // Boot UI
  bootReady: false,
  bootStatus: 'Preparing editor…',
  setBootReady: (ready: boolean) => set({ bootReady: !!ready }),
  setBootStatus: (status: string) => set({ bootStatus: typeof status === 'string' && status.trim() ? status.trim() : 'Preparing editor…' }),

  // License
  license: { ...DEFAULT_LICENSE },
  licenseResolution: { ...DEFAULT_LICENSE_RESOLUTION },
  licenseToken: null,
  setLicenseState: (patch) =>
    set((s: any) => ({
      license: { ...(s.license ?? DEFAULT_LICENSE), ...((patch as any)?.license ?? {}) },
      licenseResolution: { ...(s.licenseResolution ?? DEFAULT_LICENSE_RESOLUTION), ...((patch as any)?.resolution ?? {}) },
      licenseToken: typeof (patch as any)?.token === 'undefined' ? (s.licenseToken ?? null) : ((patch as any)?.token ?? null),
    })),
  canUseLicenseFeature: (feature) => canUseFeature((get() as any).license ?? DEFAULT_LICENSE, feature),

  // Module config
  moduleConfig: {
    upload: { multiple: false, autoAddOnSingle: true },
    designs: { items: [] },
  },
  setModuleConfig: (patch) => set((s: any) => {
    const previous = s.moduleConfig ?? {};
    const normalized = normalizeModuleConfigPatch(patch) ?? {};
    return {
      moduleConfig: {
        ...previous,
        ...normalized,
        ...(normalized.upload
          ? { upload: { ...(previous.upload ?? {}), ...normalized.upload } }
          : null),
      },
    };
  }),

  // Uploaded assets shared by Upload module, canvas uploads and media inputs
  uploadedAssets: [],
  registerUploadedAsset: (input) => {
    const now = Date.now();
    const file: any = (input as any)?.file ?? null;
    const fileSignature = getUploadFileSignature(file) || normalizeUploadSource((input as any)?.fileSignature) || undefined;
    const inputUrl = normalizeUploadSource((input as any)?.url);
    const inputPreviewUrl = normalizeUploadSource((input as any)?.previewUrl);
    const shouldCreatePreviewFromFile = !!file && (!inputPreviewUrl || isBlobUrl(inputUrl));
    const createdPreviewUrl = shouldCreatePreviewFromFile ? URL.createObjectURL(file) : undefined;
    const previewUrl = inputPreviewUrl ?? createdPreviewUrl;
    const url = inputUrl && !isBlobUrl(inputUrl) ? inputUrl : (previewUrl ?? inputUrl ?? '');
    const name = normalizeUploadSource((input as any)?.name) ?? normalizeUploadSource(file?.name) ?? 'Uploaded file';
    const mimeType = normalizeUploadSource((input as any)?.mimeType) ?? normalizeUploadSource(file?.type);
    const size = Number.isFinite(Number((input as any)?.size)) ? Number((input as any).size) : (Number.isFinite(Number(file?.size)) ? Number(file.size) : undefined);
    const lastModified = Number.isFinite(Number((input as any)?.lastModified)) ? Number((input as any).lastModified) : (Number.isFinite(Number(file?.lastModified)) ? Number(file.lastModified) : undefined);
    const source = normalizeUploadSource((input as any)?.source);
    const mode = normalizeUploadSource((input as any)?.mode);
    const strategy = normalizeUploadSource((input as any)?.strategy);
    const status = ((input as any)?.status ?? 'ready') as UploadedAssetStatus;
    const error = normalizeUploadSource((input as any)?.error);
    const metadata = (input as any)?.metadata;
    let registered: UploadedAsset | undefined;

    set((s: any) => {
      const prev: UploadedAsset[] = Array.isArray(s.uploadedAssets) ? s.uploadedAssets : [];
      const explicitId = normalizeUploadSource((input as any)?.id);
      const index = prev.findIndex((asset) => {
        if (explicitId && String(asset.id) === explicitId) return true;
        if (fileSignature && asset.fileSignature === fileSignature) return true;
        if (inputUrl && asset.url === inputUrl) return true;
        return false;
      });

      if (index >= 0) {
        const current = prev[index];
        const nextPreviewUrl = previewUrl ?? current.previewUrl;
        if (previewUrl && current.previewUrl && current.previewUrl !== previewUrl) {
          revokeUploadPreviewUrl(current.previewUrl);
        }
        const next: UploadedAsset = {
          ...current,
          name: name ?? current.name,
          url: url || current.url,
          previewUrl: nextPreviewUrl,
          mimeType: mimeType ?? current.mimeType,
          size: size ?? current.size,
          lastModified: lastModified ?? current.lastModified,
          fileSignature: fileSignature ?? current.fileSignature,
          file: file ?? current.file,
          updatedAt: now,
          source: source ?? current.source,
          mode: mode ?? current.mode,
          strategy: strategy ?? current.strategy,
          status,
          error,
          metadata: typeof metadata === 'undefined' ? current.metadata : metadata,
        };
        registered = next;
        const uploadedAssets = prev.slice();
        uploadedAssets[index] = next;
        return { uploadedAssets };
      }

      const next: UploadedAsset = {
        id: explicitId ?? makeUploadedAssetId(),
        name,
        url,
        previewUrl,
        mimeType,
        size,
        lastModified,
        fileSignature,
        file: file ?? undefined,
        createdAt: now,
        updatedAt: now,
        source,
        mode,
        strategy,
        status,
        error,
        metadata,
      };
      registered = next;
      return { uploadedAssets: [next, ...prev] };
    });

    if (!registered) {
      throw new Error('[AsukaDesigner] Failed to register uploaded asset.');
    }

    return registered;
  },
  updateUploadedAsset: (id, patch) => {
    const normalizedId = String(id ?? '').trim();
    if (!normalizedId) return;
    set((s: any) => ({
      uploadedAssets: (Array.isArray(s.uploadedAssets) ? s.uploadedAssets : []).map((asset: UploadedAsset) =>
        asset.id === normalizedId ? { ...asset, ...(patch ?? {}), updatedAt: Date.now() } : asset,
      ),
    }));
  },
  removeUploadedAsset: (id) => {
    const normalizedId = String(id ?? '').trim();
    if (!normalizedId) return;
    set((s: any) => {
      const prev: UploadedAsset[] = Array.isArray(s.uploadedAssets) ? s.uploadedAssets : [];
      const removed = prev.find((asset) => asset.id === normalizedId);
      if (removed) {
        revokeUploadPreviewUrl(removed.previewUrl);
        if (removed.url !== removed.previewUrl) revokeUploadPreviewUrl(removed.url);
      }
      return { uploadedAssets: prev.filter((asset) => asset.id !== normalizedId) };
    });
  },
  clearUploadedAssets: () => {
    set((s: any) => {
      for (const asset of Array.isArray(s.uploadedAssets) ? s.uploadedAssets : []) {
        revokeUploadPreviewUrl(asset.previewUrl);
        if (asset.url !== asset.previewUrl) revokeUploadPreviewUrl(asset.url);
      }
      return { uploadedAssets: [] };
    });
  },

  // Layout
  role: 'dev',
  permissions: DEFAULT_PERMISSIONS,
  permissionsByView: {},
  setPermissionsForView: (viewId, patch) =>
    set((state: any) => {
      const prev = state.permissionsByView?.[viewId] ?? {};
      return { permissionsByView: { ...(state.permissionsByView ?? {}), [viewId]: mergePermissions(prev, patch) } };
    }),
  clearPermissionsForView: (viewId) =>
    set((state: any) => {
      const next = { ...(state.permissionsByView ?? {}) };
      delete (next as any)[viewId];
      return { permissionsByView: next };
    }),

  setRole: (role) =>
    set((state: any) => ({ role, permissions: { ...permissionsForRole(role), moduleOpen: state.permissions?.moduleOpen } })),
  setPermissions: (patch) => set((state: any) => ({ permissions: mergePermissions(state.permissions, patch) })),

  blocksByZone: normalizeBlocksByZone(initialBlocksByZone),
  zoneCollapsed: { top: true, bottom: false, left: false, right: false },
  zoneCollapsible: { top: false, bottom: false, left: true, right: true },
  // Default UI when no instantiation config is provided:
  // - right is "panels" (dock-like properties sidebar)
  // - others stay "default" (simple lists / bars)
  zoneType: { top: 'default', bottom: 'default', left: 'default', right: 'panels' },

  // Global UI behavior
  uiConfig: {
    ...DEFAULT_ASUKA_UI_OPTIONS,
    viewsPagination: { ...DEFAULT_ASUKA_UI_OPTIONS.viewsPagination },
    historyControls: { ...DEFAULT_ASUKA_UI_OPTIONS.historyControls },
  },
  setUiConfig: (config) => set({ uiConfig: normalizeAsukaUiOptions(config) }),

  // Responsive layout
  responsiveConfig: { ...DEFAULT_RESPONSIVE_CONFIG },
  responsiveBreakpoint: 'desktop',
  responsiveWidth: 0,
  setResponsiveConfig: (config) =>
    set((state: any) => {
      const nextConfig = normalizeResponsiveConfig({ ...(state.responsiveConfig ?? DEFAULT_RESPONSIVE_CONFIG), ...(config ?? {}) });
      const nextBreakpoint = getResponsiveBreakpoint(Number(state.responsiveWidth ?? 0), nextConfig);
      return { responsiveConfig: nextConfig, responsiveBreakpoint: nextBreakpoint };
    }),
  setResponsiveMetrics: (width) =>
    set((state: any) => {
      const nextWidth = Math.max(0, Math.floor(Number(width) || 0));
      const nextConfig = normalizeResponsiveConfig(state.responsiveConfig ?? DEFAULT_RESPONSIVE_CONFIG);
      const nextBreakpoint = getResponsiveBreakpoint(nextWidth, nextConfig);
      if (state.responsiveWidth === nextWidth && state.responsiveBreakpoint === nextBreakpoint) return state;
      return { responsiveWidth: nextWidth, responsiveBreakpoint: nextBreakpoint };
    }),

  // Modules
  activeModuleId: null,
  moduleDisplayModeOverrides: {},
  moduleMountTargetOverrides: {},
  mountedModules: {},
  hiddenModules: {},
  exitingModules: {},

  // Overlay stack (F8)
  moduleOverlayStack: [],
  moduleOverlayType: 'floating',
  moduleOverlayAnchorBox: null,
  pendingRestoreSelectionMenu: false,

  // Used by the empty-state CTA (no views) to reuse the existing Views "Create" modal.
  pendingOpenCreateViewModal: false,
  requestOpenCreateViewModal: () => set(() => ({ pendingOpenCreateViewModal: true } as any)),
  clearPendingOpenCreateViewModal: () => set(() => ({ pendingOpenCreateViewModal: false } as any)),

  // Drag & drop
  draggedBlockId: null,

  // Canvas / Designer
  designerInstance: null,
  defaultRenderEngineKind: 'fabric',
  activeRenderEngineKind: 'fabric',
  renderEngineOptionsByKind: {},
  aiRuntime: null,
  exportRuntime: null,

  // Vues
  views: initialViews,
  activeViewId: initialViews[0]?.id ?? null,
  documentViews: initialViews,
  documentDrawingAreas: [],
  documentViewportByView: initialViews[0]?.id ? { [String(initialViews[0].id)]: { zoom: 1, panX: 0, panY: 0 } } : {},
  documentSelection: { activeObjectIds: [], activeDrawingAreaId: null },
  documentObjectsByView: {},
  document3dScenesByView: {},
  document3dHistoryByView: {},
  documentFabricHistoryByView: {},
  documentHistoryInfo: { index: -1, length: 0, canUndo: false, canRedo: false, maxSnapshots: DEFAULT_HISTORY_MAX_SNAPSHOTS },
  documentHistoryInfoByView: {},
  setDocumentViews: (views) => set({ documentViews: Array.isArray(views) ? views : [] }),
  setDocumentDrawingAreas: (zones) => set({ documentDrawingAreas: Array.isArray(zones) ? zones : [] }),
  setDocumentViewportForView: (viewId, viewport) => set((s: any) => ({
    documentViewportByView: {
      ...(s.documentViewportByView ?? {}),
      [String(viewId ?? '')]: {
        zoom: Number(viewport?.zoom ?? s.documentViewportByView?.[String(viewId ?? '')]?.zoom ?? 1) || 1,
        panX: Number(viewport?.panX ?? s.documentViewportByView?.[String(viewId ?? '')]?.panX ?? 0) || 0,
        panY: Number(viewport?.panY ?? s.documentViewportByView?.[String(viewId ?? '')]?.panY ?? 0) || 0,
      },
    },
  })),
  setDocumentSelection: (selection) => set((s: any) => ({
    documentSelection: {
      ...(s.documentSelection ?? { activeObjectIds: [], activeDrawingAreaId: null }),
      ...(selection ?? {}),
      activeObjectIds: Array.isArray(selection?.activeObjectIds)
        ? selection.activeObjectIds.map((id: any) => String(id)).filter(Boolean)
        : (s.documentSelection?.activeObjectIds ?? []),
      activeDrawingAreaId: selection && Object.prototype.hasOwnProperty.call(selection, 'activeDrawingAreaId')
        ? ((selection as any).activeDrawingAreaId ?? null)
        : (s.documentSelection?.activeDrawingAreaId ?? null),
    },
  })),
  setDocumentObjectsForView: (viewId, objects) => set((s: any) => {
    const perfStart = typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();
    const key = String(viewId ?? '');
    const list = Array.isArray(objects) ? objects : [];
    const next = {
      ...(s.documentObjectsByView ?? {}),
      [key]: list,
    };
    asuka2DPerfDebug('store.fabric.objects.setForView', {
      viewId: key,
      durationMs: Math.round(((((typeof performance !== 'undefined' && typeof performance.now === 'function') ? performance.now() : Date.now()) - perfStart)) * 100) / 100,
      objectCount: list.length,
      payloadBytes: getAsukaDebugPayloadSize(list),
      views: Object.keys(next).length,
    });
    return { documentObjectsByView: next } as any;
  }),
  setDocument3dSceneForView: (viewId: string, scene: Asuka3DSceneState | null | undefined) => set((s: any) => {
    const key = String(viewId ?? '').trim();
    asukaHistoryDebug('store.setDocument3dSceneForView', {
      key,
      activeViewId: s?.activeViewId ?? null,
      hasScene: Boolean(scene && typeof scene === 'object'),
      decorations: Array.isArray((scene as any)?.decorations) ? (scene as any).decorations.length : null,
      printAreas: Array.isArray((scene as any)?.printAreas) ? (scene as any).printAreas.length : null,
    });
    if (!key) return s;
    const next = { ...(s.document3dScenesByView ?? {}) } as Record<string, Asuka3DSceneState>;
    if (!scene || typeof scene !== 'object') delete next[key];
    else next[key] = compact3DSceneForStore(scene) as Asuka3DSceneState;
    return { document3dScenesByView: next } as any;
  }),
  setDocument3dCameraForView: (viewId: string, camera: Asuka3DSceneState['camera'] | null | undefined) => set((s: any) => {
    const key = String(viewId ?? '').trim();
    if (!key || !camera || typeof camera !== 'object') return s;

    const storedScene = s.document3dScenesByView?.[key];
    const viewScene = (s.views ?? []).find((view: any) => String(view?.id ?? '').trim() === key)?.scene3d;
    const previousScene = storedScene && typeof storedScene === 'object'
      ? storedScene
      : (viewScene && typeof viewScene === 'object' ? viewScene : { products: [] });
    const nextCamera = {
      ...(previousScene.camera && typeof previousScene.camera === 'object' ? previousScene.camera : {}),
      ...camera,
    };

    const previousSignature = JSON.stringify(previousScene.camera ?? null);
    const nextSignature = JSON.stringify(nextCamera);
    if (previousSignature === nextSignature) return s;

    const nextScenes = { ...(s.document3dScenesByView ?? {}) } as Record<string, Asuka3DSceneState>;
    nextScenes[key] = compact3DSceneForStore({
      ...previousScene,
      camera: nextCamera,
    }) as Asuka3DSceneState;
    return { document3dScenesByView: nextScenes } as any;
  }),
  setDocument3dHistoryForView: (viewId: string, history: Asuka3DHistoryTrackState | null | undefined) => set((s: any) => {
    const key = String(viewId ?? '').trim();
    asukaHistoryDebug('store.setDocument3dHistoryForView.start', {
      key,
      activeViewId: s?.activeViewId ?? null,
      activeKind: getActiveViewEngineKind(s.views ?? [], String(s.activeViewId ?? '').trim(), s.defaultRenderEngineKind ?? 'fabric'),
      incoming: summarizeHistoryTrack(history),
      previousForView: summarizeHistoryTrack((s.document3dHistoryByView ?? {})[key]),
      currentDocumentHistoryInfo: summarizeHistoryInfo(s.documentHistoryInfo),
    });
    if (!key) return s;
    const next = { ...(s.document3dHistoryByView ?? {}) } as Record<string, Asuka3DHistoryTrackState>;
    if (!history || typeof history !== 'object' || !Array.isArray((history as any).snapshots) || !(history as any).snapshots.length) {
      delete next[key];
    } else {
      const normalizedTrack = trimHistorySnapshots(((history as any).snapshots ?? []).filter((snapshot: any) => snapshot && typeof snapshot === 'object').map(stripHeavy3DModelsFromHistorySnapshot), (history as any).index, Number((history as any).maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS);
      const snapshots = normalizedTrack.snapshots;
      const index = normalizedTrack.index;
      const previous = (s.document3dHistoryByView ?? {})[key] as any;
      const incomingSignature = String((history as any).lastSignature ?? '');
      const previousSignature = String(previous?.lastSignature ?? '');
      const incomingAssetsSignature = get3DHistoryAssetsSignature((history as any)?.assets);
      const previousAssetsSignature = get3DHistoryAssetsSignature(previous?.assets);
      const previousSnapshots = Array.isArray(previous?.snapshots) ? previous.snapshots : [];
      if (
        previous &&
        incomingSignature &&
        previousSignature === incomingSignature &&
        incomingAssetsSignature === previousAssetsSignature &&
        Number(previous?.index ?? -1) === index &&
        previousSnapshots.length === snapshots.length
      ) {
        asukaHistoryDebug('store.setDocument3dHistoryForView.skip.sameTrack', {
          key,
          index,
          length: snapshots.length,
        });
        return s;
      }
      next[key] = {
        ...(history as any),
        engine: '3d',
        scope: 'view',
        viewId: key,
        snapshots,
        index,
      } as Asuka3DHistoryTrackState;
    }
    const activeViewId = String(s.activeViewId ?? '').trim();
    const activeKind = getActiveViewEngineKind(s.views ?? [], activeViewId, s.defaultRenderEngineKind ?? 'fabric');
    const activeTrack = activeKind === '3d' && activeViewId === key ? next[key] : null;
    const activeInfo = activeKind === '3d' && activeViewId === key
      ? (activeTrack && Array.isArray((activeTrack as any).snapshots)
        ? {
          engine: '3d',
          scope: 'view',
          viewId: key,
          index: Number((activeTrack as any).index ?? -1),
          length: Number((activeTrack as any).snapshots.length ?? 0),
          canUndo: Number((activeTrack as any).index ?? -1) > 0,
          canRedo: Number((activeTrack as any).index ?? -1) >= 0 && Number((activeTrack as any).index ?? -1) < Number((activeTrack as any).snapshots.length ?? 0) - 1,
          maxSnapshots: Number((activeTrack as any).maxSnapshots ?? s.documentHistoryInfo?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS,
        }
        : {
          engine: '3d',
          scope: 'view',
          viewId: key,
          index: -1,
          length: 0,
          canUndo: false,
          canRedo: false,
          maxSnapshots: Number(s.documentHistoryInfo?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS,
        })
      : null;
    const result = {
      document3dHistoryByView: next,
      ...(activeInfo ? { documentHistoryInfo: { ...(s.documentHistoryInfo ?? {}), ...activeInfo } } : null),
    } as any;
    asukaHistoryDebug('store.setDocument3dHistoryForView.result', {
      key,
      saved: summarizeHistoryTrack(next[key]),
      activeInfo: summarizeHistoryInfo(activeInfo),
      willUpdateDocumentHistoryInfo: Boolean(activeInfo),
    });
    return result;
  }),
  setDocumentFabricHistoryForView: (viewId: string, history: Asuka3DHistoryTrackState | null | undefined) => set((s: any) => {
    const perfStart = typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();
    const requestedKey = String(viewId ?? '').trim();
    const incomingScope = String((history as any)?.scope ?? '').trim();
    const incomingViewId = String((history as any)?.viewId ?? '').trim();
    const key = (requestedKey === DOCUMENT_FABRIC_HISTORY_KEY || incomingViewId === DOCUMENT_FABRIC_HISTORY_KEY || incomingScope === 'document')
      ? DOCUMENT_FABRIC_HISTORY_KEY
      : requestedKey;
    asukaHistoryDebug('store.setDocumentFabricHistoryForView.start', {
      key,
      activeViewId: s?.activeViewId ?? null,
      activeKind: getActiveViewEngineKind(s.views ?? [], String(s.activeViewId ?? '').trim(), s.defaultRenderEngineKind ?? 'fabric'),
      incoming: summarizeHistoryTrack(history),
      previousForView: summarizeHistoryTrack((s.documentFabricHistoryByView ?? {})[key]),
      currentDocumentHistoryInfo: summarizeHistoryInfo(s.documentHistoryInfo),
    });
    if (!key) return s;
    const previousTrack = (s.documentFabricHistoryByView ?? {})[key] as any;
    const previousSnapshots = Array.isArray(previousTrack?.snapshots) ? previousTrack.snapshots : [];
    const incomingSnapshots = Array.isArray((history as any)?.snapshots)
      ? ((history as any).snapshots ?? [])
        .filter((snapshot: any) => snapshot && typeof snapshot === 'object')
        .map(strip3DScenesFromFabricHistorySnapshot)
      : [];
    const previousLength = previousSnapshots.length;
    const incomingLength = incomingSnapshots.length;

    // Late canvas rebuilds / baseline captures can arrive just after a view switch
    // with a one-snapshot track. Never let that shorter baseline overwrite a real
    // per-view history stack. Undo/redo/reset keep the same snapshot length and
    // only move the index, so this guard does not block normal history actions.
    const isHistoryLimitTrim = previousLength > DEFAULT_HISTORY_MAX_SNAPSHOTS && incomingLength > 0 && incomingLength <= DEFAULT_HISTORY_MAX_SNAPSHOTS;
    if (!isHistoryLimitTrim && previousLength > 1 && incomingLength > 0 && incomingLength < previousLength && (history as any)?.__asukaAllowHistoryShrink !== true) {
      asukaHistoryDebug('store.setDocumentFabricHistoryForView.ignored.shorterIncomingTrack', {
        key,
        previous: summarizeHistoryTrack(previousTrack),
        incoming: summarizeHistoryTrack(history),
      });
      return s;
    }
    if (previousLength > 0 && incomingLength === 0 && (history as any)?.__asukaAllowHistoryClear !== true) {
      asukaHistoryDebug('store.setDocumentFabricHistoryForView.ignored.emptyIncomingTrack', {
        key,
        previous: summarizeHistoryTrack(previousTrack),
        incoming: summarizeHistoryTrack(history),
      });
      return s;
    }

    const next = { ...(s.documentFabricHistoryByView ?? {}) } as Record<string, Asuka3DHistoryTrackState>;
    if (!history || typeof history !== 'object' || !incomingLength) {
      delete next[key];
    } else {
      const normalizedTrack = trimHistorySnapshots(incomingSnapshots, (history as any).index, Number((history as any).maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS);
      const snapshots = normalizedTrack.snapshots;
      const index = normalizedTrack.index;
      next[key] = {
        ...(history as any),
        engine: 'fabric',
        scope: (key === DOCUMENT_FABRIC_HISTORY_KEY || String((history as any)?.scope ?? '').trim() === 'document') ? 'document' : 'view',
        viewId: (key === DOCUMENT_FABRIC_HISTORY_KEY || String((history as any)?.scope ?? '').trim() === 'document') ? DOCUMENT_FABRIC_HISTORY_KEY : key,
        snapshots,
        index,
      } as Asuka3DHistoryTrackState;
    }
    const activeViewId = String(s.activeViewId ?? '').trim();
    const activeKind = getActiveViewEngineKind(s.views ?? [], activeViewId, s.defaultRenderEngineKind ?? 'fabric');
    const isDocumentFabricHistory = key === DOCUMENT_FABRIC_HISTORY_KEY || String((history as any)?.scope ?? '').trim() === 'document';
    const activeTrack = activeKind !== '3d' && (activeViewId === key || isDocumentFabricHistory) ? next[key] : null;
    const activeInfo = activeKind !== '3d' && (activeViewId === key || isDocumentFabricHistory)
      ? (activeTrack && Array.isArray((activeTrack as any).snapshots)
        ? {
          engine: 'fabric',
          scope: isDocumentFabricHistory ? 'document' : 'view',
          viewId: isDocumentFabricHistory ? DOCUMENT_FABRIC_HISTORY_KEY : key,
          index: Number((activeTrack as any).index ?? -1),
          length: Number((activeTrack as any).snapshots.length ?? 0),
          canUndo: Number((activeTrack as any).index ?? -1) > 0,
          canRedo: Number((activeTrack as any).index ?? -1) >= 0 && Number((activeTrack as any).index ?? -1) < Number((activeTrack as any).snapshots.length ?? 0) - 1,
          maxSnapshots: Number((activeTrack as any).maxSnapshots ?? s.documentHistoryInfo?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS,
        }
        : {
          engine: 'fabric',
          scope: isDocumentFabricHistory ? 'document' : 'view',
          viewId: isDocumentFabricHistory ? DOCUMENT_FABRIC_HISTORY_KEY : key,
          index: -1,
          length: 0,
          canUndo: false,
          canRedo: false,
          maxSnapshots: Number(s.documentHistoryInfo?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS,
        })
      : null;
    const nextInfoByView = activeInfo
      ? { ...(s.documentHistoryInfoByView ?? {}), [key]: activeInfo }
      : (s.documentHistoryInfoByView ?? {});
    const result = {
      documentFabricHistoryByView: next,
      documentHistoryInfoByView: nextInfoByView,
      ...(activeInfo ? { documentHistoryInfo: { ...(s.documentHistoryInfo ?? {}), ...activeInfo } } : null),
    } as any;
    asukaHistoryDebug('store.setDocumentFabricHistoryForView.result', {
      key,
      saved: summarizeHistoryTrack(next[key]),
      activeInfo: summarizeHistoryInfo(activeInfo),
      willUpdateDocumentHistoryInfo: Boolean(activeInfo),
    });
    asuka2DPerfDebug('store.fabric.history.setForView', {
      key,
      durationMs: Math.round(((((typeof performance !== 'undefined' && typeof performance.now === 'function') ? performance.now() : Date.now()) - perfStart)) * 100) / 100,
      saved: summarizeHistoryTrack(next[key]),
      activeInfo: summarizeHistoryInfo(activeInfo),
      payloadBytes: getAsukaDebugPayloadSize(next[key]),
      mapSize: Object.keys(next).length,
    });
    return result;
  }),
  setDocumentHistoryInfo: (info) => set((s: any) => {
    const activeViewId = String(s.activeViewId ?? '').trim();
    const infoViewId = String((info as any)?.viewId ?? '').trim();
    const activeKind = getActiveViewEngineKind(s.views ?? [], activeViewId, s.defaultRenderEngineKind ?? 'fabric');
    const infoEngine = String((info as any)?.engine ?? '').trim().toLowerCase();
    const isDocumentHistoryInfo = infoViewId === DOCUMENT_FABRIC_HISTORY_KEY || String((info as any)?.scope ?? '').trim() === 'document';
    const scopedToView = Boolean(!isDocumentHistoryInfo && (infoViewId || (info as any)?.scope === 'view'));

    asukaHistoryDebug('store.setDocumentHistoryInfo.start', {
      activeViewId,
      activeKind,
      info: summarizeHistoryInfo(info),
      previous: summarizeHistoryInfo(s.documentHistoryInfo),
      scopedToView,
    });

    // History info is displayed globally, but 3D history is view-scoped. Async
    // exports from the view that is being left can complete after the switch and
    // must never overwrite the currently active view history controls.
    if (scopedToView && infoViewId && activeViewId && infoViewId !== activeViewId) {
      asukaHistoryDebug('store.setDocumentHistoryInfo.ignored.wrongView', { activeViewId, infoViewId, info: summarizeHistoryInfo(info) });
      return s;
    }
    if (scopedToView && infoEngine === '3d' && activeKind !== '3d') {
      asukaHistoryDebug('store.setDocumentHistoryInfo.ignored.3dWhileFabric', { activeViewId, activeKind, info: summarizeHistoryInfo(info) });
      return s;
    }
    if (activeKind === '3d' && infoEngine !== '3d') {
      asukaHistoryDebug('store.setDocumentHistoryInfo.ignored.fabricWhile3d', { activeViewId, activeKind, info: summarizeHistoryInfo(info) });
      return s;
    }

    const nextInfo = {
        ...(s.documentHistoryInfo ?? { index: -1, length: 0, canUndo: false, canRedo: false, maxSnapshots: DEFAULT_HISTORY_MAX_SNAPSHOTS }),
        ...(info ?? {}),
        index: Number(info?.index ?? s.documentHistoryInfo?.index ?? -1),
        length: Number(info?.length ?? s.documentHistoryInfo?.length ?? 0),
        canUndo: Boolean(info?.canUndo ?? s.documentHistoryInfo?.canUndo ?? false),
        canRedo: Boolean(info?.canRedo ?? s.documentHistoryInfo?.canRedo ?? false),
        maxSnapshots: Number(info?.maxSnapshots ?? s.documentHistoryInfo?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS),
      };
    const previousInfo = s.documentHistoryInfo ?? {};
    const sameInfo = String((previousInfo as any)?.engine ?? '') === String((nextInfo as any)?.engine ?? '')
      && String((previousInfo as any)?.viewId ?? '') === String((nextInfo as any)?.viewId ?? '')
      && Number((previousInfo as any)?.index ?? -1) === Number((nextInfo as any)?.index ?? -1)
      && Number((previousInfo as any)?.length ?? 0) === Number((nextInfo as any)?.length ?? 0)
      && Boolean((previousInfo as any)?.canUndo) === Boolean((nextInfo as any)?.canUndo)
      && Boolean((previousInfo as any)?.canRedo) === Boolean((nextInfo as any)?.canRedo)
      && Number((previousInfo as any)?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) === Number((nextInfo as any)?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS);
    const nextInfoByView = infoViewId
      ? { ...(s.documentHistoryInfoByView ?? {}), [infoViewId]: nextInfo }
      : (s.documentHistoryInfoByView ?? {});
    asukaHistoryDebug('store.setDocumentHistoryInfo.result', { activeViewId, infoViewId, next: summarizeHistoryInfo(nextInfo), sameInfo });
    if (sameInfo && !infoViewId) return s;
    return { documentHistoryInfo: sameInfo ? previousInfo : nextInfo, documentHistoryInfoByView: nextInfoByView } as any;
  }),
  viewThumbnails: {},
  setViewThumbnail: (viewId, dataUrl) =>
    set((s: any) => ({
      viewThumbnails: { ...(s.viewThumbnails ?? {}), [String(viewId)]: String(dataUrl ?? '') },
    })),

  // Selection menu (F7+)
  selectionMenuType: 'popover',
  selectionMenuRootType: 'popover',
  selectionMenuItems: ['colors', 'layers', 'history'],
  selectionMenuOpen: true,
  selectionMenuStack: [],
  selectionMenuAnchorBox: null,

  // --- Actions layout ---
  moveBlock: (blockId, targetZone, targetIndex) => {
    const perms = get().permissions ?? DEFAULT_PERMISSIONS;
    if (!perms.canMovePanels) return;

    set((state: any) => {
      const zones: Record<LayoutZoneId, LayoutBlock[]> = {
        top: [...(state.blocksByZone?.top ?? [])].map((block: LayoutBlock) => ({ ...block })),
        right: [...(state.blocksByZone?.right ?? [])].map((block: LayoutBlock) => ({ ...block })),
        bottom: [...(state.blocksByZone?.bottom ?? [])].map((block: LayoutBlock) => ({ ...block })),
        left: [...(state.blocksByZone?.left ?? [])].map((block: LayoutBlock) => ({ ...block })),
      };

      let picked: LayoutBlock | null = null;
      let sourceZone: LayoutZoneId | null = null;

      (Object.keys(zones) as LayoutZoneId[]).forEach((zone) => {
        const index = zones[zone].findIndex((block) => block.id === blockId);
        if (index >= 0) {
          picked = { ...zones[zone][index] };
          sourceZone = zone;
          zones[zone].splice(index, 1);
        }
      });

      if (!picked || !sourceZone) return state;

      const destination = zones[targetZone] ?? [];
      const index =
        typeof targetIndex === 'number'
          ? Math.max(0, Math.min(destination.length, targetIndex))
          : destination.length;

      const pickedBlock = picked as LayoutBlock;
      destination.splice(index, 0, { ...pickedBlock });
      zones[targetZone] = destination;

      (Object.keys(zones) as LayoutZoneId[]).forEach((zone) => {
        zones[zone] = zones[zone]
          .slice()
          .sort((a, b) => {
            const orderDelta = Number(a.order ?? 0) - Number(b.order ?? 0);
            return orderDelta !== 0 ? orderDelta : String(a.id).localeCompare(String(b.id));
          })
          .map((block, order) => ({ ...block, order }));
      });

      // Re-apply target zone order after the generic sort, because the destination array already
      // represents the intended visual order.
      zones[targetZone] = destination.map((block, order) => ({ ...block, order }));

      return { ...state, blocksByZone: zones, draggedBlockId: null };
    });
  },
  setZoneCollapsed: (zone, value) => {
    if (isDeniedFromStore(get, 'canCollapsePanels', { action: 'store.setZoneCollapsed', zone, value })) return;
    set((state: any) => ({
      ...state,
      zoneCollapsed: { ...state.zoneCollapsed, [zone]: value },
    }));
  },

  toggleZoneCollapsed: (zone) => {
    if (isDeniedFromStore(get, 'canCollapsePanels', { action: 'store.toggleZoneCollapsed', zone })) return;
    set((state: any) => ({
      ...state,
      zoneCollapsed: { ...state.zoneCollapsed, [zone]: !state.zoneCollapsed[zone] },
    }));
  },

  setZoneType: (zone, type) => {
    if (isDeniedFromStore(get, 'canMovePanels', { action: 'store.setZoneType', zone, type })) return;
    set((state: any) => ({
      ...state,
      zoneType: { ...(state.zoneType ?? { top: 'default', bottom: 'default', left: 'default', right: 'default' }), [zone]: type },
    }));
  },

  setZoneCollapsible: (zone, value) => {
    if (isDeniedFromStore(get, 'canMovePanels', { action: 'store.setZoneCollapsible', zone, value })) return;
    set((state: any) => ({
      ...state,
      zoneCollapsible: { ...(state.zoneCollapsible ?? { top: false, bottom: false, left: true, right: true }), [zone]: value },
    }));
  },

  setBlockCollapsed: (blockId, value) => {
    if (isDeniedFromStore(get, 'canCollapsePanels', { action: 'store.setBlockCollapsed', blockId, value })) return;
    set((state: any) => ({
      ...state,
      blocksByZone: mapBlocksByZone(state.blocksByZone, (block) =>
        block.id === blockId ? { ...block, collapsed: value } : block,
      ),
    }));
  },

  setBlockCollapsible: (blockId: string, value: boolean) => {
    if (isDeniedFromStore(get, 'canMovePanels', { action: 'store.setBlockCollapsible', blockId, value })) return;
    set((state: any) => ({
      ...state,
      blocksByZone: mapBlocksByZone(state.blocksByZone, (block) =>
        block.id === blockId ? { ...block, collapsible: value } : block,
      ),
    }));
  },

  toggleBlockCollapsed: (blockId) => {
    if (isDeniedFromStore(get, 'canCollapsePanels', { action: 'store.toggleBlockCollapsed', blockId })) return;
    set((state: any) => ({
      ...state,
      blocksByZone: mapBlocksByZone(state.blocksByZone, (block) =>
        block.id === blockId ? { ...block, collapsed: !block.collapsed } : block,
      ),
    }));
  },

  initializeLayoutFromConfig: (config: any) =>
    set((state: any) => {
      if (!config || typeof config !== 'object') return state;

      const zones: LayoutZoneId[] = ['top', 'bottom', 'left', 'right'];
      const nextCollapsed: Record<LayoutZoneId, boolean> = { ...state.zoneCollapsed };
      const nextCollapsible: Record<LayoutZoneId, boolean> = { ...state.zoneCollapsible };
      const nextZoneType: Record<LayoutZoneId, 'default' | 'panels' | 'inline'> = {
        ...(state.zoneType ?? { top: 'default', bottom: 'default', left: 'default', right: 'panels' }),
      };
      const nextBlocksByZone = normalizeBlocksByZone(state.blocksByZone ?? {});

      zones.forEach((zone) => {
        const zoneConfig = (config as any)[zone];
        if (!zoneConfig || typeof zoneConfig !== 'object') return;

        if (typeof zoneConfig.collapsed === 'boolean') nextCollapsed[zone] = zoneConfig.collapsed;
        if (typeof zoneConfig.collapsible === 'boolean') nextCollapsible[zone] = zoneConfig.collapsible;

        const hasExplicitType = typeof (zoneConfig as any).type === 'string' && String((zoneConfig as any).type).trim().length > 0;
        if (hasExplicitType) {
          const zt = String((zoneConfig as any).type).trim().toLowerCase();
          if (zt === 'inline') nextZoneType[zone] = 'inline';
          if (zt === 'default') nextZoneType[zone] = 'default';
          if (zt === 'panels') nextZoneType[zone] = 'panels';
        }

        const items = (zoneConfig as any).items;
        if (!Array.isArray(items)) return;

        // IMPORTANT product rule:
        // If the user provides a layout config for a zone (items array) but does NOT provide a zone type,
        // we must default to the SIMPLE display ("default") for that zone.
        // (This prevents accidental "panels" defaults leaking into configured layouts.)
        if (!hasExplicitType) {
          nextZoneType[zone] = 'default';
        }


        const createdForZone: LayoutBlock[] = items
          .map((raw: any, index: number): LayoutBlock | null => {
            if (!raw || typeof raw !== 'object') return null;
            const kind = typeof raw.kind === 'string' ? (raw.kind as ModuleId) : undefined;
            if (!kind) return null;

            const label = typeof raw.label === 'string' && raw.label.trim().length > 0 ? raw.label.trim() : kind;
            const icon = typeof raw.icon === 'string' && raw.icon.trim().length > 0 ? raw.icon : 'views';
            const order = typeof raw.order === 'number' && Number.isFinite(raw.order) ? raw.order : index;
            const priority = typeof raw.priority === 'number' && Number.isFinite(raw.priority) ? raw.priority : undefined;
            const id = typeof raw.id === 'string' && raw.id.trim().length > 0 ? raw.id : `block-${zone}-${kind}-${index}`;

            const type = typeof raw.type === 'string' && raw.type.trim().length > 0 ? raw.type.trim() : undefined;
            const supportedEngines = normalizeLayoutItemSupportedEngines((raw as any).supportedEngines);
            const requiredCapabilities = normalizeLayoutItemRequiredCapabilities((raw as any).requiredCapabilities);
            const mountTarget = typeof raw.mountTarget === 'string' && raw.mountTarget.trim().length > 0 ? raw.mountTarget.trim() : undefined;
            const collapsed = typeof raw.collapsed === 'boolean' ? raw.collapsed : undefined;
            const renderMode = raw.renderMode === 'inline' || raw.renderMode === 'portal' ? raw.renderMode : undefined;

            return { id, kind, label, icon, order, priority, type, supportedEngines, requiredCapabilities, mountTarget, collapsed, renderMode } as any;
          })
          .filter((b): b is LayoutBlock => b !== null);

        nextBlocksByZone[zone] = normalizeBlocksForZone(createdForZone, zone);
      });

      return {
        ...state,
        blocksByZone: normalizeBlocksByZone(nextBlocksByZone),
        zoneCollapsed: nextCollapsed,
        zoneCollapsible: nextCollapsible,
        zoneType: nextZoneType,
      };
    }),

  // --- Drag & drop ---
  startDraggingBlock: (blockId) => {
    if (isDeniedFromStore(get, 'canMovePanels', { action: 'store.startDraggingBlock', blockId })) return;
    set({ draggedBlockId: blockId });
  },
  endDraggingBlock: () => set({ draggedBlockId: null }),

  // --- Modules ---
  finalizeModuleExit: (id) =>
    set((state: any) => {
      const def = getModule(id);

      const nextExiting = { ...(state.exitingModules ?? {}) } as any;
      delete nextExiting[id];

      if (def?.keepMounted) {
        return {
          exitingModules: nextExiting,
          mountedModules: { ...(state.mountedModules ?? {}), [id]: true },
          hiddenModules: { ...(state.hiddenModules ?? {}), [id]: true },
        } as any;
      }

      const nextMounted = { ...(state.mountedModules ?? {}) } as any;
      const nextHidden = { ...(state.hiddenModules ?? {}) } as any;
      delete nextMounted[id];
      delete nextHidden[id];

      return { exitingModules: nextExiting, mountedModules: nextMounted, hiddenModules: nextHidden } as any;
    }),

  openModule: (id) => {
    if (isDeniedFromStore(get, 'canOpenModule', { action: 'store.openModule', id })) return;
    const startedAt = typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();
    const requestSeq = ++moduleOpenRequestSeq;
    logAsuka3DPerf('ui-module', 'open-requested', { id, requestSeq }, { console: false });

    const openResolved = (resolved: any) => {
      if (requestSeq !== moduleOpenRequestSeq || !resolved) return;

      if (resolved?.action) {
        if (isDeniedFromStore(get, 'canOpenModule', { action: 'store.openModule.action', id })) return;
        try {
          const ok = emitAsukaEventCancelable('before:module.action', { id });
          if (ok) {
            void resolved.action({ getEngine: () => null, ui: getVanillaUiHelpers() });
            emitAsukaEvent('after:module.action', { id });
          }
        } catch {
          void 0;
        }
        return;
      }

      set((state: any) => {
        if (requestSeq !== moduleOpenRequestSeq) return state;
        return buildModuleOpenState(state, id, resolved);
      });
      const committedDurationMs = (typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now()) - startedAt;
      logAsuka3DPerf('ui-module', 'state-committed', {
        id,
        lazy: !(resolved?.render),
        displayMode: resolved?.displayMode ?? 'floating',
      }, { durationMs: committedDurationMs, console: false });
      logAsuka3DPerf('ui-module', 'open-state-committed', {
        id,
        lazy: !(resolved?.render),
        displayMode: resolved?.displayMode ?? 'floating',
      }, { durationMs: committedDurationMs, console: false });
    };

    // Most built-in modules are already registered. Do not put their opening
    // behind Promise.then(): committing synchronously lets ModuleHost paint on
    // the current interaction frame and avoids a visible dead period.
    const current = getModule(id);
    if (current && (current.render || current.action || !current.loader)) {
      logAsuka3DPerf('ui-module', 'loaded', { id, lazy: false, requestSeq }, {
        durationMs: (typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now()) - startedAt,
        console: false,
      });
      openResolved(current);
      return;
    }

    void ensureModuleAvailable(id as any).then((resolved) => {
      if (requestSeq !== moduleOpenRequestSeq) return;
      logAsuka3DPerf('ui-module', 'loaded', { id, lazy: true, requestSeq, available: !!(resolved ?? getModule(id)) }, {
        durationMs: (typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now()) - startedAt,
        console: false,
      });
      openResolved(resolved ?? getModule(id));
    });
  },

  setModuleDisplayModeOverride: (id, mode) => {
    if (isDeniedFromStore(get, 'canMovePanels', { action: 'store.setModuleDisplayModeOverride', id, mode })) return;
    set((state: any) => {
      const next = { ...(state.moduleDisplayModeOverrides ?? {}) } as any;
      if (!mode) delete next[id];
      else next[id] = mode;
      return { ...state, moduleDisplayModeOverrides: next } as any;
    });
  },

  setModuleMountTargetOverride: (id, target) => {
    if (isDeniedFromStore(get, 'canMovePanels', { action: 'store.setModuleMountTargetOverride', id, target })) return;
    set((state: any) => {
      const next = { ...(state.moduleMountTargetOverrides ?? {}) } as any;
      if (!target) delete next[id];
      else next[id] = target;
      return { ...state, moduleMountTargetOverrides: next } as any;
    });
  },

  closeModule: () => {
    invalidatePendingModuleOpen();
    set((state: any) => {
      const currentId =
        (state.moduleOverlayStack && state.moduleOverlayStack.length > 0
          ? state.moduleOverlayStack[state.moduleOverlayStack.length - 1]
          : state.activeModuleId) ?? null;

      if (currentId) {
        const def = getModule(currentId);
        if (def?.keepMounted) {
          return {
            activeModuleId: null,
            moduleOverlayStack: [],
            hiddenModules: { ...(state.hiddenModules ?? {}), [currentId]: true },
            mountedModules: { ...(state.mountedModules ?? {}), [currentId]: true },
          } as any;
        }
      }
      return { activeModuleId: null, moduleOverlayStack: [] } as any;
    });
  },

  openMenu: (id) => {
    if (isDeniedFromStore(get, 'canOpenModule', { action: 'store.openMenu', id })) return;
    invalidatePendingModuleOpen();
    set((state: any) => {
      const prevId =
        (state.moduleOverlayStack && state.moduleOverlayStack.length > 0
          ? state.moduleOverlayStack[state.moduleOverlayStack.length - 1]
          : state.activeModuleId) ?? null;

      let nextHidden = state.hiddenModules;
      let nextExiting = state.exitingModules;
      if (prevId && prevId !== id) {
        nextExiting = { ...(state.exitingModules ?? {}), [prevId]: true } as any;
        const next = { ...(nextHidden ?? {}) } as any;
        delete next[prevId];
        nextHidden = next;
      }

      const def = getModule(id);
      const nextType = (def?.displayMode ?? 'floating') as any;
      const mounted = def?.keepMounted ? { ...(state.mountedModules ?? {}), [id]: true } : state.mountedModules;
      const hidden = def?.keepMounted
        ? (() => {
            const next = { ...(nextHidden ?? {}) } as any;
            delete next[id];
            return next;
          })()
        : nextHidden;

      return {
        activeModuleId: id,
        moduleOverlayStack: [id],
        moduleOverlayType: nextType,
        mountedModules: mounted,
        hiddenModules: hidden,
        exitingModules: nextExiting,
      } as any;
    });
  },

  closeMenu: () => {
    invalidatePendingModuleOpen();
    set((state: any) => {
      const currentId =
        (state.moduleOverlayStack && state.moduleOverlayStack.length > 0
          ? state.moduleOverlayStack[state.moduleOverlayStack.length - 1]
          : state.activeModuleId) ?? null;

      if (currentId) {
        const def = getModule(currentId);
        if (def?.keepMounted) {
          return {
            activeModuleId: null,
            moduleOverlayStack: [],
            hiddenModules: { ...(state.hiddenModules ?? {}), [currentId]: true },
            mountedModules: { ...(state.mountedModules ?? {}), [currentId]: true },
          } as any;
        }
      }
      return { activeModuleId: null, moduleOverlayStack: [] } as any;
    });
  },

  toggleMenu: (id) => {
    if (isDeniedFromStore(get, 'canOpenModule', { action: 'store.toggleMenu', id })) return;
    invalidatePendingModuleOpen();
    const def = getModule(id);
    if (def?.action) {
      if (isDeniedFromStore(get, 'canOpenModule', { action: 'store.toggleMenu.action', id })) return;
      try {
        const ok = emitAsukaEventCancelable('before:module.action', { id });
        if (ok) {
          void def.action({ getEngine: () => null, ui: getVanillaUiHelpers() });
          emitAsukaEvent('after:module.action', { id });
        }
      } catch (_e) {

        void 0;
      }
      return;
    }

    set((state: any) => {
      if (state.activeModuleId === id) {
        const def2 = getModule(id);
        if (def2?.keepMounted) {
          return {
            activeModuleId: null,
            moduleOverlayStack: [],
            hiddenModules: { ...(state.hiddenModules ?? {}), [id]: true },
            mountedModules: { ...(state.mountedModules ?? {}), [id]: true },
          } as any;
        }
        return { activeModuleId: null, moduleOverlayStack: [] } as any;
      }

      const def2 = getModule(id);
      const nextType = (def2?.displayMode ?? 'floating') as any;

      const prevId =
        (state.moduleOverlayStack && state.moduleOverlayStack.length > 0
          ? state.moduleOverlayStack[state.moduleOverlayStack.length - 1]
          : state.activeModuleId) ?? null;

      let nextHidden = state.hiddenModules;
      let nextExiting = state.exitingModules;
      if (prevId && prevId !== id) {
        nextExiting = { ...(state.exitingModules ?? {}), [prevId]: true } as any;
        const next = { ...(nextHidden ?? {}) } as any;
        delete next[prevId];
        nextHidden = next;
      }

      const mounted = def2?.keepMounted ? { ...(state.mountedModules ?? {}), [id]: true } : state.mountedModules;
      const hidden = def2?.keepMounted
        ? (() => {
            const next = { ...(nextHidden ?? {}) } as any;
            delete next[id];
            return next;
          })()
        : nextHidden;

      return {
        activeModuleId: id,
        moduleOverlayStack: [id],
        moduleOverlayType: nextType,
        mountedModules: mounted,
        hiddenModules: hidden,
        exitingModules: nextExiting,
      } as any;
    });
  },

  // --- Module overlay stack (F8) ---
  pushModuleOverlay: (id) => {
    if (isDeniedFromStore(get, 'canOpenModule', { action: 'store.pushModuleOverlay', id })) return;
    set((state: any) => ({
      ...state,
      activeModuleId: id,
      moduleOverlayStack: [...(state.moduleOverlayStack ?? []), id],
    }));
  },

  backModuleOverlay: () =>
    set((state: any) => {
      const stack = state.moduleOverlayStack ?? [];
      if (stack.length <= 1) return state as any;
      const nextStack = stack.slice(0, -1);
      const nextId = nextStack[nextStack.length - 1] ?? null;
      return { ...state, activeModuleId: nextId, moduleOverlayStack: nextStack };
    }),

  setModuleOverlayAnchorBox: (box) => set((state: any) => ({ ...state, moduleOverlayAnchorBox: box })),
  setPendingRestoreSelectionMenu: (value) =>
    set((state: any) => ({ ...state, pendingRestoreSelectionMenu: !!value })),

  // --- Canvas / Designer ---
  setDesignerInstance: (instance) => {
    set({ designerInstance: instance });
    try {
      if (!instance) return;
      const state = get() as any;
      const activeViewId = String(state.activeViewId ?? '').trim() || null;
      const hasDocumentObjects = !!(state.documentObjectsByView && typeof state.documentObjectsByView === 'object' && Object.keys(state.documentObjectsByView).length);
      const hasViews = Array.isArray(state.documentViews) ? state.documentViews.length > 0 : Array.isArray(state.views) && state.views.length > 0;
      if (!activeViewId || (!hasDocumentObjects && !hasViews)) return;
      try { state.setActiveView?.(activeViewId); } catch {}
      const schedule = typeof queueMicrotask === 'function'
        ? queueMicrotask
        : (cb: () => void) => setTimeout(cb, 0);
      schedule(() => {
        try { switchViewWithCanvas((get() as any).designerInstance ?? null, activeViewId); } catch {}
        try { (get() as any).designerInstance?.util?.state?.rebuildActiveViewFromDocument?.(activeViewId); } catch {}
        try {
          const raf = typeof requestAnimationFrame === 'function'
            ? requestAnimationFrame
            : (cb: FrameRequestCallback) => setTimeout(cb, 16) as any;
          raf(() => {
            try { switchViewWithCanvas((get() as any).designerInstance ?? null, activeViewId); } catch {}
            try { (get() as any).designerInstance?.util?.state?.rebuildActiveViewFromDocument?.(activeViewId); } catch {}
          });
        } catch {}
      });
    } catch {}
  },

  setRenderEngineOptionsByKind: (options) => {
    set({ renderEngineOptionsByKind: options && typeof options === 'object' ? { ...options } : {} } as any);
  },

  setDefaultRenderEngineKind: (kind) => {
    const next = normalizeRenderEngineKind(kind, 'fabric');
    set((state: any) => {
      const activeKind = getActiveViewEngineKind(state.views ?? [], state.activeViewId ?? null, next);
      try { setCurrentRenderEngineKind(activeKind); } catch {}
      return { defaultRenderEngineKind: next, activeRenderEngineKind: activeKind } as any;
    });
    try { notifyModuleRegistryChanged(); } catch {}
  },

  setActiveRenderEngineKind: (kind) => {
    const next = normalizeRenderEngineKind(kind, 'fabric');
    set({ activeRenderEngineKind: next } as any);
    try { setCurrentRenderEngineKind(next); } catch {}
    try { notifyModuleRegistryChanged(); } catch {}
  },

  setAiRuntime: (runtime) => {
    set({ aiRuntime: runtime });
  },

  setExportRuntime: (runtime) => {
    set({ exportRuntime: runtime });
  },

  // --- Vues ---
  addView: (payload) => {
    if (isDeniedFromStore(get, 'canCreateView', { action: 'store.addView', payload })) return '';
    if (isDeniedFromStore(get, 'canCreateView', { action: 'store.addView', payload })) return '';
    let createdId = '';
    set((state: any) => {
      const index = (state.views?.length ?? 0) + 1;
      const id = `view-${index}-${Math.random().toString(36).slice(2, 7)}`;
      createdId = id;

      const p = typeof payload === 'string' ? { name: payload } : (payload ?? {});
      const viewName = String((p as any).name ?? '').trim() || `View ${index}`;

      // Default size: current canvas design space if available, otherwise 800x600.
      let defaultW = 800;
      let defaultH = 600;
      try {
        const canvas = (state.designerInstance as any)?.getCanvas?.();
        const w = Number(canvas?.getWidth?.() ?? canvas?.width ?? 0);
        const h = Number(canvas?.getHeight?.() ?? canvas?.height ?? 0);
        if (w > 0 && h > 0) {
          defaultW = Math.floor(w);
          defaultH = Math.floor(h);
        }
      } catch {}

      const width = Math.max(1, Math.floor(Number((p as any).width ?? defaultW) || defaultW));
      const height = Math.max(1, Math.floor(Number((p as any).height ?? defaultH) || defaultH));
      const dpi = Math.max(10, Math.floor(Number((p as any).dpi ?? 96) || 96));
      const unit = ((): any => {
        const u = String((p as any).unit ?? 'px');
        return u === 'cm' || u === 'mm' || u === 'in' || u === 'px' ? u : 'px';
      })();
      const engine = normalizeRenderEngineKind((p as any).engine, state.defaultRenderEngineKind ?? 'fabric');
      const scene3d = (p as any).scene3d && typeof (p as any).scene3d === 'object' ? (p as any).scene3d : undefined;
      const physicalSize = (p as any).physicalSize && typeof (p as any).physicalSize === 'object' ? (p as any).physicalSize : undefined;

      const nextView = { id, name: viewName, width, height, dpi, unit, engine, ...(physicalSize ? { physicalSize } : null), ...(scene3d ? { scene3d } : null) };
      const nextViews = [...(state.views ?? []), nextView];
      return {
        ...state,
        views: nextViews,
        documentViews: nextViews,
        documentViewportByView: {
          ...(state.documentViewportByView ?? {}),
          [id]: { zoom: 1, panX: 0, panY: 0 },
        },
      };
    });
    // New views should start with an explicit empty snapshot.
    // This keeps navigation consistent without relying on implicit canvas wipes.
    try { ensureEmptySnapshotForView(createdId); } catch {}
    return createdId;
  },

  updateView: (id, patch) => {
    if (isDeniedFromStore(get, 'canEditView', { action: 'store.updateView', id, patch })) return;
    set((state: any) => {
      const views = state.views ?? [];
      const idx = views.findIndex((v: any) => v?.id === id);
      if (idx < 0) return state;
      const prev = views[idx];
      const next = { ...prev } as any;
      if (typeof patch?.name === 'string') next.name = patch.name;
      if (typeof patch?.width === 'number') next.width = Math.max(1, Math.floor(patch.width));
      if (typeof patch?.height === 'number') next.height = Math.max(1, Math.floor(patch.height));
      if (typeof patch?.dpi === 'number') next.dpi = Math.max(10, Math.floor(patch.dpi));
      if (patch?.unit === 'px' || patch?.unit === 'cm' || patch?.unit === 'mm' || patch?.unit === 'in') next.unit = patch.unit;
      if (Object.prototype.hasOwnProperty.call(patch ?? {}, 'physicalSize')) next.physicalSize = (patch as any).physicalSize && typeof (patch as any).physicalSize === 'object' ? (patch as any).physicalSize : undefined;
      if (Object.prototype.hasOwnProperty.call(patch ?? {}, 'engine')) next.engine = normalizeRenderEngineKind((patch as any).engine, state.defaultRenderEngineKind ?? 'fabric');
      if (Object.prototype.hasOwnProperty.call(patch ?? {}, 'scene3d')) next.scene3d = (patch as any).scene3d && typeof (patch as any).scene3d === 'object' ? (patch as any).scene3d : undefined;
      const nextViews = [...views];
      nextViews[idx] = next;
      const activeKind = getActiveViewEngineKind(nextViews, state.activeViewId, state.defaultRenderEngineKind ?? 'fabric');
      try { setCurrentRenderEngineKind(activeKind); } catch {}
      try { notifyModuleRegistryChanged(); } catch {}
      return { ...state, views: nextViews, documentViews: nextViews, activeRenderEngineKind: activeKind };
    });
  },

  setActiveView: (id) => {
    if (get().designerInstance && isDeniedFromStore(get, 'canSwitchView', { action: 'store.setActiveView', id })) return;
    const previousState = get() as any;
    const previousId = String(previousState?.activeViewId ?? '').trim() || null;
    const targetId = String(id ?? '').trim();
    asukaHistoryDebug('store.setActiveView.request', {
      from: previousId,
      to: targetId,
      activeRenderEngineKind: previousState?.activeRenderEngineKind ?? null,
      previousDocumentHistoryInfo: summarizeHistoryInfo(previousState?.documentHistoryInfo),
      previous3dTrack: summarizeHistoryTrack(previousId ? previousState?.document3dHistoryByView?.[previousId] : null),
      target3dTrack: summarizeHistoryTrack(targetId ? previousState?.document3dHistoryByView?.[targetId] : null),
      previousFabricTrack: summarizeHistoryTrack(previousId ? previousState?.documentFabricHistoryByView?.[previousId] : null),
      targetFabricTrack: summarizeHistoryTrack(targetId ? previousState?.documentFabricHistoryByView?.[targetId] : null),
    });
    if (!targetId) return;
    try { emitAsukaEvent('before:views.switch', { from: previousId, to: targetId, source: 'store.setActiveView' }); } catch {}
    let nextKind: AsukaRenderEngineKind | string = 'fabric';
    set((state: any) => {
      if (!(state.views ?? []).some((v: any) => String(v?.id ?? '') === targetId)) return state;
      const nextViewportByView = { ...(state.documentViewportByView ?? {}) };
      if (!nextViewportByView[targetId]) nextViewportByView[targetId] = { zoom: 1, panX: 0, panY: 0 };
      const activeKind = getActiveViewEngineKind(state.views ?? [], targetId, state.defaultRenderEngineKind ?? 'fabric');
      nextKind = activeKind;
      try { setCurrentRenderEngineKind(activeKind); } catch {}
      try { notifyModuleRegistryChanged(); } catch {}
      const historyTrack = activeKind === '3d' ? (state.document3dHistoryByView ?? {})[targetId] : null;
      const fabricHistoryTrack = activeKind !== '3d' ? ((state.documentFabricHistoryByView ?? {})[DOCUMENT_FABRIC_HISTORY_KEY] ?? (state.documentFabricHistoryByView ?? {})[targetId]) : null;
      const fabricHistoryInfo = (() => {
        if (activeKind === '3d') return null;
        const fromTrack = (track: any) => {
          const snapshots = Array.isArray(track?.snapshots) ? track.snapshots : [];
          if (!snapshots.length) return null;
          const index = Math.max(0, Math.min(snapshots.length - 1, Number(track?.index ?? snapshots.length - 1) || 0));
          return {
            engine: 'fabric',
            scope: String(track?.scope ?? '').trim() === 'document' ? 'document' : 'view',
            viewId: String(track?.scope ?? '').trim() === 'document' ? DOCUMENT_FABRIC_HISTORY_KEY : targetId,
            index,
            length: snapshots.length,
            canUndo: index > 0,
            canRedo: index >= 0 && index < snapshots.length - 1,
            maxSnapshots: Number(track?.maxSnapshots ?? state.documentHistoryInfo?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS,
          };
        };
        try {
          const trackInfo = fromTrack(fabricHistoryTrack);
          const raw = typeof (state.designerInstance as any)?.getHistoryInfo === 'function'
            ? (state.designerInstance as any).getHistoryInfo(targetId)
            : null;
          const rawLength = Number((raw as any)?.length ?? 0) || 0;
          const trackLength = Number((trackInfo as any)?.length ?? 0) || 0;
          // Prefer the persisted per-view track. The live Fabric runtime may still
          // contain a fresh baseline for the previously rendered canvas during a
          // switch, and using it here makes the history appear to reset right after
          // the correct track flashes on screen.
          if (trackInfo && trackLength >= rawLength) return trackInfo;
          if (rawLength > 0) {
            const index = Math.max(0, Math.min(rawLength - 1, Number((raw as any)?.index ?? -1)));
            const rawIsDocument = String((raw as any)?.viewId ?? '').trim() === DOCUMENT_FABRIC_HISTORY_KEY || String((raw as any)?.scope ?? '').trim() === 'document';
            return {
              engine: 'fabric',
              scope: rawIsDocument ? 'document' : 'view',
              viewId: rawIsDocument ? DOCUMENT_FABRIC_HISTORY_KEY : targetId,
              index,
              length: rawLength,
              canUndo: index > 0,
              canRedo: index >= 0 && index < rawLength - 1,
              maxSnapshots: Number((raw as any)?.maxSnapshots ?? state.documentHistoryInfo?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS,
            };
          }
          if (trackInfo) return trackInfo;
        } catch {
          const trackInfo = fromTrack(fabricHistoryTrack);
          if (trackInfo) return trackInfo;
        }
        return {
          engine: 'fabric',
          scope: 'document',
          viewId: DOCUMENT_FABRIC_HISTORY_KEY,
          index: -1,
          length: 0,
          canUndo: false,
          canRedo: false,
          maxSnapshots: Number(state.documentHistoryInfo?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS,
        };
      })();
      asukaHistoryDebug('store.setActiveView.computeHistory.before', {
        from: previousId,
        to: targetId,
        activeKind,
        historyTrack: summarizeHistoryTrack(historyTrack),
        fabricHistoryInfo: summarizeHistoryInfo(fabricHistoryInfo),
        fabricHistoryTrack: summarizeHistoryTrack(fabricHistoryTrack),
        currentDocumentHistoryInfo: summarizeHistoryInfo(state.documentHistoryInfo),
      });
      const historyInfo = activeKind === '3d'
        ? (historyTrack && Array.isArray((historyTrack as any).snapshots)
          ? {
            engine: '3d',
            scope: 'view',
            viewId: targetId,
            index: Number((historyTrack as any).index ?? -1),
            length: Number((historyTrack as any).snapshots.length ?? 0),
            canUndo: Number((historyTrack as any).index ?? -1) > 0,
            canRedo: Number((historyTrack as any).index ?? -1) >= 0 && Number((historyTrack as any).index ?? -1) < Number((historyTrack as any).snapshots.length ?? 0) - 1,
            maxSnapshots: Number((historyTrack as any).maxSnapshots ?? state.documentHistoryInfo?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS,
          }
          : {
            engine: '3d',
            scope: 'view',
            viewId: targetId,
            index: -1,
            length: 0,
            canUndo: false,
            canRedo: false,
            maxSnapshots: Number(state.documentHistoryInfo?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS,
          })
        : fabricHistoryInfo;
      const result = {
        ...state,
        activeViewId: targetId,
        documentViews: state.views ?? [],
        documentViewportByView: nextViewportByView,
        activeRenderEngineKind: activeKind,
        ...(historyInfo ? { documentHistoryInfo: { ...(state.documentHistoryInfo ?? {}), ...historyInfo } } : null),
      };
      asukaHistoryDebug('store.setActiveView.result', {
        from: previousId,
        to: targetId,
        activeKind,
        historyInfo: summarizeHistoryInfo(historyInfo),
        nextDocumentHistoryInfo: summarizeHistoryInfo((result as any).documentHistoryInfo),
      });
      return result;
    });
    try { emitAsukaEvent('after:views.switch', { from: previousId, to: targetId, engine: nextKind, source: 'store.setActiveView' }); } catch {}
    try {
      const afterState = get() as any;
      asukaHistoryDebug('store.setActiveView.afterEvent', {
        from: previousId,
        to: targetId,
        nextKind,
        activeViewId: afterState?.activeViewId ?? null,
        documentHistoryInfo: summarizeHistoryInfo(afterState?.documentHistoryInfo),
        active3dTrack: summarizeHistoryTrack(targetId ? afterState?.document3dHistoryByView?.[targetId] : null),
        activeFabricTrack: summarizeHistoryTrack(targetId ? afterState?.documentFabricHistoryByView?.[targetId] : null),
      });
    } catch {}
  },

  removeView: (id) => {
    if (isDeniedFromStore(get, 'canDeleteView', { action: 'store.removeView', id })) return;
    set((state: any) => {
      const viewId = String(id ?? '').trim();
      if (!viewId) return state;
      const prevViews = state.views ?? [];
      const nextViews = prevViews.filter((v: any) => v?.id !== viewId);
      if (nextViews.length === prevViews.length) return state;

      // Clear thumbnail
      const nextThumbs = { ...(state.viewThumbnails ?? {}) };
      delete (nextThumbs as any)[viewId];

      // Clear per-view permissions
      const nextPermsByView = { ...(state.permissionsByView ?? {}) };
      delete (nextPermsByView as any)[viewId];

      // If the active view is removed, fallback to the first remaining view.
      const nextActive = state.activeViewId === viewId ? (nextViews[0]?.id ?? null) : state.activeViewId;
      // If no views remain, activeViewId becomes null
      const finalActive = nextViews.length === 0 ? null : nextActive;

      const nextViewportByView = { ...(state.documentViewportByView ?? {}) };
      delete (nextViewportByView as any)[viewId];
      const next3dScenesByView = { ...(state.document3dScenesByView ?? {}) };
      delete (next3dScenesByView as any)[viewId];
      const next3dHistoryByView = { ...(state.document3dHistoryByView ?? {}) };
      delete (next3dHistoryByView as any)[viewId];
      const nextFabricHistoryByView = { ...(state.documentFabricHistoryByView ?? {}) };
      delete (nextFabricHistoryByView as any)[viewId];
      return {
        ...state,
        views: nextViews,
        documentViews: nextViews,
        activeViewId: finalActive,
        viewThumbnails: nextThumbs,
        permissionsByView: nextPermsByView,
        documentViewportByView: nextViewportByView,
        document3dScenesByView: next3dScenesByView,
        document3dHistoryByView: next3dHistoryByView,
        documentFabricHistoryByView: nextFabricHistoryByView,
      };
    });
  },

  // --- Fonts ---
  fontsCatalog: getDefaultSystemFonts(),

  setFontsCatalog: (fonts) =>
    set((state: any) => ({
      ...state,
      fontsCatalog: dedupeFonts(Array.isArray(fonts) ? fonts : []),
    })),

  addSystemFont: (payload) => {
    const nextFont = normalizeSystemFont(payload as any);
    const createdId = nextFont.id;
    set((state: any) => ({
      ...state,
      fontsCatalog: dedupeFonts([...(state.fontsCatalog ?? []), nextFont]),
    }));
    return createdId;
  },

  addGoogleFont: (payload) => {
    const nextFont = normalizeGoogleFont(payload as any);
    const createdId = nextFont.id;
    set((state: any) => ({
      ...state,
      fontsCatalog: dedupeFonts([...(state.fontsCatalog ?? []), nextFont]),
    }));
    return createdId;
  },

  addCustomFont: (payload) => {
    const nextFont = normalizeCustomFont(payload as any);
    const createdId = nextFont.id;
    set((state: any) => ({
      ...state,
      fontsCatalog: dedupeFonts([...(state.fontsCatalog ?? []), nextFont]),
    }));
    return createdId;
  },

  removeFont: (id) =>
    set((state: any) => ({
      ...state,
      fontsCatalog: (state.fontsCatalog ?? []).filter((font: any) => font?.id !== id),
    })),

  markFontState: (id, patch) =>
    set((state: any) => ({
      ...state,
      fontsCatalog: (state.fontsCatalog ?? []).map((font: any) =>
        font?.id === id ? { ...font, ...patch } : font
      ),
    })),

  // --- Selection menu (F7+) ---
  setSelectionMenuItems: (items) =>
    set((state: any) => ({
      ...state,
      selectionMenuItems: Array.isArray(items)
        ? [...items]
        : state.selectionMenuItems,
    })),

  setSelectionMenuType: (type) =>
    set((state: any) => ({
      ...state,
      selectionMenuType: type,
      selectionMenuRootType: type,
    })),

  setSelectionMenuAnchorBox: (box) => set((state: any) => ({ ...state, selectionMenuAnchorBox: box })),

  openSelectionMenu: () => set((state: any) => ({ ...state, selectionMenuOpen: true })),

  closeSelectionMenu: () =>
    set((state: any) => ({
      ...state,
      selectionMenuOpen: false,
      selectionMenuStack: [],
      selectionMenuType: state.selectionMenuRootType,
      selectionMenuAnchorBox: null,
    })),

  selectionMenuGoTo: (id, options) =>
    set((state: any) => {
      const nextStack = [...(state.selectionMenuStack ?? []), id];
      const forced = options && typeof options === 'object' ? (options as any).type : null;
      const nextType = forced ? forced : state.selectionMenuRootType;
      return { ...state, selectionMenuOpen: true, selectionMenuStack: nextStack, selectionMenuType: nextType };
    }),

  selectionMenuBack: () =>
    set((state: any) => {
      const nextStack = (state.selectionMenuStack ?? []).slice(0, -1);
      const nextType = nextStack.length === 0 ? state.selectionMenuRootType : state.selectionMenuType;
      return { ...state, selectionMenuStack: nextStack, selectionMenuType: nextType };
    }),

  resetSelectionMenuNav: () =>
    set((state: any) => ({
      ...state,
      selectionMenuStack: [],
      selectionMenuType: state.selectionMenuRootType,
      selectionMenuAnchorBox: null,
    })),
}));

setRuntimeStoreBridge(useAsukaStore);
