<?php

namespace App\Services;

use App\Models\Order;
use App\Models\OrderItem;
use App\Models\PricingTier;
use App\Models\Subscription;
use App\Repositories\SubscriptionRepository;
use Exception;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\DB;
use Random\RandomException;

class SubscriptionService
{
    protected SubscriptionRepository $subscriptionRepository;

    public function __construct(SubscriptionRepository $subscriptionRepository)
    {
        $this->subscriptionRepository = $subscriptionRepository;
    }

    public function __call($method, $arguments)
    {
        if (method_exists($this->subscriptionRepository, $method)) {
            return call_user_func_array([$this->subscriptionRepository, $method], $arguments);
        }

        throw new \BadMethodCallException("Method {$method} does not exist.");
    }

    /**
     * @throws RandomException
     */
    public function createSubscription($order): void
    {
        $subscriptionItem = $order->subscriptionItem();

        $subData = [
            'user_id' => $order->user_id,
            'purchasable_type' => $subscriptionItem->purchasable_type,
            'purchasable_id' => $subscriptionItem->purchasable_id,
            'name' => $subscriptionItem->name,
            'price' => $subscriptionItem->price,
            'billing_cycle' => $subscriptionItem->purchasable->billing_cycle,
            'billing_cycle_count' => $subscriptionItem->purchasable->billing_cycle_count,
            'trial_active' => (bool)$subscriptionItem->purchasable->trial_available,
        ];

        $subscription = $this->subscriptionRepository->create($subData);

        if($subscriptionItem->purchasable->trial_available)
        {
            $trialEndDate = $this->calculateEndDate(
                'trial',
                now(),
                $subscriptionItem->purchasable->trial_duration_unit,
                $subscriptionItem->purchasable->trial_duration
            );

            $subscription->update([
                'status' => getConstant('SUBSCRIPTION_STATUS_TRIAL'),
                'next_billing_date' => $trialEndDate,
                'trial_start_date' => now(),
                'trial_end_date' => $trialEndDate,
            ]);
        }

        $order->update([
            'subscription_id' => $subscription->id,
            'invoice_number' => $this->generateInvoiceNumber(),
        ]);

        if ($order->addonsItems()->count()) {
            $order->addonsItems()->each(function ($item) use ($subscription) {
                $this->subscriptionRepository->addAddon($subscription, [
                    'addon_id' => $item->purchasable_id,
                    'name' => $item->name,
                    'unit_price' => $item->price,
                    'total_price' => $item->price,
                ]);
            });
        }
    }

    /**
     * @throws Exception
     */
    public function renewSubscription($subscription): void
    {
        if ($subscription->status !== getConstant('SUBSCRIPTION_STATUS_ACTIVE')) {
            throw new \Exception('Subscription is not active, cannot renew.');
        }
        if (!now()->greaterThanOrEqualTo($subscription->next_billing_date)) {
            throw new \Exception('Not yet time to renew.');
        }

        // Start a database transaction
        DB::beginTransaction();

        try {
            // Fetch the last order of the subscription
            $lastOrder = Order::where('subscription_id', $subscription->id)
                ->orderBy('created_at', 'desc')
                ->first();

            if (!$lastOrder) {
                throw new Exception('No order found for this subscription.');
            }

            // Create a new order for renewal
            $newOrder = $lastOrder->replicate();
            $newOrder->invoice_number = $this->generateInvoiceNumber();
            $newOrder->status = getConstant('ORDER_STATUS_PENDING');
            $newOrder->payment_method = null;
            $newOrder->offline_payment_method = null;
            $newOrder->transaction_id = null;
            $newOrder->customer_ip_address = null;
            $newOrder->created_at = now();
            $newOrder->updated_at = now();
            $newOrder->save();

            // Fetch the items of the last order
            $lastOrderItems = OrderItem::where('order_id', $lastOrder->id)->get();

            $setupFee = 0;
            // Duplicate each item for the new order
            foreach ($lastOrderItems as $item) {
                if (!$item->purchasable_type == PricingTier::class && str_contains($item->name, 'Setup Fee')) {
                    $setupFee = $item->price;
                    continue;
                }
                $newItem = $item->replicate();
                $newItem->order_id = $newOrder->id;
                $newItem->created_at = now();
                $newItem->updated_at = now();
                $newItem->save();
            }

            $newOrder->update(['subtotal' => $newOrder->subtotal - $setupFee]);
            $newOrder->update(['total' => $newOrder->total - $setupFee]);

            $billingInfo = $lastOrder->billingInfo->toArray();
            unset($billingInfo['id']);
            $newOrder->billingInfo()->create($billingInfo);

            $subscription->status = getConstant('SUBSCRIPTION_STATUS_INACTIVE');
            // TODO: needs to confirm
            $subscription->current_period_start = null;
            $subscription->current_period_end = null;
            $subscription->next_billing_date = null;
            $subscription->save();

            // Commit the transaction
            DB::commit();

        } catch (Exception $e) {
            // Rollback the transaction if there's an error
            DB::rollBack();
            throw $e;
        }
    }

