<?php

namespace Licenzo\Modules\EDD;

use Licenzo\Helper\Action;
use Licenzo\Helper\Settings;
use Licenzo\Helper\LicenzoKey;
use Licenzo\Model\Customer;
use Licenzo\Model\License;
use Licenzo\Model\LicenseProduct;
use Licenzo\Model\Product;
use Licenzo\Model\Subscription;
use Licenzo\Model\SubscriptionMeta;
use Licenzo\Model\SubscriptionProduct;
use Licenzo\Model\SubscriptionSchedule;
use Licenzo\Model\Invoice;
use Licenzo\Model\InvoiceItem;
use Licenzo\Model\SubscriptionTransaction;
use Licenzo\Framework\Support\Facades\DB;
use Licenzo\Services\IdempotencyService;
use Licenzo\Services\Logging\ScheduleManager;
use Licenzo\Services\Logging\SubscriptionEvent;
use Licenzo\Services\Integrations\ProviderAliasService;

if (!defined('ABSPATH')) {
    exit;
}

class EddOrderService
{
    /**
     * @var IdempotencyService
     */
    private $idempotencyService;

    public function __construct(?IdempotencyService $idempotencyService = null)
    {
        $this->idempotencyService = $idempotencyService ?? new IdempotencyService();
    }

    /**
     * Process an EDD order and create Licenzo entities.
     *
     * EDD fires edd_complete_purchase only when payment is confirmed,
     * so subscriptions are created directly in STATUS_ACTIVE.
     */
    public function process(int $paymentId): void
    {

        $order = edd_get_order($paymentId);
        if (!$order) {
            return;
        }

        if (static::isLicenzoRenewalOrder($order)) {
            return;
        }

        $customer = $this->findOrCreateCustomer($order);
        if (!$customer) {
            return;
        }

        $orderItems = edd_get_order_items(['order_id' => $order->id, 'number' => 999])
            ?: [];

        foreach ($orderItems as $orderItem) {
            $downloadId = (int)$orderItem->product_id;
            $priceId = $orderItem->price_id !== null ? (int)$orderItem->price_id : null;

            $product = $this->resolveProduct($downloadId, $priceId);
            if (!$product) {
                continue;
            }

            $variation = $priceId !== null
                ? $product->variations->firstWhere('provider_id', (string)$priceId)
                : $product->variations->first();

            if (!$variation) {
                continue;
            }

            $eventKey = $this->buildOrderItemEventKey($order->id, $orderItem);
            $checksum = $this->buildOrderItemChecksum($order->id, $orderItem);

            $acquired = $this->idempotencyService->acquire(
                SubscriptionTransaction::PROVIDER_EDD,
                'edd_order_item',
                $eventKey,
                null,
                $customer->id,
                $checksum,
                gmdate('Y-m-d H:i:s', strtotime('+30 days'))
            );

            if (!$acquired) {
                $this->addOrderNote($order, sprintf(
                    'Licenzo skipped duplicate order item processing (order_id=%d, item_id=%d).',
                    $order->id,
                    (int)$orderItem->id
                ));
                continue;
            }

            try {
                // Every edd_complete_purchase is treated as a net-new purchase.
                // Renewal charges for EDD never fire edd_complete_purchase — they
                // go through SubscriptionRenewalService + EddStripeRenewalPaymentService
                // directly against Stripe's PaymentIntent API. Auto-collapsing a new
                // order into a renewal would silently eat legitimate re-purchases
                // (team seats, gifts, post-cancel replays) and fail to issue the
                // license the customer just paid for. Explicit manual-renewal UX
                // is tracked for v1.2+.
                $this->processNewPurchase($order, $orderItem, $customer, $product, $variation);

                $this->idempotencyService->markProcessed(
                    SubscriptionTransaction::PROVIDER_EDD,
                    'edd_order_item',
                    $eventKey
                );
            } catch (\Throwable $e) {
                $this->idempotencyService->markFailed(
                    SubscriptionTransaction::PROVIDER_EDD,
                    'edd_order_item',
                    $eventKey
                );
                $this->addOrderNote($order, sprintf(
                    'Licenzo failed processing order item (order_id=%d, item_id=%d): %s',
                    $order->id,
                    (int)$orderItem->id,
                    $e->getMessage()
                ));
            }
        }
    }

