<?php

namespace WeDevs\Dokan\ProductEditor;

use WC_Product;
use WeDevs\Dokan\ProductCategory\Helper as ProductCategoryHelper;

defined( 'ABSPATH' ) || exit;

/**
 * Product form schema, data resolution, and product form support.
 *
 * @since 5.0.0
 */
class FormSchema {

    /**
     * Required field attributes
     *
     * @since 5.0.0
     *
     * @var array $required_fields
     */
    private array $required_fields = [
        'id',
        'type',
        'variant',
        'label',
    ];

    /**
     * Supported field types.
     *
     * @since 5.0.0
     *
     * @var array
     */
    private array $supported_types = [
        'section',
        'field',
    ];

    /**
     * Supported field variants.
     *
     * @since 5.0.0
     *
     * @var array
     */
    private array $supported_variants = [
        'text',
        'select',
        'multiselect',
        'async_select',
        'checkbox',
        'textarea',
        'editor',
        'radio',
        'number',
        'file',
        'datetime',
        'image',
        'gallery',
        'attribute',
        'location_map',
    ];

    /**
     * Validate field schema and log developer notices for invalid fields.
     *
     * @since 5.0.0
     *
     * @param array $fields Form schema fields to validate.
     *
     * @return array The same fields array (unmodified).
     */
    private function assert_field_schema( array $fields ): array {
        foreach ( $fields as $field ) {
            // Check required attributes are present.
            foreach ( $this->required_fields as $attr ) {
                // 'variant' is only required for field-type items, not sections.
                if ( 'variant' === $attr && isset( $field['type'] ) && 'section' === $field['type'] ) {
                    continue;
                }
                if ( ! array_key_exists( $attr, $field ) || ( $field[ $attr ] === '' || $field[ $attr ] === null ) ) {
                    _doing_it_wrong(
                        __METHOD__,
                        sprintf(
                            /* translators: 1: attribute name, 2: field id */
                            esc_html__( 'Missing required attribute "%1$s" on field: %2$s', 'dokan-lite' ),
                            esc_html( $attr ),
                            esc_html( $field['id'] ?? 'unknown' )
                        ),
                        '5.0.0'
                    );
                }
            }
            if ( ! in_array( $field['type'], $this->supported_types, true ) ) {
                _doing_it_wrong(
                    __METHOD__,
                    sprintf(
                        /* translators: 1: field type, 2: field id */
                        esc_html__( 'Invalid field type: %1$s and id: %2$s', 'dokan-lite' ),
                        esc_html( $field['type'] ),
                        esc_html( $field['id'] ?? 'unknown' )
                    ),
                    '5.0.0'
                );
            }
            if ( isset( $field['variant'] ) && ! in_array( $field['variant'], $this->supported_variants, true ) ) {
                _doing_it_wrong(
                    __METHOD__,
                    sprintf(
                        /* translators: 1: field variant, 2: field id */
                        esc_html__( 'Invalid field variant: %1$s and id: %2$s', 'dokan-lite' ),
                        esc_html( $field['variant'] ),
                        esc_html( $field['id'] ?? 'unknown' )
                    ),
                    '5.0.0'
                );
            }
        }
        return $fields;
    }

    /**
     * Get available product types as label/value pairs.
     *
     * @since 5.0.0
     *
     * @return array
     */
    public function get_product_types(): array {
        $types = [
            'simple' => __( 'Simple', 'dokan-lite' ),
        ];
        $product_types = apply_filters( 'dokan_product_types', $types );
        $types = array_map(
            fn( $key, $type ) => [
                'label' => $type,
                'value' => $key,
            ],
            array_keys( $product_types ),
            $product_types
        );

        return $types;
    }

