<?php

/**
 * Subscription Admin REST Controller
 *
 * REST API endpoints for admin subscription actions.
 *
 * @package ArraySubs\Features\SubscriptionAdmin\REST
 * @since 1.0.0
 */

namespace ArraySubs\Features\SubscriptionAdmin\REST;

defined('ABSPATH') || exit;

use ArraySubs\Supports\BaseRestController;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

/**
 * Subscription admin REST controller
 */
class SubscriptionController extends BaseRestController
{
    /**
     * Enable auto-loading
     *
     * @var bool
     */
    public static $loadable = true;

    /**
     * Initialize controller
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Register REST routes
     *
     * @return void
     */
    public function registerRoutes(): void
    {
        // Get subscription products
        register_rest_route(
            $this->namespace,
            '/subscriptions/products',
            [
                'methods' => 'GET',
                'callback' => [$this, 'getSubscriptionProducts'],
                'permission_callback' => [$this, 'checkAdminPermission'],
            ]
        );

        // Get subscription detail
        register_rest_route(
            $this->namespace,
            '/subscriptions/(?P<id>\d+)/detail',
            [
                'methods' => 'GET',
                'callback' => [$this, 'getSubscriptionDetail'],
                'permission_callback' => [$this, 'checkAdminPermission'],
                'args' => [
                    'id' => [
                        'required' => true,
                        'type' => 'integer',
                        'sanitize_callback' => 'absint',
                    ],
                ],
            ]
        );

        // Update subscription
        register_rest_route(
            $this->namespace,
            '/subscriptions/(?P<id>\d+)/update',
            [
                'methods' => 'POST',
                'callback' => [$this, 'updateSubscription'],
                'permission_callback' => [$this, 'checkAdminPermission'],
                'args' => [
                    'id' => [
                        'required' => true,
                        'type' => 'integer',
                        'sanitize_callback' => 'absint',
                    ],
                ],
            ]
        );

        // Change subscription status
        register_rest_route(
            $this->namespace,
            '/subscriptions/(?P<id>\d+)/status',
            [
                'methods' => 'POST',
                'callback' => [$this, 'changeStatus'],
                'permission_callback' => [$this, 'checkAdminPermission'],
                'args' => [
                    'id' => [
                        'required' => true,
                        'type' => 'integer',
                        'sanitize_callback' => 'absint',
                    ],
                    'status' => [
                        'required' => true,
                        'type' => 'string',
                        'sanitize_callback' => 'sanitize_text_field',
                    ],
                ],
            ]
        );

        // Cancel subscription
        register_rest_route(
            $this->namespace,
            '/subscriptions/(?P<id>\d+)/cancel',
            [
                'methods' => 'POST',
                'callback' => [$this, 'cancelSubscription'],
                'permission_callback' => [$this, 'checkAdminPermission'],
                'args' => [
                    'id' => [
                        'required' => true,
                        'type' => 'integer',
                        'sanitize_callback' => 'absint',
                    ],
                    'cancel_type' => [
                        'required' => false,
                        'type' => 'string',
                        'default' => 'immediate',
                        'enum' => ['immediate', 'end_of_period'],
                        'sanitize_callback' => 'sanitize_text_field',
                    ],
                    'reason' => [
                        'required' => false,
                        'type' => 'string',
                        'sanitize_callback' => 'sanitize_textarea_field',
                    ],
                ],
            ]
        );

        // Undo scheduled cancellation
        register_rest_route(
            $this->namespace,
            '/subscriptions/(?P<id>\d+)/undo-cancellation',
            [
                'methods' => 'POST',
                'callback' => [$this, 'undoCancellation'],
                'permission_callback' => [$this, 'checkAdminPermission'],
                'args' => [
                    'id' => [
                        'required' => true,
                        'type' => 'integer',
                        'sanitize_callback' => 'absint',
                    ],
                ],
            ]
        );

        // Update shipping address
        register_rest_route(
            $this->namespace,
            '/subscriptions/(?P<id>\d+)/shipping',
            [
                'methods' => 'POST',
                'callback' => [$this, 'updateShippingAddress'],
                'permission_callback' => [$this, 'checkAdminPermission'],
                'args' => [
                    'id' => [
                        'required' => true,
                        'type' => 'integer',
                        'sanitize_callback' => 'absint',
                    ],
                    'shipping_address' => [
                        'required' => false,
                        'type' => 'object',
                    ],
                    'shipping_type' => [
                        'required' => false,
                        'type' => 'string',
                        'enum' => ['one-time', 'recurring'],
                    ],
                    'renewal_shipping_total' => [
                        'required' => false,
                        'type' => 'number',
                    ],
                ],
            ]
        );

        // Manually retry a failed renewal payment
        register_rest_route(
            $this->namespace,
            '/subscriptions/(?P<id>\d+)/retry-payment',
            [
                'methods' => 'POST',
                'callback' => [$this, 'retryPayment'],
                'permission_callback' => [$this, 'checkAdminPermission'],
                'args' => [
                    'id' => [
                        'required' => true,
                        'type' => 'integer',
                        'sanitize_callback' => 'absint',
                    ],
                ],
            ]
        );

        // Sync subscription state from gateway
        register_rest_route(
            $this->namespace,
            '/subscriptions/(?P<id>\d+)/gateway-sync',
            [
                'methods' => 'POST',
                'callback' => [$this, 'gatewaySync'],
                'permission_callback' => [$this, 'checkAdminPermission'],
                'args' => [
                    'id' => [
                        'required' => true,
                        'type' => 'integer',
                        'sanitize_callback' => 'absint',
                    ],
                ],
            ]
        );
    }

    /**
     * Check admin permission for subscription admin endpoints.
     *
     * @return bool
     */
    public function checkAdminPermission(): bool
    {
        return current_user_can('manage_woocommerce') || current_user_can('manage_options');
    }

    /**
     * Get all subscription products
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error Response
     */
    public function getSubscriptionProducts(WP_REST_Request $request)
    {
        $args = [
            'post_type' => 'product',
            'posts_per_page' => -1,
            'post_status' => 'publish',
        ];

        $query = new \WP_Query($args);
        $products = [];

        if ($query->have_posts()) {
            while ($query->have_posts()) {
                $query->the_post();
                $product_id = get_the_ID();
                $product = wc_get_product($product_id);

                if (!$product) {
                    continue;
                }

                $is_simple_subscription = false;
                $is_variable_with_subscriptions = false;

                // Check if simple/external product is a subscription
                if ($product->is_type('simple') || $product->is_type('external')) {
                    $is_simple_subscription = arraysubs_is_subscription_product($product);
                }

                // Check if variable product has subscription variations
                if ($product->is_type('variable')) {
                    $variation_ids = $product->get_children();
                    foreach ($variation_ids as $variation_id) {
                        if (arraysubs_is_subscription_product($variation_id)) {
                            $is_variable_with_subscriptions = true;
                            break;
                        }
                    }
                }

                // Skip if not a subscription product at all
                if (!$is_simple_subscription && !$is_variable_with_subscriptions) {
                    continue;
                }

                // Get subscription data for simple products
                $subscription_data = null;
                if ($is_simple_subscription) {
                    $subscription_data = arraysubs_get_product_subscription_data($product);
                }

                $product_data = [
                    'id' => $product_id,
                    'name' => $product->get_name(),
                    'type' => $product->get_type(),
                    'price' => $product->get_price(),
                    'subscription_data' => $subscription_data,
                ];

                // If variable product, get only subscription variations
                if ($product->is_type('variable') && $is_variable_with_subscriptions) {
                    $variations = [];
                    $variation_ids = $product->get_children();

                    foreach ($variation_ids as $variation_id) {
                        // Only include variations that are subscriptions
                        if (!arraysubs_is_subscription_product($variation_id)) {
                            continue;
                        }

                        $variation = wc_get_product($variation_id);
                        if ($variation) {
                            $variation_subscription_data = arraysubs_get_product_subscription_data($variation);
                            $variations[] = [
                                'id' => $variation_id,
                                'name' => $variation->get_name(),
                                'price' => $variation->get_price(),
                                'attributes' => $variation->get_variation_attributes(),
                                'subscription_data' => $variation_subscription_data,
                            ];
                        }
                    }
                    $product_data['variations'] = $variations;
                }

                $products[] = $product_data;
            }
            wp_reset_postdata();
        }

        return arraysubs_rest_response(
            $products,
            200,
            __('Subscription products retrieved successfully', 'arraysubs')
        );
    }

