<?php

// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedNamespaceFound -- APD is the established namespace shared with the Pro add-on and retained for backward compatibility.
namespace APD\Domain\Pricing;

use APD\Contracts\PricingEngineInterface;
use APD\Repositories\ProductConfigRepository;
use APD\Support\AssetCatalog;
use APD\Support\Json;
use APD\Support\PricingSnapshot;

defined('ABSPATH') || exit;

class PricingEngine implements PricingEngineInterface
{
    /** @var array<int, array<string, mixed>> */
    protected static array $configContextCache = [];

    public function calculate(array $context) : array
    {
        $base = $this->toFloat($context['basePrice'] ?? 0);
        $fixedFee = $this->toFloat($context['fixedFee'] ?? 0);
        $configId = absint($context['configId'] ?? 0);
        $design = $this->normalizeDesign($context['design'] ?? ($context['designJson'] ?? null));
        $config = $this->loadConfigContext($configId);

        $priceSeed = array_key_exists('price', $context) ? $this->toFloat($context['price']) : $this->toFloat($config['price'] ?? 0);
        $pricingRules = is_array($context['pricingRules'] ?? null) ? (array) $context['pricingRules'] : (array) ($config['pricingRules'] ?? []);
        $colors = is_array($context['colors'] ?? null) ? (array) $context['colors'] : (array) ($config['colors'] ?? []);
        $printingMethods = is_array($context['printingMethods'] ?? null) ? (array) $context['printingMethods'] : (array) ($config['printingMethods'] ?? []);

        $externalBase = $base + $fixedFee + $priceSeed;
        $activeViewId = $this->getActiveViewId($design);
        $liveObjects = $this->getViewObjects($design, $activeViewId);
        $documentObjects = $this->getDocumentObjects($design);

        $standard = $this->getStandardPrices($liveObjects, $colors, $printingMethods);
        $rulesBreakdown = $this->evaluatePricingRules($pricingRules, $design, $liveObjects, $documentObjects, $activeViewId, $externalBase + $standard);
        $rules = $rulesBreakdown['total'];
        $total = $externalBase + $standard + $rules;
        $adjustment = $total - $base;

        return PricingSnapshot::normalize([
            'success' => true,
            'base' => $base,
            'basePrice' => $base,
            'fixedFee' => $fixedFee,
            'designerBase' => $priceSeed,
            'designerBasePrice' => $priceSeed,
            'standard' => $standard,
            'rules' => $rules,
            'adjustment' => $adjustment,
            'total' => $total,
            'uiBreakdown' => [
                'base' => $base,
                'designerBase' => $priceSeed,
                'standard' => $standard,
                'rules' => $rules,
                'total' => $total,
                'adjustment' => $adjustment,
            ],
            'breakdown' => [
                ['type' => 'base', 'amount' => $base],
                ['type' => 'fixed_fee', 'amount' => $fixedFee],
                ['type' => 'designer_base', 'amount' => $priceSeed],
                ['type' => 'standard', 'amount' => $standard],
                ['type' => 'rules', 'amount' => $rules],
            ],
            'appliedRules' => $rulesBreakdown['appliedRules'],
            'warnings' => [],
        ], $base);
    }

    protected function loadConfigContext(int $configId) : array
    {
        if ($configId <= 0) {
            return [
                'price' => 0,
                'pricingRules' => [],
                'colors' => [],
                'printingMethods' => [],
            ];
        }

        if (isset(self::$configContextCache[$configId])) {
            return self::$configContextCache[$configId];
        }

        $repo = new ProductConfigRepository();
        $config = $repo->find($configId);
        if (! $config) {
            return self::$configContextCache[$configId] = [
                'price' => 0,
                'pricingRules' => [],
                'colors' => [],
                'printingMethods' => [],
            ];
        }

        $state = Json::decode((string) ($config['_apd_initial_state'] ?? ''), []);
        $price = $this->extractStatePrice($state);
        $pricingRules = $this->extractPricingRules($state, (string) ($config['_apd_pricing_rules'] ?? ''));
        $pricingRules = $this->mergeRulesById($pricingRules, (new AssetCatalog())->getSelectedPricingRules((string) ($config['_apd_pricing_rule_ids'] ?? '')));
        $colors = $this->extractCatalogBranch($state, 'colors');
        $printingMethods = $this->extractCatalogBranch($state, 'printingMethods');

        return self::$configContextCache[$configId] = [
            'price' => $price,
            'pricingRules' => $pricingRules,
            'colors' => $colors,
            'printingMethods' => $printingMethods,
        ];
    }

