<?php

namespace Licenzo\Services;

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

use Licenzo\Casts\MoneyCast;
use Licenzo\Helper\Action;
use Licenzo\Helper\Settings;
use Licenzo\Model\License;
use Licenzo\Model\LicenseProduct;
use Licenzo\Model\Subscription;
use Licenzo\Model\SubscriptionMeta;
use Licenzo\Model\SubscriptionSchedule;
use Licenzo\Model\SubscriptionTransaction;
use Licenzo\Framework\Support\Facades\DB;
use Licenzo\Services\Logging\ScheduleManager;
use Licenzo\Services\Logging\SubscriptionEvent;
use Licenzo\Services\Integrations\ProviderAliasService;

class SubscriptionRenewalService
{
    private const BATCH_LIMIT = 50;

    /**
     * Process all due renewal schedules. Called by WordPress cron.
     */
    public static function processAllDue(): array
    {
        if (!static::isRenewalEnabled()) {
            return ['processed' => 0, 'skipped' => 0, 'failed' => 0, 'reason' => 'disabled'];
        }

        $dueSchedules = SubscriptionSchedule::where('status', SubscriptionSchedule::STATUS_PENDING)
            ->where('action_type', SubscriptionSchedule::ACTION_RENEWAL_CHARGE)
            ->where('scheduled_at', '<=', gmdate('Y-m-d H:i:s'))
            ->orderBy('scheduled_at', 'asc')
            ->limit(static::BATCH_LIMIT)
            ->get();

        $processed = 0;
        $skipped = 0;
        $failed = 0;

        foreach ($dueSchedules as $schedule) {
            $result = static::processSchedule($schedule);

            if ($result['status'] === 'success') {
                $processed++;
            } elseif ($result['status'] === 'skipped') {
                $skipped++;
            } else {
                $failed++;
            }
        }

        return [
            'processed' => $processed,
            'skipped'   => $skipped,
            'failed'    => $failed,
        ];
    }

    private static function processSchedule(SubscriptionSchedule $schedule): array
    {
        $subscriptionId = (int) ($schedule->action_data['subscription_id'] ?? $schedule->subscription_id);
        $subscription = Subscription::with(['subscriptionProducts', 'customer'])->find($subscriptionId);

        if (!$subscription) {
            $schedule->markAsFailed('Subscription not found');
            return ['status' => 'skipped'];
        }

        if (!$subscription->isActive() || !$subscription->canBill()) {
            $schedule->markAsFailed('Subscription not eligible for billing');
            return ['status' => 'skipped'];
        }

        if (static::isNonRecurringCycle((string) $subscription->billing_cycle)) {
            $schedule->markAsFailed('Non-recurring billing cycle');
            return ['status' => 'skipped'];
        }

        if (str_starts_with((string) ($subscription->provider_subscription_id ?? ''), 'sub_')) {
            $schedule->markAsFailed('Managed by Stripe Subscription — cron skipped');
            return ['status' => 'skipped'];
        }

        $provider = (string) ($schedule->action_data['provider'] ?? $subscription->provider);

        if (ProviderAliasService::isEdd($provider)) {
            return static::processEddRenewal($subscription, $schedule);
        }

        $schedule->markAsFailed('Provider not eligible for cron-based renewal: ' . $provider);
        return ['status' => 'skipped'];
    }

