<?php

/**
 * Email Manager Service
 *
 * Manages all subscription-related emails and integrates with WooCommerce.
 *
 * @package ArraySubs\Features\Emails\Services
 * @since 1.0.0
 */

namespace ArraySubs\Features\Emails\Services;

defined('ABSPATH') || exit;

use ArraySubs\Features\Emails\Emails\NewSubscriptionEmail;
use ArraySubs\Features\Emails\Emails\RenewalReminderEmail;
use ArraySubs\Features\Emails\Emails\RenewalInvoiceEmail;
use ArraySubs\Features\Emails\Emails\PaymentSuccessfulEmail;
use ArraySubs\Features\Emails\Emails\PaymentFailedEmail;
use ArraySubs\Features\Emails\Emails\SubscriptionOnHoldEmail;
use ArraySubs\Features\Emails\Emails\SubscriptionCancelledEmail;
use ArraySubs\Features\Emails\Emails\SubscriptionPendingCancellationEmail;
use ArraySubs\Features\Emails\Emails\SubscriptionExpiredEmail;
use ArraySubs\Features\Emails\Emails\SubscriptionExpiringSoonEmail;
use ArraySubs\Features\Emails\Emails\RetentionDiscountAcceptedEmail;
use ArraySubs\Features\Emails\Emails\SubscriptionReactivatedEmail;
use ArraySubs\Features\Emails\Emails\TrialStartedEmail;
use ArraySubs\Features\Emails\Emails\TrialConvertedEmail;
use ArraySubs\Features\Emails\Emails\AutoDowngradeEmail;
use ArraySubs\Features\Emails\Emails\CustomerCardExpiringEmail;
use ArraySubs\Features\Emails\Emails\RenewalRequiresVerificationEmail;
use ArraySubs\Features\PlanSwitching\Services\AutoDowngradeHandler;
use ArraySubs\Features\Emails\Emails\AdminNewSubscriptionEmail;
use ArraySubs\Features\Emails\Emails\AdminPaymentFailedEmail;
use ArraySubs\Features\Emails\Emails\AdminSubscriptionCancelledEmail;
use ArraySubs\Features\Emails\Emails\AdminSubscriptionPendingCancellationEmail;
use ArraySubs\Supports\ActionScheduler;
use ArraySubs\Supports\Config;

/**
 * Email Manager class
 */
class EmailManager
{
    /**
     * Enable auto-loading
     *
     * @var bool
     */
    public static $loadable = true;

    /**
     * Registered email classes
     *
     * @var array
     */
    private array $email_classes = [];

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->register_email_classes();

        // Register with WooCommerce
        add_filter('woocommerce_email_classes', [$this, 'add_email_classes']);
        $this->suppress_woocommerce_renewal_order_emails();

        // Enqueue placeholder helper script on WooCommerce email settings
        add_action('admin_enqueue_scripts', [$this, 'enqueue_placeholder_helper']);

        // Subscription lifecycle hooks
        add_action('arraysubs_data_status_changed', [$this, 'on_status_change'], 10, 3);
        add_action('arraysubs_data_created', [$this, 'on_subscription_created'], 10, 2);

        // Scheduled (end-of-period) cancellation hooks
        // 'arraysubs_data_waiting_cancellation' fired in: cancellation-helpers.php,
        // SubscriptionAdmin/REST/SubscriptionController.php, CancellationFlow/REST/CancellationController.php
        add_action('arraysubs_data_waiting_cancellation', [$this, 'on_waiting_cancellation'], 10, 2);

        // Reset the pending-cancel email-sent guard when a scheduled
        // cancellation is undone, so a future re-schedule can email again.
        add_action('arraysubs_data_cancellation_undone', [$this, 'on_cancellation_undone'], 10, 1);

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

        // Payment hooks
        add_action('arraysubs_order_paid', [$this, 'on_payment_complete'], 10, 2);
        add_action('arraysubs_payment_failed', [$this, 'on_payment_failed'], 10, 3);
        add_action('arraysubs_renewal_payment_failed', [$this, 'on_renewal_payment_failed'], 10, 3);
        add_action('arraysubs_gateway_payment_failed', [$this, 'on_gateway_payment_failed'], 10, 4);
        add_action('arraysubs_renewal_requires_verification', [$this, 'on_renewal_requires_verification'], 10, 4);
        add_action('arraysubs_card_expiring', [$this, 'on_card_expiring'], 10, 3);

