<?php

declare(strict_types=1);

namespace App\Services\Billing;

use App\Models\Coupon;
use App\Models\Plan;
use App\Models\Service;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Laravel\Cashier\Exceptions\IncompletePayment;

class StripeBillingService implements BillingServiceInterface
{
    public function createSubscription(User $user, Plan $plan, ?string $paymentMethodId = null, ?string $couponCode = null, string $billingCycle = 'monthly'): array
    {
        if (! $user->hasStripeId()) {
            $user->createAsStripeCustomer();
        }

        if ($paymentMethodId) {
            $user->addPaymentMethod($paymentMethodId);
            $user->updateDefaultPaymentMethod($paymentMethodId);
        }

        $planPrice = $plan->priceForCycle($billingCycle);
        $stripePriceId = $planPrice?->stripe_price_id ?? $plan->stripe_price_id;

        if (! $stripePriceId) {
            throw new \RuntimeException("Plan \"{$plan->name}\" does not have a Stripe price configured for the {$billingCycle} cycle.");
        }

        $subscription = $user->newSubscription($plan->slug, $stripePriceId);

        if ($couponCode) {
            $coupon = Coupon::where('code', $couponCode)->first();
            if ($coupon?->isValid()) {
                $subscription->withCoupon($couponCode);
            }
        }

        try {
            $result = DB::transaction(function () use ($subscription, $plan, $billingCycle, $stripePriceId) {
                $cashierSubscription = $subscription->create();

                $cashierSubscription->update([
                    'plan_id' => $plan->id,
                    'billing_cycle' => $billingCycle,
                    'gateway' => 'stripe',
                    'gateway_subscription_id' => $cashierSubscription->stripe_id,
                    'gateway_customer_id' => $cashierSubscription->user->stripe_id,
                    'gateway_price_id' => $stripePriceId,
                    'current_period_start' => now(),
                    'current_period_end' => $this->calculatePeriodEnd($billingCycle),
                ]);

                return [
                    'subscription_id' => $cashierSubscription->stripe_id,
                    'status' => $cashierSubscription->stripe_status,
                ];
            });

            return $result;
        } catch (IncompletePayment $e) {
            return [
                'subscription_id' => $e->payment->subscription,
                'status' => 'incomplete',
                'client_secret' => $e->payment->clientSecret(),
            ];
        }
    }

    public function cancelSubscription(User $user, string $subscriptionId, bool $immediately = false): bool
    {
        $subscription = $user->subscriptions()->where('stripe_id', $subscriptionId)->first();

        if (! $subscription) {
            return false;
        }

        try {
            if ($immediately) {
                $subscription->cancelNow();
            } else {
                $subscription->cancel();
            }

            $subscription->update(['cancelled_at' => now()]);

            return true;
        } catch (\Exception $e) {
            Log::error('Stripe subscription cancellation failed', [
                'subscription_id' => $subscriptionId,
                'error' => $e->getMessage(),
            ]);

            return false;
        }
    }

    public function swapSubscription(User $user, string $subscriptionId, Plan $newPlan, string $billingCycle = 'monthly'): array
    {
        $subscription = $user->subscriptions()->where('stripe_id', $subscriptionId)->first();

        if (! $subscription) {
            throw new \RuntimeException('Subscription not found.');
        }

        $planPrice = $newPlan->priceForCycle($billingCycle);
        $stripePriceId = $planPrice?->stripe_price_id ?? $newPlan->stripe_price_id;

        try {
            $subscription->swap($stripePriceId);

            $subscription->update([
                'plan_id' => $newPlan->id,
                'billing_cycle' => $billingCycle,
                'gateway_price_id' => $stripePriceId,
                'current_period_end' => $this->calculatePeriodEnd($billingCycle),
            ]);

            // After the swap, update the service's plan_id
            $service = Service::where('subscription_id', $subscription->id)->first();
            if ($service) {
                $service->update(['plan_id' => $newPlan->id]);
            }

            return [
                'subscription_id' => $subscription->stripe_id,
                'status' => $subscription->stripe_status,
            ];
        } catch (IncompletePayment $e) {
            return [
                'subscription_id' => $subscription->stripe_id,
                'status' => 'incomplete',
                'client_secret' => $e->payment->clientSecret(),
            ];
        }
    }

    public function resumeSubscription(User $user, string $subscriptionId): bool
    {
        $subscription = $user->subscriptions()->where('stripe_id', $subscriptionId)->first();

        if (! $subscription) {
            return false;
        }

        try {
            $subscription->resume();

            $subscription->update(['cancelled_at' => null]);

            return true;
        } catch (\Exception $e) {
            Log::error('Stripe subscription resume failed', [
                'subscription_id' => $subscriptionId,
                'error' => $e->getMessage(),
            ]);

            return false;
        }
    }

    public function addPaymentMethod(User $user, string $paymentMethodId): bool
    {
        try {
            if (! $user->hasStripeId()) {
                $user->createAsStripeCustomer();
            }

            $user->addPaymentMethod($paymentMethodId);

            return true;
        } catch (\Exception $e) {
            Log::error('Failed to add payment method', [
                'user_id' => $user->id,
                'error' => $e->getMessage(),
            ]);

            return false;
        }
    }

    public function removePaymentMethod(User $user, string $paymentMethodId): bool
    {
        try {
            $paymentMethod = $user->findPaymentMethod($paymentMethodId);
            $paymentMethod?->delete();

            return true;
        } catch (\Exception $e) {
            Log::error('Failed to remove payment method', [
                'user_id' => $user->id,
                'error' => $e->getMessage(),
            ]);

            return false;
        }
    }

    public function setDefaultPaymentMethod(User $user, string $paymentMethodId): bool
    {
        try {
            $user->updateDefaultPaymentMethod($paymentMethodId);

            return true;
        } catch (\Exception $e) {
            Log::error('Failed to set default payment method', [
                'user_id' => $user->id,
                'error' => $e->getMessage(),
            ]);

            return false;
        }
    }

    /** @return array<int, array{id: string, brand: string, last_four: string, exp_month: int, exp_year: int, is_default: bool}> */
    public function getPaymentMethods(User $user): array
    {
        if (! $user->hasStripeId()) {
            return [];
        }

        $defaultMethod = $user->defaultPaymentMethod();
        $defaultId = $defaultMethod?->id;

        return $user->paymentMethods()->map(fn ($method) => [
            'id' => $method->id,
            'brand' => $method->card->brand ?? 'unknown',
            'last_four' => $method->card->last4 ?? '****',
            'exp_month' => $method->card->exp_month ?? 0,
            'exp_year' => $method->card->exp_year ?? 0,
            'is_default' => $method->id === $defaultId,
        ])->all();
    }

    public function getGateway(): string
    {
        return 'stripe';
    }

    private function calculatePeriodEnd(string $billingCycle): \Carbon\Carbon
    {
        return match ($billingCycle) {
            'monthly' => now()->addMonth(),
            'quarterly' => now()->addMonths(3),
            'semi_annual' => now()->addMonths(6),
            'annual' => now()->addYear(),
            default => now()->addMonth(),
        };
    }
}
