import { useAsukaStore } from '../store/asukaStore';
import { exportViewSnapshots } from './viewSnapshots';
import { isRuntimeHelperObject, isRuntimeHelperRole } from './runtime/runtimeRoles';
import { normalizePriceInput } from './pricingValue';
import { collectDeclaredPricingColors, collectPricingColorsFromSvg, normalizePricingColor } from './pricingColors';
import {
  getRuntimeRenderEngine,
  resolveViewEngineKind,
  type Asuka3DSceneState,
  type AsukaObjectSummary,
  type AsukaRenderEngine,
} from './renderEngine';
import { convertPhysicalLength, createAsukaLayerMeasurement, type AsukaPhysicalLayerMeasurement, type AsukaPhysicalSize } from './physicalMeasurement';

const pricingDbgOn = () => (globalThis as any).__ASUKA_PRICING_DEBUG__ === true;
const pricingDbg = (...args: any[]) => {
  if (pricingDbgOn()) console.debug('[Asuka pricing]', ...args);
};

export type PricingScope = 'first' | 'all';
export type PricingMatch = 'all' | 'any';
export type PricingOperator = '>' | '<' | '>=' | '<=' | '==' | '%';
export type PricingMeasurement = 'native' | 'percent' | 'physical';
export type PricingConditionAggregation = 'sum' | 'first' | 'highest';

export interface PricingRuleTarget {
  types?: string[];
  view?: string[];
}

export interface PricingRuleCondition {
  operator: PricingOperator;
  value?: number;
  width?: number;
  height?: number;
  /** Layer dimensions can use renderer-native units, percentages, or physical centimetres. */
  measurement?: PricingMeasurement;
  price: number | string;
}

export interface PricingRule {
  handler:
    | 'scale'
    | 'layerSize'
    | 'layerCoverage'
    | 'layerArea'
    | 'canvasSize'
    | 'textLength'
    | 'textLines'
    | 'layersAmount'
    | 'usedColorsAmount'
    | 'printingMethodsAmount';
  scope?: PricingScope;
  match?: PricingMatch;
  conditionAggregation?: PricingConditionAggregation;
  target?: PricingRuleTarget;
  conditions: PricingRuleCondition[];
}

export interface PriceBreakdown {
  external: number;
  standard: number;
  dynamic: number;
  total: number;
}

export type PriceFormat = {
  code: string;
  symbol: string;
  priceFormat: string;
  decimalSeparator: string;
  thousandSeparator: string;
  precision: number;
  trimZeros: boolean;
};

export interface PriceUiBreakdown {
  base: number;
  standard: number;
  rules: number;
  total: number;
}

export interface PricingCalculationContext {
  currentViewId: string;
  currentObjects: any[];
  documentObjects: any[];
  canvasSize: { width: number; height: number };
  viewSizes: PricingViewSize[];
  engine: AsukaRenderEngine | null;
}

export interface PricingViewSize {
  id: string;
  width: number;
  height: number;
  unit?: string;
  dpi?: number;
  engine?: string;
  physicalSize?: AsukaPhysicalSize;
}

export function operators(operator: PricingOperator, a: number, b: number): boolean | number {
  switch (operator) {
    case '>': return a > b;
    case '<': return a < b;
    case '>=': return a >= b;
    case '<=': return a <= b;
    case '==': return a == b;
    case '%': return Math.floor(a / b);
    default: return false;
  }
}

const getPricingPayload = (obj: any): Record<string, any> => {
  if (obj?.pricing && typeof obj.pricing === 'object') return obj.pricing;
  if (obj?.metadata?.pricing && typeof obj.metadata.pricing === 'object') return obj.metadata.pricing;
  return {};
};

const readPricingField = (obj: any, key: string): any => {
  const pricing = getPricingPayload(obj);
  if (pricing[key] !== undefined) return pricing[key];
  if (obj?.[key] !== undefined) return obj[key];
  if (obj?.metadata?.[key] !== undefined) return obj.metadata[key];
  return undefined;
};

const getObjectViewId = (obj: any, fallback = ''): string => String(
  readPricingField(obj, 'viewId')
    ?? obj?._pricingViewId
    ?? obj?._viewId
    ?? fallback,
).trim();

const getObjectPrice = (obj: any): number => {
  const total = normalizePriceInput(readPricingField(obj, 'price'));
  if (total !== undefined) return total;
  return (normalizePriceInput(readPricingField(obj, 'basePrice')) ?? 0)
    + (normalizePriceInput(readPricingField(obj, 'variationPrice')) ?? 0);
};

const getObjectPrintingMethodId = (obj: any): string => String(
  readPricingField(obj, 'printingMethodId')
    ?? obj?.printingMethod
    ?? obj?.metadata?.printingMethod,
).trim();

const getObjectText = (obj: any): string => String(
  readPricingField(obj, 'text')
    ?? obj?.source?.text
    ?? obj?.metadata?.source?.text
    ?? '',
);

const getObjectScale = (obj: any): { x: number; y: number } => {
  const x = Math.abs(Number(readPricingField(obj, 'scaleX')) || 1);
  const y = Math.abs(Number(readPricingField(obj, 'scaleY')) || 1);
  return { x, y };
};

type PricingObjectSize = {
  width: number;
  height: number;
  referenceWidth?: number;
  referenceHeight?: number;
  space?: string;
  unit?: string;
  physical?: AsukaPhysicalLayerMeasurement;
};