    /**
     * Get comprehensive subscription detail
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error Response
     */
    public function getSubscriptionDetail(WP_REST_Request $request)
    {
        $subscription_id = $request->get_param('id');
        $subscription = arraysubs_get_subscription($subscription_id);

        if (!$subscription) {
            return new WP_Error(
                'not_found',
                __('Subscription not found', 'arraysubs'),
                ['status' => 404]
            );
        }

        // Gather all subscription data
        $effective_recurring_amount = function_exists('arraysubs_get_subscription_effective_recurring_amount')
            ? arraysubs_get_subscription_effective_recurring_amount($subscription_id)
            : (float) get_post_meta($subscription_id, '_recurring_amount', true);
        $active_retention_discount = function_exists('arraysubs_get_subscription_active_retention_discount')
            ? arraysubs_get_subscription_active_retention_discount($subscription_id)
            : null;
        $base_recurring_amount = (float) ($active_retention_discount['base_amount'] ?? get_post_meta($subscription_id, '_recurring_amount', true));
        $created_date_gmt = $subscription->post_date_gmt;

        if (empty($created_date_gmt) || $created_date_gmt === '0000-00-00 00:00:00') {
            $created_date_gmt = get_gmt_from_date($subscription->post_date);
        }

        $data = [
            'id' => $subscription->ID,
            'title' => $subscription->post_title,
            'status' => $subscription->post_status,
            'created_date' => $subscription->post_date,
            'created_date_gmt' => $created_date_gmt,
            'created_date_display' => $created_date_gmt ? arraysubs_format_date_local($created_date_gmt) : '',

            // Customer info
            'customer_id' => (int) get_post_meta($subscription_id, '_customer_id', true),
            'customer_email' => get_post_meta($subscription_id, '_customer_email', true),
            'invoice_email' => get_post_meta($subscription_id, '_invoice_email', true),

            // Product info
            'product_id' => (int) get_post_meta($subscription_id, '_product_id', true),
            'variation_id' => (int) get_post_meta($subscription_id, '_variation_id', true),
            'quantity' => (int) get_post_meta($subscription_id, '_quantity', true),

            // Billing info
            'billing_period' => get_post_meta($subscription_id, '_billing_period', true),
            'billing_interval' => (int) get_post_meta($subscription_id, '_billing_interval', true),
            'subscription_length' => (int) get_post_meta($subscription_id, '_subscription_length', true),

            // Pricing
            'subscription_price' => (float) get_post_meta($subscription_id, '_subscription_price', true),
            'recurring_amount' => (float) get_post_meta($subscription_id, '_recurring_amount', true),
            'base_recurring_amount' => $base_recurring_amount,
            'effective_recurring_amount' => (float) $effective_recurring_amount,
            'active_retention_discount' => $active_retention_discount,
            'signup_fee' => (float) get_post_meta($subscription_id, '_signup_fee', true),
            'currency' => get_post_meta($subscription_id, '_currency', true),
            'enable_different_renewal_price' => get_post_meta($subscription_id, '_enable_different_renewal_price', true) === 'yes',
            'different_renewal_price' => (float) get_post_meta($subscription_id, '_different_renewal_price', true),
            'different_renewal_price_after' => (int) get_post_meta($subscription_id, '_different_renewal_price_after', true),
            'completed_payments' => (int) get_post_meta($subscription_id, '_completed_payments', true),

            // Total paid — calculated from actual paid orders, not from the completed_payments counter.
            // The counter increments for trial starts (where the order total is $0), so multiplying
            // recurring_amount × completed_payments gives a wrong result for trial subscriptions.
            'total_paid' => $this->calculateTotalPaid($subscription_id),

            // Dates
            'start_date' => get_post_meta($subscription_id, '_start_date', true),
            'start_date_display' => arraysubs_format_date_local((string) get_post_meta($subscription_id, '_start_date', true)),
            'next_payment_date' => get_post_meta($subscription_id, '_next_payment_date', true),
            'next_payment_date_display' => arraysubs_format_date_local((string) get_post_meta($subscription_id, '_next_payment_date', true)),
            'last_payment_date' => get_post_meta($subscription_id, '_last_payment_date', true),
            'last_payment_date_display' => arraysubs_format_date_local((string) get_post_meta($subscription_id, '_last_payment_date', true)),
            'end_date' => get_post_meta($subscription_id, '_end_date', true),
            'end_date_display' => arraysubs_format_date_local((string) get_post_meta($subscription_id, '_end_date', true)),

            // Trial
            'trial_length' => (int) get_post_meta($subscription_id, '_trial_length', true),
            'trial_period' => get_post_meta($subscription_id, '_trial_period', true),
            'trial_end_date' => get_post_meta($subscription_id, '_trial_end_date', true),
            'trial_end_date_display' => arraysubs_format_date_local((string) get_post_meta($subscription_id, '_trial_end_date', true)),

            // Payment
            'payment_method' => get_post_meta($subscription_id, '_payment_method', true),
            'payment_method_title' => get_post_meta($subscription_id, '_payment_method_title', true),

            // Addresses
            'billing_address' => json_decode(get_post_meta($subscription_id, '_billing_address', true) ?: '{}', true),
            'shipping_address' => json_decode(get_post_meta($subscription_id, '_shipping_address', true) ?: '{}', true),

            // Related orders
            'parent_order_id' => (int) get_post_meta($subscription_id, '_parent_order_id', true),
            'order_ids' => json_decode(get_post_meta($subscription_id, '_order_ids', true) ?: '[]', true),

            // Meta flags
            'meta' => [
                '_waiting_cancellation' => (bool) get_post_meta($subscription_id, '_waiting_cancellation', true),
                '_cancellation_scheduled_date' => get_post_meta($subscription_id, '_cancellation_scheduled_date', true),
                '_cancellation_reason' => get_post_meta($subscription_id, '_cancellation_reason', true),
                '_last_payment_failure' => get_post_meta($subscription_id, '_last_payment_failure', true) ?: null,
                '_payment_gateway' => get_post_meta($subscription_id, '_payment_gateway', true) ?: null,
                '_payment_retry_attempts' => (int) get_post_meta($subscription_id, '_payment_retry_attempts', true),
                '_payment_retry_next_attempt_at' => get_post_meta($subscription_id, '_payment_retry_next_attempt_at', true) ?: null,
                '_pending_renewal_order_id' => (int) get_post_meta($subscription_id, '_pending_renewal_order_id', true),
                '_pending_switch' => get_post_meta($subscription_id, '_pending_switch', true) ?: null,
            ],

            // Cancellation details (full history)
            'cancellation' => $this->getCancellationDetails($subscription_id),

            // Enriched deferred plan-switch data for admin/support visibility.
            'pending_switch' => function_exists('arraysubs_get_pending_switch_details')
                ? arraysubs_get_pending_switch_details($subscription_id)
                : null,

            // Computed action availability — explicit so the JSX does not
            // have to guess from raw meta keys. Pro can extend `gateway_info`
            // via the `arraysubs_subscription_detail_data` filter below to
            // describe whether the gateway supports automatic billing /
            // sync. `can_retry_payment` and `can_sync_gateway` are the
            // canonical signals the admin UI buttons should gate on.
            'actions' => $this->buildActionState($subscription_id),

            // Shipping details
            'shipping' => [
                'needs_shipping' => get_post_meta($subscription_id, '_arraysubs_needs_shipping', true) === 'yes',
                'shipping_type' => get_post_meta($subscription_id, '_arraysubs_shipping_type', true) ?: 'recurring',
                'shipping_method_id' => get_post_meta($subscription_id, '_arraysubs_shipping_method_id', true),
                'shipping_method_title' => get_post_meta($subscription_id, '_arraysubs_shipping_method_title', true),
                'shipping_total' => (float) get_post_meta($subscription_id, '_arraysubs_shipping_total', true),
                'renewal_shipping_total' => (float) get_post_meta($subscription_id, '_arraysubs_renewal_shipping_total', true),
                'initial_shipping_paid' => get_post_meta($subscription_id, '_arraysubs_initial_shipping_paid', true) === 'yes',
            ],
        ];

        // Get customer details
        if ($data['customer_id']) {
            $customer = get_userdata($data['customer_id']);
            if ($customer) {
                $data['customer'] = [
                    'id' => $customer->ID,
                    'display_name' => $customer->display_name,
                    'user_login' => $customer->user_login,
                    'email' => $customer->user_email,
                    'registered' => $customer->user_registered,
                    'registered_display' => arraysubs_format_date_local((string) $customer->user_registered),
                ];
            }
        }

        // Get product details
        if ($data['product_id']) {
            $product_id = $data['variation_id'] ?: $data['product_id'];
            $product = wc_get_product($product_id);
            if ($product) {
                $product_data = [
                    'id' => $product->get_id(),
                    'name' => $product->get_name(),
                    'sku' => $product->get_sku(),
                    'image' => wp_get_attachment_image_url($product->get_image_id(), 'thumbnail'),
                    'permalink' => $product->get_permalink(),
                    'type' => $product->get_type(),
                ];

                // If this is a variation, include parent product info and variation attributes
                if ($product->is_type('variation')) {
                    $parent_product = wc_get_product($product->get_parent_id());
                    if ($parent_product) {
                        $product_data['parent_id'] = $parent_product->get_id();
                        $product_data['parent_name'] = $parent_product->get_name();
                        // If variation image is missing, use parent image
                        if (!$product_data['image']) {
                            $product_data['image'] = wp_get_attachment_image_url($parent_product->get_image_id(), 'thumbnail');
                        }
                    }
                    // Get variation attributes
                    $attributes = $product->get_variation_attributes();
                    $product_data['variation_attributes'] = [];
                    foreach ($attributes as $attr_name => $attr_value) {
                        $taxonomy = str_replace('attribute_', '', $attr_name);
                        $term = get_term_by('slug', $attr_value, $taxonomy);
                        $product_data['variation_attributes'][] = [
                            'name' => wc_attribute_label($taxonomy),
                            'value' => $term ? $term->name : $attr_value,
                        ];
                    }
                }

                $data['product'] = $product_data;
            }
        }

        // Get all related orders
        $data['orders'] = [];
        $order_ids = array_filter(array_map('absint', $data['order_ids']));
        foreach ($order_ids as $order_id) {
            $order = wc_get_order($order_id);
            if ($order) {
                $order_created = $order->get_date_created();
                $order_timestamp = $order_created ? $order_created->getTimestamp() : 0;
                $order_data = [
                    'id' => $order->get_id(),
                    'date' => $order_timestamp > 0 ? gmdate('Y-m-d H:i:s', $order_timestamp) : '',
                    'date_timestamp' => $order_timestamp,
                    'date_display' => $order_timestamp > 0 ? arraysubs_format_timestamp_local($order_timestamp) : '',
                    'status' => $order->get_status(),
                    'total' => $order->get_total(),
                    'formatted_total' => wp_strip_all_tags(html_entity_decode($order->get_formatted_order_total())),
                    'is_renewal' => $order->get_meta('_is_renewal_order') === 'yes',
                ];

                // Add refund information
                $total_refunded = floatval($order->get_total_refunded());
                $order_data['total_refunded'] = $total_refunded;
                $order_data['formatted_refunded'] = $total_refunded > 0
                    ? wp_strip_all_tags(html_entity_decode(wc_price($total_refunded, ['currency' => $order->get_currency()])))
                    : '';
                $order_data['is_fully_refunded'] = ($order->get_total() - $total_refunded) < 0.01;
                $order_data['refunds'] = [];

                // Get individual refunds
                foreach ($order->get_refunds() as $refund) {
                    $refund_created = $refund->get_date_created();
                    $refund_timestamp = $refund_created ? $refund_created->getTimestamp() : 0;
                    $order_data['refunds'][] = [
                        'id' => $refund->get_id(),
                        'date' => $refund_timestamp > 0 ? gmdate('Y-m-d H:i:s', $refund_timestamp) : '',
                        'date_timestamp' => $refund_timestamp,
                        'date_display' => $refund_timestamp > 0 ? arraysubs_format_timestamp_local($refund_timestamp) : '',
                        'amount' => abs(floatval($refund->get_total())),
                        'formatted_amount' => wp_strip_all_tags(html_entity_decode(wc_price(abs($refund->get_total()), ['currency' => $order->get_currency()]))),
                        'reason' => $refund->get_reason(),
                    ];
                }

                $data['orders'][] = $order_data;
            }
        }

        // Add total refunded for the subscription
        $data['total_refunded'] = floatval(get_post_meta($subscription_id, '_total_refunded', true));
        $data['formatted_total_refunded'] = $data['total_refunded'] > 0
            ? wp_strip_all_tags(html_entity_decode(wc_price($data['total_refunded'], ['currency' => $data['currency']])))
            : '';

        // Sort orders by date descending (newest first)
        usort($data['orders'], function ($a, $b) {
            return $b['date_timestamp'] - $a['date_timestamp'];
        });

        /**
         * Filter subscription detail data before returning.
         *
         * Allows addons (e.g. pro) to append gateway, payment method,
         * or other extended information to the subscription detail response.
         *
         * @param array $data            The subscription detail data.
         * @param int   $subscription_id The subscription post ID.
         */
        $data = apply_filters('arraysubs_subscription_detail_data', $data, $subscription_id);

        return arraysubs_rest_response($data, 200);
    }

