<?php

namespace Mtc\Plugins\RyftPay\Classes;

use Basket;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Log;
use Order;
use Mtc\Shop\Order\Note;

class RyftPay
{
    const PAYMENT_SESSIONS_URI = 'payment-sessions';

    public array $errors = [];
    private Basket $basket;
    private array $paymentSession;

    const METHOD_POST = 'post';
    const METHOD_GET = 'get';

    const REMOTE_STATUS_VOIDED = 'Voided';
    const REMOTE_STATUS_CAPTURED = 'Captured';
    const REMOTE_STATUS_APPROVED = 'Approved';

    public function __construct()
    {
    }

    /**
     * Outputs a form of ryft payments
     */
    public static function form(Basket $basket): void
    {
        $ryft = new self();
        $ryft->basket = $basket;

        $_SESSION['ryft_order_id'] = $ryft->basket->order_id;

        $ryft->createPaymentSession()
            ->createPayment()
            ->outputForm();
    }

    private function outputForm(): void
    {
        $twig = app('twig');
        echo $twig->render('RyftPay/form.twig', [
            'dev' => config('ryftpay.testmode'),
            'site_name' => config('app.name'),
            'ryft_public_key' => config('ryftpay.public_key'),
            'ryft_client_secret' => $this->paymentSession ? $this->paymentSession['clientSecret'] : '',
            'ryft_gpay_enabled' => config('ryftpay.gpay.enabled') ? 1 : 0,
            'ryft_apay_enabled' => config('ryftpay.apay.enabled') ? 1 : 0,
            'customer_email' => $this->basket->info['email'],
            'ryft_errors' => $this->errors,
        ]);
    }

    /**
     * Creates payment session
     */
    public function createPaymentSession(): static
    {
        $data = [
            'amount' => self::getCents($this->basket->cost_total),
            'currency' => 'GBP',
            'customerEmail' => $this->basket->info['email'],
            'metadata' => [
                'orderId' => $this->basket->order_id,
                'customerId' => $this->basket->member,
            ],
            'returnUrl' => SITE_URL . '/plugins/RyftPay/checkout/return.php',
            'captureFlow' => config('ryftpay.manual_capture') ? 'Manual' : 'Automatic',
        ];

        $this->paymentSession = $this->call($data, self::PAYMENT_SESSIONS_URI);
        return $this;
    }

    /**
     * Get remote payment session
     *
     * @param RyftPayment $payment
     * @return array[]|null
     */
    public function getPaymentSession(RyftPayment $payment): ?array
    {
        $uri = self::PAYMENT_SESSIONS_URI . '/' . $payment->remote_id;
        $paymentSession = $this->call([], $uri, true);
        if (empty($paymentSession['id'])) {
            return null;
        }
        return $paymentSession;
    }

    /**
     * Creates a payment with status 'Pending'
     *
     * @return $this
     */
    public function createPayment(): static
    {
        RyftPayment::query()
            ->create([
                'order_id' => $this->basket->order_id,
                'member_id' => $this->basket->member,
                'remote_id' => $this->paymentSession ? $this->paymentSession['id'] : null,
                'amount' => $this->basket->cost_total,
                'currency' => 'GBP',
                'status' => RyftPayment::STATUS_PENDING,
                'method' => RyftPayment::METHOD_CARD,
            ]);
        return $this;
    }

    /**
     * Processes webhook request
     *
     * @param $event
     * @throws Exception
     */
    public static function processWebhookRequest($event): void
    {
        if (empty($event['data']['id'])) {
            throw new Exception('Event ID not found');
        }

        $eventType = $event['eventType'];

        /** @var RyftPayment $payment */
        $payment = RyftPayment::query()
            ->where('remote_id', $event['data']['id'])
            ->firstOrFail();

        if ($eventType === RyftPayment::EVENT_APPROVED) {

            if ($payment->status !== RyftPayment::STATUS_PENDING) {
                // Payment has already been authorised
                return;
            }
            $payment->status = RyftPayment::STATUS_RESERVED;
            $payment->transaction_id = $event['data']['paymentTransactionId'];
            $payment->save();

            self::completeOrder($payment);
        }

        if ($eventType === RyftPayment::EVENT_CAPTURED) {
            if ($payment->status === RyftPayment::STATUS_PAID) {
                // Payment has already been captured
                return;
            }

            /** Only mark paid when changing from Pending to Complete */
            if ($payment->status === RyftPayment::STATUS_PENDING) {

                $payment->transaction_id = $event['data']['paymentTransactionId'];

                self::completeOrder($payment);
            }
            $payment->status = RyftPayment::STATUS_PAID;
            $payment->save();
        }

    }