const getObjectRenderedSize = (obj: any): PricingObjectSize => {
  const pricing = getPricingPayload(obj);
  const declaredSize = pricing.size && typeof pricing.size === 'object' ? pricing.size : null;
  if (declaredSize) {
    const width = Number(declaredSize.width);
    const height = Number(declaredSize.height);
    if (Number.isFinite(width) && width >= 0 && Number.isFinite(height) && height >= 0) {
      return {
        width,
        height,
        referenceWidth: Number.isFinite(Number(declaredSize.referenceWidth)) ? Math.max(0, Number(declaredSize.referenceWidth)) : undefined,
        referenceHeight: Number.isFinite(Number(declaredSize.referenceHeight)) ? Math.max(0, Number(declaredSize.referenceHeight)) : undefined,
        space: String(declaredSize.space ?? '').trim() || undefined,
        unit: String(declaredSize.unit ?? '').trim() || undefined,
        physical: declaredSize.physical && typeof declaredSize.physical === 'object' ? declaredSize.physical : undefined,
      };
    }
  }
  const pricingWidth = Number(pricing.width);
  const pricingHeight = Number(pricing.height);
  if (Number.isFinite(pricingWidth) && pricingWidth >= 0 && Number.isFinite(pricingHeight) && pricingHeight >= 0) {
    return { width: pricingWidth, height: pricingHeight };
  }
  try {
    const bounds = obj?.getBoundingRect?.(true, true);
    const width = Number(bounds?.width);
    const height = Number(bounds?.height);
    if (Number.isFinite(width) && width > 0 && Number.isFinite(height) && height > 0) return { width, height };
  } catch {}
  const scale = getObjectScale(obj);
  return {
    width: Math.abs(Number(obj?.width) || 0) * scale.x,
    height: Math.abs(Number(obj?.height) || 0) * scale.y,
  };
};

const normalizeType = (obj: any): string => {
  const itemGroup = String(obj?.itemGroup ?? obj?.metadata?.itemGroup ?? '').trim();
  if (itemGroup === 'Designs') return 'Design';
  if (itemGroup === 'Images') return 'Image';
  if (itemGroup === 'Text') return 'Text';
  if (itemGroup === 'Tools') return 'Tools';
  const category = String(getPricingPayload(obj).category ?? '').toLowerCase();
  if (category === 'element' || category === 'product') return 'Element';
  const type = String(obj?.kind ?? obj?.type ?? obj?.metadata?.type ?? '').toLowerCase();
  if (type === 'textbox' || type === 'text' || type === 'i-text') return 'Text';
  if (type === 'image') return 'Image';
  if (type === 'design' || type === 'group') return 'Design';
  if (type === 'product-3d') return 'Element';
  return type || 'Unknown';
};

const matchesPricingTargetType = (obj: any, targets: string[]): boolean => {
  if (!targets.length) return true;
  const type = normalizeType(obj).toLowerCase();
  const element = isElementPricingObject(obj);
  return targets.some((target) => {
    const normalized = String(target ?? '').trim().toLowerCase();
    if (!normalized) return false;
    if (normalized === 'layer' || normalized === 'layers') return !element && type !== 'tools';
    if (normalized === 'element' || normalized === 'elements' || normalized === 'product' || normalized === 'products') return element;
    return normalized === type || normalized.replace(/s$/, '') === type.replace(/s$/, '');
  });
};

const isElementPricingObject = (obj: any): boolean => {
  const category = String(getPricingPayload(obj).category ?? '').toLowerCase();
  return category === 'element'
    || category === 'product'
    || Boolean(obj?._asukaElementIsInstance || obj?._asukaElementName)
    || String(obj?.kind ?? '').toLowerCase() === 'product-3d';
};

const isBillablePricingObject = (obj: any): boolean => {
  if (!obj || isRuntimeHelperObject(obj)) return false;
  const role = String(obj?._asukaRole ?? obj?.metadata?.role ?? '').trim();
  if (isRuntimeHelperRole(role)) return false;
  if (obj?.excludeFromExport) return false;
  const type = String(obj?.kind ?? obj?.type ?? '').toLowerCase();
  if (type === 'mesh' || type === 'print-area' || type === 'camera' || type === 'light') return false;
  if (normalizeType(obj) === 'Tools') return false;
  return String(obj?.itemGroup ?? obj?.metadata?.itemGroup ?? '').trim() !== 'Tools';
};

const filterBillablePricingObjects = (objects: any[]): any[] => (Array.isArray(objects) ? objects : []).filter(isBillablePricingObject);

const getPricingObjectIdentity = (obj: any): string => {
  const pricingKey = String(getPricingPayload(obj).key ?? '').trim();
  if (pricingKey) return 'pricing:' + pricingKey;
  const familyId = String(obj?._asukaElementFamilyId ?? obj?.metadata?.elementFamilyId ?? '').trim();
  const elementId = String(obj?._asukaElementId ?? obj?.metadata?.elementId ?? '').trim();
  if (isElementPricingObject(obj) && (familyId || elementId)) return 'element:' + (familyId || elementId);
  const objectId = String(
    obj?._asukaObjectId
      ?? obj?.objectId
      ?? obj?.asukaId
      ?? obj?.id
      ?? obj?._id
      ?? obj?.uuid
      ?? '',
  ).trim();
  return objectId ? 'object:' + objectId : '';
};

const dedupePricingObjects = (objects: any[]): any[] => {
  const seen = new Set<string>();
  const result: any[] = [];
  for (const object of Array.isArray(objects) ? objects : []) {
    if (!object) continue;
    const key = getPricingObjectIdentity(object);
    if (key && seen.has(key)) continue;
    if (key) seen.add(key);
    result.push(object);
  }
  return result;
};

