<?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\Support;

defined('ABSPATH') || exit;

/**
 * Canonical data contract used by WordPress storage and the JS runtime.
 *
 * Development phase policy: no legacy payload repair here. Catalog editors,
 * imports and product-config meta must be stored in this shape immediately so
 * the runtime builder can mostly assemble data instead of guessing formats.
 */
final class RuntimeDataSchema
{
    public const VERSION = 7;

    public const COLLECTION_KEYS = [
        'designs',
        'fonts',
        'colors',
        'printing_methods',
        'shapes',
        'pricing_rules',
    ];

    public static function catalogDefaults() : array
    {
        return [
            'schemaVersion' => self::VERSION,
            'designs' => [],
            'fonts' => [],
            'colors' => [],
            'printing_methods' => [],
            'shapes' => [],
            'pricing_rules' => [],
        ];
    }

    public static function sanitizeCatalog(array $input, array $existing = []) : array
    {
        $existing = wp_parse_args($existing, self::catalogDefaults());
        $output = self::catalogDefaults();

        foreach (self::COLLECTION_KEYS as $key) {
            $output[$key] = array_key_exists($key, $input)
                ? self::sanitizeCollection($key, is_array($input[$key] ?? null) ? (array) $input[$key] : [])
                : (array) ($existing[$key] ?? []);
        }

        return $output;
    }

    public static function sanitizeCollection(string $key, array $rows) : array
    {
        $items = [];
        foreach ($rows as $row) {
            if (! is_array($row)) {
                continue;
            }

            $item = match ($key) {
                'designs' => self::sanitizeDesign($row, count($items)),
                'fonts' => self::sanitizeFont($row, count($items)),
                'colors' => self::sanitizeColor($row, count($items)),
                'printing_methods' => self::sanitizePrintingMethod($row, count($items)),
                'shapes' => self::sanitizeShape($row, count($items)),
                'pricing_rules' => self::sanitizePricingRule($row, count($items)),
                default => null,
            };

            if ($item !== null) {
                $items[] = $item;
            }
        }

        return $items;
    }

    public static function sanitizeColor(array $row, int $index = 0) : ?array
    {
        $label = self::text($row['label'] ?? '');
        $value = self::text($row['value'] ?? '');
        if ($label === '' && $value === '') {
            return null;
        }
        if ($label === '') {
            $label = $value;
        }

        $type = sanitize_key((string) ($row['type'] ?? 'solid')) ?: 'solid';
        if (! in_array($type, ['solid', 'gradient', 'pattern', 'image'], true)) {
            $type = 'solid';
        }

        return [
            'schemaVersion' => self::VERSION,
            'id' => self::id((string) ($row['id'] ?? ''), $label ?: $value, 'color', $index),
            'label' => $label,
            'value' => $value,
            'type' => $type,
            'group' => self::text($row['group'] ?? ''),
            'price' => self::float($row['price'] ?? 0),
            'printingMethodMode' => self::printingMethodMode($row),
            'printingMethodIds' => self::stringList($row['printingMethodIds'] ?? [], true),
            'isDefault' => self::bool($row['isDefault'] ?? false),
            'enabled' => self::bool($row['enabled'] ?? true),
            'sortOrder' => self::int($row['sortOrder'] ?? $index),
        ];
    }

    public static function sanitizePrintingMethod(array $row, int $index = 0) : ?array
    {
        $label = self::text($row['label'] ?? '');
        if ($label === '') {
            return null;
        }

        $colorIds = [];
        if (isset($row['colorIds'])) {
            $colorIds = self::stringList($row['colorIds'], true);
        } elseif (isset($row['colors'])) {
            $colorIds = self::stringList($row['colors'], true);
        }

        $palette = is_array($row['palette'] ?? null) ? (array) $row['palette'] : [];
        $paletteMode = sanitize_key((string) ($palette['mode'] ?? ($colorIds !== [] ? 'restricted' : 'all')));
        if (! in_array($paletteMode, ['all', 'restricted', 'none'], true)) {
            $paletteMode = 'all';
        }

        return [
            'schemaVersion' => self::VERSION,
            'id' => self::id((string) ($row['id'] ?? ''), $label, 'print', $index),
            'label' => $label,
            'name' => self::text($row['name'] ?? $label),
            'category' => self::text($row['category'] ?? ''),
            'description' => self::textarea($row['description'] ?? ''),
            'price' => self::float($row['price'] ?? 0),
            'colorIds' => $colorIds,
            'palette' => [
                'mode' => $paletteMode,
                'colorIds' => $colorIds,
            ],
            'colors' => [],
            'enabled' => self::bool($row['enabled'] ?? true),
            'sortOrder' => self::int($row['sortOrder'] ?? $index),
        ];
    }

