<?php

namespace WeDevs\DokanPro\Modules\Germanized\Dashboard;

use WC_Germanized_Meta_Box_Product_Data;
use WC_Product;
use WeDevs\Dokan\ProductEditor\Elements;
use WeDevs\DokanPro\Modules\Germanized\Helper;

defined( 'ABSPATH' ) || exit;

/**
 * Integrates the Germanized EU compliance fields with the new Dokan Product Form Manager.
 *
 * The legacy vendor dashboard rendered these fields through a PHP template
 * (templates/product-simple.php) hooked on `dokan_product_edit_after_main` and saved via
 * `WC_Germanized_Meta_Box_Product_Data::save()`, which reads `$_POST`. The new vendor
 * dashboard uses a schema-driven React editor (/dashboard/new/#/products/create) that
 * persists through the WooCommerce-based REST controller and never renders that template.
 *
 * This class bridges the gap entirely through the editor's documented extension points —
 * no React components and no JS build:
 *  - adds an "EU Compliance Fields" section + fields to the form schema/layout, gated by
 *    dependencies so they mirror the legacy `show_if_*`/`hide_if_virtual` wrappers while
 *    remaining enabled-by-default and admin-toggleable via the Product Form Manager,
 *  - resolves the saved values back into the form when editing, and
 *  - persists on save by writing only the fields this integration manages directly through
 *    the Germanized product setters, so unmanaged Germanized fields (manufacturer, food,
 *    safety, allergens, type flags, …) keep their admin-set values.
 *
 * @since 5.0.12
 */
class ProductEditor {

    /**
     * Form schema section id for the EU compliance block.
     */
    const SECTION_EU = 'eu_compliance';

    /**
     * Schema field id for the delivery time select.
     *
     * NOT `delivery_time`: WooCommerce Germanized registers `delivery_time` as a WC REST
     * product field that expects an object, so a scalar term id under that key fails REST
     * validation. We use a dedicated id here and resolve its term id to a delivery-time slug
     * via the Germanized product setter at save time (see save_eu_data()).
     */
    const DELIVERY_TIME_KEY = 'dokan_gzd_delivery_time';

    /**
     * Product types the EU compliance fields apply to on save (mirrors the legacy
     * `show_if_simple show_if_external show_if_variable` wrappers).
     */
    const APPLIES_TO = [ 'simple', 'external', 'variable' ];

    /**
     * Memoized product-type dependencies shared by every EU field.
     *
     * Built once per request from the editor's product-type registry; see base_dependencies().
     *
     * @since 5.0.12
     *
     * @var array|null
     */
    private $base_dependencies = null;

    /**
     * Class constructor.
     *
     * @since 5.0.12
     */
    public function __construct() {
        // Add the EU compliance section + fields to the schema and place the section card.
        add_filter( 'dokan_product_editor_schema', [ $this, 'add_fields' ] );
        add_filter( 'dokan_product_editor_layouts', [ $this, 'add_layout' ] );

        // Single-select values must reach $_POST as scalars (not single-element arrays) so the
        // Germanized meta-box writer reads them correctly.
        add_filter( 'dokan_product_editor_single_select_fields', [ $this, 'register_single_select_fields' ] );

        // Resolve saved values back into the form when editing.
        add_filter( 'dokan_product_editor_schema_value', [ $this, 'resolve_field_value' ], 10, 3 );

        // Persist on save. The REST editor pre-creates an auto-draft then saves via update,
        // so hook both actions.
        add_action( 'dokan_new_product_added', [ $this, 'save_eu_data' ], 10, 2 );
        add_action( 'dokan_product_updated', [ $this, 'save_eu_data' ], 10, 2 );
    }