const extractFillStrings = (obj: any, out: Set<string>) => {
  const pushFill = (...values: any[]) => collectDeclaredPricingColors(...values).forEach((color) => out.add(color));
  const pricing = getPricingPayload(obj);
  pushFill(
    pricing.fill,
    pricing.fills,
    obj?.source?.color,
    obj?.source?.colors,
    obj?.metadata?.source?.color,
    obj?.metadata?.source?.colors,
    obj?.metadata?.pricingColors,
    obj?.fill,
    obj?.stroke,
  );
  collectPricingColorsFromSvg(obj?.source?.svg ?? obj?.metadata?.source?.svg ?? obj?.svg).forEach((color) => out.add(color));
  if (Array.isArray(obj?._objects)) obj._objects.forEach((child: any) => extractFillStrings(child, out));
};

const withViewId = (obj: any, viewId: string): any => {
  if (!obj || !viewId || getObjectViewId(obj)) return obj;
  const pricing = getPricingPayload(obj);
  return {
    ...obj,
    _pricingViewId: viewId,
    ...(Object.keys(pricing).length ? { pricing: { ...pricing, viewId } } : null),
  };
};

const scene3DToPricingObjects = (scene: Asuka3DSceneState | any, viewId: string): AsukaObjectSummary[] => {
  const objects: AsukaObjectSummary[] = [];
  const materials = scene?.materials && typeof scene.materials === 'object' ? scene.materials : {};
  const printAreas = Array.isArray(scene?.printAreas) ? scene.printAreas : [];
  const getSceneArea = (areaId: unknown): any => {
    const key = String(areaId ?? '').trim();
    return key ? printAreas.find((area: any) => String(area?.id ?? '').trim() === key) ?? null : null;
  };
  const getSceneSourceColors = (source: any, metadata?: any): string[] => {
    const colors = new Set(collectDeclaredPricingColors(source?.color, source?.colors, metadata?.color, metadata?.colors, metadata?.pricingColors));
    collectPricingColorsFromSvg(source?.svg).forEach((color) => colors.add(color));
    return [...colors];
  };
  for (const product of Array.isArray(scene?.products) ? scene.products : []) {
    const id = String(product?.id ?? '').trim();
    if (!id) continue;
    const fills = collectDeclaredPricingColors(Object.entries(materials)
      .filter(([key]) => key.startsWith(id + '::'))
      .map(([, value]: [string, any]) => String(value?.color ?? '').trim())
      .filter(Boolean));
    objects.push({
      id: 'product:' + id,
      kind: 'product-3d',
      label: String(product?.name ?? id),
      pricing: {
        key: 'product:' + id,
        category: 'product',
        price: product?.price,
        basePrice: product?.basePrice,
        variationPrice: product?.variationPrice,
        fill: fills[0] ?? null,
        fills,
        viewId,
      },
      metadata: { engine: 'babylon', itemGroup: 'Product', viewId },
    });
  }
  for (const decoration of Array.isArray(scene?.decorations) ? scene.decorations : []) {
    const id = String(decoration?.id ?? '').trim();
    if (!id) continue;
    const source = decoration?.source ?? {};
    const scale = Array.isArray(decoration?.placement?.scale) ? decoration.placement.scale : [];
    const declaredSize = Array.isArray(decoration?.metadata?.decalSize) ? decoration.metadata.decalSize : null;
    const width = declaredSize
      ? Math.abs(Number(declaredSize[0]) || 0) * Math.max(0.025, Math.abs(Number(scale[0]) || 1))
      : Math.abs(Number(scale[0]) || 0);
    const height = declaredSize
      ? Math.abs(Number(declaredSize[1]) || 0) * Math.max(0.025, Math.abs(Number(scale[1]) || 1))
      : Math.abs(Number(scale[1]) || 0);
    const area = getSceneArea(decoration?.targetPrintAreaId ?? decoration?.metadata?.printAreaId);
    const areaWorldSize = Array.isArray(area?.worldBounds?.size) ? area.worldBounds.size : Array.isArray(area?.placement?.scale) ? area.placement.scale : null;
    const areaPhysicalSize = (area?.physicalSize ?? area?.metadata?.physicalSize ?? area?.metadata?.policy?.physicalSize) as AsukaPhysicalSize | undefined;
    const physical = areaPhysicalSize && areaWorldSize
      ? createAsukaLayerMeasurement({
          objectId: id,
          native: {
            width,
            height,
            referenceWidth: Math.abs(Number(areaWorldSize[0]) || 0),
            referenceHeight: Math.abs(Number(areaWorldSize[1]) || 0),
            space: 'surface-tangent',
            unit: 'world-unit',
          },
          physicalSize: areaPhysicalSize,
          physicalSource: 'print-area',
        }).physical
      : undefined;
    const fills = getSceneSourceColors(source, decoration?.metadata);
    objects.push({
      id,
      kind: String(decoration?.type ?? 'object'),
      label: String(source?.text ?? id),
      pricing: {
        key: id,
        category: 'layer',
        price: decoration?.price,
        text: typeof source?.text === 'string' ? source.text : undefined,
        fill: fills[0] ?? null,
        fills,
        printingMethodId: decoration?.printingMethodId ?? null,
        width,
        height,
        scaleX: 1,
        scaleY: 1,
        viewId,
        size: {
          width,
          height,
          referenceWidth: areaWorldSize ? Math.abs(Number(areaWorldSize[0]) || 0) : 0,
          referenceHeight: areaWorldSize ? Math.abs(Number(areaWorldSize[1]) || 0) : 0,
          space: 'surface-tangent',
           unit: 'world-unit',
           ...(physical ? { physical } : null),
         },
      },
      metadata: { engine: 'babylon', itemGroup: decoration?.type === 'text' ? 'Text' : decoration?.type === 'image' ? 'Images' : 'Designs', source, viewId, sourcePixelWidth: decoration?.metadata?.sourcePixelWidth, sourcePixelHeight: decoration?.metadata?.sourcePixelHeight, sourceVector: decoration?.metadata?.sourceVector },
    });
  }
  for (const area of Array.isArray(scene?.printAreas) ? scene.printAreas : []) {
    for (const object of Array.isArray(area?.objects) ? area.objects : []) {
      const id = String(object?.id ?? '').trim();
      if (!id) continue;
      const source = object?.source ?? {};
      const fills = getSceneSourceColors(source, object?.metadata);
      const canvasWidth = Math.max(0, Number(area?.canvasSize?.width ?? 1024) || 0);
      const canvasHeight = Math.max(0, Number(area?.canvasSize?.height ?? 1024) || 0);
      const areaPhysicalSize = (area?.physicalSize ?? area?.metadata?.physicalSize ?? area?.metadata?.policy?.physicalSize) as AsukaPhysicalSize | undefined;
      const physical = areaPhysicalSize
        ? createAsukaLayerMeasurement({
            objectId: id,
            native: {
              width: Math.abs(Number(object?.width) || 0),
              height: Math.abs(Number(object?.height) || 0),
              referenceWidth: canvasWidth,
              referenceHeight: canvasHeight,
              space: 'texture',
              unit: 'texture-px',
            },
            physicalSize: areaPhysicalSize,
            physicalSource: 'print-area',
          }).physical
        : undefined;
      objects.push({
        id,
        kind: String(object?.type ?? 'object'),
        label: String(source?.text ?? id),
        pricing: {
          key: id,
          category: 'layer',
          price: object?.price,
          text: typeof source?.text === 'string' ? source.text : undefined,
          fill: fills[0] ?? null,
          fills,
          printingMethodId: object?.printingMethodId ?? null,
          width: Math.abs(Number(object?.width) || 0),
          height: Math.abs(Number(object?.height) || 0),
          scaleX: 1,
          scaleY: 1,
          viewId,
          size: {
            width: Math.abs(Number(object?.width) || 0),
            height: Math.abs(Number(object?.height) || 0),
            referenceWidth: canvasWidth,
            referenceHeight: canvasHeight,
            space: 'texture',
            unit: 'texture-px',
            ...(physical ? { physical } : null),
          },
        },
        metadata: { engine: 'babylon', itemGroup: object?.type === 'text' ? 'Text' : object?.type === 'image' ? 'Images' : 'Designs', source, viewId, sourcePixelWidth: object?.metadata?.sourcePixelWidth, sourcePixelHeight: object?.metadata?.sourcePixelHeight, sourceVector: object?.metadata?.sourceVector },
      });
    }
  }
  return dedupePricingObjects(objects);
};