    public static function sanitizeFont(array $row, int $index = 0) : ?array
    {
        $label = self::text($row['label'] ?? '');
        $family = self::text($row['family'] ?? '');
        if ($label === '' && $family === '') {
            return null;
        }
        if ($label === '') {
            $label = $family;
        }
        if ($family === '') {
            $family = $label;
        }

        $source = sanitize_key((string) ($row['source'] ?? 'system')) ?: 'system';
        if (! in_array($source, ['system', 'google', 'custom'], true)) {
            $source = 'system';
        }

        $item = [
            'schemaVersion' => self::VERSION,
            'id' => self::id((string) ($row['id'] ?? ''), $label ?: $family, 'font', $index),
            'label' => $label,
            'family' => $family,
            'source' => $source,
            'category' => self::text($row['category'] ?? ''),
            'enabled' => self::bool($row['enabled'] ?? true),
            'sortOrder' => self::int($row['sortOrder'] ?? $index),
        ];

        $cssUrl = esc_url_raw((string) ($row['cssUrl'] ?? ''));
        $fileUrl = esc_url_raw((string) ($row['fileUrl'] ?? ''));
        if ($source === 'google') {
            $item['weights'] = self::stringList($row['weights'] ?? []);
            $item['styles'] = array_values(array_intersect(self::stringList($row['styles'] ?? []), ['normal', 'italic']));
        }
        if (in_array($source, ['google', 'custom'], true) && $cssUrl !== '') {
            $item['cssUrl'] = $cssUrl;
        }
        if ($source === 'custom' && $fileUrl !== '') {
            $item['fileUrl'] = $fileUrl;
            $item['url'] = $fileUrl;
            $format = sanitize_key((string) ($row['format'] ?? pathinfo((string) wp_parse_url($fileUrl, PHP_URL_PATH), PATHINFO_EXTENSION)));
            if (in_array($format, ['woff2', 'woff', 'ttf', 'otf'], true)) {
                $item['format'] = $format;
            }
        }

        if ($source === 'custom') {
            $weight = self::text($row['weight'] ?? '');
            if ($weight !== '') {
                $item['weight'] = $weight;
            }
            $style = sanitize_key((string) ($row['style'] ?? ''));
            if (in_array($style, ['normal', 'italic'], true)) {
                $item['style'] = $style;
            }
        }

        return $item;
    }

    private static function printingMethodMode(array $row) : string
    {
        $mode = sanitize_key((string) ($row['printingMethodMode'] ?? ''));
        if (in_array($mode, ['all', 'restricted', 'none'], true)) {
            return $mode;
        }

        return self::stringList($row['printingMethodIds'] ?? [], true) !== [] ? 'restricted' : 'all';
    }

    public static function sanitizeDesign(array $row, int $index = 0) : ?array
    {
        $label = self::text($row['label'] ?? '');
        $url = esc_url_raw((string) ($row['url'] ?? ''));
        $svgText = SvgSanitizer::sanitize((string) ($row['svgText'] ?? ''));
        if ($label === '' && $url === '' && $svgText === '') {
            return null;
        }
        if ($label === '') {
            $label = __('Untitled design', 'asuka-product-designer');
        }

        if ($url === '' && $svgText !== '') {
            $url = 'data:image/svg+xml;charset=utf-8,' . rawurlencode($svgText);
        }

        $item = [
            'schemaVersion' => self::VERSION,
            'id' => self::id((string) ($row['id'] ?? ''), $label, 'design', $index),
            'label' => $label,
            'name' => self::text($row['name'] ?? $label),
            'itemType' => 'design',
            'category' => self::text($row['category'] ?? ''),
            'description' => self::textarea($row['description'] ?? ''),
            'url' => $url,
            'sourceUrl' => $url,
            'previewUrl' => $url,
            'thumbnail' => $url,
            'price' => self::float($row['price'] ?? 0),
            'enabled' => self::bool($row['enabled'] ?? true),
            'sortOrder' => self::int($row['sortOrder'] ?? $index),
        ];

        if ($svgText !== '') {
            $item['svgText'] = $svgText;
        }

        return $item;
    }