    /**
     * Append the EU compliance section and fields to the form schema.
     *
     * Fields are enabled by default (`visibility => true`) so the admin Product Form Manager
     * shows them as enabled and can toggle them off; product-type/virtual gating is expressed
     * through `dependencies` (which the Form Manager does not override) so it always mirrors
     * the legacy template's show_if_type / hide_if_virtual behaviour.
     *
     * @since 5.0.12
     *
     * @param array $fields Flat list of sections + fields.
     *
     * @return array
     */
    public function add_fields( array $fields ): array {
        $fields[] = [
            'id'          => self::SECTION_EU,
            'type'        => 'section',
            'label'       => __( 'EU Compliance Fields', 'dokan' ),
            'description' => __( 'Manage extra EU compliance fields for this product.', 'dokan' ),
            'visibility'  => true,
        ];

        $price_labels = array_merge( [ '' => __( 'Select Price Label', 'dokan' ) ], WC_germanized()->price_labels->get_labels() );
        $units        = array_merge( [ '' => __( 'Select Unit', 'dokan' ) ], WC_germanized()->units->get_units() );
        $ages         = wc_gzd_get_age_verification_min_ages_select();
        $delivery     = [ '' => __( 'Select Delivery Time', 'dokan' ) ] + Helper::get_terms( 'product_delivery_time', 'id' );

        $fields[] = $this->field( '_sale_price_label', __( 'Sale Label', 'dokan' ), 'select', 10, [ 'options' => $price_labels ] );
        $fields[] = $this->field( '_sale_price_regular_label', __( 'Sale Regular Label', 'dokan' ), 'select', 20, [ 'options' => $price_labels ] );
        $fields[] = $this->field( '_unit', __( 'Unit', 'dokan' ), 'select', 30, [ 'options' => $units ] );
        $fields[] = $this->field( '_min_age', __( 'Minimum Age', 'dokan' ), 'select', 40, [ 'options' => $ages ] );
        $fields[] = $this->field( '_unit_product', __( 'Product Units', 'dokan' ), 'text', 50 );
        $fields[] = $this->field( '_unit_base', __( 'Base Price Units', 'dokan' ), 'text', 60 );

        // Delivery time + free shipping were wrapped in hide_if_virtual on the legacy form.
        $fields[] = $this->field(
            self::DELIVERY_TIME_KEY, __( 'Delivery Time', 'dokan' ), 'select', 70, [
				'options' => $delivery,
				'dependencies' => $this->non_virtual_dependencies(),
			]
        );
        $fields[] = $this->field( '_free_shipping', __( 'Free shipping?', 'dokan' ), 'checkbox', 80, [ 'dependencies' => $this->non_virtual_dependencies() ] );

        // The unit-price block (auto-calc + regular/sale unit price) was show_if_simple/external
        // only on the legacy form; the Germanized writer discards these for variable products.
        // The automatic base-price calculation is a Germanized Pro only feature.
        if ( WC_germanized()->is_pro() ) {
            $fields[] = $this->field( '_unit_price_auto', __( 'Auto-calculate base price', 'dokan' ), 'checkbox', 90, [ 'dependencies' => $this->non_variable_dependencies() ] );
        }

        $fields[] = $this->field(
            '_unit_price_regular', __( 'Regular Unit Price', 'dokan' ), 'text', 100, [
				'placeholder' => '0.00',
				'dependencies' => $this->non_variable_dependencies(),
			]
        );
        $fields[] = $this->field(
            '_unit_price_sale', __( 'Sale Unit Price', 'dokan' ), 'text', 110, [
				'placeholder' => '0.00',
				'dependencies' => $this->non_variable_dependencies(),
			]
        );
        $fields[] = $this->field( '_mini_desc', __( 'Optional Mini Description', 'dokan' ), 'editor', 120 );

        return $fields;
    }

    /**
     * Place the EU compliance section card in the primary column of the editor layout.
     *
     * @since 5.0.12
     *
     * @param array $layouts Flat layout nodes.
     *
     * @return array
     */
    public function add_layout( array $layouts ): array {
        $layouts[] = [
            'id'        => self::SECTION_EU,
            'parent_id' => Elements::PRIMARY_COLUMN,
            'priority'  => 45,
            'layout'    => [
                'type'       => 'card',
                'withHeader' => true,
            ],
        ];

        return $layouts;
    }

    /**
     * Ensure the EU select fields are unwrapped from single-element arrays to scalar
     * strings before validation, so the Germanized meta-box writer reads plain values.
     *
     * @since 5.0.12
     *
     * @param array $string_fields Field ids to unwrap.
     *
     * @return array
     */
    public function register_single_select_fields( array $string_fields ): array {
        return array_merge(
            $string_fields,
            [ '_sale_price_label', '_sale_price_regular_label', '_unit', '_min_age', self::DELIVERY_TIME_KEY ]
        );
    }