    private static function processEddRenewal(Subscription $subscription, SubscriptionSchedule $schedule): array
    {
        if (\Licenzo\Modules\EDD\EddRecurringDetector::hasBlockingConflict()) {
            $schedule->markAsFailed('EDD Recurring Payments addon active — Licenzo renewal suppressed');
            static::logRenewalEvent($subscription, 'renewal_skipped', ['reason' => 'edd_recurring_active']);
            return ['status' => 'skipped'];
        }

        $subscriptionGateway = (string) ($subscription->current_payment_method ?? '');
        if ($subscriptionGateway !== '' && !\Licenzo\Modules\EDD\EddGatewayPolicy::isRecurringEligible($subscriptionGateway)) {
            $schedule->markAsFailed('Subscription gateway not recurring-eligible: ' . $subscriptionGateway);
            static::logRenewalEvent($subscription, 'renewal_skipped', [
                'reason'  => 'gateway_not_recurring_eligible',
                'gateway' => \Licenzo\Modules\EDD\EddGatewayPolicy::normalizeSlug($subscriptionGateway),
            ]);
            return ['status' => 'skipped'];
        }

        $renewalService = new \Licenzo\Modules\EDD\EddStripeRenewalPaymentService();

        if (!$renewalService->isAvailable()) {
            $schedule->markAsFailed('EDD Stripe gateway unavailable');
            static::logRenewalEvent($subscription, 'renewal_skipped', ['reason' => 'gateway_unavailable']);
            return ['status' => 'skipped'];
        }

        $stripeCustomerId = (string) ($subscription->provider_customer_id ?? '');
        if ($stripeCustomerId === '') {
            $schedule->markAsFailed('Missing Stripe customer ID');
            static::logRenewalEvent($subscription, 'renewal_skipped', ['reason' => 'missing_stripe_customer']);
            return ['status' => 'skipped'];
        }

        $paymentMethodId = static::resolveEddPaymentMethod($subscription, $stripeCustomerId);
        if ($paymentMethodId === '') {
            $schedule->markAsFailed('Unable to resolve Stripe payment method');
            static::logRenewalEvent($subscription, 'renewal_skipped', ['reason' => 'missing_payment_method']);
            return ['status' => 'skipped'];
        }

        $lazyMigrated = static::tryLazyMigrateToStripeSubscription($subscription, $stripeCustomerId, $paymentMethodId);
        if ($lazyMigrated) {
            $schedule->markAsExecuted(['mode' => 'lazy_migration_to_stripe_subscription']);
            return ['status' => 'success'];
        }

        $amountCents = static::calculateRenewalAmountCents($subscription);
        if ($amountCents <= 0) {
            $schedule->markAsFailed('Invalid renewal amount');
            return ['status' => 'skipped'];
        }

        $currency = strtoupper((string) ($subscription->currency ?? ''));
        if ($currency === '') {
            $schedule->markAsFailed('Subscription has no currency set — cannot charge');
            static::logRenewalEvent($subscription, 'renewal_skipped', ['reason' => 'missing_currency']);
            return ['status' => 'skipped'];
        }

        $productName = static::resolveProductNameForSubscription($subscription);
        $description = sprintf('%s renewal #%d', $productName, $subscription->id);

        $chargeResult = $renewalService->chargeRenewal(
            $stripeCustomerId,
            $paymentMethodId,
            $amountCents,
            $currency,
            $description,
            [
                'subscription_id' => (string) $subscription->id,
                'customer_id'     => (string) $subscription->customer_id,
                'billing_cycle'   => (string) $subscription->billing_cycle,
            ]
        );

        if (!($chargeResult['success'] ?? false)) {
            if (($chargeResult['mode'] ?? '') === 'requires_action') {
                return static::handleRequiresActionRenewal($subscription, $schedule, $chargeResult, $amountCents, $currency);
            }

            return static::handleFailedRenewal($subscription, $schedule, $chargeResult);
        }

        return static::handleSuccessfulEddRenewal($subscription, $schedule, $chargeResult, $amountCents, $currency);
    }

