<?php
/**
 * ZERTI — WooCommerce product create/edit (all core product types).
 */

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Path to the inventory product edit partial.
 */
function zerti_inventory_product_edit_component_path(): string
{
    return plugin_dir_path(ZERTI_PLUGIN_FILE) . 'includes/components/inventory-product-edit.php';
}

/**
 * Product editor route param is present in the request (0 = new, >0 = edit).
 */
function zerti_inventory_product_id_from_request(): ?int
{
    $routes = zerti_routes();
    $param = $routes['params']['product'];

    if (wp_doing_ajax() && array_key_exists('inventory_product_id', $_POST)) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only route param for editor context.
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- read-only route param for editor context.
        $raw = sanitize_text_field(wp_unslash((string) $_POST['inventory_product_id']));
    } elseif (isset($_GET[$param])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only admin navigation.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only admin navigation.
        $raw = sanitize_text_field(wp_unslash((string) $_GET[$param]));
    } else {
        return null;
    }

    if ($raw === '' || $raw === 'new') {
        return 0;
    }

    return absint($raw);
}

/**
 * Whether the warehouse view should show the product editor instead of the grid.
 */
function zerti_inventory_should_show_product_editor(): bool
{
    return zerti_get_inventory_sub() === 'warehouse' && zerti_inventory_product_id_from_request() !== null;
}

/**
 * Whether the current user may create or edit a product in ZERTI.
 */
function zerti_user_can_edit_product_in_zerti(int $product_id = 0): bool
{
    if (!zerti_user_can_view_products_grid()) {
        return false;
    }
    if (!current_user_can('edit_products')) {
        return false;
    }
    if ($product_id > 0) {
        return current_user_can('edit_post', $product_id);
    }

    return current_user_can('publish_products') || current_user_can('edit_products');
}

/**
 * Product status choices for the editor.
 *
 * @return array<string, string>
 */
function zerti_product_edit_status_choices(): array
{
    return [
        'publish' => __('Published', 'zerti'),
        'draft' => __('Draft', 'zerti'),
        'pending' => __('Awaiting re-review', 'zerti'),
        'private' => __('Private', 'zerti'),
    ];
}

/**
 * WooCommerce product type choices for the editor dropdown.
 *
 * @return array<string, string>
 */
function zerti_product_edit_type_choices(): array
{
    return [
        'simple' => __('Simple product', 'zerti'),
        'grouped' => __('Grouped product', 'zerti'),
        'external' => __('External/Affiliate product', 'zerti'),
        'variable' => __('Variable product', 'zerti'),
    ];
}

/**
 * Sanitize a product type slug from POST.
 */
function zerti_product_edit_sanitize_type(string $type): string
{
    $type = sanitize_title($type);
    $choices = zerti_product_edit_type_choices();

    return array_key_exists($type, $choices) ? $type : 'simple';
}

/**
 * Clear WooCommerce caches that pin a product to its previous type.
 */
function zerti_product_edit_clear_product_type_cache(int $product_id): void
{
    if ($product_id < 1) {
        return;
    }

    if (class_exists('WC_Cache_Helper')) {
        $type_cache_key = WC_Cache_Helper::get_cache_prefix('product_' . $product_id) . '_type_' . $product_id;
        wp_cache_delete($type_cache_key, 'products');
    }

    wp_cache_delete('product-' . $product_id, 'products');

    if (function_exists('wc_delete_product_transients')) {
        wc_delete_product_transients($product_id);
    }

    if (function_exists('wc_get_container')) {
        try {
            $product_cache = wc_get_container()->get(\Automattic\WooCommerce\Internal\Caches\ProductCache::class);
            if (is_object($product_cache) && method_exists($product_cache, 'delete')) {
                $product_cache->delete($product_id);
            }
        } catch (Throwable $e) {
            // Product instance cache is optional; type cache clear above is the critical fix.
        }
    }
}

/**
 * Instantiate or reload a product for the requested type.
 */
function zerti_product_edit_prepare_product(int $product_id, string $requested_type): ?WC_Product
{
    if (!function_exists('wc_get_product') || !class_exists('WC_Product_Factory')) {
        return null;
    }

    $requested_type = zerti_product_edit_sanitize_type($requested_type);

    if ($product_id < 1) {
        $classname = WC_Product_Factory::get_product_classname(0, $requested_type);
        if ($classname === '' || !class_exists($classname)) {
            return new WC_Product_Simple();
        }

        return new $classname();
    }

    $current_type = WC_Product_Factory::get_product_type($product_id);
    if (!is_string($current_type) || $current_type === '') {
        $current_type = 'simple';
    }
    $current_type = zerti_product_edit_sanitize_type($current_type);

    if ($current_type !== $requested_type) {
        wp_set_object_terms($product_id, $requested_type, 'product_type');
        clean_object_term_cache($product_id, 'product');
        zerti_product_edit_clear_product_type_cache($product_id);
    }

    $classname = WC_Product_Factory::get_product_classname($product_id, $requested_type);
    if ($classname === '' || !class_exists($classname)) {
        return wc_get_product($product_id) ?: null;
    }

    return new $classname($product_id);
}

/**
 * Stock status choices (when stock is not managed per quantity).
 *
 * @return array<string, string>
 */
function zerti_product_edit_stock_status_choices(): array
{
    return [
        'instock' => __('In stock', 'zerti'),
        'outofstock' => __('Out of stock', 'zerti'),
        'onbackorder' => __('Pre-order', 'zerti'),
    ];
}

/**
 * Catalog visibility choices.
 *
 * @return array<string, string>
 */
function zerti_product_edit_catalog_visibility_choices(): array
{
    return [
        'visible' => __('Shop and search results', 'zerti'),
        'catalog' => __('Shop only', 'zerti'),
        'search' => __('Search only', 'zerti'),
        'hidden' => __('Hidden', 'zerti'),
    ];
}

/**
 * Tax status choices.
 *
 * @return array<string, string>
 */
function zerti_product_edit_tax_status_choices(): array
{
    return [
        'taxable' => __('Taxable', 'zerti'),
        'shipping' => __('Shipping only', 'zerti'),
        'none' => __('No tax', 'zerti'),
    ];
}

/**
 * Tax class options (slug => label).
 *
 * @return array<string, string>
 */
function zerti_product_edit_tax_class_choices(): array
{
    if (!function_exists('wc_get_product_tax_class_options')) {
        return ['' => __('Standard', 'zerti')];
    }

    $choices = [];
    foreach (wc_get_product_tax_class_options() as $slug => $label) {
        $choices[$slug] = ($slug === '' || $slug === 'standard')
            ? __('Standard', 'zerti')
            : (string) $label;
    }

    return $choices;
}

/**
 * Backorder choices.
 *
 * @return array<string, string>
 */
function zerti_product_edit_backorder_choices(): array
{
    return [
        'no' => __('Not allowed', 'zerti'),
        'notify' => __('Allowed — notify customer', 'zerti'),
        'yes' => __('Allowed', 'zerti'),
    ];
}

/**
 * Shipping class options (term_id => label).
 *
 * @return array<int, string>
 */
function zerti_product_edit_shipping_class_choices(): array
{
    if (!taxonomy_exists('product_shipping_class')) {
        return [];
    }

    $terms = get_terms([
        'taxonomy' => 'product_shipping_class',
        'hide_empty' => false,
    ]);
    if (!is_array($terms)) {
        return [];
    }

    $choices = [0 => __('No shipping class', 'zerti')];
    foreach ($terms as $term) {
        if ($term instanceof WP_Term) {
            $choices[(int) $term->term_id] = (string) $term->name;
        }
    }

    return $choices;
}

/**
 * Product category tree for the product editor (same structure as inventory categories).
 *
 * @return list<array<string, mixed>>
 */
function zerti_product_edit_category_tree(): array
{
    $flat = function_exists('zerti_product_categories_flat_list') ? zerti_product_categories_flat_list() : [];
    if ($flat === []) {
        return [];
    }

    return function_exists('zerti_product_categories_build_tree')
        ? zerti_product_categories_build_tree($flat)
        : $flat;
}

/**
 * Whether a category node or any descendant is selected.
 *
 * @param array<string, mixed> $node
 * @param list<int> $selected
 */
function zerti_product_edit_category_branch_has_selection(array $node, array $selected): bool
{
    $id = (int) ($node['id'] ?? 0);
    if ($id > 0 && in_array($id, $selected, true)) {
        return true;
    }
    $children = isset($node['children']) && is_array($node['children']) ? $node['children'] : [];
    foreach ($children as $child) {
        if (is_array($child) && zerti_product_edit_category_branch_has_selection($child, $selected)) {
            return true;
        }
    }

    return false;
}

/**
 * Font Awesome chevron for expandable tree rows (product editor + inventory).
 */
function zerti_tree_chevron_class(bool $expanded): string
{
    if ($expanded) {
        return 'fa-chevron-down';
    }

    return function_exists('zerti_get_text_direction') && zerti_get_text_direction() === 'ltr'
        ? 'fa-chevron-right'
        : 'fa-chevron-left';
}

/**
 * Echo nested category checkboxes as an expandable tree.
 *
 * @param list<array<string, mixed>> $nodes
 * @param list<int> $selected
 */
function zerti_product_edit_render_category_tree(array $nodes, array $selected, bool $read_only, int $depth = 0, bool $is_root = true): void
{
    if ($nodes === []) {
        return;
    }

    if ($is_root) {
        echo '<div class="zerti-product-cat-tree" data-zerti-product-cat-tree role="tree">';
        echo '<ul class="zerti-product-cat-tree__level list-unstyled mb-0" role="group">';
    } else {
        echo '<ul class="zerti-product-cat-tree__level list-unstyled mb-0" role="group">';
    }

    $dis = $read_only ? ' disabled' : '';

    foreach ($nodes as $node) {
        if (!is_array($node)) {
            continue;
        }
        $id = (int) ($node['id'] ?? 0);
        if ($id < 1) {
            continue;
        }
        $name = (string) ($node['name'] ?? '');
        $children = isset($node['children']) && is_array($node['children']) ? $node['children'] : [];
        $has_children = $children !== [];
        $expanded = $has_children && zerti_product_edit_category_branch_has_selection($node, $selected);
        $checked = in_array($id, $selected, true);

        echo '<li class="zerti-product-cat-tree__item" data-zerti-product-cat-tree-item role="treeitem"';
        echo $has_children ? ' aria-expanded="' . ($expanded ? 'true' : 'false') . '"' : '';
        echo '>';

        echo '<div class="zerti-product-cat-tree__row" style="--zerti-cat-depth:' . esc_attr((string) $depth) . '">';

        if ($has_children) {
            printf(
                '<button type="button" class="zerti-product-cat-tree__toggle btn btn-link btn-sm p-0" data-zerti-product-cat-toggle aria-expanded="%1$s" aria-label="%2$s"><i class="fa-solid %3$s" aria-hidden="true"></i></button>',
                $expanded ? 'true' : 'false',
                esc_attr($expanded ? __('Close', 'zerti') : __('Open', 'zerti')),
                esc_attr(zerti_tree_chevron_class($expanded))
            );
        } else {
            echo '<span class="zerti-product-cat-tree__toggle-spacer" aria-hidden="true"></span>';
        }

        echo '<div class="form-check zerti-product-cat-tree__check mb-0">';
        printf(
            '<input class="form-check-input" type="checkbox" name="category_ids[]" id="%1$s" value="%2$s"%3$s%4$s />',
            esc_attr('zertiProductCat' . $id),
            esc_attr((string) $id),
            checked($checked, true, false),
            esc_attr($dis)
        );
        printf(
            '<label class="form-check-label" for="%1$s">%2$s</label>',
            esc_attr('zertiProductCat' . $id),
            esc_html($name)
        );
        echo '</div>';
        echo '</div>';

        if ($has_children) {
            echo '<div class="zerti-product-cat-tree__children" data-zerti-product-cat-children' . ($expanded ? '' : ' hidden') . '>';
            zerti_product_edit_render_category_tree($children, $selected, $read_only, $depth + 1, false);
            echo '</div>';
        }

        echo '</li>';
    }

    echo '</ul>';
    if ($is_root) {
        echo '</div>';
    }
}

