<?php

/**
 * CouponTracking Hooks
 *
 * Manages subscription coupon lifecycle: capture at checkout, apply to renewals,
 * decrement after payment, and display on admin coupon edit screen.
 *
 * Supports manual payment gateways and Stripe automatic payments only.
 *
 * @package ArraySubs\Features\CouponTracking\Services
 * @since 1.7.0
 */

namespace ArraySubs\Features\CouponTracking\Services;

defined('ABSPATH') || exit;

use ArraySubs\Supports\Config;

/**
 * Coupon tracking hooks
 */
class Hooks
{
    /**
     * Enable auto-loading
     *
     * @var bool
     */
    public static $loadable = true;

    /**
     * Meta key: whether the coupon applies to subscriptions.
     *
     * @var string
     */
    const COUPON_META_APPLY = '_arraysubs_apply_to_subscriptions';

    /**
     * Meta key: discount duration (one-time or recurring).
     *
     * @var string
     */
    const COUPON_META_DURATION = '_arraysubs_discount_duration';

    /**
     * Meta key: number of discount cycles (0 = unlimited).
     *
     * @var string
     */
    const COUPON_META_CYCLES = '_arraysubs_discount_cycles';

    /**
     * Meta key: whether the initial checkout counts as a cycle.
     *
     * @var string
     */
    const COUPON_META_COUNT_INITIAL = '_arraysubs_count_initial_checkout';

    /**
     * Initialize hooks.
     */
    public function __construct()
    {
        // WooCommerce coupon edit page: add fields.
        add_action('woocommerce_coupon_options', [$this, 'addCouponFields'], 10, 2);
        add_action('woocommerce_coupon_options_save', [$this, 'saveCouponFields'], 10, 2);

        // Coupon CRUD audit logging.
        add_action('wp_after_insert_post', [$this, 'logCouponCreatedOrUpdated'], 20, 4);
        add_action('woocommerce_coupon_object_updated_props', [$this, 'logCouponUpdatedProps'], 20, 2);
        add_action('before_delete_post', [$this, 'logCouponDeleting'], 10, 2);
        add_action('wp_trash_post', [$this, 'logCouponTrashing'], 10, 1);

        // Enqueue coupon edit page script.
        add_action('admin_enqueue_scripts', [$this, 'enqueueCouponEditAssets']);

        // Capture coupon from order at subscription creation.
        add_action('arraysubs_data_created_from_order', [$this, 'captureOrderCoupons'], 20, 3);

        // Count the initial checkout cycle only after the parent order is successfully paid.
        add_action('arraysubs_order_paid', [$this, 'maybeCountInitialCheckoutCycle'], 20, 2);

        // Apply coupon discount to renewal invoice.
        add_action('arraysubs_renewal_invoice_created', [$this, 'applyRecurringCoupons'], 5, 2);

        // Decrement cycle counter after successful renewal payment.
        add_action('arraysubs_renewal_payment_complete', [$this, 'onRenewalPaymentComplete'], 10, 2);

        // Append coupon info to subscription detail REST response.
        add_filter('arraysubs_subscription_detail_data', [$this, 'appendCouponToDetailData'], 10, 2);
    }

    /**
     * Log coupon create/update events.
     *
     * @param int           $post_id Coupon post ID.
     * @param \WP_Post      $post Coupon post object.
     * @param bool          $update Whether this is an update.
     * @param \WP_Post|null $post_before Post object before update.
     * @return void
     */
    public function logCouponCreatedOrUpdated(int $post_id, \WP_Post $post, bool $update, $post_before): void
    {
        if ('shop_coupon' !== $post->post_type) {
            return;
        }

        if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
            return;
        }

        if (!$update && in_array($post->post_status, ['auto-draft', 'inherit'], true)) {
            return;
        }

        if ($update) {
            return;
        }

        if (!function_exists('arraysubs_add_global_note')) {
            return;
        }

        $coupon_code = $this->getCouponAuditLabel($post_id, $post);
        $actor_id    = get_current_user_id();