const resolveCurrentObjects = (designer: any, engine: AsukaRenderEngine | null): any[] => {
  try {
    const summaries = engine?.objects?.list?.({
      includeTechnical: false,
      includeReadonly: true,
      includeScreenBox: false,
      source: 'pricing',
    });
    if (Array.isArray(summaries)) return summaries;
  } catch {}
  try {
    const objects = designer?.getCanvas?.()?.getObjects?.();
    return Array.isArray(objects) ? objects : [];
  } catch {
    return [];
  }
};

export function getViewId(_designer?: any): string {
  try {
    return String((useAsukaStore.getState() as any).activeViewId ?? '');
  } catch {
    return '';
  }
}

export function getPricingCalculationContext({
  designer,
  engine,
}: {
  designer?: any;
  engine?: AsukaRenderEngine | null;
} = {}): PricingCalculationContext {
  const state: any = useAsukaStore.getState();
  const currentViewId = String(state?.activeViewId ?? '').trim();
  const activeKind = String(state?.activeRenderEngineKind ?? '').toLowerCase() === '3d' ? '3d' : 'fabric';
  const activeEngine = engine ?? getRuntimeRenderEngine(activeKind) ?? null;
  const currentObjects = dedupePricingObjects(resolveCurrentObjects(designer, activeEngine));
  const snapshots = exportViewSnapshots();
  const views = Array.isArray(state?.views) && state.views.length
    ? state.views
    : (Array.isArray(state?.documentViews) ? state.documentViews : []);
  const viewsById = new Map<string, any>();
  for (const view of views) {
    const id = String(view?.id ?? '').trim();
    if (id) viewsById.set(id, view);
  }
  const objectViews = state?.documentObjectsByView && typeof state.documentObjectsByView === 'object' ? state.documentObjectsByView : {};
  const sceneViews = state?.document3dScenesByView && typeof state.document3dScenesByView === 'object' ? state.document3dScenesByView : {};
  const viewIds = new Set<string>([
    ...viewsById.keys(),
    ...Object.keys(objectViews),
    ...Object.keys(sceneViews),
    ...Object.keys(snapshots ?? {}),
  ]);
  if (currentViewId) viewIds.add(currentViewId);

  const documentObjects: any[] = [];
  if (!viewIds.size) documentObjects.push(...currentObjects);
  for (const viewId of viewIds) {
    if (viewId === currentViewId) {
      const fallback = currentObjects.length
        ? currentObjects
        : (activeKind === '3d'
          ? scene3DToPricingObjects(sceneViews[viewId] ?? viewsById.get(viewId)?.scene3d, viewId)
          : (Array.isArray(objectViews[viewId]) ? objectViews[viewId] : (snapshots as any)?.[viewId]?.objects ?? []));
      documentObjects.push(...fallback.map((object: any) => withViewId(object, viewId)));
      continue;
    }
    const view = viewsById.get(viewId);
    const kind = sceneViews[viewId]
      ? '3d'
      : resolveViewEngineKind(view, state?.defaultRenderEngineKind ?? 'fabric');
    if (kind === '3d') {
      documentObjects.push(...scene3DToPricingObjects(sceneViews[viewId] ?? view?.scene3d, viewId));
    } else {
      const stored = Array.isArray(objectViews[viewId])
        ? objectViews[viewId]
        : ((snapshots as any)?.[viewId]?.objects ?? []);
      documentObjects.push(...stored.map((object: any) => withViewId(object, viewId)));
    }
  }

  const activeView = viewsById.get(currentViewId);
  const canvas = designer?.getCanvas?.();
  const canvasWidth = Number(canvas?.getWidth?.() ?? canvas?.width);
  const canvasHeight = Number(canvas?.getHeight?.() ?? canvas?.height);
  const activeViewWidth = Number(activeView?.width);
  const activeViewHeight = Number(activeView?.height);
  const canvasSize = {
    // A hidden Fabric canvas keeps the last 2D view while Babylon is active.
    // Prefer the canonical active-view dimensions so 3D canvas rules never read stale 2D values.
    width: Number.isFinite(activeViewWidth) && activeViewWidth > 0 ? activeViewWidth : (Number.isFinite(canvasWidth) && canvasWidth > 0 ? canvasWidth : 0),
    height: Number.isFinite(activeViewHeight) && activeViewHeight > 0 ? activeViewHeight : (Number.isFinite(canvasHeight) && canvasHeight > 0 ? canvasHeight : 0),
  };
  const viewSizes: PricingViewSize[] = [...viewsById.values()]
    .map((view: any) => ({
      id: String(view?.id ?? '').trim(),
      width: Math.max(0, Number(view?.width) || 0),
      height: Math.max(0, Number(view?.height) || 0),
      unit: String(view?.unit ?? 'px'),
      dpi: Number(view?.dpi) || undefined,
      engine: resolveViewEngineKind(view, state?.defaultRenderEngineKind ?? 'fabric'),
      physicalSize: view?.physicalSize && typeof view.physicalSize === 'object' ? view.physicalSize : undefined,
    }))
    .filter((view) => Boolean(view.id));
  if (currentViewId && !viewSizes.some((view) => view.id === currentViewId)) {
    viewSizes.push({ id: currentViewId, ...canvasSize, unit: 'px', engine: activeKind });
  }

  return {
    currentViewId,
    currentObjects,
    documentObjects: dedupePricingObjects(documentObjects),
    canvasSize,
    viewSizes,
    engine: activeEngine,
  };
}