    /**
     * Resolve a saved EU compliance value back into the form when editing.
     *
     * Delivery time comes from the taxonomy (not meta); checkboxes are normalised to a
     * boolean; the min-age "None" option is keyed `-1` (not ''); everything else falls
     * through to the product meta.
     *
     * @since 5.0.12
     *
     * @param mixed      $value      Resolved value so far (null when unresolved).
     * @param string     $field_name The schema field id.
     * @param WC_Product $product    The product being edited.
     *
     * @return mixed
     */
    public function resolve_field_value( $value, $field_name, $product ) {
        if ( ! $product instanceof WC_Product || ! in_array( $field_name, $this->field_keys(), true ) ) {
            return $value;
        }

        // Delivery time is a taxonomy term, not product meta.
        if ( self::DELIVERY_TIME_KEY === $field_name ) {
            $gzd_product = wc_gzd_get_product( $product );
            $term        = $gzd_product ? $gzd_product->get_delivery_time() : null;

            return is_object( $term ) ? $term->term_id : '';
        }

        if ( in_array( $field_name, $this->checkbox_keys(), true ) ) {
            return in_array( $product->get_meta( $field_name, true ), [ 'yes', '1', 1, true ], true );
        }

        // The min-age select's "no age" option is keyed '-1'; map empty or unknown legacy
        // meta to it so the "None" option preselects instead of nothing at all.
        if ( '_min_age' === $field_name ) {
            $min_age    = (string) $product->get_meta( '_min_age', true );
            $valid_ages = array_map( 'strval', array_keys( wc_gzd_get_age_verification_min_ages() ) );

            return in_array( $min_age, $valid_ages, true ) ? $min_age : '-1';
        }

        // Plain string/meta fields: let FormSchema's default fallback (get_meta) resolve
        // them so other `dokan_product_editor_schema_value` callbacks stay composable.
        return $value;
    }