/**
 * Format WC sale date for the active admin calendar input.
 */
function zerti_product_edit_format_sale_date($date): string
{
    if ($date === null || $date === '') {
        return '';
    }

    $ts = null;
    if (is_a($date, 'WC_DateTime')) {
        $ts = $date->getTimestamp();
    } elseif (is_numeric($date)) {
        $ts = (int) $date;
    }

    if ($ts === null || $ts <= 0) {
        return '';
    }

    $local = (new DateTimeImmutable('@' . $ts))->setTimezone(zerti_get_timezone());

    if (function_exists('zerti_is_gregorian_calendar') && zerti_is_gregorian_calendar()) {
        return $local->format('Y-m-d');
    }

    return zerti_format_date($local);
}

/**
 * Parse sale date from POST using the configured admin calendar.
 */
function zerti_product_edit_parse_sale_date(string $raw): ?int
{
    return zerti_parse_admin_date_string($raw);
}

/**
 * Comma-separated product IDs to int list.
 *
 * @return list<int>
 */
function zerti_product_edit_parse_product_id_list(string $raw): array
{
    $raw = zerti_to_latin_digits(trim($raw));
    if ($raw === '') {
        return [];
    }
    $parts = preg_split('/[\s,،]+/u', $raw) ?: [];
    $ids = [];
    foreach ($parts as $part) {
        $id = absint($part);
        if ($id > 0) {
            $ids[] = $id;
        }
    }

    return array_values(array_unique($ids));
}

/**
 * Product tag names for a product (comma-separated display string).
 */
function zerti_product_edit_tags_display_string(int $product_id): string
{
    if ($product_id < 1 || !taxonomy_exists('product_tag')) {
        return '';
    }
    $terms = get_the_terms($product_id, 'product_tag');
    if (!is_array($terms)) {
        return '';
    }
    $names = [];
    foreach ($terms as $term) {
        if ($term instanceof WP_Term) {
            $names[] = $term->name;
        }
    }

    return implode('، ', $names);
}

/**
 * Product brand names for a product (comma-separated display string).
 */
function zerti_product_edit_brands_display_string(int $product_id): string
{
    if ($product_id < 1 || !zerti_product_brands_available()) {
        return '';
    }

    $taxonomy = zerti_product_brand_taxonomy();
    $terms = get_the_terms($product_id, $taxonomy);
    if (!is_array($terms)) {
        return '';
    }

    $names = [];
    foreach ($terms as $term) {
        if ($term instanceof WP_Term) {
            $names[] = $term->name;
        }
    }

    return implode('، ', $names);
}

/**
 * Gallery items for the editor UI.
 *
 * @return list<array{id:int,url:string}>
 */
function zerti_product_edit_gallery_items(?WC_Product $product): array
{
    if (!$product) {
        return [];
    }
    $items = [];
    foreach ($product->get_gallery_image_ids() as $attach_id) {
        $attach_id = (int) $attach_id;
        if ($attach_id < 1) {
            continue;
        }
        $url = (string) wp_get_attachment_image_url($attach_id, 'thumbnail');
        if ($url !== '') {
            $items[] = ['id' => $attach_id, 'url' => $url];
        }
    }

    return $items;
}

/**
 * Linked product chips for upsells / cross-sells UI.
 *
 * @param list<int> $ids
 * @return list<array{id:int,name:string,sku:string}>
 */
function zerti_product_edit_linked_product_items(array $ids): array
{
    $items = [];
    foreach ($ids as $id) {
        $id = (int) $id;
        if ($id < 1) {
            continue;
        }
        $product = wc_get_product($id);
        if (!is_a($product, 'WC_Product') || $product->is_type('variation')) {
            continue;
        }
        $items[] = [
            'id' => $id,
            'name' => (string) $product->get_name(),
            'sku' => (string) $product->get_sku(),
        ];
    }

    return $items;
}

/**
 * Global attribute taxonomy choices (taxonomy slug => label).
 *
 * @return array<string, string>
 */
function zerti_product_edit_global_attribute_choices(): array
{
    $choices = ['' => __('Custom', 'zerti')];
    if (!function_exists('wc_get_attribute_taxonomies')) {
        return $choices;
    }
    foreach (wc_get_attribute_taxonomies() as $tax) {
        if (!isset($tax->attribute_name)) {
            continue;
        }
        $taxonomy = function_exists('wc_attribute_taxonomy_name')
            ? wc_attribute_taxonomy_name((string) $tax->attribute_name)
            : 'pa_' . (string) $tax->attribute_name;
        $label = isset($tax->attribute_label) && (string) $tax->attribute_label !== ''
            ? (string) $tax->attribute_label
            : (string) $tax->attribute_name;
        $choices[$taxonomy] = $label;
    }

    return $choices;
}

/**
 * Attribute rows for the editor UI.
 *
 * @return list<array{taxonomy:string,name:string,values:string,visible:bool,variation:bool}>
 */
function zerti_product_edit_attributes_from_product(?WC_Product $product): array
{
    if (!$product) {
        return [];
    }
    $rows = [];
    foreach ($product->get_attributes() as $attribute) {
        if (!is_a($attribute, 'WC_Product_Attribute')) {
            continue;
        }
        $is_taxonomy = $attribute->is_taxonomy();
        $taxonomy = $is_taxonomy ? (string) $attribute->get_name() : '';
        $name = $is_taxonomy && function_exists('wc_attribute_label')
            ? (string) wc_attribute_label($taxonomy)
            : (string) $attribute->get_name();
        $options = $attribute->get_options();
        $value_labels = [];
        if ($is_taxonomy && $taxonomy !== '') {
            foreach ($options as $opt) {
                $term = null;
                if (is_numeric($opt)) {
                    $found = get_term((int) $opt, $taxonomy);
                    $term = $found instanceof WP_Term ? $found : null;
                } else {
                    $term = zerti_product_edit_find_attribute_term($taxonomy, (string) $opt);
                }
                if ($term instanceof WP_Term) {
                    $value_labels[] = $term->name;
                } elseif (is_string($opt) && $opt !== '') {
                    $value_labels[] = $opt;
                }
            }
        } else {
            foreach ($options as $opt) {
                if (is_string($opt) && $opt !== '') {
                    $value_labels[] = $opt;
                }
            }
        }
        $rows[] = [
            'taxonomy' => $taxonomy,
            'name' => $name,
            'values' => zerti_product_edit_implode_attribute_value_names($value_labels),
            'visible' => (bool) $attribute->get_visible(),
            'variation' => (bool) $attribute->get_variation(),
        ];
    }

    return $rows;
}

/**
 * Resolve a taxonomy term slug from a display name.
 */
function zerti_product_edit_term_slug_for_name(string $taxonomy, string $name): string
{
    $term = zerti_product_edit_find_attribute_term($taxonomy, $name);
    if ($term instanceof WP_Term) {
        return (string) $term->slug;
    }

    return sanitize_title($name);
}

/**
 * Variation attribute definitions for the variations UI.
 *
 * @param list<array{taxonomy?:string,name?:string,values?:string,variation?:bool}> $attribute_rows
 * @return list<array{key:string,label:string,options:list<array{slug:string,label:string}>}>
 */
function zerti_product_edit_variation_attribute_defs(array $attribute_rows): array
{
    $defs = [];
    foreach ($attribute_rows as $row) {
        if (empty($row['variation'])) {
            continue;
        }
        $taxonomy = (string) ($row['taxonomy'] ?? '');
        $name = (string) ($row['name'] ?? '');
        $key = $taxonomy !== '' ? $taxonomy : sanitize_title($name !== '' ? $name : __('Attribute', 'zerti'));
        if ($key === '') {
            continue;
        }
        $label = $name;
        if ($label === '' && $taxonomy !== '' && function_exists('wc_attribute_label')) {
            $label = (string) wc_attribute_label($taxonomy);
        }
        if ($label === '') {
            $label = $key;
        }
        $options = [];
        foreach (zerti_product_edit_parse_attribute_value_names((string) ($row['values'] ?? '')) as $value_name) {
            $options[] = [
                'slug' => $taxonomy !== ''
                    ? zerti_product_edit_term_slug_for_name($taxonomy, $value_name)
                    : $value_name,
                'label' => $value_name,
            ];
        }
        if ($options === []) {
            continue;
        }
        $defs[] = [
            'key' => $key,
            'label' => $label,
            'options' => $options,
        ];
    }

    return $defs;
}

/**
 * @return list<array{variation_id:int,attributes:array<string,string>,sku:string,regular_price:string,sale_price:string,stock_quantity:string,manage_stock:bool,enabled:bool,label:string}>
 */
function zerti_product_edit_variations_from_product(?WC_Product $product): array
{
    if (!$product || !$product->is_type('variable')) {
        return [];
    }

    $rows = [];
    foreach ($product->get_children() as $variation_id) {
        $variation_id = (int) $variation_id;
        if ($variation_id < 1) {
            continue;
        }
        $variation = wc_get_product($variation_id);
        if (!$variation || !$variation->is_type('variation')) {
            continue;
        }
        $attributes = [];
        $label_parts = [];
        foreach ($variation->get_attributes() as $attr_key => $attr_slug) {
            $attr_key = (string) $attr_key;
            $attr_slug = (string) $attr_slug;
            if ($attr_key === '' || $attr_slug === '') {
                continue;
            }
            $attributes[$attr_key] = $attr_slug;
            if (taxonomy_exists($attr_key)) {
                $term = get_term_by('slug', $attr_slug, $attr_key);
                $label_parts[] = $term instanceof WP_Term ? $term->name : $attr_slug;
            } else {
                $label_parts[] = $attr_slug;
            }
        }
        $qty = $variation->get_stock_quantity();
        $rows[] = [
            'variation_id' => $variation_id,
            'attributes' => $attributes,
            'sku' => (string) $variation->get_sku(),
            'regular_price' => (string) $variation->get_regular_price(),
            'sale_price' => (string) $variation->get_sale_price(),
            'stock_quantity' => $qty !== null ? (string) wc_stock_amount($qty) : '',
            'manage_stock' => (bool) $variation->get_manage_stock(),
            'enabled' => $variation->get_status() === 'publish',
            'label' => $label_parts !== [] ? implode(' / ', $label_parts) : ('#' . $variation_id),
        ];
    }

    return $rows;
}

/**
 * Download file rows for the editor UI.
 *
 * @return list<array{key:string,name:string,file:string}>
 */
function zerti_product_edit_downloads_from_product(?WC_Product $product): array
{
    if (!$product) {
        return [];
    }
    $rows = [];
    foreach ($product->get_downloads() as $key => $download) {
        if (!is_array($download)) {
            continue;
        }
        $file = isset($download['file']) ? (string) $download['file'] : '';
        $name = isset($download['name']) ? (string) $download['name'] : '';
        if ($file === '' && $name === '') {
            continue;
        }
        $rows[] = [
            'key' => (string) $key,
            'name' => $name,
            'file' => $file,
        ];
    }

    return $rows;
}

/**
 * Parse linked product IDs from POST (array or comma-separated).
 *
 * @param array<string, mixed> $source
 * @return list<int>
 */
function zerti_product_edit_linked_ids_from_array(array $source, string $field): array
{
    if (isset($source[$field]) && is_array($source[$field])) {
        $ids = [];
        foreach ($source[$field] as $raw) {
            $id = absint(wp_unslash((string) $raw));
            if ($id > 0) {
                $ids[] = $id;
            }
        }

        return array_values(array_unique($ids));
    }
    if (isset($source[$field])) {
        return zerti_product_edit_parse_product_id_list(wp_unslash((string) $source[$field]));
    }

    return [];
}

/**
 * Find an existing global attribute term without creating a duplicate.
 */
function zerti_product_edit_find_attribute_term(string $taxonomy, string $opt): ?WP_Term
{
    $opt = trim($opt);
    if ($taxonomy === '' || $opt === '' || !taxonomy_exists($taxonomy)) {
        return null;
    }

    $term = get_term_by('name', $opt, $taxonomy);
    if ($term instanceof WP_Term) {
        return $term;
    }

    $term = get_term_by('slug', $opt, $taxonomy);
    if ($term instanceof WP_Term) {
        return $term;
    }

    $slug = sanitize_title($opt);
    if ($slug !== '' && $slug !== $opt) {
        $term = get_term_by('slug', $slug, $taxonomy);
        if ($term instanceof WP_Term) {
            return $term;
        }
    }

    if (is_numeric($opt)) {
        $found = get_term((int) $opt, $taxonomy);
        if ($found instanceof WP_Term) {
            return $found;
        }
    }

    return null;
}