    /**
     * Completes order, marks paid, sends confirmation email
     *
     * @throws Exception
     */
    protected static function completeOrder(RyftPayment $payment): void
    {
        $order = new Order($payment->order_id);
        if ($order->paid) {
            return;
        }

        $order->markPaid(self::class);
        $order->sendOrderConfirmation();

        if (defined('PPC_TRACKING') && PPC_TRACKING) {
            track('sale', $order->getTotalCost(), $order->ref, $order->keywords);
        }
    }

    /**
     * Adding payment gateway info to the order
     *
     * @param Order $order
     */
    public static function initializePayment(Order $order): void
    {
        /** @var RyftPayment $payment */
        $payment = RyftPayment::query()
            ->where('order_id', $order->getId())
            ->first();

        if (empty($payment)) {
            return;
        }

        $order->order_ref = 'C-' . $order->getId();
        $order->amount_paid = $payment->amount;
        $order->payment_gateway = RyftPayment::$paymentMethods[$payment->method]['name'];
        $order->payment_icon = RyftPayment::$paymentMethods[$payment->method]['icon'];
        $order->ryftpay_payment = $payment;
    }

    /**
     * Calls Ryft API
     *
     * @param array $data
     * @param string $uri
     * @param string $method
     * @return array
     */
    protected function call(array $data, string $uri, string $method = self::METHOD_POST): array
    {
        try {
            $ch = curl_init();
            $url = config('ryftpay.api_endpoint') . $uri;

            $params = [
                CURLOPT_URL => $url,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_ENCODING => '',
                CURLOPT_MAXREDIRS => 10,
                CURLOPT_TIMEOUT => 0,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
                CURLOPT_HTTPHEADER => [
                    'Content-Type: application/json',
                    'Authorization: ' . config('ryftpay.secret_key'),
                ],
            ];

            if ($method === self::METHOD_POST) {
                $params[CURLOPT_POST] = true;
                $params[CURLOPT_POSTFIELDS] = json_encode($data);
            }

            curl_setopt_array($ch, $params);

            $response = json_decode(curl_exec($ch), true);
            if (!empty($response['errors'])) {

                Log::error('RYFT API CALL ERRORS');
                Log::error('URI: ' . $uri);
                Log::error('REQUEST DATA: ' . print_r($data, true));
                Log::error('RESPONSE: ' . $response);

                $this->errors = $response['errors'];
                return [
                    'errors' => $this->errors,
                ];
            }

        } catch (Exception $e) {

            Log::error('RYFT API CALL EXCEPTION');
            Log::error('ERROR: ' . $e->getMessage());
            Log::error('URI: ' . $uri);
            Log::error('REQUEST DATA: ' . print_r($data, true));

            $this->errors[] = $e->getMessage();
            return [
                'errors' => $this->errors,
            ];
        }
        return $response;
    }

    /**
     * Listens to order status update event
     *
     * @param Order $order
     * @return void
     */
    public static function orderStatusUpdateListener(Order $order): void
    {
        if (!config('ryftpay.manual_capture')) {
            return;
        }
        /** @var RyftPayment $payment */
        $payment = RyftPayment::query()
            ->where('order_id', $order->getId())
            ->first();
        if (empty($payment)) {
            return;
        }
        if ($payment->status !== RyftPayment::STATUS_RESERVED) {
            return;
        }
        if ((int) $order->status === \Mtc\Shop\Order::STATUS_SHIPPED) {
            // Capture payment
            (new self())->capturePayment($payment, $order->getTotalCost());
        }
        if ((int) $order->status === \Mtc\Shop\Order::STATUS_CANCELLED) {
            // Cancel payment
            (new self())->cancelPayment($payment);
        }
    }

    /**
     * Charges authorised payment
     *
     * @param RyftPayment $payment
     * @param $amount
     * @return bool
     */
    public function capturePayment(RyftPayment $payment, $amount = null): bool
    {
        if ($payment->status !== RyftPayment::STATUS_RESERVED) {
            return false;
        }

        $paymentSession = $this->getPaymentSession($payment);
        if (empty($paymentSession)) {
            return false;
        }

        if (empty($paymentSession['status'])) {
            return false;
        }

        if ($paymentSession['status'] !== self::REMOTE_STATUS_APPROVED) {
            $this->updatePaymentStatusFromRemote($payment, $paymentSession['status']);
            return false;
        }

        if (empty($amount)) {
            // If no amount specified, we capture full payment amount
            $amount = $payment->amount;
        } elseif ($amount > $payment->amount) {
            // If for some reason the specified amount exceeds full payment amount, we capture full payment amount.
            $amount = $payment->amount;
        }

        $data = [
            'amount' => self::getCents($amount),
        ];

        $uri = self::PAYMENT_SESSIONS_URI . '/' . $payment->remote_id . '/captures';

        $response = $this->call($data, $uri);
        if (empty($response['id'])) {

            Log::error('RYFT CAPTURE ERROR');
            Log::error('URI: ' . $uri);
            Log::error('REQUEST DATA: ' . print_r($data, true));
            Log::error('RESPONSE: ' . print_r($response, true));

            return false;
        }

        Log::debug('RYFT LOG API RESPONSE');
        Log::debug('URI: ' . $uri);
        Log::debug('REQUEST DATA: ' . print_r($data, true));
        Log::debug('RESPONSE: ' . print_r($response, true));

        $payment->status = RyftPayment::STATUS_PAID;
        $payment->save();

        return true;
    }