    /**
     * Calculate the total amount actually paid for a subscription.
     *
     * Sums the order totals of all paid (completed / processing) orders linked
     * to the subscription.  This is the authoritative value — DO NOT use
     * recurring_amount × completed_payments because the completed_payments
     * counter is incremented for trial starts where the order total is $0.
     *
     * @param int $subscription_id The subscription post ID.
     * @return float Total amount paid.
     */
    private function calculateTotalPaid(int $subscription_id): float
    {
        $order_ids_raw = get_post_meta($subscription_id, '_order_ids', true);
        $order_ids     = !empty($order_ids_raw) ? json_decode($order_ids_raw, true) : [];

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

        $total = 0.0;

        foreach ($order_ids as $order_id) {
            $order_id = absint($order_id);
            if ($order_id <= 0) {
                continue;
            }

            $order = wc_get_order($order_id);
            if ($order instanceof \WC_Order && $order->is_paid()) {
                $total += (float) $order->get_total();
            }
        }

        return $total;
    }

    /**
     * Update subscription
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error Response
     */
    public function updateSubscription(WP_REST_Request $request)
    {
        $subscription_id = $request->get_param('id');
        $subscription = arraysubs_get_subscription($subscription_id);

        if (!$subscription) {
            return new WP_Error(
                'not_found',
                __('Subscription not found', 'arraysubs'),
                ['status' => 404]
            );
        }

        // Get update data
        $updates = $request->get_json_params();

        if (!is_array($updates)) {
            $updates = [];
        }

        if (array_key_exists('next_payment_date', $updates)) {
            return new WP_Error(
                'next_payment_date_locked',
                __('Manual next payment date changes are no longer supported. ArraySubs calculates this automatically from the billing schedule.', 'arraysubs'),
                ['status' => 400]
            );
        }

        $before_snapshot = $this->getSubscriptionAuditSnapshot((int) $subscription_id);

        // Capture old values for change hooks
        $old_product_id     = (int) get_post_meta($subscription_id, '_product_id', true);
        $old_variation_id   = (int) get_post_meta($subscription_id, '_variation_id', true);
        $old_quantity       = (int) get_post_meta($subscription_id, '_quantity', true);
        $old_payment_method = (string) get_post_meta($subscription_id, '_payment_method', true);
        $old_recurring      = (float) get_post_meta($subscription_id, '_recurring_amount', true);

        // Update product/variation
        if (isset($updates['product_id'])) {
            update_post_meta($subscription_id, '_product_id', absint($updates['product_id']));
        }

        if (isset($updates['variation_id'])) {
            update_post_meta($subscription_id, '_variation_id', absint($updates['variation_id']));
        }

        if (isset($updates['quantity'])) {
            update_post_meta($subscription_id, '_quantity', absint($updates['quantity']));
        }

        // Update invoice email
        if (isset($updates['invoice_email'])) {
            update_post_meta($subscription_id, '_invoice_email', sanitize_email($updates['invoice_email']));
        }

        // Update billing address
        if (isset($updates['billing_address']) && is_array($updates['billing_address'])) {
            $billing = [];
            $billing['first_name'] = sanitize_text_field($updates['billing_address']['first_name'] ?? '');
            $billing['last_name'] = sanitize_text_field($updates['billing_address']['last_name'] ?? '');
            $billing['company'] = sanitize_text_field($updates['billing_address']['company'] ?? '');
            $billing['address_1'] = sanitize_text_field($updates['billing_address']['address_1'] ?? '');
            $billing['address_2'] = sanitize_text_field($updates['billing_address']['address_2'] ?? '');
            $billing['city'] = sanitize_text_field($updates['billing_address']['city'] ?? '');
            $billing['state'] = sanitize_text_field($updates['billing_address']['state'] ?? '');
            $billing['postcode'] = sanitize_text_field($updates['billing_address']['postcode'] ?? '');
            $billing['country'] = sanitize_text_field($updates['billing_address']['country'] ?? '');
            $billing['email'] = sanitize_email($updates['billing_address']['email'] ?? '');
            $billing['phone'] = sanitize_text_field($updates['billing_address']['phone'] ?? '');

            update_post_meta($subscription_id, '_billing_address', wp_json_encode($billing));
        }

        // Update shipping address
        if (isset($updates['shipping_address']) && is_array($updates['shipping_address'])) {
            $shipping = [];
            $shipping['first_name'] = sanitize_text_field($updates['shipping_address']['first_name'] ?? '');
            $shipping['last_name'] = sanitize_text_field($updates['shipping_address']['last_name'] ?? '');
            $shipping['company'] = sanitize_text_field($updates['shipping_address']['company'] ?? '');
            $shipping['address_1'] = sanitize_text_field($updates['shipping_address']['address_1'] ?? '');
            $shipping['address_2'] = sanitize_text_field($updates['shipping_address']['address_2'] ?? '');
            $shipping['city'] = sanitize_text_field($updates['shipping_address']['city'] ?? '');
            $shipping['state'] = sanitize_text_field($updates['shipping_address']['state'] ?? '');
            $shipping['postcode'] = sanitize_text_field($updates['shipping_address']['postcode'] ?? '');
            $shipping['country'] = sanitize_text_field($updates['shipping_address']['country'] ?? '');

            update_post_meta($subscription_id, '_shipping_address', wp_json_encode($shipping));
        }

        // Update billing schedule
        if (isset($updates['billing_period'])) {
            update_post_meta($subscription_id, '_billing_period', sanitize_text_field($updates['billing_period']));
        }

        if (isset($updates['billing_interval'])) {
            update_post_meta($subscription_id, '_billing_interval', absint($updates['billing_interval']));
        }

        // Update payment method
        if (isset($updates['payment_method'])) {
            update_post_meta($subscription_id, '_payment_method', sanitize_text_field($updates['payment_method']));
        }

        if (isset($updates['payment_method_title'])) {
            update_post_meta($subscription_id, '_payment_method_title', sanitize_text_field($updates['payment_method_title']));
        }

        // Update recurring amount
        if (isset($updates['recurring_amount'])) {
            update_post_meta($subscription_id, '_recurring_amount', floatval($updates['recurring_amount']));
        }

        // Update subscription length
        if (isset($updates['subscription_length'])) {
            update_post_meta($subscription_id, '_subscription_length', absint($updates['subscription_length']));
        }

        // Update trial settings
        if (isset($updates['trial_length'])) {
            update_post_meta($subscription_id, '_trial_length', absint($updates['trial_length']));
        }

        if (isset($updates['trial_period'])) {
            update_post_meta($subscription_id, '_trial_period', sanitize_text_field($updates['trial_period']));
        }

        // Update signup fee
        if (isset($updates['signup_fee'])) {
            update_post_meta($subscription_id, '_signup_fee', floatval($updates['signup_fee']));
        }

        // Update different renewal price settings
        if (isset($updates['enable_different_renewal_price'])) {
            update_post_meta($subscription_id, '_enable_different_renewal_price', $updates['enable_different_renewal_price'] ? 'yes' : 'no');
        }

        if (isset($updates['different_renewal_price'])) {
            update_post_meta($subscription_id, '_different_renewal_price', floatval($updates['different_renewal_price']));
        }

        if (isset($updates['different_renewal_price_after'])) {
            update_post_meta($subscription_id, '_different_renewal_price_after', absint($updates['different_renewal_price_after']));
        }

        // Fire granular change hooks for audit logging
        $new_product_id       = isset($updates['product_id']) ? absint($updates['product_id']) : $old_product_id;
        $new_variation_id     = isset($updates['variation_id']) ? absint($updates['variation_id']) : $old_variation_id;
        $old_effective_product = $old_variation_id ?: $old_product_id;
        $new_effective_product = $new_variation_id ?: $new_product_id;

        if ($new_effective_product > 0 && $new_effective_product !== $old_effective_product) {
            do_action('arraysubs_product_changed', $subscription_id, $old_effective_product, $new_effective_product);
        }

        if (isset($updates['quantity'])) {
            $new_quantity = absint($updates['quantity']);
            if ($new_quantity !== $old_quantity) {
                do_action('arraysubs_quantity_changed', $subscription_id, $new_quantity, $old_quantity);
            }
        }

        if (isset($updates['payment_method'])) {
            $new_payment_method = sanitize_text_field($updates['payment_method']);
            if ($new_payment_method !== $old_payment_method) {
                do_action('arraysubs_payment_method_changed', $subscription_id, $new_payment_method, $old_payment_method);
            }
        }

        if (isset($updates['recurring_amount'])) {
            $new_recurring = floatval($updates['recurring_amount']);
            if (abs($new_recurring - $old_recurring) > 0.001) {
                do_action('arraysubs_recurring_amount_changed', $subscription_id, $new_recurring, $old_recurring);
            }
        }

        $after_snapshot = $this->getSubscriptionAuditSnapshot((int) $subscription_id);
        $this->logSubscriptionDetailAuditChanges((int) $subscription_id, $before_snapshot, $after_snapshot);

        // Trigger action
        do_action('arraysubs_data_updated_via_api', $subscription_id, $updates);

        return arraysubs_rest_response(
            ['subscription_id' => $subscription_id],
            200,
            __('Subscription updated successfully', 'arraysubs')
        );
    }