    /**
     * Get the flat layout definition.
     *
     * Predefined items are sorted by array position.
     * Extensions can set an optional `priority` key to control insertion order
     * when adding items via the `dokan_product_editor_layouts` filter.
     *
     * @since 5.0.0
     *
     * @return array Flat array of layout items.
     */
    public static function get_layouts(): array {
        $layouts = [
            // Root layout with responsive breakpoints.
            [
                'id'         => Elements::ROOT_LAYOUT,
                'parent_id'  => null,
                'priority'   => 10,
                'layout'     => [
                    'type'      => 'row',
                    'alignment' => 'start',
                    'styles'    => [
                        Elements::PRIMARY_COLUMN => [ 'flex' => '1' ],
                        Elements::SIDEBAR_COLUMN => [
                            'flex'     => '0 0 25%',
                            'minWidth' => '280px',
                        ],
                    ],
                ],
                'responsive' => [
                    [
                        'maxWidth' => 768,
                        'layout'   => [
                            'type' => 'regular',
                        ],
                    ],
                ],
            ],

            // Two-column structure.
            [
                'id'        => Elements::PRIMARY_COLUMN,
                'parent_id' => Elements::ROOT_LAYOUT,
                'priority'  => 10,
            ],
            [
                'id'        => Elements::SIDEBAR_COLUMN,
                'parent_id' => Elements::ROOT_LAYOUT,
                'priority'  => 20,
            ],

            // ── Primary column sections ───────────────────────────

            // 1. General info section (card).
            [
                'id'        => Elements::SECTION_GENERAL,
                'parent_id' => Elements::PRIMARY_COLUMN,
                'priority'  => 10,
                'layout'    => [ 'type' => 'card' ],
                'children'  => [
                    Elements::NAME,
                    Elements::SLUG,
                    Elements::TYPE,
                    Elements::EXTERNAL_URL,
                    Elements::BUTTON_TEXT,
                    Elements::CATEGORIES,
                    Elements::TAGS,
                    Elements::BRANDS,
                    Elements::REGULAR_PRICE,
                    Elements::SALE_PRICE,
                    Elements::CREATE_SCHEDULE_FOR_DISCOUNT,
                ],
            ],
            // Digital product options group (inside general info, after 'type').
            [
                'id'        => Elements::SECTION_DIGITAL_OPTIONS,
                'parent_id' => Elements::SECTION_GENERAL,
                'after'     => Elements::TYPE,
                'priority'  => 10,
                'layout'    => [ 'type' => 'regular' ],
                'label'     => __( 'Digital Product Options', 'dokan-lite' ),
                'children'  => [
                    Elements::DOWNLOADABLE,
                    Elements::VIRTUAL,
                ],
            ],
            // Discount schedule row (inside general info, after 'create_schedule_for_discount').
            [
                'id'        => Elements::SECTION_DISCOUNT_SCHEDULE,
                'parent_id' => Elements::SECTION_GENERAL,
                'after'     => Elements::CREATE_SCHEDULE_FOR_DISCOUNT,
                'priority'  => 20,
                'layout'    => [ 'type' => 'row' ],
                'children'  => [
                    Elements::DATE_ON_SALE_FROM,
                    Elements::DATE_ON_SALE_TO,
                ],
            ],

            // 2. Description section (card).
            [
                'id'        => Elements::SECTION_DESCRIPTION,
                'parent_id' => Elements::PRIMARY_COLUMN,
                'priority'  => 20,
                'layout'    => [ 'type' => 'card' ],
                'children'  => [
                    Elements::SHORT_DESCRIPTION,
                    Elements::DESCRIPTION,
                ],
            ],

            // 3. Inventory section (card with header).
            [
                'id'        => Elements::SECTION_INVENTORY,
                'parent_id' => Elements::PRIMARY_COLUMN,
                'layout'    => [
                    'type'       => 'card',
                    'withHeader' => true,
                ],
                'priority'  => 40,
                'children'  => [
                    Elements::SKU,
                    Elements::GLOBAL_UNIQUE_ID,
                    Elements::MANAGE_STOCK,
                    Elements::STOCK_STATUS,
                    Elements::STOCK_QUANTITY,
                    Elements::LOW_STOCK_AMOUNT,
                    Elements::BACKORDERS,
                    Elements::SOLD_INDIVIDUALLY,
                ],
            ],

            // 4. Shipping and Tax section (card with header).
            [
                'id'        => Elements::SECTION_SHIPPING,
                'parent_id' => Elements::PRIMARY_COLUMN,
                'priority'  => 40,
                'layout'    => [
                    'type'       => 'card',
                    'withHeader' => true,
                ],
                'children'  => [
                    Elements::DISABLE_SHIPPING_META,
                    Elements::SHIPPING_CLASS,
                    Elements::TAX_STATUS,
                    Elements::TAX_CLASS,
                    Elements::OVERWRITE_SHIPPING_META,
                    Elements::ADDITIONAL_SHIPPING_PROCESSING_TIME_META,
                ],
            ],
            // Shipping dimensions row (after '_disable_shipping').
            [
                'id'        => Elements::SECTION_SHIPPING_DIMENSIONS,
                'parent_id' => Elements::SECTION_SHIPPING,
                'after'     => Elements::DISABLE_SHIPPING_META,
                'priority'  => 10,
                'layout'    => [ 'type' => 'row' ],
                'children'  => [
                    Elements::WEIGHT,
                    Elements::DIMENSIONS_LENGTH,
                    Elements::DIMENSIONS_WIDTH,
                    Elements::DIMENSIONS_HEIGHT,
                ],
            ],
            // Overwrite shipping price/qty row (after '_overwrite_shipping').
            [
                'id'        => Elements::SECTION_SHIPPING_OVERWRITE,
                'parent_id' => Elements::SECTION_SHIPPING,
                'after'     => Elements::OVERWRITE_SHIPPING_META,
                'priority'  => 20,
                'layout'    => [ 'type' => 'row' ],
                'children'  => [
                    Elements::ADDITIONAL_SHIPPING_COST_META,
                    Elements::ADDITIONAL_SHIPPING_QUANTITY_META,
                ],
            ],

            // ── Sidebar column sections ───────────────────────────

            // Organize product (card, not collapsible).
            [
                'id'        => Elements::SECTION_PUBLISHING,
                'parent_id' => Elements::SIDEBAR_COLUMN,
                'priority'  => 10,
                'layout'    => [
                    'type'          => 'card',
                    'isCollapsible' => false,
                ],
                'children'  => [
                    Elements::STATUS,
                    Elements::CATALOG_VISIBILITY,
                    Elements::FEATURED_IMAGE_ID,
                    Elements::GALLERY_IMAGE_IDS,
                    Elements::REVIEWS_ALLOWED,
                ],
            ],

            // Purchase note section (card with header, not collapsible).
            [
                'id'        => Elements::SECTION_PURCHASE_NOTE,
                'parent_id' => Elements::SIDEBAR_COLUMN,
                'priority'  => 20,
                'layout'    => [
                    'type'          => 'card',
                    'withHeader'    => true,
                    'isCollapsible' => false,
                ],
                'children'  => [
                    Elements::PURCHASE_NOTE,
                ],
            ],
        ];

        $layouts = array_map(
            function ( $item ) {
                // Ensure 'children' key exists for all items.
                if ( ! isset( $item['children'] ) ) {
                    $item['children'] = [];
                }
                $item['children'] = apply_filters( 'dokan_product_editor_layout_children', $item['children'], $item );
                return $item;
            }, $layouts
        );

        /**
         * Filter the product editor form layout.
         *
         * Flat array of layout items with parent-child relationships.
         * Each item has: id, parent_id, layout, priority, label, description, children, after.
         * Sorting is by priority (default 999); items with equal priority preserve array order.
         *
         * @since 5.0.0
         *
         * @param array $layout Flat layout items.
         */
        $layouts = apply_filters( 'dokan_product_editor_layouts', $layouts );

        // Sort by priority after all extensions have added their items.
        usort(
            $layouts,
            function ( $a, $b ) {
                return ( $a['priority'] ?? 999 ) <=> ( $b['priority'] ?? 999 );
            }
        );

        return $layouts;
    }