    /**
     * @throws Exception
     */
    public function createOrder($subscription): void
    {
        if ($subscription->status !== getConstant('SUBSCRIPTION_STATUS_ACTIVE')) {
            throw new \Exception('Subscription is not active, cannot renew.');
        }

        DB::beginTransaction();

        try {
            // Fetch the last order of the subscription
            $lastOrder = Order::where('subscription_id', $subscription->id)->latest('id')->first();

            if (!$lastOrder) {
                throw new Exception('No order found for this subscription.');
            }

            // Create a new order for renewal
            $newOrder = $lastOrder->replicate();
            $newOrder->invoice_number = $this->generateInvoiceNumber();
            $newOrder->status = getConstant('ORDER_STATUS_PENDING');
            $newOrder->payment_method = null;
            $newOrder->offline_payment_method = null;
            $newOrder->transaction_id = null;
            $newOrder->customer_ip_address = null;
            $newOrder->created_at = now();
            $newOrder->updated_at = now();
            $newOrder->save();

            // Fetch the items of the last order
            $lastOrderItems = OrderItem::where('order_id', $lastOrder->id)->get();

            $setupFee = 0;
            // Duplicate each item for the new order
            foreach ($lastOrderItems as $item) {
                if (!$item->purchasable_type == PricingTier::class && str_contains($item->name, 'Setup Fee')) {
                    $setupFee = $item->price;
                    continue;
                }
                $newItem = $item->replicate();
                $newItem->order_id = $newOrder->id;
                $newItem->created_at = now();
                $newItem->updated_at = now();
                $newItem->save();
            }

            $newOrder->update(['subtotal' => $newOrder->subtotal - $setupFee]);
            $newOrder->update(['total' => $newOrder->total - $setupFee]);

            $billingInfo = $lastOrder->billingInfo->toArray();
            unset($billingInfo['id']);
            $newOrder->billingInfo()->create($billingInfo);

            // Commit the transaction
            DB::commit();

        } catch (Exception $e) {
            // Rollback the transaction if there's an error
            DB::rollBack();
            throw $e;
        }
    }

    public function cancelSubscription(Subscription $subscription)
    {
        $immediate = false;
        if ($subscription->status !== getConstant('SUBSCRIPTION_STATUS_INACTIVE') || $subscription->status !== getConstant('SUBSCRIPTION_STATUS_CANCELLED')) {

            // $subscription->status = getConstant('SUBSCRIPTION_STATUS_CANCELLED');
            // $subscription->canceled_at = now();

            $subscription->update([
                'cancelled_at' => now(),
                'cancellation_reason' => getConstant('CR_CUSTOMER_REQUESTED')
            ]);

            if ($immediate) {
                // End service right now
                $subscription->update([
                    'current_period_end' => now()
                ]);
            }
        }

        // (Optional) Notify user of cancellation
        // (Optional) trigger any partial refund logic if needed
    }