    /**
     * SCA / 3D-Secure response handler. The renewal charge did NOT fail in
     * the dead-card sense — Stripe is asking the customer to authenticate
     * the transaction interactively. Dunning retry loops are the wrong
     * remedy here: retrying off-session will produce the same
     * `requires_action` status until the customer authenticates.
     *
     * Policy for v1.1.0:
     *  - Mark the schedule as failed with a specific reason so cron does
     *    not reprocess the same schedule row.
     *  - Do NOT escalate the dunning counter. Requires-action is a
     *    customer-authentication state, not a failed-card state.
     *  - Stamp three `SubscriptionMeta` rows carrying the intent id,
     *    client_secret, and next_action type so the admin UI or a
     *    customer-portal surface can build a resume-payment link.
     *  - Log a `renewal_requires_action` event.
     *  - Dispatch `notification_subscription_requires_action` so email
     *    integrations can send the customer a link to complete 3DS.
     *  - Leave `subscription.status` untouched (the customer still owns
     *    the current paid cycle; the next cycle is what's blocked).
     *  - Do NOT auto-reschedule. The customer must authenticate first;
     *    any follow-up charge will be admin-initiated or triggered by a
     *    future webhook-driven flow in v1.2+.
     */
    private static function handleRequiresActionRenewal(
        Subscription $subscription,
        SubscriptionSchedule $schedule,
        array $chargeResult,
        int $amountCents,
        string $currency
    ): array {
        $intent = is_array($chargeResult['intent'] ?? null) ? $chargeResult['intent'] : [];
        $intentId = isset($intent['id']) ? (string) $intent['id'] : '';
        $clientSecret = isset($intent['client_secret']) ? (string) $intent['client_secret'] : '';
        $nextActionType = isset($intent['next_action_type']) ? (string) $intent['next_action_type'] : '';

        $schedule->markAsFailed('SCA / 3D-Secure authentication required');

        if ($intentId !== '') {
            SubscriptionMeta::setMeta($subscription->id, 'renewal_requires_action', '1');
            SubscriptionMeta::setMeta($subscription->id, 'renewal_requires_action_intent_id', $intentId);
            if ($clientSecret !== '') {
                SubscriptionMeta::setMeta($subscription->id, 'renewal_requires_action_client_secret', $clientSecret);
            }
            if ($nextActionType !== '') {
                SubscriptionMeta::setMeta($subscription->id, 'renewal_requires_action_type', $nextActionType);
            }
            SubscriptionMeta::setMeta($subscription->id, 'renewal_requires_action_at', gmdate('Y-m-d H:i:s'));
        }

        static::logRenewalEvent($subscription, 'renewal_requires_action', [
            'mode'             => 'edd_stripe',
            'intent_id'        => $intentId,
            'next_action_type' => $nextActionType,
            'amount_cents'     => $amountCents,
            'currency'         => $currency,
            'message'          => $chargeResult['message'] ?? null,
        ]);

        Action::do('notification_subscription_requires_action', [
            'subscription_id'  => (int) $subscription->id,
            'customer_id'      => (int) $subscription->customer_id,
            'intent_id'        => $intentId,
            'client_secret'    => $clientSecret,
            'next_action_type' => $nextActionType,
            'amount_cents'     => $amountCents,
            'currency'         => $currency,
        ]);

        return ['status' => 'requires_action'];
    }