/**
 * Join attribute value labels using WooCommerce's delimiter.
 *
 * @param list<string> $names
 */
function zerti_product_edit_implode_attribute_value_names(array $names): string
{
    $names = array_values(array_filter(array_map('strval', $names), static function ($name) {
        return trim($name) !== '';
    }));
    if ($names === []) {
        return '';
    }
    if (function_exists('wc_implode_text_attributes')) {
        return wc_implode_text_attributes($names);
    }

    return implode(' | ', $names);
}

/**
 * Split an attribute values string into display names.
 *
 * WooCommerce uses "|" only. Also accept commas (including Persian) so existing
 * editor values still parse, but never split on spaces inside a value.
 *
 * @return list<string>
 */
function zerti_product_edit_parse_attribute_value_names(string $values): array
{
    $values = html_entity_decode($values, ENT_QUOTES, get_bloginfo('charset'));
    $normalized = preg_replace('/\s*[,،]+\s*/u', ' | ', $values);
    if (!is_string($normalized) || $normalized === '') {
        $normalized = $values;
    }
    if (function_exists('wc_get_text_attributes')) {
        return array_values(wc_get_text_attributes($normalized));
    }

    $parts = explode('|', $normalized);
    $names = [];
    foreach ($parts as $part) {
        $part = trim((string) $part);
        if ($part !== '') {
            $names[] = $part;
        }
    }

    return $names;
}

/**
 * Sanitize a posted global attribute taxonomy slug (e.g. pa_color).
 */
function zerti_product_edit_sanitize_attribute_taxonomy(string $taxonomy): string
{
    $taxonomy = function_exists('wc_clean') ? wc_clean($taxonomy) : sanitize_text_field($taxonomy);
    if ($taxonomy === '' || $taxonomy === '0') {
        return '';
    }

    return $taxonomy;
}

/**
 * @param array<string, mixed> $source
 * @return list<array{taxonomy:string,name:string,values:string,visible:bool,variation:bool}>
 */
function zerti_product_edit_attribute_rows_from_array(array $source): array
{
    if (!isset($source['attribute_rows']) || !is_array($source['attribute_rows'])) {
        return [];
    }
    $rows = [];
    foreach ($source['attribute_rows'] as $raw) {
        if (!is_array($raw)) {
            continue;
        }
        $taxonomy = isset($raw['taxonomy'])
            ? zerti_product_edit_sanitize_attribute_taxonomy(wp_unslash((string) $raw['taxonomy']))
            : '';
        $name = isset($raw['name']) ? sanitize_text_field(wp_unslash((string) $raw['name'])) : '';
        $values = isset($raw['values']) ? sanitize_text_field(wp_unslash((string) $raw['values'])) : '';
        $visible = !empty($raw['visible']);
        $variation = !empty($raw['variation']);
        if ($taxonomy === '' && $name === '') {
            continue;
        }
        if ($values === '') {
            continue;
        }
        $rows[] = [
            'taxonomy' => $taxonomy,
            'name' => $name,
            'values' => $values,
            'visible' => $visible,
            'variation' => $variation,
        ];
    }

    return $rows;
}

/**
 * @param array<string, mixed> $source
 * @return list<array{variation_id:int,attributes:array<string,string>,sku:string,regular_price:string,sale_price:string,stock_quantity:string,manage_stock:bool,enabled:bool}>
 */
function zerti_product_edit_variation_rows_from_array(array $source): array
{
    if (!isset($source['variation_rows']) || !is_array($source['variation_rows'])) {
        return [];
    }

    $decimals = function_exists('wc_get_price_decimals') ? wc_get_price_decimals() : 2;
    $rows = [];
    foreach ($source['variation_rows'] as $raw) {
        if (!is_array($raw)) {
            continue;
        }
        $attributes = [];
        if (isset($raw['attributes']) && is_array($raw['attributes'])) {
            foreach ($raw['attributes'] as $attr_key => $attr_val) {
                $attr_key = function_exists('wc_clean')
                    ? wc_clean(wp_unslash((string) $attr_key))
                    : sanitize_text_field(wp_unslash((string) $attr_key));
                $attr_val = function_exists('wc_clean')
                    ? wc_clean(wp_unslash((string) $attr_val))
                    : sanitize_text_field(wp_unslash((string) $attr_val));
                if ($attr_key !== '' && $attr_val !== '') {
                    $attributes[$attr_key] = $attr_val;
                }
            }
        }
        if ($attributes === []) {
            continue;
        }

        $regular_raw = isset($raw['regular_price']) ? zerti_to_latin_digits(trim(wp_unslash((string) $raw['regular_price']))) : '';
        $sale_raw = isset($raw['sale_price']) ? zerti_to_latin_digits(trim(wp_unslash((string) $raw['sale_price']))) : '';
        $regular_raw = str_replace([',', '٬', ' '], ['', '', ''], $regular_raw);
        $sale_raw = str_replace([',', '٬', ' '], ['', '', ''], $sale_raw);

        $regular_price = '';
        if ($regular_raw !== '' && is_numeric($regular_raw)) {
            $regular_price = wc_format_decimal($regular_raw, $decimals);
        }

        $sale_price = '';
        if ($sale_raw !== '' && is_numeric($sale_raw)) {
            $sale_price = wc_format_decimal($sale_raw, $decimals);
        }

        $stock_raw = isset($raw['stock_quantity']) ? zerti_to_latin_digits(trim(wp_unslash((string) $raw['stock_quantity']))) : '';
        $stock_raw = str_replace([',', '٬', ' '], ['', '', ''], $stock_raw);

        $rows[] = [
            'variation_id' => isset($raw['variation_id']) ? absint(wp_unslash((string) $raw['variation_id'])) : 0,
            'attributes' => $attributes,
            'sku' => isset($raw['sku']) ? wc_clean(wp_unslash((string) $raw['sku'])) : '',
            'regular_price' => $regular_price,
            'sale_price' => $sale_price,
            'stock_quantity' => ($stock_raw !== '' && is_numeric($stock_raw)) ? (string) wc_stock_amount((float) $stock_raw) : '',
            'manage_stock' => !empty($raw['manage_stock']),
            'enabled' => !empty($raw['enabled']),
        ];
    }

    return $rows;
}

/**
 * @param array<string, mixed> $source
 * @return list<array{key:string,name:string,file:string}>
 */
function zerti_product_edit_download_rows_from_array(array $source): array
{
    if (!isset($source['download_rows']) || !is_array($source['download_rows'])) {
        return [];
    }
    $rows = [];
    foreach ($source['download_rows'] as $raw) {
        if (!is_array($raw)) {
            continue;
        }
        $name = isset($raw['name']) ? sanitize_text_field(wp_unslash((string) $raw['name'])) : '';
        $file = isset($raw['file']) ? esc_url_raw(wp_unslash((string) $raw['file'])) : '';
        $key = isset($raw['key']) ? sanitize_key(wp_unslash((string) $raw['key'])) : '';
        if ($name === '' || $file === '') {
            continue;
        }
        $rows[] = [
            'key' => $key,
            'name' => $name,
            'file' => $file,
        ];
    }

    return $rows;
}

/**
 * Map posted variation attributes onto the parent product's variation attributes.
 *
 * Matches WooCommerce's prepare_set_attributes(): taxonomy values are term slugs,
 * custom values are the exact option text.
 *
 * @param array<string, string> $posted
 * @return array<string, string>
 */
function zerti_product_edit_normalize_variation_attributes(WC_Product $product, array $posted): array
{
    $normalized = [];
    $posted_by_sanitized = [];
    foreach ($posted as $key => $value) {
        $posted_by_sanitized[sanitize_title((string) $key)] = (string) $value;
    }

    foreach ($product->get_attributes() as $attribute) {
        if (!is_a($attribute, 'WC_Product_Attribute') || !$attribute->get_variation()) {
            continue;
        }

        $attribute_key = sanitize_title($attribute->get_name());
        $value = '';
        if (isset($posted[$attribute_key]) && (string) $posted[$attribute_key] !== '') {
            $value = (string) $posted[$attribute_key];
        } elseif (isset($posted[$attribute->get_name()]) && (string) $posted[$attribute->get_name()] !== '') {
            $value = (string) $posted[$attribute->get_name()];
        } elseif (isset($posted_by_sanitized[$attribute_key]) && $posted_by_sanitized[$attribute_key] !== '') {
            $value = $posted_by_sanitized[$attribute_key];
        }

        if ($value === '') {
            $normalized[$attribute_key] = '';
            continue;
        }

        if ($attribute->is_taxonomy()) {
            $taxonomy = (string) $attribute->get_name();
            $term = zerti_product_edit_find_attribute_term($taxonomy, $value);
            $normalized[$attribute_key] = $term instanceof WP_Term
                ? (string) $term->slug
                : sanitize_title($value);
            continue;
        }

        $matched = '';
        foreach ($attribute->get_options() as $option) {
            $option = (string) $option;
            if ($option === $value || strcasecmp($option, $value) === 0) {
                $matched = $option;
                break;
            }
            if (sanitize_title($option) === $value || sanitize_title($option) === sanitize_title($value)) {
                $matched = $option;
                break;
            }
        }

        $raw = $matched !== '' ? $matched : $value;
        $normalized[$attribute_key] = html_entity_decode(
            function_exists('wc_clean') ? wc_clean($raw) : sanitize_text_field($raw),
            ENT_QUOTES,
            get_bloginfo('charset')
        );
    }

    return $normalized;
}

/**
 * Apply attribute rows to a product object.
 *
 * @param list<array{taxonomy:string,name:string,values:string,visible:bool,variation?:bool}> $rows
 */
function zerti_product_edit_apply_attributes(WC_Product $product, array $rows): void
{
    $attributes = [];
    $position = 0;
    foreach ($rows as $row) {
        $taxonomy = zerti_product_edit_sanitize_attribute_taxonomy((string) ($row['taxonomy'] ?? ''));
        $name = (string) ($row['name'] ?? '');
        $values_raw = (string) ($row['values'] ?? '');
        $visible = !empty($row['visible']);
        $variation = $product->is_type('variable') && !empty($row['variation']);

        $options = zerti_product_edit_parse_attribute_value_names($values_raw);
        if ($options === []) {
            continue;
        }

        $attribute = new WC_Product_Attribute();
        $attribute->set_position($position);
        $attribute->set_visible($visible);
        $attribute->set_variation($variation);
        ++$position;

        if ($taxonomy !== '' && taxonomy_exists($taxonomy)) {
            $attribute_id = function_exists('wc_attribute_taxonomy_id_by_name')
                ? (int) wc_attribute_taxonomy_id_by_name($taxonomy)
                : 0;
            $attribute->set_id($attribute_id);
            $attribute->set_name($taxonomy);

            $term_ids = [];
            foreach ($options as $opt) {
                $term = zerti_product_edit_find_attribute_term($taxonomy, $opt);
                if ($term instanceof WP_Term) {
                    $term_ids[] = (int) $term->term_id;
                }
            }
            if ($term_ids === []) {
                --$position;
                continue;
            }
            $attribute->set_options(array_values(array_unique($term_ids)));
        } else {
            $attribute->set_id(0);
            $attribute->set_name($name !== '' ? $name : __('Attribute', 'zerti'));
            $attribute->set_options($options);
        }

        $attributes[] = $attribute;
    }

    $product->set_attributes($attributes);
}

/**
 * Create, update, and remove variation children for a variable product.
 *
 * @param list<array{variation_id:int,attributes:array<string,string>,sku:string,regular_price:string,sale_price:string,stock_quantity:string,manage_stock:bool,enabled:bool}> $rows
 */