        // Renewal payment complete (after renewal order is paid)
        // 'arraysubs_renewal_payment_complete' fired in: Subscriptions/Services/OrderIntegration::processRenewalPayment()
        add_action('arraysubs_renewal_payment_complete', [$this, 'on_renewal_payment_complete'], 10, 2);

        // Trial hooks
        // 'arraysubs_trial_started' fired in: FreeTrials/Services/TrialHandler.php
        add_action('arraysubs_trial_started', [$this, 'on_trial_started'], 10, 1);
        // 'arraysubs_trial_converted' fired in: RecurringBilling/Services/Hooks.php::processTrialConversion()
        add_action('arraysubs_trial_converted', [$this, 'on_trial_converted'], 10, 1);

        // Subscription reactivated
        // 'arraysubs_data_reactivated' fired in: Subscriptions/Services/Hooks.php::reactivateSubscription()
        add_action('arraysubs_data_reactivated', [$this, 'on_subscription_reactivated'], 10, 2);

        // Auto-downgrade completed
        // 'arraysubs_auto_downgrade_completed' fired in: PlanSwitching/Services/AutoDowngradeHandler::executeAutoDowngrade()
        add_action('arraysubs_auto_downgrade_completed', [$this, 'on_auto_downgrade_completed'], 10, 3);

        // Retention discount accepted
        // 'arraysubs_retention_offer_accepted' fired in: CancellationFlow/Services/RetentionOfferProcessor::processOffer()
        add_action('arraysubs_retention_offer_accepted', [$this, 'on_retention_offer_accepted'], 10, 3);