    /**
     * Sync linked Licenzo subscriptions when EDD order status changes.
     */
    public function syncOrderStatus(int $orderId, string $fromStatus, string $toStatus): void
    {
        if ($orderId <= 0) {
            return;
        }

        $order = edd_get_order($orderId);
        if (!$order) {
            return;
        }

        $subscriptionIds = $this->extractLinkedSubscriptionIdsFromOrder($order);
        if (empty($subscriptionIds)) {
            return;
        }

        $isFullRefund = $toStatus === 'refunded';
        $isPartialRefund = $toStatus === 'partially_refunded';

        if ($isFullRefund || $isPartialRefund) {
            $this->processRefundForOrder($order, $subscriptionIds, $fromStatus, $toStatus, $isFullRefund);

            // Partial refunds do not change subscription or license status.
            // Full refunds fall through to the status mapper so the
            // subscription is canceled + WP role is synced via the shared
            // code path below.
            if ($isPartialRefund) {
                return;
            }
        }

        $mappedStatus = $this->mapEddOrderStatusToSubscriptionStatus($toStatus);
        if ($mappedStatus === null) {
            return;
        }

        foreach ($subscriptionIds as $subscriptionId) {
            $eventScope = 'edd_order_status_sync';
            $eventKey = sprintf('%d:%d:%s:%s', $orderId, $subscriptionId, $fromStatus, $toStatus);

            $reserved = $this->idempotencyService->acquire(
                SubscriptionTransaction::PROVIDER_EDD,
                $eventScope,
                $eventKey,
                $subscriptionId,
                null,
                hash('sha256', $orderId . ':' . $fromStatus . ':' . $toStatus . ':' . $subscriptionId),
                gmdate('Y-m-d H:i:s', strtotime('+30 days'))
            );

            if (!$reserved) {
                continue;
            }

            try {
                $subscription = Subscription::find($subscriptionId);
                if (!$subscription) {
                    $this->idempotencyService->markFailed(SubscriptionTransaction::PROVIDER_EDD, $eventScope, $eventKey);
                    continue;
                }

                $currentStatus = (string)$subscription->status;
                if ($currentStatus !== $mappedStatus) {
                    $changes = ['from' => $currentStatus, 'to' => $mappedStatus];

                    DB::transaction(function () use ($subscription, $mappedStatus, $changes, $orderId, $fromStatus, $toStatus) {
                        $subscription->status = $mappedStatus;
                        if ($mappedStatus === Subscription::STATUS_CANCELED) {
                            $subscription->canceled_at = gmdate('Y-m-d H:i:s');
                        }
                        if ($mappedStatus === Subscription::STATUS_ACTIVE && in_array($subscription->getOriginal('status'), [
                                Subscription::STATUS_CANCELED,
                                Subscription::STATUS_PAUSED,
                                Subscription::STATUS_PENDING,
                            ], true)) {
                            $subscription->restored_at = gmdate('Y-m-d H:i:s');
                        }
                        $subscription->save();

                        $this->syncLicensesForSubscriptionStatus($subscription, $mappedStatus);
                        $this->syncWordPressUserRoleForSubscription($subscription, $mappedStatus);

                        SubscriptionEvent::log($subscription, 'status_changed', null, [
                            'source'  => 'edd',
                            'changes' => $changes,
                            'data'    => [
                                'order_id'    => $orderId,
                                'from_status' => $fromStatus,
                                'to_status'   => $toStatus,
                            ],
                        ]);
                    });
                }

                $this->idempotencyService->markProcessed(SubscriptionTransaction::PROVIDER_EDD, $eventScope, $eventKey);
            } catch (\Throwable $e) {
                $this->idempotencyService->markFailed(SubscriptionTransaction::PROVIDER_EDD, $eventScope, $eventKey);
                $this->addOrderNote($order, sprintf(
                    'Licenzo failed syncing order status to subscription (order_id=%d, subscription_id=%d): %s',
                    $orderId,
                    $subscriptionId,
                    $e->getMessage()
                ));
            }
        }
    }