    public static function sanitizeShape(array $row, int $index = 0) : ?array
    {
        $label = self::text($row['label'] ?? '');
        $type = sanitize_key((string) ($row['type'] ?? 'svg')) ?: 'svg';
        if (! in_array($type, ['svg', 'rect', 'circle', 'triangle', 'line', 'star'], true)) {
            $type = 'svg';
        }

        $svgText = SvgSanitizer::sanitize((string) ($row['svgText'] ?? ''));
        $svgPath = trim((string) ($row['svgPath'] ?? ''));
        $sourceUrl = esc_url_raw((string) ($row['sourceUrl'] ?? ''));
        $viewBox = self::text($row['viewBox'] ?? '0 0 100 100');
        $fill = self::text($row['fill'] ?? 'currentColor');
        $stroke = self::text($row['stroke'] ?? '');

        if ($svgText === '' && $svgPath !== '') {
            $svgText = SvgSanitizer::sanitize('<svg xmlns="http://www.w3.org/2000/svg" viewBox="' . esc_attr($viewBox) . '"><path d="' . esc_attr($svgPath) . '" fill="' . esc_attr($fill) . '"' . ($stroke !== '' ? ' stroke="' . esc_attr($stroke) . '"' : '') . '/></svg>');
        }

        if ($label === '' && $svgText === '' && $sourceUrl === '') {
            return null;
        }
        if ($label === '') {
            $label = __('Untitled shape', 'asuka-product-designer');
        }

        $item = [
            'schemaVersion' => self::VERSION,
            'id' => self::id((string) ($row['id'] ?? ''), $label, 'shape', $index),
            'label' => $label,
            'name' => self::text($row['name'] ?? $label),
            'itemType' => 'shape',
            'category' => self::text($row['category'] ?? ''),
            'type' => $type,
            'svgText' => $svgText,
            'sourceUrl' => $sourceUrl,
            'viewBox' => $viewBox,
            'fill' => $fill,
            'stroke' => $stroke,
            'price' => self::float($row['price'] ?? 0),
            'enabled' => self::bool($row['enabled'] ?? true),
            'sortOrder' => self::int($row['sortOrder'] ?? $index),
        ];

        if ($svgPath !== '') {
            $item['svgPath'] = $svgPath;
        }

        return $item;
    }