export function getDocumentObjects(designer: any, engine?: AsukaRenderEngine | null): any[] {
  return getPricingCalculationContext({ designer, engine }).documentObjects;
}

export function getStandardPrices({
  objects,
  designer,
  engine,
}: {
  objects?: any[];
  designer?: any;
  engine?: AsukaRenderEngine | null;
} = {}): number {
  const sourceObjects = Array.isArray(objects)
    ? objects
    : getPricingCalculationContext({ designer, engine }).documentObjects;
  const objectsList = filterBillablePricingObjects(dedupePricingObjects(sourceObjects));
  const state: any = useAsukaStore.getState();
  const colors = Array.isArray(state?.colors) ? state.colors : [];
  const printingMethods = Array.isArray(state?.printingMethods) ? state.printingMethods : [];

  const printingMethodPrices = new Map<string, number>();
  for (const method of printingMethods) {
    const price = normalizePriceInput(method?.price) ?? 0;
    for (const key of [method?.id, method?.name, method?.label]) {
      const normalized = String(key ?? '').trim();
      if (normalized) printingMethodPrices.set(normalized, price);
    }
  }

  const colorPrices = new Map<string, { price: number; printingMethodMode: 'all' | 'restricted' | 'none'; printingMethodIds: string[] }>();
  for (const color of colors) {
    const value = normalizePricingColor(color?.value);
    if (!value) continue;
    const printingMethodIds = Array.isArray(color?.printingMethodIds) ? color.printingMethodIds.map(String) : [];
    const configuredMode = String(color?.printingMethodMode ?? '');
    const printingMethodMode = configuredMode === 'all' || configuredMode === 'restricted' || configuredMode === 'none'
      ? configuredMode
      : (printingMethodIds.length ? 'restricted' : 'all');
    colorPrices.set(value, {
      price: normalizePriceInput(color?.price) ?? 0,
      printingMethodMode,
      printingMethodIds,
    });
  }

  let total = 0;
  const usedColors = new Set<string>();
  for (const object of objectsList) {
    total += getObjectPrice(object);
    const printingMethodId = getObjectPrintingMethodId(object);
    if (printingMethodId) total += printingMethodPrices.get(printingMethodId) ?? 0;
    extractFillStrings(object, usedColors);
  }
  for (const colorValue of usedColors) {
    const color = colorPrices.get(colorValue);
    if (!color?.price) continue;
    const compatible = color.printingMethodMode === 'all' || (color.printingMethodMode === 'restricted' && objectsList.some((object) => {
      const printingMethodId = getObjectPrintingMethodId(object);
      if (!printingMethodId || !color.printingMethodIds.includes(printingMethodId)) return false;
      const fills = new Set<string>();
      extractFillStrings(object, fills);
      return fills.has(colorValue);
    }));
    if (compatible) total += color.price;
  }
  pricingDbg('standard', { objectCount: objectsList.length, total });
  return total;
}