    private function processNewPurchase($order, $orderItem, Customer $customer, Product $product, $variation): void
    {
        $gatewaySlug = (string)$order->gateway;
        $isRecurringCycle = !in_array($variation->billing_cycle, ['onetime', 'lifetime'], true);
        $isGatewayRecurringEligible = EddGatewayPolicy::isRecurringEligible($gatewaySlug);
        $treatAsSingleCycle = $isRecurringCycle && !$isGatewayRecurringEligible;

        DB::transaction(function () use ($order, $orderItem, $customer, $product, $variation, $gatewaySlug, $isRecurringCycle, $treatAsSingleCycle) {
            $pricing = $this->resolveEddOrderItemPricing($order, $orderItem, $variation);
            $unitPrice = $pricing['unit_price'];
            $lineTotal = $pricing['line_total'];
            $nextBillingDate = $this->getNextBillingDate($variation->billing_cycle);

            $subscription = Subscription::create([
                'uuid'                     => wp_generate_uuid4(),
                'customer_id'              => $customer->id,
                'parent_order_id'          => $order->id,
                'provider'                 => SubscriptionTransaction::PROVIDER_EDD,
                'provider_id'              => (string)$order->id,
                'subscription_name'        => "Subscription for {$variation->name}",
                'currency'                 => strtoupper((string)$order->currency),
                'billing_cycle'            => $variation->billing_cycle,
                // Non-Stripe EDD gateways cannot be auto-renewed by Licenzo
                // (see EddGatewayPolicy). We treat those orders as single-
                // cycle: the customer keeps the paid period of access, but
                // we do not set a recurring amount so dashboards and
                // dunning do not report a next-charge expectation that
                // will never fire.
                'recurring_amount'         => ($treatAsSingleCycle || !$isRecurringCycle) ? 0 : $lineTotal,
                'billing_total_cycles'     => 0,
                'bill_count'               => 1,
                'trial_days'               => $variation->trial_days,
                'trial_ends_at'            => $variation->trial_days > 0 ? gmdate('Y-m-d', strtotime("+{$variation->trial_days} days")) : null,
                'status'                   => Subscription::STATUS_ACTIVE,
                'next_billing_date'        => $nextBillingDate,
                'expire_at'                => $nextBillingDate,
                'total_paid'               => $lineTotal,
                'amount'                   => $lineTotal,
                'collection_method'        => 'charge_automatically',
                'current_payment_method'   => (string)$order->gateway,
                'provider_plan_id'         => $variation->id,
                'provider_subscription_id' => 'edd_order_' . $order->id . '_' . $orderItem->id,
                'provider_response'        => [
                    'order_id' => (int)$order->id,
                    'item_id'  => (int)$orderItem->id,
                ],
                'config'                   => null,
                'last_payment_attempt'     => gmdate('Y-m-d H:i:s'),
                'next_payment_attempt'     => gmdate('Y-m-d H:i:s', strtotime('+1 day')),
                'dunning_status'           => 'active',
            ]);

            $this->markEddOrderLinks($order, $orderItem, (int)$subscription->id);
            $this->persistProviderMetadataForSubscription($subscription, $order, $orderItem);

            SubscriptionProduct::create([
                'subscription_id' => $subscription->id,
                'product_id'      => $product->id,
                'variation_id'    => $variation->id,
                'provider'        => $product->provider,
                'provider_id'     => $variation->provider_id ?: $product->provider_id,
                'product_name'    => $product->name,
                'variation_name'  => $variation->name,
                'currency'        => strtoupper((string)$order->currency),
                'price'           => $unitPrice,
                'quantity'        => (int)$orderItem->quantity,
                'product_data'    => $product->toArray(),
                'variation_data'  => $variation->toArray(),
            ]);

            SubscriptionEvent::log($subscription, 'subscription_created', $customer, [
                'source' => 'EDD Order ID: ' . $order->id,
            ]);

            $this->createOrGetOrderTransaction([
                'uuid'                => wp_generate_uuid4(),
                'provider'            => SubscriptionTransaction::PROVIDER_EDD,
                'provider_id'         => $order->id,
                'provider_charge_id'  => 'edd_order_' . $order->id . '_' . $orderItem->id,
                'order_id'            => $order->id,
                'order_type'          => SubscriptionTransaction::PROVIDER_EDD,
                'subscription_id'     => $subscription->id,
                'customer_id'         => $customer->id,
                'total'               => $lineTotal,
                'status'              => SubscriptionTransaction::STATUS_SUCCESS,
                'transaction_type'    => SubscriptionTransaction::TYPE_PURCHASE,
                'card_last_4'         => null,
                'card_brand'          => null,
                'payment_method'      => (string)$order->gateway,
                'payment_mode'        => $this->resolveEddPaymentMode($order),
                'payment_method_type' => null,
                'currency'            => strtoupper((string)$order->currency),
                'meta'                => [
                    'order_id' => (int)$order->id,
                    'item_id'  => (int)$orderItem->id,
                ],
            ]);

            $this->createInitialInvoice($subscription, $order, $orderItem, $product, $variation, $lineTotal, $customer);

            $license = License::create([
                'uuid'             => wp_generate_uuid4(),
                'license_key'      => LicenzoKey::generate(),
                'customer_id'      => $customer->id,
                'subscription_id'  => $subscription->id,
                'status'           => 'active',
                'activation_count' => 0,
                'activation_limit' => $variation->max_activations,
                'expire_date'      => $nextBillingDate,
                'support_till'     => gmdate('Y-m-d', strtotime("+{$variation->support_for} {$variation->support_for_unit}")),
                'activated_at'     => gmdate('Y-m-d H:i:s'),
                'config'           => null,
            ]);

            LicenseProduct::create([
                'license_id'       => $license->id,
                'product_id'       => $product->id,
                'variation_id'     => $variation->id,
                'product_name'     => $product->name,
                'variation_name'   => $variation->name,
                'started_at'       => gmdate('Y-m-d'),
                'expire_date'      => $nextBillingDate,
                'activation_count' => 0,
            ]);

            $this->syncWordPressUserRoleForCustomer($customer, Subscription::STATUS_ACTIVE);

            if ($treatAsSingleCycle) {
                SubscriptionMeta::setMeta($subscription->id, 'edd_single_cycle', '1');
                SubscriptionMeta::setMeta($subscription->id, 'edd_single_cycle_reason', 'non_stripe_gateway');
                SubscriptionMeta::setMeta($subscription->id, 'edd_single_cycle_gateway', EddGatewayPolicy::normalizeSlug($gatewaySlug));

                SubscriptionEvent::log($subscription, EddGatewayPolicy::EVENT_SINGLE_CYCLE, $customer, [
                    'source'          => 'EDD Order ID: ' . $order->id,
                    'gateway'         => EddGatewayPolicy::normalizeSlug($gatewaySlug),
                    'billing_cycle'   => (string)$variation->billing_cycle,
                    'reason'          => 'Licenzo can only auto-renew Stripe-backed EDD subscriptions on v1.1.0.',
                    'customer_action' => 'Subscription entitlement is valid until ' . (string)$nextBillingDate . '; after that the customer must re-purchase.',
                ]);

                $this->addOrderNote($order, sprintf(
                    'Licenzo: order gateway "%s" is not recurring-eligible. Subscription #%d is marked single-cycle — license access runs until %s, no auto-renewal will be attempted.',
                    EddGatewayPolicy::normalizeSlug($gatewaySlug),
                    (int)$subscription->id,
                    (string)$nextBillingDate
                ));

                return;
            }

            $autoRenewEnabled = strtolower((string) (Settings::getSubscriptionSettings()['stripe_auto_renew'] ?? 'no')) === 'yes';

            if ($nextBillingDate !== null && $isRecurringCycle && $autoRenewEnabled) {
                $stripeSubCreated = false;
                $stripeCustomerId = (string)($subscription->provider_customer_id ?? '');
                $paymentMethodId = (string)SubscriptionMeta::getMeta($subscription->id, 'edd_stripe_payment_method_id');

                if ($stripeCustomerId !== '' && $paymentMethodId !== '') {
                    $stripeSubId = EddStripeSubscriptionService::createForSubscription(
                        $subscription->fresh()->load('subscriptionProducts'),
                        $variation,
                        $stripeCustomerId,
                        $paymentMethodId,
                        $nextBillingDate
                    );

                    if ($stripeSubId) {
                        $subscription->provider_subscription_id = $stripeSubId;
                        $subscription->save();
                        $stripeSubCreated = true;
                    }
                }

                if (!$stripeSubCreated) {
                    $scheduled = ScheduleManager::schedule(
                        $subscription,
                        SubscriptionSchedule::ACTION_RENEWAL_CHARGE,
                        $nextBillingDate,
                        [
                            'provider'        => SubscriptionTransaction::PROVIDER_EDD,
                            'subscription_id' => $subscription->id,
                            'variation_id'    => $variation->id,
                        ],
                        'Auto-scheduled renewal at purchase'
                    );

                    if ($scheduled === null) {
                        SubscriptionEvent::log($subscription, 'renewal_schedule_failed', $customer, [
                            'source' => 'processNewPurchase — ScheduleManager returned null',
                        ]);
                    }
                }
            }
        });
    }