    protected function extractStatePrice(mixed $state) : float
    {
        if (! is_array($state)) {
            return 0.0;
        }

        if (isset($state['price'])) {
            return $this->toFloat($state['price']);
        }

        return 0.0;
    }

    protected function extractPricingRules(mixed $state, string $legacyMeta) : array
    {
        if (is_array($state) && is_array($state['pricingRules'] ?? null)) {
            return array_values(array_filter($state['pricingRules'], 'is_array'));
        }

        $decoded = Json::decode($legacyMeta, []);
        if (is_array($decoded['pricingRules'] ?? null)) {
            return array_values(array_filter($decoded['pricingRules'], 'is_array'));
        }
        if (is_array($decoded['rules'] ?? null)) {
            return array_values(array_filter($decoded['rules'], 'is_array'));
        }
        if ($this->isSequentialArray($decoded)) {
            return array_values(array_filter($decoded, 'is_array'));
        }

        return [];
    }

    protected function extractCatalogBranch(mixed $state, string $key) : array
    {
        if (! is_array($state)) {
            return [];
        }

        $catalog = $state['catalog'] ?? null;
        if (! is_array($catalog) || ! is_array($catalog[$key] ?? null)) {
            return [];
        }

        return array_values(array_filter($catalog[$key], 'is_array'));
    }

    protected function normalizeDesign(mixed $design) : array
    {
        if (is_array($design)) {
            return $design;
        }
        if (is_string($design) && trim($design) !== '') {
            $decoded = Json::decode($design, []);
            return is_array($decoded) ? $decoded : [];
        }

        return [];
    }

    protected function getActiveViewId(array $design) : string
    {
        $views = $design['views'] ?? [];
        if (is_array($views) && is_string($views['activeViewId'] ?? null)) {
            return (string) $views['activeViewId'];
        }

        $objects = $design['objects']['byView'] ?? null;
        if (is_array($objects)) {
            $keys = array_keys($objects);
            return isset($keys[0]) ? (string) $keys[0] : '';
        }

        $snapshots = $design['snapshots']['snapshotsByView'] ?? null;
        if (is_array($snapshots)) {
            $keys = array_keys($snapshots);
            return isset($keys[0]) ? (string) $keys[0] : '';
        }

        return '';
    }

    protected function getViewObjects(array $design, string $viewId) : array
    {
        if ($viewId === '') {
            return [];
        }

        $objectsByView = $design['objects']['byView'] ?? null;
        if (is_array($objectsByView) && is_array($objectsByView[$viewId] ?? null)) {
            return array_values(array_filter($objectsByView[$viewId], fn($object) => is_array($object) && $this->isPricedObject($object)));
        }

        $snapshotObjects = $design['snapshots']['snapshotsByView'][$viewId]['objects'] ?? null;
        if (is_array($snapshotObjects)) {
            return array_values(array_filter($snapshotObjects, fn($object) => is_array($object) && $this->isPricedObject($object)));
        }

        return [];
    }

