<?php

/**
 * Auto-generated Subscription Notes
 *
 * Automatically creates notes when important subscription events occur.
 *
 * @package ArraySubs\Features\SubscriptionNotes\Services
 */

namespace ArraySubs\Features\SubscriptionNotes\Services;

defined('ABSPATH') || exit;

use ArraySubs\Features\PlanSwitching\Services\AutoDowngradeHandler;

/**
 * Auto Notes Service
 */
class AutoNotes
{
    /**
     * Enable auto-loading
     *
     * @var bool
     */
    public static $loadable = true;

    /**
     * Constructor - Register hooks for auto-notes
     *
     * Note: Only register hooks that are actually fired elsewhere in the codebase.
     * Each hook is documented with where it's fired from.
     */
    public function __construct()
    {
        // Status change hooks
        // Fired in: Subscriptions/Services/Hooks.php, SubscriptionAdmin/REST/SubscriptionController.php
        add_action('arraysubs_data_status_changed', [$this, 'onStatusChanged'], 10, 3);

        // Payment hooks
        // 'arraysubs_order_paid' fired in: Subscriptions/Services/OrderIntegration.php
        add_action('arraysubs_order_paid', [$this, 'onPaymentComplete'], 10, 2);
        add_action('arraysubs_payment_failed', [$this, 'onPaymentFailed'], 10, 3);
        add_action('arraysubs_renewal_payment_failed', [$this, 'onRenewalPaymentFailed'], 10, 3);
        add_action('arraysubs_gateway_payment_failed', [$this, 'onGatewayPaymentFailed'], 10, 4);

        // Renewal hooks
        // 'arraysubs_renewal_invoice_created' fired in: RecurringBilling/Services/RenewalProcessor::createRenewalInvoice()
        // This is the main flow when invoice is created X hours before due date
        add_action('arraysubs_renewal_invoice_created', [$this, 'onRenewalInvoiceCreated'], 10, 2);

        // Subscription lifecycle
        // 'arraysubs_data_created' fired in: Subscriptions/Services/Hooks.php (canonical emitter via wp_insert_post)
        add_action('arraysubs_data_created', [$this, 'onSubscriptionCreated'], 10, 2);

        // Trial conversion
        // 'arraysubs_trial_converted' fired in: RecurringBilling/Services/TrialConverter.php
        add_action('arraysubs_trial_converted', [$this, 'onTrialConverted'], 10, 1);

        // Product changes
        // 'arraysubs_product_changed' fired in: SubscriptionAdmin/REST/ManualController.php
        add_action('arraysubs_product_changed', [$this, 'onProductChanged'], 10, 3);

        // Plan switching hooks
        // 'arraysubs_plan_switch_completed' fired in: PlanSwitching/Services/PlanManager.php
        add_action('arraysubs_plan_switch_completed', [$this, 'onPlanSwitchCompleted'], 10, 4);

        // Subscription put on hold hook (separate from status change for payment-related on-hold)
        // 'arraysubs_data_put_on_hold' fired in: RecurringBilling/Services/Hooks.php
        add_action('arraysubs_data_put_on_hold', [$this, 'onSubscriptionPutOnHold'], 10, 1);

        // Cancellation specific hooks
        // 'arraysubs_data_waiting_cancellation' fired in: cancellation-helpers.php, SubscriptionAdmin/REST/SubscriptionController.php
        add_action('arraysubs_data_waiting_cancellation', [$this, 'onWaitingCancellation'], 10, 2);

        // Granular change hooks (fired in SubscriptionAdmin/REST/SubscriptionController.php)
        add_action('arraysubs_payment_method_changed', [$this, 'onPaymentMethodChanged'], 10, 3);
        add_action('arraysubs_quantity_changed', [$this, 'onQuantityChanged'], 10, 3);
        add_action('arraysubs_recurring_amount_changed', [$this, 'onRecurringAmountChanged'], 10, 3);

        // Subscription reactivated (fired in SubscriptionController, ManualController)
        add_action('arraysubs_subscription_reactivated', [$this, 'onSubscriptionReactivated'], 10, 1);

        // Trial lifecycle (fired in OrderIntegration.php)
        add_action('arraysubs_trial_started', [$this, 'onTrialStarted'], 10, 1);
    }