function parsePercentString(value: string): number | null {
  const match = String(value).trim().match(/^(-?\d+(?:\.\d+)?)%$/);
  if (!match) return null;
  const parsed = Number(match[1]);
  return Number.isFinite(parsed) ? parsed : null;
}

function applyConditionPrice(currentTotal: number, price: number | string): number {
  if (typeof price === 'number') return Number.isFinite(price) ? price : 0;
  const percent = parsePercentString(price);
  if (percent == null) return normalizePriceInput(price) ?? 0;
  return (currentTotal * percent) / 100;
}

const getRuleObjects = (objects: any[], rule: PricingRule, currentViewId: string): any[] => {
  const targets = rule.target;
  return filterBillablePricingObjects(objects).filter((object) => {
    if (Array.isArray(targets?.types) && !matchesPricingTargetType(object, targets.types)) return false;
    if (Array.isArray(targets?.view) && targets.view.length && !targets.view.includes(getObjectViewId(object, currentViewId))) return false;
    return true;
  });
};

type PricingConditionEvaluator = (condition: PricingRuleCondition, runningTotal: number) => number | null;

const aggregateConditionPrices = (
  conditions: PricingRuleCondition[],
  runningTotal: number,
  aggregation: PricingConditionAggregation,
  evaluate: PricingConditionEvaluator,
): number => {
  if (aggregation === 'first') {
    for (const condition of conditions) {
      const value = evaluate(condition, runningTotal);
      if (value != null) return value;
    }
    return 0;
  }
  if (aggregation === 'highest') {
    let highest: number | null = null;
    for (const condition of conditions) {
      const value = evaluate(condition, runningTotal);
      if (value != null && (highest == null || value > highest)) highest = value;
    }
    return highest ?? 0;
  }

  let delta = 0;
  for (const condition of conditions) {
    const value = evaluate(condition, runningTotal + delta);
    if (value != null) delta += value;
  }
  return delta;
};

const addMetricConditionPrices = (
  conditions: PricingRuleCondition[],
  metric: number,
  runningTotal: number,
  aggregation: PricingConditionAggregation,
): number => aggregateConditionPrices(conditions, runningTotal, aggregation, (condition, total) => {
  const threshold = Number(condition.value) || 0;
  if (condition.operator === '%') {
    const multiplier = Number(operators('%', metric, threshold || 1)) || 0;
    return multiplier > 0 ? multiplier * applyConditionPrice(total, condition.price) : null;
  }
  return operators(condition.operator, metric, threshold) ? applyConditionPrice(total, condition.price) : null;
});

const getObjectPricingSize = (
  object: any,
  viewSizes: PricingViewSize[],
  currentViewId: string,
): PricingObjectSize => {
  let size = getObjectRenderedSize(object);
  const viewId = getObjectViewId(object, currentViewId);
  const view = viewSizes.find((candidate) => candidate.id === viewId);
  if (!((size.referenceWidth ?? 0) > 0 && (size.referenceHeight ?? 0) > 0) && (!size.unit || size.unit === 'canvas-px') && view) {
    size = { ...size, referenceWidth: view.width, referenceHeight: view.height };
  }
  if (!size.physical?.calibrated && view?.physicalSize && (size.referenceWidth ?? 0) > 0 && (size.referenceHeight ?? 0) > 0) {
    const frameSpace = (size.space === 'screen' || size.space === 'canvas' || size.space === 'texture' || size.space === 'uv' || size.space === 'world' || size.space === 'surface-tangent')
      ? size.space
      : 'canvas';
    const frameUnit = (size.unit === 'css-px' || size.unit === 'canvas-px' || size.unit === 'texture-px' || size.unit === 'normalized' || size.unit === 'world-unit')
      ? size.unit
      : 'canvas-px';
    size = {
      ...size,
      physical: createAsukaLayerMeasurement({
        objectId: String(object?.id ?? object?._asukaObjectId ?? ''),
        native: {
          width: size.width,
          height: size.height,
          referenceWidth: size.referenceWidth,
          referenceHeight: size.referenceHeight,
          space: frameSpace,
          unit: frameUnit,
        },
        physicalSize: view.physicalSize,
        physicalSource: 'view',
      }).physical,
    };
  }
  return size;
};

const getLayerCoverage = (size: PricingObjectSize): number | null => {
  const referenceWidth = Number(size.referenceWidth);
  const referenceHeight = Number(size.referenceHeight);
  if (!(referenceWidth > 0) || !(referenceHeight > 0)) return null;
  return (Math.max(0, size.width) * Math.max(0, size.height) * 100) / (referenceWidth * referenceHeight);
};

const getLayerPhysicalAreaCm2 = (size: PricingObjectSize): number | null => {
  const physical = size.physical;
  if (!physical?.calibrated || !physical.unit) return null;
  const width = Number(physical.width);
  const height = Number(physical.height);
  if (!(width >= 0) || !(height >= 0)) return null;
  const centimetresPerUnit = convertPhysicalLength(1, physical.unit, 'cm');
  return width * height * centimetresPerUnit * centimetresPerUnit;
};