    protected function getDocumentObjects(array $design) : array
    {
        $seen = [];
        $result = [];

        $push = function (array $object) use (&$seen, &$result) : void {
            $id = '';
            if (isset($object['_asukaObjectId'])) {
                $id = (string) $object['_asukaObjectId'];
            } elseif (isset($object['id'])) {
                $id = (string) $object['id'];
            }
            if ($id !== '' && isset($seen[$id])) {
                return;
            }
            if ($id !== '') {
                $seen[$id] = true;
            }
            $result[] = $object;
        };

        $objectsByView = $design['objects']['byView'] ?? [];
        if (is_array($objectsByView)) {
            foreach ($objectsByView as $viewId => $objects) {
                if (! is_array($objects)) {
                    continue;
                }
                foreach ($objects as $object) {
                    if (! is_array($object) || ! $this->isPricedObject($object)) {
                        continue;
                    }
                    if (! isset($object['viewId']) && ! isset($object['_viewId'])) {
                        $object['viewId'] = (string) $viewId;
                    }
                    $push($object);
                }
            }
        }

        $snapshotsByView = $design['snapshots']['snapshotsByView'] ?? [];
        if (is_array($snapshotsByView)) {
            foreach ($snapshotsByView as $viewId => $snapshot) {
                $objects = is_array($snapshot) ? ($snapshot['objects'] ?? null) : null;
                if (! is_array($objects)) {
                    continue;
                }
                foreach ($objects as $object) {
                    if (! is_array($object) || ! $this->isPricedObject($object)) {
                        continue;
                    }
                    if (! isset($object['viewId']) && ! isset($object['_viewId'])) {
                        $object['viewId'] = (string) $viewId;
                    }
                    $push($object);
                }
            }
        }

        return $result;
    }

    protected function getStandardPrices(array $objects, array $colors, array $printingMethods) : float
    {
        $total = 0.0;

        $printingMethodsMap = [];
        foreach ($printingMethods as $method) {
            if (! is_array($method)) {
                continue;
            }
            $key = (string) ($method['id'] ?? $method['name'] ?? $method['label'] ?? '');
            if ($key === '') {
                continue;
            }
            $printingMethodsMap[$key] = $this->toFloat($method['price'] ?? 0);
        }

        $colorMap = [];
        foreach ($colors as $color) {
            if (! is_array($color)) {
                continue;
            }
            $key = (string) ($color['value'] ?? '');
            if ($key === '') {
                continue;
            }
            $colorMap[$key] = [
                'price' => $this->toFloat($color['price'] ?? 0),
                'printingMethodMode' => in_array(($color['printingMethodMode'] ?? ''), ['all', 'restricted', 'none'], true)
                    ? (string) $color['printingMethodMode']
                    : (! empty($color['printingMethodIds']) ? 'restricted' : 'all'),
                'printingMethodIds' => array_map('strval', is_array($color['printingMethodIds'] ?? null) ? $color['printingMethodIds'] : []),
            ];
        }

        $usedColorValues = [];
        foreach ($objects as $object) {
            if (! is_array($object)) {
                continue;
            }

            $total += $this->toFloat($object['price'] ?? 0);

            $printingMethodId = (string) ($object['printingMethodId'] ?? $object['printingMethod'] ?? '');
            if ($printingMethodId !== '' && isset($printingMethodsMap[$printingMethodId])) {
                $total += $printingMethodsMap[$printingMethodId];
            }

            foreach ($this->extractFillStrings($object) as $fill) {
                $usedColorValues[$fill] = true;
            }
        }

        foreach (array_keys($usedColorValues) as $value) {
            $color = $colorMap[$value] ?? null;
            if (! $color || empty($color['price'])) {
                continue;
            }

            $compatible = $color['printingMethodMode'] !== 'none';
            if ($color['printingMethodMode'] === 'restricted') {
                $compatible = false;
                foreach ($objects as $object) {
                    if (! is_array($object)) {
                        continue;
                    }
                    $printingMethodId = (string) ($object['printingMethodId'] ?? $object['printingMethod'] ?? '');
                    if ($printingMethodId !== '' && in_array($printingMethodId, $color['printingMethodIds'], true)) {
                        $compatible = true;
                        break;
                    }
                }
            }

            if ($compatible) {
                $total += $this->toFloat($color['price']);
            }
        }

        return $total;
    }

