<?php

namespace MtcPharmacy\Subscriptions\Classes;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Mtc\Modules\Members\Classes\Auth;
use Mtc\Modules\Members\Models\Member;
use Mtc\Shop\Assessment\Assessment;
use Mtc\Shop\Basket;
use Mtc\Shop\Basket\Address as BasketAddress;
use Mtc\Shop\Basket\Item as BasketItem;
use Mtc\Shop\Order;
use MtcPharmacy\Multisite\Classes\MultisiteSite;
use Illuminate\Support\Facades\Log;
use MtcPharmacy\Subscriptions\Classes\PaymentGateways\OpayoPaymentGateway;
use MtcPharmacy\Subscriptions\Classes\PaymentGateways\RyftPaymentGateway;
use MtcPharmacy\Subscriptions\Classes\PaymentGateways\TestPaymentGateway;
use MtcPharmacy\Subscriptions\Classes\Services\PaymentService;
use MtcPharmacy\Subscriptions\Classes\Traits\LogsChanges;


class Subscription extends Model
{
    use LogsChanges;

    const ORDER_CREATE_DAYS_BEFORE_DISPATCH = 3; // Number of days before next issue date to generate the order
    const STATUS_PENDING = 'pending';
    const STATUS_ACTIVE = 'active';
    const STATUS_PAUSED = 'paused';
    const STATUS_EXPIRED = 'expired';
    const STATUS_PAYMENT_FAILED = 'payment-failed';
    const STATUS_CANCELLED = 'cancelled';
    const STATUS_COMPLETED = 'completed';

    const INTERVAL_DAYS = 'days';
    const INTERVAL_WEEKS = 'weeks';
    const INTERVAL_MONTHS = 'months';

    const CANCELLATION_REASONS = [
        'Too expensive',
        'Not needed now',
        'Switched to another service',
        'Other'
    ];

    protected $table = 'subscriptions';

    protected $fillable = [
        'label',
        'price_per_issue',
        'interval_multiplier',
        'interval',
        'can_be_paused',
        'max_number_of_issues',
        'expiry_date',
        'next_issue_date',
        'previous_payment_id',
        'is_assessment_updated',
    ];

    protected $casts = [
        'can_be_paused' => 'boolean',
    ];

    private $rules = [
        'interval_multiplier' => 'integer | required',
        'interval' => 'required',
        'status' => 'required',
        'can_be_paused' => 'nullable | boolean',
        'max_number_of_issues' => 'nullable | integer | min:1',
        'expiry_date' => 'nullable | date',
        'next_issue_date' => 'nullable | date',
        'price_per_issue' => 'nullable | numeric',
    ];

    private function getValidationRules()
    {
        $this->rules['status'] = ['required', Rule::in(Subscription::get_allowed_statuses())];
        $this->rules['interval'] = ['required', Rule::in(Subscription::get_allowed_intervals())];

        return $this->rules;
    }


    public function validate()
    {
        return Validator::make($this->attributes, $this->getValidationRules());
    }



    public function items()
    {
        return $this->hasMany(SubscriptionItem::class);
    }


    public function orders()
    {
        return $this->belongsToMany(Order::class, 'subscriptions_orders');
    }


    public function member()
    {
        return $this->belongsTo(Member::class);
    }

    public function cancellations()
    {
        return $this->hasMany(SubscriptionCancellation::class);
    }

    public function logs()
    {
        return $this->hasMany(SubscriptionLog::class);
    }

    public static function get_allowed_statuses()
    {
        return [
            Subscription::STATUS_PENDING,
            Subscription::STATUS_ACTIVE,
            Subscription::STATUS_PAUSED,
            Subscription::STATUS_EXPIRED,
            Subscription::STATUS_PAYMENT_FAILED,
            Subscription::STATUS_CANCELLED,
            Subscription::STATUS_COMPLETED,
        ];
    }


    public static function get_allowed_intervals()
    {
        return [
            Subscription::INTERVAL_DAYS,
            Subscription::INTERVAL_WEEKS,
            Subscription::INTERVAL_MONTHS,
        ];
    }

    public function scopeActive($query)
    {
        return $query->where('status', Subscription::STATUS_ACTIVE);
    }