    private static function handleSuccessfulEddRenewal(
        Subscription $subscription,
        SubscriptionSchedule $schedule,
        array $chargeResult,
        int $amountCents,
        string $currency
    ): array {
        $factor = (int) pow(10, MoneyCast::minorUnits($currency));
        $amountDollars = $amountCents / $factor;
        $nextBillingDate = static::calculateNextBillingDate((string) $subscription->billing_cycle);
        $intentId = $chargeResult['intent']['id'] ?? null;

        DB::transaction(function () use ($subscription, $amountDollars, $nextBillingDate, $currency, $intentId, $chargeResult) {
            $nextBillCount = (int) $subscription->bill_count + 1;
            $chargeKey = sprintf(
                'edd_renewal_%d_cycle_%d',
                $subscription->id,
                $nextBillCount
            );

            $eddOrderId = \Licenzo\Modules\EDD\EddRenewalOrderService::createRenewalOrder(
                $subscription,
                $chargeResult,
                $amountDollars,
                $currency
            );

            $transactionMeta = [
                'source'           => 'auto_renewal',
                'stripe_intent_id' => $intentId,
            ];

            if ($eddOrderId) {
                $transactionMeta['edd_order_id'] = $eddOrderId;
            }

            SubscriptionTransaction::create([
                'uuid'               => wp_generate_uuid4(),
                'provider'           => SubscriptionTransaction::PROVIDER_EDD,
                'provider_id'        => $eddOrderId ?: null,
                'provider_charge_id' => $chargeKey,
                'order_id'           => $eddOrderId ?: null,
                'order_type'         => SubscriptionTransaction::PROVIDER_EDD,
                'subscription_id'    => $subscription->id,
                'customer_id'        => $subscription->customer_id,
                'total'              => $amountDollars,
                'status'             => SubscriptionTransaction::STATUS_SUCCESS,
                'transaction_type'   => SubscriptionTransaction::TYPE_RENEWAL,
                'payment_method'     => (string) $subscription->current_payment_method,
                'payment_mode'       => (function_exists('edd_is_test_mode') && edd_is_test_mode())
                    ? SubscriptionTransaction::PAYMENT_MODE_TEST
                    : SubscriptionTransaction::PAYMENT_MODE_LIVE,
                'currency'           => $currency,
                'meta'               => $transactionMeta,
            ]);

            $subscription->bill_count = $nextBillCount;
            $subscription->total_paid = (float) $subscription->total_paid + $amountDollars;
            $subscription->next_billing_date = $nextBillingDate;
            $subscription->expire_at = $nextBillingDate;
            $subscription->last_payment_attempt = gmdate('Y-m-d H:i:s');
            $subscription->save();

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

                LicenseProduct::where('license_id', $license->id)
                    ->update(['expire_date' => $nextBillingDate]);
            }

            DunningService::resetDunningState($subscription);
        });

        $schedule->markAsExecuted([
            'mode'      => 'edd_stripe',
            'intent_id' => $intentId,
            'amount'    => $amountDollars,
            'currency'  => $currency,
        ]);

        static::scheduleNextRenewal($subscription->fresh(), $nextBillingDate);

        static::logRenewalEvent($subscription, 'renewal_succeeded', [
            'mode'      => 'edd_stripe',
            'intent_id' => $intentId,
            'amount'    => $amountDollars,
        ]);

        Action::do('notification_subscription_renewed', [
            'subscription_id' => (int) $subscription->id,
            'customer_id'     => (int) $subscription->customer_id,
            'amount'          => $amountDollars,
            'currency'        => $currency,
        ]);