    /**
     * Capture auditable subscription detail values.
     *
     * @param int $subscription_id Subscription ID.
     * @return array<string, mixed>
     */
    private function getSubscriptionAuditSnapshot(int $subscription_id): array
    {
        return [
            'invoice_email' => sanitize_email((string) get_post_meta($subscription_id, '_invoice_email', true)),
            'product_id' => (int) get_post_meta($subscription_id, '_product_id', true),
            'variation_id' => (int) get_post_meta($subscription_id, '_variation_id', true),
            'quantity' => (int) get_post_meta($subscription_id, '_quantity', true),
            'billing_period' => sanitize_text_field((string) get_post_meta($subscription_id, '_billing_period', true)),
            'billing_interval' => (int) get_post_meta($subscription_id, '_billing_interval', true),
            'subscription_length' => (int) get_post_meta($subscription_id, '_subscription_length', true),
            'payment_method' => sanitize_text_field((string) get_post_meta($subscription_id, '_payment_method', true)),
            'payment_method_title' => sanitize_text_field((string) get_post_meta($subscription_id, '_payment_method_title', true)),
            'recurring_amount' => (float) get_post_meta($subscription_id, '_recurring_amount', true),
            'trial_length' => (int) get_post_meta($subscription_id, '_trial_length', true),
            'trial_period' => sanitize_text_field((string) get_post_meta($subscription_id, '_trial_period', true)),
            'signup_fee' => (float) get_post_meta($subscription_id, '_signup_fee', true),
            'enable_different_renewal_price' => get_post_meta($subscription_id, '_enable_different_renewal_price', true) === 'yes',
            'different_renewal_price' => (float) get_post_meta($subscription_id, '_different_renewal_price', true),
            'different_renewal_price_after' => (int) get_post_meta($subscription_id, '_different_renewal_price_after', true),
            'billing_address' => $this->normalizeAuditAddress(
                json_decode((string) get_post_meta($subscription_id, '_billing_address', true), true)
            ),
            'shipping_address' => $this->normalizeAuditAddress(
                json_decode((string) get_post_meta($subscription_id, '_shipping_address', true), true)
            ),
            'currency' => sanitize_text_field((string) get_post_meta($subscription_id, '_currency', true)),
        ];
    }