    public function cancelTrialSubscription(Subscription $subscription)
    {
        $immediate = false;

        $subscription->update([
            'status' => getConstant('SUBSCRIPTION_STATUS_CANCELLED'),
            'cancelled_at' => now(),
            'cancellation_reason' => getConstant('CR_CUSTOMER_REQUESTED')
        ]);

        if ($immediate) {
            // End service right now
            $subscription->update([
                'current_period_end' => now()
            ]);
        }

        // (Optional) trigger any partial refund logic if needed
    }

    public function reactive(Subscription $subscription)
    {
        $subscription->update([
            'cancelled_at' => null,
            'cancellation_reason' => null
        ]);
    }

    /**
     * @throws RandomException
     */
    private function generateInvoiceNumber(): string
    {
        // Start a transaction to ensure no collisions
        DB::beginTransaction();
        try {
            // Lock the row containing the max id to prevent race conditions
            $maxId = Order::lockForUpdate()->max('id'); // Lock the row for updating

            // Generate the invoice number by combining the timestamp and ID
            $timestamp = time(); // Get current Unix timestamp
            $invoiceNumber = $timestamp . ($maxId + 1000); // Combine timestamp with auto-incremented ID

            // Commit the transaction
            DB::commit();
        } catch (\Exception $e) {
            // Rollback in case of any error
            DB::rollBack();
            throw $e;
        }

        return $invoiceNumber;
    }

    public function updateSubscriptionStatus($order): void
    {
        $subscription = $this->subscriptionRepository->getById($order->subscription_id);

        if (!$subscription) {
            throw new ModelNotFoundException("Subscription not found for order ID: $order->id");
        }

        if ($subscription->status == getConstant('SUBSCRIPTION_STATUS_INACTIVE')
            && $order->status == getConstant('ORDER_STATUS_COMPLETED')) {
            $this->changeStatus($subscription, getConstant('SUBSCRIPTION_STATUS_ACTIVE'));
        }
    }

    public function changeStatus(Subscription $subscription, $newStatus): void
    {
        DB::transaction(function () use ($subscription, $newStatus) {
            $subscription->update(['status' => $newStatus]);

            match ($newStatus) {
                getConstant('SUBSCRIPTION_STATUS_ACTIVE') => $this->activateSubscription($subscription),
                getConstant('SUBSCRIPTION_STATUS_PAST_DUE') => $this->moveSubscriptionToPastDue($subscription),
//                getConstant('SUBSCRIPTION_STATUS_INACTIVE') => $this->deactivateSubscription($subscription),
                default => throw new Exception("Invalid subscription status: $newStatus"),
            };

            // Dispatch event
//            event(new SubscriptionStatusChanged($subscription, $newStatus));

            // Log status change
            $subscription->logs()->create([
                'status' => $newStatus,
                'changed_at' => now(),
            ]);
        });
    }

    /**
     * @throws Exception
     */
    public function activateSubscription($subscription): void
    {
        $pricingTier = $subscription->purchasable;

        // 1. Calculate the end of the current billing period
        $currentPeriodEnd = $this->calculateEndDate(
            'billing',
            now(),
            $pricingTier->billing_cycle,
        );

        // 2. Update subscription
        $subscription->update([
            'status' => getConstant('SUBSCRIPTION_STATUS_ACTIVE'),
            'start_date' => now(),
            'current_period_start' => now(),
            'current_period_end' => $currentPeriodEnd,
            'next_billing_date' => $currentPeriodEnd
        ]);

        $order = $subscription->lastOrder();
        $order->status = (string)getConstant('ORDER_STATUS_COMPLETED');
        $order->save();
    }

    public function moveSubscriptionToPastDue($subscription): void
    {
        $subscription->status = getConstant('SUBSCRIPTION_STATUS_PAST_DUE');
        $subscription->save();
    }