function zerti_product_edit_apply_variations(WC_Product $product, array $rows): void
{
    if (!$product->is_type('variable')) {
        return;
    }

    $parent_id = $product->get_id();
    if ($parent_id < 1) {
        return;
    }

    $existing_ids = array_map('intval', $product->get_children());
    $kept_ids = [];

    foreach ($rows as $row) {
        $attributes = isset($row['attributes']) && is_array($row['attributes']) ? $row['attributes'] : [];
        $attributes = zerti_product_edit_normalize_variation_attributes($product, $attributes);
        $has_value = false;
        foreach ($attributes as $attr_val) {
            if ((string) $attr_val !== '') {
                $has_value = true;
                break;
            }
        }
        if (!$has_value) {
            continue;
        }

        $variation_id = (int) ($row['variation_id'] ?? 0);
        $variation = null;

        if ($variation_id > 0 && in_array($variation_id, $existing_ids, true)) {
            $candidate = wc_get_product($variation_id);
            if (
                $candidate instanceof WC_Product_Variation
                && (int) $candidate->get_parent_id() === $parent_id
            ) {
                $variation = $candidate;
            } else {
                $variation_id = 0;
            }
        }

        if (!$variation) {
            $variation = new WC_Product_Variation();
            $variation->set_parent_id($parent_id);
        }

        $variation->set_attributes($attributes);
        $variation->set_sku((string) ($row['sku'] ?? ''));

        $regular_price = (string) ($row['regular_price'] ?? '');
        $sale_price = (string) ($row['sale_price'] ?? '');
        $variation->set_regular_price($regular_price);
        if ($sale_price !== '') {
            $variation->set_sale_price($sale_price);
            $variation->set_price($sale_price);
        } else {
            $variation->set_sale_price('');
            $variation->set_price($regular_price);
        }

        $manage_stock = !empty($row['manage_stock']);
        $variation->set_manage_stock($manage_stock);
        if ($manage_stock) {
            $qty_raw = (string) ($row['stock_quantity'] ?? '');
            if ($qty_raw !== '' && is_numeric($qty_raw)) {
                $qty = wc_stock_amount((float) $qty_raw);
                $variation->set_stock_quantity($qty);
                $variation->set_stock_status($qty > 0 ? 'instock' : 'outofstock');
            } else {
                $variation->set_stock_quantity(null);
            }
        } else {
            $variation->set_stock_quantity(null);
        }

        $variation->set_status(!empty($row['enabled']) ? 'publish' : 'private');

        $saved_id = $variation->save();
        if ($saved_id > 0) {
            $kept_ids[] = $saved_id;
        }
    }

    foreach ($existing_ids as $existing_id) {
        if (in_array($existing_id, $kept_ids, true)) {
            continue;
        }
        $orphan = wc_get_product($existing_id);
        if ($orphan instanceof WC_Product_Variation && (int) $orphan->get_parent_id() === $parent_id) {
            $orphan->delete(true);
        }
    }

    if (is_callable(['WC_Product_Variable', 'sync'])) {
        WC_Product_Variable::sync($parent_id);
    }
    wc_delete_product_transients($parent_id);
}

/**
 * Apply downloadable files to a product object.
 *
 * @param list<array{key:string,name:string,file:string}> $rows
 */
function zerti_product_edit_apply_downloads(WC_Product $product, array $rows, array $limits): void
{
    if (empty($limits['downloadable'])) {
        $product->set_downloads([]);
        $product->set_download_limit(-1);
        $product->set_download_expiry(-1);

        return;
    }

    $downloads = [];
    foreach ($rows as $row) {
        $file = (string) ($row['file'] ?? '');
        $dl_name = (string) ($row['name'] ?? '');
        if ($file === '' || $dl_name === '') {
            continue;
        }
        $key = (string) ($row['key'] ?? '');
        if ($key === '') {
            $key = md5($file);
        }
        $downloads[$key] = [
            'name' => $dl_name,
            'file' => $file,
        ];
    }
    $product->set_downloads($downloads);

    $limit = isset($limits['download_limit']) ? (int) $limits['download_limit'] : -1;
    $expiry = isset($limits['download_expiry']) ? (int) $limits['download_expiry'] : -1;
    $product->set_download_limit($limit > 0 ? $limit : -1);
    $product->set_download_expiry($expiry > 0 ? $expiry : -1);
}

/**
 * Search products for linked-product picker.
 *
 * @return list<array{id:int,name:string,sku:string,type:string}>
 */
function zerti_product_search_for_picker(string $term, int $exclude_id = 0, int $limit = 20): array
{
    if (!function_exists('wc_get_product') || !function_exists('wc_get_products')) {
        return [];
    }

    $term = trim(zerti_to_latin_digits($term));
    $limit = max(1, min(30, $limit));
    $collected = [];

    if ($term !== '') {
        $args = [
            'limit' => $limit,
            'status' => ['publish', 'private', 'draft', 'pending'],
            'return' => 'ids',
            's' => $term,
            'type' => ['simple', 'variable', 'grouped', 'external'],
        ];
        if ($exclude_id > 0) {
            // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Exclude current product in admin picker search.
            $args['exclude'] = [$exclude_id];
        }
        $ids = wc_get_products($args);
        if (is_array($ids)) {
            foreach ($ids as $id) {
                $collected[(int) $id] = (int) $id;
            }
        }

        global $wpdb;
        if (count($collected) < $limit) {
            $like = '%' . $wpdb->esc_like($term) . '%';
            $sql = $wpdb->prepare(
                "SELECT DISTINCT pm.post_id FROM {$wpdb->postmeta} pm
                INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
                WHERE pm.meta_key = '_sku' AND pm.meta_value LIKE %s
                AND p.post_type = 'product' AND p.post_status IN ('publish','private','draft','pending')
                LIMIT %d",
                $like,
                $limit
            );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
            $sku_ids = $wpdb->get_col($sql);
            if (is_array($sku_ids)) {
                foreach ($sku_ids as $id) {
                    $id = (int) $id;
                    if ($id > 0 && $id !== $exclude_id) {
                        $collected[$id] = $id;
                    }
                }
            }
        }
    }

    $items = [];
    foreach (array_slice(array_values($collected), 0, $limit) as $id) {
        $product = wc_get_product($id);
        if (!is_a($product, 'WC_Product') || $product->is_type('variation')) {
            continue;
        }
        $items[] = [
            'id' => $id,
            'name' => (string) $product->get_name(),
            'sku' => (string) $product->get_sku(),
            'type' => (string) $product->get_type(),
        ];
    }

    return $items;
}

/**
 * Build form context for create (0) or edit (>0).
 *
 * @return array<string, mixed>|WP_Error
 */
function zerti_product_edit_form_context(int $product_id)
{
    if (!function_exists('wc_get_product')) {
        return new WP_Error('zerti_wc_missing', __('WooCommerce is not available.', 'zerti'));
    }

    if (!zerti_user_can_edit_product_in_zerti($product_id)) {
        return new WP_Error('zerti_forbidden', __('You do not have permission to edit products.', 'zerti'));
    }

    $decimals = function_exists('wc_get_price_decimals') ? wc_get_price_decimals() : 2;
    $is_new = $product_id < 1;
    $product = null;
    $type_slug = 'simple';
    $read_only = false;
    $read_only_reason = '';
    $product_url = '';
    $button_text = '';
    $grouped_children = [];

    if (!$is_new) {
        $product = wc_get_product($product_id);
        if (!is_a($product, 'WC_Product')) {
            return new WP_Error('zerti_not_found', __('Product not found.', 'zerti'));
        }
        $type_slug = zerti_product_edit_sanitize_type((string) $product->get_type());
        if ($product->is_type('external')) {
            $product_url = (string) $product->get_product_url();
            $button_text = (string) $product->get_button_text();
        }
        if ($product->is_type('grouped')) {
            $child_ids = $product->get_children();
            $grouped_children = zerti_product_edit_linked_product_items(is_array($child_ids) ? $child_ids : []);
        }
    }

    $regular = $product ? (string) $product->get_regular_price() : '';
    $sale = $product ? (string) $product->get_sale_price() : '';
    $manage_stock = $product ? $product->get_manage_stock() : true;
    $qty = $product ? $product->get_stock_quantity() : null;
    $stock_status = $product ? (string) $product->get_stock_status() : 'instock';
    $status = $product ? (string) $product->get_status() : 'publish';
    if (!array_key_exists($status, zerti_product_edit_status_choices())) {
        $status = 'draft';
    }

    $catalog_visibility = $product ? (string) $product->get_catalog_visibility() : 'visible';
    if (!array_key_exists($catalog_visibility, zerti_product_edit_catalog_visibility_choices())) {
        $catalog_visibility = 'visible';
    }

    $tax_status = $product ? (string) $product->get_tax_status() : 'taxable';
    if (!array_key_exists($tax_status, zerti_product_edit_tax_status_choices())) {
        $tax_status = 'taxable';
    }

    $tax_class = $product ? (string) $product->get_tax_class() : '';
    $tax_class_choices = zerti_product_edit_tax_class_choices();
    if ($tax_class !== '' && !array_key_exists($tax_class, $tax_class_choices)) {
        $tax_class_choices[$tax_class] = $tax_class;
    }

    $backorders = $product ? (string) $product->get_backorders() : 'no';
    if (!array_key_exists($backorders, zerti_product_edit_backorder_choices())) {
        $backorders = 'no';
    }

    $low_stock = $product ? $product->get_low_stock_amount() : '';
    $shipping_class_id = $product ? (int) $product->get_shipping_class_id() : 0;

    $image_id = $product ? (int) $product->get_image_id() : 0;
    $image_url = $image_id > 0 ? (string) wp_get_attachment_image_url($image_id, 'medium') : '';

    $category_ids = [];
    if ($product_id > 0) {
        $terms = get_the_terms($product_id, zerti_product_cat_taxonomy());
        if (is_array($terms)) {
            foreach ($terms as $term) {
                if ($term instanceof WP_Term) {
                    $category_ids[] = (int) $term->term_id;
                }
            }
        }
    }

    $upsell_ids = $product ? array_map('intval', $product->get_upsell_ids()) : [];
    $cross_sell_ids = $product ? array_map('intval', $product->get_cross_sell_ids()) : [];
    $download_limit = $product ? (int) $product->get_download_limit() : -1;
    $download_expiry = $product ? (int) $product->get_download_expiry() : -1;

    $wp_edit_link = '';
    if ($product_id > 0) {
        $raw = get_edit_post_link($product_id, 'raw');
        if (is_string($raw) && $raw !== '') {
            $wp_edit_link = $raw;
        }
    }

    $weight_unit = function_exists('get_option') ? get_option('woocommerce_weight_unit', 'kg') : 'kg';
    $dimension_unit = function_exists('get_option') ? get_option('woocommerce_dimension_unit', 'cm') : 'cm';

    return [
        'product_id' => $product_id,
        'is_new' => $is_new,
        'read_only' => $read_only,
        'read_only_reason' => $read_only_reason,
        'type_slug' => $type_slug,
        'type_choices' => zerti_product_edit_type_choices(),
        'product_url' => $product_url,
        'button_text' => $button_text,
        'grouped_children' => $grouped_children,
        'name' => $product ? (string) $product->get_name() : '',
        'sku' => $product ? (string) $product->get_sku() : '',
        'short_description' => $product ? (string) $product->get_short_description() : '',
        'description' => $product ? (string) $product->get_description() : '',
        'regular_price' => $regular !== '' ? wc_format_decimal($regular, $decimals) : '',
        'sale_price' => $sale !== '' ? wc_format_decimal($sale, $decimals) : '',
        'sale_price_dates_from' => $product ? zerti_product_edit_format_sale_date($product->get_date_on_sale_from()) : '',
        'sale_price_dates_to' => $product ? zerti_product_edit_format_sale_date($product->get_date_on_sale_to()) : '',
        'tax_status' => $tax_status,
        'tax_class' => $tax_class,
        'manage_stock' => (bool) $manage_stock,
        'stock_quantity' => ($manage_stock && $qty !== null) ? (string) wc_stock_amount($qty) : '',
        'stock_status' => $stock_status,
        'backorders' => $backorders,
        'low_stock_amount' => ($low_stock !== null && $low_stock !== '') ? (string) wc_stock_amount($low_stock) : '',
        'sold_individually' => $product ? $product->get_sold_individually() : false,
        'status' => $status,
        'catalog_visibility' => $catalog_visibility,
        'featured' => $product ? $product->get_featured() : false,
        'virtual' => $product ? $product->get_virtual() : false,
        'downloadable' => $product ? $product->get_downloadable() : false,
        'weight' => $product && $product->get_weight() !== '' ? (string) $product->get_weight() : '',
        'length' => $product && $product->get_length() !== '' ? (string) $product->get_length() : '',
        'width' => $product && $product->get_width() !== '' ? (string) $product->get_width() : '',
        'height' => $product && $product->get_height() !== '' ? (string) $product->get_height() : '',
        'shipping_class_id' => $shipping_class_id,
        'upsell_products' => zerti_product_edit_linked_product_items($upsell_ids),
        'cross_sell_products' => zerti_product_edit_linked_product_items($cross_sell_ids),
        'attribute_rows' => zerti_product_edit_attributes_from_product($product),
        'variation_rows' => zerti_product_edit_variations_from_product($product),
        'variation_attribute_defs' => zerti_product_edit_variation_attribute_defs(
            zerti_product_edit_attributes_from_product($product)
        ),
        'global_attribute_choices' => zerti_product_edit_global_attribute_choices(),
        'download_rows' => zerti_product_edit_downloads_from_product($product),
        'download_limit' => $download_limit > 0 ? (string) $download_limit : '',
        'download_expiry' => $download_expiry > 0 ? (string) $download_expiry : '',
        'purchase_note' => $product ? (string) $product->get_purchase_note() : '',
        'menu_order' => $product ? (int) $product->get_menu_order() : 0,
        'reviews_allowed' => $product ? $product->get_reviews_allowed() : true,
        'image_id' => $image_id,
        'image_url' => $image_url,
        'gallery_items' => zerti_product_edit_gallery_items($product),
        'product_tags' => $product_id > 0 ? zerti_product_edit_tags_display_string($product_id) : '',
        'product_brands' => $product_id > 0 ? zerti_product_edit_brands_display_string($product_id) : '',
        'brands_available' => zerti_product_brands_available(),
        'category_ids' => $category_ids,
        'category_tree' => zerti_product_edit_category_tree(),
        'status_choices' => zerti_product_edit_status_choices(),
        'stock_status_choices' => zerti_product_edit_stock_status_choices(),
        'catalog_visibility_choices' => zerti_product_edit_catalog_visibility_choices(),
        'tax_status_choices' => zerti_product_edit_tax_status_choices(),
        'tax_class_choices' => $tax_class_choices,
        'backorder_choices' => zerti_product_edit_backorder_choices(),
        'shipping_class_choices' => zerti_product_edit_shipping_class_choices(),
        'weight_unit' => $weight_unit,
        'dimension_unit' => $dimension_unit,
        'wp_edit_link' => $wp_edit_link,
        'back_url' => function_exists('zerti_admin_inventory_warehouse_list_return_url')
            ? zerti_admin_inventory_warehouse_list_return_url()
            : zerti_admin_route_url(['tab' => 'inventory', 'inv' => 'warehouse']),
    ];
}