    protected function evaluatePricingRules(array $rules, array $design, array $liveObjects, array $documentObjects, string $currentViewId, float $baseTotal) : array
    {
        if (! $rules) {
            return ['total' => 0.0, 'appliedRules' => []];
        }

        $dynamic = 0.0;
        $appliedRules = [];
        $canvas = $this->getCanvasSize($design, $currentViewId);

        foreach ($rules as $rule) {
            if (! is_array($rule) || ($rule['enabled'] ?? true) === false) {
                continue;
            }

            $objects = (($rule['scope'] ?? 'all') === 'all') ? $documentObjects : $liveObjects;
            $objects = $this->filterTargets($objects, is_array($rule['target'] ?? null) ? $rule['target'] : [], $currentViewId);
            $matchMode = (($rule['match'] ?? 'all') === 'any') ? 'any' : 'all';
            $conditions = is_array($rule['conditions'] ?? null) ? $rule['conditions'] : [];
            if (! $conditions) {
                continue;
            }

            $delta = 0.0;
            $runningTotal = $baseTotal + $dynamic;
            $handler = $this->normalizeHandlerIdentifier((string) ($rule['handler'] ?? ''));

            switch ($handler) {
                case 'layerSize':
                    $targets = (($rule['scope'] ?? 'all') === 'first') ? array_slice($objects, 0, 1) : $objects;
                    foreach ($targets as $object) {
                        $w = $this->toFloat($object['width'] ?? 0) * $this->toFloat($object['scaleX'] ?? 1, 1);
                        $h = $this->toFloat($object['height'] ?? 0) * $this->toFloat($object['scaleY'] ?? 1, 1);
                        foreach ($conditions as $condition) {
                            if (! is_array($condition)) {
                                continue;
                            }
                            $checks = [];
                            if (isset($condition['width'])) {
                                $checks[] = $this->operator((string) ($condition['operator'] ?? '>='), $w, $this->toFloat($condition['width']));
                            }
                            if (isset($condition['height'])) {
                                $checks[] = $this->operator((string) ($condition['operator'] ?? '>='), $h, $this->toFloat($condition['height']));
                            }
                            if (! $checks) {
                                continue;
                            }
                            if (($condition['operator'] ?? null) === '%') {
                                $denom = isset($condition['width']) ? $this->toFloat($condition['width'], 1) : $this->toFloat($condition['height'] ?? 1, 1);
                                $source = isset($condition['width']) ? $w : $h;
                                $n = (int) $this->operator('%', $source, $denom);
                                if ($n > 0) {
                                    $delta += $n * $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                                }
                            } elseif ($this->matchChecks($checks, $matchMode)) {
                                $delta += $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                            }
                        }
                    }
                    break;

                case 'canvasSize':
                    foreach ($conditions as $condition) {
                        if (! is_array($condition)) {
                            continue;
                        }
                        $checks = [];
                        if (isset($condition['width'])) {
                            if (($condition['operator'] ?? null) === '%') {
                                $n = (int) $this->operator('%', $canvas['width'], $this->toFloat($condition['width'], 1));
                                if ($n > 0) {
                                    $delta += $n * $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                                }
                            } else {
                                $checks[] = $this->operator((string) ($condition['operator'] ?? '>='), $canvas['width'], $this->toFloat($condition['width']));
                            }
                        }
                        if (isset($condition['height'])) {
                            if (($condition['operator'] ?? null) === '%') {
                                $n = (int) $this->operator('%', $canvas['height'], $this->toFloat($condition['height'], 1));
                                if ($n > 0) {
                                    $delta += $n * $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                                }
                            } else {
                                $checks[] = $this->operator((string) ($condition['operator'] ?? '>='), $canvas['height'], $this->toFloat($condition['height']));
                            }
                        }
                        if ($checks && $this->matchChecks($checks, $matchMode)) {
                            $delta += $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                        }
                    }
                    break;

                case 'textLength':
                    $texts = array_values(array_filter($objects, fn($object) => $this->normalizeType($object) === 'Text'));
                    $targets = (($rule['scope'] ?? 'all') === 'first') ? array_slice($texts, 0, 1) : $texts;
                    foreach ($targets as $object) {
                        $length = strlen((string) ($object['text'] ?? ''));
                        foreach ($conditions as $condition) {
                            if (! is_array($condition)) {
                                continue;
                            }
                            $threshold = $this->toFloat($condition['value'] ?? 0);
                            if (($condition['operator'] ?? null) === '%') {
                                $n = (int) $this->operator('%', $length, $this->toFloat($threshold, 1));
                                if ($n > 0) {
                                    $delta += $n * $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                                }
                            } elseif ($this->operator((string) ($condition['operator'] ?? '>='), $length, $threshold)) {
                                $delta += $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                            }
                        }
                    }
                    break;

                case 'textLines':
                    $texts = array_values(array_filter($objects, fn($object) => $this->normalizeType($object) === 'Text'));
                    $targets = (($rule['scope'] ?? 'all') === 'first') ? array_slice($texts, 0, 1) : $texts;
                    foreach ($targets as $object) {
                        $lines = preg_split('/\r?\n/', (string) ($object['text'] ?? ''));
                        $count = is_array($lines) ? count($lines) : 0;
                        foreach ($conditions as $condition) {
                            if (! is_array($condition)) {
                                continue;
                            }
                            $threshold = $this->toFloat($condition['value'] ?? 0);
                            if (($condition['operator'] ?? null) === '%') {
                                $n = (int) $this->operator('%', $count, $this->toFloat($threshold, 1));
                                if ($n > 0) {
                                    $delta += $n * $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                                }
                            } elseif ($this->operator((string) ($condition['operator'] ?? '>='), $count, $threshold)) {
                                $delta += $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                            }
                        }
                    }
                    break;

                case 'layersAmount':
                    $count = count(array_filter($objects, fn($object) => $this->normalizeType($object) !== 'Tools'));
                    foreach ($conditions as $condition) {
                        if (! is_array($condition)) {
                            continue;
                        }
                        $threshold = $this->toFloat($condition['value'] ?? 0);
                        if (($condition['operator'] ?? null) === '%') {
                            $n = (int) $this->operator('%', $count, $this->toFloat($threshold, 1));
                            if ($n > 0) {
                                $delta += $n * $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                            }
                        } elseif ($this->operator((string) ($condition['operator'] ?? '>='), $count, $threshold)) {
                            $delta += $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                        }
                    }
                    break;

                case 'usedColorsAmount':
                    $used = [];
                    foreach ($objects as $object) {
                        foreach ($this->extractFillStrings($object) as $fill) {
                            $used[$fill] = true;
                        }
                    }
                    $count = count($used);
                    foreach ($conditions as $condition) {
                        if (! is_array($condition)) {
                            continue;
                        }
                        $threshold = $this->toFloat($condition['value'] ?? 0);
                        if (($condition['operator'] ?? null) === '%') {
                            $n = (int) $this->operator('%', $count, $this->toFloat($threshold, 1));
                            if ($n > 0) {
                                $delta += $n * $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                            }
                        } elseif ($this->operator((string) ($condition['operator'] ?? '>='), $count, $threshold)) {
                            $delta += $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                        }
                    }
                    break;

                case 'scale':
                    $targets = (($rule['scope'] ?? 'all') === 'first') ? array_slice($objects, 0, 1) : $objects;
                    foreach ($targets as $object) {
                        $scale = max($this->toFloat($object['scaleX'] ?? 1, 1), $this->toFloat($object['scaleY'] ?? 1, 1));
                        foreach ($conditions as $condition) {
                            if (! is_array($condition)) {
                                continue;
                            }
                            $threshold = $this->toFloat($condition['value'] ?? 1, 1);
                            if (($condition['operator'] ?? null) === '%') {
                                $n = (int) $this->operator('%', $scale, $this->toFloat($threshold, 1));
                                if ($n > 0) {
                                    $delta += $n * $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                                }
                            } elseif ($this->operator((string) ($condition['operator'] ?? '>='), $scale, $threshold)) {
                                $delta += $this->applyConditionPrice($runningTotal + $delta, $condition['price'] ?? 0);
                            }
                        }
                    }
                    break;

                default:
                    break;
            }

            if ($delta !== 0.0) {
                $dynamic += $delta;
                $appliedRules[] = [
                    'id' => (string) ($rule['id'] ?? ''),
                    'label' => (string) ($rule['label'] ?? $handler),
                    'handler' => $handler,
                    'amount' => $delta,
                ];
            }
        }

        return [
            'total' => $dynamic,
            'appliedRules' => $appliedRules,
        ];
    }

