<?php

namespace YooMoney\Helpers;

use Exception;
use JUri;
use YooKassa\Model\CancellationDetailsInterface;
use YooKassa\Model\CancellationDetailsPartyCode;
use YooKassa\Model\CancellationDetailsReasonCode;
use YooKassa\Model\Notification\NotificationRefundSucceeded;
use YooKassa\Model\NotificationEventType;
use YooKassa\Model\PaymentMethod\PaymentMethodSberLoan;
use YooKassa\Model\PaymentStatus;
use YooKassa\Model\Refund;
use YooKassa\Model\RefundStatus;
use YooKassa\Model\PaymentMethodType;
use YooMoney\Model\KassaPaymentMethod;
use YooMoney\Model\OrderModel;
use Joomla\Component\Jshopping\Site\Table\OrderTable;

/**
 * В классе объединены методы для обработки входящих уведомлений от Юkassa
 */
class TransactionHelper
{
    const REFUNDED_STATUS_ID = 4;

    /**
     * @var LoggerHelper
     */
    private $logger;

    /**
     * @var OrderHelper
     */
    private $orderHelper;

    /**
     * @var ReceiptHelper
     */
    private $receiptHelper;

    /**
     * @var YookassaNotificationFactory
     */
    private $yooNotificationHelper;

    /**
     * @var OrderModel
     */
    private $orderModel;

    /** @var DatabaseHelper */
    private $databaseHelper;

    public function __construct(LoggerHelper $logger = null)
    {
        $this->logger = $logger ?: new LoggerHelper();
        $this->yooNotificationHelper = new YookassaNotificationFactory($this->logger);
        $this->orderHelper = new OrderHelper();
        $this->receiptHelper = new ReceiptHelper($this->logger);
        $this->orderModel = new OrderModel($this->logger);
        $this->databaseHelper = new DatabaseHelper();
    }