    public function isPaused()
    {
        return (bool) ($this->status == Subscription::STATUS_PAUSED);
    }

    public function setPaused()
    {
        $this->next_issue_date = null;
        $this->status = Subscription::STATUS_PAUSED;
        $this->save();
    }


    public function isActive()
    {
        return (bool) ($this->status == Subscription::STATUS_ACTIVE);
    }

    public function setActive()
    {
        $this->next_issue_date = $this->getNextIssueDate();
        $this->status = Subscription::STATUS_ACTIVE;
        $this->save();
    }


    public function isCancelled()
    {
        return (bool) ($this->status == Subscription::STATUS_CANCELLED);
    }

    public function setCancelled(
        $reason
    ) {
        $this->next_issue_date = null;
        $this->status = Subscription::STATUS_CANCELLED;
        $this->save();

        $this->cancellations()->create([
            'reason' => $reason
        ]);
    }


    public function setFailedPayment()
    {
        $this->status = Subscription::STATUS_PAYMENT_FAILED;
        $this->save();
    }

    public function hasFailedPayment()
    {
        return (bool) ($this->status == Subscription::STATUS_PAYMENT_FAILED);
    }

    public function getLatestFailedPayment()
    {
        $order = $this
            ->orders()
            ->where('order.paid', false)
            ->orderBy('order_id', 'desc')
            ->first()
        ;

        return $order;
    }


    public function getLatestSuccessfulOrder()
    {
        $order = $this
            ->orders()
            ->where('order.paid', true)
            ->orderBy('order_id', 'desc')
            ->first()
        ;

        return $order;
    }


    public function getLatestOrder()
    {
        $order = $this
            ->orders()
            ->orderBy('order_id', 'desc')
            ->first()
        ;

        return $order;
    }


    public function getCanBePaused()
    {
        // If can_be_paused is null (old subscriptions), default to true for backward compatibility
        if ($this->can_be_paused === null) {
            return true;
        }
        return $this->can_be_paused;
    }


    public function getCanBePausedString()
    {
        return $this->getCanBePaused() ? 'Y' : 'N';
    }


    public function getMaxNumberOfIssues()
    {
        return $this->max_number_of_issues ?? 99;
    }


    public function getPricePerIssue()
    {
        $price = 0;

        foreach ($this->items as $item) {
            if ($item->shop_item_size) {
                $price += $item->shop_item_size->price;
            } else {
                $price += $item->shop_item->price;
            }
        }

        return $this->price_per_issue ?? $price;
    }


    public function getNextIssueDate($force_new_date = false): string
    {
        $next_date = $this->next_issue_date;

        if (!$next_date || $force_new_date) {
            $latest_order = $this->getLatestOrder();
            $base_date = $latest_order
                ? Carbon::createFromFormat('Y-m-d H:i:s', $latest_order->date)
                : $this->created_at;

            // Start with the first possible next date
            $next_order_date = self::calculateNextIssueDate($base_date, $this->interval, $this->interval_multiplier);

            // If we're resuming from a pause, keep adding intervals until we find a date at least self::ORDER_CREATE_DAYS_BEFORE_DISPATCH days in the future
            $now = Carbon::now();
            $maxIterations = 100;
            $iterations = 0;
            while ($next_order_date->lt($now->copy()->addDays(self::ORDER_CREATE_DAYS_BEFORE_DISPATCH))) {
                if ($this->interval_multiplier <= 0 || ++$iterations >= $maxIterations) {
                    break;
                }
                $next_order_date = self::calculateNextIssueDate($next_order_date, $this->interval, $this->interval_multiplier);
            }

            $next_date = $next_order_date->format('Y-m-d');
        }

        return $next_date;
    }

    public static function calculateNextIssueDate(Carbon $base_date, string $interval, int $interval_multiplier): Carbon
    {
        switch ($interval) {
            case 'week':
            case self::INTERVAL_WEEKS:
                return $base_date->copy()->addWeeks($interval_multiplier);

            case 'month':
            case self::INTERVAL_MONTHS:
                return $base_date->copy()->addMonths($interval_multiplier);

            case self::INTERVAL_DAYS:
            case 'day':
            default:
                return $base_date->copy()->addDays($interval_multiplier);
        }
    }