    protected function mergeRulesById(array $first, array $second) : array
    {
        $merged = [];
        foreach ([$first, $second] as $group) {
            foreach ($group as $rule) {
                if (! is_array($rule)) {
                    continue;
                }
                $id = sanitize_key((string) ($rule['id'] ?? ''));
                if ($id === '') {
                    $merged[] = $rule;
                    continue;
                }
                $merged[$id] = isset($merged[$id]) && is_array($merged[$id]) ? array_replace($merged[$id], $rule) : $rule;
            }
        }

        return array_values($merged);
    }

    protected function getCanvasSize(array $design, string $currentViewId) : array
    {
        $views = $design['views']['views'] ?? [];
        if (is_array($views)) {
            foreach ($views as $view) {
                if (! is_array($view)) {
                    continue;
                }
                $id = (string) ($view['id'] ?? '');
                if ($id !== '' && $id === $currentViewId) {
                    return [
                        'width' => $this->toFloat($view['width'] ?? 0),
                        'height' => $this->toFloat($view['height'] ?? 0),
                    ];
                }
            }
        }

        return ['width' => 0.0, 'height' => 0.0];
    }

    protected function filterTargets(array $objects, array $target, string $currentViewId) : array
    {
        if (! $target) {
            return $objects;
        }

        return array_values(array_filter($objects, function ($object) use ($target, $currentViewId) {
            if (! is_array($object)) {
                return false;
            }

            if (is_array($target['types'] ?? null) && $target['types']) {
                $objectType = strtolower($this->normalizeType($object));
                $allowedTypes = array_values(array_filter(array_map(fn($type) => $this->normalizeTargetTypeIdentifier((string) $type), $target['types'])));
                if (! in_array($objectType, $allowedTypes, true)) {
                    return false;
                }
            }

            if (is_array($target['view'] ?? null) && $target['view']) {
                $viewId = (string) ($object['viewId'] ?? $object['_viewId'] ?? $currentViewId);
                if (! in_array($viewId, array_map('strval', $target['view']), true)) {
                    return false;
                }
            }

            return true;
        }));
    }