        arraysubs_add_global_note(
            sprintf(
                /* translators: 1: coupon ID, 2: coupon code. */
                __('Coupon #%1$d created: %2$s.', 'arraysubs'),
                $post_id,
                esc_html($coupon_code)
            ),
            'private',
            $actor_id > 0 ? $actor_id : 0,
            [],
            'coupon'
        );
    }

    /**
     * Log WooCommerce coupon edits with changed field labels.
     *
     * @param \WC_Coupon|mixed $coupon        Coupon object.
     * @param array            $updated_props Updated coupon props.
     * @return void
     */
    public function logCouponUpdatedProps($coupon, array $updated_props): void
    {
        if (!$coupon instanceof \WC_Coupon) {
            return;
        }

        $changed_fields = $this->getChangedCouponProps($updated_props);

        if (empty($changed_fields) || !function_exists('arraysubs_add_global_note')) {
            return;
        }

        $actor_id    = get_current_user_id();
        $coupon_code = $this->getCouponAuditNameFromObject($coupon);

        arraysubs_add_global_note(
            sprintf(
                /* translators: 1: coupon ID, 2: coupon code, 3: changed field labels. */
                __('Coupon #%1$d updated: %2$s — changed %3$s.', 'arraysubs'),
                $coupon->get_id(),
                esc_html($coupon_code),
                esc_html(implode(', ', $changed_fields))
            ),
            'private',
            $actor_id > 0 ? $actor_id : 0,
            [],
            'coupon'
        );
    }

    /**
     * Log permanent coupon deletion.
     *
     * @param int      $post_id Coupon post ID.
     * @param \WP_Post $post   Coupon post object.
     * @return void
     */
    public function logCouponDeleting(int $post_id, \WP_Post $post): void
    {
        if ('shop_coupon' !== $post->post_type || !function_exists('arraysubs_add_global_note')) {
            return;
        }

        $actor_id    = get_current_user_id();
        $coupon_code = $this->getCouponAuditLabel($post_id, $post);

        arraysubs_add_global_note(
            sprintf(
                /* translators: 1: coupon ID, 2: coupon code. */
                __('Coupon #%1$d deleted: %2$s.', 'arraysubs'),
                $post_id,
                esc_html($coupon_code)
            ),
            'private',
            $actor_id > 0 ? $actor_id : 0,
            [],
            'coupon'
        );
    }

    /**
     * Log coupon trash events.
     *
     * @param int $post_id Coupon post ID.
     * @return void
     */
    public function logCouponTrashing(int $post_id): void
    {
        $post = get_post($post_id);

        if (!$post instanceof \WP_Post || 'shop_coupon' !== $post->post_type || !function_exists('arraysubs_add_global_note')) {
            return;
        }

        $actor_id    = get_current_user_id();
        $coupon_code = $this->getCouponAuditLabel($post_id, $post);

        arraysubs_add_global_note(
            sprintf(
                /* translators: 1: coupon ID, 2: coupon code. */
                __('Coupon #%1$d moved to trash: %2$s.', 'arraysubs'),
                $post_id,
                esc_html($coupon_code)
            ),
            'private',
            $actor_id > 0 ? $actor_id : 0,
            [],
            'coupon'
        );
    }

    /**
     * Add subscription coupon fields to the WooCommerce coupon edit page.
     *
     * @param int        $coupon_id Coupon post ID.
     * @param \WC_Coupon $coupon    Coupon object.
     * @return void
     */
    public function addCouponFields(int $coupon_id, \WC_Coupon $coupon): void
    {
        echo '<div class="options_group" id="arraysubs-coupon-settings">';

        echo '<p class="form-field"><strong>' . esc_html__('ArraySubs Subscription Settings', 'arraysubs') . '</strong></p>';

        woocommerce_wp_checkbox([
            'id'          => self::COUPON_META_APPLY,
            'label'       => esc_html__('Apply to subscriptions', 'arraysubs'),
            'description' => esc_html__('Enable this to apply the coupon discount to subscription renewals.', 'arraysubs'),
            'value'       => get_post_meta($coupon_id, self::COUPON_META_APPLY, true),
        ]);

        woocommerce_wp_select([
            'id'          => self::COUPON_META_DURATION,
            'label'       => esc_html__('Discount duration', 'arraysubs'),
            'description' => esc_html__('Choose whether the discount applies once or recurs on renewals.', 'arraysubs'),
            'options'     => [
                'one-time'  => esc_html__('One-time (initial order only)', 'arraysubs'),
                'recurring' => esc_html__('Recurring (applies to renewals)', 'arraysubs'),
            ],
            'value'       => get_post_meta($coupon_id, self::COUPON_META_DURATION, true) ?: 'one-time',
        ]);

        woocommerce_wp_text_input([
            'id'                => self::COUPON_META_CYCLES,
            'label'             => esc_html__('Number of renewal cycles', 'arraysubs'),
            'description'       => esc_html__('How many renewal billing cycles should receive this discount? Set 0 for unlimited renewals. Only applied for "Recurring" duration.', 'arraysubs'),
            'desc_tip'          => true,
            'type'              => 'number',
            'custom_attributes' => [
                'step' => '1',
                'min'  => '0',
            ],
            'value'             => get_post_meta($coupon_id, self::COUPON_META_CYCLES, true) ?: '0',
        ]);

        woocommerce_wp_checkbox([
            'id'          => self::COUPON_META_COUNT_INITIAL,
            'label'       => esc_html__('Count initial checkout', 'arraysubs'),
            'description' => esc_html__('If enabled, the initial checkout purchase counts as one of the discount cycles. Otherwise, only renewals are counted.', 'arraysubs'),
            'value'       => get_post_meta($coupon_id, self::COUPON_META_COUNT_INITIAL, true),
        ]);

        echo '<p class="form-field arraysubs-coupon-help-text">';
        echo '<em>' . esc_html__(
            'Note: Recurring renewal discounts are currently supported on manual payments and Stripe automatic payments only. Other automatic gateways will skip the coupon safely.',
            'arraysubs'
        ) . '</em>';
        echo '</p>';

        echo '</div>';
    }

    /**
     * Save subscription coupon fields.
     *
     * @param int        $coupon_id Coupon post ID.
     * @param \WC_Coupon $coupon    Coupon object.
     * @return void
     */
    public function saveCouponFields(int $coupon_id, \WC_Coupon $coupon): void
    {
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- WooCommerce handles nonce verification.
        $old_apply         = get_post_meta($coupon_id, self::COUPON_META_APPLY, true);
        $old_duration      = get_post_meta($coupon_id, self::COUPON_META_DURATION, true);
        $old_cycles        = get_post_meta($coupon_id, self::COUPON_META_CYCLES, true);
        $old_count_initial = get_post_meta($coupon_id, self::COUPON_META_COUNT_INITIAL, true);

        $apply = isset($_POST[self::COUPON_META_APPLY]) ? 'yes' : '';
        update_post_meta($coupon_id, self::COUPON_META_APPLY, $apply);

        $duration = isset($_POST[self::COUPON_META_DURATION])
            ? sanitize_text_field(wp_unslash($_POST[self::COUPON_META_DURATION]))
            : 'one-time';
        if (! in_array($duration, ['one-time', 'recurring'], true)) {
            $duration = 'one-time';
        }
        update_post_meta($coupon_id, self::COUPON_META_DURATION, $duration);

        $cycles = isset($_POST[self::COUPON_META_CYCLES])
            ? absint(wp_unslash($_POST[self::COUPON_META_CYCLES]))
            : 0;
        update_post_meta($coupon_id, self::COUPON_META_CYCLES, $cycles);

        $count_initial = isset($_POST[self::COUPON_META_COUNT_INITIAL]) ? 'yes' : '';
        update_post_meta($coupon_id, self::COUPON_META_COUNT_INITIAL, $count_initial);

        $changed = [];
        $audit_changes = [];

        if ((string) $old_apply !== (string) $apply) {
            $changed[] = __('apply to subscriptions', 'arraysubs');
            $audit_changes[] = arraysubs_create_audit_change_entry(
                __('Apply to subscriptions', 'arraysubs'),
                arraysubs_format_audit_value((string) $old_apply === 'yes'),
                arraysubs_format_audit_value((string) $apply === 'yes')
            );
        }

        if ((string) $old_duration !== (string) $duration) {
            $changed[] = __('discount duration', 'arraysubs');
            $audit_changes[] = arraysubs_create_audit_change_entry(
                __('Discount duration', 'arraysubs'),
                arraysubs_format_audit_value((string) $old_duration),
                arraysubs_format_audit_value((string) $duration)
            );
        }

        if ((string) $old_cycles !== (string) $cycles) {
            $changed[] = __('renewal cycles', 'arraysubs');
            $audit_changes[] = arraysubs_create_audit_change_entry(
                __('Renewal cycles', 'arraysubs'),
                arraysubs_format_audit_value((string) $old_cycles),
                arraysubs_format_audit_value((string) $cycles)
            );
        }

        if ((string) $old_count_initial !== (string) $count_initial) {
            $changed[] = __('count initial checkout', 'arraysubs');
            $audit_changes[] = arraysubs_create_audit_change_entry(
                __('Count initial checkout', 'arraysubs'),
                arraysubs_format_audit_value((string) $old_count_initial === 'yes'),
                arraysubs_format_audit_value((string) $count_initial === 'yes')
            );
        }

        if (!empty($changed) && function_exists('arraysubs_add_global_note')) {
            $actor_id = get_current_user_id();

            arraysubs_add_global_note(
                sprintf(
                    /* translators: 1: coupon ID, 2: coupon code, 3: comma-separated changed coupon fields. */
                    __('Coupon #%1$d settings updated for %2$s: %3$s.', 'arraysubs'),
                    $coupon_id,
                    esc_html($coupon->get_code()),
                    esc_html(implode(', ', $changed))
                ),
                'private',
                $actor_id > 0 ? $actor_id : 0,
                $audit_changes,
                'coupon'
            );
        }
        // phpcs:enable WordPress.Security.NonceVerification.Missing
    }

    /**
     * Enqueue the coupon edit page script for toggling fields.
     *
     * @param string $hook Current admin page hook.
     * @return void
     */
    public function enqueueCouponEditAssets(string $hook): void
    {
        $screen = get_current_screen();

        if (! $screen || 'shop_coupon' !== $screen->id) {
            return;
        }

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

        wp_enqueue_script(
            'arraysubs-coupon-edit-page',
            Config::get('plugin.public_url') . 'build/admin/coupon-edit-page.js',
            $asset['dependencies'],
            $asset['version'],
            true
        );
    }

    /**
     * Capture coupon data from the checkout order and store on the new subscription.
     *
     * Fires on `arraysubs_data_created_from_order`.
     *
     * @param int                    $subscription_id Subscription ID.
     * @param int                    $order_id        Order ID.
     * @param \WC_Order_Item_Product $item            Line item that created this subscription.
     * @return void
     */
    public function captureOrderCoupons(int $subscription_id, int $order_id, $item): void
    {
        $order = wc_get_order($order_id);

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

        $coupons = $order->get_coupon_codes();

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

        // Only capture the first applicable subscription coupon.
        foreach ($coupons as $code) {
            $coupon = new \WC_Coupon($code);

            if (! $coupon->get_id()) {
                continue;
            }

            $expiry_date = $coupon->get_date_expires();

            if ($expiry_date && time() > $expiry_date->getTimestamp()) {
                continue;
            }

            $apply_to_subs = get_post_meta($coupon->get_id(), self::COUPON_META_APPLY, true);

            if ('yes' !== $apply_to_subs) {
                continue;
            }

            $duration        = get_post_meta($coupon->get_id(), self::COUPON_META_DURATION, true) ?: 'one-time';
            $cycles          = absint(get_post_meta($coupon->get_id(), self::COUPON_META_CYCLES, true));
            $count_initial   = get_post_meta($coupon->get_id(), self::COUPON_META_COUNT_INITIAL, true) === 'yes';
            $capture_data    = $this->getCouponCaptureData($coupon, $order, $item);

            if (! is_array($capture_data)) {
                continue;
            }

            $wc_type         = $capture_data['wc_discount_type'];
            $discount_amount = $capture_data['source_amount'];
            $percent_value   = $capture_data['discount_percent'];
            $fixed_value     = $capture_data['discount_amount'];
            $is_percent      = $percent_value > 0;

            // Calculate remaining cycles.
            $remaining_cycles = $cycles;

            // Store coupon data on subscription.
            update_post_meta($subscription_id, '_applied_coupon_id', $coupon->get_id());
            update_post_meta($subscription_id, '_coupon_code', $coupon->get_code());
            update_post_meta($subscription_id, '_coupon_discount_type', $duration);
            update_post_meta($subscription_id, '_coupon_remaining_cycles', $remaining_cycles);
            update_post_meta($subscription_id, '_coupon_original_cycles', $cycles);
            update_post_meta($subscription_id, '_coupon_discount_amount', $fixed_value);
            update_post_meta($subscription_id, '_coupon_discount_percent', $percent_value);
            update_post_meta($subscription_id, '_coupon_wc_discount_type', $wc_type);
            update_post_meta($subscription_id, '_coupon_captured_at', current_time('mysql', true));
            update_post_meta($subscription_id, '_coupon_count_initial', $count_initial ? 'yes' : 'no');

            if ($cycles > 0 && $count_initial && 'recurring' === $duration) {
                update_post_meta($subscription_id, '_coupon_initial_cycle_pending', 'yes');
            } else {
                delete_post_meta($subscription_id, '_coupon_initial_cycle_pending');
            }

            // Add subscription note.
            $note_parts = [
                /* translators: %s: coupon code */
                sprintf(esc_html__('Coupon "%s" captured from checkout order.', 'arraysubs'), $coupon->get_code()),
            ];

            if ('recurring' === $duration) {
                if ($cycles > 0) {
                    /* translators: %d: number of cycles */
                    $note_parts[] = sprintf(esc_html__('Duration: recurring for %d cycle(s).', 'arraysubs'), $cycles);
                    if ($count_initial) {
                        $note_parts[] = esc_html__('Initial checkout will count as one cycle after the parent order is successfully paid.', 'arraysubs');
                    }
                } else {
                    $note_parts[] = esc_html__('Duration: recurring (unlimited).', 'arraysubs');
                }
            } else {
                $note_parts[] = esc_html__('Duration: one-time (initial order only).', 'arraysubs');
            }

            if ($is_percent) {
                /* translators: %s: percentage value */
                $note_parts[] = sprintf(esc_html__('Discount: %s%% off.', 'arraysubs'), $discount_amount);
            } else {
                /* translators: %s: formatted currency amount */
                $note_parts[] = sprintf(esc_html__('Discount: %s off per eligible renewal.', 'arraysubs'), wc_price($fixed_value));
            }

            arraysubs_add_subscription_note($subscription_id, implode(' ', $note_parts), 'system', 0, [], 'coupon');

            do_action(
                'arraysubs_coupon_applied_to_subscription',
                $subscription_id,
                $coupon,
                [
                    'code'             => $coupon->get_code(),
                    'duration'         => $duration,
                    'cycles'           => $cycles,
                    'remaining_cycles' => $remaining_cycles,
                    'discount_amount'  => $fixed_value,
                    'discount_percent' => $percent_value,
                    'wc_discount_type' => $wc_type,
                    'count_initial'    => $count_initial,
                ]
            );

            // Only capture the first applicable coupon per subscription.
            break;
        }
    }

    /**
     * Count the initial checkout cycle after the initial parent order is paid.
     *
     * @param int       $order_id Order ID.
     * @param \WC_Order $order    Order object.
     * @return void
     */
    public function maybeCountInitialCheckoutCycle(int $order_id, $order): void
    {
        if (! $order instanceof \WC_Order) {
            $order = wc_get_order($order_id);
        }

        if (! $order instanceof \WC_Order || 'yes' === $order->get_meta('_is_renewal_order', true)) {
            return;
        }

        if ('yes' === $order->get_meta('_arraysubs_coupon_initial_cycles_processed', true)) {
            return;
        }

        $subscription_ids = $this->getOrderSubscriptionIds($order);

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

        foreach ($subscription_ids as $subscription_id) {
            if ('yes' !== get_post_meta($subscription_id, '_coupon_initial_cycle_pending', true)) {
                continue;
            }

            $this->decrementCouponCycles($subscription_id, true);
            delete_post_meta($subscription_id, '_coupon_initial_cycle_pending');
        }

        $order->update_meta_data('_arraysubs_coupon_initial_cycles_processed', 'yes');
        $order->save();
    }

    /**
     * Apply recurring coupon discount to a renewal order.
     *
     * Fires on `arraysubs_renewal_invoice_created`.
     *
     * @param int $subscription_id Subscription ID.
     * @param int $order_id        Renewal order ID.
     * @return void
     */
    public function applyRecurringCoupons(int $subscription_id, int $order_id): void
    {
        $coupon_code   = get_post_meta($subscription_id, '_coupon_code', true);
        $discount_type = get_post_meta($subscription_id, '_coupon_discount_type', true);

        if (empty($coupon_code) || 'recurring' !== $discount_type) {
            return;
        }

        // Check remaining cycles (0 means already exhausted for limited coupons).
        $original_cycles  = absint(get_post_meta($subscription_id, '_coupon_original_cycles', true));
        $remaining_cycles = absint(get_post_meta($subscription_id, '_coupon_remaining_cycles', true));

        // If original > 0, this is a limited coupon; check remaining.
        if ($original_cycles > 0 && $remaining_cycles <= 0) {
            return;
        }

        $order = wc_get_order($order_id);

        if (! $order) {
            return;
        }

        // Prevent double application.
        foreach ($order->get_fees() as $fee_item) {
            if ($fee_item->get_meta('_subscription_coupon') === 'yes') {
                return;
            }
        }

        // Validate coupon if it still exists (optional safety check).
        $coupon_id = absint(get_post_meta($subscription_id, '_applied_coupon_id', true));

        if ($coupon_id > 0) {
            $coupon = new \WC_Coupon($coupon_id);

            $renewal_item = $this->getPrimaryRenewalLineItem($order);

            if ($renewal_item instanceof \WC_Order_Item_Product && ! $this->isCouponApplicableToOrderItem($coupon, $renewal_item)) {
                return;
            }

            /**
             * Filter whether the coupon should be applied to this renewal.
             *
             * @param bool       $should_apply    Whether to apply the coupon.
             * @param \WC_Coupon $coupon          The coupon object.
             * @param int        $subscription_id Subscription ID.
             */
            $should_apply = apply_filters('arraysubs_should_apply_coupon_to_renewal', true, $coupon, $subscription_id);

            if (! $should_apply) {
                return;
            }

            // Check if coupon has expired.
            $expiry_date = $coupon->get_date_expires();
            if ($expiry_date && time() > $expiry_date->getTimestamp()) {
                return;
            }
        }

        // Calculate discount from stored subscription data (not live coupon, in case coupon was modified/deleted).
        $discount_percent = (float) get_post_meta($subscription_id, '_coupon_discount_percent', true);
        $discount_amount  = (float) get_post_meta($subscription_id, '_coupon_discount_amount', true);
        $order_subtotal   = (float) $order->get_subtotal();
        $order_total      = (float) $order->get_total();

        if ($order_subtotal <= 0 || $order_total <= 0) {
            return;
        }

        $calculated_discount = 0.0;

        if ($discount_percent > 0) {
            $calculated_discount = ($order_subtotal * $discount_percent) / 100;
        } elseif ($discount_amount > 0) {
            $calculated_discount = $discount_amount;
        }

        // Cap discount so combined discounts (retention + coupon) don't exceed order total.
        $calculated_discount = min($calculated_discount, $order_total);

        if ($calculated_discount <= 0) {
            return;
        }

        // Apply as negative fee item (avoids WooCommerce coupon validation issues).
        $fee = new \WC_Order_Item_Fee();
        $fee->set_name(
            sprintf(
                /* translators: %s: coupon code */
                esc_html__('Recurring Discount: %s', 'arraysubs'),
                esc_html($coupon_code)
            )
        );
        $fee->set_total(-1 * $calculated_discount);
        $fee->set_tax_status('none');

        // Store meta for identification.
        $fee->update_meta_data('_coupon_code', $coupon_code);
        $fee->update_meta_data('_subscription_coupon', 'yes');
        $fee->update_meta_data('_subscription_id', $subscription_id);
        $fee->update_meta_data('_discount_type', get_post_meta($subscription_id, '_coupon_wc_discount_type', true));

        $order->add_item($fee);
        $order->calculate_totals();
        $order->save();
    }

    /**
     * Decrement coupon cycle counter after a successful renewal payment.
     *
     * Fires on `arraysubs_renewal_payment_complete`.
     *
     * @param int $subscription_id Subscription ID.
     * @param int $order_id        Renewal order ID.
     * @return void
     */
    public function onRenewalPaymentComplete(int $subscription_id, int $order_id): void
    {
        $order = wc_get_order($order_id);

        if (!$order || !$this->orderHasSubscriptionCouponDiscount($order, $subscription_id)) {
            return;
        }

        $this->decrementCouponCycles($subscription_id);
    }

    /**
     * Check whether a renewal order actually received a subscription coupon discount.
     *
     * @param \WC_Order $order           Order object.
     * @param int       $subscription_id Subscription ID.
     * @return bool
     */
    private function orderHasSubscriptionCouponDiscount(\WC_Order $order, int $subscription_id): bool
    {
        foreach ($order->get_items('fee') as $fee_item) {
            if (
                $fee_item->get_meta('_subscription_coupon') === 'yes'
                && absint($fee_item->get_meta('_subscription_id')) === $subscription_id
            ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Decrement the remaining coupon cycles for a subscription.
     *
     * @param int  $subscription_id       Subscription ID.
     * @param bool $from_initial_checkout Whether the decrement comes from the initial paid checkout.
     * @return void
     */
    private function decrementCouponCycles(int $subscription_id, bool $from_initial_checkout = false): void
    {
        $discount_type    = get_post_meta($subscription_id, '_coupon_discount_type', true);
        $original_cycles  = absint(get_post_meta($subscription_id, '_coupon_original_cycles', true));
        $remaining_cycles = absint(get_post_meta($subscription_id, '_coupon_remaining_cycles', true));
        $coupon_code      = get_post_meta($subscription_id, '_coupon_code', true);

        // Only decrement for recurring coupons with a limited cycle count.
        if ('recurring' !== $discount_type || $original_cycles <= 0 || $remaining_cycles <= 0) {
            return;
        }

        $new_remaining = $remaining_cycles - 1;
        update_post_meta($subscription_id, '_coupon_remaining_cycles', max(0, $new_remaining));

        if ($new_remaining <= 0) {
            arraysubs_add_subscription_note(
                $subscription_id,
                sprintf(
                    /* translators: %s: coupon code */
                    esc_html__('Coupon "%s" has been fully used. All discount cycles have been consumed. Future renewals will be charged at full price.', 'arraysubs'),
                    $coupon_code
                ),
                'system',
                0,
                [],
                'coupon'
            );

            do_action('arraysubs_coupon_expired_on_subscription', $subscription_id, $coupon_code);

            return;
        }

        if ($from_initial_checkout) {
            arraysubs_add_subscription_note(
                $subscription_id,
                sprintf(
                    /* translators: 1: coupon code, 2: remaining cycles after counting the initial checkout. */
                    esc_html__('Coupon "%1$s" counted against the initial checkout. %2$d discount cycle(s) remaining.', 'arraysubs'),
                    $coupon_code,
                    $new_remaining
                ),
                'system',
                0,
                [],
                'coupon'
            );

            return;
        }

        arraysubs_add_subscription_note(
            $subscription_id,
            sprintf(
                /* translators: 1: coupon code, 2: remaining cycles after the successful renewal payment. */
                esc_html__('Coupon "%1$s" used for this renewal. %2$d discount cycle(s) remaining.', 'arraysubs'),
                $coupon_code,
                $new_remaining
            ),
            'system',
            0,
            [],
            'coupon'
        );
    }

    /**
     * Append coupon data to the subscription detail REST response.
     *
     * @param array $data            Subscription detail data.
     * @param int   $subscription_id Subscription ID.
     * @return array
     */
    public function appendCouponToDetailData(array $data, int $subscription_id): array
    {
        $coupon_code = get_post_meta($subscription_id, '_coupon_code', true);

        if (empty($coupon_code)) {
            $data['coupon'] = null;
            return $data;
        }

        $original_cycles  = absint(get_post_meta($subscription_id, '_coupon_original_cycles', true));
        $remaining_cycles = absint(get_post_meta($subscription_id, '_coupon_remaining_cycles', true));
        $discount_type    = get_post_meta($subscription_id, '_coupon_discount_type', true);
        $discount_percent = (float) get_post_meta($subscription_id, '_coupon_discount_percent', true);
        $discount_amount  = (float) get_post_meta($subscription_id, '_coupon_discount_amount', true);
        $wc_discount_type = get_post_meta($subscription_id, '_coupon_wc_discount_type', true);
        $captured_at      = get_post_meta($subscription_id, '_coupon_captured_at', true);
        $count_initial    = get_post_meta($subscription_id, '_coupon_count_initial', true) === 'yes';
        $coupon_id        = absint(get_post_meta($subscription_id, '_applied_coupon_id', true));

        // Check if the coupon still exists in WooCommerce.
        $coupon_exists = $coupon_id > 0 && get_post($coupon_id) instanceof \WP_Post;

        $data['coupon'] = [
            'coupon_id'        => $coupon_id,
            'code'             => $coupon_code,
            'discount_type'    => $discount_type,
            'wc_discount_type' => $wc_discount_type,
            'discount_amount'  => $discount_amount,
            'discount_percent' => $discount_percent,
            'original_cycles'  => $original_cycles,
            'remaining_cycles' => $remaining_cycles,
            'count_initial'    => $count_initial,
            'captured_at'      => $captured_at,
            'captured_at_display' => ! empty($captured_at)
                ? arraysubs_format_date_local($captured_at)
                : '',
            'coupon_exists'    => $coupon_exists,
            'is_expired'       => 'recurring' === $discount_type && $original_cycles > 0 && $remaining_cycles <= 0,
            'is_unlimited'     => 'recurring' === $discount_type && 0 === $original_cycles,
        ];

        return $data;
    }

    /**
     * Get coupon information for a subscription.
     *
     * @param int $subscription_id Subscription ID.
     * @return array|null Coupon data or null if no coupon.
     */
    public static function getCouponInfo(int $subscription_id): ?array
    {
        $coupon_code = get_post_meta($subscription_id, '_coupon_code', true);

        if (empty($coupon_code)) {
            return null;
        }

        return [
            'coupon_id'        => absint(get_post_meta($subscription_id, '_applied_coupon_id', true)),
            'code'             => $coupon_code,
            'discount_type'    => get_post_meta($subscription_id, '_coupon_discount_type', true),
            'wc_discount_type' => get_post_meta($subscription_id, '_coupon_wc_discount_type', true),
            'discount_amount'  => (float) get_post_meta($subscription_id, '_coupon_discount_amount', true),
            'discount_percent' => (float) get_post_meta($subscription_id, '_coupon_discount_percent', true),
            'original_cycles'  => absint(get_post_meta($subscription_id, '_coupon_original_cycles', true)),
            'remaining_cycles' => absint(get_post_meta($subscription_id, '_coupon_remaining_cycles', true)),
        ];
    }

    /**
     * Check if a subscription has an active recurring coupon.
     *
     * @param int $subscription_id Subscription ID.
     * @return bool
     */
    public static function hasActiveRecurringCoupon(int $subscription_id): bool
    {
        $coupon_code   = get_post_meta($subscription_id, '_coupon_code', true);
        $discount_type = get_post_meta($subscription_id, '_coupon_discount_type', true);

        if (empty($coupon_code) || 'recurring' !== $discount_type) {
            return false;
        }

        $original_cycles  = absint(get_post_meta($subscription_id, '_coupon_original_cycles', true));
        $remaining_cycles = absint(get_post_meta($subscription_id, '_coupon_remaining_cycles', true));

        // Unlimited coupons always active.
        if (0 === $original_cycles) {
            return true;
        }

        return $remaining_cycles > 0;
    }

    /**
     * Build captured coupon data for a specific subscription order item.
     *
     * @param \WC_Coupon             $coupon Coupon object.
     * @param \WC_Order              $order  Order object.
     * @param \WC_Order_Item_Product $item   Order line item.
     * @return array<string, float|string>|null
     */
    private function getCouponCaptureData(\WC_Coupon $coupon, \WC_Order $order, \WC_Order_Item_Product $item): ?array
    {
        if (! $this->isCouponApplicableToOrderItem($coupon, $item)) {
            return null;
        }

        $source_amount = (float) $coupon->get_amount();
        $line_subtotal = (float) $item->get_subtotal();
        $quantity      = max(1, (int) $item->get_quantity());
        $wc_type       = $coupon->get_discount_type();

        if ($source_amount <= 0 || $line_subtotal <= 0) {
            return null;
        }

        $percent_value = 0.0;
        $fixed_value   = 0.0;

        if ('percent' === $wc_type) {
            $percent_value = $source_amount;
        } elseif ('fixed_product' === $wc_type) {
            $unit_subtotal = $line_subtotal / $quantity;
            $fixed_value   = min($source_amount, $unit_subtotal) * $quantity;
        } elseif ('fixed_cart' === $wc_type) {
            $eligible_subtotal = $this->getEligibleOrderSubtotalForCoupon($order, $coupon);

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

            $fixed_value = $source_amount * ($line_subtotal / $eligible_subtotal);
        } else {
            return null;
        }

        $fixed_value = (float) wc_format_decimal(min($fixed_value, $line_subtotal), wc_get_price_decimals());

        return [
            'wc_discount_type' => $wc_type,
            'source_amount'    => $source_amount,
            'discount_percent' => $percent_value,
            'discount_amount'  => $fixed_value,
        ];
    }

    /**
     * Check whether a coupon is applicable to a specific order item.
     *
     * @param \WC_Coupon             $coupon Coupon object.
     * @param \WC_Order_Item_Product $item   Order line item.
     * @return bool
     */
    private function isCouponApplicableToOrderItem(\WC_Coupon $coupon, \WC_Order_Item_Product $item): bool
    {
        $product = $item->get_product();

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

        $product_id     = $product->get_id();
        $parent_id      = $product->get_parent_id();
        $product_ids    = array_filter([$product_id, $parent_id]);
        $product_cat_id = $parent_id > 0 ? $parent_id : $product_id;
        $product_cats   = wc_get_product_cat_ids($product_cat_id);

        $included_products = array_map('absint', $coupon->get_product_ids());
        $included_cats     = array_map('absint', $coupon->get_product_categories());
        $excluded_products = array_map('absint', $coupon->get_excluded_product_ids());
        $excluded_cats     = array_map('absint', $coupon->get_excluded_product_categories());

        if (! empty($included_products) || ! empty($included_cats)) {
            $matches_product  = ! empty(array_intersect($product_ids, $included_products));
            $matches_category = ! empty(array_intersect($product_cats, $included_cats));

            if (! $matches_product && ! $matches_category) {
                return false;
            }
        }

        if (! empty(array_intersect($product_ids, $excluded_products))) {
            return false;
        }

        if (! empty(array_intersect($product_cats, $excluded_cats))) {
            return false;
        }

        if ($coupon->get_exclude_sale_items() && $product->is_on_sale()) {
            return false;
        }

        return true;
    }

    /**
     * Get the total eligible subtotal for a coupon across an order.
     *
     * @param \WC_Order  $order  Order object.
     * @param \WC_Coupon $coupon Coupon object.
     * @return float
     */
    private function getEligibleOrderSubtotalForCoupon(\WC_Order $order, \WC_Coupon $coupon): float
    {
        $eligible_subtotal = 0.0;

        foreach ($order->get_items() as $order_item) {
            if (! $order_item instanceof \WC_Order_Item_Product) {
                continue;
            }

            if (! $this->isCouponApplicableToOrderItem($coupon, $order_item)) {
                continue;
            }

            $eligible_subtotal += (float) $order_item->get_subtotal();
        }

        return $eligible_subtotal;
    }

    /**
     * Get subscription IDs linked to an order.
     *
     * @param \WC_Order $order Order object.
     * @return int[]
     */
    private function getOrderSubscriptionIds(\WC_Order $order): array
    {
        $subscription_ids = get_post_meta($order->get_id(), '_subscription_ids', true);

        if (is_array($subscription_ids) && ! empty($subscription_ids)) {
            return array_values(array_unique(array_map('absint', $subscription_ids)));
        }

        $subscription_ids = [];

        foreach ($order->get_items() as $item) {
            $subscription_id = absint($item->get_meta('_subscription_id'));

            if ($subscription_id > 0) {
                $subscription_ids[] = $subscription_id;
            }
        }

        return array_values(array_unique($subscription_ids));
    }

    /**
     * Get the primary product line item from a renewal order.
     *
     * @param \WC_Order $order Renewal order.
     * @return \WC_Order_Item_Product|null
     */
    private function getPrimaryRenewalLineItem(\WC_Order $order): ?\WC_Order_Item_Product
    {
        foreach ($order->get_items() as $item) {
            if ($item instanceof \WC_Order_Item_Product) {
                return $item;
            }
        }

        return null;
    }

    /**
     * Build a readable coupon label for audit notes.
     *
     * @param int      $coupon_id Coupon post ID.
     * @param \WP_Post $post      Coupon post object.
     * @return string
     */
    private function getCouponAuditLabel(int $coupon_id, \WP_Post $post): string
    {
        $coupon = new \WC_Coupon($coupon_id);

        if ($coupon->get_id() && $coupon->get_code() !== '') {
            return $coupon->get_code();
        }

        if (!empty($post->post_title)) {
            return $post->post_title;
        }

        return sprintf(
            /* translators: %d: coupon ID. */
            __('Coupon #%d', 'arraysubs'),
            $coupon_id
        );
    }

    /**
     * Get a readable coupon code from a WooCommerce coupon object.
     *
     * @param \WC_Coupon $coupon Coupon object.
     * @return string
     */
    private function getCouponAuditNameFromObject(\WC_Coupon $coupon): string
    {
        $code = $coupon->get_code();

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

        return sprintf(
            /* translators: %d: coupon ID. */
            __('Coupon #%d', 'arraysubs'),
            $coupon->get_id()
        );
    }

    /**
     * Get changed WooCommerce coupon props as readable labels.
     *
     * @param array $updated_props Updated coupon props.
     * @return string[]
     */
    private function getChangedCouponProps(array $updated_props): array
    {
        if (empty($updated_props)) {
            return [];
        }

        $labels = [
            'code' => __('coupon code', 'arraysubs'),
            'amount' => __('discount amount', 'arraysubs'),
            'date_expires' => __('expiry date', 'arraysubs'),
            'discount_type' => __('discount type', 'arraysubs'),
            'description' => __('description', 'arraysubs'),
            'individual_use' => __('individual use', 'arraysubs'),
            'product_ids' => __('included products', 'arraysubs'),
            'excluded_product_ids' => __('excluded products', 'arraysubs'),
            'usage_limit' => __('usage limit', 'arraysubs'),
            'usage_limit_per_user' => __('usage limit per user', 'arraysubs'),
            'limit_usage_to_x_items' => __('item usage limit', 'arraysubs'),
            'free_shipping' => __('free shipping', 'arraysubs'),
            'product_categories' => __('included categories', 'arraysubs'),
            'excluded_product_categories' => __('excluded categories', 'arraysubs'),
            'exclude_sale_items' => __('exclude sale items', 'arraysubs'),
            'minimum_amount' => __('minimum spend', 'arraysubs'),
            'maximum_amount' => __('maximum spend', 'arraysubs'),
            'email_restrictions' => __('email restrictions', 'arraysubs'),
        ];

        $changed = [];

        foreach ($updated_props as $key) {
            $changed[] = $labels[$key] ?? ucwords(str_replace('_', ' ', (string) $key));
        }

        return array_values(array_unique($changed));
    }
}