const addDimensionConditionPrices = (
  conditions: PricingRuleCondition[],
  size: PricingObjectSize,
  runningTotal: number,
  matchMode: PricingMatch,
  aggregation: PricingConditionAggregation,
): number => aggregateConditionPrices(conditions, runningTotal, aggregation, (condition, total) => {
  const percent = condition.measurement === 'percent';
  const physical = condition.measurement === 'physical';
  const referenceWidth = Number(size.referenceWidth);
  const referenceHeight = Number(size.referenceHeight);
  const width = physical
    ? (size.physical?.calibrated && size.physical.width != null && size.physical.unit
        ? convertPhysicalLength(size.physical.width, size.physical.unit, 'cm')
        : Number.NaN)
    : percent
      ? (referenceWidth > 0 ? (size.width * 100) / referenceWidth : Number.NaN)
      : size.width;
  const height = physical
    ? (size.physical?.calibrated && size.physical.height != null && size.physical.unit
        ? convertPhysicalLength(size.physical.height, size.physical.unit, 'cm')
        : Number.NaN)
    : percent
      ? (referenceHeight > 0 ? (size.height * 100) / referenceHeight : Number.NaN)
      : size.height;

  if (condition.operator === '%') {
    const multipliers: number[] = [];
    if (condition.width != null && Number.isFinite(width)) multipliers.push(Math.floor(width / (Number(condition.width) || 1)));
    if (condition.height != null && Number.isFinite(height)) multipliers.push(Math.floor(height / (Number(condition.height) || 1)));
    if (!multipliers.length) return null;
    const multiplier = matchMode === 'all' ? Math.min(...multipliers) : Math.max(...multipliers);
    return multiplier > 0 ? multiplier * applyConditionPrice(total, condition.price) : null;
  }

  const checks: boolean[] = [];
  if (condition.width != null && Number.isFinite(width)) checks.push(Boolean(operators(condition.operator, width, Number(condition.width))));
  if (condition.height != null && Number.isFinite(height)) checks.push(Boolean(operators(condition.operator, height, Number(condition.height))));
  if (!checks.length || !(matchMode === 'all' ? checks.every(Boolean) : checks.some(Boolean))) return null;
  return applyConditionPrice(total, condition.price);
});