    public function moveSubscriptionToSuspended($subscription): void
    {
        $subscription->status = getConstant('SUBSCRIPTION_STATUS_SUSPENDED');
        $subscription->save();
    }

    public function moveSubscriptionToCancelled($subscription): void
    {
        $subscription->status = getConstant('SUBSCRIPTION_STATUS_CANCELLED');
        $subscription->save();
    }

    public function processCustomerCancelledSubscriptions($subscription): void
    {
        $subscription->status = getConstant('SUBSCRIPTION_STATUS_CANCELLED');
        $subscription->save();
    }

    public function moveSubscriptionToExpired($subscription): void
    {
        $subscription->status = getConstant('SUBSCRIPTION_STATUS_EXPIRED');
        $subscription->save();
    }

    public function moveTrialSubscriptionToPastDue($subscription): void
    {
        $subscription->status = getConstant('SUBSCRIPTION_STATUS_EXPIRED');
        $subscription->save();
    }

    public function moveTrialSubscriptionToSuspended($subscription): void
    {
        $subscription->status = getConstant('SUBSCRIPTION_STATUS_EXPIRED');
        $subscription->save();
    }

    public function moveTrialSubscriptionToCancelled($subscription): void
    {
        $subscription->status = getConstant('SUBSCRIPTION_STATUS_EXPIRED');
        $subscription->save();
    }

    public function moveTrialSubscriptionToExpired($subscription): void
    {
        $subscription->status = getConstant('SUBSCRIPTION_STATUS_EXPIRED');
        $subscription->save();
    }

    /**
     * @throws Exception
     */
    private function calculateEndDate($durationType, $startDate, $unit, $duration = 1)
    {
        if ($durationType == 'trial') {
            $methodMap = $this->getTrialMethodMap();
        } elseif ($durationType == 'billing') {
            $methodMap = $this->getBillingUnitMethodMap();
            // For quarterly and semi-annually billing cycles, we need to multiply the duration
            if ($unit == getConstant('BILLING_CYCLE_QUARTERLY')) {
                $duration *= 3;
            } elseif ($unit == getConstant('BILLING_CYCLE_SEMI_ANNUALLY')) {
                $duration *= 6;
            }
        } else {
            throw new Exception("Invalid duration type: $durationType");
        }

        if (!isset($methodMap[$unit])) {
            throw new Exception("Invalid duration unit: $unit");
        }

        $method = $methodMap[$unit];

        return $startDate->copy()->$method($duration);
    }

    private function getTrialMethodMap(): array
    {
        return [
            1 => 'addDays',
            2 => 'addWeeks',
            3 => 'addMonths',
            4 => 'addYears',
        ];
    }

    private function getBillingUnitMethodMap(): array
    {
        return [
            getConstant('BILLING_CYCLE_DAILY') => 'addDay',
            getConstant('BILLING_CYCLE_WEEKLY') => 'addWeek',
            getConstant('BILLING_CYCLE_MONTHLY') => 'addMonth',
            getConstant('BILLING_CYCLE_QUARTERLY') => 'addMonths',
            getConstant('BILLING_CYCLE_SEMI_ANNUALLY') => 'addMonths',
            getConstant('BILLING_CYCLE_YEARLY') => 'addYear',
        ];
    }

    public function getByOrderId($orderId)
    {
        return $this->subscriptionRepository->getByOrderId($orderId);
    }

    public function save($subscription)
    {
        $this->subscriptionRepository->save($subscription);
    }

