<?php

/**
 * Opayo Payment Gateway class for MTC Subscription Plugin
 *
 * @author: Ashok Basnet <ashok.basnet@mtc.co.uk>
 * @since: 01/06/2025
 */

namespace MtcPharmacy\Subscriptions\Classes\PaymentGateways;

use Order;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Event;
use Mtc\Modules\Members\Classes\Member;
use MtcPharmacy\Subscriptions\Classes\Subscription;
use MtcPharmacy\Subscriptions\Classes\SubscriptionLog;
use MtcPharmacy\Subscriptions\Classes\PaymentGatewayInterface;

class OpayoPaymentGateway extends PaymentGatewayInterface
{
    private $strConnectTo;
    private $strVendorName;
    private $strCurrency;
    private $strProtocol;
    private $strRepeatURL;

    public function __construct($order)
    {
        parent::__construct($order);

        // Initialize Opayo configuration
        $this->strConnectTo = SAGEPAY_CONNECT;
        $this->strVendorName = SAGEPAY_VENDOR;
        $this->strCurrency = SAGEPAY_CURRENCY;
        $this->strProtocol = "4.00";

        // Set the appropriate URL based on environment
        if ($this->strConnectTo == "LIVE") {
            $this->strRepeatURL = "https://live.sagepay.com/gateway/service/repeat.vsp";
        } elseif ($this->strConnectTo == "TEST") {
            $this->strRepeatURL = "https://sandbox.opayo.eu.elavon.com/gateway/service/repeat.vsp";
        } else {
            // Simulator
            $this->strRepeatURL = "https://sandbox.opayo.eu.elavon.com/simulator/VSPServerGateway.asp?Service=VendorRepeatTx";
        }
    }


    public function processPayment()
    {
        if (defined('SAGEPAY_SERVER') && SAGEPAY_SERVER === "1") {
            try {
                Log::info('Opayo subscription payment started', [
                    'order_id' => $this->order->id,
                    'subscription_id' => $this->order->subscription_id,
                    'member_id' => $this->order->member
                ]);

                $order = new Order($this->order->id);
                $subscription = Subscription::find($this->order->subscription_id);
                $member = Member::find($this->order->member);

                if (!$subscription) {
                    Log::error('Opayo subscription payment failed: Subscription not found', [
                        'subscription_id' => $this->order->subscription_id,
                        'order_id' => $this->order->id,
                    ]);
                    return false;
                }

                if (!$member) {
                    Log::error('Opayo subscription payment failed: Member not found', [
                        'member_id' => $this->order->member,
                        'order_id' => $this->order->id,
                    ]);
                    return false;
                }

                // Check if member has a stored Opayo token
                if (empty($member->sagepay_token)) {
                    Log::error('Opayo subscription payment failed: No stored token', [
                        'order_id' => $order->getId(),
                        'member_id' => $member->id,
                        'subscription_id' => $subscription->id
                    ]);
                    Event::dispatch('subscriptions.paymentFailed', $subscription);
                    return false;
                }

                // Get the original transaction details for the repeat payment
                $originalOrderProtx = $this->getPreviousPaymentInfo();
                if (empty($originalOrderProtx)) {
                    Log::error('Opayo subscription payment failed: No previous payment reference', [
                        'order_id' => $order->getId(),
                        'member_id' => $member->id,
                        'subscription_id' => $subscription->id
                    ]);
                    Event::dispatch('subscriptions.paymentFailed', $subscription);
                    return false;
                }

                // Generate new transaction code for this payment
                $strVendorTxCode = config('site.tnx_prefix') . '-SUB-' . $order->getId() . '-' . time();
                $totalAmount = number_format($order->getTotalCost(), 2, '.', '');

                // Build the repeat payment request
                $strPost = $this->buildRepeatPaymentRequest(
                    $strVendorTxCode,
                    $totalAmount,
                    $originalOrderProtx,
                    $member
                );

                Log::debug('Opayo subscription payment request', [
                    'order_id' => $order->getId(),
                    'subscription_id' => $subscription->id,
                    'amount' => $totalAmount,
                    'vendor_tx_code' => $strVendorTxCode,
                ]);

                // Make the API call to Opayo

                $response = $this->requestPost($this->strRepeatURL, $strPost);

                SubscriptionLog::logPaymentAttempted(
                    $subscription,
                    $order->getTotalCost(),
                    SubscriptionLog::SOURCE_SYSTEM,
                    ['command' => 'payment_attempted', 'order_id' => $order->getId()]
                );


                Log::info('Opayo subscription payment response', [
                    'order_id' => $order->getId(),
                    'response' => $response
                ]);

                // Process the response
                if (isset($response['Status']) && $response['Status'] === 'OK') {
                    // Payment successful
                    $order->markPaid(\Order::PAYMENT_TYPE_SAGEPAY_SERVER);

                    // Save the payment details
                    $this->savePaymentDetails($order, $response, $strVendorTxCode);

                    // Send order confirmation
                    $order->sendOrderConfirmation();

                    Log::info('Opayo subscription payment successful', [
                        'order_id' => $order->getId(),
                        'tx_auth_no' => $response['TxAuthNo'] ?? '',
                        'vps_tx_id' => $response['VPSTxId'] ?? ''
                    ]);

                    SubscriptionLog::logPaymentSucceeded(
                        $subscription,
                        $order->getTotalCost(),
                        null,
                        SubscriptionLog::SOURCE_SYSTEM,
                        ['command' => 'payment_succeeded', 'order_id' => $order->getId()]
                    );

                    return true;
                } else {
                    // Payment failed
                    $statusDetail = $response['StatusDetail'] ?? 'Unknown error';
                    $status = $response['Status'] ?? 'Unknown';

                    // Check for specific error conditions and provide helpful messages
                    $errorContext = [
                        'order_id' => $order->getId(),
                        'status' => $status,
                        'status_detail' => $statusDetail,
                        'response' => $response
                    ];

                    if ($status === 'INVALID') {
                        if (strpos($statusDetail, 'RecurringExpiry') !== false) {
                            if (strpos($statusDetail, 'missing') !== false) {
                                $errorContext['fix_suggestion'] = 'RecurringExpiry field is missing. Should be in MMYYYY format (e.g., 062030)';
                            } else {
                                $errorContext['fix_suggestion'] = 'RecurringExpiry field format is invalid. Expected MMYYYY format (e.g., 062030 for June 2030)';
                            }
                        } elseif (strpos($statusDetail, 'Token') !== false) {
                            $errorContext['fix_suggestion'] = 'Token field issue. Check if the stored token is valid and not expired.';
                        } elseif (strpos($statusDetail, 'RelatedVendorTxCode') !== false) {
                            $errorContext['fix_suggestion'] = 'Original transaction reference not found. Check if the related transaction exists.';
                        } elseif (strpos($statusDetail, 'InitiatedType') !== false) {
                            $errorContext['fix_suggestion'] = 'InitiatedType field issue. Should be MIT for merchant-initiated transactions.';
                        }
                    }

                    Log::error('Opayo subscription payment failed', $errorContext);

                    Event::dispatch('subscriptions.paymentFailed', $subscription);
                    return false;
                }

            } catch (\Exception $e) {
                Log::error('Opayo subscription payment exception', [
                    'order_id' => $this->order->id,
                    'error' => $e->getMessage(),
                    'trace' => $e->getTraceAsString()
                ]);

                if (isset($subscription)) {
                    Event::dispatch('subscriptions.paymentFailed', $subscription);
                }

                return false;
            }
        }
        return false;
    }