    /**
     * Normalize auditable address values.
     *
     * @param mixed $address Raw address payload.
     * @return array<string, string>
     */
    private function normalizeAuditAddress($address): array
    {
        $normalized = [];
        $keys       = [
            'first_name',
            'last_name',
            'company',
            'address_1',
            'address_2',
            'city',
            'state',
            'postcode',
            'country',
            'email',
            'phone',
        ];

        if (!is_array($address)) {
            $address = [];
        }

        foreach ($keys as $key) {
            $value = $address[$key] ?? '';

            $normalized[$key] = $key === 'email'
                ? sanitize_email((string) $value)
                : sanitize_text_field((string) $value);
        }

        return $normalized;
    }

    /**
     * Log auditable subscription detail changes with old/new values.
     *
     * @param int   $subscription_id Subscription ID.
     * @param array $before          Snapshot before the update.
     * @param array $after           Snapshot after the update.
     * @return void
     */
    private function logSubscriptionDetailAuditChanges(int $subscription_id, array $before, array $after): void
    {
        if (!function_exists('arraysubs_add_subscription_note')) {
            return;
        }

        $changes       = [];
        $audit_changes = [];
        $currency      = $after['currency'] ?: get_woocommerce_currency();

        $append_change = static function (string $label, string $old_value, string $new_value) use (&$changes, &$audit_changes): void {
            $changes[] = arraysubs_format_audit_change_sentence($label, $old_value, $new_value);
            $audit_changes[] = arraysubs_create_audit_change_entry($label, $old_value, $new_value);
        };

        $before_product = arraysubs_format_audit_value(
            $before['product_id'],
            'product',
            ['variation_id' => $before['variation_id']]
        );
        $after_product  = arraysubs_format_audit_value(
            $after['product_id'],
            'product',
            ['variation_id' => $after['variation_id']]
        );

        if ($before['product_id'] !== $after['product_id'] || $before['variation_id'] !== $after['variation_id']) {
            $append_change(__('Product', 'arraysubs'), $before_product, $after_product);
        }

        $scalar_fields = [
            'invoice_email' => ['label' => __('Invoice email', 'arraysubs')],
            'quantity' => ['label' => __('Quantity', 'arraysubs')],
            'billing_period' => ['label' => __('Billing period', 'arraysubs')],
            'billing_interval' => ['label' => __('Billing interval', 'arraysubs')],
            'subscription_length' => ['label' => __('Subscription length', 'arraysubs')],
            'recurring_amount' => ['label' => __('Recurring amount', 'arraysubs'), 'context' => 'money'],
            'trial_length' => ['label' => __('Trial length', 'arraysubs')],
            'trial_period' => ['label' => __('Trial period', 'arraysubs')],
            'signup_fee' => ['label' => __('Signup fee', 'arraysubs'), 'context' => 'money'],
            'enable_different_renewal_price' => ['label' => __('Different renewal price enabled', 'arraysubs')],
            'different_renewal_price' => ['label' => __('Different renewal price', 'arraysubs'), 'context' => 'money'],
            'different_renewal_price_after' => ['label' => __('Different renewal price after', 'arraysubs')],
        ];

        foreach ($scalar_fields as $key => $config) {
            $old_value = $before[$key] ?? '';
            $new_value = $after[$key] ?? '';

            if ($old_value === $new_value) {
                continue;
            }

            $context = $config['context'] ?? 'text';
            $args    = $context === 'money' ? ['currency' => $currency] : [];

            $append_change(
                $config['label'],
                arraysubs_format_audit_value($old_value, $context, $args),
                arraysubs_format_audit_value($new_value, $context, $args)
            );
        }

        if (
            ($before['payment_method'] ?? '') !== ($after['payment_method'] ?? '')
            || ($before['payment_method_title'] ?? '') !== ($after['payment_method_title'] ?? '')
        ) {
            $append_change(
                __('Payment method', 'arraysubs'),
                arraysubs_format_audit_value(
                    $before['payment_method'] ?? '',
                    'payment_method',
                    ['title' => $before['payment_method_title'] ?? '']
                ),
                arraysubs_format_audit_value(
                    $after['payment_method'] ?? '',
                    'payment_method',
                    ['title' => $after['payment_method_title'] ?? '']
                )
            );
        }

        $address_labels = [
            'first_name' => __('First name', 'arraysubs'),
            'last_name' => __('Last name', 'arraysubs'),
            'company' => __('Company', 'arraysubs'),
            'address_1' => __('Address line 1', 'arraysubs'),
            'address_2' => __('Address line 2', 'arraysubs'),
            'city' => __('City', 'arraysubs'),
            'state' => __('State', 'arraysubs'),
            'postcode' => __('Postcode', 'arraysubs'),
            'country' => __('Country', 'arraysubs'),
            'email' => __('Email', 'arraysubs'),
            'phone' => __('Phone', 'arraysubs'),
        ];

        foreach ($address_labels as $key => $label) {
            $billing_old = $before['billing_address'][$key] ?? '';
            $billing_new = $after['billing_address'][$key] ?? '';

            if ($billing_old !== $billing_new) {
                $append_change(
                    sprintf(
                        /* translators: %s: address field label. */
                        __('Billing %s', 'arraysubs'),
                        $label
                    ),
                    arraysubs_format_audit_value($billing_old),
                    arraysubs_format_audit_value($billing_new)
                );
            }
        }

        foreach ($address_labels as $key => $label) {
            $shipping_old = $before['shipping_address'][$key] ?? '';
            $shipping_new = $after['shipping_address'][$key] ?? '';

            if ($shipping_old !== $shipping_new) {
                $append_change(
                    sprintf(
                        /* translators: %s: address field label. */
                        __('Shipping %s', 'arraysubs'),
                        $label
                    ),
                    arraysubs_format_audit_value($shipping_old),
                    arraysubs_format_audit_value($shipping_new)
                );
            }
        }

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

        $content = esc_html__('Subscription details updated by admin:', 'arraysubs');
        $content .= '<br>' . implode(
            '<br>',
            array_map(
                static fn(string $change): string => '• ' . esc_html($change),
                $changes
            )
        );

        arraysubs_add_subscription_note(
            $subscription_id,
            $content,
            'private',
            get_current_user_id(),
            $audit_changes,
            'subscription'
        );
    }