    /**
     * Get the fields to display based on subscription status
     *
     * @param string $subscriptionStatus
     * @return array
     */
    public function getSubscriptionFields(Subscription $subscription, $lastOrder, int $subscriptionStatus): array
    {
        return match ($subscriptionStatus) {
            getConstant('SUBSCRIPTION_STATUS_SUSPENDED'), getConstant('SUBSCRIPTION_STATUS_ACTIVE') => [
                'Start Date' => $subscription->start_date->format('M d, Y'),
                'Current Period Start' => $subscription->current_period_start->format('M d, Y'),
                'Next Payment Date' => $subscription->next_billing_date->format('M d, Y'),
                'Last Order Date' => $lastOrder->created_at->format('M d, Y'),
                'Payment Method' => $lastOrder->payment_method == 'offline_payments' ? $lastOrder->offline_payment_method : $lastOrder->payment_method
            ],
            getConstant('SUBSCRIPTION_STATUS_TRIAL') => [
                'Trial Start Date' => $subscription->trial_start_date->format('M d, Y'),
                'Trial End Date' => $subscription->trial_end_date->format('M d, Y'),
                'Next Payment Date' => $subscription->next_billing_date->format('M d, Y'),
            ],
            getConstant('SUBSCRIPTION_STATUS_CANCELLED'), getConstant('SUBSCRIPTION_STATUS_INACTIVE') => [
                'Last Order Date' => $lastOrder->created_at->format('M d, Y'),
                'Payment Method' => $lastOrder->payment_method == 'offline_payments' ? $lastOrder->offline_payment_method : $lastOrder->payment_method
            ],
            getConstant('SUBSCRIPTION_STATUS_PAUSED') => [
                'Start Date' => $subscription->start_date->format('M d, Y'),
                'Current Period Start' => $subscription->current_period_start->format('M d, Y'),
                'Next Payment Date' => $subscription->next_billing_date->format('M d, Y'),
                'Last Order Date' => $lastOrder->created_at->format('M d, Y'),
            ],
            getConstant('SUBSCRIPTION_STATUS_EXPIRED') => [
                'Start Date' => $subscription->start_date->format('M d, Y'),
                'End Date' => $subscription->end_date->format('M d, Y'),
                'Last Order Date' => $lastOrder->created_at->format('M d, Y'),
                'Payment Method' => $lastOrder->payment_method == 'offline_payments' ? $lastOrder->offline_payment_method : $lastOrder->payment_method
            ],
            getConstant('SUBSCRIPTION_STATUS_PAST_DUE') => [
                'Start Date' => $subscription->start_date->format('M d, Y'),
                'Last Order Date' => $lastOrder->created_at->format('M d, Y'),
                'Next Payment Date' => $subscription->next_billing_date->format('M d, Y'),
                'Payment Method' => $lastOrder->payment_method == 'offline_payments' ? $lastOrder->offline_payment_method : $lastOrder->payment_method
            ],
            default => [],
        };
    }

    public function getProductFilesFromSubscription($subscription)
    {
        $purchasedPlanIds = [];

        // Collect all plan IDs from purchased PricingTiers
        $purchasedPlanIds[] = $subscription->purchasable->plan_id;

        $productFiles = $subscription->purchasable->product->files;

        // Filter files that are either global or have a matching plan ID in the pivot table
        return $productFiles->filter(function ($file) use ($purchasedPlanIds) {
            return $file->is_global || $file->plans->pluck('id')->intersect($purchasedPlanIds)->isNotEmpty();
        });
    }

    /**
     * @throws Exception
     */
//    public function create($userId, $orderId, $pricingTier)
//    {
//        // Check if the plan has a trial period
//        $trialStartDate = $pricingTier->trial_available ? Carbon::now() : null;
//        $trialEndDate = $pricingTier->trial_available ? $this->calculateEndDate(
//            'trial',
//            $trialStartDate,
//            $pricingTier->trial_duration_unit,
//            $pricingTier->trial_duration,
//        ) : null;
//
//        // Calculate the next billing date based on the billing cycle unit
//        $nextBillingDate = $this->calculateEndDate(
//            'billing',
//            $trialEndDate ?? Carbon::now(),
//            $pricingTier->billing_cycle,
//        );
//
//        // Create a new subscription entry
//        return $this->subscriptionRepository->create($userId, $orderId, $pricingTier, $pricingTier->trial_available, $trialStartDate, $trialEndDate, $nextBillingDate);
//    }
}