    /**
     * Detect orders created by EddRenewalOrderService to prevent re-processing
     * them as new purchases when EDD fires edd_complete_purchase.
     */
    private static function isLicenzoRenewalOrder($order): bool
    {
        return edd_get_order_meta($order->id, '_licenzo_renewal', true) === '1';
    }

    private function findOrCreateCustomer($order): ?Customer
    {
        $email = (string)$order->email;
        if ($email === '') {
            return null;
        }

        $wpUserId = (int)$order->user_id;

        $matchedByEmail = get_user_by('email', $email);
        if ($matchedByEmail && isset($matchedByEmail->ID)) {
            $wpUserId = (int)$matchedByEmail->ID;
        }

        $customer = null;
        $createdCustomer = false;

        if ($wpUserId) {
            $customer = Customer::where('user_id', $wpUserId)->first();
        }

        if (!$customer) {
            $customer = Customer::where('email', $email)->first();
        }

        $eddCustomer = edd_get_customer_by('email', $email) ?: null;
        $firstName = '';
        $lastName = '';
        if ($eddCustomer && !empty($eddCustomer->name)) {
            $nameParts = explode(' ', (string)$eddCustomer->name, 2);
            $firstName = $nameParts[0] ?? '';
            $lastName = $nameParts[1] ?? '';
        }

        if ($customer) {
            $customer->email = $email;
            $customer->first_name = $firstName;
            $customer->last_name = $lastName;
            $customer->ip = (string)$order->ip;
        } else {
            $customer = Customer::create([
                'provider'    => Product::PROVIDER_EDD,
                'provider_id' => $eddCustomer ? (string)$eddCustomer->id : null,
                'email'       => $email,
                'first_name'  => $firstName,
                'last_name'   => $lastName,
                'ip'          => (string)$order->ip,
            ]);
            $createdCustomer = true;
        }

        if ($wpUserId && (int)($customer->user_id ?? 0) !== $wpUserId) {
            $customer->user_id = $wpUserId;
        }

        if ($eddCustomer && $eddCustomer->id) {
            $customer->provider = Product::PROVIDER_EDD;
            $customer->provider_id = (string)$eddCustomer->id;
        }

        if ($customer->isDirty()) {
            $customer->save();
        }

        if ($createdCustomer) {
            Action::do('notification_customer_welcome', [
                'source'   => 'edd_order_customer_create',
                'customer' => (array)$customer->toArray(),
                'meta'     => [
                    'order_id' => (int)$order->id,
                ],
            ]);
        }

        return $customer;
    }

    private function resolveProduct(int $downloadId, ?int $priceId): ?Product
    {
        return Product::whereIn('provider', ProviderAliasService::eddAliases())
            ->where(function ($query) use ($downloadId, $priceId) {
                $query->where('provider_id', (string)$downloadId);
                if ($priceId !== null) {
                    $query->orWhereHas('variations', function ($vq) use ($priceId) {
                        $vq->where('provider_id', (string)$priceId);
                    });
                }
            })
            ->with('variations')
            ->first()
        ;
    }

    /**
     * Resolve paid amounts from EDD order item as dollar floats.
     *
     * EDD stores amounts as decimal(18,9). MoneyCast on model fields handles
     * the conversion from dollars to integer cents — do NOT pre-convert here.
     *
     * @return array{unit_price:float,line_total:float}
     */
    private function resolveEddOrderItemPricing($order, $orderItem, $variation): array
    {
        $quantity = max(1, (int)$orderItem->quantity);
        $lineTotal = (float)$orderItem->total;

        if ($lineTotal <= 0) {
            $fallbackUnit = (float)($variation->sale_price ?: $variation->regular_price ?: 0);
            $lineTotal = $fallbackUnit * $quantity;
        }

        $unitPrice = $lineTotal / $quantity;

        return [
            'unit_price' => (float)$unitPrice,
            'line_total' => (float)$lineTotal,
        ];
    }