    public function authoriseAccessByCurrentUser()
    {
        $can_access = false;

        // Check member ownership
        $logged_in_member = Auth::getLoggedInMember();

        if ($logged_in_member && $logged_in_member->id == $this->member_id) {
            $can_access = true;
        }

        // Check admin access with proper verification
        if (isset($_SESSION['adminId'])) {
            $admin = \Mtc\Core\Admin\User::find($_SESSION['adminId']);

            // Verify admin exists and has an active role
            if ($admin && $admin->role_id) {
                $role = $admin->role;

                // Verify role exists and is active
                if ($role) {
                    $can_access = true;
                }
            }
        }

        if (!$can_access) {
            abort(403, 'You have no permissions to access this subscription.');
        }
    }


    private function getShopItemCategories()
    {
        $categories = collect([]);

        foreach ($this->items as $sub_item) {
            $category = $sub_item->shop_item->getMainCategory();
            if ($category) {
                $categories->push($category);
            }
        }

        return $categories;
    }


    public function getSimilarSubscriptions(): array
    {
        $similar_subscriptions = [];

        $potentially_active_subscriptions = self::query()
            ->where('member_id', $this->member->id)
            ->where('id', '!=', $this->id)
            ->whereNotIn('status', [Subscription::STATUS_EXPIRED, Subscription::STATUS_CANCELLED])
            ->get()
        ;

        $subscription_item_category_ids = $this->getShopItemCategories()->pluck('id');

        $temp_items = collect([]);
        foreach ($potentially_active_subscriptions as $sub) {
            foreach ($sub->items as $sub_item) {
                $shop_item = $sub_item->shop_item;
                if (!$shop_item)
                    continue;

                if (!$shop_item->product_type)
                    continue; // Ignore general products.

                $mainCategory = $shop_item->getMainCategory();
                if ($mainCategory && !$subscription_item_category_ids->contains($mainCategory->id))
                    continue;

                $similar_subscriptions[] = $sub;
            }

        }

        return $similar_subscriptions;
    }


    public function getIssueNumber(Order $order)
    {
        return $this
            ->orders()
            ->where('order.date', '<=', $order->date)
            ->count()
        ;
    }


    public function canOrderBeGenerated()
    {
        $can = true;

        if (!$this->isActive()) {
            return false;
        }

        $today = Carbon::now();

        if ($this->expiry_date && Carbon::canBeCreatedFromFormat($this->expiry_date, 'Y-m-d')) {
            $expiry_date = Carbon::createFromFormat('Y-m-d', $this->expiry_date);
            if ($today->gt($expiry_date)) {
                $can = false;
            }
        }

        // Subtract 4 days for next_issue_date for order creation
        $next_order_creation_date = $this->getNextOrderDate();

        if ($today->format('Y-m-d') < $next_order_creation_date) {
            $can = false;
        }

        if ($this->orders()->count() >= $this->max_number_of_issues) {
            $can = false;
        }
        return $can;
    }

    function getNextOrderDate()
    {
        $next_issue_date = Carbon::createFromFormat('Y-m-d', $this->getNextIssueDate());
        $next_issue_date = $next_issue_date->subDays(self::ORDER_CREATE_DAYS_BEFORE_DISPATCH)->format('Y-m-d');
        return $next_issue_date;
    }

    public function getNextIssueDateForReminder()
    {
        $next_issue_date = Carbon::createFromFormat('Y-m-d', $this->getNextIssueDate());
        $next_issue_date = $next_issue_date->subDays(self::ORDER_CREATE_DAYS_BEFORE_DISPATCH + 1)->format('Y-m-d'); // 4
        return $next_issue_date;
    }