/**
 * Open a WooCommerce-style options_group wrapper.
 */
function zerti_wc_options_group_open(): void
{
    echo '<div class="zerti-wc-options-group options_group">';
}

/**
 * Close options_group wrapper.
 */
function zerti_wc_options_group_close(): void
{
    echo '</div>';
}

/**
 * Allowed HTML tags/attributes for zerti_wc_form_field() control markup.
 *
 * @return array<string, array<string, bool>>
 */
function zerti_wc_form_field_allowed_html(): array
{
    $input_attrs = [
        'type'                          => true,
        'class'                         => true,
        'id'                            => true,
        'name'                          => true,
        'value'                         => true,
        'dir'                           => true,
        'inputmode'                     => true,
        'autocomplete'                  => true,
        'placeholder'                   => true,
        'min'                           => true,
        'disabled'                      => true,
        'readonly'                      => true,
        'checked'                       => true,
        'data-zerti-jalali-date'        => true,
        'data-zerti-product-manage-stock' => true,
        'data-zerti-linked-search'      => true,
    ];

    return [
        'input'    => $input_attrs,
        'select'   => [
            'class'    => true,
            'id'       => true,
            'name'     => true,
            'disabled' => true,
        ],
        'option'   => [
            'value'    => true,
            'selected' => true,
        ],
        'textarea' => [
            'class'    => true,
            'id'       => true,
            'name'     => true,
            'rows'     => true,
            'dir'      => true,
            'disabled' => true,
            'readonly' => true,
        ],
        'label'    => [
            'class' => true,
            'for'   => true,
        ],
        'span'     => [
            'class'       => true,
            'dir'         => true,
            'aria-hidden' => true,
        ],
        'div'      => [
            'class'                    => true,
            'data-zerti-linked-picker' => true,
            'hidden'                   => true,
        ],
        'ul'       => [
            'class'                     => true,
            'hidden'                    => true,
            'data-zerti-linked-chips'   => true,
            'data-zerti-linked-results' => true,
        ],
        'li'       => [
            'class'   => true,
            'data-id' => true,
        ],
        'button'   => [
            'type'                     => true,
            'class'                    => true,
            'data-zerti-linked-remove' => true,
            'aria-label'               => true,
        ],
    ];
}

/**
 * WooCommerce-style label + field row (Bootstrap inputs inside).
 */
function zerti_wc_form_field_buffered(string $id, string $label, callable $render, string $tip = '', string $field_class = ''): void
{
    $input_html = zerti_capture_output(static function () use ($render): void {
        $render();
    });
    zerti_wc_form_field($id, $label, $input_html, $tip, $field_class);
}

/**
 * WooCommerce-style label + field row (Bootstrap inputs inside).
 */
function zerti_wc_form_field(string $id, string $label, string $input_html, string $tip = '', string $field_class = ''): void
{
    $classes = trim('zerti-wc-form-field form-field ' . $field_class);
    echo '<p class="' . esc_attr($classes) . '">';
    echo '<label for="' . esc_attr($id) . '">' . esc_html($label) . '</label>';
    echo wp_kses($input_html, zerti_wc_form_field_allowed_html());
    if ($tip !== '') {
        echo '<span class="zerti-wc-form-field__description">' . esc_html($tip) . '</span>';
    }
    echo '</p>';
}

/**
 * Sidebar / meta postbox (WooCommerce postbox pattern).
 */
function zerti_wc_postbox_open(string $title, string $id = ''): void
{
    $id_attr = $id !== '' ? ' id="' . esc_attr($id) . '"' : '';
    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- id_attr is built with esc_attr() only.
    echo '<div class="zerti-wc-postbox zerti-wc-postbox--meta mb-3"' . $id_attr . '>';
    echo '<div class="zerti-wc-postbox__header py-2 px-3">';
    echo '<h2 class="zerti-wc-postbox__title h6 mb-0">' . esc_html($title) . '</h2>';
    echo '</div><div class="zerti-wc-postbox__inside">';
}

/**
 * Close postbox.
 */
function zerti_wc_postbox_close(): void
{
    echo '</div></div>';
}

/**
 * Render linked-product picker (upsells / cross-sells).
 *
 * @param list<array{id:int,name:string,sku:string}> $products
 */
function zerti_product_edit_render_linked_picker(string $field, string $label, string $hint, array $products, bool $read_only): void
{
    $field_ids = [
        'upsell_ids' => 'zertiUpsellPicker',
        'cross_sell_ids' => 'zertiCrossSellPicker',
        'children' => 'zertiGroupedChildrenPicker',
    ];
    $field_id = $field_ids[$field] ?? 'zertiLinkedPicker_' . sanitize_key($field);
    ob_start();
    ?>
        <ul class="zerti-product-linked-picker__chips list-unstyled mb-2" data-zerti-linked-chips>
            <?php foreach ($products as $item) : ?>
                <?php
                $pid = (int) ($item['id'] ?? 0);
                if ($pid < 1) {
                    continue;
                }
                $pname = (string) ($item['name'] ?? '');
                $psku = (string) ($item['sku'] ?? '');
                ?>
                <li class="zerti-product-linked-picker__chip" data-id="<?php echo esc_attr((string) $pid); ?>">
                    <span class="zerti-product-linked-picker__chip-label">
                        <?php echo esc_html($pname); ?>
                        <?php if ($psku !== '') : ?>
                            <span class="text-body-secondary small">(<?php echo esc_html($psku); ?>)</span>
                        <?php endif; ?>
                    </span>
                    <input type="hidden" name="<?php echo esc_attr($field); ?>[]" value="<?php echo esc_attr((string) $pid); ?>" />
                    <?php if (!$read_only) : ?>
                        <button type="button" class="btn-close btn-close-sm" data-zerti-linked-remove aria-label="<?php esc_attr_e('Delete', 'zerti'); ?>"></button>
                    <?php endif; ?>
                </li>
            <?php endforeach; ?>
        </ul>
        <?php if (!$read_only) : ?>
            <div class="zerti-product-linked-picker__search position-relative">
                <input
                    type="search"
                    class="form-control form-control-sm"
                    id="<?php echo esc_attr($field_id); ?>"
                    data-zerti-linked-search
                    autocomplete="off"
                    placeholder="<?php esc_attr_e('Product name or SKU…', 'zerti'); ?>"
                />
                <ul class="zerti-product-linked-picker__results list-unstyled mb-0" data-zerti-linked-results hidden></ul>
            </div>
        <?php endif; ?>
    <?php
    $inner = (string) ob_get_clean();
    zerti_wc_form_field(
        $field_id,
        $label,
        '<div class="zerti-product-linked-picker" data-zerti-linked-picker="' . esc_attr($field) . '">' . $inner . '</div>',
        $hint,
        'zerti-wc-form-field--wide'
    );
}

/**
 * Render one attribute row.
 *
 * @param array{taxonomy?:string,name?:string,values?:string,visible?:bool,variation?:bool} $row
 * @param array<string, string> $global_choices
 */