    private function resolveEddPaymentMode($order): string
    {
        $mode = isset($order->mode) ? (string)$order->mode : '';

        if ($mode === 'test' || $mode === 'sandbox') {
            return SubscriptionTransaction::PAYMENT_MODE_TEST;
        }

        return SubscriptionTransaction::PAYMENT_MODE_LIVE;
    }

    private function cancelStaleRenewalSchedules(Subscription $subscription): void
    {
        $pending = ScheduleManager::getPending($subscription);

        foreach ($pending as $schedule) {
            if (($schedule->action_type ?? '') === SubscriptionSchedule::ACTION_RENEWAL_CHARGE) {
                ScheduleManager::cancel($schedule);
            }
        }
    }

    private function getNextBillingDate(string $cycle): ?string
    {
        switch ($cycle) {
            case 'weekly':
                return gmdate('Y-m-d', strtotime('+1 week'));
            case 'monthly':
                return gmdate('Y-m-d', strtotime('+1 month'));
            case 'yearly':
                return gmdate('Y-m-d', strtotime('+1 year'));
            case 'onetime':
            case 'lifetime':
            default:
                return null;
        }
    }

    private function createInitialInvoice(
        Subscription $subscription,
                     $order,
                     $orderItem,
        Product      $product,
                     $variation,
        float        $lineTotal,
        Customer     $customer
    ): void
    {
        $currency = strtoupper((string)$order->currency);
        $now = gmdate('Y-m-d H:i:s');

        $invoice = Invoice::create([
            'uuid'                => wp_generate_uuid4(),
            'subscription_id'     => $subscription->id,
            'customer_id'         => $customer->id,
            'provider'            => 'edd',
            'provider_invoice_id' => 'edd_order_' . $order->id,
            'invoice_number'      => 'INV-' . $subscription->id . '-1',
            'billing_cycle_no'    => 1,
            'collection_method'   => Invoice::COLLECTION_CHARGE_AUTOMATICALLY,
            'status'              => Invoice::STATUS_PAID,
            'currency'            => $currency,
            'subtotal'            => $lineTotal,
            'tax_total'           => 0,
            'total'               => $lineTotal,
            'amount_due'          => 0,
            'amount_paid'         => $lineTotal,
            'period_start'        => $now,
            'period_end'          => $subscription->next_billing_date
                ? (string)$subscription->next_billing_date
                : null,
            'paid_at'             => $now,
            'meta'                => [
                'edd_order_id' => (int)$order->id,
                'edd_item_id'  => (int)$orderItem->id,
            ],
        ]);

        InvoiceItem::create([
            'invoice_id'     => $invoice->id,
            'line_type'      => InvoiceItem::LINE_SUBSCRIPTION,
            'reference_type' => 'subscription_product',
            'reference_id'   => null,
            'description'    => sprintf('%s — %s', $product->name, $variation->name),
            'quantity'       => (int)$orderItem->quantity,
            'unit_amount'    => $lineTotal / max(1, (int)$orderItem->quantity),
            'line_total'     => $lineTotal,
            'currency'       => $currency,
        ]);
    }

    private function persistProviderMetadataForSubscription(Subscription $subscription, $order, $orderItem): void
    {
        $paymentMethod = (string)$order->gateway;

        if ($paymentMethod !== '') {
            $subscription->current_payment_method = $paymentMethod;
        }

        $stripeCustomerId = $this->extractEddStripeMeta($order, '_edds_stripe_customer_id');
        if ($stripeCustomerId !== '') {
            $subscription->provider_customer_id = $stripeCustomerId;
            $subscription->collection_method = Subscription::COLLECTION_CHARGE_AUTOMATICALLY;
        }

        $stripePaymentIntentId = $this->extractEddStripeMeta($order, '_edds_stripe_payment_intent_id');
        $stripePaymentMethodId = $this->resolveStripePaymentMethodFromIntent($stripePaymentIntentId);

        $providerResponse = is_array($subscription->provider_response) ? $subscription->provider_response : [];
        $providerResponse['edd'] = [
            'order_id'              => (int)$order->id,
            'item_id'               => (int)$orderItem->id,
            'gateway'               => $paymentMethod,
            'currency'              => strtoupper((string)$order->currency),
            'mode'                  => (string)($order->mode ?? ''),
            'stripe_customer_id'    => $stripeCustomerId,
            'stripe_payment_intent' => $stripePaymentIntentId,
            'stripe_payment_method' => $stripePaymentMethodId,
        ];
        $subscription->provider_response = $providerResponse;
        $subscription->save();

        SubscriptionMeta::setMeta($subscription->id, 'edd_source_order_id', (int)$order->id);
        SubscriptionMeta::setMeta($subscription->id, 'edd_last_order_item_id', (int)$orderItem->id);
        SubscriptionMeta::setMeta($subscription->id, 'edd_gateway', $paymentMethod);

        if ($stripePaymentMethodId !== '') {
            SubscriptionMeta::setMeta($subscription->id, 'edd_stripe_payment_method_id', $stripePaymentMethodId);
        }
    }