    /**
     * Get flat form schema (sections and fields). Resolves field values when $product_id is provided.
     *
     * @since 5.0.0
     * @param int $product_id Optional. Product ID to resolve values from.
     * @return array Form schema items (sections and fields).
     */
    public function get_schema( int $product_id = 0 ): array {
        $product = ( $product_id > 0 ) ? wc_get_product( $product_id ) : null;

        $can_create_tags = dokan()->is_pro_exists() ? dokan_get_option( 'product_vendors_can_create_tags', 'dokan_selling', 'off' ) : 'off';

        // Vendors may be restricted to a single product category via the admin selling settings.
        $is_single_category = ProductCategoryHelper::product_category_selection_is_single();

        $dep_downloadable = [
            [
                'comparison' => '==',
                'key'        => Elements::DOWNLOADABLE,
                'value'      => true,
            ],
        ];

        // Per-type visibility: show for simple & variation, hide for variable (price is set per-variation).
        $pre_type_visibilities = [
            Elements::PRODUCT_TYPE_SIMPLE    => true,
            Elements::PRODUCT_TYPE_VARIABLE  => false,
            Elements::PRODUCT_TYPE_VARIATION => true,
            Elements::PRODUCT_TYPE_GROUPED   => false,
        ];
        $price_visibilities = apply_filters(
            'dokan_product_editor_price_visibilities',
            $pre_type_visibilities,
        );

        $digital_field_visibilities = apply_filters(
            'dokan_product_editor_digital_option_visibilities',
            $pre_type_visibilities,
        );

        $price_labels = apply_filters(
            'dokan_product_editor_price_labels',
            []
        );

        $schedule_deps = [
			[
				'comparison' => '==',
				'key'        => Elements::CREATE_SCHEDULE_FOR_DISCOUNT,
				'value'      => true,
			],
		];

        $general_fields = [
            [
                'id'         => Elements::SECTION_GENERAL,
                'section_id' => null,
                'type'       => 'section',
                'label'      => __( 'General', 'dokan-lite' ),
                'required'   => true,
                'visibility' => true,
                'is_mandatory' => true,
            ],
            [
                'id'             => Elements::NAME,
                'section_id'     => Elements::SECTION_GENERAL,
                'type'           => 'field',
                'label'          => __( 'Title', 'dokan-lite' ),
                'variant'        => 'text',
                'placeholder'    => __( 'Enter product title...', 'dokan-lite' ),
                'required'       => true,
                'visibility'     => true,
                'is_mandatory'   => true,
            ],
            [
                'id'               => Elements::SLUG,
                'section_id'       => Elements::SECTION_GENERAL,
                'type'             => 'field',
                'label'            => __( 'Permalink', 'dokan-lite' ),
                'variant'          => 'text',
                'placeholder'      => __( 'Enter product slug...', 'dokan-lite' ),
                'prefix'           => home_url( wc_get_permalink_structure()['product_base'] ?? '/product/' ),
                'required'         => false,
                'visibility'       => true,
            ],
            [
                'id'             => Elements::TYPE,
                'section_id'     => Elements::SECTION_GENERAL,
                'type'           => 'field',
                'label'          => __( 'Product Type', 'dokan-lite' ),
                'variant'        => 'select',
                'value'          => 'simple',
                'required'       => true,
                'is_mandatory'   => true,
                'options'        => $this->get_product_types(),
                'description'    => __( 'Choose Variable if your product has multiple attributes - like sizes, colors, quality etc', 'dokan-lite' ),
                'tooltip'        => __( 'Choose product type.', 'dokan-lite' ),
                'visibility'     => true,
            ],
            [
                'id'            => Elements::ENABLED,
                'section_id'    => Elements::SECTION_GENERAL,
                'type'          => 'field',
                'label'         => __( 'Enabled', 'dokan-lite' ),
                'variant'       => 'checkbox',
                'visibility'    => true,
                'show_in_admin' => false,
                'dependencies'  => [
                    [
                        'comparison' => 'contains',
                        'key'        => Elements::TYPE,
                        'value'      => 'variation',
                    ],
                ],
            ],
            [
                'id'           => Elements::REGULAR_PRICE,
                'section_id'   => Elements::SECTION_GENERAL,
                'type'         => 'field',
                'label'        => __( 'Price', 'dokan-lite' ),
                'labels'       => $price_labels,
                'variant'      => 'text',
                'placeholder'  => '0.00',
                'visibility'   => true,
                'visibilities' => $price_visibilities,
                'requireds'    => [
                    'simple'   => true,
                    'external' => true,
                ],
            ],
            [
                'id'           => Elements::SALE_PRICE,
                'section_id'   => Elements::SECTION_GENERAL,
                'type'         => 'field',
                'label'        => __( 'Sale Price', 'dokan-lite' ),
                'variant'      => 'text',
                'placeholder'  => '0.00',
                'visibility'   => true,
                'visibilities' => $price_visibilities,
                'validations'  => [
                    [
                        'rules'        => 'less_than',
                        'message'      => __( 'Sale price must be less than the regular price.', 'dokan-lite' ),
                        'params'       => [ 'field' => Elements::REGULAR_PRICE ],
                        'dependencies' => [
                            [
                                'comparison' => 'not_empty',
                                'key'        => Elements::REGULAR_PRICE,
                            ],
                        ],
                    ],
                ],
            ],
            [
                'id'            => Elements::CREATE_SCHEDULE_FOR_DISCOUNT,
                'section_id'    => Elements::SECTION_GENERAL,
                'type'          => 'field',
                'label'         => __( 'Create Schedule for Discount', 'dokan-lite' ),
                'variant'       => 'checkbox',
                'visibility'    => true,
                'visibilities'  => $price_visibilities,
                'show_in_admin' => false,
            ],
            [
                'id'             => Elements::DATE_ON_SALE_FROM,
                'section_id'     => Elements::SECTION_GENERAL,
                'type'           => 'field',
                'label'          => __( 'From', 'dokan-lite' ),
                'variant'        => 'datetime',
                'placeholder'    => 'YYYY-MM-DD HH:MM',
                'visibility'     => true,
                'visibilities'   => $price_visibilities,
                'dependencies'   => $schedule_deps,
            ],
            [
                'id'             => Elements::DATE_ON_SALE_TO,
                'section_id'     => Elements::SECTION_GENERAL,
                'type'           => 'field',
                'label'          => __( 'To', 'dokan-lite' ),
                'variant'        => 'datetime',
                'placeholder'    => 'YYYY-MM-DD HH:MM',
                'visibility'     => true,
                'visibilities'   => $price_visibilities,
                'dependencies'   => $schedule_deps,
            ],
            [
                'id'               => Elements::CATEGORIES,
                'section_id'       => Elements::SECTION_GENERAL,
                'type'             => 'field',
                'label'            => __( 'Categories', 'dokan-lite' ),
                'variant'          => 'async_select',
                'placeholder'      => __( 'Select product categories', 'dokan-lite' ),
                'value'            => [],
                // Loaded on demand as a nested tree instead of embedding the whole
                // category hierarchy in the schema, which bloats memory on large catalogs.
                'api_endpoint'     => '/dokan/v1/products/categories/tree',
                'tree'             => true,
                // Honor the admin "single vs. multiple" category selection setting.
                'multiple'         => ! $is_single_category,
                'required'         => true,
                'is_mandatory'     => true,
                'visibility'       => true,
            ],
            [
                'id'               => Elements::TAGS,
                'section_id'       => Elements::SECTION_GENERAL,
                'type'             => 'field',
                'label'            => __( 'Tags', 'dokan-lite' ),
                'variant'          => 'async_select',
                'placeholder'      => 'on' === $can_create_tags ? __( 'Select tags/Add tags', 'dokan-lite' ) : __( 'Select product tags', 'dokan-lite' ),
                'value'            => [],
                // Tags load on demand from WooCommerce core (searchable/paginated) instead of
                // embedding the whole tag taxonomy in the schema, which can exhaust memory on large stores.
                'api_endpoint'     => '/wc/v3/products/tags',
                'creatable'        => 'on' === $can_create_tags,
                'visibility'       => true,
            ],
            [
                'id'               => Elements::BRANDS,
                'section_id'       => Elements::SECTION_GENERAL,
                'type'             => 'field',
                'label'            => __( 'Brands', 'dokan-lite' ),
                'variant'          => 'multiselect',
                'placeholder'      => __( 'Select product brands', 'dokan-lite' ),
                'value'            => [],
                'options'          => self::get_products_brands(),
                'visibility'       => true,
            ],
            [
                'id'             => Elements::FEATURED_IMAGE_ID,
                'section_id'   => Elements::SECTION_GENERAL,
                'type'           => 'field',
                'label'          => __( 'Feature Image', 'dokan-lite' ),
                'variant'        => 'image',
                'value'          => [],
                'tooltip'        => __( 'Select product image', 'dokan-lite' ),
                'visibility'     => true,
            ],
            [
                'id'             => Elements::GALLERY_IMAGE_IDS,
                'section_id'   => Elements::SECTION_GENERAL,
                'type'           => 'field',
                'label'          => __( 'Gallery Image', 'dokan-lite' ),
                'variant'        => 'gallery',
                'value'          => [],
                'tooltip'        => __( 'Select product gallery images', 'dokan-lite' ),
                'visibility'     => true,
            ],
            [
                'id'             => Elements::SHORT_DESCRIPTION,
                'section_id'   => Elements::SECTION_GENERAL,
                'type'           => 'field',
                'label'          => __( 'Short Description', 'dokan-lite' ),
                'variant'        => 'editor',
                'placeholder'    => __( 'Enter product short description', 'dokan-lite' ),
                'visibility'     => true,
            ],
            [
                'id'             => Elements::DESCRIPTION,
                'section_id'   => Elements::SECTION_GENERAL,
                'type'           => 'field',
                'label'          => __( 'Description', 'dokan-lite' ),
                'variant'        => 'editor',
                'placeholder'    => __( 'Enter product description', 'dokan-lite' ),
                'required'       => true,
                'is_mandatory'   => true,
                'visibility'     => true,
            ],
            [
                'id'            => Elements::DOWNLOADABLE,
                'section_id'   => Elements::SECTION_GENERAL,
                'type'          => 'field',
                'label'         => __( 'Downloadable', 'dokan-lite' ),
                'variant'       => 'checkbox',
                'tooltip'       => __( 'Downloadable products give access to a file upon purchase.', 'dokan-lite' ),
                'visibility'    => true,
                'visibilities'  => $digital_field_visibilities,
            ],
            [
                'id'            => Elements::VIRTUAL,
                'section_id'   => Elements::SECTION_GENERAL,
                'type'          => 'field',
                'label'         => __( 'Virtual', 'dokan-lite' ),
                'variant'       => 'checkbox',
                'tooltip'       => __( 'Virtual products are intangible and are not shipped.', 'dokan-lite' ),
                'visibility'    => true,
                'visibilities'  => $digital_field_visibilities,
            ],
        ];

        $inventory_fields = [
            [
                'id'          => Elements::SECTION_INVENTORY,
                'section_id'  => null,
                'type'        => 'section',
                'label'       => __( 'Inventory', 'dokan-lite' ),
                'description' => __( 'Manage inventory for this product', 'dokan-lite' ),
                'visibility'  => true,
            ],
            [
                'id'           => Elements::SKU,
                'section_id'   => Elements::SECTION_INVENTORY,
                'type'         => 'field',
                'label'        => sprintf( '%s <span>(%s)</span>', esc_html__( 'SKU', 'dokan-lite' ), esc_html__( 'Stock Keeping Unit', 'dokan-lite' ) ),
                'variant'      => 'text',
                'placeholder'  => __( 'Enter product SKU', 'dokan-lite' ),
                'description'  => __( 'SKU refers to a Stock-keeping unit, a unique identifier for each distinct product and service that can be purchased.', 'dokan-lite' ),
                'visibility'   => true,
            ],
            [
                'id'           => Elements::GLOBAL_UNIQUE_ID,
                'section_id'   => Elements::SECTION_INVENTORY,
                'type'         => 'field',
                'label'        => sprintf( '%s <span>(%s)</span>', esc_html__( 'GTIN, UPC, EAN, or ISBN', 'dokan-lite' ), esc_html__( 'Product Identifiers', 'dokan-lite' ) ),
                'variant'      => 'text',
                'placeholder'  => __( 'Enter code', 'dokan-lite' ),
                'tooltip'      => __( 'Enter a barcode or any other identifier unique to this product. It can help you list this product on other channels or marketplaces.', 'dokan-lite' ),
                'visibility'   => true,
            ],
            [
                'id'           => Elements::STOCK_STATUS,
                'section_id'   => Elements::SECTION_INVENTORY,
                'type'         => 'field',
                'label'        => __( 'Stock Status', 'dokan-lite' ),
                'variant'      => 'select',
                'description'  => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'dokan-lite' ),
                'options'      => wc_get_product_stock_status_options(),
                'dependencies' => [
                    [
                        'comparison' => '!=',
                        'key'        => Elements::MANAGE_STOCK,
                        'value'      => true,
                    ],
                ],
                'visibility'   => true,
            ],
            [
                'id'           => Elements::MANAGE_STOCK,
                'section_id'   => Elements::SECTION_INVENTORY,
                'type'         => 'field',
                'label'        => __( 'Manage stock?', 'dokan-lite' ),
                'variant'      => 'checkbox',
                'tooltip'      => __( 'Manage stock level (quantity)', 'dokan-lite' ),
                'visibility'   => true,
            ],
            [
                'id'           => Elements::STOCK_QUANTITY,
                'section_id'   => Elements::SECTION_INVENTORY,
                'type'         => 'field',
                'label'        => __( 'Stock quantity', 'dokan-lite' ),
                'variant'      => 'number',
                'placeholder'  => '1',
                'description'  => __( 'Stock quantity. If this is a variable product this value will be used to control stock for all variations, unless you define stock at variation level.', 'dokan-lite' ),
                'dependencies' => [
                    [
                        'comparison' => '==',
                        'key'        => Elements::MANAGE_STOCK,
                        'value'      => true,
                    ],
                ],
                'visibility'   => true,
            ],
            [
                'id'           => Elements::LOW_STOCK_AMOUNT,
                'section_id'   => Elements::SECTION_INVENTORY,
                'type'         => 'field',
                'label'        => __( 'Low stock threshold', 'dokan-lite' ),
                'variant'      => 'number',
                'placeholder'  => sprintf(
                    /* translators: %d: store-wide threshold */
                    esc_attr__( 'Store-wide threshold (%d)', 'dokan-lite' ),
                    esc_attr( get_option( 'woocommerce_notify_low_stock_amount' ) )
                ),
                'description' => __( 'When product stock reaches this amount you will be notified by email. It is possible to define different values for each variation individually.', 'dokan-lite' ),
                'dependencies' => [
                    [
                        'comparison' => '==',
                        'key'        => Elements::MANAGE_STOCK,
                        'value'      => true,
                    ],
                ],
                'visibility'   => true,
            ],
            [
                'id'           => Elements::BACKORDERS,
                'section_id'   => Elements::SECTION_INVENTORY,
                'type'         => 'field',
                'label'        => __( 'Allow Backorders', 'dokan-lite' ),
                'variant'      => 'select',
                'description'  => __( 'If managing stock, this controls whether or not backorders are allowed. If enabled, stock quantity can go below 0.', 'dokan-lite' ),
                'options'      => wc_get_product_backorder_options(),
                'dependencies' => [
                    [
                        'comparison' => '==',
                        'key'        => Elements::MANAGE_STOCK,
                        'value'      => true,
                    ],
                ],
                'visibility'   => true,
            ],
            [
                'id'           => Elements::SOLD_INDIVIDUALLY,
                'section_id'   => Elements::SECTION_INVENTORY,
                'type'         => 'field',
                'label'        => __( 'Allow only one quantity of this product to be bought in a single order.', 'dokan-lite' ),
                'variant'      => 'checkbox',
                'tooltip'      => __( 'Check to let customers to purchase only 1 item in a single order. This is particularly useful for items that have limited quantity, for example art or handmade goods.', 'dokan-lite' ),
                'visibility'   => true,
            ],
        ];
        $downloadable_fields = [
            [
                'id'           => Elements::SECTION_DOWNLOADABLE,
                'section_id'   => null,
                'type'         => 'section',
                'label'        => __( 'Downloadable Options', 'dokan-lite' ),
                'description'  => __( 'Configure your downloadable product settings', 'dokan-lite' ),
                'visibility'   => true,
                'dependencies' => $dep_downloadable,
            ],
            [
                'id'           => Elements::DOWNLOADS,
                'section_id'   => Elements::SECTION_DOWNLOADABLE,
                'type'         => 'field',
                'label'        => __( 'Downloadable Files', 'dokan-lite' ),
                'variant'      => 'file',
                'value'        => [],
                'tooltip'      => __( 'Pick downloadable files from upload directory which is approved by the store admin.', 'dokan-lite' ),
                'description'  => __( 'Upload files that customers can download after purchase.', 'dokan-lite' ),
                'dependencies' => $dep_downloadable,
                'visibility'   => true,
            ],
            [
                'id'           => Elements::DOWNLOAD_LIMIT,
                'section_id'   => Elements::SECTION_DOWNLOADABLE,
                'type'         => 'field',
                'label'        => __( 'Download Limit', 'dokan-lite' ),
                'variant'      => 'number',
                'placeholder'  => __( 'Unlimited', 'dokan-lite' ),
                'description'  => __( 'Leave blank for unlimited re-downloads.', 'dokan-lite' ),
                'dependencies' => $dep_downloadable,
                'visibility'   => true,
            ],
            [
                'id'           => Elements::DOWNLOAD_EXPIRY,
                'section_id'   => Elements::SECTION_DOWNLOADABLE,
                'type'         => 'field',
                'label'        => __( 'Download Expiry', 'dokan-lite' ),
                'variant'      => 'number',
                'placeholder'  => __( 'Never', 'dokan-lite' ),
                'description'  => __( 'Enter the number of days before a download link expires, or leave blank.', 'dokan-lite' ),
                'dependencies' => $dep_downloadable,
                'visibility'   => true,
            ],
        ];
        $others_fields = [
            [
                'id'          => Elements::SECTION_OTHERS,
                'section_id'  => null,
                'type'        => 'section',
                'label'       => __( 'Other Options', 'dokan-lite' ),
                'description' => __( 'Set your extra product options', 'dokan-lite' ),
                'visibility'  => true,
            ],
            [
                'id'           => Elements::STATUS,
                'section_id'   => Elements::SECTION_OTHERS,
                'type'         => 'field',
                'label'        => __( 'Status', 'dokan-lite' ),
                'variant'      => 'radio',
                'value'        => 'draft',
                'required'     => true,
                'options'      => dokan_get_available_post_status( $product_id ),
                'visibility'   => true,
            ],
            [
                'id'           => Elements::CATALOG_VISIBILITY,
                'section_id'   => Elements::SECTION_OTHERS,
                'type'         => 'field',
                'label'        => __( 'Visibility', 'dokan-lite' ),
                'variant'      => 'select',
                'options'      => dokan_get_product_visibility_options(),
                'required'     => true,
                'is_mandatory' => true,
                'visibility'   => true,
            ],
            [
                'id'           => Elements::PURCHASE_NOTE,
                'section_id'   => Elements::SECTION_OTHERS,
                'type'         => 'field',
                'label'        => __( 'Purchase Note', 'dokan-lite' ),
                'variant'      => 'textarea',
                'placeholder'     => __( 'Purchase Note', 'dokan-lite' ),
                'description'     => __( 'Customer will get this in order email.', 'dokan-lite' ),
                'visibility'      => true,
            ],
            [
                'id'           => Elements::REVIEWS_ALLOWED,
                'section_id'   => Elements::SECTION_OTHERS,
                'type'         => 'field',
                'label'        => __( 'Enable product reviews', 'dokan-lite' ),
                'variant'      => 'checkbox',
                'visibility'   => true,
            ],
        ];