        return ['status' => 'success'];
    }

    private static function handleFailedRenewal(
        Subscription $subscription,
        SubscriptionSchedule $schedule,
        array $chargeResult
    ): array {
        $message = $chargeResult['message'] ?? 'Unknown payment failure';

        $schedule->markAsFailed($message);

        DunningService::processFailedPayment($subscription);

        if (DunningService::shouldRetry($subscription)) {
            $retryAt = $subscription->fresh()->next_payment_attempt;
            if ($retryAt) {
                ScheduleManager::schedule(
                    $subscription,
                    SubscriptionSchedule::ACTION_RENEWAL_CHARGE,
                    (string) $retryAt,
                    [
                        'provider'        => (string) $subscription->provider,
                        'subscription_id' => $subscription->id,
                        'retry'           => true,
                        'attempt'         => (int) $subscription->fresh()->failed_payment_count,
                    ],
                    'Retry after failed renewal'
                );
            }
        }

        static::logRenewalEvent($subscription, 'renewal_failed', [
            'message' => $message,
            'mode'    => $chargeResult['mode'] ?? 'unknown',
            'intent'  => $chargeResult['intent'] ?? null,
        ]);

        return ['status' => 'failed'];
    }

    /**
     * Attempt to migrate a cron-based subscription to a Stripe Subscription.
     *
     * Called when a due subscription still uses the old PaymentIntent flow.
     * Creates a Stripe Subscription with trial_end=now so Stripe charges
     * immediately, then Stripe handles all future renewals via webhooks.
     */
    private static function tryLazyMigrateToStripeSubscription(
        Subscription $subscription,
        string $stripeCustomerId,
        string $paymentMethodId
    ): bool {
        if (!function_exists('edds_api_request')) {
            return false;
        }

        $subscription->load('subscriptionProducts');
        $subProduct = $subscription->subscriptionProducts ? $subscription->subscriptionProducts->first() : null;

        $stripeSubId = \Licenzo\Modules\EDD\EddStripeSubscriptionService::createForSubscription(
            $subscription,
            $subProduct,
            $stripeCustomerId,
            $paymentMethodId,
            gmdate('Y-m-d', time() + 60)
        );

        if (!$stripeSubId) {
            return false;
        }

        $subscription->provider_subscription_id = $stripeSubId;
        $subscription->save();

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

        static::logRenewalEvent($subscription, 'lazy_migrated_to_stripe_subscription', [
            'stripe_subscription_id' => $stripeSubId,
        ]);

        return true;
    }

    /**
     * Resolve the Stripe payment method for an EDD subscription.
     *
     * Strategy:
     *   1. Check stored payment method ID in subscription meta
     *   2. Fall back to listing the customer's payment methods via Stripe API
     */
    private static function resolveEddPaymentMethod(Subscription $subscription, string $stripeCustomerId): string
    {
        $stored = SubscriptionMeta::getMeta($subscription->id, 'edd_stripe_payment_method_id');
        if (is_string($stored) && $stored !== '') {
            return $stored;
        }

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

        try {
            $methods = edds_api_request('PaymentMethod', 'all', [
                'customer' => $stripeCustomerId,
                'type'     => 'card',
                'limit'    => 1,
            ]);

            if (is_object($methods) && !empty($methods->data) && is_array($methods->data)) {
                $first = $methods->data[0] ?? null;
                if (is_object($first) && !empty($first->id)) {
                    $pmId = (string) $first->id;
                    SubscriptionMeta::setMeta($subscription->id, 'edd_stripe_payment_method_id', $pmId);
                    return $pmId;
                }
            }
        } catch (\Throwable $e) {
            // Non-fatal — renewal cannot proceed without a payment method
        }

        return '';
    }

    private static function calculateRenewalAmountCents(Subscription $subscription): int
    {
        $currency = strtoupper((string) ($subscription->currency ?? ''));
        if ($currency === '') {
            return 0;
        }

        $factor = (int) pow(10, MoneyCast::minorUnits($currency));
        $amount = (float) ($subscription->recurring_amount ?? 0);

        return (int) round($amount * $factor);
    }

    private static function calculateNextBillingDate(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'));
            default:
                return null;
        }
    }

    private static function scheduleNextRenewal(Subscription $subscription, ?string $nextBillingDate): void
    {
        if ($nextBillingDate === null || !$subscription->canBill()) {
            return;
        }

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

        ScheduleManager::schedule(
            $subscription,
            SubscriptionSchedule::ACTION_RENEWAL_CHARGE,
            $nextBillingDate,
            [
                'provider'        => (string) $subscription->provider,
                'subscription_id' => $subscription->id,
            ],
            'Auto-scheduled next renewal cycle'
        );
    }

    private static function resolveProductNameForSubscription(Subscription $subscription): string
    {
        $subProduct = $subscription->subscriptionProducts->first();
        if ($subProduct && !empty($subProduct->product_name)) {
            return (string) $subProduct->product_name;
        }

        $name = (string) ($subscription->subscription_name ?? '');

        return $name !== '' ? $name : 'Subscription';
    }

    private static function isNonRecurringCycle(string $cycle): bool
    {
        return in_array($cycle, ['onetime', 'lifetime', ''], true);
    }

    private static function isRenewalEnabled(): bool
    {
        $settings = Settings::getSubscriptionSettings();

        if (($settings['enable_subscriptions'] ?? 'yes') !== 'yes') {
            return false;
        }

        return ($settings['renewal_process'] ?? 'auto') === 'auto';
    }

    private static function logRenewalEvent(Subscription $subscription, string $eventType, array $data = []): void
    {
        SubscriptionEvent::log($subscription, $eventType, null, [
            'source' => 'renewal_service',
            'data'   => $data,
        ]);
    }
}