    /**
     * Retrieve the payment method ID from a confirmed PaymentIntent via Stripe API.
     *
     * EDD Stripe does not store pm_xxx in order meta, so we retrieve it
     * from the PaymentIntent object which has the payment_method attached
     * after setup_future_usage=off_session confirmation.
     */
    private function resolveStripePaymentMethodFromIntent(string $intentId): string
    {
        try {
            $intent = edds_api_request('PaymentIntent', 'retrieve', $intentId);
            if (is_object($intent) && !empty($intent->payment_method)) {
                return (string)$intent->payment_method;
            }
        } catch (\Throwable $e) {
            // Non-fatal — renewal will fall back to listing customer payment methods
        }

        return '';
    }

    private function extractEddStripeMeta($order, string $metaKey): string
    {
        return (string)edd_get_order_meta($order->id, $metaKey, true);
    }

    private function markEddOrderLinks($order, $orderItem, int $subscriptionId): void
    {


        edd_update_order_meta($order->id, '_licenzo_subscription_id', $subscriptionId);

        $existingIds = edd_get_order_meta($order->id, '_licenzo_subscription_ids', true);
        if (!is_array($existingIds)) {
            $existingIds = [];
        }
        $existingIds[] = $subscriptionId;
        $existingIds = array_values(array_unique(array_map('intval', $existingIds)));
        edd_update_order_meta($order->id, '_licenzo_subscription_ids', $existingIds);

        edd_update_order_item_meta($orderItem->id, '_licenzo_subscription_id', $subscriptionId);
    }

    private function extractLinkedSubscriptionIdsFromOrder($order): array
    {
        $ids = [];

        $single = (int)edd_get_order_meta($order->id, '_licenzo_subscription_id', true);

        if ($single > 0) {
            $ids[] = $single;
        }

        $multi = edd_get_order_meta($order->id, '_licenzo_subscription_ids', true);

        if (is_array($multi)) {
            foreach ($multi as $id) {
                $id = (int)$id;
                if ($id > 0) {
                    $ids[] = $id;
                }
            }
        }

        $ids = array_values(array_unique(array_filter(array_map('intval', $ids))));
        sort($ids);

        return $ids;
    }

    /**
     * Refund processing entry point for an EDD order transition to
     * `refunded` or `partially_refunded`. Writes a REFUND row to the
     * SubscriptionTransaction ledger for each affected subscription,
     * adjusts the subscription's `total_paid` aggregate, and — for full
     * refunds only — revokes licenses and cancels pending renewal
     * schedules. The outer `syncOrderStatus()` caller handles the
     * subscription-status transition for full refunds; partial refunds
     * leave the subscription status untouched.
     */
    private function processRefundForOrder($order, array $subscriptionIds, string $fromStatus, string $toStatus, bool $isFullRefund): void
    {
        $isMultiSubscription = count($subscriptionIds) > 1;

        foreach ($subscriptionIds as $subscriptionId) {
            $this->processRefundForSubscription(
                $order,
                (int)$subscriptionId,
                $fromStatus,
                $toStatus,
                $isFullRefund,
                $isMultiSubscription
            );
        }
    }