    /**
     * Обрабатывает уведомление от Юkassa в зависимости от статуса платежа в уведомлении
     *
     * @param KassaPaymentMethod $kassa
     * @param array $pmConfigs
     * @param OrderTable $order
     * @return bool|void
     * @throws \Exception
     */
    public function processNotification($kassa, $pmConfigs, $order)
    {
        $notificationObj = $this->yooNotificationHelper->getNotificationObject();
        $paymentId = $notificationObj instanceof NotificationRefundSucceeded
            ? $notificationObj->getObject()->getPaymentId()
            : $notificationObj->getObject()->getId();

        $shopId = $kassa->getShopId() ?: 'null';
        $metadata = $notificationObj->getObject()->getMetadata();
        $this->logger->sendHeka(array('payment.notification.init'));

        if (
            !empty($metadata['cms_name'])
            && !in_array($metadata['cms_name'], array(KassaPaymentMethod::CMS_NAME, KassaPaymentMethod::CMS_NAME_OLD), true)
        ) {
            $this->logger->sendHeka(array('payment.notification.skip'));
            $this->logger->log('info', 'This notification not for Joomla. This notification for: ' . $metadata['cms_name']);
            return true;
        }

        $refund = null;
        if ($notificationObj->getEvent() === NotificationEventType::REFUND_SUCCEEDED) {
            $refundId = $notificationObj->getObject()->getId();
            $refund = $kassa->fetchRefund($refundId);

            if (!$refund) {
                $this->logger->sendHeka(array('payment.notification.skip'));
                $this->logger->log('debug', 'Notification error: refund is not exist');
                header('HTTP/1.1 404 Refund is not exist');
                die();
            }
            $paymentId = $refund->getPaymentId();
        }

        $payment = $kassa->fetchPayment($paymentId);
        if (!$payment) {
            $this->logger->sendHeka(array('payment.notification.skip'));
            $this->logger->log('debug', 'Notification error: payment is not exist');
            header('HTTP/1.1 404 Payment not exists');
            die();
        }

        if (
            $notificationObj->getEvent() === NotificationEventType::PAYMENT_SUCCEEDED
            && $payment->getStatus() === PaymentStatus::SUCCEEDED
        ) {
            $this->processSucceedNotification($pmConfigs, $order, $payment, $kassa);
            $metrics = [
                'shop.' . $shopId . '.payment.succeeded',
                'payment.notification.success',
            ];
            $stat = $this->databaseHelper->getSuccessPaymentStat();
            if (!empty($stat)) {
                $host = str_replace(array('http://', 'https://', '.', '/', ':'), array('', '', '-', '-', '-'), $_SERVER['HTTP_HOST']);
                $metrics['shop.' . $shopId . '.host.' . $host . '.payment-count'] = array(
                    'metric_type' => "gauges",
                    'metric_count' => $stat['count']
                );
                $metrics['shop.' . $shopId . '.host.' . $host . '.payment-total'] = array(
                    'metric_type' => "gauges",
                    'metric_count' => $stat['total']
                );
            }
            $this->logger->sendHeka($metrics);
            return true;
        }

        if (
            $notificationObj->getEvent() === NotificationEventType::PAYMENT_WAITING_FOR_CAPTURE
            && $payment->getStatus() === PaymentStatus::WAITING_FOR_CAPTURE
        ) {
            $this->logger->sendHeka(array('shop.'.$shopId.'.payment.waiting_for_capture', 'payment.notification.success'));
            $this->processWaitingForCaptureNtfctn($pmConfigs, $order, $payment, $kassa, $notificationObj);
            return true;
        }

        if (
            $notificationObj->getEvent() === NotificationEventType::PAYMENT_CANCELED
            && $payment->getStatus() === PaymentStatus::CANCELED
        ) {
            $cancellationDetails = $payment->getCancellationDetails() ?: null;
            $paymentMethod = $payment->getPaymentMethod();

            if ($paymentMethod->getType() === PaymentMethodType::SBER_LOAN && $paymentMethod->getDiscountAmount()) {
                /** @var PaymentMethodSberLoan $paymentMethod */
                $this->orderHelper->tryToAddSberLoanDiscount($order, $paymentMethod->getDiscountAmount()->value);
            }
            $this->logger->sendHeka(array('shop.'.$shopId.'.payment.canceled', 'payment.notification.success'));
            if (
                $cancellationDetails !== null
                && $kassa->isEnableHoldMode()
                && $this->isCancelledByMerchant($cancellationDetails)
            ) {
                $this->processCanceledHoldPaymentNtfctn($pmConfigs, $order, $payment);
                return true;
            }

            if (!$kassa->isEnableHoldMode()) {
                $this->logger->log('info', 'Canceled payment ' . $payment->getId());
                $this->processCanceledPaymentNtfctn($order, $payment);
                return true;
            }
        }

        if (
            $notificationObj->getEvent() === NotificationEventType::REFUND_SUCCEEDED
            && $payment->getStatus() === PaymentStatus::SUCCEEDED
            && $refund->getStatus() === RefundStatus::SUCCEEDED
            && !$this->isRefundAlreadyGot($refund->getId())
        ) {
            $this->logger->log(
                'info', 'Refund payment ' . $payment->getId() . '. Refund ID ' . $refund->getId()
            );
            $this->processRefundNtfctn($order, $refund);
            $this->logger->sendHeka(array('payment.notification.skip'));
            return true;
        }

        if (
            $notificationObj->getEvent() === NotificationEventType::DEAL_CLOSED
            || $notificationObj->getEvent() === NotificationEventType::PAYOUT_CANCELED
            || $notificationObj->getEvent() === NotificationEventType::PAYOUT_SUCCEEDED
        ) {
            $this->logger->sendHeka(array('payment.notification.skip'));
            return true;
        }

        $this->logger->sendHeka(array('payment.notification.fail'));
        return false;
    }

    /**
     * Проверяет в БД было ли уже получено уведомление о возврате с таким id
     *
     * @param $refundId
     * @return bool
     */
    private function isRefundAlreadyGot($refundId)
    {
        try {
            $refund = $this->orderModel->getRefundById($refundId);
        } catch (\Exception $e) {
            $this->logger->log('debug', 'Failed to read a refund from DB: ' . $e->getMessage());
        }

        return !empty($refund);
    }