        $items = array_merge(
            $general_fields,
            $inventory_fields,
            $downloadable_fields,
            $others_fields,
        );

        $items = apply_filters( 'dokan_product_editor_schema', $items, $product_id );

        // Sort the items by priority (fallback to 30 when not set).
        usort(
            $items,
            function ( $a, $b ) {
                $a_priority = isset( $a['priority'] ) ? (int) $a['priority'] : 30;
                $b_priority = isset( $b['priority'] ) ? (int) $b['priority'] : 30;

                return $a_priority <=> $b_priority;
            }
        );

        // validate the fields
        $this->assert_field_schema( $items );

        $items = apply_filters( 'dokan_product_editor_prepared_schema', $items, $product_id );

        if ( $product instanceof WC_Product ) {
            $values = $this->get_field_values( $items, $product );
            foreach ( $items as &$item ) {
                if ( $item['type'] === 'field' && array_key_exists( $item['id'], $values ) ) {
                    $item['value'] = $values[ $item['id'] ];
                }
            }
            unset( $item );
        }

        return $items;
    }

    /**
     * Resolve and format values for a set of schema fields against a product.
     *
     * Lets callers that already hold field definitions resolve per-product
     * values without rebuilding the whole schema. The frontend variation
     * renderer uses this to avoid one full schema build per variation.
     *
     * @since 5.0.5
     *
     * @param array      $fields  Schema field items (each with at least 'id', 'type', 'variant').
     * @param WC_Product $product Product to resolve values against.
     *
     * @return array<string, mixed> Map of field id => formatted value.
     */
    public function get_field_values( array $fields, WC_Product $product ): array {
        $values = [];

        foreach ( $fields as $field ) {
            if ( ( $field['type'] ?? '' ) !== 'field' || ! isset( $field['id'] ) ) {
                continue;
            }

            $value = $this->resolve_field_value( $field['id'], $product );
            $value = $this->format_field_value( $value, $field['variant'] ?? 'text' );

            if ( empty( $value ) && isset( $field['value'] ) ) {
                // Fall back to the field's schema default, e.g. for new products or unset meta.
                $value = $field['value'];
            }

            $values[ $field['id'] ] = $value;
        }

        return $values;
    }

    /**
     * Format a resolved field value to the shape expected by the frontend based on variant.
     *
     * Resolve_field_value() returns raw values (int, array of ints, etc.).
     * This method transforms them to the structured shape the React frontend expects.
     *
     * @since 5.0.0
     *
     * @param mixed  $value   Raw resolved value.
     * @param string $variant Field variant type.
     *
     * @return mixed Formatted value.
     */
    private function format_field_value( $value, string $variant ) {
        switch ( $variant ) {
            case 'image':
                $image_id = absint( $value );

                return [
                    'id'  => $image_id,
                    'url' => $image_id ? wp_get_attachment_url( $image_id ) : '',
                ];

            case 'file':
                if ( ! is_array( $value ) || empty( $value ) ) {
                    return [];
                }

                // Already in [ { id, file, name }, ... ] shape (e.g. from DOWNLOADS case).
                if ( isset( $value[0] ) && is_array( $value[0] ) ) {
                    return $value;
                }

                // Raw array of attachment IDs from custom meta — resolve to [ { id, file, name } ].
                return array_filter(
                    array_map(
                        function ( $id ) {
                            $attachment_id = absint( $id );
                            $url           = $attachment_id ? wp_get_attachment_url( $attachment_id ) : '';

                            if ( ! $url ) {
                                return null;
                            }

                            return [
                                'id'   => $attachment_id,
                                'file' => $url,
                                'name' => basename( get_attached_file( $attachment_id ) ?? '' ),
                            ];
                        },
                        $value
                    )
                );

            case 'gallery':
                if ( ! is_array( $value ) ) {
                    return [];
                }

                return array_map(
                    function ( $id ) {
                        $attachment_id = absint( $id );

                        return [
                            'id'  => $attachment_id,
                            'url' => $attachment_id ? wp_get_attachment_url( $attachment_id ) : '',
                        ];
                    },
                    $value
                );

            case 'number':
                return is_numeric( $value ) ? (float) $value : 0;

            default:
                return $value;
        }
    }

    /**
     * Resolve a field's value from product. Mirrors Field::get_value() and original value_callback logic.
     *
     * @param string     $field_id Field id (Elements constant value, e.g. Elements::REVIEWS_ALLOWED).
     * @param WC_Product $product  Product instance.
     * @return mixed
     */
    private function resolve_field_value( string $field_id, WC_Product $product ) {
        $key = $field_id;

        switch ( $key ) {
            case Elements::NAME:
                if ( $product->get_status() === 'auto-draft' ) {
                    return '';
                }
                return $product->get_name();
            case Elements::CREATE_SCHEDULE_FOR_DISCOUNT:
                return ! empty( $product->get_date_on_sale_to() ) || ! empty( $product->get_date_on_sale_from() );
            case Elements::DATE_ON_SALE_FROM:
                $from = $product->get_date_on_sale_from( 'edit' );
                return $from ? $from->date( 'Y-m-d' ) : '';
            case Elements::DATE_ON_SALE_TO:
                $to = $product->get_date_on_sale_to( 'edit' );
                return $to ? $to->date( 'Y-m-d' ) : '';
            case Elements::CATEGORIES:
                // Async select expects [ { value, label }, ... ] so selected categories render
                // without the full category tree being embedded in the schema.
                $category_options = self::terms_to_async_options( $product->get_category_ids(), 'product_cat' );

                // Single-category stores keep one selection; surface only the first saved term.
                if ( ProductCategoryHelper::product_category_selection_is_single() ) {
                    return array_slice( $category_options, 0, 1 );
                }

                return $category_options;
            case Elements::TAGS:
                // Async select expects [ { value, label }, ... ] so selected tags render
                // without the full tag list being embedded in the schema.
                return self::terms_to_async_options( $product->get_tag_ids(), 'product_tag' );
            case Elements::BRANDS:
                if ( method_exists( $product, 'get_brand_ids' ) ) {
                    return $product->get_brand_ids();
                }
                return wp_get_post_terms( $product->get_id(), 'product_brand', [ 'fields' => 'ids' ] );
            case Elements::DOWNLOADABLE:
                return $product->is_downloadable();
            case Elements::VIRTUAL:
                return $product->is_virtual();
            case Elements::SOLD_INDIVIDUALLY:
                return $product->is_sold_individually();
            case Elements::DOWNLOADS:
                $downloads = [];
                foreach ( $product->get_downloads() as $download ) {
                    $downloads[] = [
                        'id' => (string) attachment_url_to_postid( $download['file'] ),
                        'name' => $download['name'],
                        'file'   => $download['file'],
                    ];
                }
                return $downloads;
            case Elements::STATUS:
                if ( 'auto-draft' !== $product->get_status() ) {
                    return $product->get_status();
                }
                $seller_id          = dokan_get_vendor_by_product( $product->get_id(), true );
                $new_product_status = dokan_get_default_product_status( $seller_id );
                $current_status     = 'publish' === $new_product_status ? 'publish' : ( 'pending' === $new_product_status ? 'pending' : 'draft' );
                return apply_filters( 'dokan_post_edit_default_status', $current_status, $product );
            case Elements::ENABLED:
                return $product->get_status() === 'publish';
            default:
                // Get the field name from the key.
                $field_name = sanitize_key( $key );

                $value = apply_filters( 'dokan_product_editor_schema_value', null, $field_name, $product );
                if ( null !== $value ) {
                    return $value;
                }

                // If the field name does not start with an underscore, it is a prop field.
                $method_name = 'get_' . $field_name;

                // If the method exists, return the value.
                if ( method_exists( $product, $method_name ) ) {
                    return $product->{$method_name}();
                }

                // Fallback to product meta for custom fields without a dedicated getter.
                $meta_value = $product->get_meta( $field_name, true );
                if ( '' !== $meta_value && null !== $meta_value ) {
                    return $meta_value;
                }

                return '';
        }
    }


    /**
     * Get product tags for form options.
     *
     * @since 5.0.0
     *
     * @return array
     */
    public static function get_product_tags(): array {
        $args = apply_filters(
            'dokan_product_tags_args', [
				'taxonomy'   => 'product_tag',
				'hide_empty' => 0,
				'orderby'    => 'name',
				'order'      => 'ASC',
			]
        );
        $data = [];
        $terms = get_terms( $args );
        if ( $terms && ! is_wp_error( $terms ) ) {
            foreach ( $terms as $term ) {
                $data[] = [
					'value' => $term->term_id,
					'slug' => $term->slug,
					'label' => $term->name,
				];
            }
        }
        return $data;
    }

    /**
     * Convert a list of term IDs to async-select options: [ { value, label }, ... ].
     *
     * Used by async-select fields (e.g. tags) so the currently selected terms render
     * their labels without embedding the whole taxonomy in the form schema.
     *
     * @since 5.0.5
     *
     * @param array  $term_ids Term IDs.
     * @param string $taxonomy Taxonomy name.
     *
     * @return array
     */
    public static function terms_to_async_options( array $term_ids, string $taxonomy ): array {
        $term_ids = array_filter( array_map( 'absint', $term_ids ) );
        if ( empty( $term_ids ) ) {
            return [];
        }

        $terms = get_terms(
            [
                'taxonomy'   => $taxonomy,
                'include'    => $term_ids,
                'hide_empty' => false,
                'orderby'    => 'name',
                'order'      => 'ASC',
            ]
        );

        if ( is_wp_error( $terms ) ) {
            return [];
        }

        return array_map(
            function ( $term ) {
                return [
                    'value' => $term->term_id,
                    'label' => $term->name,
                ];
            },
            $terms
        );
    }

    /**
     * Get product brands recursively for form options.
     *
     * @since 5.0.0
     *
     * @param int $parent_id Parent term ID (0 for top-level).
     *
     * @return array
     */
    public static function get_products_brands( int $parent_id = 0 ): array {
        $args = apply_filters(
            'dokan_product_brands_args', [
				'taxonomy'   => 'product_brand',
				'hide_empty' => 0,
				'parent'     => $parent_id,
				'orderby'    => 'name',
				'order'      => 'ASC',
			]
        );
        $terms  = get_terms( $args );
        $results = [];
        if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
            foreach ( $terms as $term ) {
                $children = self::get_products_brands( $term->term_id );
                $data = [
                    'value'  => $term->term_id,
                    'slug'   => $term->slug,
                    'label'  => $term->name,
                    'parent' => $parent_id,
                ];
                if ( ! empty( $children ) ) {
                    $data['children'] = $children;
                }
                $results[] = $data;
            }
        }
        return apply_filters( 'dokan_get_products_brands_data', $results );
    }
}