    /**
     * Change subscription status
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error Response
     */
    public function changeStatus(WP_REST_Request $request)
    {
        $subscription_id = $request->get_param('id');
        $new_status = $request->get_param('status');

        // Validate status - all subscription statuses that admin can set
        $valid_statuses = [
            'arraysubs-pending',
            'arraysubs-active',
            'arraysubs-on-hold',
            'arraysubs-cancelled',
            'arraysubs-expired',
            'arraysubs-trial',
        ];
        if (!in_array($new_status, $valid_statuses, true)) {
            return new WP_Error(
                'invalid_status',
                __('Invalid subscription status', 'arraysubs'),
                ['status' => 400]
            );
        }

        // Get subscription and validate
        $subscription = get_post($subscription_id);
        if (!$subscription || $subscription->post_type !== 'arraysubs_data') {
            return new WP_Error(
                'subscription_not_found',
                __('Subscription not found', 'arraysubs'),
                ['status' => 404]
            );
        }

        $old_status = $subscription->post_status;

        // Don't update if status is the same
        if ($old_status === $new_status) {
            return arraysubs_rest_response(
                ['subscription_id' => $subscription_id, 'status' => $new_status],
                200,
                __('Status unchanged', 'arraysubs')
            );
        }

        // Cancellation must go through the canonical helper so all
        // lifecycle meta is normalized (override detection, retry-state
        // cleanup, AS unscheduling, pending-switch cleanup, action firing).
        // A raw post-status flip leaves _waiting_cancellation behind and
        // produces the "Cancelled + Cancels at end of period" double-badge
        // regression that motivated this guard.
        if ($new_status === 'arraysubs-cancelled') {
            $cancelled = arraysubs_cancel_subscription_immediately(
                $subscription_id,
                '',
                'admin:' . get_current_user_id()
            );

            if (!$cancelled) {
                return new WP_Error(
                    'cancel_failed',
                    __('Failed to cancel subscription', 'arraysubs'),
                    ['status' => 500]
                );
            }

            return arraysubs_rest_response(
                ['subscription_id' => $subscription_id, 'status' => 'arraysubs-cancelled', 'old_status' => $old_status],
                200,
                __('Subscription cancelled', 'arraysubs')
            );
        }

        // Update status (non-cancelled transitions)
        $result = wp_update_post([
            'ID' => $subscription_id,
            'post_status' => $new_status,
        ]);

        if (is_wp_error($result)) {
            return $result;
        }

        return arraysubs_rest_response(
            ['subscription_id' => $subscription_id, 'status' => $new_status, 'old_status' => $old_status],
            200,
            __('Subscription status updated', 'arraysubs')
        );
    }

    /**
     * Cancel subscription
     *
     * Supports two cancellation modes:
     * - immediate: Cancels subscription right now
     * - end_of_period: Sets _waiting_cancellation meta, subscription remains active until next payment date
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error Response
     */
    public function cancelSubscription(WP_REST_Request $request)
    {
        $subscription_id = (int) $request->get_param('id');
        $cancel_type = $request->get_param('cancel_type') ?: 'immediate';
        $reason = sanitize_textarea_field($request->get_param('reason') ?? '');

        $subscription = arraysubs_get_subscription($subscription_id);
        if (!$subscription instanceof \WP_Post) {
            return new \WP_Error(
                'subscription_not_found',
                __('Subscription not found', 'arraysubs'),
                ['status' => 404]
            );
        }

        $current_status = $subscription->post_status;
        $cancellable_statuses = ['arraysubs-active', 'arraysubs-on-hold', 'arraysubs-trial', 'arraysubs-pending'];
        if (!in_array($current_status, $cancellable_statuses, true)) {
            return new \WP_Error(
                'cannot_cancel',
                __('This subscription cannot be cancelled', 'arraysubs'),
                ['status' => 400]
            );
        }

        $cancelled_by = 'admin:' . get_current_user_id();
        $reason_details = $reason !== '' ? ['reason' => $reason] : [];

        // Delegate to the canonical helpers. They:
        //  - update lifecycle meta (_cancellation_type, _cancelled_by,
        //    _cancelled_date) BEFORE wp_update_post() so the
        //    transition_post_status listener (AutoNotes) sees the final
        //    state and produces the correct note;
        //  - clear _waiting_cancellation / _cancellation_scheduled_date
        //    when an immediate cancel overrides a scheduled one,
        //    eliminating the "Cancelled + Cancels at end of period"
        //    double-badge UI conflict;
        //  - mark _cancellation_override_scheduled when overriding so
        //    AutoNotes can describe the override accurately;
        //  - fire the canonical lifecycle actions
        //    (arraysubs_data_waiting_cancellation /
        //    arraysubs_data_cancelled) that emails, gateway sync, and
        //    the renewal-pipeline guards listen on.
        if ($cancel_type === 'end_of_period') {
            $ok = arraysubs_schedule_cancellation($subscription_id, $reason, $cancelled_by, $reason_details);
        } else {
            $ok = arraysubs_cancel_subscription_immediately($subscription_id, $reason, $cancelled_by, $reason_details);
        }

        if (!$ok) {
            return new \WP_Error(
                'cancel_failed',
                __('Failed to cancel subscription', 'arraysubs'),
                ['status' => 500]
            );
        }

        $is_scheduled = arraysubs_is_waiting_cancellation($subscription_id);
        $new_status = get_post_status($subscription_id) ?: $current_status;

        return arraysubs_rest_response(
            [
                'subscription_id' => $subscription_id,
                'status' => $new_status,
                'waiting_cancellation' => $is_scheduled,
                'cancel_type' => $is_scheduled ? 'end_of_period' : 'immediate',
            ],
            200,
            $is_scheduled
                ? __('Subscription scheduled for cancellation', 'arraysubs')
                : __('Subscription cancelled', 'arraysubs')
        );
    }

    /**
     * Undo a scheduled cancellation
     *
     * Removes the _waiting_cancellation meta flag, allowing the subscription
     * to continue renewing normally.
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error Response
     */
    public function undoCancellation(WP_REST_Request $request)
    {
        $subscription_id = (int) $request->get_param('id');
        $subscription = arraysubs_get_subscription($subscription_id);

        if (!$subscription instanceof \WP_Post) {
            return new WP_Error(
                'not_found',
                __('Subscription not found', 'arraysubs'),
                ['status' => 404]
            );
        }

        if (!arraysubs_is_waiting_cancellation($subscription_id)) {
            return new WP_Error(
                'not_scheduled',
                __('This subscription is not scheduled for cancellation', 'arraysubs'),
                ['status' => 400]
            );
        }

        // Delegate to the canonical helper. It clears the scheduled-
        // cancel meta, unschedules the queued cancel job, adds the
        // appropriate note, and fires arraysubs_data_cancellation_undone
        // — which our renewal-pipeline listener uses to re-arm the
        // per-subscription renewal jobs that arraysubs_schedule_cancellation
        // had unscheduled.
        $undone_by = 'admin:' . get_current_user_id();
        $ok = arraysubs_undo_scheduled_cancellation($subscription_id, $undone_by);

        if (!$ok) {
            return new WP_Error(
                'undo_failed',
                __('Failed to remove scheduled cancellation', 'arraysubs'),
                ['status' => 500]
            );
        }

        return arraysubs_rest_response(
            [
                'subscription_id' => $subscription_id,
                'status' => get_post_status($subscription_id) ?: $subscription->post_status,
            ],
            200,
            __('Scheduled cancellation removed successfully', 'arraysubs')
        );
    }