function zerti_product_edit_render_attribute_row(array $row, int $index, array $global_choices, bool $read_only): void
{
    $taxonomy = (string) ($row['taxonomy'] ?? '');
    $name = (string) ($row['name'] ?? '');
    $values = (string) ($row['values'] ?? '');
    $visible = !empty($row['visible']);
    $variation = !empty($row['variation']);
    $dis = $read_only ? ' disabled' : '';
    $ro = $read_only ? ' readonly' : '';
    ?>
    <tr class="zerti-product-attribute-row" data-zerti-attribute-row>
        <td>
            <select class="form-select form-select-sm" name="attribute_rows[<?php echo esc_attr((string) $index); ?>][taxonomy]" data-zerti-attr-taxonomy<?php echo esc_attr($dis); ?>>
                <?php foreach ($global_choices as $tax_slug => $tax_label) : ?>
                    <option value="<?php echo esc_attr((string) $tax_slug); ?>"<?php selected($taxonomy, (string) $tax_slug); ?>><?php echo esc_html($tax_label); ?></option>
                <?php endforeach; ?>
            </select>
            <input
                type="text"
                class="form-control form-control-sm mt-1<?php echo $taxonomy !== '' ? ' d-none' : ''; ?>"
                name="attribute_rows[<?php echo esc_attr((string) $index); ?>][name]"
                data-zerti-attr-custom-name
                placeholder="<?php esc_attr_e('Attribute name', 'zerti'); ?>"
                value="<?php echo esc_attr($taxonomy === '' ? $name : ''); ?>"
                <?php echo esc_attr($ro); ?>
            />
        </td>
        <td data-zerti-attr-values-cell>
            <?php
            $is_global_attr = $taxonomy !== '';
            $selected_names = zerti_product_edit_parse_attribute_value_names($values);
            $attr_terms = $is_global_attr && function_exists('zerti_product_attribute_terms_for_taxonomy')
                ? zerti_product_attribute_terms_for_taxonomy($taxonomy)
                : [];
            $attribute_id = $is_global_attr && function_exists('zerti_product_attribute_id_from_taxonomy')
                ? zerti_product_attribute_id_from_taxonomy($taxonomy)
                : 0;
            $values_field = 'attribute_rows[' . $index . '][values]';
            ?>
            <div class="zerti-product-attr-values" data-zerti-attr-values>
                <input
                    type="text"
                    class="form-control form-control-sm<?php echo $is_global_attr ? ' d-none' : ''; ?>"
                    name="<?php echo $is_global_attr ? '' : esc_attr($values_field); ?>"
                    data-zerti-attr-values-text
                    data-zerti-attr-values-name="<?php echo esc_attr($values_field); ?>"
                    dir="rtl"
                    placeholder="<?php esc_attr_e('e.g. Blue | Large', 'zerti'); ?>"
                    value="<?php echo esc_attr($values); ?>"
                    <?php echo esc_attr($ro); ?>
                />
                <div
                    class="zerti-product-attr-values-global<?php echo $is_global_attr ? '' : ' d-none'; ?>"
                    data-zerti-attr-values-global
                    data-zerti-attr-attribute-id="<?php echo esc_attr((string) $attribute_id); ?>"
                >
                    <select
                        class="wc-enhanced-select zerti-product-attr-values-select"
                        data-zerti-attr-values-select
                        data-placeholder="<?php esc_attr_e('Select values', 'zerti'); ?>"
                        multiple
                        aria-label="<?php esc_attr_e('Select values', 'zerti'); ?>"
                        <?php echo esc_attr($dis); ?>
                    >
                        <?php foreach ($attr_terms as $term) : ?>
                            <?php
                            if (!is_array($term)) {
                                continue;
                            }
                            $term_name = (string) ($term['name'] ?? '');
                            if ($term_name === '') {
                                continue;
                            }
                            ?>
                            <?php $term_slug = (string) ($term['slug'] ?? sanitize_title($term_name)); ?>
                            <option value="<?php echo esc_attr($term_name); ?>" data-slug="<?php echo esc_attr($term_slug); ?>"<?php selected(in_array($term_name, $selected_names, true)); ?>><?php echo esc_html($term_name); ?></option>
                        <?php endforeach; ?>
                    </select>
                    <?php if (!$read_only) : ?>
                        <div class="zerti-product-attr-values-toolbar">
                            <button type="button" class="btn btn-outline-secondary btn-sm" data-zerti-attr-select-all><?php esc_html_e('Select all', 'zerti'); ?></button>
                            <button type="button" class="btn btn-outline-secondary btn-sm" data-zerti-attr-select-none><?php esc_html_e('Select none', 'zerti'); ?></button>
                            <button type="button" class="btn btn-outline-secondary btn-sm ms-auto" data-zerti-attr-create-value><?php esc_html_e('Create value', 'zerti'); ?></button>
                        </div>
                    <?php endif; ?>
                    <input
                        type="hidden"
                        name="<?php echo $is_global_attr ? esc_attr($values_field) : ''; ?>"
                        data-zerti-attr-values-input
                        data-zerti-attr-values-name="<?php echo esc_attr($values_field); ?>"
                        value="<?php echo esc_attr($values); ?>"
                    />
                </div>
            </div>
        </td>
        <td class="text-center">
            <input
                class="form-check-input"
                type="checkbox"
                name="attribute_rows[<?php echo esc_attr((string) $index); ?>][visible]"
                value="1"
                <?php checked($visible); echo esc_attr($dis); ?>
            />
        </td>
        <td class="text-center" data-zerti-attr-variation-col>
            <input
                class="form-check-input"
                type="checkbox"
                name="attribute_rows[<?php echo esc_attr((string) $index); ?>][variation]"
                value="1"
                data-zerti-attr-variation
                <?php checked($variation); echo esc_attr($dis); ?>
            />
        </td>
        <?php if (!$read_only) : ?>
            <td class="text-end">
                <button type="button" class="btn btn-outline-danger btn-sm" data-zerti-attribute-remove>&times;</button>
            </td>
        <?php endif; ?>
    </tr>
    <?php
}

/**
 * Render one variation row.
 *
 * @param array{variation_id?:int,attributes?:array<string,string>,sku?:string,regular_price?:string,sale_price?:string,stock_quantity?:string,manage_stock?:bool,enabled?:bool,label?:string} $row
 * @param list<array{key:string,label:string,options:list<array{slug:string,label:string}>}> $attribute_defs
 */
function zerti_product_edit_render_variation_row(array $row, int $index, array $attribute_defs, bool $read_only): void
{
    $variation_id = (int) ($row['variation_id'] ?? 0);
    $attributes = is_array($row['attributes'] ?? null) ? $row['attributes'] : [];
    $label = (string) ($row['label'] ?? '');
    $sku = (string) ($row['sku'] ?? '');
    $regular_price = (string) ($row['regular_price'] ?? '');
    $sale_price = (string) ($row['sale_price'] ?? '');
    $stock_quantity = (string) ($row['stock_quantity'] ?? '');
    $manage_stock = !empty($row['manage_stock']);
    $enabled = !isset($row['enabled']) || !empty($row['enabled']);
    $dis = $read_only ? ' disabled' : '';
    $ro = $read_only ? ' readonly' : '';
    $display_label = $label !== '' ? $label : __('Variation', 'zerti');
    ?>
    <div class="zerti-variation-item" data-zerti-variation-row>
        <input type="hidden" name="variation_rows[<?php echo esc_attr((string) $index); ?>][variation_id]" value="<?php echo esc_attr((string) $variation_id); ?>" />
        <?php foreach ($attributes as $attr_key => $attr_slug) : ?>
            <input type="hidden" name="variation_rows[<?php echo esc_attr((string) $index); ?>][attributes][<?php echo esc_attr((string) $attr_key); ?>]" value="<?php echo esc_attr((string) $attr_slug); ?>" data-zerti-variation-attr="<?php echo esc_attr((string) $attr_key); ?>" />
        <?php endforeach; ?>
        <div class="zerti-variation-item__head">
            <button type="button" class="zerti-variation-item__toggle btn btn-link btn-sm" data-zerti-variation-toggle aria-expanded="true">
                <i class="fa-solid fa-chevron-down zerti-variation-item__chevron" aria-hidden="true"></i>
                <span class="zerti-variation-item__label" data-zerti-variation-label><?php echo esc_html($display_label); ?></span>
            </button>
            <div class="zerti-variation-item__head-actions d-flex align-items-center gap-2">
                <label class="form-check form-check-inline mb-0 small">
                    <input class="form-check-input" type="checkbox" name="variation_rows[<?php echo esc_attr((string) $index); ?>][enabled]" value="1"<?php checked($enabled); echo esc_attr($dis); ?> />
                    <?php esc_html_e('Enabled', 'zerti'); ?>
                </label>
                <?php if (!$read_only) : ?>
                    <button type="button" class="btn btn-outline-danger btn-sm" data-zerti-variation-remove aria-label="<?php esc_attr_e('Delete', 'zerti'); ?>">&times;</button>
                <?php endif; ?>
            </div>
        </div>
        <div class="zerti-variation-item__body" data-zerti-variation-body>
            <div class="row g-2">
                <div class="col-sm-6 col-md-4">
                    <label class="form-label small mb-1"><?php esc_html_e('Product ID (SKU)', 'zerti'); ?></label>
                    <input type="text" class="form-control form-control-sm" name="variation_rows[<?php echo esc_attr((string) $index); ?>][sku]" dir="ltr" value="<?php echo esc_attr($sku); ?>"<?php echo esc_attr($ro); ?> />
                </div>
                <div class="col-sm-6 col-md-4">
                    <label class="form-label small mb-1"><?php esc_html_e('Regular price', 'zerti'); ?></label>
                    <input type="text" class="form-control form-control-sm wc_input_price" name="variation_rows[<?php echo esc_attr((string) $index); ?>][regular_price]" dir="ltr" inputmode="decimal" placeholder="0" value="<?php echo esc_attr($regular_price); ?>"<?php echo esc_attr($ro); ?> />
                </div>
                <div class="col-sm-6 col-md-4">
                    <label class="form-label small mb-1"><?php esc_html_e('Sale price', 'zerti'); ?></label>
                    <input type="text" class="form-control form-control-sm wc_input_price" name="variation_rows[<?php echo esc_attr((string) $index); ?>][sale_price]" dir="ltr" inputmode="decimal" placeholder="0" value="<?php echo esc_attr($sale_price); ?>"<?php echo esc_attr($ro); ?> />
                </div>
                <div class="col-sm-6 col-md-4">
                    <label class="form-check form-check-inline mb-1 small d-block">
                        <input class="form-check-input" type="checkbox" name="variation_rows[<?php echo esc_attr((string) $index); ?>][manage_stock]" value="1" data-zerti-variation-manage-stock<?php checked($manage_stock); echo esc_attr($dis); ?> />
                        <?php esc_html_e('Manage stock', 'zerti'); ?>
                    </label>
                    <input type="text" class="form-control form-control-sm" name="variation_rows[<?php echo esc_attr((string) $index); ?>][stock_quantity]" dir="ltr" inputmode="numeric" placeholder="0" value="<?php echo esc_attr($stock_quantity); ?>" data-zerti-variation-stock<?php echo esc_attr($manage_stock ? '' : ' disabled'); echo esc_attr($ro); ?> />
                </div>
            </div>
        </div>
    </div>
    <?php
}

/**
 * Render one downloadable file row.
 *
 * @param array{key?:string,name?:string,file?:string} $row
 */
function zerti_product_edit_render_download_row(array $row, int $index, bool $read_only): void
{
    $key = (string) ($row['key'] ?? '');
    $name = (string) ($row['name'] ?? '');
    $file = (string) ($row['file'] ?? '');
    $ro = $read_only ? ' readonly' : '';
    ?>
    <tr data-zerti-download-row>
        <td>
            <input type="hidden" name="download_rows[<?php echo esc_attr((string) $index); ?>][key]" value="<?php echo esc_attr($key); ?>" />
            <input type="text" class="form-control form-control-sm" name="download_rows[<?php echo esc_attr((string) $index); ?>][name]" value="<?php echo esc_attr($name); ?>"<?php echo esc_attr($ro); ?> />
        </td>
        <td>
            <div class="input-group input-group-sm">
                <input type="url" class="form-control" name="download_rows[<?php echo esc_attr((string) $index); ?>][file]" dir="ltr" value="<?php echo esc_attr($file); ?>" data-zerti-download-file<?php echo esc_attr($ro); ?> />
                <?php if (!$read_only) : ?>
                    <button type="button" class="btn btn-outline-secondary" data-zerti-download-pick-file><?php esc_html_e('Select', 'zerti'); ?></button>
                <?php endif; ?>
            </div>
        </td>
        <?php if (!$read_only) : ?>
            <td class="text-end">
                <button type="button" class="btn btn-outline-danger btn-sm" data-zerti-download-remove aria-label="<?php esc_attr_e('Delete', 'zerti'); ?>">&times;</button>
            </td>
        <?php endif; ?>
    </tr>
    <?php
}

/**
 * Echo inventory product editor partial.
 */
function zerti_render_inventory_product_edit(?int $product_id = null): void
{
    $product_id = $product_id ?? zerti_inventory_product_id_from_request();
    if ($product_id === null) {
        $product_id = 0;
    }

    $context = zerti_product_edit_form_context($product_id);
    if (is_wp_error($context)) {
        ?>
        <div class="zerti-component zerti-component-inventory zerti-inventory-product-edit zerti-inventory-sub-slot"
             data-zerti-inv-active="warehouse"
             data-zerti-inv-component="warehouse"
             data-zerti-product-id="0">
            <p class="alert alert-warning mb-0" role="alert"><?php echo esc_html($context->get_error_message()); ?></p>
            <div class="mt-3">
                <a href="<?php echo esc_url(zerti_admin_route_url(['tab' => 'inventory', 'inv' => 'warehouse'])); ?>"
                   class="btn btn-outline-secondary btn-sm zerti-inventory-product-back d-inline-flex align-items-center gap-1">
                    <i class="fa-solid fa-arrow-right" aria-hidden="true"></i>
                    <?php esc_html_e('Back to products list', 'zerti'); ?>
                </a>
            </div>
        </div>
        <?php

        return;
    }

    $path = zerti_inventory_product_edit_component_path();
    if (!is_readable($path)) {
        ?>
        <div class="zerti-component zerti-component-inventory zerti-inventory-product-edit zerti-inventory-sub-slot"
             data-zerti-inv-active="warehouse"
             data-zerti-inv-component="warehouse"
             data-zerti-product-id="<?php echo esc_attr((string) $product_id); ?>">
            <p class="alert alert-warning mb-0" role="alert"><?php esc_html_e('Product edit template is not available.', 'zerti'); ?></p>
        </div>
        <?php

        return;
    }

    /** @var array<string, mixed> $zerti_product_edit */
    $zerti_product_edit = $context;
    include $path;
}