    public function generateNewOrder($ignore_checks = false, $attempt_token_payment = false)
    {
        $order = DB::transaction(function () use ($ignore_checks) {
            if (!$ignore_checks && !$this->canOrderBeGenerated()) {
                return null;
            }

            $basket = new \Mtc\Shop\Basket();
            $basket->customer()->associate($this->member);
            $basket->subscription()->associate($this);

            // Find the first order of the subscription to get details to copy
            $firstOrder = $this->orders()->first();
            $admin_id = null;
            $is_phone_order = false;
            $firstOrderBillingAddress = null;
            $firstOrderShippingAddress = null;

            if ($firstOrder) {
                // Copy delivery details from first order
                $basket->delivery = $firstOrder->delivery_method_id;

                $orderBasket = Basket::query()->find($firstOrder->basket_id);
                if ($orderBasket) {
                    $basket->billing_address_used = $orderBasket->billing_address_used;
                }

                // Copy admin_id if this was a phone/manual order
                if ($firstOrder->admin_id) {
                    $admin_id = $firstOrder->admin_id;

                    // Check if this was a phone order by looking at the Protx payment record
                    $firstOrderProtx = \Mtc\Shop\Order\Protx::where('order_id', $firstOrder->id)->first();
                    if ($firstOrderProtx && $firstOrderProtx->VendorTxCode && strpos($firstOrderProtx->VendorTxCode, 'PHONE-') === 0) {
                        $is_phone_order = true;
                    }
                }

                // Get addresses from first order to preserve all fields including gender
                $firstOrderAddresses = $firstOrder->address;
                if ($firstOrderAddresses) {
                    foreach ($firstOrderAddresses as $address) {
                        if ($address->type === 'billing') {
                            $firstOrderBillingAddress = $address;
                        } elseif ($address->type === 'shipping') {
                            $firstOrderShippingAddress = $address;
                        }
                    }
                }
            } else {
                // No previous order - use default delivery method (1 is typically standard delivery)
                $basket->delivery = 1;
                $basket->billing_address_used = 1; // Use billing address as delivery
            }

            $basket->save();

            foreach ($this->items as $subscription_item) {
                $basket_item = new BasketItem();
                $basket_item->item_id = $subscription_item->shop_item->id;
                $basket_item->quantity = 1;
                $basket_item->PLU = $subscription_item->shop_item->name;

                $basket_item->size = $subscription_item->shop_item_size ? $subscription_item->shop_item_size->size : 'One Size';
                $basket_item->sizeid = $subscription_item->shop_item_size_id ?? 0;

                // For calculating discount for subscription
                $basket_item->has_requested_subscription = 1;

                $basket_item->assessment_id = $this->getAssessmentId();

                $basket_item->save();
                $basket->items()->save($basket_item);
            }

            // Use addresses from first order if available (preserves gender and other fields)
            // Otherwise fall back to member's current addresses
            if ($firstOrderBillingAddress) {
                $billing_address_data = $firstOrderBillingAddress->toArray();
                unset($billing_address_data['id']); // Remove ID so a new address is created
                $order_billing_address = new BasketAddress($billing_address_data);
            } else {
                $billing_address_data = $this->member->addressBilling->toArray();
                $order_billing_address = new BasketAddress($billing_address_data);
            }
            $basket->billingAddress()->save($order_billing_address);

            if ($firstOrderShippingAddress) {
                $shipping_address_data = $firstOrderShippingAddress->toArray();
                unset($shipping_address_data['id']); // Remove ID so a new address is created
                $order_shipping_address = new BasketAddress($shipping_address_data);
                $basket->shippingAddress()->save($order_shipping_address);
            } elseif (isset($this->member->addressShipping)) {
                $shipping_address_data = $this->member->addressShipping->toArray();
                $order_shipping_address = new BasketAddress($shipping_address_data);
                $basket->shippingAddress()->save($order_shipping_address);
            } else {
                $basket->shippingAddress()->save($order_billing_address);
            }

            $basket_legacy = $basket->legacy();
            Order::newOrder($basket_legacy);
            $order = Order::find($basket_legacy->order_id);
            $order->legacy()->updateStatus(Order::STATUS_PROCESSING);

            $this->orders()->attach($order);
            // has_subscription to 0 for order
            $order->has_subscription = 0;

            // Copy admin_id if this was a phone/manual order
            if ($admin_id) {
                $order->admin_id = $admin_id;
            }

            // Generate PHONE- order reference if this was originally a phone order
            if ($is_phone_order) {
                // Generate a unique transaction ID for the phone order reference
                $transaction_id = 'sub_' . $this->id . '_' . date('Ymd_His') . '_' . substr(uniqid(), -4);
                $vendor_tx_code = 'PHONE-' . $transaction_id;
                $order->order_ref = $vendor_tx_code;

                // Create a Protx payment record for the phone order
                // This is needed so the order displays the PHONE- reference correctly
                \Mtc\Shop\Order\Protx::query()->updateOrCreate(
                    [
                        'order_id' => $order->id,
                    ],
                    [
                        'VendorTxCode' => $vendor_tx_code,
                        'VPSTxId' => '',
                        'TxAuthNo' => '',
                        'Amount' => $order->cost_total ?? 0,
                        'AVSCV2' => '',
                        'AddressResult' => '',
                        'PostCodeResult' => '',
                        'CV2Result' => '',
                        'GiftAid' => '',
                        '3DSecureStatus' => 'PHONE',
                        'CAVV' => '',
                        'SecurityKey' => '',
                        'released' => 1,
                        'released_amount' => $order->cost_total ?? 0,
                        'AmountRemaining' => 0,
                        'type' => 'phone',
                    ]
                );
            }

            $order->save();

            return $order;
        });

        // Payment collection involves external API calls and must be outside the DB transaction
        if ($order && $attempt_token_payment) {
            $this->collectPayment($order);
            $order->refresh();
        }

        return $order;
    }