    public function getPreviousPaymentInfo()
    {
        try {
            $subscription = Subscription::find($this->order->subscription_id);

            // Try to get the original order's transaction code
            $originalOrder = $subscription->orders()->where('paid', 1)->first();

            if ($originalOrder) {
                // Get the VendorTxCode from the original order
                $db = new \MtcPdo();
                $sql = "SELECT * FROM order_protx WHERE order_id = :order_id LIMIT 1";
                $db->run($sql, [':order_id' => $originalOrder->id]);

                if ($db->num_rows() > 0) {
                    $data = $db->fetch_array();
                    return $data;
                }
            }
            return null;

        } catch (\Exception $e) {
            Log::error('Error getting previous payment info', [
                'order_id' => $this->order->id,
                'error' => $e->getMessage()
            ]);
            return null;
        }
    }

    private function buildRepeatPaymentRequest($vendorTxCode, $amount, $originalOrderProtx, $member)
    {
        $strPost = "VPSProtocol=" . $this->strProtocol;
        $strPost .= "&TxType=REPEAT";
        $strPost .= "&Vendor=" . $this->strVendorName;
        $strPost .= "&VendorTxCode=" . $vendorTxCode;
        $strPost .= "&Amount=" . $amount;
        $strPost .= "&Currency=" . $this->strCurrency;
        $strPost .= "&Description=" . urlencode(config('app.name') . " Subscription Payment");
        $strPost .= "&RelatedVendorTxCode=" . $originalOrderProtx['VendorTxCode'];
        $strPost .= "&RelatedVPSTxId=" . $originalOrderProtx['VPSTxId'];
        $strPost .= "&RelatedSecurityKey=" . $originalOrderProtx['SecurityKey'];
        $strPost .= "&RelatedTxAuthNo=" . $originalOrderProtx['TxAuthNo'];
        $strPost .= '&SchemeTraceID=' . $originalOrderProtx['SchemeTraceID'];

        // Add the stored token for the repeat payment
        if (!empty($member->sagepay_token)) {
            $strPost .= "&Token=" . $member->sagepay_token;
        }

        // Required fields for recurring payments
        $strPost .= "&InitiatedType=MIT"; // Merchant Initiated Transaction
        $strPost .= "&MITType=UNSCHEDULED"; // Recurring payment
        $strPost .= "&COFUsage=SUBSEQUENT"; // Subsequent use of stored credentials

        $recurringExpiryDate = $member->sagepay_expiry;
        $strPost .= '&RecurringExpiry=' . urlencode($recurringExpiryDate);

        // Log the recurring expiry date for debugging
        Log::info('Opayo RecurringExpiry value', [
            'vendor_tx_code' => $vendorTxCode,
            'recurring_expiry' => $recurringExpiryDate,
            'original_tx_code' => $originalOrderProtx['VendorTxCode'],
            'token_included' => !empty($member->sagepay_token),
            'token_present' => !empty($member->sagepay_token)
        ]);

        // Add SchemeTraceID if available (helps with some card schemes)
        // $schemeTraceId = $this->getSchemeTraceId($originalTxCode);
        // if (!empty($schemeTraceId)) {
        //     $strPost .= "&SchemeTraceID=" . $schemeTraceId;
        // }

        return $strPost;
    }