    private function processRefundForSubscription($order, int $subscriptionId, string $fromStatus, string $toStatus, bool $isFullRefund, bool $isMultiSubscription): void
    {
        $eventScope = $isFullRefund ? 'edd_refund_full' : 'edd_refund_partial';
        $eventKey = sprintf('%d:%d:%s', (int)$order->id, $subscriptionId, $toStatus);
        $chargeKey = sprintf('edd_refund_%d_%d', (int)$order->id, $subscriptionId);

        $reserved = $this->idempotencyService->acquire(
            SubscriptionTransaction::PROVIDER_EDD,
            $eventScope,
            $eventKey,
            $subscriptionId,
            null,
            hash('sha256', $chargeKey . ':' . $fromStatus . ':' . $toStatus),
            gmdate('Y-m-d H:i:s', strtotime('+30 days'))
        );

        if (!$reserved) {
            return;
        }

        try {
            $subscription = Subscription::find($subscriptionId);
            if (!$subscription) {
                $this->idempotencyService->markFailed(SubscriptionTransaction::PROVIDER_EDD, $eventScope, $eventKey);
                return;
            }

            $originalTransaction = SubscriptionTransaction::where('provider', SubscriptionTransaction::PROVIDER_EDD)
                ->where('order_id', (int)$order->id)
                ->where('subscription_id', $subscription->id)
                ->whereIn('transaction_type', [SubscriptionTransaction::TYPE_PURCHASE, SubscriptionTransaction::TYPE_RENEWAL])
                ->where('status', SubscriptionTransaction::STATUS_SUCCESS)
                ->orderBy('id', 'desc')
                ->first()
            ;

            $refundAmount = $this->resolveEddRefundAmount($order, $subscription, $originalTransaction, $isFullRefund, $isMultiSubscription);

            if ($refundAmount === null) {
                SubscriptionEvent::log($subscription, 'refund_reconciliation_needed', null, [
                    'source'         => 'EDD Order ID: ' . (int)$order->id,
                    'from_status'    => $fromStatus,
                    'to_status'      => $toStatus,
                    'reason'         => $isMultiSubscription
                        ? 'Partial refund on an order linked to multiple subscriptions — Licenzo cannot allocate the refund amount deterministically.'
                        : 'Could not resolve refund amount from EDD order data.',
                    'is_full_refund' => $isFullRefund,
                ]);

                $this->addOrderNote($order, sprintf(
                    'Licenzo: refund on order #%d needs manual reconciliation for subscription #%d (%s). No ledger row written.',
                    (int)$order->id,
                    $subscription->id,
                    $isMultiSubscription ? 'multi-subscription partial refund' : 'refund amount unresolved'
                ));

                $this->idempotencyService->markProcessed(SubscriptionTransaction::PROVIDER_EDD, $eventScope, $eventKey);
                return;
            }

            if ($refundAmount <= 0) {
                // No new refund to record — already fully reconciled.
                $this->idempotencyService->markProcessed(SubscriptionTransaction::PROVIDER_EDD, $eventScope, $eventKey);
                return;
            }

            DB::transaction(function () use ($order, $subscription, $originalTransaction, $refundAmount, $isFullRefund, $chargeKey, $fromStatus, $toStatus) {
                $this->createOrGetOrderTransaction([
                    'uuid'               => wp_generate_uuid4(),
                    'provider'           => SubscriptionTransaction::PROVIDER_EDD,
                    'provider_id'        => (int)$order->id,
                    'provider_charge_id' => $chargeKey,
                    'order_id'           => (int)$order->id,
                    'order_type'         => SubscriptionTransaction::PROVIDER_EDD,
                    'subscription_id'    => $subscription->id,
                    'customer_id'        => $subscription->customer_id,
                    'total'              => $refundAmount,
                    'status'             => SubscriptionTransaction::STATUS_SUCCESS,
                    'transaction_type'   => SubscriptionTransaction::TYPE_REFUND,
                    'payment_method'     => (string)$order->gateway,
                    'payment_mode'       => $this->resolveEddPaymentMode($order),
                    'currency'           => strtoupper((string)$order->currency),
                    'meta'               => [
                        'order_id'                => (int)$order->id,
                        'is_full_refund'          => $isFullRefund,
                        'original_transaction_id' => $originalTransaction->id ?? null,
                        'from_status'             => $fromStatus,
                        'to_status'               => $toStatus,
                    ],
                ]);

                $subscription->total_paid = max(0.0, (float)$subscription->total_paid - $refundAmount);
                $subscription->save();

                if ($isFullRefund) {
                    if ($originalTransaction) {
                        $originalTransaction->status = SubscriptionTransaction::STATUS_REFUNDED;
                        $originalTransaction->save();
                    }

                    $licenses = License::where('subscription_id', $subscription->id)->get();
                    foreach ($licenses as $license) {
                        $license->status = License::STATUS_REVOKED;
                        $license->save();
                    }

                    SubscriptionSchedule::where('subscription_id', $subscription->id)
                        ->where('status', SubscriptionSchedule::STATUS_PENDING)
                        ->get()
                        ->each(function ($schedule) {
                            $schedule->markAsCanceled();
                        })
                    ;
                }

                SubscriptionEvent::log(
                    $subscription,
                    $isFullRefund ? 'subscription_refunded_full' : 'subscription_refunded_partial',
                    null,
                    [
                        'source'                  => 'EDD Order ID: ' . (int)$order->id,
                        'refund_amount'           => $refundAmount,
                        'currency'                => strtoupper((string)$order->currency),
                        'original_transaction_id' => $originalTransaction->id ?? null,
                        'from_status'             => $fromStatus,
                        'to_status'               => $toStatus,
                    ]
                );
            });

            $this->addOrderNote($order, sprintf(
                'Licenzo recorded %s refund of %s %s on subscription #%d%s.',
                $isFullRefund ? 'full' : 'partial',
                number_format((float)$refundAmount, 2),
                strtoupper((string)$order->currency),
                $subscription->id,
                $isFullRefund ? ' (licenses revoked, pending renewals canceled)' : ''
            ));

            $this->idempotencyService->markProcessed(SubscriptionTransaction::PROVIDER_EDD, $eventScope, $eventKey);
        } catch (\Throwable $e) {
            $this->idempotencyService->markFailed(SubscriptionTransaction::PROVIDER_EDD, $eventScope, $eventKey);
            $this->addOrderNote($order, sprintf(
                'Licenzo failed processing refund (order_id=%d, subscription_id=%d): %s',
                (int)$order->id,
                $subscriptionId,
                $e->getMessage()
            ));
        }
    }

    /**
     * Returns the refund amount in dollars (matching MoneyCast input
     * semantics) or null when the amount cannot be deterministically
     * resolved and a manual reconciliation event should be logged.
     *
     *  - Full refund, single or multi subscription: the original
     *    transaction total (each subscription's original purchase is
     *    fully reversed on the ledger).
     *  - Partial refund, single subscription: EDD's reported refunded
     *    amount minus the sum of already-recorded REFUND rows on this
     *    order. Returns 0 (harmless no-op) when already reconciled.
     *  - Partial refund, multi subscription: returns null (needs
     *    manual reconciliation — Licenzo cannot allocate proportionally
     *    without domain input).
     */
    private function resolveEddRefundAmount($order, Subscription $subscription, ?SubscriptionTransaction $originalTransaction, bool $isFullRefund, bool $isMultiSubscription): ?float
    {
        if ($isFullRefund) {
            if ($originalTransaction) {
                return (float)$originalTransaction->total;
            }

            $total = (float)($order->total ?? 0);
            return $total > 0 ? $total : null;
        }

        if ($isMultiSubscription) {
            return null;
        }

        $totalRefunded = $this->sumEddOrderRefunds($order);
        if ($totalRefunded === null || $totalRefunded <= 0) {
            return null;
        }

        $alreadyRecorded = (float)SubscriptionTransaction::where('provider', SubscriptionTransaction::PROVIDER_EDD)
            ->where('order_id', (int)$order->id)
            ->where('subscription_id', $subscription->id)
            ->where('transaction_type', SubscriptionTransaction::TYPE_REFUND)
            ->where('status', SubscriptionTransaction::STATUS_SUCCESS)
            ->sum('total')
        ;

        $delta = $totalRefunded - $alreadyRecorded;
        if ($delta <= 0) {
            return 0.0;
        }

        return $delta;
    }

