<?php

namespace Licenzo\Modules\EDD;

use Licenzo\Casts\MoneyCast;
use Licenzo\Model\Invoice;
use Licenzo\Model\InvoiceItem;
use Licenzo\Model\License;
use Licenzo\Model\LicenseProduct;
use Licenzo\Model\Subscription;
use Licenzo\Model\SubscriptionMeta;
use Licenzo\Model\SubscriptionProduct;
use Licenzo\Model\SubscriptionTransaction;
use Licenzo\Framework\Support\Facades\DB;
use Licenzo\Helper\Action;
use Licenzo\Services\DunningService;
use Licenzo\Services\Logging\SubscriptionEvent;

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

/**
 * Manages Stripe Subscription lifecycle for EDD-originated purchases.
 *
 * After EDD completes a purchase, this service creates a Stripe Subscription
 * with trial_end set to the next billing date so Stripe takes over future
 * billing. Webhook events from Stripe are handled by reconcile methods here,
 * called from StripeWebhookService.
 */
class EddStripeSubscriptionService
{
    /**
     * Create a Stripe Subscription after a Licenzo subscription is created from an EDD purchase.
     *
     * @return string|null The Stripe Subscription ID (sub_xxx), or null on failure.
     */
    public static function createForSubscription(
        Subscription $subscription,
                     $variation,
        string       $stripeCustomerId,
        string       $paymentMethodId,
        ?string      $nextBillingDate
    ): ?string
    {

        if ($stripeCustomerId === '' || $paymentMethodId === '') {
            return null;
        }

        $interval = static::mapBillingCycleToStripeInterval((string)$subscription->billing_cycle);
        if ($interval === null) {
            return null;
        }

        $currency = strtolower((string)($subscription->currency ?? 'usd'));
        $factor = (int)pow(10, MoneyCast::minorUnits(strtoupper($currency)));
        $amountCents = (int)round((float)($subscription->recurring_amount ?? 0) * $factor);

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

        $productName = static::resolveProductName($subscription);
        $eddDownloadId = static::resolveEddDownloadId($subscription);

        $anchorTs = $nextBillingDate ? strtotime($nextBillingDate) : null;
        if ($anchorTs && $anchorTs <= time()) {
            $anchorTs = time() + 3600;
        }

        try {
            $stripeProductId = static::getOrCreateStripeProduct($eddDownloadId, $productName);
            if ($stripeProductId === null) {
                return null;
            }

            $stripePriceId = static::getOrCreateStripePrice(
                $eddDownloadId,
                $stripeProductId,
                $amountCents,
                $currency,
                $interval
            );

            if ($stripePriceId === null) {
                return null;
            }

            $args = [
                'customer'               => $stripeCustomerId,
                'default_payment_method' => $paymentMethodId,
                'items'                  => [['price' => $stripePriceId]],
                'metadata'               => [
                    'licenzo_subscription_id' => (string)$subscription->id,
                    'licenzo_customer_id'     => (string)$subscription->customer_id,
                ],
                'payment_settings'       => [
                    'save_default_payment_method' => 'on_subscription',
                ],
            ];

            if ($anchorTs) {
                $args['billing_cycle_anchor'] = $anchorTs;
                $args['proration_behavior'] = 'none';
            }

            $stripeSub = edds_api_request('Subscription', 'create', $args);

            if (!is_object($stripeSub) || empty($stripeSub->id)) {
                return null;
            }

            $stripeSubId = (string)$stripeSub->id;

            SubscriptionMeta::setMeta($subscription->id, 'stripe_subscription_id', $stripeSubId);

            SubscriptionEvent::log($subscription, 'stripe_subscription_created', null, [
                'source'                 => 'edd_purchase',
                'stripe_subscription_id' => $stripeSubId,
                'billing_cycle_anchor'   => $anchorTs ? gmdate('Y-m-d H:i:s', $anchorTs) : null,
            ]);

            return $stripeSubId;
        } catch (\Throwable $e) {
            SubscriptionEvent::log($subscription, 'stripe_subscription_creation_failed', null, [
                'source' => 'edd_purchase',
                'error'  => $e->getMessage(),
            ]);

            return null;
        }
    }