    private function savePaymentDetails($order, $response, $vendorTxCode)
    {
        try {
            $db = new \MtcPdo();

            $sql = "INSERT INTO order_protx (
                order_id, VendorTxCode, VPSTxId,
                TxAuthNo, AVSCV2, AddressResult, PostCodeResult, CV2Result,
                SecurityKey, Amount,`type`, SchemeTraceID, BankAuthCode,
                DeclineCode, protocol_version, 3DSecureStatus
            ) VALUES (
                :order_id, :vendor_tx_code, :vps_tx_id,
                :tx_auth_no, :avscv2, :address_result, :post_code_result, :cv2_result,
                :security_key, :amount, 'REPEAT', :scheme_trace_id, :bank_auth_code,
                :decline_code, :protocol_version, :3d_secure_status
            )";

            $params = [
                ':order_id' => $order->getId(),
                ':vendor_tx_code' => $vendorTxCode,
                ':vps_tx_id' => $response['VPSTxId'] ?? '',
                ':tx_auth_no' => $response['TxAuthNo'] ?? '',
                ':avscv2' => $response['AVSCV2'] ?? '',
                ':address_result' => $response['AddressResult'] ?? '',
                ':post_code_result' => $response['PostCodeResult'] ?? '',
                ':cv2_result' => $response['CV2Result'] ?? '',
                ':security_key' => $response['SecurityKey'] ?? '',
                ':amount' => $order->getTotalCost(),
                ':scheme_trace_id' => $response['SchemeTraceID'] ?? '',
                ':bank_auth_code' => $response['BankAuthCode'] ?? '',
                ':decline_code' => $response['DeclineCode'] ?? '',
                ':protocol_version' => $response['VPSProtocol'] ?? '',
                ':3d_secure_status' => $response['3DSecureStatus'] ?? ''
            ];

            $db->run($sql, $params);

        } catch (\Exception $e) {
            Log::error('Error saving Opayo payment details', [
                'order_id' => $order->getId(),
                'error' => $e->getMessage()
            ]);
        }
    }

    private function requestPost($url, $data)
    {
        // Set a one-minute timeout for this script
        set_time_limit(60);

        // Initialise output variable
        $output = array();

        // Open the cURL session
        $curlSession = curl_init();

        // Set the URL
        curl_setopt($curlSession, CURLOPT_URL, $url);
        // No headers, please
        curl_setopt($curlSession, CURLOPT_HEADER, 0);
        // It's a POST request
        curl_setopt($curlSession, CURLOPT_POST, 1);
        // Set the fields for the POST
        curl_setopt($curlSession, CURLOPT_POSTFIELDS, $data);
        // Return it direct, don't print it out
        curl_setopt($curlSession, CURLOPT_RETURNTRANSFER, 1);
        // This connection will timeout in 30 seconds
        curl_setopt($curlSession, CURLOPT_TIMEOUT, 30);
        // SSL settings
        curl_setopt($curlSession, CURLOPT_SSL_VERIFYPEER, TRUE);
        curl_setopt($curlSession, CURLOPT_SSL_VERIFYHOST, 2);

        // Send the request and store the result in an array
        $rawresponse = curl_exec($curlSession);

        // Check that a connection was made
        if (curl_error($curlSession)) {
            $output['Status'] = "FAIL";
            $output['StatusDetail'] = curl_error($curlSession);
            curl_close($curlSession);
            return $output;
        }

        // Split response into name=value pairs
        $response = mb_split(chr(10), $rawresponse);

        // Close the cURL session
        curl_close($curlSession);

        // Tokenise the response
        for ($i = 0; $i < count($response); $i++) {
            // Find position of first "=" character
            $splitAt = strpos($response[$i], "=");
            if ($splitAt !== false) {
                // Create an associative (hash) array with key/value pairs ('trim' strips excess whitespace)
                $output[trim(substr($response[$i], 0, $splitAt))] = trim(substr($response[$i], ($splitAt + 1)));
            }
        }

        return $output;
    }
}