    private function sumEddOrderRefunds($order): ?float
    {
        $orderId = (int) ($order->id ?? 0);

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

        $refunds = edd_get_order_refunds($orderId);
        if (!is_array($refunds)) {
            return null;
        }

        $sum = 0.0;
        foreach ($refunds as $refund) {
            $sum += abs((float) ($refund->total ?? 0));
        }

        return $sum;
    }

    private function mapEddOrderStatusToSubscriptionStatus(string $eddStatus): ?string
    {
        switch ($eddStatus) {
            case 'complete':
                return Subscription::STATUS_ACTIVE;

            case 'pending':
            case 'processing':
            case 'on_hold':
                return Subscription::STATUS_PENDING;

            case 'refunded':
            case 'revoked':
            case 'failed':
            case 'abandoned':
                return Subscription::STATUS_CANCELED;

            case 'partially_refunded':
            default:
                return null;
        }
    }

    private function syncLicensesForSubscriptionStatus(Subscription $subscription, string $status): void
    {
        $targetStatus = $status === Subscription::STATUS_ACTIVE ? License::STATUS_ACTIVE : License::STATUS_INACTIVE;

        $licenses = License::where('subscription_id', $subscription->id)->get();
        foreach ($licenses as $license) {
            // Revoked is a terminal entitlement-denial state (e.g. set by
            // processRefundForSubscription on a full refund). An
            // ambient status-sync must not un-revoke it by downgrading
            // to `inactive` or re-activating via `active`.
            if ((string)$license->status === License::STATUS_REVOKED) {
                continue;
            }

            $license->status = $targetStatus;
            $license->save();
        }
    }

    private function createOrGetOrderTransaction(array $attributes): SubscriptionTransaction
    {
        $existing = SubscriptionTransaction::where('provider', $attributes['provider'])
            ->where('provider_charge_id', $attributes['provider_charge_id'])
            ->first()
        ;

        if ($existing) {
            return $existing;
        }

        try {
            SubscriptionTransaction::create($attributes);
        } catch (\Throwable $e) {
            // Race condition fallback
        }

        $persisted = SubscriptionTransaction::where('provider', $attributes['provider'])
            ->where('provider_charge_id', $attributes['provider_charge_id'])
            ->first()
        ;

        if ($persisted) {
            return $persisted;
        }

        throw new \RuntimeException('Failed to create or resolve subscription transaction by unique provider charge key.');
    }

    private function buildOrderItemEventKey(int $orderId, $orderItem): string
    {
        return sprintf('%d:%d', $orderId, (int)$orderItem->id);
    }

    private function buildOrderItemChecksum(int $orderId, $orderItem): string
    {
        return hash('sha256', wp_json_encode([
            'order_id'   => $orderId,
            'item_id'    => (int)$orderItem->id,
            'product_id' => (int)$orderItem->product_id,
            'price_id'   => $orderItem->price_id,
            'quantity'   => (int)$orderItem->quantity,
            'total'      => (string)$orderItem->total,
        ]));
    }

    private function addOrderNote($order, string $message): void
    {
        edd_add_note([
            'object_id'   => (int)$order->id,
            'object_type' => 'order',
            'content'     => $message,
        ]);
    }

    private function syncWordPressUserRoleForSubscription(Subscription $subscription, string $status): void
    {
        $customerId = (int)($subscription->customer_id ?? 0);
        if ($customerId <= 0) {
            return;
        }

        $customer = Customer::find($customerId);
        if (!$customer) {
            return;
        }

        $this->syncWordPressUserRoleForCustomer($customer, $status);
    }

    private function syncWordPressUserRoleForCustomer(Customer $customer, string $status): void
    {
        $wpUserId = (int)($customer->user_id ?? 0);
        if ($wpUserId <= 0) {
            return;
        }

        $wpUser = get_userdata($wpUserId);
        if (!$wpUser || !($wpUser instanceof \WP_User)) {
            return;
        }

        if (user_can($wpUser, 'manage_options') || user_can($wpUser, 'manage_woocommerce')) {
            return;
        }

        $settings = Settings::getSubscriptionSettings();
        $roleKey = $status === Subscription::STATUS_ACTIVE ? 'active_role' : 'inactive_role';
        $targetRole = sanitize_key((string)($settings[$roleKey] ?? ''));

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

        $roles = wp_roles() ?: null;

        if (!$roles || !isset($roles->roles) || !is_array($roles->roles) || !isset($roles->roles[$targetRole])) {
            return;
        }

        $currentRoles = is_array($wpUser->roles ?? null) ? $wpUser->roles : [];

        if (count($currentRoles) === 1 && in_array($targetRole, $currentRoles, true)) {
            return;
        }

        $wpUser->set_role($targetRole);
    }
}