/**
 * Sanitize posted product editor payload.
 *
 * @param array<string, mixed> $source
 * @return array<string, mixed>|WP_Error
 */
function zerti_product_edit_payload_from_array(array $source)
{
    $product_id = isset($source['product_id']) ? absint(wp_unslash((string) $source['product_id'])) : 0;
    if (!zerti_user_can_edit_product_in_zerti($product_id)) {
        return new WP_Error('zerti_forbidden', __('You do not have permission to edit products.', 'zerti'), ['status' => 403]);
    }

    $name = isset($source['name']) ? sanitize_text_field(wp_unslash((string) $source['name'])) : '';
    if ($name === '') {
        return new WP_Error('zerti_name_required', __('Product name is required.', 'zerti'), ['status' => 400]);
    }

    $status = isset($source['status']) ? sanitize_key(wp_unslash((string) $source['status'])) : 'publish';
    if (!array_key_exists($status, zerti_product_edit_status_choices())) {
        $status = 'publish';
    }

    $catalog_visibility = isset($source['catalog_visibility']) ? sanitize_key(wp_unslash((string) $source['catalog_visibility'])) : 'visible';
    if (!array_key_exists($catalog_visibility, zerti_product_edit_catalog_visibility_choices())) {
        $catalog_visibility = 'visible';
    }

    $sku = isset($source['sku']) ? wc_clean(wp_unslash((string) $source['sku'])) : '';
    $short_description = isset($source['short_description']) ? wp_kses_post(wp_unslash((string) $source['short_description'])) : '';
    $description = isset($source['description']) ? wp_kses_post(wp_unslash((string) $source['description'])) : '';

    $regular_raw = isset($source['regular_price']) ? zerti_to_latin_digits(trim(wp_unslash((string) $source['regular_price']))) : '';
    $sale_raw = isset($source['sale_price']) ? zerti_to_latin_digits(trim(wp_unslash((string) $source['sale_price']))) : '';
    $regular_raw = str_replace([',', '٬', ' '], ['', '', ''], $regular_raw);
    $sale_raw = str_replace([',', '٬', ' '], ['', '', ''], $sale_raw);

    $decimals = function_exists('wc_get_price_decimals') ? wc_get_price_decimals() : 2;
    $regular_price = '';
    if ($regular_raw !== '') {
        if (!is_numeric($regular_raw)) {
            return new WP_Error('zerti_price_invalid', __('Invalid regular price.', 'zerti'), ['status' => 400]);
        }
        $regular_price = wc_format_decimal($regular_raw, $decimals);
    }

    $sale_price = '';
    if ($sale_raw !== '') {
        if (!is_numeric($sale_raw)) {
            return new WP_Error('zerti_sale_invalid', __('Invalid sale price.', 'zerti'), ['status' => 400]);
        }
        $sale_price = wc_format_decimal($sale_raw, $decimals);
    }

    $sale_from_raw = isset($source['sale_price_dates_from']) ? sanitize_text_field(wp_unslash((string) $source['sale_price_dates_from'])) : '';
    $sale_to_raw = isset($source['sale_price_dates_to']) ? sanitize_text_field(wp_unslash((string) $source['sale_price_dates_to'])) : '';
    $sale_from_ts = zerti_product_edit_parse_sale_date($sale_from_raw);
    $sale_to_ts = zerti_product_edit_parse_sale_date($sale_to_raw);

    $tax_status = isset($source['tax_status']) ? sanitize_key(wp_unslash((string) $source['tax_status'])) : 'taxable';
    if (!array_key_exists($tax_status, zerti_product_edit_tax_status_choices())) {
        $tax_status = 'taxable';
    }

    $tax_class = isset($source['tax_class']) ? sanitize_title(wp_unslash((string) $source['tax_class'])) : '';
    $tax_class_choices = zerti_product_edit_tax_class_choices();
    if ($tax_class !== '' && !array_key_exists($tax_class, $tax_class_choices)) {
        $tax_class = '';
    }

    $manage_stock = !empty($source['manage_stock']);
    $stock_qty_raw = isset($source['stock_quantity']) ? zerti_to_latin_digits(trim(wp_unslash((string) $source['stock_quantity']))) : '';
    $stock_qty_raw = str_replace([',', '٬', ' '], ['', '', ''], $stock_qty_raw);
    $stock_status = isset($source['stock_status']) ? sanitize_key(wp_unslash((string) $source['stock_status'])) : 'instock';
    if (!array_key_exists($stock_status, zerti_product_edit_stock_status_choices())) {
        $stock_status = 'instock';
    }

    $backorders = isset($source['backorders']) ? sanitize_key(wp_unslash((string) $source['backorders'])) : 'no';
    if (!array_key_exists($backorders, zerti_product_edit_backorder_choices())) {
        $backorders = 'no';
    }

    $low_stock_raw = isset($source['low_stock_amount']) ? zerti_to_latin_digits(trim(wp_unslash((string) $source['low_stock_amount']))) : '';
    $low_stock_raw = str_replace([',', '٬', ' '], ['', '', ''], $low_stock_raw);

    $image_id = isset($source['image_id']) ? absint(wp_unslash((string) $source['image_id'])) : 0;

    $gallery_ids = [];
    if (isset($source['gallery_ids']) && is_array($source['gallery_ids'])) {
        foreach ($source['gallery_ids'] as $gid_raw) {
            $gid = absint(wp_unslash((string) $gid_raw));
            if ($gid > 0) {
                $gallery_ids[] = $gid;
            }
        }
    }
    $gallery_ids = array_values(array_unique($gallery_ids));

    $category_ids = [];
    if (isset($source['category_ids']) && is_array($source['category_ids'])) {
        foreach ($source['category_ids'] as $cat_raw) {
            $cat_id = absint(wp_unslash((string) $cat_raw));
            if ($cat_id > 0) {
                $category_ids[] = $cat_id;
            }
        }
    }
    $category_ids = array_values(array_unique($category_ids));

    $product_tags = isset($source['product_tags']) ? sanitize_text_field(wp_unslash((string) $source['product_tags'])) : '';
    $product_brands = isset($source['product_brands']) ? sanitize_text_field(wp_unslash((string) $source['product_brands'])) : '';

    $weight_raw = isset($source['weight']) ? zerti_to_latin_digits(trim(wp_unslash((string) $source['weight']))) : '';
    $length_raw = isset($source['length']) ? zerti_to_latin_digits(trim(wp_unslash((string) $source['length']))) : '';
    $width_raw = isset($source['width']) ? zerti_to_latin_digits(trim(wp_unslash((string) $source['width']))) : '';
    $height_raw = isset($source['height']) ? zerti_to_latin_digits(trim(wp_unslash((string) $source['height']))) : '';

    $shipping_class_id = isset($source['shipping_class_id']) ? absint(wp_unslash((string) $source['shipping_class_id'])) : 0;
    $shipping_choices = zerti_product_edit_shipping_class_choices();
    if ($shipping_class_id > 0 && !array_key_exists($shipping_class_id, $shipping_choices)) {
        $shipping_class_id = 0;
    }

    $product_type = isset($source['product_type'])
        ? zerti_product_edit_sanitize_type(sanitize_title(wp_unslash((string) $source['product_type'])))
        : 'simple';
    $product_url = isset($source['product_url']) ? esc_url_raw(wp_unslash((string) $source['product_url'])) : '';
    $button_text = isset($source['button_text']) ? sanitize_text_field(wp_unslash((string) $source['button_text'])) : '';

    $upsell_ids = zerti_product_edit_linked_ids_from_array($source, 'upsell_ids');
    $cross_sell_ids = zerti_product_edit_linked_ids_from_array($source, 'cross_sell_ids');
    $children_ids = zerti_product_edit_linked_ids_from_array($source, 'children');

    $attribute_rows = zerti_product_edit_attribute_rows_from_array($source);
    $variation_rows = zerti_product_edit_variation_rows_from_array($source);
    $download_rows = zerti_product_edit_download_rows_from_array($source);
    $download_limit_raw = isset($source['download_limit']) ? zerti_to_latin_digits(trim(wp_unslash((string) $source['download_limit']))) : '';
    $download_expiry_raw = isset($source['download_expiry']) ? zerti_to_latin_digits(trim(wp_unslash((string) $source['download_expiry']))) : '';

    $purchase_note = isset($source['purchase_note']) ? wp_kses_post(wp_unslash((string) $source['purchase_note'])) : '';
    $menu_order = isset($source['menu_order']) ? (int) zerti_to_latin_digits(trim(wp_unslash((string) $source['menu_order']))) : 0;

    return [
        'product_id' => $product_id,
        'product_type' => $product_type,
        'product_url' => $product_url,
        'button_text' => $button_text,
        'name' => $name,
        'status' => $status,
        'catalog_visibility' => $catalog_visibility,
        'featured' => !empty($source['featured']),
        'virtual' => !empty($source['virtual']),
        'downloadable' => !empty($source['downloadable']),
        'sku' => $sku,
        'short_description' => $short_description,
        'description' => $description,
        'regular_price' => $regular_price,
        'sale_price' => $sale_price,
        'sale_from_ts' => $sale_from_ts,
        'sale_to_ts' => $sale_to_ts,
        'tax_status' => $tax_status,
        'tax_class' => $tax_class,
        'manage_stock' => $manage_stock,
        'stock_quantity_raw' => $stock_qty_raw,
        'stock_status' => $stock_status,
        'backorders' => $backorders,
        'low_stock_raw' => $low_stock_raw,
        'sold_individually' => !empty($source['sold_individually']),
        'reviews_allowed' => !empty($source['reviews_allowed']),
        'image_id' => $image_id,
        'gallery_ids' => $gallery_ids,
        'category_ids' => $category_ids,
        'product_tags' => $product_tags,
        'product_brands' => $product_brands,
        'weight_raw' => $weight_raw,
        'length_raw' => $length_raw,
        'width_raw' => $width_raw,
        'height_raw' => $height_raw,
        'shipping_class_id' => $shipping_class_id,
        'upsell_ids' => $upsell_ids,
        'cross_sell_ids' => $cross_sell_ids,
        'children_ids' => $children_ids,
        'attribute_rows' => $attribute_rows,
        'variation_rows' => $variation_rows,
        'download_rows' => $download_rows,
        'download_limit' => ($download_limit_raw !== '' && is_numeric($download_limit_raw)) ? (int) $download_limit_raw : -1,
        'download_expiry' => ($download_expiry_raw !== '' && is_numeric($download_expiry_raw)) ? (int) $download_expiry_raw : -1,
        'purchase_note' => $purchase_note,
        'menu_order' => $menu_order,
    ];
}

/**
 * Persist product via WooCommerce APIs.
 *
 * @param array<string, mixed> $payload
 * @return array{product_id:int}|WP_Error
 */
function zerti_product_edit_save(array $payload)
{
    if (!function_exists('wc_get_product')) {
        return new WP_Error('zerti_wc_missing', __('WooCommerce is not available.', 'zerti'), ['status' => 400]);
    }

    try {
        return zerti_product_edit_save_inner($payload);
    } catch (WC_Data_Exception $e) {
        return new WP_Error('zerti_wc_data', $e->getMessage(), ['status' => 400]);
    } catch (Throwable $e) {
        return new WP_Error('zerti_save_exception', $e->getMessage(), ['status' => 500]);
    }
}

/**
 * @param array<string, mixed> $payload
 * @return array{product_id:int}|WP_Error
 */