    protected function isPricedObject(array $object) : bool
    {
        if (! $object) {
            return false;
        }

        if (! empty($object['excludeFromExport']) || ! empty($object['_asukaFixedSlotIsIndicator'])) {
            return false;
        }

        $role = trim((string) ($object['_asukaRole'] ?? ''));
        if ($role !== '') {
            if (str_starts_with($role, 'drawingZone')) {
                return false;
            }

            if (in_array($role, [
                'tool',
                'drawingAreaDraft',
                'clipPath',
                'snapVertical',
                'snapHorizontal',
                'shapePreview',
                'drawingZoneOutline',
                'drawingZonePicker',
                'selectionMenuAnchor',
                'curvedGuidePath',
            ], true)) {
                return false;
            }
        }

        $itemGroup = trim((string) ($object['itemGroup'] ?? ''));
        if ($itemGroup === 'Tools') {
            return false;
        }

        return (string) ($object['type'] ?? '') !== 'activeSelection';
    }

    protected function normalizeType(array $object) : string
    {
        $group = (string) ($object['itemGroup'] ?? '');
        if ($group === 'Designs') {
            return 'Design';
        }
        if ($group === 'Images') {
            return 'Image';
        }
        if ($group === 'Text') {
            return 'Text';
        }
        if ($group === 'Tools') {
            return 'Tools';
        }

        $type = strtolower((string) ($object['type'] ?? ''));
        if (in_array($type, ['textbox', 'text', 'i-text'], true)) {
            return 'Text';
        }
        if ($type === 'image') {
            return 'Image';
        }
        if ($type === 'group') {
            return 'Design';
        }

        return (string) ($object['type'] ?? 'Unknown');
    }