    public static function sanitizePricingRule(array $row, int $index = 0) : ?array
    {
        $label = self::text($row['label'] ?? '');
        $handler = self::pricingHandler((string) ($row['handler'] ?? 'layersAmount'));
        $conditions = self::pricingConditions($row);
        if ($label === '' && $conditions === []) {
            return null;
        }
        if ($label === '') {
            $label = __('Untitled pricing rule', 'asuka-product-designer');
        }

        $target = is_array($row['target'] ?? null) ? (array) $row['target'] : [];
        $targetTypes = self::pricingTargetTypes($target['types'] ?? ($row['targetTypes'] ?? []));
        $targetViews = self::stringList($target['view'] ?? ($row['targetViews'] ?? []), true);
        $runtimeTarget = [];
        if ($targetTypes !== []) {
            $runtimeTarget['types'] = $targetTypes;
        }
        if ($targetViews !== []) {
            $runtimeTarget['view'] = $targetViews;
        }

        $scope = sanitize_key((string) ($row['scope'] ?? 'all'));
        $scope = in_array($scope, ['all', 'first', 'current'], true) ? $scope : 'all';
        $match = sanitize_key((string) ($row['match'] ?? 'all')) === 'any' ? 'any' : 'all';
        $builder = self::pricingRuleEditorPayload($row, $conditions);

        return [
            'schemaVersion' => self::VERSION,
            'id' => self::id((string) ($row['id'] ?? ''), $label, 'pricing-rule', $index),
            'label' => $label,
            'category' => self::text($row['category'] ?? ''),
            'description' => self::textarea($row['description'] ?? ''),
            'handler' => $handler,
            'scope' => $scope,
            'match' => $match,
            'target' => $runtimeTarget,
            'conditions' => $conditions,
            'enabled' => self::bool($row['enabled'] ?? true),
            'sortOrder' => self::int($row['sortOrder'] ?? $index),
            // Editor helpers are intentionally namespaced but mirrored at the
            // top level so the current focused admin editor remains simple.
            'editor' => $builder,
            'targetTypes' => implode(', ', $targetTypes),
            'targetViews' => implode(', ', $targetViews),
            'builderMode' => $builder['builderMode'],
            'builderOperator' => $builder['builderOperator'],
            'builderAxis' => $builder['builderAxis'],
            'builderValue' => $builder['builderValue'],
            'builderPrice' => $builder['builderPrice'],
            'conditionsJson' => wp_json_encode($conditions, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
        ];
    }

    public static function pricingConditions(array $row) : array
    {
        $builderMode = sanitize_key((string) ($row['builderMode'] ?? (($row['editor']['builderMode'] ?? '') ?: 'threshold_fixed')));
        if (! in_array($builderMode, ['threshold_fixed', 'threshold_percent', 'step_fixed', 'step_percent', 'raw_json'], true)) {
            $builderMode = 'threshold_fixed';
        }

        if ($builderMode !== 'raw_json') {
            $builderValue = trim((string) ($row['builderValue'] ?? ($row['editor']['builderValue'] ?? '')));
            $builderPrice = trim((string) ($row['builderPrice'] ?? ($row['editor']['builderPrice'] ?? '')));
            if ($builderValue === '') {
                return [];
            }
            $builderOperator = sanitize_text_field((string) ($row['builderOperator'] ?? ($row['editor']['builderOperator'] ?? '>=')));
            if (! in_array($builderOperator, ['>', '<', '>=', '<=', '=='], true)) {
                $builderOperator = '>=';
            }
            $builderAxis = sanitize_key((string) ($row['builderAxis'] ?? ($row['editor']['builderAxis'] ?? 'value')));
            if (! in_array($builderAxis, ['value', 'width', 'height'], true)) {
                $builderAxis = 'value';
            }
            $condition = [
                'operator' => str_starts_with($builderMode, 'step_') ? '%' : $builderOperator,
                'price' => str_ends_with($builderMode, '_percent') && $builderPrice !== '' && ! str_contains($builderPrice, '%') ? $builderPrice . '%' : $builderPrice,
            ];
            $condition[$builderAxis] = self::float($builderValue);
            return [$condition];
        }

        $conditions = [];
        if (is_array($row['conditions'] ?? null)) {
            $conditions = (array) $row['conditions'];
        } else {
            $conditionsJson = trim((string) ($row['conditionsJson'] ?? ''));
            if ($conditionsJson !== '') {
                $decoded = json_decode($conditionsJson, true);
                if (is_array($decoded)) {
                    $conditions = $decoded;
                }
            }
        }

        return array_values(array_filter(array_map(static function ($condition) : ?array {
            if (! is_array($condition)) {
                return null;
            }
            $output = [];
            foreach (['value', 'width', 'height'] as $axis) {
                if (isset($condition[$axis])) {
                    $output[$axis] = self::float($condition[$axis]);
                }
            }
            $operator = sanitize_text_field((string) ($condition['operator'] ?? '>='));
            $output['operator'] = in_array($operator, ['>', '<', '>=', '<=', '==', '%'], true) ? $operator : '>=';
            $output['price'] = sanitize_text_field((string) ($condition['price'] ?? 0));
            return count($output) > 2 ? $output : null;
        }, $conditions)));
    }

    public static function sanitizeProductColorsConfig(mixed $payload) : array
    {
        $payload = is_array($payload) ? $payload : [];
        $scope = sanitize_key((string) ($payload['scope'] ?? 'object'));
        if (! in_array($scope, ['object', 'canvas', 'document'], true)) {
            $scope = 'object';
        }

        return [
            'schemaVersion' => self::VERSION,
            'scope' => $scope,
            'items' => self::sanitizeCollection('colors', is_array($payload['items'] ?? null) ? (array) $payload['items'] : []),
            'printingMethods' => self::sanitizeCollection('printing_methods', is_array($payload['printingMethods'] ?? null) ? (array) $payload['printingMethods'] : []),
        ];
    }

    public static function sanitizeProductFontsConfig(mixed $payload) : array
    {
        $payload = is_array($payload) ? $payload : [];
        $flat = [];
        foreach (['system', 'google', 'custom'] as $source) {
            foreach (is_array($payload[$source] ?? null) ? (array) $payload[$source] : [] as $font) {
                if (is_array($font)) {
                    $font['source'] = $source;
                    $flat[] = $font;
                }
            }
        }
        return [
            'schemaVersion' => self::VERSION,
            'system' => array_values(array_filter(self::sanitizeCollection('fonts', array_filter($flat, static fn($font) => is_array($font) && ($font['source'] ?? '') === 'system')), 'is_array')),
            'google' => array_values(array_filter(self::sanitizeCollection('fonts', array_filter($flat, static fn($font) => is_array($font) && ($font['source'] ?? '') === 'google')), 'is_array')),
            'custom' => array_values(array_filter(self::sanitizeCollection('fonts', array_filter($flat, static fn($font) => is_array($font) && ($font['source'] ?? '') === 'custom')), 'is_array')),
        ];
    }

    public static function sanitizeProductPricingConfig(mixed $payload) : array
    {
        $payload = is_array($payload) ? $payload : [];
        $rules = [];
        if (is_array($payload['pricingRules'] ?? null)) {
            $rules = (array) $payload['pricingRules'];
        } elseif (is_array($payload['rules'] ?? null)) {
            $rules = (array) $payload['rules'];
        }

        $priceFormat = is_array($payload['priceFormat'] ?? null) ? (array) $payload['priceFormat'] : [];
        $basePrice = self::float($payload['price'] ?? ($payload['basePrice'] ?? 0));
        $pricingRules = self::sanitizeCollection('pricing_rules', $rules);

        return [
            'schemaVersion' => self::VERSION,
            'enabled' => self::bool($payload['enabled'] ?? ($pricingRules !== [])),
            'price' => $basePrice,
            'basePrice' => $basePrice,
            'pricingRules' => $pricingRules,
            'rules' => $pricingRules,
            'priceFormat' => array_filter([
                'code' => isset($priceFormat['code']) ? self::text($priceFormat['code']) : null,
                'symbol' => isset($priceFormat['symbol']) ? self::text($priceFormat['symbol']) : null,
                'priceFormat' => isset($priceFormat['priceFormat']) ? self::text($priceFormat['priceFormat']) : null,
                'decimalSeparator' => isset($priceFormat['decimalSeparator']) ? self::text($priceFormat['decimalSeparator']) : null,
                'thousandSeparator' => isset($priceFormat['thousandSeparator']) ? self::text($priceFormat['thousandSeparator']) : null,
                'precision' => isset($priceFormat['precision']) ? max(0, (int) $priceFormat['precision']) : null,
                'trimZeros' => array_key_exists('trimZeros', $priceFormat) ? self::bool($priceFormat['trimZeros']) : null,
            ], static fn($value) => $value !== null && $value !== ''),
        ];
    }

    public static function sanitizeProductAssetFilters(mixed $payload) : array
    {
        $payload = is_array($payload) ? $payload : [];
        $types = ['designs', 'fonts', 'colors', 'printingMethods', 'shapes'];
        $output = ['schemaVersion' => self::VERSION];
        foreach ($types as $type) {
            $group = is_array($payload[$type] ?? null) ? (array) $payload[$type] : [];
            $mode = sanitize_key((string) ($group['mode'] ?? 'all'));
            if (! in_array($mode, ['all', 'selected', 'none'], true)) {
                $mode = 'all';
            }
            $output[$type] = [
                'mode' => $mode,
                'ids' => self::stringList($group['ids'] ?? [], true),
            ];
        }
        return $output;
    }

    private static function pricingRuleEditorPayload(array $row, array $conditions) : array
    {
        $firstCondition = is_array($conditions[0] ?? null) ? (array) $conditions[0] : [];
        $axis = isset($firstCondition['width']) ? 'width' : (isset($firstCondition['height']) ? 'height' : 'value');
        $operator = (string) ($firstCondition['operator'] ?? ($row['builderOperator'] ?? '>='));
        $price = (string) ($firstCondition['price'] ?? ($row['builderPrice'] ?? '0'));
        $isPercentPrice = str_contains($price, '%');
        $builderMode = sanitize_key((string) ($row['builderMode'] ?? ($row['editor']['builderMode'] ?? '')));
        if (! in_array($builderMode, ['threshold_fixed', 'threshold_percent', 'step_fixed', 'step_percent', 'raw_json'], true)) {
            $builderMode = $operator === '%' ? ($isPercentPrice ? 'step_percent' : 'step_fixed') : ($isPercentPrice ? 'threshold_percent' : 'threshold_fixed');
        }

        return [
            'builderMode' => $builderMode,
            'builderOperator' => in_array($operator, ['>', '<', '>=', '<=', '=='], true) ? $operator : '>=',
            'builderAxis' => in_array($axis, ['value', 'width', 'height'], true) ? $axis : 'value',
            'builderValue' => isset($firstCondition[$axis]) ? (string) $firstCondition[$axis] : sanitize_text_field((string) ($row['builderValue'] ?? '')),
            'builderPrice' => $price !== '' ? $price : sanitize_text_field((string) ($row['builderPrice'] ?? '0')),
        ];
    }

    private static function pricingHandler(string $handler) : string
    {
        $normalized = strtolower(preg_replace('/[^a-z0-9]+/i', '', $handler) ?: '');
        return match ($normalized) {
            'layersamount' => 'layersAmount',
            'textlength' => 'textLength',
            'textlines' => 'textLines',
            'usedcolorsamount', 'colorslength' => 'usedColorsAmount',
            'scale' => 'scale',
            'layersize' => 'layerSize',
            'canvassize' => 'canvasSize',
            default => 'layersAmount',
        };
    }

    private static function pricingTargetTypes(mixed $value) : array
    {
        $items = [];
        foreach (self::stringList($value) as $part) {
            $normalized = strtolower(preg_replace('/[^a-z0-9]+/i', '', $part) ?: '');
            $items[] = match ($normalized) {
                'text', 'textbox', 'itext' => 'Text',
                'image', 'images' => 'Image',
                'design', 'designs', 'group' => 'Design',
                'tool', 'tools' => 'Tools',
                default => sanitize_text_field((string) $part),
            };
        }
        return array_values(array_unique(array_filter($items)));
    }

    private static function id(string $id, string $fallbackLabel, string $prefix, int $index = 0) : string
    {
        $id = sanitize_key($id);
        if ($id !== '') {
            return $id;
        }
        $fallback = sanitize_title($fallbackLabel);
        if ($fallback === '') {
            $fallback = $prefix . '-' . ($index + 1);
        }
        return $fallback;
    }

    private static function stringList(mixed $value, bool $asKeys = false) : array
    {
        if (is_string($value)) {
            $value = preg_split('/[\s,]+/', trim($value));
        }
        if (! is_array($value)) {
            return [];
        }

        $items = [];
        foreach ($value as $item) {
            if (is_array($item)) {
                $item = (string) ($item['id'] ?? '');
            }
            $item = $asKeys ? sanitize_key((string) $item) : sanitize_text_field((string) $item);
            if ($item !== '') {
                $items[] = $item;
            }
        }
        return array_values(array_unique($items));
    }

    private static function text(mixed $value) : string
    {
        return sanitize_text_field(is_scalar($value) ? (string) $value : '');
    }

    private static function textarea(mixed $value) : string
    {
        return sanitize_textarea_field(is_scalar($value) ? (string) $value : '');
    }

    private static function bool(mixed $value) : bool
    {
        if (is_bool($value)) {
            return $value;
        }
        if (is_numeric($value)) {
            return (int) $value === 1;
        }
        $value = strtolower(trim((string) $value));
        return in_array($value, ['1', 'true', 'yes', 'on'], true);
    }

    private static function float(mixed $value) : float
    {
        if (is_numeric($value)) {
            return (float) $value;
        }
        $value = str_replace(',', '.', trim((string) $value));
        return is_numeric($value) ? (float) $value : 0.0;
    }

    private static function int(mixed $value) : int
    {
        return is_numeric($value) ? (int) $value : 0;
    }
}