function zerti_product_edit_save_inner(array $payload)
{
    $product_id = (int) ($payload['product_id'] ?? 0);
    $product_type = zerti_product_edit_sanitize_type((string) ($payload['product_type'] ?? 'simple'));

    $product = zerti_product_edit_prepare_product($product_id, $product_type);
    if (!$product instanceof WC_Product) {
        return new WP_Error('zerti_not_found', __('Product not found.', 'zerti'), ['status' => 404]);
    }

    $product->set_name((string) $payload['name']);
    $product->set_status((string) $payload['status']);
    $product->set_catalog_visibility((string) ($payload['catalog_visibility'] ?? 'visible'));
    $product->set_featured(!empty($payload['featured']));
    $product->set_sku((string) $payload['sku']);
    $product->set_short_description((string) $payload['short_description']);
    $product->set_description((string) $payload['description']);

    if ($product->is_type('simple') || $product->is_type('variable')) {
        $product->set_virtual(!empty($payload['virtual']));
        $product->set_downloadable(!empty($payload['downloadable']));
    } else {
        $product->set_virtual(false);
        $product->set_downloadable(false);
    }

    if ($product->is_type('simple') || $product->is_type('external')) {
        $regular = (string) ($payload['regular_price'] ?? '');
        $sale = (string) ($payload['sale_price'] ?? '');
        $product->set_regular_price($regular);
        if ($sale !== '') {
            $product->set_sale_price($sale);
            $product->set_price($sale);
        } else {
            $product->set_sale_price('');
            $product->set_price($regular);
        }

        $sale_from = $payload['sale_from_ts'] ?? null;
        $sale_to = $payload['sale_to_ts'] ?? null;
        if ($sale_from) {
            $product->set_date_on_sale_from(gmdate('Y-m-d H:i:s', (int) $sale_from));
        } else {
            $product->set_date_on_sale_from('');
        }
        if ($sale_to) {
            $product->set_date_on_sale_to(gmdate('Y-m-d H:i:s', (int) $sale_to + DAY_IN_SECONDS - 1));
        } else {
            $product->set_date_on_sale_to('');
        }
    } elseif ($product->is_type('grouped') || $product->is_type('variable')) {
        $product->set_regular_price('');
        $product->set_sale_price('');
        $product->set_price('');
        $product->set_date_on_sale_from('');
        $product->set_date_on_sale_to('');
    }

    if ($product->is_type('external')) {
        $product->set_product_url((string) ($payload['product_url'] ?? ''));
        $product->set_button_text((string) ($payload['button_text'] ?? ''));
    }

    if ($product->is_type('grouped')) {
        $children = isset($payload['children_ids']) && is_array($payload['children_ids']) ? $payload['children_ids'] : [];
        $product->set_children(array_map('intval', $children));
    }

    $product->set_tax_status((string) ($payload['tax_status'] ?? 'taxable'));
    $product->set_tax_class((string) ($payload['tax_class'] ?? ''));

    if ($product->is_type('simple')) {
        $manage_stock = !empty($payload['manage_stock']);
        $product->set_manage_stock($manage_stock);
        if ($manage_stock) {
            $qty_raw = (string) ($payload['stock_quantity_raw'] ?? '');
            if ($qty_raw === '' || !is_numeric($qty_raw)) {
                return new WP_Error('zerti_stock_invalid', __('Invalid stock quantity.', 'zerti'), ['status' => 400]);
            }
            $qty = wc_stock_amount((float) $qty_raw);
            $product->set_stock_quantity($qty);
            $product->set_stock_status($qty > 0 ? 'instock' : 'outofstock');
        } else {
            $product->set_stock_quantity(null);
            $product->set_stock_status((string) ($payload['stock_status'] ?? 'instock'));
        }

        $product->set_backorders((string) ($payload['backorders'] ?? 'no'));
        $low_raw = (string) ($payload['low_stock_raw'] ?? '');
        if ($low_raw !== '' && is_numeric($low_raw)) {
            $product->set_low_stock_amount(wc_stock_amount((float) $low_raw));
        } else {
            $product->set_low_stock_amount('');
        }
        $product->set_sold_individually(!empty($payload['sold_individually']));
    }

    $product->set_reviews_allowed(!empty($payload['reviews_allowed']));

    if ($product->is_type('simple') || $product->is_type('variable')) {
        if (!empty($payload['virtual'])) {
            $product->set_weight('');
            $product->set_length('');
            $product->set_width('');
            $product->set_height('');
            $product->set_shipping_class_id(0);
        } else {
            foreach (['weight' => 'weight_raw', 'length' => 'length_raw', 'width' => 'width_raw', 'height' => 'height_raw'] as $setter => $key) {
                $raw = (string) ($payload[$key] ?? '');
                $method = 'set_' . $setter;
                if ($raw !== '' && is_numeric($raw)) {
                    $product->{$method}(wc_format_decimal($raw, 3));
                } else {
                    $product->{$method}('');
                }
            }
            $product->set_shipping_class_id((int) ($payload['shipping_class_id'] ?? 0));
        }
    }

    $image_id = (int) ($payload['image_id'] ?? 0);
    $product->set_image_id($image_id > 0 ? $image_id : 0);

    $gallery = isset($payload['gallery_ids']) && is_array($payload['gallery_ids']) ? $payload['gallery_ids'] : [];
    $product->set_gallery_image_ids(array_map('intval', $gallery));

    if (!$product->is_type('grouped')) {
        $upsells = isset($payload['upsell_ids']) && is_array($payload['upsell_ids']) ? $payload['upsell_ids'] : [];
        $cross = isset($payload['cross_sell_ids']) && is_array($payload['cross_sell_ids']) ? $payload['cross_sell_ids'] : [];
        $product->set_upsell_ids($upsells);
        $product->set_cross_sell_ids($cross);
    }

    $product->set_purchase_note((string) ($payload['purchase_note'] ?? ''));
    $product->set_menu_order((int) ($payload['menu_order'] ?? 0));

    if ($product->is_type('simple') || $product->is_type('variable')) {
        $attr_rows = isset($payload['attribute_rows']) && is_array($payload['attribute_rows']) ? $payload['attribute_rows'] : [];
        zerti_product_edit_apply_attributes($product, $attr_rows);
    }

    if ($product->is_type('simple') || $product->is_type('variable')) {
        $dl_rows = isset($payload['download_rows']) && is_array($payload['download_rows']) ? $payload['download_rows'] : [];
        zerti_product_edit_apply_downloads($product, $dl_rows, [
            'downloadable' => !empty($payload['downloadable']),
            'download_limit' => $payload['download_limit'] ?? -1,
            'download_expiry' => $payload['download_expiry'] ?? -1,
        ]);
    }

    $saved_id = $product->save();
    if ($saved_id < 1) {
        return new WP_Error('zerti_save_failed', __('Could not save product.', 'zerti'), ['status' => 500]);
    }

    $saved_type = zerti_product_edit_sanitize_type((string) ($payload['product_type'] ?? 'simple'));
    if ($saved_type === 'variable') {
        zerti_product_edit_clear_product_type_cache($saved_id);
        $variable_product = zerti_product_edit_prepare_product($saved_id, 'variable');
        if ($variable_product instanceof WC_Product) {
            $variation_rows = isset($payload['variation_rows']) && is_array($payload['variation_rows'])
                ? $payload['variation_rows']
                : [];
            zerti_product_edit_apply_variations($variable_product, $variation_rows);
        }
    }

    $cat_ids = isset($payload['category_ids']) && is_array($payload['category_ids']) ? $payload['category_ids'] : [];
    wp_set_object_terms($saved_id, $cat_ids, zerti_product_cat_taxonomy());

    if (taxonomy_exists('product_tag')) {
        $tag_string = (string) ($payload['product_tags'] ?? '');
        $tag_names = [];
        if ($tag_string !== '') {
            $parts = preg_split('/[\s,،]+/u', $tag_string) ?: [];
            foreach ($parts as $part) {
                $part = trim((string) $part);
                if ($part !== '') {
                    $tag_names[] = $part;
                }
            }
        }
        wp_set_object_terms($saved_id, $tag_names, 'product_tag');
    }

    if (zerti_product_brands_available()) {
        $brand_string = (string) ($payload['product_brands'] ?? '');
        $brand_names = [];
        if ($brand_string !== '') {
            $parts = preg_split('/[\s,،]+/u', $brand_string) ?: [];
            foreach ($parts as $part) {
                $part = trim((string) $part);
                if ($part !== '') {
                    $brand_names[] = $part;
                }
            }
        }
        wp_set_object_terms($saved_id, $brand_names, zerti_product_brand_taxonomy());
    }

    return ['product_id' => $saved_id];
}

/**
 * @param array<string, mixed> $source
 * @return array{taxonomy:string}
 */
function zerti_product_edit_attribute_terms_input_from_array(array $source): array
{
    return [
        'taxonomy' => isset($source['taxonomy'])
            ? zerti_product_edit_sanitize_attribute_taxonomy(wp_unslash((string) $source['taxonomy']))
            : '',
    ];
}

/**
 * @param array<string, mixed> $source
 * @return array{term:string,exclude:int}
 */
function zerti_product_edit_search_input_from_array(array $source): array
{
    return [
        'term' => isset($source['term']) ? sanitize_text_field(wp_unslash((string) $source['term'])) : '',
        // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Request field name, not a WP_Query call.
        'exclude' => isset($source['exclude']) ? absint(wp_unslash((string) $source['exclude'])) : 0,
    ];
}

/**
 * AJAX: list terms for a global attribute taxonomy (product editor).
 */
function zerti_ajax_product_edit_attribute_terms(): void
{
    check_ajax_referer('zerti_product_attributes', 'nonce');

    if (!zerti_user_can_access_admin() || !current_user_can('edit_products')) {
        wp_send_json_error(['message' => 'forbidden'], 403);
    }

    if (!function_exists('zerti_product_attributes_available') || !zerti_product_attributes_available()) {
        wp_send_json_error(['message' => 'woocommerce_missing'], 400);
    }

    $post = zerti_unslash_post_array();
    // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
    $taxonomy = zerti_product_edit_attribute_terms_input_from_array($post)['taxonomy'];
    $attribute_id = function_exists('zerti_product_attribute_id_from_taxonomy')
        ? zerti_product_attribute_id_from_taxonomy($taxonomy)
        : 0;

    if ($attribute_id < 1) {
        wp_send_json_error(['message' => __('Taxonomy missing', 'zerti'), 'code' => 'taxonomy_missing'], 400);
    }

    $terms = zerti_product_attribute_terms_list($attribute_id);

    wp_send_json_success([
        'attribute_id' => $attribute_id,
        'terms' => $terms,
    ]);
}

/**
 * AJAX: save product from the editor.
 */
function zerti_ajax_product_save(): void
{
    check_ajax_referer('zerti_product_save', 'nonce');

    $post = zerti_unslash_post_array();
    // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
    $payload = zerti_product_edit_payload_from_array($post);
    if (is_wp_error($payload)) {
        $status = (int) ($payload->get_error_data()['status'] ?? 400);
        wp_send_json_error(['message' => $payload->get_error_message()], $status > 0 ? $status : 400);
    }

    $result = zerti_product_edit_save($payload);
    if (is_wp_error($result)) {
        $status = (int) ($result->get_error_data()['status'] ?? 400);
        wp_send_json_error(['message' => $result->get_error_message()], $status > 0 ? $status : 400);
    }

    $saved_id = (int) ($result['product_id'] ?? 0);

    if (function_exists('zerti_invalidate_sidebar_badge_counts')) {
        zerti_invalidate_sidebar_badge_counts();
    }

    $edit_url = zerti_admin_route_url([
        'tab' => 'inventory',
        'inv' => 'warehouse',
        'product' => $saved_id,
    ]);

    wp_send_json_success([
        'message' => __('Product saved.', 'zerti'),
        'product_id' => $saved_id,
        'edit_url' => $edit_url,
        'list_url' => zerti_admin_route_url(['tab' => 'inventory', 'inv' => 'warehouse']),
    ]);
}

/**
 * AJAX: search products for linked-product picker.
 */
function zerti_ajax_product_search(): void
{
    check_ajax_referer('zerti_product_save', 'nonce');

    if (!zerti_user_can_view_products_grid()) {
        wp_send_json_error(['message' => __('Insufficient permissions.', 'zerti')], 403);
    }

    $post = zerti_unslash_post_array();
    // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
    $search = zerti_product_edit_search_input_from_array($post);

    wp_send_json_success([
        'products' => zerti_product_search_for_picker($search['term'], $search['exclude']),
    ]);
}

