<?php

/**
 * Subscription Products Hooks
 *
 * @package ArraySubs\Features\SubscriptionProducts\Services
 */

namespace ArraySubs\Features\SubscriptionProducts\Services;

use ArraySubs\Supports\Config;

defined('ABSPATH') || exit;

/**
 * WordPress hooks for subscription product functionality
 */
class Hooks
{
    /**
     * Hidden order-item meta containing the subscription rows shown in emails.
     *
     * @var string
     */
    public const ORDER_ITEM_EMAIL_DATA_META = '_arraysubs_subscription_email_item_data';

    /**
     * Enable auto-loading
     *
     * @var bool
     */
    public static $loadable = true;

    /**
     * Product IDs whose current admin save should be blocked.
     *
     * @var array<int,bool>
     */
    private array $blocked_product_saves = [];

    /**
     * Product IDs whose validation errors were already added this request.
     *
     * @var array<int,bool>
     */
    private array $reported_product_validation_errors = [];

    /**
     * Resolve the current single-product object without trusting the global blindly.
     *
     * @return \WC_Product|null
     */
    private function getCurrentProduct(): ?\WC_Product
    {
        global $product;

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

        if (!function_exists('is_product') || !is_product() || !function_exists('wc_get_product')) {
            return null;
        }

        $product_id = (int) get_queried_object_id();

        if ($product_id <= 0) {
            return null;
        }

        $queried_product = wc_get_product($product_id);

        return $queried_product instanceof \WC_Product ? $queried_product : null;
    }

    /**
     * Constructor
     */
    public function __construct()
    {
        // Product data tabs (Simple products)
        add_filter('woocommerce_product_data_tabs', [$this, 'addSubscriptionTab'], 10, 1);
        add_action('woocommerce_product_data_panels', [$this, 'addSubscriptionPanel']);

        // Add subscription checkbox to product type options header (next to Virtual/Downloadable)
        add_filter('product_type_options', [$this, 'addSubscriptionTypeOption'], 10, 1);

        // Variable product settings
        add_action('woocommerce_product_after_variable_attributes', [$this, 'addVariationFields'], 10, 3);

        // Save product meta
        add_action('woocommerce_process_product_meta', [$this, 'saveProductMeta'], 10, 1);
        add_action('woocommerce_save_product_variation', [$this, 'saveVariationMeta'], 10, 2);

        // Admin products list indicator
        add_action('manage_product_posts_custom_column', [$this, 'renderProductListSubscriptionIndicator'], 20, 2);
        add_action('manage_product_posts_custom_column', [$this, 'renderProductListSubscriptionPriceInfo'], 15, 2);

        // Frontend display - Product page
        add_action('woocommerce_single_product_summary', [$this, 'displaySubscriptionInfo'], 11);
        add_filter('woocommerce_get_price_html', [$this, 'subscriptionPriceHtml'], 20, 2);

        // Add subscription data to variation JSON for frontend display
        add_filter('woocommerce_available_variation', [$this, 'addVariationSubscriptionData'], 10, 3);

        // Enqueue frontend scripts for variable products
        add_action('wp_enqueue_scripts', [$this, 'enqueueVariableProductScripts']);
        add_action('wp_enqueue_scripts', [$this, 'enqueueCartCheckoutStyles']);

        // Cart page display
        add_filter('woocommerce_cart_item_price', [$this, 'cartItemPrice'], 10, 3);
        add_filter('woocommerce_cart_item_subtotal', [$this, 'cartItemSubtotal'], 10, 3);
        add_filter('woocommerce_cart_item_name', [$this, 'cartItemName'], 10, 3);
        add_filter('woocommerce_get_item_data', [$this, 'cartItemData'], 20, 2);
        add_action('woocommerce_checkout_create_order_line_item', [$this, 'addOrderItemEmailData'], 20, 4);

        // Mini-cart / Widget cart display
        add_filter('woocommerce_widget_cart_item_quantity', [$this, 'widgetCartItemQuantity'], 10, 3);
        add_filter('woocommerce_mini_cart_item_class', [$this, 'miniCartItemClass'], 10, 3);

        // Display fees and total in mini-cart using multiple hooks to ensure visibility
        add_action('woocommerce_widget_shopping_cart_total', [$this, 'displayWidgetCartFees'], 20);
        add_action('woocommerce_widget_shopping_cart_before_buttons', [$this, 'displayWidgetCartFees'], 10);

        // Checkout order review display (these work with AJAX updates)
        add_filter('woocommerce_checkout_cart_item_quantity', [$this, 'checkoutCartItemQuantity'], 10, 3);

        // Order items display
        add_filter('woocommerce_order_item_name', [$this, 'orderItemName'], 10, 2);

        // Add signup fee to cart - runs on all cart calculations including AJAX
        add_action('woocommerce_cart_calculate_fees', [$this, 'addSignupFeeToCart'], 20, 1);

        // Force cart fragments refresh when subscription product added
        add_filter('woocommerce_add_to_cart_fragments', [$this, 'addCartFragments'], 10, 1);

        // Ensure fees are recalculated on checkout AJAX updates
        add_action('woocommerce_before_calculate_totals', [$this, 'beforeCalculateTotals'], 10, 1);

        // Button text
        add_filter('woocommerce_product_single_add_to_cart_text', [$this, 'addToCartButtonText'], 10, 2);
        add_filter('woocommerce_product_add_to_cart_text', [$this, 'addToCartButtonText'], 10, 2);

        // Validation
        add_filter('wp_insert_post_data', [$this, 'preserveProductStatusForInvalidSubscriptionSave'], 10, 2);
        add_filter('woocommerce_admin_process_product_object', [$this, 'validateProduct'], 10, 1);
    }

    /**
     * Render product row indicators below the product title in the products list.
     *
     * @param string $column_name Column name.
     * @param int    $post_id     Product post ID.
     * @return void
     */
    public function renderProductListSubscriptionIndicator(string $column_name, int $post_id): void
    {
        if ('name' !== $column_name || $post_id <= 0) {
            return;
        }

        if (!function_exists('wc_get_product') || !function_exists('wc_get_product_types')) {
            return;
        }

        $product = wc_get_product($post_id);

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

        $tags = [];
        $product_type = (string) $product->get_type();
        $product_types = wc_get_product_types();

        if ('' !== $product_type) {
            $tags[] = [
                'class' => 'arraysubs-product-list-indicator__tag arraysubs-product-list-indicator__tag--type',
                'label' => $product_types[$product_type] ?? ucwords(str_replace(['-', '_'], ' ', $product_type)),
            ];
        }

        $is_subscription = function_exists('arraysubs_is_subscription_product') && arraysubs_is_subscription_product($post_id);

        if ($is_subscription) {
            $tags[] = [
                'class' => 'arraysubs-product-list-indicator__tag arraysubs-product-list-indicator__tag--subscription',
                'label' => __('Subscription', 'arraysubs'),
            ];

            $has_trial = false;

            if ($product->is_type('variable')) {
                foreach ($product->get_children() as $variation_id) {
                    if ((int) get_post_meta($variation_id, '_trial_length', true) > 0) {
                        $has_trial = true;
                        break;
                    }
                }
            } elseif (function_exists('arraysubs_get_product_subscription_data')) {
                $sub_data = arraysubs_get_product_subscription_data($post_id);
                $has_trial = $sub_data && (int) ($sub_data['trial_length'] ?? 0) > 0;
            }

            if ($has_trial) {
                $tags[] = [
                    'class' => 'arraysubs-product-list-indicator__tag arraysubs-product-list-indicator__tag--trial',
                    'label' => __('Trial', 'arraysubs'),
                ];
            }
        }

        if (empty($tags)) {
            return;
        }

        echo '<div class="arraysubs-product-list-indicator">';

        foreach ($tags as $tag) {
            echo '<span class="' . esc_attr($tag['class']) . '">' . esc_html($tag['label']) . '</span>';
        }

        echo '</div>';
    }

    /**
     * Render subscription pricing details in the product price column.
     *
     * @param string $column_name Column name.
     * @param int    $post_id     Product post ID.
     * @return void
     */
    public function renderProductListSubscriptionPriceInfo(string $column_name, int $post_id): void
    {
        if ('price' !== $column_name || $post_id <= 0) {
            return;
        }

        if (!function_exists('wc_get_product') || !function_exists('arraysubs_is_subscription_product')) {
            return;
        }

        $product = wc_get_product($post_id);

        if (!$product instanceof \WC_Product || !arraysubs_is_subscription_product($post_id)) {
            return;
        }

        if ($product->is_type('variable')) {
            $this->renderVariableSubscriptionPriceInfo($product);
        } elseif (function_exists('arraysubs_get_product_subscription_data')) {
            $data = arraysubs_get_product_subscription_data($post_id);
            if ($data) {
                $this->renderSimpleSubscriptionPriceInfo($data);
            }
        }
    }