    /**
     * Add note when subscription status changes
     *
     * @param int    $subscription_id The subscription ID.
     * @param string $new_status      The new status.
     * @param string $old_status      The old status.
     * @return void
     */
    public function onStatusChanged(int $subscription_id, string $new_status, string $old_status): void
    {
        if (
            class_exists(AutoDowngradeHandler::class)
            && (
                AutoDowngradeHandler::shouldSuppressLifecycleEmail($subscription_id, $new_status, $old_status)
                || AutoDowngradeHandler::isProcessing()
            )
        ) {
            return;
        }

        $old_label = arraysubs_get_subscription_status_name($old_status);
        $new_label = arraysubs_get_subscription_status_name($new_status);

        $content = $this->buildStatusChangeMessage($subscription_id, $old_status, $new_status, $old_label, $new_label);

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'subscription');
    }

    /**
     * Add note when payment completes (order paid)
     *
     * Hook signature: arraysubs_order_paid ($order_id, $order)
     *
     * @param int       $order_id The order ID.
     * @param \WC_Order $order    The order object.
     * @return void
     */
    public function onPaymentComplete(int $order_id, \WC_Order $order): void
    {
        $subscription_ids = function_exists('arraysubs_get_subscription_ids_for_order')
            ? arraysubs_get_subscription_ids_for_order($order)
            : [];

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

        $order_url = admin_url('post.php?post=' . $order_id . '&action=edit');

        $content = sprintf(
            /* translators: 1: order link, 2: order total */
            __('Payment successful - <a href="%1$s">Order #%2$s</a> (%3$s)', 'arraysubs'),
            esc_url($order_url),
            esc_html($order_id),
            wp_kses_post($order->get_formatted_order_total())
        );

        $event_type = $order->get_meta('_is_renewal_order', true) === 'yes'
            ? 'renewal_payment_succeeded'
            : 'payment_succeeded';

        foreach ($subscription_ids as $subscription_id) {
            arraysubs_add_system_note($subscription_id, $content, 'customer', 'order', $event_type);
        }
    }

    /**
     * Add note when payment fails
     *
     * Hook signature: arraysubs_payment_failed ($subscription_id, $order_id, $error_data)
     *
     * @param int   $subscription_id The subscription ID.
     * @param int   $order_id        The order ID.
     * @param array $error_data      Error details (optional).
     * @return void
     */
    public function onPaymentFailed(int $subscription_id, int $order_id, array $error_data = []): void
    {
        $order_url = admin_url('post.php?post=' . $order_id . '&action=edit');
        $gateway_label = $this->getGatewayLabel($subscription_id);

        $content = sprintf(
            /* translators: 1: order link */
            __('Payment failed for <a href="%1$s">Order #%2$s</a>.', 'arraysubs'),
            esc_url($order_url),
            esc_html($order_id)
        );

        if (!empty($gateway_label)) {
            $content .= ' ' . sprintf(
                /* translators: %s: gateway label */
                __('Gateway: %s.', 'arraysubs'),
                esc_html($gateway_label)
            );
        }

        if (!empty($error_data['message'])) {
            $content .= ' ' . sprintf(
                /* translators: %s: reason */
                __('Reason: %s', 'arraysubs'),
                esc_html($error_data['message'])
            );
        }

        $event_type = 'payment_failed';
        $order      = wc_get_order($order_id);

        if (
            ($order instanceof \WC_Order && $order->get_meta('_is_renewal_order', true) === 'yes')
            || in_array($error_data['source'] ?? '', ['renewal_processor', 'gateway_webhook'], true)
        ) {
            $event_type = 'renewal_payment_failed';
        }

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'order', $event_type);
    }

    /**
     * Add note when renewal payment fails.
     *
     * Hook signature: arraysubs_renewal_payment_failed ($subscription_id, $order_id, $error_message)
     *
     * @param int    $subscription_id The subscription ID.
     * @param int    $order_id        The order ID.
     * @param string $error_message   Error message.
     * @return void
     */
    public function onRenewalPaymentFailed(int $subscription_id, int $order_id, string $error_message = ''): void
    {
        $gateway_slug = (string) \get_post_meta($subscription_id, '_payment_gateway', true);

        // Gateway-backed renewal failures also emit arraysubs_gateway_payment_failed.
        // Defer to that hook so the note is recorded only once with gateway context.
        if (! empty($gateway_slug)) {
            return;
        }

        $this->onPaymentFailed(
            $subscription_id,
            $order_id,
            [
                'message' => $error_message,
                'source' => 'renewal_processor',
            ]
        );
    }

    /**
     * Add note when gateway reports a payment failure.
     *
     * Hook signature: arraysubs_gateway_payment_failed ($subscription_id, $order_id, $error_message, $gateway_slug)
     *
     * @param int    $subscription_id The subscription ID.
     * @param int    $order_id        The order ID.
     * @param string $error_message   Error message.
     * @param string $gateway_slug    Gateway slug.
     * @return void
     */
    public function onGatewayPaymentFailed(
        int $subscription_id,
        int $order_id,
        string $error_message = '',
        string $gateway_slug = ''
    ): void {
        $this->onPaymentFailed(
            $subscription_id,
            $order_id,
            [
                'message' => $error_message,
                'gateway' => $gateway_slug,
                'source' => 'gateway_webhook',
            ]
        );
    }

    /**
     * Add note when payment method changes
     *
     * @param int    $subscription_id The subscription ID.
     * @param string $new_method      The new payment method.
     * @param string $old_method      The old payment method.
     * @return void
     */
    public function onPaymentMethodChanged(int $subscription_id, string $new_method, string $old_method): void
    {
        $content = sprintf(
            /* translators: 1: old payment method, 2: new payment method */
            __('Payment method changed from %1$s to %2$s.', 'arraysubs'),
            '<strong>' . esc_html($old_method) . '</strong>',
            '<strong>' . esc_html($new_method) . '</strong>'
        );

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'subscription', 'renewal_invoice_created');
    }

    /**
     * Add note when renewal is processed
     *
     * @param int $subscription_id The subscription ID.
     * @param int $order_id        The renewal order ID.
     * @return void
     */
    public function onRenewalProcessed(int $subscription_id, int $order_id): void
    {
        $order_url = admin_url('post.php?post=' . $order_id . '&action=edit');

        $content = sprintf(
            /* translators: 1: order link */
            __('Renewal invoice created - <a href="%1$s">Order #%2$s</a> awaiting payment.', 'arraysubs'),
            esc_url($order_url),
            esc_html($order_id)
        );

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'subscription', 'renewal_invoice_created');
    }

    /**
     * Add note when renewal invoice is created (main flow)
     * This is fired X hours before the subscription due date
     *
     * @param int $subscription_id The subscription ID.
     * @param int $order_id        The renewal order ID.
     * @return void
     */
    public function onRenewalInvoiceCreated(int $subscription_id, int $order_id): void
    {
        $order_url = admin_url('post.php?post=' . $order_id . '&action=edit');
        $next_payment = get_post_meta($subscription_id, '_next_payment_date', true);
        $due_date_formatted = !empty($next_payment)
            ? arraysubs_format_date_local($next_payment)
            : __('N/A', 'arraysubs');

        $content = sprintf(
            /* translators: 1: order link, 2: due date */
            __('Renewal invoice <a href="%1$s">#%2$s</a> generated for payment due on %3$s.', 'arraysubs'),
            esc_url($order_url),
            esc_html($order_id),
            esc_html($due_date_formatted)
        );

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'subscription');
    }

    /**
     * Add note when trial starts
     *
     * @param int $subscription_id The subscription ID.
     * @return void
     */
    public function onTrialStarted(int $subscription_id): void
    {
        $subscription = get_post($subscription_id);
        $trial_end = get_post_meta($subscription_id, '_trial_end_date', true);

        $content = __('Free trial started.', 'arraysubs');

        if (!empty($trial_end)) {
            $content .= ' ' . sprintf(
                /* translators: %s: trial end date */
                __('Trial ends on %s.', 'arraysubs'),
                '<strong>' . esc_html(arraysubs_format_date_local($trial_end)) . '</strong>'
            );
        }

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'subscription');
    }

    /**
     * Add note when trial ends
     *
     * @param int  $subscription_id The subscription ID.
     * @param bool $converted       Whether trial converted to paid.
     * @return void
     */
    public function onTrialEnded(int $subscription_id, bool $converted = true): void
    {
        if ($converted) {
            $content = __('Free trial ended. Subscription converted to paid.', 'arraysubs');
        } else {
            $content = __('Free trial ended. Subscription expired.', 'arraysubs');
        }

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'subscription');
    }

    /**
     * Add note when subscription is created
     *
     * Hook signature: arraysubs_data_created ($subscription_id, $subscription_post)
     *
     * @param int            $subscription_id The subscription ID.
     * @param \WP_Post|array $data            The subscription post object or data array.
     * @return void
     */
    public function onSubscriptionCreated(int $subscription_id, $data = null): void
    {
        // Get order ID from subscription meta
        $order_id = absint(get_post_meta($subscription_id, '_original_order_id', true));

        if ($order_id > 0) {
            $order_url = admin_url('post.php?post=' . $order_id . '&action=edit');

            $content = sprintf(
                /* translators: 1: order link */
                __('Subscription created from <a href="%1$s">Order #%2$s</a>.', 'arraysubs'),
                esc_url($order_url),
                esc_html($order_id)
            );
        } else {
            $content = __('Subscription created.', 'arraysubs');
        }

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'subscription');
    }

    /**
     * Add note when trial converts to paid subscription
     *
     * Hook signature: arraysubs_trial_converted ($subscription_id)
     *
     * @param int $subscription_id The subscription ID.
     * @return void
     */
    public function onTrialConverted(int $subscription_id): void
    {
        if (
            class_exists(AutoDowngradeHandler::class)
            && AutoDowngradeHandler::shouldHandleTrialConversion($subscription_id)
        ) {
            return;
        }

        $content = __('Free trial ended. Subscription converted to paid.', 'arraysubs');

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'subscription');
    }

    /**
     * Add note when subscription is reactivated
     *
     * @param int $subscription_id The subscription ID.
     * @return void
     */
    public function onSubscriptionReactivated(int $subscription_id): void
    {
        $content = __('Subscription reactivated.', 'arraysubs');

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'subscription');
    }

    /**
     * Add note when product changes
     *
     * @param int $subscription_id The subscription ID.
     * @param int $old_product_id  The old product ID.
     * @param int $new_product_id  The new product ID.
     * @return void
     */
    public function onProductChanged(int $subscription_id, int $old_product_id, int $new_product_id): void
    {
        $old_product = wc_get_product($old_product_id);
        $new_product = wc_get_product($new_product_id);

        $old_name = $old_product ? arraysubs_get_subscription_product_name($old_product) : __('Unknown', 'arraysubs');
        $new_name = $new_product ? arraysubs_get_subscription_product_name($new_product) : __('Unknown', 'arraysubs');

        $content = sprintf(
            /* translators: 1: old product name, 2: new product name */
            __('Product changed from %1$s to %2$s.', 'arraysubs'),
            '<strong>' . esc_html($old_name) . '</strong>',
            '<strong>' . esc_html($new_name) . '</strong>'
        );

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'product');
    }

    /**
     * Add note when quantity changes
     *
     * @param int $subscription_id The subscription ID.
     * @param int $new_quantity    The new quantity.
     * @param int $old_quantity    The old quantity.
     * @return void
     */
    public function onQuantityChanged(int $subscription_id, int $new_quantity, int $old_quantity): void
    {
        $content = sprintf(
            /* translators: 1: old quantity, 2: new quantity */
            __('Quantity changed from %1$d to %2$d.', 'arraysubs'),
            $old_quantity,
            $new_quantity
        );

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'product');
    }

    /**
     * Add note when recurring amount changes
     *
     * @param int   $subscription_id The subscription ID.
     * @param float $new_amount      The new amount.
     * @param float $old_amount      The old amount.
     * @return void
     */
    public function onRecurringAmountChanged(int $subscription_id, float $new_amount, float $old_amount): void
    {
        $content = sprintf(
            /* translators: 1: old amount, 2: new amount */
            __('Recurring amount changed from %1$s to %2$s.', 'arraysubs'),
            '<strong>' . wp_kses_post(wc_price($old_amount)) . '</strong>',
            '<strong>' . wp_kses_post(wc_price($new_amount)) . '</strong>'
        );

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'product');
    }

    /**
     * Add note when plan switch is completed
     *
     * @param int    $subscription_id The subscription ID.
     * @param int    $old_product_id  The old product ID.
     * @param int    $new_product_id  The new product ID.
     * @param string $switch_type     The switch type (upgrade, downgrade, crossgrade).
     * @return void
     */
    public function onPlanSwitchCompleted(
        int $subscription_id,
        int $old_product_id,
        int $new_product_id,
        string $switch_type
    ): void {
        if ($switch_type === 'auto_downgrade') {
            return;
        }

        $old_product = wc_get_product($old_product_id);
        $new_product = wc_get_product($new_product_id);

        $old_name = $old_product ? arraysubs_get_subscription_product_name($old_product) : __('Unknown', 'arraysubs');
        $new_name = $new_product ? arraysubs_get_subscription_product_name($new_product) : __('Unknown', 'arraysubs');

        $switch_labels = [
            'upgrade' => __('Upgraded', 'arraysubs'),
            'downgrade' => __('Downgraded', 'arraysubs'),
            'crossgrade' => __('Crossgraded', 'arraysubs'),
        ];

        $switch_label = $switch_labels[$switch_type] ?? __('Switched', 'arraysubs');

        $content = sprintf(
            /* translators: 1: switch type (Upgraded/Downgraded/Crossgraded), 2: old product name, 3: new product name */
            __('%1$s from %2$s to %3$s.', 'arraysubs'),
            '<strong>' . esc_html($switch_label) . '</strong>',
            '<strong>' . esc_html($old_name) . '</strong>',
            '<strong>' . esc_html($new_name) . '</strong>'
        );

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'product');
    }

    /**
     * Add note when subscription is put on hold due to payment issues
     *
     * Hook signature: arraysubs_data_put_on_hold ($subscription_id)
     *
     * @param int $subscription_id The subscription ID.
     * @return void
     */
    public function onSubscriptionPutOnHold(int $subscription_id): void
    {
        $pending_order_id = (int) get_post_meta($subscription_id, '_pending_renewal_order_id', true);
        $next_payment     = get_post_meta($subscription_id, '_next_payment_date', true);

        $content = __('Subscription was automatically put on hold by System because a renewal payment is still unpaid. Customer access may be restricted.', 'arraysubs');

        if ($pending_order_id > 0) {
            $content .= ' ' . sprintf(
                /* translators: 1: admin order URL, 2: order ID */
                __('Outstanding renewal invoice: <a href="%1$s">Order #%2$s</a>.', 'arraysubs'),
                esc_url(admin_url('post.php?post=' . $pending_order_id . '&action=edit')),
                esc_html($pending_order_id)
            );
        }

        if (!empty($next_payment)) {
            $content .= ' ' . sprintf(
                /* translators: %s: payment due date */
                __('Original renewal due date: %s.', 'arraysubs'),
                '<strong>' . esc_html(arraysubs_format_date_local($next_payment)) . '</strong>'
            );
        }

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'subscription');
    }

    /**
     * Add note when subscription is set to cancel at end of billing period
     *
     * Hook signature: arraysubs_data_waiting_cancellation ($subscription_id, $reason)
     *
     * @param int    $subscription_id The subscription ID.
     * @param string $reason          Cancellation reason.
     * @return void
     */
    public function onWaitingCancellation(int $subscription_id, string $reason = ''): void
    {
        $end_date = get_post_meta($subscription_id, '_next_payment_date', true);
        $formatted_date = !empty($end_date) ? arraysubs_format_date_local($end_date) : __('end of billing period', 'arraysubs');
        $actor = $this->getStatusActorDetails($subscription_id, true);

        $content = sprintf(
            /* translators: %s: cancellation date */
            __('Subscription scheduled to cancel on %s.', 'arraysubs'),
            '<strong>' . esc_html($formatted_date) . '</strong>'
        );

        $content .= ' ' . sprintf(
            /* translators: %s: actor display */
            __('Scheduled by %s.', 'arraysubs'),
            esc_html($actor['display'])
        );

        if (!empty($reason)) {
            $content .= ' ' . sprintf(
                /* translators: %s: reason */
                __('Reason: %s', 'arraysubs'),
                esc_html($this->getReadableCancellationReason($reason))
            );
        }

        arraysubs_add_system_note($subscription_id, $content, 'customer', 'subscription');
    }

    /**
     * Build a contextual status change note.
     *
     * @param int    $subscription_id Subscription ID.
     * @param string $old_status      Old status slug.
     * @param string $new_status      New status slug.
     * @param string $old_label       Old status label.
     * @param string $new_label       New status label.
     * @return string
     */
    private function buildStatusChangeMessage(
        int $subscription_id,
        string $old_status,
        string $new_status,
        string $old_label,
        string $new_label
    ): string {
        $actor = $this->getStatusActorDetails(
            $subscription_id,
            'arraysubs-cancelled' === $new_status,
            $old_status,
            $new_status
        );
        $status_context = (string) get_post_meta($subscription_id, '_arraysubs_status_change_context', true);

        $base = sprintf(
            /* translators: 1: old subscription status label, 2: new subscription status label */
            __('Status changed from %1$s to %2$s.', 'arraysubs'),
            '<strong>' . esc_html($old_label) . '</strong>',
            '<strong>' . esc_html($new_label) . '</strong>'
        );

        if ('arraysubs-cancelled' === $new_status) {
            return $this->buildCancellationStatusMessage($subscription_id, $base, $actor);
        }

        if ('arraysubs-on-hold' === $new_status) {
            $message = $base . ' ' . sprintf(
                /* translators: %s: actor display */
                __('Changed by %s.', 'arraysubs'),
                esc_html($actor['display'])
            );

            if ('payment_failure' === $status_context) {
                $message .= ' ' . __('The subscription was placed on hold because the payment did not complete.', 'arraysubs');
            } elseif ('system' === $actor['role']) {
                $message .= ' ' . __('The subscription was placed on hold because the renewal invoice is still unpaid.', 'arraysubs');
            }

            return $message;
        }

        if ('arraysubs-active' === $new_status && 'arraysubs-on-hold' === $old_status) {
            return $base . ' ' . sprintf(
                /* translators: %s: actor display */
                __('Changed by %s after payment or manual review restored the subscription.', 'arraysubs'),
                esc_html($actor['display'])
            );
        }

        return $base . ' ' . sprintf(
            /* translators: %s: actor display */
            __('Changed by %s.', 'arraysubs'),
            esc_html($actor['display'])
        );
    }

    /**
     * Build a detailed cancellation status message.
     *
     * @param int    $subscription_id Subscription ID.
     * @param string $base           Base status text.
     * @param array  $actor           Actor details.
     * @return string
     */
    private function buildCancellationStatusMessage(int $subscription_id, string $base, array $actor): string
    {
        $raw_reason         = (string) get_post_meta($subscription_id, '_cancellation_reason', true);
        $reason             = $this->getReadableCancellationReason($raw_reason);
        $cancellation_type  = (string) get_post_meta($subscription_id, '_cancellation_type', true);
        $gateway_source     = (string) get_post_meta($subscription_id, '_cancellation_source', true);
        $pending_order_id   = (int) get_post_meta($subscription_id, '_pending_renewal_order_id', true);
        $override_scheduled = (bool) get_post_meta($subscription_id, '_cancellation_override_scheduled', true);

        if ('overdue_payment' === $raw_reason) {
            $message = $base . ' ' . __('Automatically cancelled by System after the unpaid renewal remained overdue beyond the grace period.', 'arraysubs');

            if ($pending_order_id > 0) {
                $message .= ' ' . sprintf(
                    /* translators: 1: admin order URL, 2: order ID */
                    __('Final unpaid renewal invoice: <a href="%1$s">Order #%2$s</a>.', 'arraysubs'),
                    esc_url(admin_url('post.php?post=' . $pending_order_id . '&action=edit')),
                    esc_html($pending_order_id)
                );
            }

            return $message;
        }

        if ('gateway' === $gateway_source || 'gateway' === $actor['role']) {
            $message = $base . ' ' . sprintf(
                /* translators: %s: actor display */
                __('Cancellation was received from %s.', 'arraysubs'),
                esc_html($actor['display'])
            );

            if (!empty($reason)) {
                $message .= ' ' . sprintf(
                    /* translators: %s: cancellation reason */
                    __('Reason: %s.', 'arraysubs'),
                    esc_html($reason)
                );
            }

            return $message;
        }

        $message = $base . ' ' . sprintf(
            /* translators: %s: actor display */
            __('Cancelled by %s.', 'arraysubs'),
            esc_html($actor['display'])
        );

        if ('end_of_period' === $cancellation_type) {
            $message .= ' ' . __('The previously scheduled end-of-period cancellation has now taken effect.', 'arraysubs');
        } elseif ($override_scheduled) {
            // The cancellation override sentinel is set by
            // arraysubs_cancel_subscription_immediately() when the
            // subscription was already in a pending-cancellation state.
            // Spelling out the override prevents the misleading "took
            // effect immediately" wording the customer/admin sees after
            // the previous "scheduled to cancel on X" note/email.
            $message .= ' ' . __('The cancellation took effect immediately, overriding the previously scheduled end-of-period cancellation.', 'arraysubs');
        } else {
            $message .= ' ' . __('The cancellation took effect immediately.', 'arraysubs');
        }

        if (!empty($reason)) {
            $message .= ' ' . sprintf(
                /* translators: %s: cancellation reason */
                __('Reason: %s.', 'arraysubs'),
                esc_html($reason)
            );
        }

        return $message;
    }

    /**
     * Resolve actor details for status notes.
     *
     * @param int    $subscription_id     Subscription ID.
     * @param bool   $useCancellationMeta Whether to prefer stored cancellation actor metadata.
     * @param string $old_status          Previous status slug.
     * @param string $new_status          New status slug.
     * @return array{role:string,label:string,display:string}
     */
    private function getStatusActorDetails(
        int $subscription_id,
        bool $useCancellationMeta = false,
        string $old_status = '',
        string $new_status = ''
    ): array
    {
        $cancelled_by   = $useCancellationMeta ? (string) get_post_meta($subscription_id, '_cancelled_by', true) : '';
        $gateway_source = $useCancellationMeta ? (string) get_post_meta($subscription_id, '_cancellation_source', true) : '';
        $status_context = (string) get_post_meta($subscription_id, '_arraysubs_status_change_context', true);
        $current_user   = get_current_user_id();
        $customer_id    = (int) get_post_meta($subscription_id, '_customer_id', true);

        if ('new' === $old_status || in_array($status_context, ['initial_payment', 'renewal_payment', 'payment_failure'], true)) {
            if (in_array($status_context, ['initial_payment', 'renewal_payment'], true)) {
                $gateway_label = $this->getGatewayLabel($subscription_id);

                if (!empty($gateway_label)) {
                    return [
                        'role'    => 'gateway',
                        'label'   => __('Gateway', 'arraysubs'),
                        'display' => sprintf(
                            /* translators: %s: gateway name */
                            __('Gateway (%s)', 'arraysubs'),
                            $gateway_label
                        ),
                    ];
                }
            }

            return [
                'role'    => 'system',
                'label'   => __('System', 'arraysubs'),
                'display' => __('System', 'arraysubs'),
            ];
        }

        if ('gateway' === $gateway_source || 'gateway' === $cancelled_by) {
            $gateway_label = $this->getGatewayLabel($subscription_id) ?: __('Gateway', 'arraysubs');

            return [
                'role' => 'gateway',
                'label' => __('Gateway', 'arraysubs'),
                'display' => sprintf(
                    /* translators: %s: gateway name */
                    __('Gateway (%s)', 'arraysubs'),
                    $gateway_label
                ),
            ];
        }

        if (0 === strpos($cancelled_by, 'admin:')) {
            $user_id = absint(substr($cancelled_by, 6));

            return [
                'role' => 'admin',
                'label' => __('Admin', 'arraysubs'),
                'display' => sprintf(
                    /* translators: %s: admin display name */
                    __('Admin (%s)', 'arraysubs'),
                    $this->getUserDisplayName($user_id, __('Admin', 'arraysubs'))
                ),
            ];
        }

        if ('customer' === $cancelled_by) {
            return [
                'role' => 'customer',
                'label' => __('Customer', 'arraysubs'),
                'display' => sprintf(
                    /* translators: %s: customer display name */
                    __('Customer (%s)', 'arraysubs'),
                    $this->getUserDisplayName($customer_id, __('Customer', 'arraysubs'))
                ),
            ];
        }

        if ($current_user > 0) {
            $user = get_userdata($current_user);

            if ($user instanceof \WP_User && (user_can($user, 'manage_woocommerce') || user_can($user, 'manage_options'))) {
                return [
                    'role' => 'admin',
                    'label' => __('Admin', 'arraysubs'),
                    'display' => sprintf(
                        /* translators: %s: admin display name */
                        __('Admin (%s)', 'arraysubs'),
                        $user->display_name
                    ),
                ];
            }

            if ($customer_id > 0 && $customer_id === $current_user && $user instanceof \WP_User) {
                return [
                    'role' => 'customer',
                    'label' => __('Customer', 'arraysubs'),
                    'display' => sprintf(
                        /* translators: %s: customer display name */
                        __('Customer (%s)', 'arraysubs'),
                        $user->display_name
                    ),
                ];
            }
        }

        return [
            'role' => 'system',
            'label' => __('System', 'arraysubs'),
            'display' => __('System', 'arraysubs'),
        ];
    }

    /**
     * Convert a cancellation reason key to readable text.
     *
     * @param string $reason Reason key or text.
     * @return string
     */
    private function getReadableCancellationReason(string $reason): string
    {
        if (empty($reason)) {
            return '';
        }

        if (function_exists('arraysubs_get_cancellation_reason_label')) {
            return arraysubs_get_cancellation_reason_label($reason);
        }

        return ucfirst(str_replace(['_', '-'], ' ', $reason));
    }

    /**
     * Get a readable gateway label.
     *
     * @param int $subscription_id Subscription ID.
     * @return string
     */
    private function getGatewayLabel(int $subscription_id): string
    {
        $gateway = (string) get_post_meta($subscription_id, '_payment_gateway', true);

        if (empty($gateway)) {
            $gateway = (string) get_post_meta($subscription_id, '_payment_method', true);
        }

        if (empty($gateway)) {
            return '';
        }

        $labels = [
            'arraysubs_paypal' => 'PayPal',
            'arraysubs_paddle' => 'Paddle',
            'stripe'          => 'Stripe',
            'paypal'          => 'PayPal',
            'paddle'          => 'Paddle',
        ];

        if (isset($labels[$gateway])) {
            return $labels[$gateway];
        }

        $gateway = preg_replace('/^arraysubs_/', '', $gateway) ?: $gateway;

        return ucwords(str_replace(['_', '-'], ' ', $gateway));
    }

    /**
     * Get a display name for a user.
     *
     * @param int    $user_id  User ID.
     * @param string $fallback Fallback text.
     * @return string
     */
    private function getUserDisplayName(int $user_id, string $fallback): string
    {
        if ($user_id <= 0) {
            return $fallback;
        }

        $user = get_userdata($user_id);

        return $user instanceof \WP_User ? $user->display_name : $fallback;
    }
}