    /**
     * Build the explicit action-availability state for the admin UI.
     *
     * The UI gates buttons (Retry payment, Sync from Gateway) on these
     * fields rather than guessing from raw meta keys. Pro should extend
     * this via the `arraysubs_subscription_detail_data` filter — adding
     * a `gateway_info` block (slug, supports_sync, supports_auto_renew,
     * etc.) without rewriting the actions block.
     *
     * @param int $subscription_id Subscription ID
     * @return array<string, mixed>
     */
    private function buildActionState(int $subscription_id): array
    {
        $subscription = get_post($subscription_id);
        $status = $subscription ? $subscription->post_status : '';

        $payment_gateway = (string) get_post_meta($subscription_id, '_payment_gateway', true);
        $last_failure = get_post_meta($subscription_id, '_last_payment_failure', true);
        $pending_renewal_id = (int) get_post_meta($subscription_id, '_pending_renewal_order_id', true);
        $waiting_cancel = (bool) get_post_meta($subscription_id, '_waiting_cancellation', true);

        $live_statuses = ['arraysubs-active', 'arraysubs-on-hold', 'arraysubs-pending', 'arraysubs-trial'];
        $is_live = in_array($status, $live_statuses, true);

        // Manual retry is offered when there's a recorded failure AND the
        // subscription is in a state where retrying makes sense.
        $can_retry_payment = $is_live && !empty($last_failure);

        // Gateway sync is offered when the subscription is on a payment
        // gateway (pro-managed). The actual capability check for whether
        // the gateway supports sync lives in Pro and is exposed via
        // `gateway_info.supports_sync` — JSX should AND both flags.
        $can_sync_gateway = $is_live && $payment_gateway !== '';

        return [
            'status' => $status,
            'can_retry_payment' => $can_retry_payment,
            'can_sync_gateway' => $can_sync_gateway,
            'payment_gateway' => $payment_gateway ?: null,
            'last_payment_failure' => $last_failure ?: null,
            'pending_renewal_order_id' => $pending_renewal_id ?: null,
            'waiting_cancellation' => $waiting_cancel,
        ];
    }

    /**
     * Get comprehensive cancellation details for a subscription
     *
     * @param int $subscription_id Subscription ID
     * @return array Cancellation details
     */
    private function getCancellationDetails(int $subscription_id): array
    {
        $waiting_cancellation = (bool) get_post_meta($subscription_id, '_waiting_cancellation', true);
        $scheduled_date = get_post_meta($subscription_id, '_cancellation_scheduled_date', true);
        $cancelled_date = get_post_meta($subscription_id, '_cancelled_date', true);
        $reason = get_post_meta($subscription_id, '_cancellation_reason', true);
        $reason_details = get_post_meta($subscription_id, '_cancellation_reason_details', true);

        // Parse reason details (stored as JSON)
        $reason_details_array = [];
        if (!empty($reason_details)) {
            $decoded = is_string($reason_details) ? json_decode($reason_details, true) : $reason_details;
            $reason_details_array = is_array($decoded) ? $decoded : [];
        }

        // Get human-readable reason label
        $reason_label = '';
        if (!empty($reason)) {
            $available_reasons = arraysubs_get_cancellation_reasons();
            $reason_label = $available_reasons[$reason] ?? ucfirst(str_replace('_', ' ', $reason));
        }

        // Retention offers data
        $retention_offers_shown = get_post_meta($subscription_id, '_retention_offers_shown', true);
        if (empty($retention_offers_shown)) {
            $retention_offers_shown = get_post_meta($subscription_id, '_retention_offer_shown', true);
        }
        $retention_offers_shown_date = get_post_meta($subscription_id, '_retention_offers_shown_date', true);
        if (empty($retention_offers_shown_date)) {
            $retention_offers_shown_date = get_post_meta($subscription_id, '_retention_offer_shown_date', true);
        }

        $accepted_retention_offer = get_post_meta($subscription_id, '_accepted_retention_offer', true);
        if (empty($accepted_retention_offer)) {
            $accepted_retention_offer = get_post_meta($subscription_id, '_retention_offer_id', true);
        }
        if (empty($accepted_retention_offer)) {
            $accepted_retention_offer = get_post_meta($subscription_id, '_retention_offer_type', true);
        }
        $retention_offer_accepted_date = get_post_meta($subscription_id, '_retention_offer_accepted_date', true);
        if (empty($retention_offer_accepted_date)) {
            $retention_offer_accepted_date = get_post_meta($subscription_id, '_retention_accepted_date', true);
        }

        // Parse retention offers shown (stored as JSON array)
        $offers_shown_array = [];
        if (!empty($retention_offers_shown)) {
            $decoded = is_string($retention_offers_shown) ? json_decode($retention_offers_shown, true) : $retention_offers_shown;
            if (is_array($decoded)) {
                $offers_shown_array = $decoded;
            } elseif ($retention_offers_shown) {
                $offers_shown_array = [sanitize_text_field((string) $retention_offers_shown)];
            }
        }

        // Get subscription status
        $subscription = arraysubs_get_subscription($subscription_id);
        $is_cancelled = $subscription && $subscription->post_status === 'arraysubs-cancelled';

        return [
            // Current state
            'is_cancelled' => $is_cancelled,
            'is_waiting_cancellation' => $waiting_cancellation,
            'scheduled_date' => $scheduled_date ?: null,
            'scheduled_date_display' => $scheduled_date ? arraysubs_format_date_local($scheduled_date) : null,
            'cancelled_date' => $cancelled_date ?: null,
            'cancelled_date_display' => $cancelled_date ? arraysubs_format_date_local($cancelled_date) : null,

            // Reason
            'reason' => $reason ?: null,
            'reason_label' => $reason_label ?: null,
            'reason_details' => $reason_details_array,
            'other_reason' => $reason_details_array['other_reason'] ?? null,

            // Retention offers
            'retention_offers' => [
                'shown' => $offers_shown_array,
                'shown_date' => $retention_offers_shown_date ?: null,
                'shown_date_display' => $retention_offers_shown_date ? arraysubs_format_date_local($retention_offers_shown_date) : null,
                'accepted' => $accepted_retention_offer ?: null,
                'accepted_date' => $retention_offer_accepted_date ?: null,
                'accepted_date_display' => $retention_offer_accepted_date ? arraysubs_format_date_local($retention_offer_accepted_date) : null,
            ],

            // Flags
            'has_retention_history' => !empty($offers_shown_array) || !empty($accepted_retention_offer),
        ];
    }