    public function collectPayment(Order $order)
    {
        $gateway = $this->resolvePaymentGateway($order);
        $paymentService = new PaymentService($gateway);
        $paymentStatus = $paymentService->processPayment();

        return $paymentStatus;
    }


    /**
     * Resolve the appropriate payment gateway based on the subscription's previous payment method.
     */
    private function resolvePaymentGateway(Order $order): PaymentGatewayInterface
    {
        if (app()->environment('testing')) {
            return new TestPaymentGateway($order);
        }

        // Check if the subscription has a previous Ryft payment
        if ($this->previous_payment_id) {
            $ryftPaymentClass = 'Mtc\Plugins\RyftPay\Classes\RyftPayment';
            if (class_exists($ryftPaymentClass)) {
                $ryftPayment = $ryftPaymentClass::query()->where('remote_id', $this->previous_payment_id)->first();
                if ($ryftPayment) {
                    return new RyftPaymentGateway($order);
                }
            }

            // Check if the subscription has a previous Opayo payment
            $protx = \Mtc\Shop\Order\Protx::where('VPSTxId', $this->previous_payment_id)->first();
            if ($protx) {
                return new OpayoPaymentGateway($order);
            }
        }

        // No previous_payment_id means this is a phone/manual order -- use TestPaymentGateway
        if (empty($this->previous_payment_id)) {
            Log::info('No previous payment ID for subscription, using TestPaymentGateway (phone/manual order)', [
                'subscription_id' => $this->id,
                'order_id' => $order->id,
            ]);
            return new TestPaymentGateway($order);
        }

        // Has a previous_payment_id but no matching gateway -- this is a data integrity issue
        Log::error('No payment gateway resolved for subscription -- cannot process payment', [
            'subscription_id' => $this->id,
            'previous_payment_id' => $this->previous_payment_id,
            'order_id' => $order->id,
        ]);

        throw new \RuntimeException(
            "No payment gateway could be resolved for subscription {$this->id}. " .
            "Previous payment ID: {$this->previous_payment_id}"
        );
    }