    /**
     * Выполняет действия, если получено уведомление о статусе payment.succeeded
     *
     * @param $pmConfigs
     * @param $order
     * @param $payment
     * @param KassaPaymentMethod $kassa
     * @throws Exception
     */
    private function processSucceedNotification($pmConfigs, $order, $payment, $kassa)
    {
        $paymentMethod = $payment->getPaymentMethod();

        if ($paymentMethod->getType() === PaymentMethodType::SBER_LOAN && $paymentMethod->getDiscountAmount()) {
            /** @var PaymentMethodSberLoan $paymentMethod */
            $this->orderHelper->tryToAddSberLoanDiscount($order, $paymentMethod->getDiscountAmount()->value);
        }

        $jshopConfig = \JSFactory::getConfig();

        /** @var jshopCheckout $checkout */
        $checkout             = \JSFactory::getModel('checkout', 'jshop');
        $endStatus            = $pmConfigs['transaction_end_status'];
        $order->order_created = 1;
        $order->order_status  = $endStatus;
        $order->store();

        try {
            if ($jshopConfig->send_order_email) {
                $checkout->sendOrderEmail($order->order_id);
            }
        } catch (\Exception $exception) {
            $this->logger->log('debug', $exception->getMessage());
        }

        $product_stock_removed = true;
        if ($jshopConfig->order_stock_removed_only_paid_status) {
            $product_stock_removed = in_array($endStatus, $jshopConfig->payment_status_enable_download_sale_file);
        }

        if ($product_stock_removed) {
            $order->changeProductQTYinStock("-");
        }

        $this->receiptHelper->sendSecondReceipt($order->order_id, $kassa, $endStatus);

        $this->logger->sendHeka(array('order-status.change.init'));
        if ($checkout->changeStatusOrder($order->order_id, $endStatus, 0)) {
            $this->orderModel->savePayment($order->order_id, $payment);
            $this->logger->sendHeka(array('order-status.change.success'));
        } else {
            $this->logger->sendHeka(array('order-status.change.fail'));
        }

        if ($paymentMethod->getType() === PaymentMethodType::B2B_SBERBANK) {
            $message = $this->getSuccessOrderHistoryMessageForB2B($paymentMethod);
        }

        if (!empty($message)) {
            $this->orderHelper->saveOrderHistory($order, $message);
        }
    }

    /**
     * Возвращает сообщение для истории статусов заказа, если тип платежа b2b_sberbank
     *
     * @param $paymentMethod
     * @return string
     */
    private function getSuccessOrderHistoryMessageForB2B($paymentMethod)
    {
        $payerBankDetails = $paymentMethod->getPayerBankDetails();

        $fields  = array(
            'fullName'   => 'Полное наименование организации',
            'shortName'  => 'Сокращенное наименование организации',
            'adress'     => 'Адрес организации',
            'inn'        => 'ИНН организации',
            'kpp'        => 'КПП организации',
            'bankName'   => 'Наименование банка организации',
            'bankBranch' => 'Отделение банка организации',
            'bankBik'    => 'БИК банка организации',
            'account'    => 'Номер счета организации',
        );
        $message = '';
        foreach ($fields as $field => $caption) {
            if (isset($requestData[$field])) {
                $message .= $caption.': '.$payerBankDetails->offsetGet($field).'\n';
            }
        }
        return $message;
    }

    /**
     * Выполняет действия, если получено уведомление о статусе payment.waiting_for_capture
     *
     * @param $pmConfigs
     * @param $order
     * @param $payment
     * @param $kassa
     * @param $notificationObj
     */
    private function processWaitingForCaptureNtfctn($pmConfigs, $order, $payment, $kassa, $notificationObj)
    {
        if ($kassa->isEnableHoldMode()) {
            $this->logger->log('info', 'Hold payment '.$payment->getId());

            /** @var jshopCheckout $checkout */
            $checkout             = \JSFactory::getModel('checkout', 'jshop');
            $onHoldStatus         = $pmConfigs['yookassa_hold_mode_on_hold_status'];
            $order->order_created = 1;
            $order->order_status  = $onHoldStatus;
            $order->store();

            $jshopConfig = \JSFactory::getConfig();

            if ($onHoldStatus != $pmConfigs['transaction_end_status']) {
                $jshopConfig->order_stock_removed_only_paid_status = 1;
            }

            try {
                if ($jshopConfig->send_order_email) {
                    $checkout->sendOrderEmail($order->order_id);
                }
            } catch (\Exception $exception) {
                $this->logger->log('debug', $exception->getMessage());
            }

            $this->logger->sendHeka(array('order-status.change.init'));
            if ($checkout->changeStatusOrder($order->order_id, $onHoldStatus, 0)) {
                $this->orderModel->savePayment($order->order_id, $payment);
                $this->logger->sendHeka(array('order-status.change.success'));
            } else {
                $this->logger->sendHeka(array('order-status.change.fail'));
            }

            $this->orderHelper->saveOrderHistory(
                $order,
                sprintf(_JSHOP_YOO_HOLD_MODE_COMMENT_ON_HOLD,
                $payment->getExpiresAt()->format('d.m.Y H:i'))
            );

        } else {
            $payment = $kassa->capturePayment($notificationObj->getObject());
            if (!$payment || $payment->getStatus() !== PaymentStatus::SUCCEEDED) {
                $this->logger->log('debug', 'Capture payment error');
                header('HTTP/1.1 400 Bad Request');
            }
        }
    }