    /**
     * Update shipping address for a subscription
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response|WP_Error Response
     */
    public function updateShippingAddress(WP_REST_Request $request)
    {
        $subscription_id = $request->get_param('id');
        $subscription = arraysubs_get_subscription($subscription_id);

        if (!$subscription) {
            return new WP_Error(
                'not_found',
                __('Subscription not found', 'arraysubs'),
                ['status' => 404]
            );
        }

        // Check if subscription needs shipping
        $needs_shipping = get_post_meta($subscription_id, '_arraysubs_needs_shipping', true);
        if ($needs_shipping !== 'yes') {
            return new WP_Error(
                'invalid_subscription',
                __('This subscription does not require shipping', 'arraysubs'),
                ['status' => 400]
            );
        }

        $updated_fields = [];

        // Update shipping address if provided
        $shipping_address = $request->get_param('shipping_address');
        if (!empty($shipping_address) && is_array($shipping_address)) {
            // Sanitize address fields
            $allowed_fields = [
                'first_name',
                'last_name',
                'company',
                'address_1',
                'address_2',
                'city',
                'state',
                'postcode',
                'country',
                'phone'
            ];

            $sanitized_address = [];
            foreach ($allowed_fields as $field) {
                if (isset($shipping_address[$field])) {
                    $sanitized_address[$field] = sanitize_text_field($shipping_address[$field]);
                }
            }

            // Merge with existing address
            $existing_address_json = get_post_meta($subscription_id, '_shipping_address', true);
            $existing_address = !empty($existing_address_json) ? json_decode($existing_address_json, true) : [];
            if (!is_array($existing_address)) {
                $existing_address = [];
            }

            $merged_address = array_merge($existing_address, $sanitized_address);
            update_post_meta($subscription_id, '_shipping_address', wp_json_encode($merged_address));
            $updated_fields[] = 'shipping_address';

            // Add subscription note
            if (function_exists('arraysubs_add_subscription_note')) {
                $address_labels = [
                    'first_name' => __('Shipping first name', 'arraysubs'),
                    'last_name' => __('Shipping last name', 'arraysubs'),
                    'company' => __('Shipping company', 'arraysubs'),
                    'address_1' => __('Shipping address line 1', 'arraysubs'),
                    'address_2' => __('Shipping address line 2', 'arraysubs'),
                    'city' => __('Shipping city', 'arraysubs'),
                    'state' => __('Shipping state', 'arraysubs'),
                    'postcode' => __('Shipping postcode', 'arraysubs'),
                    'country' => __('Shipping country', 'arraysubs'),
                    'phone' => __('Shipping phone', 'arraysubs'),
                ];
                $address_changes = [];
                $audit_changes   = [];

                foreach ($address_labels as $field => $label) {
                    $old_value = arraysubs_format_audit_value($existing_address[$field] ?? '');
                    $new_value = arraysubs_format_audit_value($merged_address[$field] ?? '');

                    if ($old_value === $new_value) {
                        continue;
                    }

                    $address_changes[] = arraysubs_format_audit_change_sentence($label, $old_value, $new_value);
                    $audit_changes[] = arraysubs_create_audit_change_entry($label, $old_value, $new_value);
                }

                $note = __('Shipping address updated by admin.', 'arraysubs');

                if (!empty($address_changes)) {
                    $note .= '<br>' . implode(
                        '<br>',
                        array_map(
                            static fn(string $change): string => '• ' . esc_html($change),
                            $address_changes
                        )
                    );
                }

                arraysubs_add_subscription_note(
                    $subscription_id,
                    $note,
                    'private',
                    get_current_user_id(),
                    $audit_changes,
                    'subscription'
                );
            }
        }

        // Update shipping type if provided
        $shipping_type = $request->get_param('shipping_type');
        if (!empty($shipping_type) && in_array($shipping_type, ['one-time', 'recurring'], true)) {
            $old_type = get_post_meta($subscription_id, '_arraysubs_shipping_type', true);
            if ($old_type !== $shipping_type) {
                update_post_meta($subscription_id, '_arraysubs_shipping_type', $shipping_type);
                $updated_fields[] = 'shipping_type';

                if (function_exists('arraysubs_add_subscription_note')) {
                    arraysubs_add_subscription_note(
                        $subscription_id,
                        sprintf(
                            /* translators: 1: old shipping type, 2: new shipping type */
                            __('Shipping type changed from "%1$s" to "%2$s" by admin.', 'arraysubs'),
                            $old_type ?: 'recurring',
                            $shipping_type
                        ),
                        'private',
                        get_current_user_id(),
                        [
                            arraysubs_create_audit_change_entry(
                                __('Shipping type', 'arraysubs'),
                                arraysubs_format_audit_value($old_type ?: 'recurring'),
                                arraysubs_format_audit_value($shipping_type)
                            ),
                        ],
                        'subscription'
                    );
                }
            }
        }

        // Update renewal shipping total if provided
        $renewal_shipping_total = $request->get_param('renewal_shipping_total');
        if ($renewal_shipping_total !== null) {
            $renewal_shipping_total = floatval($renewal_shipping_total);
            $old_total = floatval(get_post_meta($subscription_id, '_arraysubs_renewal_shipping_total', true));

            if ($old_total !== $renewal_shipping_total) {
                update_post_meta($subscription_id, '_arraysubs_renewal_shipping_total', $renewal_shipping_total);
                $updated_fields[] = 'renewal_shipping_total';

                if (function_exists('arraysubs_add_subscription_note')) {
                    arraysubs_add_subscription_note(
                        $subscription_id,
                        sprintf(
                            /* translators: 1: old total, 2: new total */
                            __('Renewal shipping total changed from %1$s to %2$s by admin.', 'arraysubs'),
                            wc_price($old_total),
                            wc_price($renewal_shipping_total)
                        ),
                        'private',
                        get_current_user_id(),
                        [
                            arraysubs_create_audit_change_entry(
                                __('Renewal shipping total', 'arraysubs'),
                                arraysubs_format_audit_value($old_total, 'money'),
                                arraysubs_format_audit_value($renewal_shipping_total, 'money')
                            ),
                        ],
                        'subscription'
                    );
                }
            }
        }

        if (empty($updated_fields)) {
            return arraysubs_rest_response(
                ['updated' => []],
                200,
                __('No changes made', 'arraysubs')
            );
        }

        return arraysubs_rest_response(
            [
                'updated' => $updated_fields,
                'shipping' => [
                    'needs_shipping' => true,
                    'shipping_type' => get_post_meta($subscription_id, '_arraysubs_shipping_type', true),
                    'shipping_method_id' => get_post_meta($subscription_id, '_arraysubs_shipping_method_id', true),
                    'shipping_method_title' => get_post_meta($subscription_id, '_arraysubs_shipping_method_title', true),
                    'shipping_total' => (float) get_post_meta($subscription_id, '_arraysubs_shipping_total', true),
                    'renewal_shipping_total' => (float) get_post_meta($subscription_id, '_arraysubs_renewal_shipping_total', true),
                    'initial_shipping_paid' => get_post_meta($subscription_id, '_arraysubs_initial_shipping_paid', true) === 'yes',
                ],
            ],
            200,
            __('Shipping updated successfully', 'arraysubs')
        );
    }

    /**
     * Manually retry a failed renewal payment (admin action).
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response|WP_Error Response.
     */
    public function retryPayment(WP_REST_Request $request)
    {
        $subscription_id = (int) $request->get_param('id');
        $subscription    = arraysubs_get_subscription($subscription_id);

        if (! $subscription instanceof \WP_Post) {
            return new WP_Error('subscription_not_found', __('Subscription not found', 'arraysubs'), ['status' => 404]);
        }

        $processor = new \ArraySubs\Features\RecurringBilling\Services\RenewalProcessor();
        $result    = $processor->manualRetry($subscription_id, 'admin:' . get_current_user_id());

        if (! empty($result['success'])) {
            return arraysubs_rest_response(
                [
                    'subscription_id' => $subscription_id,
                    'status'          => $result['status'],
                    'order_id'        => $result['order_id'] ?? 0,
                ],
                200,
                $result['message']
            );
        }

        return new WP_Error(
            'retry_failed',
            $result['message'] ?? __('Renewal payment retry failed.', 'arraysubs'),
            [
                'status'   => 'no_failed_renewal' === ($result['status'] ?? '') ? 409 : 402,
                'order_id' => $result['order_id'] ?? 0,
            ]
        );
    }

    /**
     * Trigger a gateway-state sync for a subscription (admin action). Pulls
     * authoritative state from the gateway and applies safe corrections to
     * local meta. Pro provides the actual gateway integration via the
     * `arraysubs_gateway_sync_subscription` filter; without pro, this is a
     * no-op success.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response|WP_Error Response.
     */
    public function gatewaySync(WP_REST_Request $request)
    {
        $subscription_id = (int) $request->get_param('id');
        $subscription    = arraysubs_get_subscription($subscription_id);

        if (! $subscription instanceof \WP_Post) {
            return new WP_Error('subscription_not_found', __('Subscription not found', 'arraysubs'), ['status' => 404]);
        }

        $default = [
            'success'          => false,
            'changes'          => [],
            'message'          => __('No gateway integration is active for this subscription.', 'arraysubs'),
            'gateway'          => '',
        ];

        /**
         * Filter triggered when the admin requests a gateway sync.
         *
         * Pro hooks this filter to pull authoritative state from the gateway
         * (Stripe, PayPal, Paddle), reconcile local meta, and reconcile any
         * orders that match a recent successful gateway charge.
         *
         * @param array $result          Default result shape.
         * @param int   $subscription_id Subscription ID.
         */
        $result = apply_filters('arraysubs_gateway_sync_subscription', $default, $subscription_id);

        if (! is_array($result)) {
            $result = $default;
        }

        if (empty($result['success'])) {
            return new WP_Error(
                'gateway_sync_failed',
                $result['message'] ?? __('Gateway sync failed.', 'arraysubs'),
                ['status' => 422]
            );
        }

        return arraysubs_rest_response(
            [
                'subscription_id' => $subscription_id,
                'changes'         => $result['changes'] ?? [],
                'gateway'         => $result['gateway'] ?? '',
            ],
            200,
            $result['message'] ?? __('Subscription synced from gateway.', 'arraysubs')
        );
    }
}