    protected function normalizeHandlerIdentifier(string $handler) : string
    {
        $normalized = strtolower(trim($handler));
        return match ($normalized) {
            'layersamount', 'layers_amount', 'layers-amount' => 'layersAmount',
            'textlength', 'text_length', 'text-length' => 'textLength',
            'textlines', 'text_lines', 'text-lines' => 'textLines',
            'usedcolorsamount', 'used_colors_amount', 'used-colors-amount' => 'usedColorsAmount',
            'layersize', 'layer_size', 'layer-size' => 'layerSize',
            'canvassize', 'canvas_size', 'canvas-size' => 'canvasSize',
            default => $handler,
        };
    }

    protected function normalizeTargetTypeIdentifier(string $type) : string
    {
        $normalized = strtolower(trim($type));
        return match ($normalized) {
            'text', 'textbox', 'i-text', 'itext' => 'text',
            'image', 'images' => 'image',
            'design', 'designs', 'group' => 'design',
            'tools', 'tool' => 'tools',
            default => $normalized,
        };
    }

    protected function extractFillStrings(array $object) : array
    {
        $fills = [];
        $push = function ($fill) use (&$fills) : void {
            if (is_string($fill) && $fill !== '') {
                $fills[$fill] = true;
                return;
            }
            if (is_array($fill)) {
                $source = $fill['source'] ?? null;
                if (is_array($source) && is_string($source['src'] ?? null) && $source['src'] !== '') {
                    $fills[(string) $source['src']] = true;
                }
            }
        };

        if (($object['type'] ?? null) === 'group' && is_array($object['_objects'] ?? null)) {
            foreach ($object['_objects'] as $sub) {
                if (is_array($sub)) {
                    $push($sub['fill'] ?? null);
                }
            }
        } else {
            $push($object['fill'] ?? null);
        }

        return array_keys($fills);
    }

    protected function operator(string $operator, float|int $a, float|int $b) : bool|int
    {
        return match ($operator) {
            '>' => $a > $b,
            '<' => $a < $b,
            '>=' => $a >= $b,
            '<=' => $a <= $b,
            '==' => $a == $b,
            '%' => (int) floor($a / max((float) $b, 1.0)),
            default => false,
        };
    }

    protected function applyConditionPrice(float $currentTotal, mixed $price) : float
    {
        if (is_numeric($price)) {
            return (float) $price;
        }

        if (is_string($price) && preg_match('/^(-?\d+(?:\.\d+)?)%$/', trim($price), $matches)) {
            return $currentTotal * (((float) $matches[1]) / 100);
        }

        return $this->toFloat($price);
    }

    protected function matchChecks(array $checks, string $matchMode) : bool
    {
        if (! $checks) {
            return false;
        }

        return $matchMode === 'any'
            ? (bool) array_filter($checks)
            : count(array_filter($checks)) === count($checks);
    }

    protected function isSequentialArray(mixed $value) : bool
    {
        if (! is_array($value)) {
            return false;
        }

        if ($value === []) {
            return true;
        }

        return array_keys($value) === range(0, count($value) - 1);
    }

    protected function toFloat(mixed $value, float $fallback = 0.0) : float
    {
        if ($value === null || $value === '') {
            return $fallback;
        }

        if (is_string($value)) {
            $value = str_replace([' ', ','], ['', '.'], $value);
        }

        return is_numeric($value) ? (float) $value : $fallback;
    }
}