    /**
     * Render subscription price details for a simple subscription product.
     *
     * @param array $data Subscription product data.
     * @return void
     */
    private function renderSimpleSubscriptionPriceInfo(array $data): void
    {
        if (!function_exists('arraysubs_get_billing_schedule_string')) {
            return;
        }

        $schedule = arraysubs_get_billing_schedule_string((int) $data['interval'], (string) $data['period']);

        echo '<div class="arraysubs-price-info">';

        echo '<div class="arraysubs-price-info__row arraysubs-price-info__row--schedule">';
        echo '/ <span class="arraysubs-price-info__schedule">' . esc_html($schedule) . '</span>';
        echo '</div>';

        if ((float) $data['signup_fee'] > 0) {
            echo '<div class="arraysubs-price-info__row arraysubs-price-info__row--signup">';
            echo '+ ' . wp_kses_post(wc_price((float) $data['signup_fee'])) . ' ' . esc_html__('signup fee', 'arraysubs');
            echo '</div>';
        }

        if (!empty($data['enable_different_renewal_price']) && !empty($data['different_renewal_price'])) {
            $after = (int) ($data['different_renewal_price_after'] ?? 1);
            $renewal_price_html = wp_kses_post(wc_price((float) $data['different_renewal_price']));

            echo '<div class="arraysubs-price-info__row arraysubs-price-info__row--renewal">';
            echo esc_html__('then', 'arraysubs') . ' ' . $renewal_price_html . ' / ' . esc_html($schedule); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            if ($after > 1) {
                echo ' ' . sprintf(
                    /* translators: %d: number of initial payments before renewal price kicks in */
                    esc_html__('after %d payments', 'arraysubs'),
                    $after
                );
            }
            echo '</div>';
        }

        echo '</div>';
    }

    /**
     * Render subscription price details for a variable subscription product.
     *
     * @param \WC_Product $product Variable product object.
     * @return void
     */
    private function renderVariableSubscriptionPriceInfo(\WC_Product $product): void
    {
        if (!function_exists('arraysubs_get_billing_schedule_string') || !function_exists('arraysubs_get_product_subscription_data')) {
            return;
        }

        $sub_data_list = [];

        foreach ($product->get_children() as $variation_id) {
            $var_data = arraysubs_get_product_subscription_data($variation_id);
            if ($var_data) {
                $sub_data_list[] = $var_data;
            }
        }

        if (empty($sub_data_list)) {
            return;
        }

        $periods = array_unique(array_map(
            static fn(array $d): string => arraysubs_get_billing_schedule_string((int) $d['interval'], (string) $d['period']),
            $sub_data_list
        ));

        $with_signup  = array_values(array_filter($sub_data_list, static fn(array $d): bool => (float) $d['signup_fee'] > 0));
        $with_renewal = array_filter($sub_data_list, static fn(array $d): bool => !empty($d['enable_different_renewal_price']) && !empty($d['different_renewal_price']));

        echo '<div class="arraysubs-price-info">';

        echo '<div class="arraysubs-price-info__row arraysubs-price-info__row--schedule">';
        echo '/ ';
        if (count($periods) === 1) {
            echo '<span class="arraysubs-price-info__schedule">' . esc_html(reset($periods)) . '</span>';
        } else {
            echo '<span class="arraysubs-price-info__schedule">' . esc_html__('varies', 'arraysubs') . '</span>';
        }
        echo '</div>';

        if (!empty($with_signup)) {
            $signup_fees = array_map(static fn(array $d): float => (float) $d['signup_fee'], $with_signup);
            $min_fee     = min($signup_fees);
            $max_fee     = max($signup_fees);
            $is_partial  = count($with_signup) < count($sub_data_list);

            echo '<div class="arraysubs-price-info__row arraysubs-price-info__row--signup">';
            if ($min_fee < $max_fee) {
                echo '+ ' . wp_kses_post(wc_price($min_fee)) . ' &ndash; ' . wp_kses_post(wc_price($max_fee)) . ' ' . esc_html__('signup fee', 'arraysubs'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            } elseif ($is_partial) {
                echo '+ ' . wp_kses_post(wc_price($min_fee)) . ' ' . esc_html__('signup fee (some variations)', 'arraysubs'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            } else {
                echo '+ ' . wp_kses_post(wc_price($min_fee)) . ' ' . esc_html__('signup fee', 'arraysubs'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            }
            echo '</div>';
        }

        if (!empty($with_renewal)) {
            echo '<div class="arraysubs-price-info__row arraysubs-price-info__row--renewal">';
            esc_html_e('renewal price varies', 'arraysubs');
            echo '</div>';
        }

        echo '</div>';
    }

    /**
     * Add Subscription checkbox to product type options header
     *
     * Adds the "Subscription" checkbox next to Virtual/Downloadable in the product data header.
     *
     * @param array $options Product type options
     * @return array Modified options
     */
    public function addSubscriptionTypeOption(array $options): array
    {
        $options['is_subscription'] = [
            'id'            => '_is_subscription',
            'wrapper_class' => 'show_if_simple show_if_variable',
            'label'         => __('Subscription', 'arraysubs'),
            'description'   => __('Enable subscription for this product', 'arraysubs'),
            'default'       => 'no',
        ];

        return $options;
    }

    /**
     * Add Subscription tab to product data tabs
     *
     * @param array $tabs Product data tabs
     * @return array Modified tabs
     */
    public function addSubscriptionTab(array $tabs): array
    {
        $tabs['arraysubs'] = [
            'label'    => __('Subscription', 'arraysubs'),
            'target'   => 'arraysubs_product_data',
            'class'    => ['show_if_simple'],
            'priority' => 25, // Before Advanced tab (30)
        ];

        return $tabs;
    }

    /**
     * Add Subscription panel content
     */
    public function addSubscriptionPanel(): void
    {
        global $post;
        include dirname(__DIR__) . '/views/simple-product-fields.php';
    }

    /**
     * Add subscription fields to variations
     *
     * @param int      $loop           Variation loop index
     * @param array    $variation_data Variation data
     * @param \WP_Post $variation      Variation post object
     */
    public function addVariationFields(int $loop, array $variation_data, \WP_Post $variation): void
    {
        $arraysubs_variation_id = $variation->ID;
        include dirname(__DIR__) . '/views/variation-fields.php';
    }

    /**
     * Save product meta for simple products
     *
     * @param int $post_id Product post ID
     */
    public function saveProductMeta(int $post_id): void
    {
        // Verify nonce
        if (!isset($_POST['woocommerce_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['woocommerce_meta_nonce'])), 'woocommerce_save_data')) {
            return;
        }

        $validation_errors = $this->getPostedSubscriptionProductValidationErrors(null, $post_id);
        if (!empty($validation_errors)) {
            $this->blocked_product_saves[$post_id] = true;
            $this->addProductValidationErrors($post_id, $validation_errors);
            return;
        }

        // Save subscription fields
        // Note: _subscription_price is removed as we use product's regular/sale price
        $fields = [
            '_is_subscription'                   => 'sanitize_text_field',
            '_subscription_period'               => 'sanitize_text_field',
            '_subscription_interval'             => 'absint',
            '_subscription_length'               => 'absint',
            '_trial_length'                      => 'absint',
            '_trial_period'                      => 'sanitize_text_field',
            '_signup_fee'                        => 'floatval',
            '_enable_renewal_price'              => 'sanitize_text_field',
            '_renewal_price'                     => 'floatval',
            '_renewal_price_after'               => 'absint',
        ];

        // Shipping settings only when pro shipping module is active
        if (function_exists('arraysubs_has_module') && arraysubs_has_module('SubscriptionShipping') === 1) {
            $fields['_arraysubs_shipping_type']             = 'sanitize_text_field';
            $fields['_arraysubs_initial_shipping_override'] = 'floatval';
            $fields['_arraysubs_renewal_shipping_override'] = 'floatval';
        }

        $old_values = [];
        foreach (array_keys($fields) as $key) {
            $old_values[$key] = get_post_meta($post_id, $key, true);
        }

        // Checkbox fields that need value normalization (header checkboxes post 'on', panel ones post 'yes')
        $checkbox_fields = ['_is_subscription', '_enable_renewal_price'];

        foreach ($fields as $key => $sanitize_callback) {
            if (isset($_POST[$key])) {
                if (in_array($key, $checkbox_fields, true)) {
                    // Normalize checkbox value to 'yes' regardless of posted value ('on' or 'yes')
                    $value = 'yes';
                } else {
                    $raw_value = sanitize_text_field(wp_unslash($_POST[$key]));
                    $value = call_user_func($sanitize_callback, $raw_value);
                }
                update_post_meta($post_id, $key, $value);
            } else {
                // Delete meta if not set (for checkboxes)
                delete_post_meta($post_id, $key);
            }
        }

        // Safety: lifetime deals have no recurring cycle or finite billing length.
        if (get_post_meta($post_id, '_subscription_period', true) === 'lifetime') {
            update_post_meta($post_id, '_subscription_interval', 1);
            update_post_meta($post_id, '_subscription_length', 0);
        }

        // For variable products: sync _is_subscription to all variations
        $product = wc_get_product($post_id);
        if ($product && $product->is_type('variable')) {
            $is_subscription = isset($_POST['_is_subscription']) ? 'yes' : '';
            $variations = $product->get_children();
            foreach ($variations as $variation_id) {
                if ($is_subscription === 'yes') {
                    update_post_meta($variation_id, '_is_subscription', 'yes');
                } else {
                    delete_post_meta($variation_id, '_is_subscription');
                }
            }
        }

        $audit_changes = $this->getChangedProductAuditChanges($post_id, $old_values, $fields);
        $changed_fields = array_map(static fn(array $change): string => $change['label'], $audit_changes);

        if (!empty($changed_fields) && function_exists('arraysubs_add_global_note')) {
            $product = wc_get_product($post_id);
            $product_name = $product ? arraysubs_get_subscription_product_name($product) : '#' . $post_id;
            $actor_id = get_current_user_id();

            arraysubs_add_global_note(
                sprintf(
                    /* translators: 1: product ID, 2: product name, 3: changed field labels. */
                    __('Product #%1$d settings updated for %2$s: %3$s.', 'arraysubs'),
                    $post_id,
                    esc_html($product_name),
                    esc_html(implode(', ', $changed_fields))
                ),
                'private',
                $actor_id > 0 ? $actor_id : 0,
                $audit_changes,
                'product'
            );
        }
    }

    /**
     * Save variation meta
     *
     * @param int $variation_id Variation post ID
     * @param int $loop         Loop index
     */
    public function saveVariationMeta(int $variation_id, int $loop): void
    {
        // Verify WooCommerce nonce (regular save or AJAX save)
        $nonce_verified = false;
        if (
            isset($_POST['woocommerce_meta_nonce'])
            && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['woocommerce_meta_nonce'])), 'woocommerce_save_data')
        ) {
            $nonce_verified = true;
        }
        if (
            !$nonce_verified && isset($_POST['security'])
            && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['security'])), 'save-variations')
        ) {
            $nonce_verified = true;
        }
        if (!$nonce_verified) {
            return;
        }