export function evaluatePricingRulesForObjects({
  designer,
  engine,
  baseTotal,
  objectsOverride,
  documentObjectsOverride,
  canvasSizeOverride,
  viewSizesOverride,
  rulesOverride,
}: {
  designer: any;
  engine?: AsukaRenderEngine | null;
  baseTotal: number;
  objectsOverride?: any[];
  documentObjectsOverride?: any[];
  canvasSizeOverride?: { width: number; height: number };
  viewSizesOverride?: PricingViewSize[];
  rulesOverride?: PricingRule[];
}): number {
  const state: any = useAsukaStore.getState();
  const rules = (Array.isArray(rulesOverride) ? rulesOverride : (Array.isArray(state?.pricingRules) ? state.pricingRules : []))
    .filter((rule: any) => rule?.enabled !== false) as PricingRule[];
  if (!rules.length) return 0;

  const context = (!Array.isArray(objectsOverride) || !Array.isArray(documentObjectsOverride) || !canvasSizeOverride || !Array.isArray(viewSizesOverride))
    ? getPricingCalculationContext({ designer, engine })
    : null;
  const currentObjects = dedupePricingObjects(Array.isArray(objectsOverride) ? objectsOverride : (context?.currentObjects ?? []));
  const documentObjects = dedupePricingObjects(Array.isArray(documentObjectsOverride)
    ? documentObjectsOverride
    : (Array.isArray(objectsOverride) ? objectsOverride : (context?.documentObjects ?? [])));
  const currentViewId = context?.currentViewId ?? getViewId(designer);
  const canvasSize = canvasSizeOverride ?? context?.canvasSize ?? { width: 0, height: 0 };
  const viewSizes = Array.isArray(viewSizesOverride) ? viewSizesOverride : (context?.viewSizes ?? []);
  let dynamic = 0;

  for (const rule of rules) {
    const scopedObjects = rule.scope === 'first' ? currentObjects : documentObjects;
    const objects = getRuleObjects(scopedObjects, rule, currentViewId);
    const runningTotal = baseTotal + dynamic;
    const matchMode = rule.match ?? 'all';
    const aggregation = rule.conditionAggregation ?? 'sum';
    let delta = 0;

    if (rule.handler === 'layerSize') {
      const evaluateObject = (object: any) => {
        const size = getObjectPricingSize(object, viewSizes, currentViewId);
        delta += addDimensionConditionPrices(rule.conditions ?? [], size, runningTotal + delta, matchMode, aggregation);
      };
      if (rule.scope === 'first') {
        if (objects[0]) evaluateObject(objects[0]);
      } else {
        objects.forEach(evaluateObject);
      }
    } else if (rule.handler === 'layerCoverage') {
      const evaluateObject = (object: any) => {
        const coverage = getLayerCoverage(getObjectPricingSize(object, viewSizes, currentViewId));
        if (coverage != null) delta += addMetricConditionPrices(rule.conditions ?? [], coverage, runningTotal + delta, aggregation);
      };
      if (rule.scope === 'first') {
        if (objects[0]) evaluateObject(objects[0]);
      } else {
        objects.forEach(evaluateObject);
      }
    } else if (rule.handler === 'layerArea') {
      const evaluateObject = (object: any) => {
        const area = getLayerPhysicalAreaCm2(getObjectPricingSize(object, viewSizes, currentViewId));
        if (area != null) delta += addMetricConditionPrices(rule.conditions ?? [], area, runningTotal + delta, aggregation);
      };
      if (rule.scope === 'first') {
        if (objects[0]) evaluateObject(objects[0]);
      } else {
        objects.forEach(evaluateObject);
      }
    } else if (rule.handler === 'canvasSize') {
      const declaredViews = viewSizes.length
        ? viewSizes
        : [{ id: currentViewId, ...canvasSize }];
      const targetViews = Array.isArray(rule.target?.view) ? new Set(rule.target?.view.map(String)) : null;
      const eligibleViews = declaredViews.filter((view) => !targetViews?.size || targetViews.has(view.id));
      const viewsToEvaluate = rule.scope === 'first'
        ? eligibleViews.filter((view) => view.id === currentViewId).slice(0, 1)
        : eligibleViews;
      for (const view of viewsToEvaluate) {
        delta += addDimensionConditionPrices(
          rule.conditions ?? [],
          { width: view.width, height: view.height },
          runningTotal + delta,
          matchMode,
          aggregation,
        );
      }
    } else if (rule.handler === 'textLength' || rule.handler === 'textLines') {
      const textObjects = objects.filter((object) => normalizeType(object) === 'Text');
      const evaluateObject = (object: any) => {
        const text = getObjectText(object);
        const metric = rule.handler === 'textLength' ? text.length : text.split(/\r?\n/).length;
        delta += addMetricConditionPrices(rule.conditions ?? [], metric, runningTotal + delta, aggregation);
      };
      if (rule.scope === 'first') {
        if (textObjects[0]) evaluateObject(textObjects[0]);
      } else {
        textObjects.forEach(evaluateObject);
      }
    } else if (rule.handler === 'layersAmount') {
      const layers = objects.filter((object) => !isElementPricingObject(object));
      delta += addMetricConditionPrices(rule.conditions ?? [], layers.length, runningTotal + delta, aggregation);
    } else if (rule.handler === 'usedColorsAmount') {
      const usedColors = new Set<string>();
      objects.forEach((object) => extractFillStrings(object, usedColors));
      delta += addMetricConditionPrices(rule.conditions ?? [], usedColors.size, runningTotal + delta, aggregation);
    } else if (rule.handler === 'printingMethodsAmount') {
      const methods = new Set<string>();
      objects.forEach((object) => {
        const method = getObjectPrintingMethodId(object);
        if (method) methods.add(method);
      });
      delta += addMetricConditionPrices(rule.conditions ?? [], methods.size, runningTotal + delta, aggregation);
    } else if (rule.handler === 'scale') {
      const evaluateObject = (object: any) => {
        const scale = getObjectScale(object);
        delta += addMetricConditionPrices(rule.conditions ?? [], Math.max(scale.x, scale.y), runningTotal + delta, aggregation);
      };
      if (rule.scope === 'first') {
        if (objects[0]) evaluateObject(objects[0]);
      } else {
        objects.forEach(evaluateObject);
      }
    }
    dynamic += delta;
  }

  pricingDbg('rules', { ruleCount: rules.length, dynamic });
  return dynamic;
}

export function evaluatePricingRules(designer: any, baseTotal: number, engine?: AsukaRenderEngine | null): number {
  return evaluatePricingRulesForObjects({ designer, engine, baseTotal });
}

export function getPricingRules({
  designer,
  engine,
  baseTotal,
}: {
  designer: any;
  engine?: AsukaRenderEngine | null;
  baseTotal: number;
}): number {
  return evaluatePricingRules(designer, baseTotal, engine);
}

export function getPrice({
  designer,
  engine,
  externalPrice,
}: {
  designer: any;
  engine?: AsukaRenderEngine | null;
  externalPrice?: number | string;
}): number {
  const store = useAsukaStore.getState() as any;
  const external = normalizePriceInput(externalPrice) ?? normalizePriceInput(store?.price) ?? 0;
  const context = getPricingCalculationContext({ designer, engine });
  const standard = getStandardPrices({ objects: context.documentObjects });
  const dynamic = evaluatePricingRulesForObjects({
    designer,
    engine: context.engine,
    baseTotal: external + standard,
    objectsOverride: context.currentObjects,
    documentObjectsOverride: context.documentObjects,
    canvasSizeOverride: context.canvasSize,
    viewSizesOverride: context.viewSizes,
  });
  const total = external + standard + dynamic;
  try { designer?.util?.publish?.('getPrice', { external, standard, dynamic, total }); } catch {}
  return total;
}

export function getPriceUiBreakdown({
  designer,
  engine,
  externalPrice,
}: {
  designer: any;
  engine?: AsukaRenderEngine | null;
  externalPrice?: number | string;
}): PriceUiBreakdown {
  const store = useAsukaStore.getState() as any;
  const base = normalizePriceInput(externalPrice) ?? normalizePriceInput(store?.price) ?? 0;
  const context = getPricingCalculationContext({ designer, engine });
  const standard = getStandardPrices({ objects: context.documentObjects });
  const rules = evaluatePricingRulesForObjects({
    designer,
    engine: context.engine,
    baseTotal: base + standard,
    objectsOverride: context.currentObjects,
    documentObjectsOverride: context.documentObjects,
    canvasSizeOverride: context.canvasSize,
    viewSizesOverride: context.viewSizes,
  });
  return { base, standard, rules, total: base + standard + rules };
}