    /**
     * Выполняет действия, если получено уведомление о статусе payment.canceled и включен режим холдирования
     *
     * @param $pmConfigs
     * @param $order
     * @param $payment
     */
    private function processCanceledHoldPaymentNtfctn($pmConfigs, $order, $payment)
    {
        $this->logger->log('info', 'Canceled hold payment ' . $payment->getId());

        /** @var jshopCheckout $checkout */
        $checkout             = \JSFactory::getModel('checkout', 'jshop');
        $cancelHoldStatus         = $pmConfigs['yookassa_hold_mode_cancel_status'];
        $order->order_created = 1;
        $order->order_status  = $cancelHoldStatus;
        $order->store();
        $this->logger->sendHeka(array('order-status.change.init'));
        if ($checkout->changeStatusOrder($order->order_id, $cancelHoldStatus, 0)) {
            $this->orderModel->savePayment($order->order_id, $payment);
            $this->logger->sendHeka(array('order-status.change.success'));
        } else {
            $this->logger->sendHeka(array('order-status.change.fail'));
        }
    }

    /**
     * Выполняет действия, если получено уведомление о статусе payment.canceled
     *
     * @param $order
     * @param $payment
     */
    private function processCanceledPaymentNtfctn($order, $payment)
    {
        $order->order_created = 1;
        $order->store();
        $this->orderModel->savePayment($order->order_id, $payment);
    }

    /**
     * Выполняет действия, если получено уведомление о статусе refund.succeeded
     *
     * @param $order
     * @param Refund $refund
     */
    private function processRefundNtfctn($order, $refund)
    {
        try {
            $this->orderModel->insertRefund($order->getId(), $refund);
        } catch (\Exception $e) {
            $this->logger->log('debug', 'Failed to save a refund to DB: ' . $e->getMessage());

            $this->logger->log('debug', 'Will create the refund table.');
            try {
                $tableChecker = new \YooMoney\Updater\Tables\TableChecker();
                $tableChecker->checkYoomoneyRefunds();
            } catch (\Exception $e) {
                $this->logger->log('debug', 'Failure to create the refund table: ' . $e->getMessage());

                return;
            }

            $this->orderModel->insertRefund($order->getId(), $refund);
        }

        /** @var jshopCheckout $checkout */
        $checkout = \JSFactory::getModel('checkout', 'jshop');

        $order->order_created = 1;
        $order->order_status  = self::REFUNDED_STATUS_ID;
        $order->store();
        $checkout->changeStatusOrder($order->order_id, self::REFUNDED_STATUS_ID, 0);
        $this->orderHelper->saveOrderHistory(
            $order,
            sprintf(_JSHOP_YOO_KASSA_REFUND_SUCCEDED_ORDER_HISTORY,
                $refund->getAmount()->getValue())
        );
    }

    /**
     * Проверяет, действительно ли платеж был отменен мерчантом
     *
     * @param CancellationDetailsInterface $cancellationDetails
     * @return bool
     */
    public function isCancelledByMerchant($cancellationDetails)
    {
        return $cancellationDetails->getParty() === CancellationDetailsPartyCode::MERCHANT
               && $cancellationDetails->getReason() === CancellationDetailsReasonCode::CANCELED_BY_MERCHANT;
    }
}