        // Scheduled email hooks (from Action Scheduler)
        add_action(ActionScheduler::HOOK_SEND_RENEWAL_REMINDER, [$this, 'send_renewal_reminder'], 10, 2);
        add_action(ActionScheduler::HOOK_SEND_PAYMENT_FAILED, [$this, 'send_payment_failed_email'], 10, 2);
        add_action(ActionScheduler::HOOK_SEND_EXPIRING_SOON, [$this, 'send_expiring_soon_email'], 10, 1);
    }

    /**
     * Register email classes
     */
    private function register_email_classes(): void
    {
        // Customer emails
        $this->email_classes = [
            'new_subscription'           => NewSubscriptionEmail::class,
            'renewal_reminder'           => RenewalReminderEmail::class,
            'renewal_invoice'            => RenewalInvoiceEmail::class,
            'payment_successful'         => PaymentSuccessfulEmail::class,
            'payment_failed'             => PaymentFailedEmail::class,
            'subscription_on_hold'       => SubscriptionOnHoldEmail::class,
            'subscription_pending_cancellation' => SubscriptionPendingCancellationEmail::class,
            'subscription_cancelled'     => SubscriptionCancelledEmail::class,
            'subscription_expired'       => SubscriptionExpiredEmail::class,
            'expiring_soon'              => SubscriptionExpiringSoonEmail::class,
            'auto_downgrade'             => AutoDowngradeEmail::class,
            'retention_discount_accepted' => RetentionDiscountAcceptedEmail::class,
            'subscription_reactivated'   => SubscriptionReactivatedEmail::class,
            'trial_started'              => TrialStartedEmail::class,
            'trial_converted'            => TrialConvertedEmail::class,
            'renewal_requires_verification' => RenewalRequiresVerificationEmail::class,
            'card_expiring'              => CustomerCardExpiringEmail::class,
            // Admin emails
            'admin_new_subscription'     => AdminNewSubscriptionEmail::class,
            'admin_payment_failed'       => AdminPaymentFailedEmail::class,
            'admin_subscription_pending_cancellation' => AdminSubscriptionPendingCancellationEmail::class,
            'admin_subscription_cancelled' => AdminSubscriptionCancelledEmail::class,
        ];
    }

    /**
     * Add email classes to WooCommerce
     *
     * @param array $emails Existing email classes
     * @return array
     */
    public function add_email_classes(array $emails): array
    {
        foreach ($this->email_classes as $id => $class) {
            $email_key = 'arraysubs_' . $id;
            $emails[$email_key] = new $class();
        }
        return $emails;
    }

    /**
     * Send renewal verification email.
     *
     * @param int    $subscription_id Subscription ID.
     * @param int    $order_id        Renewal order ID.
     * @param string $intent_id       Stripe PaymentIntent ID.
     * @param string $gateway         Gateway slug.
     * @return void
     */
    public function on_renewal_requires_verification(int $subscription_id, int $order_id, string $intent_id = '', string $gateway = ''): void
    {
        if (! arraysubs_is_email_enabled('renewal_requires_verification')) {
            return;
        }

        $order      = wc_get_order($order_id);
        $action_url = $order instanceof \WC_Order
            ? add_query_arg('wc-stripe-confirmation', 1, $order->get_checkout_payment_url(false))
            : (string) get_post_meta($subscription_id, '_arraysubs_payment_action_url', true);

        arraysubs_send_subscription_email(
            'renewal_requires_verification',
            $subscription_id,
            [
                'order_id'   => $order_id,
                'intent_id'  => $intent_id,
                'gateway'    => $gateway,
                'action_url' => $action_url,
            ]
        );
    }

    /**
     * Send card expiring email.
     *
     * @param int    $subscription_id Subscription ID.
     * @param array  $payload         Stripe payload.
     * @param string $gateway         Gateway slug.
     * @return void
     */
    public function on_card_expiring(int $subscription_id, array $payload = [], string $gateway = ''): void
    {
        if (! arraysubs_is_email_enabled('card_expiring')) {
            return;
        }

        arraysubs_send_subscription_email(
            'card_expiring',
            $subscription_id,
            [
                'payload' => $payload,
                'gateway' => $gateway,
            ]
        );
    }

    /**
     * Suppress generic WooCommerce customer order emails for renewal orders.
     *
     * ArraySubs sends purpose-built renewal invoice, payment successful, and
     * payment failed emails. Letting WooCommerce send its standard order
     * messages for renewal orders results in confusing/wrong customer emails.
     *
     * @return void
     */
    private function suppress_woocommerce_renewal_order_emails(): void
    {
        $email_filters = [
            'woocommerce_email_recipient_customer_processing_order',
            'woocommerce_email_recipient_customer_completed_order',
            'woocommerce_email_recipient_customer_on_hold_order',
            'woocommerce_email_recipient_customer_invoice',
            'woocommerce_email_recipient_customer_failed_order',
        ];

        foreach ($email_filters as $filter) {
            add_filter($filter, [$this, 'suppress_woocommerce_renewal_order_email'], 10, 3);
        }
    }

    /**
     * Suppress WooCommerce email recipient for ArraySubs renewal orders.
     *
     * @param string          $recipient Email recipient.
     * @param \WC_Order|mixed $order     Order object.
     * @param mixed           $email     Email object.
     * @return string Recipient or empty string.
     */
    public function suppress_woocommerce_renewal_order_email(string $recipient, $order, $email = null): string
    {
        if ($order instanceof \WC_Order && 'yes' === $order->get_meta('_is_renewal_order', true)) {
            return '';
        }

        return $recipient;
    }

    /**
     * Enqueue placeholder helper script on WooCommerce email settings page
     *
     * @param string $hook Current admin page hook
     */
    public function enqueue_placeholder_helper(string $hook): void
    {
        // Only load on WooCommerce settings page
        if ($hook !== 'woocommerce_page_wc-settings') {
            return;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading tab parameter only
        $tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading section parameter only
        $section = isset($_GET['section']) ? sanitize_text_field(wp_unslash($_GET['section'])) : '';

        // Only load on email tab with arraysubs email sections
        if ($tab !== 'email' || strpos($section, 'arraysubs_') !== 0) {
            return;
        }

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

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

    /**
     * Handle subscription status change
     *
     * @param int    $subscription_id Subscription ID
     * @param string $new_status      New status
     * @param string $old_status      Old status
     */
    public function on_status_change(int $subscription_id, string $new_status, string $old_status): void
    {
        $suppress_lifecycle_email = class_exists(AutoDowngradeHandler::class)
            && (
                AutoDowngradeHandler::shouldSuppressLifecycleEmail($subscription_id, $new_status, $old_status)
                || AutoDowngradeHandler::isProcessing()
            );
        $status_context = (string) get_post_meta($subscription_id, '_arraysubs_status_change_context', true);

        switch ($new_status) {
            case 'arraysubs-active':
                if (get_post_meta($subscription_id, '_arraysubs_renewal_activation_in_progress', true)) {
                    break;
                }

                if ('initial_payment' === $status_context && !$suppress_lifecycle_email) {
                    $this->trigger('new_subscription', $subscription_id);
                    $this->trigger('admin_new_subscription', $subscription_id);
                    $this->schedule_renewal_reminder($subscription_id);
                    break;
                }

                // New subscription activated (from pending/trial)
                if (in_array($old_status, ['arraysubs-pending', 'arraysubs-trial', 'auto-draft'], true) && !$suppress_lifecycle_email) {
                    $this->trigger('new_subscription', $subscription_id);
                    $this->trigger('admin_new_subscription', $subscription_id);

                    // Schedule renewal reminder
                    $this->schedule_renewal_reminder($subscription_id);
                }
                // Reactivated from on-hold or cancelled
                if (in_array($old_status, ['arraysubs-on-hold', 'arraysubs-cancelled'], true) && !$suppress_lifecycle_email) {
                    // arraysubs_data_reactivated handles the email and reminder after
                    // reactivation-specific billing checks finish.
                }
                break;

            case 'arraysubs-trial':
                // Trial started
                if ($old_status === 'arraysubs-pending' || $old_status === 'auto-draft') {
                    $this->trigger('trial_started', $subscription_id);
                }
                break;

            case 'arraysubs-on-hold':
                $this->trigger('subscription_on_hold', $subscription_id);
                break;

            case 'arraysubs-cancelled':
                if ($suppress_lifecycle_email) {
                    break;
                }

                $this->trigger('subscription_cancelled', $subscription_id);
                $this->trigger('admin_subscription_cancelled', $subscription_id);
                break;

            case 'arraysubs-expired':
                if ($suppress_lifecycle_email) {
                    break;
                }

                $this->trigger('subscription_expired', $subscription_id);
                break;
        }
    }

    /**
     * Handle scheduled (end-of-period) cancellation.
     *
     * Hook signature: arraysubs_data_waiting_cancellation ($subscription_id, $reason)
     *
     * Sends both the customer-facing pending-cancellation notice and an
     * admin notification, guarded by a per-scheduled-date sentinel so a
     * second fire (e.g. the scheduler retrying or an integration replaying
     * the action) does not produce duplicate emails. The sentinel is
     * cleared by on_cancellation_undone so that re-scheduling after an
     * undo will email again.
     *
     * @param int   $subscription_id Subscription ID.
     * @param mixed $reason          Cancellation reason key or post object (legacy callers).
     * @return void
     */
    public function on_waiting_cancellation(int $subscription_id, $reason = ''): void
    {
        // Suppress lifecycle emails during auto-downgrade churn.
        if (
            class_exists(AutoDowngradeHandler::class)
            && (
                AutoDowngradeHandler::shouldSuppressLifecycleEmail($subscription_id, '', '')
                || AutoDowngradeHandler::isProcessing()
            )
        ) {
            return;
        }

        $scheduled_date = (string) get_post_meta($subscription_id, '_cancellation_scheduled_date', true);
        $sent_for       = (string) get_post_meta($subscription_id, '_arraysubs_pending_cancel_email_sent_for', true);

        // Idempotency: only skip when we have a definitive scheduled date
        // and we've already sent for that exact date. Empty scheduled_date
        // falls through so degraded data still produces a notice.
        if ('' !== $scheduled_date && $sent_for === $scheduled_date) {
            return;
        }

        $this->trigger('subscription_pending_cancellation', $subscription_id);
        $this->trigger('admin_subscription_pending_cancellation', $subscription_id);

        if ('' !== $scheduled_date) {
            update_post_meta(
                $subscription_id,
                '_arraysubs_pending_cancel_email_sent_for',
                $scheduled_date
            );
        }
    }

    /**
     * Reset the pending-cancellation email-sent guard when a scheduled
     * cancellation is undone. Without this, re-scheduling for the same
     * date after an undo would silently skip the notification.
     *
     * Hook: arraysubs_data_cancellation_undone (subscription_id)
     *
     * @param int $subscription_id Subscription ID.
     * @return void
     */
    public function on_cancellation_undone(int $subscription_id): void
    {
        delete_post_meta($subscription_id, '_arraysubs_pending_cancel_email_sent_for');
    }

    /**
     * Handle new subscription created
     *
     * Fired once by Subscriptions\Services\Hooks via wp_insert_post.
     *
     * @param int   $subscription_id Subscription ID
     * @param mixed $data            Post object expected; mixed kept for backwards compatibility
     */
    public function on_subscription_created(int $subscription_id, $data): void
    {
        // Get WP_Post object - handle both WP_Post and array (backwards compat)
        if ($data instanceof \WP_Post) {
            $subscription = $data;
        } else {
            // Fallback: fetch the post if array was passed (legacy support)
            $subscription = arraysubs_get_subscription($subscription_id);
            if (!$subscription) {
                return;
            }
        }

        // Schedule renewal reminder if subscription is active
        if ($subscription->post_status === 'arraysubs-active') {
            $this->schedule_renewal_reminder($subscription_id);
        }
    }

    /**
     * Handle renewal invoice created (main flow)
     * This is fired when a renewal invoice is generated X hours before due date
     *
     * Hook: arraysubs_renewal_invoice_created (subscription_id, order_id)
     *
     * @param int $subscription_id Subscription ID
     * @param int $order_id        Order ID
     */
    public function on_renewal_invoice_created(int $subscription_id, int $order_id): void
    {
        // Automatic-payment subscriptions (e.g. Stripe with a saved payment
        // method) are charged off-session moments after the invoice order is
        // created, so a "please pay" renewal invoice email would be wrong and
        // confusing. Those customers only get the payment-success email (or a
        // payment-failed email if the auto-charge fails). Subscriptions where
        // the customer disabled auto-renew fall back to manual invoices and
        // must still receive the invoice email.
        if (
            function_exists('arraysubs_is_automatic_payment_subscription')
            && arraysubs_is_automatic_payment_subscription($subscription_id)
            && (!function_exists('arraysubs_is_auto_renew_enabled') || arraysubs_is_auto_renew_enabled($subscription_id))
        ) {
            return;
        }

        $this->trigger_renewal_invoice_once($subscription_id, $order_id);
    }

    /**
     * Handle renewal payment complete
     * This is fired when a renewal order is paid
     *
     * Hook: arraysubs_renewal_payment_complete (subscription_id, order_id)
     *
     * @param int $subscription_id Subscription ID
     * @param int $order_id        Order ID
     */
    public function on_renewal_payment_complete(int $subscription_id, int $order_id): void
    {
        $order = wc_get_order($order_id);

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

            if ($this->trigger('payment_successful', $subscription_id, ['order_id' => $order_id])) {
                $order->update_meta_data('_arraysubs_renewal_payment_success_email_sent', 'yes');
                $order->save();
            }
        } else {
            $this->trigger('payment_successful', $subscription_id, ['order_id' => $order_id]);
        }

        // Reschedule renewal reminder for next payment
        $this->schedule_renewal_reminder($subscription_id);
    }

    /**
     * Trigger the renewal invoice email once per renewal order.
     *
     * @param int $subscription_id Subscription ID.
     * @param int $order_id        Renewal order ID.
     * @return void
     */
    private function trigger_renewal_invoice_once(int $subscription_id, int $order_id): void
    {
        $order = wc_get_order($order_id);

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

            if ($this->trigger('renewal_invoice', $subscription_id, ['order_id' => $order_id])) {
                $order->update_meta_data('_arraysubs_renewal_invoice_email_sent', 'yes');
                $order->save();
            }

            return;
        }

        $this->trigger('renewal_invoice', $subscription_id, ['order_id' => $order_id]);
    }

    /**
     * Handle payment complete
     *
     * Note: This hook fires for ALL paid orders (initial and renewal).
     * For RENEWAL orders, emails are handled by on_renewal_payment_complete()
     * which listens to the more specific arraysubs_renewal_payment_complete hook.
     *
     * This method is kept for potential future use (e.g., initial order payment emails)
     * but currently does nothing to avoid duplicate emails.
     *
     * @param int       $order_id Order ID
     * @param \WC_Order $order    Order object
     */
    public function on_payment_complete(int $order_id, \WC_Order $order): void
    {
        // IMPORTANT: Do NOT send payment_successful email here for renewals.
        // Renewal payment emails are handled by on_renewal_payment_complete()
        // which listens to arraysubs_renewal_payment_complete hook.
        // Sending here would cause duplicate emails.

        // For renewal orders, just return - handled by on_renewal_payment_complete
        if ($order->get_meta('_is_renewal_order') === 'yes') {
            return;
        }

        // For initial orders, we don't currently send a payment successful email
        // because new subscription emails are triggered by status change.
        // This method is reserved for future use if we want separate payment emails.
    }

    /**
     * Handle payment failed
     *
     * Hook signature: arraysubs_payment_failed ($subscription_id, $order_id, $error_data)
     *
     * @param int   $subscription_id Subscription ID
     * @param int   $order_id        Order ID
     * @param array $error_data      Error details (optional)
     */
    public function on_payment_failed(int $subscription_id, int $order_id, array $error_data = []): void
    {
        $this->trigger('payment_failed', $subscription_id, ['order_id' => $order_id, 'error_data' => $error_data]);
        $this->trigger('admin_payment_failed', $subscription_id, ['order_id' => $order_id, 'error_data' => $error_data]);
    }

    /**
     * Handle renewal payment failed.
     *
     * Hook signature: arraysubs_renewal_payment_failed ($subscription_id, $order_id, $error_message)
     *
     * @param int    $subscription_id Subscription ID.
     * @param int    $order_id        Order ID.
     * @param string $error_message   Error message.
     * @return void
     */
    public function on_renewal_payment_failed(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 shared email handling runs once with gateway context.
        if (! empty($gateway_slug)) {
            return;
        }

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

    /**
     * Handle gateway payment failed.
     *
     * Hook signature: arraysubs_gateway_payment_failed ($subscription_id, $order_id, $error_message, $gateway_slug)
     *
     * @param int    $subscription_id Subscription ID.
     * @param int    $order_id        Order ID.
     * @param string $error_message   Error message.
     * @param string $gateway_slug    Gateway slug.
     * @return void
     */
    public function on_gateway_payment_failed(
        int $subscription_id,
        int $order_id,
        string $error_message = '',
        string $gateway_slug = ''
    ): void {
        $this->on_payment_failed(
            $subscription_id,
            $order_id,
            [
                'message' => $error_message,
                'gateway' => $gateway_slug,
                'source' => 'gateway_webhook',
            ]
        );
    }

    /**
     * Trigger an email
     *
     * @param string $email_id        Email identifier
     * @param int    $subscription_id Subscription ID
     * @param array  $args            Additional arguments
     * @return bool
     */
    public function trigger(string $email_id, int $subscription_id, array $args = []): bool
    {
        // Check if email is enabled in plugin settings
        if (!$this->is_email_enabled($email_id)) {
            return false;
        }

        $mailer = WC()->mailer();
        $email_key = 'arraysubs_' . $email_id;
        $email = $mailer->emails[$email_key] ?? null;

        if ($email) {
            $email->trigger($subscription_id, $args);
            return true;
        }

        return false;
    }

    /**
     * Check if email is enabled
     *
     * @param string $email_id Email identifier
     * @return bool
     */
    private function is_email_enabled(string $email_id): bool
    {
        // Map email IDs to setting keys
        $setting_map = [
            'new_subscription'             => 'emails.new_subscription.enabled',
            'renewal_reminder'             => 'emails.renewal_upcoming.enabled',
            'renewal_invoice'              => 'emails.renewal_invoice.enabled',
            'payment_successful'           => 'emails.renewal_payment_received.enabled',
            'payment_failed'               => 'emails.payment_failed.enabled',
            'subscription_on_hold'         => 'emails.subscription_on_hold.enabled',
            'subscription_pending_cancellation' => 'emails.subscription_pending_cancellation.enabled',
            'subscription_cancelled'       => 'emails.subscription_cancelled.enabled',
            'subscription_expired'         => 'emails.subscription_expired.enabled',
            'expiring_soon'                => 'emails.expiring_soon.enabled',
            'auto_downgrade'               => 'emails.auto_downgrade.enabled',
            'retention_discount_accepted'  => 'emails.retention_discount_accepted.enabled',
            'admin_new_subscription'       => 'emails.admin_new_subscription',
            'admin_payment_failed'         => 'emails.admin_payment_failed',
            'admin_subscription_pending_cancellation' => 'emails.admin_pending_cancellation',
            'admin_subscription_cancelled' => 'emails.admin_cancelled',
        ];

        $setting_key = $setting_map[$email_id] ?? null;

        if (!$setting_key) {
            return true; // Enable by default if no setting exists
        }

        return (bool) arraysubs_get_setting($setting_key, true);
    }

    /**
     * Schedule renewal reminder email
     *
     * @param int $subscription_id Subscription ID
     */
    public function schedule_renewal_reminder(int $subscription_id): void
    {
        $days_before = (int) arraysubs_get_setting('emails.renewal_upcoming.days_before', 3);
        $next_payment = get_post_meta($subscription_id, '_next_payment_date', true);

        if (empty($next_payment) || $days_before <= 0) {
            return;
        }

        // Calculate reminder time
        $reminder_time = strtotime("-{$days_before} days", strtotime($next_payment));

        // Only schedule if reminder time is in the future
        if ($reminder_time > time()) {
            // Unschedule any existing reminder first
            ActionScheduler::unscheduleAll(
                ActionScheduler::HOOK_SEND_RENEWAL_REMINDER,
                [$subscription_id, $days_before],
                ActionScheduler::GROUP_EMAILS
            );

            ActionScheduler::scheduleSingle(
                ActionScheduler::HOOK_SEND_RENEWAL_REMINDER,
                $reminder_time,
                [$subscription_id, $days_before],
                ActionScheduler::GROUP_EMAILS
            );
        }
    }

    /**
     * Send renewal reminder email (called by Action Scheduler)
     *
     * @param int $subscription_id Subscription ID
     * @param int $days_before     Days before renewal
     */
    public function send_renewal_reminder(int $subscription_id, int $days_before): void
    {
        // Verify subscription is still active
        $subscription = arraysubs_get_subscription($subscription_id);
        if (!$subscription || $subscription->post_status !== 'arraysubs-active') {
            return;
        }

        $next_payment = (string) get_post_meta($subscription_id, '_next_payment_date', true);

        if ($next_payment === '') {
            return;
        }

        $sent_key = sanitize_text_field($next_payment . '|' . $days_before);

        if ((string) get_post_meta($subscription_id, '_arraysubs_renewal_reminder_sent_for', true) === $sent_key) {
            return;
        }

        if ($this->trigger('renewal_reminder', $subscription_id, ['days_before' => $days_before])) {
            update_post_meta($subscription_id, '_arraysubs_renewal_reminder_sent_for', $sent_key);
            update_post_meta($subscription_id, '_arraysubs_renewal_reminder_sent_at', current_time('mysql', true));
        }
    }

    /**
     * Send payment failed email (called by Action Scheduler)
     *
     * @param int $subscription_id Subscription ID
     * @param int $order_id        Order ID
     */
    public function send_payment_failed_email(int $subscription_id, int $order_id): void
    {
        $this->trigger('payment_failed', $subscription_id, ['order_id' => $order_id]);
        $this->trigger('admin_payment_failed', $subscription_id, ['order_id' => $order_id]);
    }

    /**
     * Send expiring soon email (called by Action Scheduler)
     *
     * @param int $subscription_id Subscription ID
     */
    public function send_expiring_soon_email(int $subscription_id): void
    {
        // Check if subscription is still active and has an end date
        $subscription = arraysubs_get_subscription($subscription_id);
        if (!$subscription || !in_array($subscription->post_status, ['arraysubs-active', 'arraysubs-trial'], true)) {
            return;
        }

        $days_before = (int) arraysubs_get_setting('emails.expiring_soon.days_before', 7);
        $target_date = $this->get_expiring_soon_target_date($subscription_id);

        if ($target_date === '') {
            return;
        }

        $sent_key = sanitize_text_field($target_date . '|' . $days_before);

        if ((string) get_post_meta($subscription_id, '_arraysubs_expiring_soon_sent_for', true) === $sent_key) {
            return;
        }

        if ($this->trigger('expiring_soon', $subscription_id, ['days_before' => $days_before])) {
            update_post_meta($subscription_id, '_arraysubs_expiring_soon_sent_for', $sent_key);
            update_post_meta($subscription_id, '_arraysubs_expiring_soon_sent_at', current_time('mysql', true));
        }
    }

    /**
     * Get the date used as the expiring-soon email cycle key.
     *
     * @param int $subscription_id Subscription ID.
     * @return string UTC MySQL date, or empty string when unavailable.
     */
    private function get_expiring_soon_target_date(int $subscription_id): string
    {
        $end_date = (string) get_post_meta($subscription_id, '_end_date', true);

        if ($this->is_valid_email_date($end_date)) {
            return $end_date;
        }

        $next_payment = (string) get_post_meta($subscription_id, '_next_payment_date', true);

        return $this->is_valid_email_date($next_payment) ? $next_payment : '';
    }

    /**
     * Check whether a stored date can be used for email guards.
     *
     * @param string $date Date value.
     * @return bool
     */
    private function is_valid_email_date(string $date): bool
    {
        return '' !== $date && '0000-00-00 00:00:00' !== $date && false !== strtotime($date);
    }

    /**
     * Handle trial started
     *
     * Hook: arraysubs_trial_started
     *
     * @param int $subscription_id Subscription ID
     */
    public function on_trial_started(int $subscription_id): void
    {
        $this->trigger('trial_started', $subscription_id);
    }

    /**
     * Handle trial converted to paid subscription
     *
     * Hook: arraysubs_trial_converted
     *
     * @param int $subscription_id Subscription ID
     */
    public function on_trial_converted(int $subscription_id): void
    {
        if (
            class_exists(AutoDowngradeHandler::class)
            && AutoDowngradeHandler::shouldHandleTrialConversion($subscription_id)
        ) {
            return;
        }

        $this->trigger('trial_converted', $subscription_id);
        $this->schedule_renewal_reminder($subscription_id);
    }

    /**
     * Handle subscription reactivated
     *
     * Hook: arraysubs_data_reactivated
     *
     * @param int            $subscription_id Subscription ID
     * @param \WP_Post|mixed $subscription    Subscription post object
     */
    public function on_subscription_reactivated(int $subscription_id, $subscription): void
    {
        $this->trigger('subscription_reactivated', $subscription_id);

        // Also schedule renewal reminder for the reactivated subscription
        $this->schedule_renewal_reminder($subscription_id);
    }

    /**
     * Handle auto-downgrade completed.
     *
     * Hook: arraysubs_auto_downgrade_completed
     *
     * @param int $subscription_id Subscription ID.
     * @param int $old_product_id  Old product ID.
     * @param int $new_product_id  New product ID.
     */
    public function on_auto_downgrade_completed(int $subscription_id, int $old_product_id, int $new_product_id): void
    {
        $this->trigger('auto_downgrade', $subscription_id, [
            'old_product_id' => $old_product_id,
            'new_product_id' => $new_product_id,
        ]);
    }

    /**
     * Handle retention offer accepted.
     *
     * Hook: arraysubs_retention_offer_accepted
     *
     * @param int    $subscription_id Subscription ID.
     * @param string $offer_type      Accepted offer type.
     * @param array  $offer_data      Offer data payload.
     * @return void
     */
    public function on_retention_offer_accepted(int $subscription_id, string $offer_type, array $offer_data = []): void
    {
        if (!in_array($offer_type, ['discount', 'coupon'], true)) {
            return;
        }

        $this->trigger('retention_discount_accepted', $subscription_id, [
            'offer_type' => $offer_type,
            'offer_data' => $offer_data,
        ]);
    }
}