        // Note: _subscription_price is removed as we use variation's regular/sale price
        $fields = [
            '_is_subscription'                     => 'sanitize_text_field',
            '_subscription_period'                 => 'sanitize_text_field',
            '_subscription_interval'               => 'absint',
            '_subscription_length'                 => 'absint',
            '_trial_length'                        => 'absint',
            '_trial_period'                        => 'sanitize_text_field',
            '_signup_fee'                          => 'floatval',
            '_enable_renewal_price'                => 'sanitize_text_field',
            '_renewal_price'                       => 'floatval',
            '_renewal_price_after'                 => 'absint',
        ];

        // Shipping settings only when pro shipping module is active
        if (function_exists('arraysubs_has_module') && arraysubs_has_module('SubscriptionShipping') === 1) {
            $fields['_arraysubs_shipping_type']             = 'sanitize_text_field';
            $fields['_arraysubs_initial_shipping_override'] = 'floatval';
            $fields['_arraysubs_renewal_shipping_override'] = 'floatval';
        }

        $old_values = [];
        foreach (array_keys($fields) as $key) {
            $old_values[$key] = get_post_meta($variation_id, $key, true);
        }

        foreach ($fields as $key => $sanitize_callback) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via callback below
            if (isset($_POST[$key][$loop])) {
                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via callback
                $value = call_user_func($sanitize_callback, wp_unslash($_POST[$key][$loop]));
                update_post_meta($variation_id, $key, $value);
            } else {
                delete_post_meta($variation_id, $key);
            }
        }

        // Safety: lifetime deals have no recurring cycle or finite billing length.
        if (get_post_meta($variation_id, '_subscription_period', true) === 'lifetime') {
            update_post_meta($variation_id, '_subscription_interval', 1);
            update_post_meta($variation_id, '_subscription_length', 0);
        }

        $audit_changes = $this->getChangedProductAuditChanges($variation_id, $old_values, $fields);
        $changed_fields = array_map(static fn(array $change): string => $change['label'], $audit_changes);

        if (!empty($changed_fields) && function_exists('arraysubs_add_global_note')) {
            $product = wc_get_product($variation_id);
            $product_name = $product ? arraysubs_get_subscription_product_name($product) : '#' . $variation_id;
            $actor_id = get_current_user_id();

            arraysubs_add_global_note(
                sprintf(
                    /* translators: 1: variation ID, 2: variation name, 3: changed field labels. */
                    __('Product variation #%1$d settings updated for %2$s: %3$s.', 'arraysubs'),
                    $variation_id,
                    esc_html($product_name),
                    esc_html(implode(', ', $changed_fields))
                ),
                'private',
                $actor_id > 0 ? $actor_id : 0,
                $audit_changes,
                'product'
            );
        }
    }

    /**
     * Build structured changed product field entries for audit notes.
     *
     * @param int   $post_id    Product or variation ID.
     * @param array $old_values Previous values.
     * @param array $fields     Supported field list.
     * @return array<int, array<string, string>>
     */
    private function getChangedProductAuditChanges(int $post_id, array $old_values, array $fields): array
    {
        $config = [
            '_is_subscription' => ['label' => __('Subscription enabled', 'arraysubs'), 'context' => 'toggle'],
            '_subscription_period' => ['label' => __('Billing period', 'arraysubs')],
            '_subscription_interval' => ['label' => __('Billing interval', 'arraysubs')],
            '_subscription_length' => ['label' => __('Subscription length', 'arraysubs')],
            '_trial_length' => ['label' => __('Trial length', 'arraysubs')],
            '_trial_period' => ['label' => __('Trial period', 'arraysubs')],
            '_signup_fee' => ['label' => __('Signup fee', 'arraysubs'), 'context' => 'money'],
            '_enable_renewal_price' => ['label' => __('Different renewal price toggle', 'arraysubs'), 'context' => 'toggle'],
            '_renewal_price' => ['label' => __('Different renewal price', 'arraysubs'), 'context' => 'money'],
            '_renewal_price_after' => ['label' => __('Different renewal price after', 'arraysubs')],
            '_arraysubs_shipping_type' => ['label' => __('Shipping type', 'arraysubs')],
            '_arraysubs_initial_shipping_override' => ['label' => __('Initial shipping override', 'arraysubs'), 'context' => 'money'],
            '_arraysubs_renewal_shipping_override' => ['label' => __('Renewal shipping override', 'arraysubs'), 'context' => 'money'],
        ];

        $changes = [];

        foreach (array_keys($fields) as $key) {
            $old_value = $old_values[$key] ?? '';
            $new_value = get_post_meta($post_id, $key, true);

            if ((string) $old_value !== (string) $new_value) {
                $field_config = $config[$key] ?? ['label' => $key, 'context' => 'text'];
                $context = $field_config['context'] ?? 'text';

                if ($context === 'toggle') {
                    $old_display = arraysubs_format_audit_value((string) $old_value === 'yes');
                    $new_display = arraysubs_format_audit_value((string) $new_value === 'yes');
                } elseif ($context === 'money') {
                    $old_display = arraysubs_format_audit_value($old_value, 'money');
                    $new_display = arraysubs_format_audit_value($new_value, 'money');
                } else {
                    $old_display = arraysubs_format_audit_value($old_value);
                    $new_display = arraysubs_format_audit_value($new_value);
                }

                $changes[] = arraysubs_create_audit_change_entry(
                    $field_config['label'],
                    $old_display,
                    $new_display
                );
            }
        }

        return $changes;
    }

    /**
     * Display subscription info on product page
     * Handles both simple products (static display) and variable products (container for JS)
     */
    public function displaySubscriptionInfo(): void
    {
        $product = $this->getCurrentProduct();

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

        // For variable products with subscription variations
        if ($product->is_type('variable') && arraysubs_has_subscription_variations($product)) {
            include dirname(__DIR__) . '/views/variable-product-subscription-info.php';
            return;
        }

        $product_id = $product->get_id();
        $is_subscription = get_post_meta($product_id, '_is_subscription', true);

        // For simple subscription products
        if ($is_subscription === 'yes') {
            include dirname(__DIR__) . '/views/product-subscription-info.php';
        }
    }

    /**
     * Add compact billing suffixes to simple subscription product prices.
     *
     * @param string      $price_html Existing WooCommerce price HTML.
     * @param \WC_Product $product    Product object.
     * @return string
     */
    public function subscriptionPriceHtml(string $price_html, \WC_Product $product): string
    {
        if (
            !function_exists('arraysubs_is_subscription_product')
            || !function_exists('arraysubs_get_product_price_html')
            || !($product instanceof \WC_Product)
            || $product->is_type('variable')
            || !arraysubs_is_subscription_product($product)
        ) {
            return $price_html;
        }

        $subscription_price_html = arraysubs_get_product_price_html($product);

        return $subscription_price_html !== '' ? $subscription_price_html : $price_html;
    }

    /**
     * Add subscription data to variation JSON for frontend display
     *
     * @param array                 $variation_data Variation data array
     * @param \WC_Product_Variable  $product        Parent variable product
     * @param \WC_Product_Variation $variation     Variation product
     * @return array Modified variation data
     */
    public function addVariationSubscriptionData(array $variation_data, $product, $variation): array
    {
        $variation_id = $variation->get_id();
        $is_subscription = get_post_meta($variation_id, '_is_subscription', true);

        if ($is_subscription === 'yes') {
            $data = arraysubs_get_product_subscription_data($variation_id);

            if ($data) {
                $billing_schedule = arraysubs_get_billing_schedule_string($data['interval'], $data['period']);

                $variation_data['arraysubs_is_subscription'] = true;
                $variation_data['arraysubs_data'] = [
                    'price'                         => $data['price'],
                    'price_html'                    => wc_price($data['price']),
                    'period'                        => $data['period'],
                    'interval'                      => $data['interval'],
                    'length'                        => $data['length'],
                    'billing_schedule'              => $billing_schedule,
                    'trial_length'                  => $data['trial_length'],
                    'trial_period'                  => $data['trial_period'],
                    'signup_fee'                    => $data['signup_fee'],
                    'signup_fee_html'               => $data['signup_fee'] > 0 ? wc_price($data['signup_fee']) : '',
                    'enable_different_renewal_price' => $data['enable_different_renewal_price'],
                    'different_renewal_price'       => $data['different_renewal_price'],
                    'different_renewal_price_html'  => $data['different_renewal_price'] ? wc_price($data['different_renewal_price']) : '',
                    'different_renewal_price_after' => $data['different_renewal_price_after'],
                    'is_on_sale'                    => $variation->is_on_sale(),
                    'regular_price_html'            => wc_price($variation->get_regular_price()),
                ];
            }
        } else {
            $variation_data['arraysubs_is_subscription'] = false;
        }

        return $variation_data;
    }

    /**
     * Enqueue frontend scripts for variable subscription products
     */
    public function enqueueVariableProductScripts(): void
    {
        if (!function_exists('is_product') || !is_product()) {
            return;
        }

        $product = $this->getCurrentProduct();

        if (!$product instanceof \WC_Product || !$product->is_type('variable')) {
            return;
        }

        // Only enqueue if this variable product has subscription variations
        if (!arraysubs_has_subscription_variations($product)) {
            return;
        }

        $asset_file = Config::get('plugin.public_path') . 'build/subscription-variable-product.asset.php';
        $asset = file_exists($asset_file)
            ? require $asset_file
            : ['dependencies' => ['jquery'], 'version' => ARRAYSUBS_VERSION];

        $dependencies = array_unique(array_merge($asset['dependencies'], ['jquery', 'wc-add-to-cart-variation']));

        wp_enqueue_script(
            'arraysubs-subscription-variable-product',
            Config::get('plugin.public_url') . 'build/subscription-variable-product.js',
            $dependencies,
            $asset['version'],
            true
        );

        $arraysubs_variable_product_data = [
            'i18n' => [
                /* translators: %1$s initial price html, %2$s billing schedule text, %3$d payment count, %4$s renewal price html */
                'pricingAfterN' => __('%1$s %2$s for the first %3$d payments, then %4$s %2$s', 'arraysubs'),
                /* translators: %1$s initial price html, %2$s renewal price html, %3$s billing schedule text */
                'pricingFirstThen' => __('First payment: %1$s, then %2$s %3$s', 'arraysubs'),
                /* translators: %1$s current price html, %2$s billing schedule text, %3$s regular price html */
                'pricingOnSale' => __('%1$s %2$s (was %3$s)', 'arraysubs'),
                /* translators: %1$s price html, %2$s billing schedule text */
                'pricingDefault' => __('%1$s %2$s', 'arraysubs'),
                /* translators: %1$s signup fee html */
                'signupFee' => __('+ %1$s signup fee', 'arraysubs'),
                /* translators: %1$d trial length, %2$s trial period */
                'trialInfo' => __('Includes %1$d-%2$s free trial', 'arraysubs'),
                /* translators: %1$d total periods, %2$s period unit */
                'subscriptionLength' => __('Subscription length: %1$d %2$s', 'arraysubs'),
            ],
        ];

        $arraysubs_json_flags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;

        wp_add_inline_script(
            'arraysubs-subscription-variable-product',
            'window.arraySubsVariableProduct = ' . wp_json_encode($arraysubs_variable_product_data, $arraysubs_json_flags) . ';',
            'before'
        );
    }

    /**
     * Enqueue cart and checkout subscription detail assets.
     *
     * @return void
     */
    public function enqueueCartCheckoutStyles(): void
    {
        if (!is_cart() && !is_checkout()) {
            return;
        }

        $asset_file = Config::get('plugin.public_path') . 'build/store-api-cart-subscription.asset.php';
        $asset = file_exists($asset_file)
            ? require $asset_file
            : ['dependencies' => [], 'version' => ARRAYSUBS_VERSION];

        wp_enqueue_style(
            'arraysubs-store-api-cart-subscription',
            Config::get('plugin.public_url') . 'build/store-api-cart-subscription.css',
            [],
            $asset['version']
        );

        $dependencies = array_unique(
            array_merge($asset['dependencies'] ?? [], ['wc-blocks-checkout'])
        );

        wp_enqueue_script(
            'arraysubs-store-api-cart-subscription',
            Config::get('plugin.public_url') . 'build/store-api-cart-subscription.js',
            $dependencies,
            $asset['version'],
            true
        );

        wp_set_script_translations(
            'arraysubs-store-api-cart-subscription',
            'arraysubs',
            Config::get('plugin.path') . 'languages'
        );
    }

    /**
     * Modify cart item price display
     *
     * @param string $price        Price HTML
     * @param array  $cart_item    Cart item data
     * @param string $cart_item_key Cart item key
     * @return string Modified price HTML
     */
    public function cartItemPrice(string $price, array $cart_item, string $cart_item_key): string
    {
        $product_id = $cart_item['product_id'];
        $variation_id = $cart_item['variation_id'] ?? 0;
        $check_id = $variation_id > 0 ? $variation_id : $product_id;

        if (!arraysubs_is_subscription_product($check_id)) {
            return $price;
        }

        $data = arraysubs_get_cart_item_subscription_data($cart_item);

        if (!$data) {
            return $price;
        }

        $is_lifetime = ($data['period'] ?? '') === 'lifetime';
        $output = $price;

        if (!$is_lifetime) {
            $billing_schedule = arraysubs_get_billing_schedule_string($data['interval'], $data['period']);
            $output .= ' <small class="subscription-schedule">/ ' . esc_html($billing_schedule) . '</small>';
        }

        // Add signup fee indicator if exists
        if ($data['signup_fee'] > 0) {
            $output .= '<br><small class="subscription-signup-fee">+ ' . wp_kses_post(wc_price($data['signup_fee'])) . ' ' . esc_html__('signup fee', 'arraysubs') . '</small>';
        }

        // Add trial period indicator if exists
        if (!$is_lifetime && $data['trial_length'] > 0) {
            $trial_schedule = arraysubs_get_duration_string($data['trial_length'], $data['trial_period']);
            /* translators: %s: trial period duration (e.g., "7 days", "1 month") */
            $output .= '<br><small class="subscription-trial">' . sprintf(__('%s free trial', 'arraysubs'), esc_html($trial_schedule)) . '</small>';
        }

        // Add different renewal price indicator if enabled
        if (!$is_lifetime && $data['enable_different_renewal_price'] && !empty($data['different_renewal_price'])) {
            if ($data['different_renewal_price_after'] > 1) {
                /* translators: 1: renewal price amount, 2: payment cycle count */
                $renewal_text = sprintf(
                    /* translators: 1: renewal price amount, 2: payment cycle count */
                    __('then %1$s after %2$d payments', 'arraysubs'),
                    wp_kses_post(wc_price($data['different_renewal_price'])),
                    $data['different_renewal_price_after']
                );
            } else {
                /* translators: %s: renewal price amount after initial payment */
                $renewal_text = sprintf(
                    /* translators: %s: renewal price amount after initial payment */
                    __('then %s', 'arraysubs'),
                    wp_kses_post(wc_price($data['different_renewal_price']))
                );
            }
            $output .= '<br><small class="subscription-renewal-price">' . $renewal_text . '</small>';
        }

        return $output;
    }

    /**
     * Modify cart item subtotal display to show subscription info
     *
     * @param string $subtotal     Subtotal HTML
     * @param array  $cart_item    Cart item data
     * @param string $cart_item_key Cart item key
     * @return string Modified subtotal HTML
     */
    public function cartItemSubtotal(string $subtotal, array $cart_item, string $cart_item_key): string
    {
        $product_id = $cart_item['product_id'];
        $variation_id = $cart_item['variation_id'] ?? 0;
        $check_id = $variation_id > 0 ? $variation_id : $product_id;

        if (!arraysubs_is_subscription_product($check_id)) {
            return $subtotal;
        }

        $data = arraysubs_get_cart_item_subscription_data($cart_item);

        if (!$data) {
            return $subtotal;
        }

        $quantity = max(1, absint($cart_item['quantity'] ?? 1));
        $is_lifetime = ($data['period'] ?? '') === 'lifetime';
        $has_trial = !$is_lifetime && !empty($data['trial_length']) && (int) $data['trial_length'] > 0;

        // Build detailed subtotal display
        $output = '<span class="subscription-subtotal">';

        if ($has_trial) {
            $trial_schedule = arraysubs_get_duration_string((int) $data['trial_length'], (string) $data['trial_period']);
            $billing_summary = $this->getSubscriptionBillingSummary($data, $quantity);

            $output .= sprintf(
                /* translators: %s: free trial duration, such as "2 days". */
                esc_html__('Free for %s', 'arraysubs'),
                esc_html($trial_schedule)
            );
            $output .= '<br><small class="subscription-renewal-info">' . sprintf(
                /* translators: %s: recurring billing summary, such as "$50 every 5 days". */
                esc_html__('Then %s', 'arraysubs'),
                wp_kses_post($billing_summary)
            ) . '</small>';
        } else {
            $output .= $subtotal;
        }

        if (!$is_lifetime && !$has_trial) {
            $billing_schedule = arraysubs_get_billing_schedule_string($data['interval'], $data['period']);
            $output .= ' <small class="subscription-schedule">' . esc_html($billing_schedule) . '</small>';
        }

        // Different renewal price info
        if (!$is_lifetime && !$has_trial && $data['enable_different_renewal_price'] && !empty($data['different_renewal_price'])) {
            $output .= '<br><small class="subscription-renewal-info">';
            if ($data['different_renewal_price_after'] > 1) {
                /* translators: 1: renewal price amount, 2: count of payment cycles */
                $output .= sprintf(
                    /* translators: 1: renewal price amount, 2: count of payment cycles */
                    esc_html__('First %2$d payments, then %1$s', 'arraysubs'),
                    wp_kses_post(wc_price($data['different_renewal_price'] * $quantity)),
                    $data['different_renewal_price_after']
                );
            } else {
                /* translators: %s: renewal price amount */
                $output .= sprintf(
                    /* translators: %s: renewal price amount */
                    esc_html__('Then %s', 'arraysubs'),
                    wp_kses_post(wc_price($data['different_renewal_price'] * $quantity))
                );
            }
            $output .= '</small>';
        }

        // Signup fee info (shown separately, added as cart fee)
        if ($data['signup_fee'] > 0) {
            $output .= '<br><small class="subscription-signup-info">';
            if ($has_trial) {
                $output .= sprintf(
                    /* translators: %s: one-time signup fee amount due today. */
                    esc_html__('+ %s signup fee due today', 'arraysubs'),
                    wp_kses_post(wc_price($data['signup_fee']))
                );
            } else {
                $output .= sprintf(
                    /* translators: %s: one-time signup fee amount charged at purchase. */
                    esc_html__('+ %s signup fee', 'arraysubs'),
                    wp_kses_post(wc_price($data['signup_fee']))
                );
            }
            $output .= '</small>';
        }

        $output .= '</span>';

        return $output;
    }

    /**
     * Modify cart item name to include subscription info
     *
     * @param string $name         Product name HTML
     * @param array  $cart_item    Cart item data
     * @param string $cart_item_key Cart item key
     * @return string Modified name HTML
     */
    public function cartItemName(string $name, array $cart_item, string $cart_item_key): string
    {
        $product_id = $cart_item['product_id'];
        $variation_id = $cart_item['variation_id'] ?? 0;
        $check_id = $variation_id > 0 ? $variation_id : $product_id;

        if (!arraysubs_is_subscription_product($check_id)) {
            return $name;
        }

        $data = arraysubs_get_cart_item_subscription_data($cart_item);

        if (!$data) {
            return $name;
        }

        $billing_schedule = arraysubs_get_billing_schedule_string($data['interval'], $data['period']);

        // Add billing schedule to product name
        $subscription_badge = '<span class="subscription-badge">' . esc_html($billing_schedule) . '</span>';
        $full_name = $name . ' ' . $subscription_badge;

        // For mini-cart that doesn't use the quantity hook, we need to append detailed info here
        // We detect mini-cart context broadly by checking if we are NOT on cart or checkout page
        // or if specific mini-cart actions are running
        // Note: is_cart() is distinct from is_active_widget
        $is_mini_cart = !is_cart() && !is_checkout();

        if ($is_mini_cart) {
            $subscription_info = '<span class="mini-cart-subscription-info">';

            if ($data['signup_fee'] > 0) {
                $subscription_info .= '<br><small>+ ' . wp_kses_post(wc_price($data['signup_fee'])) . ' ' . esc_html__('signup', 'arraysubs') . '</small>';
            }

            if ($data['enable_different_renewal_price'] && !empty($data['different_renewal_price'])) {
                $subscription_info .= '<br><small>';
                if ($data['different_renewal_price_after'] > 1) {
                    /* translators: 1: renewal price, 2: number of payments */
                    $subscription_info .= sprintf(
                        /* translators: 1: renewal price, 2: number of payments */
                        esc_html__('then %1$s after %2$d', 'arraysubs'),
                        wp_kses_post(wc_price($data['different_renewal_price'])),
                        $data['different_renewal_price_after']
                    );
                } else {
                    /* translators: %s: renewal price amount after initial payment */
                    $subscription_info .= sprintf(
                        /* translators: %s: renewal price amount after initial payment */
                        esc_html__('then %s', 'arraysubs'),
                        wp_kses_post(wc_price($data['different_renewal_price']))
                    );
                }
                $subscription_info .= '</small>';
            }
            $subscription_info .= '</span>';

            return $full_name . $subscription_info;
        }

        return $full_name;
    }

    /**
     * Add detailed subscription metadata to cart and checkout line items.
     *
     * This powers classic cart/checkout templates, WooCommerce Blocks item data,
     * and third-party renderers that rely on WooCommerce cart item meta output.
     *
     * @param array $item_data Existing cart item data.
     * @param array $cart_item Cart item data.
     * @return array
     */
    public function cartItemData(array $item_data, array $cart_item): array
    {
        $product_id = (int) ($cart_item['product_id'] ?? 0);
        $variation_id = (int) ($cart_item['variation_id'] ?? 0);
        $check_id = $variation_id > 0 ? $variation_id : $product_id;

        if ($check_id <= 0 || !arraysubs_is_subscription_product($check_id)) {
            return $item_data;
        }

        $data = arraysubs_get_cart_item_subscription_data($cart_item);

        if (!is_array($data) || empty($data)) {
            return $item_data;
        }

        $quantity = max(1, absint($cart_item['quantity'] ?? 1));
        $subscription_details = $this->getSubscriptionCartItemData($data, $quantity, $cart_item);

        if (empty($subscription_details)) {
            return $item_data;
        }

        return array_merge($item_data, $subscription_details);
    }

    /**
     * Snapshot subscription display rows for order emails.
     *
     * The structured values are stored as one hidden meta record so native and
     * third-party order-item metadata remain untouched. Capturing at checkout
     * also keeps the purchased terms stable if the product changes later.
     *
     * @param \WC_Order_Item_Product $item          Order line item.
     * @param string                 $cart_item_key Cart item key.
     * @param array                  $values        Cart item data.
     * @param \WC_Order              $order         Order object.
     * @return void
     */
    public function addOrderItemEmailData($item, string $cart_item_key, array $values, $order): void
    {
        unset($cart_item_key, $order);

        if (!($item instanceof \WC_Order_Item_Product)) {
            return;
        }

        $product_id = (int) ($values['product_id'] ?? 0);
        $variation_id = (int) ($values['variation_id'] ?? 0);
        $check_id = $variation_id > 0 ? $variation_id : $product_id;

        if ($check_id <= 0 || !arraysubs_is_subscription_product($check_id)) {
            return;
        }

        $data = arraysubs_get_cart_item_subscription_data($values);

        if (!is_array($data) || empty($data)) {
            return;
        }

        $quantity = max(1, absint($values['quantity'] ?? 1));
        $details = $this->getSubscriptionCartItemData($data, $quantity, $values, 'email_snapshot');

        if (empty($details)) {
            return;
        }

        $item->add_meta_data(self::ORDER_ITEM_EMAIL_DATA_META, $details, true);
    }

    /**
     * Build structured subscription details for cart item metadata.
     *
     * @param array  $data      Subscription product data.
     * @param int    $quantity  Cart item quantity.
     * @param array  $cart_item Cart item data.
     * @param string $context   Display context: cart or email_snapshot.
     * @return array
     */
    private function getSubscriptionCartItemData(array $data, int $quantity, array $cart_item, string $context = 'cart'): array
    {
        $details = [];
        $is_lifetime = ($data['period'] ?? '') === 'lifetime';
        $has_trial = !$is_lifetime && !empty($data['trial_length']) && (int) $data['trial_length'] > 0;

        if ($is_lifetime) {
            $details[] = [
                'key'       => __('Subscription', 'arraysubs'),
                'value'     => __('Lifetime Deal — No recurring charges', 'arraysubs'),
                'className' => 'arraysubs-subscription-meta arraysubs-subscription-meta--lifetime',
            ];
        } else {
            $details[] = [
                'key'       => $has_trial ? __('After trial', 'arraysubs') : __('Renewals', 'arraysubs'),
                'value'     => $this->getSubscriptionBillingSummary($data, $quantity),
                'className' => 'arraysubs-subscription-meta arraysubs-subscription-meta--renewals',
            ];
        }

        if (!empty($data['signup_fee']) && (float) $data['signup_fee'] > 0) {
            $details[] = [
                'key'       => __('Signup fee', 'arraysubs'),
                'value'     => sprintf(
                    /* translators: 1: signup fee amount, 2: one-time label */
                    __('%1$s (%2$s)', 'arraysubs'),
                    wp_kses_post(wc_price((float) $data['signup_fee'])),
                    esc_html__('one-time', 'arraysubs')
                ),
                'className' => 'arraysubs-subscription-meta arraysubs-subscription-meta--signup-fee',
            ];
        }

        if ($has_trial) {
            $trial_schedule = arraysubs_get_duration_string((int) $data['trial_length'], (string) $data['trial_period']);

            $details[] = [
                'key'       => __('Free trial', 'arraysubs'),
                'value'     => esc_html($trial_schedule),
                'className' => 'arraysubs-subscription-meta arraysubs-subscription-meta--trial',
            ];
        }

        $details[] = [
            'key'       => __('Today\'s charge', 'arraysubs'),
            'value'     => $this->getSubscriptionTodayChargeSummary($data, $quantity, $cart_item),
            'className' => 'arraysubs-subscription-meta arraysubs-subscription-meta--today-charge',
        ];

        if (!$is_lifetime) {
            $details[] = [
                'key'       => $has_trial ? __('First charge', 'arraysubs') : __('Next charge', 'arraysubs'),
                'value'     => $this->getSubscriptionNextChargeSummary($data, $quantity, $cart_item),
                'className' => 'arraysubs-subscription-meta arraysubs-subscription-meta--next-charge',
            ];

            $details[] = [
                'key'       => __('Duration', 'arraysubs'),
                'value'     => $this->getSubscriptionDurationSummary($data),
                'className' => 'arraysubs-subscription-meta arraysubs-subscription-meta--duration',
            ];
        }

        /**
         * Filter subscription cart item metadata rows.
         *
         * @param array  $details   Structured subscription metadata rows.
         * @param array  $cart_item Cart item data.
         * @param array  $data      Subscription product data.
         * @param int    $quantity  Cart item quantity.
         * @param string $context   Display context: cart or email_snapshot.
         */
        return apply_filters('arraysubs_subscription_cart_item_data', $details, $cart_item, $data, $quantity, $context);
    }

    /**
     * Build the main billing summary for a subscription line item.
     *
     * @param array $data     Subscription product data.
     * @param int   $quantity Cart item quantity.
     * @return string
     */
    private function getSubscriptionBillingSummary(array $data, int $quantity): string
    {
        if (($data['period'] ?? '') === 'lifetime') {
            return __('Lifetime Deal — No recurring charges', 'arraysubs');
        }

        $billing_schedule_phrase = arraysubs_get_billing_schedule_phrase((int) $data['interval'], (string) $data['period']);
        $recurring_amount = wp_kses_post(wc_price((float) $data['price'] * $quantity));

        if (!empty($data['enable_different_renewal_price']) && !empty($data['different_renewal_price'])) {
            $renewal_amount = wp_kses_post(wc_price((float) $data['different_renewal_price'] * $quantity));
            $different_after = max(1, absint($data['different_renewal_price_after'] ?? 1));

            if ($different_after > 1) {
                return sprintf(
                    /* translators: %1$s: initial amount, %2$s: billing schedule, %3$d: initial payment count, %4$s: later amount. */
                    __('%1$s %2$s for the first %3$d payments, then %4$s %2$s', 'arraysubs'),
                    $recurring_amount,
                    esc_html($billing_schedule_phrase),
                    $different_after,
                    $renewal_amount,
                );
            }

            return sprintf(
                /* translators: %1$s: first payment amount, %2$s: later amount, %3$s: billing schedule. */
                __('%1$s first payment, then %2$s %3$s', 'arraysubs'),
                $recurring_amount,
                $renewal_amount,
                esc_html($billing_schedule_phrase)
            );
        }

        return sprintf(
            /* translators: 1: recurring amount, 2: billing schedule */
            __('%1$s %2$s', 'arraysubs'),
            $recurring_amount,
            esc_html($billing_schedule_phrase)
        );
    }

    /**
     * Build the initial checkout charge summary for cart and checkout item meta.
     *
     * @param array $data     Subscription product data.
     * @param int   $quantity Cart item quantity.
     * @return string
     */
    private function getSubscriptionTodayChargeSummary(array $data, int $quantity, array $cart_item = []): string
    {
        $has_trial = !empty($data['trial_length']) && (int) $data['trial_length'] > 0;
        $signup_fee = (float) ($data['signup_fee'] ?? 0);
        $gateway_context = function_exists('arraysubs_get_current_renewal_sync_gateway_context')
            ? arraysubs_get_current_renewal_sync_gateway_context()
            : ['id' => '', 'gateway' => null];
        $sync_context = function_exists('arraysubs_get_renewal_sync_context')
            ? arraysubs_get_renewal_sync_context($data, $quantity, '', (string) $gateway_context['id'], $gateway_context['gateway'])
            : ['applies' => false];
        $first_payment_recurring = $has_trial ? 0 : (
            !empty($sync_context['applies'])
                ? (float) $sync_context['initial_line_amount']
                : ((float) $data['price'] * $quantity)
        );
        $first_payment_total = $first_payment_recurring + $signup_fee;

        if ($has_trial && $signup_fee > 0) {
            return sprintf(
                /* translators: %s: signup fee amount */
                __('%s (signup fee only)', 'arraysubs'),
                wp_kses_post(wc_price($signup_fee))
            );
        }

        if ($has_trial) {
            return __('Free today', 'arraysubs');
        }

        if ($signup_fee > 0) {
            if (!empty($sync_context['applies']) && 'prorate' === ($sync_context['mode'] ?? '')) {
                return sprintf(
                    /* translators: 1: prorated first charge, 2: signup fee amount, 3: total initial charge */
                    __('%1$s prorated + %2$s signup = %3$s', 'arraysubs'),
                    wp_kses_post(wc_price($first_payment_recurring)),
                    wp_kses_post(wc_price($signup_fee)),
                    wp_kses_post(wc_price($first_payment_total))
                );
            }

            return sprintf(
                /* translators: 1: first payment amount, 2: signup fee amount, 3: total initial charge */
                __('%1$s + %2$s signup = %3$s', 'arraysubs'),
                wp_kses_post(wc_price($first_payment_recurring)),
                wp_kses_post(wc_price($signup_fee)),
                wp_kses_post(wc_price($first_payment_total))
            );
        }

        if (!empty($sync_context['applies']) && 'prorate' === ($sync_context['mode'] ?? '')) {
            return sprintf(
                /* translators: %s: prorated first charge amount */
                __('%s prorated until the synced renewal date', 'arraysubs'),
                wp_kses_post(wc_price($first_payment_recurring))
            );
        }

        if (!empty($sync_context['applies']) && 'full' === ($sync_context['mode'] ?? '')) {
            return sprintf(
                /* translators: %s: full first charge amount */
                __('%s full first charge', 'arraysubs'),
                wp_kses_post(wc_price($first_payment_recurring))
            );
        }

        return wp_kses_post(wc_price($first_payment_recurring));
    }

    /**
     * Build the next charge summary for cart and checkout item meta.
     *
     * @param array $data     Subscription product data.
     * @param int   $quantity Cart item quantity.
     * @return string
     */
    private function getSubscriptionNextChargeSummary(array $data, int $quantity, array $cart_item = []): string
    {
        if (($data['period'] ?? '') === 'lifetime') {
            return __('Lifetime Deal — No recurring charges', 'arraysubs');
        }

        $has_trial = !empty($data['trial_length']) && (int) $data['trial_length'] > 0;
        $gateway_context = function_exists('arraysubs_get_current_renewal_sync_gateway_context')
            ? arraysubs_get_current_renewal_sync_gateway_context()
            : ['id' => '', 'gateway' => null];
        $sync_context = function_exists('arraysubs_get_renewal_sync_context')
            ? arraysubs_get_renewal_sync_context($data, $quantity, '', (string) $gateway_context['id'], $gateway_context['gateway'])
            : ['applies' => false];

        if (!empty($sync_context['applies']) && !empty($sync_context['next_payment_date'])) {
            $next_charge_date = (string) $sync_context['next_payment_date'];
        } elseif ($has_trial) {
            $next_charge_date = arraysubs_calculate_next_payment_date(
                current_time('mysql', true),
                (int) $data['trial_length'],
                (string) $data['trial_period']
            );
        } else {
            $next_charge_date = arraysubs_calculate_next_payment_date(
                current_time('mysql', true),
                (int) $data['interval'],
                (string) $data['period']
            );
        }

        $summary = esc_html(arraysubs_format_date_local($next_charge_date, \get_option('date_format')));

        if ($has_trial || !empty($sync_context['applies'])) {
            $summary .= ' <small>(' . wp_kses_post(wc_price((float) $data['price'] * $quantity)) . ')</small>';
        }

        return $summary;
    }

    /**
     * Build the subscription duration summary.
     *
     * @param array $data Subscription product data.
     * @return string
     */
    private function getSubscriptionDurationSummary(array $data): string
    {
        if (($data['period'] ?? '') === 'lifetime') {
            return __('Lifetime Deal — No recurring charges', 'arraysubs');
        }

        $length = absint($data['length'] ?? 0);

        if ($length > 0) {
            return sprintf(
                /* translators: %d: number of billing cycles */
                _n('%d billing cycle', '%d billing cycles', $length, 'arraysubs'),
                $length
            );
        }

        return __('Continues until cancelled', 'arraysubs');
    }

    /**
     * Add subscription signup fees to cart as fees
     *
     * @param \WC_Cart $cart Cart object
     */
    public function addSignupFeeToCart($cart): void
    {
        // Skip in admin unless it's an AJAX request
        if (is_admin() && !defined('DOING_AJAX')) {
            return;
        }

        // Allow REST API requests (needed for Block Checkout / Store API)
        // But still prevent renewal order creation from adding fees (handled by specific action check below)

        // Check if this is a renewal order context - skip signup fee for renewals
        if (did_action('arraysubs_creating_renewal_order')) {
            return;
        }

        // Check existing fees to prevent duplicates
        $fees = $cart->get_fees();
        foreach ($fees as $fee) {
            if ($fee->name === __('Subscription Signup Fee', 'arraysubs')) {
                // Fee already exists, don't add again
                return;
            }
        }

        $total_signup_fee = 0;

        foreach ($cart->get_cart() as $cart_item) {
            $product_id = $cart_item['product_id'];
            $variation_id = $cart_item['variation_id'] ?? 0;
            $check_id = $variation_id > 0 ? $variation_id : $product_id;

            if (!arraysubs_is_subscription_product($check_id)) {
                continue;
            }

            $data = arraysubs_get_product_subscription_data($check_id);

            if (!$data || empty($data['signup_fee'])) {
                continue;
            }

            // Signup fee is per subscription item in cart (not multiplied by quantity)
            $total_signup_fee += (float) $data['signup_fee'];
        }

        if ($total_signup_fee > 0) {
            $cart->add_fee(
                __('Subscription Signup Fee', 'arraysubs'),
                $total_signup_fee,
                true, // Taxable fee
                ''    // Inherit tax class
            );
        }
    }

    /**
     * Adjust cart prices for trial subscriptions.
     *
     * When a subscription product has a free trial, the line-item price must be
     * set to $0 so that WooCommerce does not charge the recurring amount at
     * checkout.  Signup fees are handled separately via addSignupFeeToCart().
     *
     * @param \WC_Cart $cart Cart object.
     */
    public function beforeCalculateTotals($cart): void
    {
        if (is_admin() && !defined('DOING_AJAX')) {
            return;
        }

        if (did_action('arraysubs_creating_renewal_order')) {
            return;
        }

        foreach ($cart->get_cart() as $cart_item) {
            $product_id   = $cart_item['product_id'];
            $variation_id = $cart_item['variation_id'] ?? 0;
            $check_id     = $variation_id > 0 ? $variation_id : $product_id;

            if (!arraysubs_is_subscription_product($check_id)) {
                continue;
            }

            $data = arraysubs_get_cart_item_subscription_data($cart_item);

            if (!$data) {
                continue;
            }

            $has_trial = !empty($data['trial_length']) && (int) $data['trial_length'] > 0;

            if (!isset($cart_item['data']) || !method_exists($cart_item['data'], 'set_price')) {
                continue;
            }

            if ($has_trial) {
                $cart_item['data']->set_price(0);
                continue;
            }

            $quantity = max(1, absint($cart_item['quantity'] ?? 1));
            $gateway_context = function_exists('arraysubs_get_current_renewal_sync_gateway_context')
                ? arraysubs_get_current_renewal_sync_gateway_context()
                : ['id' => '', 'gateway' => null];
            $sync_context = function_exists('arraysubs_get_renewal_sync_context')
                ? arraysubs_get_renewal_sync_context($data, $quantity, '', (string) $gateway_context['id'], $gateway_context['gateway'])
                : ['applies' => false];

            if (!empty($sync_context['applies'])) {
                $cart_item['data']->set_price((float) $sync_context['initial_unit_amount']);
                continue;
            }

            $cart_item['data']->set_price((float) $data['price']);
        }
    }

    /**
     * Add custom cart fragments for mini-cart and AJAX cart updates
     *
     * @param array $fragments Cart fragments
     * @return array Modified fragments
     */
    public function addCartFragments(array $fragments): array
    {
        // Add subscription info to cart fragments for 3rd party plugins
        $cart = WC()->cart;

        if (!$cart) {
            return $fragments;
        }

        $has_subscription = false;
        $total_signup_fee = 0;

        foreach ($cart->get_cart() as $cart_item) {
            $product_id = $cart_item['product_id'];
            $variation_id = $cart_item['variation_id'] ?? 0;
            $check_id = $variation_id > 0 ? $variation_id : $product_id;

            if (arraysubs_is_subscription_product($check_id)) {
                $has_subscription = true;
                $data = arraysubs_get_product_subscription_data($check_id);
                if ($data && !empty($data['signup_fee'])) {
                    $total_signup_fee += (float) $data['signup_fee'];
                }
            }
        }

        if ($has_subscription) {
            // Add custom fragment with subscription cart info
            $fragments['div.arraysubs-cart-info'] = '<div class="arraysubs-cart-info" data-has-subscription="1" data-signup-fee="' . esc_attr($total_signup_fee) . '"></div>';
        }

        return $fragments;
    }

    /**
     * Add CSS class to mini-cart items that are subscriptions
     *
     * @param string $class      Item class
     * @param array  $cart_item  Cart item data
     * @param string $cart_item_key Cart item key
     * @return string Modified class
     */
    public function miniCartItemClass(string $class, array $cart_item, string $cart_item_key): string
    {
        $product_id = $cart_item['product_id'];
        $variation_id = $cart_item['variation_id'] ?? 0;
        $check_id = $variation_id > 0 ? $variation_id : $product_id;

        if (arraysubs_is_subscription_product($check_id)) {
            $class .= ' arraysubs-mini-cart-item';
        }

        return $class;
    }

    /**
     * Modify widget/mini-cart item quantity display to show subscription info
     *
     * @param string $quantity     Quantity HTML
     * @param array  $cart_item    Cart item data
     * @param string $cart_item_key Cart item key
     * @return string Modified quantity HTML
     */
    public function widgetCartItemQuantity(string $quantity, array $cart_item, string $cart_item_key): string
    {
        $product_id = $cart_item['product_id'];
        $variation_id = $cart_item['variation_id'] ?? 0;
        $check_id = $variation_id > 0 ? $variation_id : $product_id;

        if (!arraysubs_is_subscription_product($check_id)) {
            return $quantity;
        }

        $data = arraysubs_get_cart_item_subscription_data($cart_item);

        if (!$data) {
            return $quantity;
        }

        $billing_schedule = arraysubs_get_billing_schedule_string($data['interval'], $data['period']);

        // Build subscription info for mini-cart
        $subscription_info = '<span class="mini-cart-subscription-info">';
        $subscription_info .= '<br><small>' . esc_html($billing_schedule) . '</small>';

        if ($data['signup_fee'] > 0) {
            $subscription_info .= '<br><small>+ ' . wp_kses_post(wc_price($data['signup_fee'])) . ' ' . esc_html__('signup', 'arraysubs') . '</small>';
        }

        if ($data['enable_different_renewal_price'] && !empty($data['different_renewal_price'])) {
            $subscription_info .= '<br><small>';
            if ($data['different_renewal_price_after'] > 1) {
                /* translators: 1: renewal price, 2: number of payments */
                $subscription_info .= sprintf(
                    /* translators: 1: renewal price, 2: number of payments */
                    esc_html__('then %1$s after %2$d', 'arraysubs'),
                    wp_kses_post(wc_price($data['different_renewal_price'])),
                    $data['different_renewal_price_after']
                );
            } else {
                /* translators: %s: renewal price amount after initial payment */
                $subscription_info .= sprintf(
                    /* translators: %s: renewal price amount after initial payment */
                    esc_html__('then %s', 'arraysubs'),
                    wp_kses_post(wc_price($data['different_renewal_price']))
                );
            }
            $subscription_info .= '</small>';
        }

        $subscription_info .= '</span>';

        return $quantity . $subscription_info;
    }

    /**
     * Modify checkout order review item quantity to show subscription info
     *
     * @param string $quantity     Quantity HTML
     * @param array  $cart_item    Cart item data
     * @param string $cart_item_key Cart item key
     * @return string Modified quantity HTML
     */
    public function checkoutCartItemQuantity(string $quantity, array $cart_item, string $cart_item_key): string
    {
        $product_id = $cart_item['product_id'];
        $variation_id = $cart_item['variation_id'] ?? 0;
        $check_id = $variation_id > 0 ? $variation_id : $product_id;

        if (!arraysubs_is_subscription_product($check_id)) {
            return $quantity;
        }

        $data = arraysubs_get_cart_item_subscription_data($cart_item);

        if (!$data) {
            return $quantity;
        }

        $item_quantity = $cart_item['quantity'];
        $is_lifetime = ($data['period'] ?? '') === 'lifetime';

        // Build detailed checkout subscription info
        $output = '<span class="checkout-subscription-item">';
        $output .= '<strong class="product-quantity">&times;&nbsp;' . esc_html($item_quantity) . '</strong>';

        if (!$is_lifetime) {
            $billing_schedule = arraysubs_get_billing_schedule_string($data['interval'], $data['period']);
            $output .= '<br><small class="subscription-schedule">' . esc_html($billing_schedule) . '</small>';
        }

        // Different renewal price
        if (!$is_lifetime && $data['enable_different_renewal_price'] && !empty($data['different_renewal_price'])) {
            $output .= '<br><small class="renewal-price">';
            if ($data['different_renewal_price_after'] > 1) {
                /* translators: 1: payment cycle count, 2: renewal price amount */
                $output .= sprintf(
                    /* translators: 1: payment cycle count, 2: renewal price amount */
                    esc_html__('First %1$d, then %2$s', 'arraysubs'),
                    $data['different_renewal_price_after'],
                    wp_kses_post(wc_price($data['different_renewal_price'] * $item_quantity))
                );
            } else {
                /* translators: %s: renewal price amount */
                $output .= sprintf(
                    /* translators: %s: renewal price amount */
                    esc_html__('Then %s', 'arraysubs'),
                    wp_kses_post(wc_price($data['different_renewal_price'] * $item_quantity))
                );
            }
            $output .= '</small>';
        }

        // Trial period
        if (!$is_lifetime && $data['trial_length'] > 0) {
            $trial_schedule = arraysubs_get_duration_string($data['trial_length'], $data['trial_period']);
            /* translators: %s: trial period duration (e.g., "7 days", "1 month") */
            $output .= '<br><small class="trial-period">' . sprintf(/* translators: %s: trial period duration (e.g., "7 days", "1 month") */esc_html__('%s free trial', 'arraysubs'), esc_html($trial_schedule)) . '</small>';
        }

        // Signup fee
        if ($data['signup_fee'] > 0) {
            $output .= '<br><small class="signup-fee">+ ' . wp_kses_post(wc_price($data['signup_fee'])) . ' ' . esc_html__('signup fee', 'arraysubs') . '</small>';
        }

        $output .= '</span>';

        return $output;
    }

    /**
     * Add subscription info to order item name
     *
     * @param string         $item_name Order item name
     * @param \WC_Order_Item $item Order item object
     * @return string Modified item name
     */
    public function orderItemName(string $item_name, $item): string
    {
        if (!is_a($item, 'WC_Order_Item_Product')) {
            return $item_name;
        }

        $product_id = $item->get_product_id();
        $variation_id = $item->get_variation_id();
        $check_id = $variation_id > 0 ? $variation_id : $product_id;

        $is_subscription = get_post_meta($check_id, '_is_subscription', true);

        if ($is_subscription !== 'yes') {
            return $item_name;
        }

        $data = arraysubs_get_order_item_subscription_data($item);

        if (is_array($data)) {
            $period = (string) ($data['period'] ?? 'month');
            $interval = (int) ($data['interval'] ?? 1);
        } else {
            $period = get_post_meta($check_id, '_subscription_period', true);
            $interval = (int) get_post_meta($check_id, '_subscription_interval', true);
        }

        $billing_schedule = arraysubs_get_billing_schedule_string($interval, $period);

        return $item_name . ' <small>(' . esc_html($billing_schedule) . ')</small>';
    }

    /**
     * Change add to cart button text for subscriptions
     *
     * @param string      $text    Button text
     * @param \WC_Product $product Product object
     * @return string Modified button text
     */
    public function addToCartButtonText(string $text, $product): string
    {
        if (!$product) {
            return $text;
        }

        $product_id = $product->get_id();

        if (function_exists('arraysubs_is_subscription_checkout_product') && arraysubs_is_subscription_checkout_product($product_id)) {
            return arraysubs_get_add_to_cart_text();
        }

        if (function_exists('arraysubs_is_one_click_checkout_for_all_items') && arraysubs_is_one_click_checkout_for_all_items()) {
            $button_text = arraysubs_get_one_click_non_subscription_button_text();

            if (!empty($button_text)) {
                return $button_text;
            }
        }

        return $text;
    }

    /**
     * Keep an invalid subscription product save from publishing the product.
     *
     * @param array $data    Slashed post data prepared for insert/update.
     * @param array $postarr Raw post array.
     * @return array
     */
    public function preserveProductStatusForInvalidSubscriptionSave(array $data, array $postarr): array
    {
        if (($data['post_type'] ?? '') !== 'product') {
            return $data;
        }

        $post_id = absint($postarr['ID'] ?? ($_POST['post_ID'] ?? 0));

        if (!$this->isSubscriptionProductSaveRequest($post_id)) {
            return $data;
        }

        $errors = $this->getPostedSubscriptionProductValidationErrors(null, $post_id);
        if (empty($errors)) {
            return $data;
        }

        if ($post_id > 0) {
            $existing_status = get_post_status($post_id);
            if (!empty($existing_status)) {
                $data['post_status'] = $existing_status;
            }
        } else {
            $data['post_status'] = 'draft';
        }

        return $data;
    }

    /**
     * Validate product before WooCommerce persists product props.
     *
     * @param \WC_Product $product Product object.
     * @return \WC_Product Product object.
     */
    public function validateProduct($product): \WC_Product
    {
        $product_id = $product->get_id();
        $errors = $this->getPostedSubscriptionProductValidationErrors($product, $product_id);

        if (empty($errors)) {
            return $product;
        }

        $this->blocked_product_saves[$product_id] = true;
        $this->addProductValidationErrors($product_id, $errors);
        $this->restoreProductPricingFromSavedMeta($product);

        return $product;
    }

    /**
     * Detect whether the current request is saving a subscription product.
     *
     * @param int $post_id Product post ID.
     * @return bool
     */
    private function isSubscriptionProductSaveRequest(int $post_id): bool
    {
        if (empty($_POST['woocommerce_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['woocommerce_meta_nonce'])), 'woocommerce_save_data')) {
            return false;
        }

        $product_type = sanitize_text_field(wp_unslash($_POST['product-type'] ?? ($_POST['product_type'] ?? '')));
        if ($product_type === 'variable') {
            return false;
        }

        return isset($_POST['_is_subscription']);
    }

    /**
     * Get validation errors for posted subscription product values.
     *
     * @param \WC_Product|null $product Product object with posted props applied.
     * @param int              $post_id Product post ID.
     * @return string[]
     */
    private function getPostedSubscriptionProductValidationErrors($product = null, int $post_id = 0): array
    {
        if (!$this->isSubscriptionProductSaveRequest($post_id)) {
            return [];
        }

        $errors = [];
        $regular_price = $product instanceof \WC_Product
            ? $product->get_regular_price()
            : sanitize_text_field(wp_unslash($_POST['_regular_price'] ?? ''));

        if ($regular_price === '' || (float) $regular_price <= 0) {
            $errors[] = __('Subscription products must have a valid regular price greater than zero.', 'arraysubs');
        }

        $subscription_period = sanitize_text_field(wp_unslash($_POST['_subscription_period'] ?? get_post_meta($post_id, '_subscription_period', true)));
        if ($subscription_period !== 'lifetime') {
            $billing_interval = sanitize_text_field(wp_unslash($_POST['_subscription_interval'] ?? get_post_meta($post_id, '_subscription_interval', true)));
            if ($billing_interval === '' || (int) $billing_interval < 1 || (int) $billing_interval > 12) {
                $errors[] = __('Billing interval must be between 1 and 12.', 'arraysubs');
            }
        }

        $enable_renewal_price = isset($_POST['_enable_renewal_price'])
            ? sanitize_text_field(wp_unslash($_POST['_enable_renewal_price']))
            : '';

        if ($enable_renewal_price === 'yes' || $enable_renewal_price === 'on') {
            $renewal_price = sanitize_text_field(wp_unslash($_POST['_renewal_price'] ?? ''));
            if ($renewal_price === '' || (float) $renewal_price <= 0) {
                $errors[] = __('If different renewal price is enabled, you must set a valid renewal price.', 'arraysubs');
            }

            $renewal_price_after = sanitize_text_field(wp_unslash($_POST['_renewal_price_after'] ?? ''));
            if ($renewal_price_after === '' || (int) $renewal_price_after < 1) {
                $errors[] = __('Renewal price after period must be at least 1.', 'arraysubs');
            }
        }

        return array_values(array_unique($errors));
    }

    /**
     * Add WooCommerce admin errors once per product save.
     *
     * @param int      $product_id Product ID.
     * @param string[] $errors     Error messages.
     * @return void
     */
    private function addProductValidationErrors(int $product_id, array $errors): void
    {
        if (isset($this->reported_product_validation_errors[$product_id])) {
            return;
        }

        $this->reported_product_validation_errors[$product_id] = true;

        if (!class_exists('\WC_Admin_Meta_Boxes')) {
            return;
        }

        foreach ($errors as $error) {
            \WC_Admin_Meta_Boxes::add_error(esc_html($error));
        }
    }

    /**
     * Restore price props so WooCommerce does not persist invalid posted prices.
     *
     * @param \WC_Product $product Product object.
     * @return void
     */
    private function restoreProductPricingFromSavedMeta(\WC_Product $product): void
    {
        $product_id = $product->get_id();

        $product->set_regular_price(get_post_meta($product_id, '_regular_price', true));
        $product->set_sale_price(get_post_meta($product_id, '_sale_price', true));
        $product->set_price(get_post_meta($product_id, '_price', true));
    }

    /**
     * Display fees in widget/mini-cart
     */
    public function displayWidgetCartFees(): void
    {
        // Prevent duplicate display if we hook into multiple places
        static $displayed = false;
        if ($displayed) {
            return;
        }

        $cart = WC()->cart;

        if (!$cart) {
            return;
        }

        $fees = $cart->get_fees();
        $has_fees = false;

        if (!empty($fees)) {
            foreach ($fees as $fee) {
                // Check if it's our signup fee
                if ($fee->name === __('Subscription Signup Fee', 'arraysubs')) {
                    $has_fees = true;
                    echo '<p class="woocommerce-mini-cart__total total array-subscription-signup-fee">';
                    echo '<strong>' . esc_html($fee->name) . ':</strong> ';
                    echo wp_kses_post(wc_price($fee->amount));
                    echo '</p>';
                    $displayed = true;
                }
            }
        }

        // If we showed a fee, let's also show the "Total Due Today" to be helpful
        if ($has_fees) {
            echo '<p class="woocommerce-mini-cart__total total array-subscription-due-today">';
            echo '<strong>' . esc_html__('Due Today', 'arraysubs') . ':</strong> ';
            echo wp_kses_post($cart->get_total());
            echo '</p>';
        }
    }
}