    /**
     * Cancels authorised payment
     *
     * @param RyftPayment $payment
     * @return bool
     */
    public function cancelPayment(RyftPayment $payment): bool
    {
        if ($payment->status !== RyftPayment::STATUS_RESERVED) {
            return false;
        }

        $paymentSession = $this->getPaymentSession($payment);
        if (empty($paymentSession)) {
            return false;
        }

        if ($paymentSession['status'] !== self::REMOTE_STATUS_APPROVED) {
            $this->updatePaymentStatusFromRemote($payment, $paymentSession['status']);
            return false;
        }

        $uri = self::PAYMENT_SESSIONS_URI . '/' . $payment->remote_id . '/voids';

        $transaction = $this->call([], $uri);
        if (empty($transaction['id'])) {
            return false;
        }

        $payment->status = RyftPayment::STATUS_CANCELLED;
        $payment->save();

        return true;
    }

    /**
     * Updates Ryft status based on the remote status
     *
     * @param RyftPayment $payment
     * @param $remoteStatus
     * @return void
     */
    protected function updatePaymentStatusFromRemote(RyftPayment $payment, $remoteStatus): void
    {
        if ($remoteStatus === self::REMOTE_STATUS_VOIDED) {
            $payment->status = RyftPayment::STATUS_CANCELLED;
            $payment->save();
            return;
        }
        if ($remoteStatus === self::REMOTE_STATUS_CAPTURED) {
            $payment->status = RyftPayment::STATUS_PAID;
            $payment->save();
        }
    }

    /**
     * Handles refund done on order
     *
     * @param $orderId
     * @param $amount
     * @return void
     * @throws Exception
     */
    public static function handleRefund($orderId, $amount): void
    {
        Log::debug('handleRefund');
        /** @var RyftPayment $payment */
        $payment = RyftPayment::query()
            ->where('order_id', $orderId)
            ->whereIn('status', [
                RyftPayment::STATUS_RESERVED,
                RyftPayment::STATUS_PAID,
            ])
            ->firstOrFail();

        Log::debug(print_r($payment, true));
        if (!empty($amount)) {
            $amount = number_format($amount, 2);
            $fullRefund = $amount == $payment->amount;
        } else {
            $amount = $payment->amount;
            $fullRefund = true;
        }

        if ($payment->status === RyftPayment::STATUS_PAID) {
            $result = (new self())->refundPayment($payment, $fullRefund ? null : $amount);
        } else {
            if ($fullRefund) {
                $result = (new self())->cancelPayment($payment);
            } else {
                $chargeAmount = $payment->amount - $amount;
                $result = (new self())->capturePayment($payment, $chargeAmount);
            }
        }

        if (!$result) {
            throw new Exception('Failed to create a refund!');
        }

        Note::query()
            ->create([
                'order_id' => $orderId,
                'note' => 'A RyftPay refund request for £' . $amount . ' has been sent',
            ]);
    }

    /**
     * Refunds payment
     *
     * @param RyftPayment $payment
     * @param null $amount
     * @return bool
     */
    public function refundPayment(RyftPayment $payment, $amount = null): bool
    {
        if ($payment->status !== RyftPayment::STATUS_PAID) {
            return false;
        }

        $data = [
            'amount' => self::getCents($amount ?: $payment->amount),
        ];

        $uri = self::PAYMENT_SESSIONS_URI . '/' . $payment->remote_id . '/refunds';

        $transaction = $this->call($data, $uri);
        if (empty($transaction['id'])) {
            return false;
        }

        $payment->status = RyftPayment::STATUS_REFUNDED;
        $payment->save();

        return true;
    }

    /**
     * Gets cents from an amount
     *
     * @param $amount
     * @return int
     */
    protected static function getCents($amount): int
    {
        return ceil($amount * 100);
    }

    /**
     * Bulk capture payments
     *
     * @return void
     */
    public static function bulkProcessReservedPayments(): void
    {
        RyftPayment::query()
            ->where('status', RyftPayment::STATUS_RESERVED)
            ->where('created_at', '<', Carbon::now()->subDays(6))
            ->get()
            ->each(function (RyftPayment $payment) {
                (new self())->capturePayment($payment);
            });
    }

    public static function displayPaymentDetails(\Order $order): void
    {
        if (empty($order->ryftpay_payment)) {
            return;
        }

        echo app('twig')->render('RyftPay/admin/payment.twig', [
            'payment' => $order->ryftpay_payment,
        ]);
    }
}