    /**
     * Persist the EU compliance fields when a product is saved through the new editor.
     *
     * Only runs for REST (new-editor) saves — the legacy vendor form is handled by
     * Dashboard\Product. Writes ONLY the fields this integration manages, directly through
     * the Germanized product setters. It deliberately does NOT call the full meta-box writer
     * (WC_Germanized_Meta_Box_Product_Data::save()), which rebuilds the entire product from a
     * full form submission and would reset every unmanaged Germanized field (manufacturer,
     * food, safety, allergens, type flags, …) to its default on a partial vendor save.
     *
     * @since 5.0.12
     *
     * @param int   $product_id Saved product ID.
     * @param array $params     Resolved request params.
     *
     * @return void
     */
    public function save_eu_data( $product_id, $params ) {
        // Only handle new-editor (REST) saves; the legacy form path is Dashboard\Product.
        if ( ! ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
            return;
        }

        if ( ! class_exists( WC_Germanized_Meta_Box_Product_Data::class ) || ! function_exists( 'wc_gzd_get_product' ) ) {
            return;
        }

        // `dokan_new_product_added` / `dokan_product_updated` also fire on REST paths that do not
        // populate $_POST at all (e.g. dokan/v1 products via REST\Manager::on_dokan_rest_insert_product,
        // used by the mobile app). Bail unless this request actually carries a field we manage —
        // otherwise no setter would run yet WC_GZD_Product::save() would still execute, and that
        // drops the product_manufacturer / product_unit / product_price_label / product_deposit_type
        // term relationships of any product whose terms were set without the matching meta.
        if ( ! $this->has_managed_field() ) {
            return;
        }

        $product = wc_get_product( $product_id );

        if ( ! $product instanceof WC_Product ) {
            return;
        }

        // Prefer the payload type; fall back to the actual product type for partial updates
        // that omit `type`. Select variants can arrive as a single-element array, so unwrap.
        $type = is_array( $params ) && ! empty( $params['type'] ) ? $params['type'] : $product->get_type();
        if ( is_array( $type ) ) {
            $type = (string) reset( $type );
        }

        if ( ! in_array( $type, self::APPLIES_TO, true ) ) {
            return;
        }

        $gzd_product = wc_gzd_get_product( $product );

        if ( ! $gzd_product ) {
            return;
        }

        // Plain string/select props — set only when the field was part of this request.
        $string_setters = [
            '_sale_price_label'         => 'set_sale_price_label',
            '_sale_price_regular_label' => 'set_sale_price_regular_label',
            '_unit'                     => 'set_unit',
            '_unit_base'                => 'set_unit_base',
            '_unit_product'             => 'set_unit_product',
            '_mini_desc'                => 'set_mini_desc',
        ];

        foreach ( $string_setters as $key => $setter ) {
            if ( $this->has_post( $key ) ) {
                $gzd_product->{$setter}( $this->sanitized_post( $key ) );
            }
        }

        // Minimum age: only accept a valid age key, otherwise clear (mirrors the writer).
        if ( $this->has_post( '_min_age' ) ) {
            $valid_ages = array_map( 'strval', array_keys( wc_gzd_get_age_verification_min_ages() ) );
            $min_age    = (string) $this->sanitized_post( '_min_age' );
            $gzd_product->set_min_age( in_array( $min_age, $valid_ages, true ) ? $min_age : '' );
        }

        // Unit-price block is not applicable to variable products (the writer discards it too).
        if ( 'variable' !== $type ) {
            if ( $this->has_post( '_unit_price_regular' ) ) {
                $gzd_product->set_unit_price_regular( $this->sanitized_post( '_unit_price_regular' ) );
            }
            if ( $this->has_post( '_unit_price_sale' ) ) {
                $gzd_product->set_unit_price_sale( $this->sanitized_post( '_unit_price_sale' ) );
            }
            if ( $this->has_post( '_unit_price_auto' ) ) {
                $gzd_product->set_unit_price_auto( $this->is_checked( '_unit_price_auto' ) );
            }

            // Derive the active unit price (_unit_price) — the value actually shown on the
            // storefront. Mirrors WC_Germanized_Meta_Box_Product_Data::save_unit_price(): it
            // tracks the sale unit price while the product is on sale, otherwise the regular
            // unit price. Setting only the regular/sale props above would leave _unit_price
            // empty/stale, so the storefront would show no unit price after a vendor save.
            //
            // The sale unit price only wins when it is actually set: the field is always part
            // of the payload for simple/external products, so an on-sale product whose vendor
            // left "Sale Unit Price" blank would otherwise blank out _unit_price entirely even
            // though a regular unit price exists. Germanized guards the same way (see the
            // `'' !== $data['_unit_price_sale']` checks in the writer).
            if ( $this->has_post( '_unit_price_sale' ) || $this->has_post( '_unit_price_regular' ) ) {
                $unit_price_sale = (string) $gzd_product->get_unit_price_sale( 'edit' );

                $gzd_product->set_unit_price(
                    ( $product->is_on_sale() && '' !== $unit_price_sale )
                        ? $unit_price_sale
                        : $gzd_product->get_unit_price_regular( 'edit' )
                );
            }
        }

        // Free shipping checkbox.
        if ( $this->has_post( '_free_shipping' ) ) {
            $gzd_product->set_free_shipping( $this->is_checked( '_free_shipping' ) ? 'yes' : 'no' );
        }

        // Delivery time: the select value is a term id; resolve it to the term slug.
        if ( $this->has_post( self::DELIVERY_TIME_KEY ) ) {
            $term_id = absint( $this->sanitized_post( self::DELIVERY_TIME_KEY ) );
            $term    = $term_id ? get_term( $term_id, 'product_delivery_time' ) : null;
            $slug    = ( $term && ! is_wp_error( $term ) ) ? $term->slug : '';

            $gzd_product->set_default_delivery_time_slug( $slug );
        }

        // Persist: the underlying WC product save writes the meta props; the GZD product save
        // writes GZD-specific data (e.g. the delivery-time taxonomy term).
        $gzd_product->get_wc_product()->save();
        $gzd_product->save();
    }