    public function emailSubscriptionUpdates(
        $subject,
        $action = '',
        $content_below_basket = ''
    ) {
        global $twig; // instantiated in core/includes/header.inc.php
        global $email_content; // instantiated in shop/includes/header.inc.php

        $subscription = $this;
        $order_eloquent = $subscription->orders->first();

        $order = $order_eloquent ? new \Order($order_eloquent->id) : null;

        $site_config = MultisiteSite::getSiteConfig(
            ($order_eloquent && $order_eloquent->info) ? $order_eloquent->info->site : null
        );

        $order_sub_total = 0;
        $items = [];
        $url = '';
        $call_to_action = [];
        $email_template = 'emails/subscription_update_email.twig';
        foreach ($subscription->items as $subscription_item) {
            $subscription_preference = SubscriptionPreference::query()->where('item_size_id', $subscription_item->shop_item_size_id)->first();

            $subscription_item->shop_item->item_image = SITE_URL . $subscription_item->shop_item->getImagePath('default', 'thumbs');
            $subscription_item->shop_item->size = $subscription_item->shop_item_size;

            // Get price from size or fall back to item price if no size
            if ($subscription_item->shop_item->size) {
                $base_price = $subscription_item->shop_item->size->price;
            } else {
                $base_price = $subscription_item->shop_item->price;
            }

            // Apply discount if subscription preference exists
            if ($subscription_preference) {
                $discount_decimal = $subscription_preference->discount_percentage / 100;
                $discounted_price = round($base_price * (1 - $discount_decimal), 2);
            } else {
                $discounted_price = $base_price;
            }

            // Set the price on the size object if it exists, otherwise create a simple object
            if ($subscription_item->shop_item->size) {
                $subscription_item->shop_item->size->price = $discounted_price;
            } else {
                $subscription_item->shop_item->size = (object)['price' => $discounted_price];
            }

            $items[] = $subscription_item->shop_item;
            $order_sub_total += $subscription_item->shop_item->size->price;
            $assessment_id = $subscription_item->assessment_id;
        }

        // Send an email to customer notifying the changes in the dosages
        $to = $subscription->member->email;

        if ($action == 'update_assessment') {
            $assessment = Assessment::query()->find($this->getAssessmentId());
            if ($assessment) {
                $url = SITE_URL . '/assessment/' . $assessment->form_id . '?a=' . $assessment_id . '&redirect-type=subscription&subscription_id=' . $this->id;
            } else {
                $url = SITE_URL . '/member/orders/subscriptions';
            }
            $call_to_action = [
                'url' => $url,
                'text' => 'Please follow the link to provide the updated answers to the questionnaire.',
                'button_text' => 'Click Here'
            ];
        } else if ($action == 'pause_subscription') {
            $url = SITE_URL . '/member/orders/subscriptions';
            $call_to_action = [
                'url' => $url,
                'text' => 'No further issues will be sent until you reactivate your subscription. You can resume your subscription at any time by logging into your account.',
                'button_text' => 'Resume Subscription Now'
            ];
        } else if ($action == 'cancel_subscription') {
            $url = SITE_URL . '/member/orders/subscriptions';
            $site_name = $site_config['SITE_NAME'] ?? config('app.name', 'us');
            $call_to_action = [
                'url' => $url,
                'text' => "Thank you for choosing {$site_name}! We hope we've been able to support you during your journey. If you ever need us again, we'd love to welcome you back. You can place a new order at any time by logging into your account.",
                'button_text' => 'View Subscriptions'
            ];
        } else if ($action == 'update_subscription_reminder') {
            $email_template = 'emails/subscription_update_reminder_email.twig';
            $url = SITE_URL . '/member/orders/subscriptions';
            $call_to_action = [
                'url' => $url,
                'text' => "If you’d like to make any changes to your dosage, please log into your account and update your subscription before " . $subscription->getNextIssueDateForReminder(),
                'button_text' => 'Update Your Subscription Now'
            ];
        }

        ob_start();
        $options = [
            'email_content' => $email_content, // email content array contains site information for display within the template
            'order' => $order,
            'order_subtotal' => $order_sub_total,
            'order_date' => date('Y-m-d H:i:s'), // order date sent in full... twig can re-render into any date format from here
            'delivery_cost' => $order ? $order->getDelivery()->cost : 0, // Delivery Cost
            'delivery_name' => $order ? $order->getDelivery()->name : '', // Delivery Name
            'order_total' => $order_sub_total + ($order ? $order->getDelivery()->cost : 0),
            'site_config' => $site_config,
            'update_date' => date("Y-m-d H:i:s"),
            'items' => $items,
            'subscription' => $subscription,
            'next_issue_date' => $subscription->next_issue_date,
            'message' => $subject,
            'call_to_action' => $call_to_action,
            'content_below_basket' => $content_below_basket,
            'page_meta' => [],
        ];
        echo template($email_template, $options);
        $mail = ob_get_clean();

        email($to, $subject, $mail, [
            'queue' => true,
            'bcc' => config('site.shop_email'),
        ]);

        SubscriptionLog::logEmailSent(
            $this,
            $subject,
            $action ?: 'subscription_update'
        );
    }

    private function getAssessmentId()
    {
        $first_item = $this->items->first();
        return $first_item ? $first_item->assessment_id : null;
    }
}