    /**
     * Cancel a Stripe Subscription at period end.
     */
    public static function cancelAtPeriodEnd(string $stripeSubId): bool
    {


        try {
            $result = edds_api_request('Subscription', 'update', $stripeSubId, [
                'cancel_at_period_end' => true,
            ]);

            return is_object($result) && !empty($result->id);
        } catch (\Throwable $e) {
            return false;
        }
    }

    /**
     * Handle invoice.paid — Stripe charged the customer for a renewal.
     */
    public static function handleInvoicePaid(array $invoice): array
    {
        $subscriptionId = static::extractLicenzoSubscriptionId($invoice);
        if ($subscriptionId <= 0) {
            return ['handled' => false, 'reason' => 'no_licenzo_subscription_id'];
        }

        $subscription = Subscription::with(['subscriptionProducts', 'customer'])->find($subscriptionId);
        if (!$subscription) {
            return ['handled' => false, 'reason' => 'subscription_not_found'];
        }

        $billingReason = (string)($invoice['billing_reason'] ?? '');
        if ($billingReason === 'subscription_create') {
            return ['handled' => true, 'reason' => 'initial_invoice_skipped'];
        }

        $amountPaidCents = (int)($invoice['amount_paid'] ?? 0);
        if ($amountPaidCents <= 0) {
            return ['handled' => false, 'reason' => 'zero_amount'];
        }

        $currency = strtoupper((string)($invoice['currency'] ?? 'usd'));
        $factor = (int)pow(10, MoneyCast::minorUnits($currency));
        $amountDollars = $amountPaidCents / $factor;
        $stripeInvoiceId = (string)($invoice['id'] ?? '');
        $chargeId = (string)($invoice['charge'] ?? '');
        $intentId = (string)($invoice['payment_intent'] ?? '');

        $nextBillingDate = static::calculateNextBillingDate((string)$subscription->billing_cycle);

        $chargeResult = ['intent' => ['id' => $intentId]];

        DB::transaction(function () use (
            $subscription, $amountDollars, $nextBillingDate, $currency,
            $stripeInvoiceId, $chargeId, $intentId, $chargeResult, $invoice
        ) {
            $nextBillCount = (int)$subscription->bill_count + 1;
            $chargeKey = sprintf('stripe_sub_invoice_%s', $stripeInvoiceId ?: wp_generate_uuid4());

            $eddOrderId = EddRenewalOrderService::createRenewalOrder(
                $subscription,
                $chargeResult,
                $amountDollars,
                $currency
            );

            $transactionMeta = [
                'source'            => 'stripe_subscription',
                'stripe_invoice_id' => $stripeInvoiceId,
                'stripe_charge_id'  => $chargeId,
                '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'     => 'stripe',
                'payment_mode'       => (edd_is_test_mode())
                    ? SubscriptionTransaction::PAYMENT_MODE_TEST
                    : SubscriptionTransaction::PAYMENT_MODE_LIVE,
                'currency'           => $currency,
                'meta'               => $transactionMeta,
            ]);

            static::createLicenzoInvoice($subscription, $invoice, $amountDollars, $currency);

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

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

                $license->update([
                    'expire_date' => $nextBillingDate,
                    'status'      => License::STATUS_ACTIVE
                ]);

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

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

        SubscriptionEvent::log($subscription, 'renewal_succeeded', null, [
            'source'            => 'stripe_subscription',
            'stripe_invoice_id' => $stripeInvoiceId,
            'amount'            => $amountDollars,
        ]);

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

        return ['handled' => true, 'action' => 'renewal_processed'];
    }

    /**
     * Handle invoice.payment_failed — Stripe failed to charge the customer.
     */
    public static function handleInvoicePaymentFailed(array $invoice): array
    {
        $subscriptionId = static::extractLicenzoSubscriptionId($invoice);

        if ($subscriptionId <= 0) {
            return ['handled' => false, 'reason' => 'no_licenzo_subscription_id'];
        }

        $subscription = Subscription::find($subscriptionId);
        if (!$subscription) {
            return ['handled' => false, 'reason' => 'subscription_not_found'];
        }

        DunningService::processFailedPayment($subscription);

        SubscriptionEvent::log($subscription, 'renewal_failed', null, [
            'source'            => 'stripe_subscription',
            'stripe_invoice_id' => (string)($invoice['id'] ?? ''),
        ]);

        return ['handled' => true, 'action' => 'dunning_updated'];
    }

    /**
     * Handle customer.subscription.updated — sync status changes from Stripe.
     */
    public static function handleSubscriptionUpdated(array $stripeSubscription): array
    {
        $subscriptionId = static::extractLicenzoSubscriptionIdFromSub($stripeSubscription);

        if ($subscriptionId <= 0) {
            return ['handled' => false, 'reason' => 'no_licenzo_subscription_id'];
        }

        $subscription = Subscription::find($subscriptionId);

        if (!$subscription) {
            return ['handled' => false, 'reason' => 'subscription_not_found'];
        }

        $stripeStatus = (string)($stripeSubscription['status'] ?? '');
        $mappedStatus = static::mapStripeStatusToLicenzo($stripeStatus);

        if ($mappedStatus === null || $mappedStatus === (string)$subscription->status) {
            return ['handled' => true, 'reason' => 'no_status_change'];
        }

        $from = (string)$subscription->status;
        $subscription->status = $mappedStatus;

        if ($mappedStatus === Subscription::STATUS_CANCELED) {
            $subscription->canceled_at = gmdate('Y-m-d H:i:s');
        }
        $subscription->save();

        static::syncLicensesForStatus($subscription, $mappedStatus);

        SubscriptionEvent::log($subscription, 'status_changed', null, [
            'source'        => 'stripe_subscription_webhook',
            'from'          => $from,
            'to'            => $mappedStatus,
            'stripe_status' => $stripeStatus,
        ]);

        return ['handled' => true, 'action' => 'status_synced', 'from' => $from, 'to' => $mappedStatus];
    }

    /**
     * Handle customer.subscription.deleted — subscription canceled in Stripe.
     */
    public static function handleSubscriptionDeleted(array $stripeSubscription): array
    {
        $subscriptionId = static::extractLicenzoSubscriptionIdFromSub($stripeSubscription);

        if ($subscriptionId <= 0) {
            return ['handled' => false, 'reason' => 'no_licenzo_subscription_id'];
        }

        $subscription = Subscription::find($subscriptionId);

        if (!$subscription) {
            return ['handled' => false, 'reason' => 'subscription_not_found'];
        }

        if ($subscription->status === Subscription::STATUS_CANCELED) {
            return ['handled' => true, 'reason' => 'already_canceled'];
        }

        $from = (string)$subscription->status;

        $subscription->update([
            'status'      => Subscription::STATUS_CANCELED,
            'canceled_at' => gmdate('Y-m-d H:i:s'),
        ]);

        static::syncLicensesForStatus($subscription, Subscription::STATUS_CANCELED);

        SubscriptionEvent::log($subscription, 'subscription_canceled', null, [
            'source' => 'stripe_subscription_deleted',
            'from'   => $from,
        ]);

        Action::do('notification_subscription_canceled', [
            'subscription_id' => (int)$subscription->id,
            'customer_id'     => (int)$subscription->customer_id,
        ]);

        return ['handled' => true, 'action' => 'canceled'];
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private static function createLicenzoInvoice(
        Subscription $subscription,
        array        $stripeInvoice,
        float        $amountDollars,
        string       $currency
    ): void
    {
        $stripeInvoiceId = (string)($stripeInvoice['id'] ?? '');
        $invoiceNumber = (string)($stripeInvoice['number'] ?? $stripeInvoiceId);
        $periodStart = isset($stripeInvoice['period_start'])
            ? gmdate('Y-m-d H:i:s', (int)$stripeInvoice['period_start'])
            : null;
        $periodEnd = isset($stripeInvoice['period_end'])
            ? gmdate('Y-m-d H:i:s', (int)$stripeInvoice['period_end'])
            : null;

        $invoice = Invoice::create([
            'uuid'                => wp_generate_uuid4(),
            'subscription_id'     => $subscription->id,
            'customer_id'         => $subscription->customer_id,
            'provider'            => 'edd',
            'provider_invoice_id' => $stripeInvoiceId,
            'invoice_number'      => $invoiceNumber,
            'billing_cycle_no'    => (int)$subscription->bill_count + 1,
            'collection_method'   => Invoice::COLLECTION_CHARGE_AUTOMATICALLY,
            'status'              => Invoice::STATUS_PAID,
            'currency'            => $currency,
            'subtotal'            => $amountDollars,
            'tax_total'           => 0,
            'total'               => $amountDollars,
            'amount_due'          => 0,
            'amount_paid'         => $amountDollars,
            'period_start'        => $periodStart,
            'period_end'          => $periodEnd,
            'paid_at'             => gmdate('Y-m-d H:i:s'),
        ]);

        $subProduct = $subscription->subscriptionProducts
            ? $subscription->subscriptionProducts->first()
            : SubscriptionProduct::where('subscription_id', $subscription->id)->first();

        $description = $subProduct
            ? sprintf('%s renewal', $subProduct->product_name ?? 'Subscription')
            : 'Subscription renewal';

        InvoiceItem::create([
            'invoice_id'     => $invoice->id,
            'line_type'      => InvoiceItem::LINE_RENEWAL,
            'reference_type' => 'subscription_product',
            'reference_id'   => $subProduct ? $subProduct->id : null,
            'description'    => $description,
            'quantity'       => 1,
            'unit_amount'    => $amountDollars,
            'line_total'     => $amountDollars,
            'currency'       => $currency,
        ]);
    }

    private static function extractLicenzoSubscriptionId(array $invoice): int
    {
        $subId = (string)($invoice['subscription'] ?? '');

        try {
            $stripeSub = edds_api_request('Subscription', 'retrieve', $subId);
            if (is_object($stripeSub) && isset($stripeSub->metadata->licenzo_subscription_id)) {
                return (int)$stripeSub->metadata->licenzo_subscription_id;
            }
        } catch (\Throwable $e) {
            // Fall through to metadata check
        }

        $metadata = is_array($invoice['metadata'] ?? null) ? $invoice['metadata'] : [];

        if (isset($metadata['licenzo_subscription_id'])) {
            return (int)$metadata['licenzo_subscription_id'];
        }

        if ($subId !== '') {

            $sub = Subscription::where('provider_subscription_id', $subId)->first();

            if ($sub) {
                return (int)$sub->id;
            }
        }

        return 0;
    }

    private static function extractLicenzoSubscriptionIdFromSub(array $stripeSubscription): int
    {
        $metadata = is_array($stripeSubscription['metadata'] ?? null) ? $stripeSubscription['metadata'] : [];

        if (isset($metadata['licenzo_subscription_id'])) {
            return (int)$metadata['licenzo_subscription_id'];
        }

        $stripeSubId = (string)($stripeSubscription['id'] ?? '');

        if ($stripeSubId !== '') {

            $sub = Subscription::where('provider_subscription_id', $stripeSubId)->first();

            if ($sub) {
                return (int)$sub->id;
            }
        }

        return 0;
    }

    private static function mapStripeStatusToLicenzo(string $stripeStatus): ?string
    {
        switch ($stripeStatus) {
            case 'active':
            case 'past_due':
            case 'trialing':
                return Subscription::STATUS_ACTIVE;
            case 'canceled':
            case 'unpaid':
                return Subscription::STATUS_CANCELED;
            case 'paused':
                return Subscription::STATUS_PAUSED;
            case 'incomplete':
            case 'incomplete_expired':
                return Subscription::STATUS_PENDING;
            default:
                return null;
        }
    }

    private static function syncLicensesForStatus(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) {

            if ((string)$license->status === License::STATUS_REVOKED) {
                continue;
            }

            $license->update([
                'status' => $targetStatus,
            ]);
        }
    }

    public static function mapBillingCycleToStripeInterval(string $cycle): ?string
    {
        switch ($cycle) {
            case 'weekly':
                return 'week';
            case 'monthly':
                return 'month';
            case 'yearly':
                return 'year';
            default:
                return null;
        }
    }

    private static function resolveProductName(Subscription $subscription): string
    {
        $subProduct = $subscription->subscriptionProducts
            ? $subscription->subscriptionProducts->first()
            : SubscriptionProduct::where('subscription_id', $subscription->id)->first();

        if ($subProduct && !empty($subProduct->product_name)) {
            return (string)$subProduct->product_name;
        }

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

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

    /**
     * Resolve the EDD download (post) ID from the subscription's product chain.
     * Returns 0 if not resolvable.
     */
    private static function resolveEddDownloadId(Subscription $subscription): int
    {
        $subProduct = $subscription->subscriptionProducts
            ? $subscription->subscriptionProducts->first()
            : SubscriptionProduct::where('subscription_id', $subscription->id)->first();

        if ($subProduct && $subProduct->product && !empty($subProduct->product->provider_id)) {
            return (int)$subProduct->product->provider_id;
        }

        return 0;
    }

    /**
     * Get or create a Stripe Product for the EDD download.
     *
     * Stores `_licenzo_stripe_product_id` on the EDD download post_meta
     * so all subscriptions for the same product share one Stripe Product.
     */
    private static function getOrCreateStripeProduct(int $eddDownloadId, string $productName): ?string
    {
        if ($eddDownloadId > 0) {
            $existing = (string)get_post_meta($eddDownloadId, '_licenzo_stripe_product_id', true);
            if ($existing !== '') {
                return $existing;
            }
        }

        try {
            $metadata = [];
            if ($eddDownloadId > 0) {
                $metadata['edd_download_id'] = (string)$eddDownloadId;
            }

            $stripeProduct = edds_api_request('Product', 'create', [
                'name'     => $productName,
                'metadata' => $metadata,
            ]);

            if (!is_object($stripeProduct) || empty($stripeProduct->id)) {
                return null;
            }

            $stripeProductId = (string)$stripeProduct->id;

            if ($eddDownloadId > 0) {
                update_post_meta($eddDownloadId, '_licenzo_stripe_product_id', $stripeProductId);
            }

            return $stripeProductId;
        } catch (\Throwable $e) {
            return null;
        }
    }

    /**
     * Get or create a Stripe Price for the given product/amount/interval.
     *
     * The Price ID is stored on the EDD download post_meta keyed by
     * amount+currency+interval so price changes create a new Price
     * while unchanged prices are reused.
     */
    private static function getOrCreateStripePrice(
        int    $eddDownloadId,
        string $stripeProductId,
        int    $amountCents,
        string $currency,
        string $interval
    ): ?string
    {
        $metaKey = sprintf('_licenzo_stripe_price_%s_%d_%s', $currency, $amountCents, $interval);

        if ($eddDownloadId > 0) {
            $existing = (string)get_post_meta($eddDownloadId, $metaKey, true);
            if ($existing !== '') {
                return $existing;
            }
        }

        try {
            $stripePrice = edds_api_request('Price', 'create', [
                'product'     => $stripeProductId,
                'currency'    => $currency,
                'unit_amount' => $amountCents,
                'recurring'   => ['interval' => $interval],
            ]);

            if (!is_object($stripePrice) || empty($stripePrice->id)) {
                return null;
            }

            $stripePriceId = (string)$stripePrice->id;

            if ($eddDownloadId > 0) {
                update_post_meta($eddDownloadId, $metaKey, $stripePriceId);
            }

            return $stripePriceId;
        } catch (\Throwable $e) {
            return null;
        }
    }

    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;
        }
    }
}