    /**
     * Whether the current request carries at least one field this integration manages.
     *
     * Guards the save path against REST requests that never populate $_POST, where every
     * has_post() check is false and persisting would be a pure no-op write.
     *
     * @since 5.0.12
     *
     * @return bool
     */
    private function has_managed_field(): bool {
        foreach ( $this->field_keys() as $key ) {
            if ( $this->has_post( $key ) ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Whether a field id is present in the current request body.
     *
     * @since 5.0.12
     *
     * @param string $key Field id.
     *
     * @return bool
     */
    private function has_post( string $key ): bool {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- REST already authenticated the request.
        return array_key_exists( $key, $_POST );
    }

    /**
     * Read a request value through Germanized's own per-field sanitizer.
     *
     * @since 5.0.12
     *
     * @param string $key Field id / meta-box key.
     *
     * @return mixed
     */
    private function sanitized_post( string $key ) {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- REST authenticated; get_sanitized_field_value() sanitizes and unslashes.
        return WC_Germanized_Meta_Box_Product_Data::get_sanitized_field_value( $key, isset( $_POST[ $key ] ) ? $_POST[ $key ] : null );
    }

    /**
     * Whether a checkbox field id is truthy in the request.
     *
     * @since 5.0.12
     *
     * @param string $key Field id.
     *
     * @return bool
     */
    private function is_checked( string $key ): bool {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- REST already authenticated the request.
        return isset( $_POST[ $key ] ) && filter_var( wp_unslash( $_POST[ $key ] ), FILTER_VALIDATE_BOOLEAN );
    }

    /**
     * Build a single EU compliance field definition with the shared defaults.
     *
     * @since 5.0.12
     *
     * @param string $id       Field id (== Germanized meta-box field key).
     * @param string $label    Field label.
     * @param string $variant  Field variant (select|text|checkbox|editor).
     * @param int    $priority Ordering priority within the section.
     * @param array  $extra    Extra/overriding field properties.
     *
     * @return array
     */
    private function field( string $id, string $label, string $variant, int $priority, array $extra = [] ): array {
        return array_merge(
            [
                'id'           => $id,
                'section_id'   => self::SECTION_EU,
                'type'         => 'field',
                'label'        => $label,
                'variant'      => $variant,
                'required'     => false,
                'visibility'   => true,
                'priority'     => $priority,
                'dependencies' => $this->base_dependencies(),
            ],
            $extra
        );
    }

    /**
     * Dependencies shown for every EU field: visible only for the product types this
     * integration actually saves (APPLIES_TO), which mirrors the legacy
     * show_if_simple/show_if_external/show_if_variable wrappers.
     *
     * Derived from the editor's own product-type registry (`dokan_product_types`, the same
     * filter FormSchema::get_product_types() reads) rather than a hard-coded `!= grouped`,
     * so types contributed by other modules — e.g. `subscription` / `variable-subscription`
     * from vendor-subscription-product — are hidden too. Otherwise the fields would render
     * on those types, be filled in by the vendor and then be silently dropped by
     * save_eu_data()'s APPLIES_TO gate.
     *
     * @since 5.0.12
     *
     * @return array
     */
    private function base_dependencies(): array {
        if ( null !== $this->base_dependencies ) {
            return $this->base_dependencies;
        }

        $product_types = (array) apply_filters( 'dokan_product_types', [ 'simple' => __( 'Simple', 'dokan' ) ] );
        $unsupported   = array_values( array_diff( array_keys( $product_types ), self::APPLIES_TO ) );

        $this->base_dependencies = array_map(
            function ( $type ) {
                return [
                    'comparison' => '!=',
                    'key'        => Elements::TYPE,
                    'value'      => $type,
                ];
            },
            $unsupported
        );

        return $this->base_dependencies;
    }

    /**
     * Dependencies for the unit-price block: also hidden for variable products (legacy
     * show_if_simple show_if_external), whose parent unit prices the writer discards.
     *
     * @since 5.0.12
     *
     * @return array
     */
    private function non_variable_dependencies(): array {
        return array_merge(
            $this->base_dependencies(),
            [
                [
                    'comparison' => '!=',
                    'key'        => Elements::TYPE,
                    'value'      => 'variable',
                ],
            ]
        );
    }

    /**
     * Dependencies for delivery time + free shipping: hidden for virtual products
     * (legacy hide_if_virtual).
     *
     * @since 5.0.12
     *
     * @return array
     */
    private function non_virtual_dependencies(): array {
        return array_merge(
            $this->base_dependencies(),
            [
                [
                    'comparison' => '!=',
                    'key'        => Elements::VIRTUAL,
                    'value'      => true,
                ],
            ]
        );
    }

    /**
     * Schema field ids managed by this integration (used for value resolution).
     *
     * @since 5.0.12
     *
     * @return string[]
     */
    private function field_keys(): array {
        return [
            '_sale_price_label',
            '_sale_price_regular_label',
            '_unit',
            '_min_age',
            '_unit_product',
            '_unit_base',
            self::DELIVERY_TIME_KEY,
            '_free_shipping',
            '_unit_price_auto',
            '_unit_price_regular',
            '_unit_price_sale',
            '_mini_desc',
        ];
    }

    /**
     * EU compliance field ids rendered as checkboxes.
     *
     * @since 5.0.12
     *
     * @return string[]
     */
    private function checkbox_keys(): array {
        return [
            '_free_shipping',
            '_unit_price_auto',
        ];
    }
}
