<?php

namespace Sibs\Sibs\Model\Api;

use Exception;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\ResponseInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Sibs\Sibs\Api\SibsWebhookInterface;
use Sibs\Sibs\DB\SibsDB;
use Sibs\Sibs\Helper\DataHelper;
use Sibs\Sibs\Model\SibsConstants;
use Sibs\Sibs\Utils\SibsException;
use Sibs\Sibs\Utils\SibsLog;
use SodiumException;

/**
 * Class SibsWebhook.
 */
class SibsWebhook implements SibsWebhookInterface
{
    /**
     * @var \Magento\Framework\App\RequestInterface
     */
    protected $request;

    /**
     * @var \Magento\Framework\App\ResponseInterface
     */
    protected $response;

    /**
     * @var \Sibs\Sibs\Helper\DataHelper
     */
    protected $_sibsHelper;

    /**
     * @var \Magento\Sales\Api\OrderRepositoryInterface
     */
    protected $orderRepository;

    const PAYMENT_TYPES = [
        SibsConstants::SIBS_PAYMENT_TYPE_AUTH,
        SibsConstants::SIBS_PAYMENT_TYPE_PURS,
    ];

    const CAPTURE_TYPES = [
        SibsConstants::SIBS_PAYMENT_TYPE_CAPT,
    ];

    const REFUND_TYPES = [
        SibsConstants::SIBS_PAYMENT_TYPE_RFND,
    ];

    const TO_ENC = 'UTF-8';

    const FROM_ENC = 'BASE64';

    /**
     * Constructor
     *
     * @param \Magento\Framework\App\RequestInterface $request
     * @param \Magento\Framework\App\ResponseInterface $response
     * @param \Sibs\Sibs\Helper\DataHelper $sibsHelper
     * @param \Magento\Sales\Api\OrderRepositoryInterface $orderRepository
     */
    public function __construct(
        RequestInterface $request,
        ResponseInterface $response,
        DataHelper $sibsHelper,
        OrderRepositoryInterface $orderRepository
    ) {
        $this->request         = $request;
        $this->response        = $response;
        $this->_sibsHelper     = $sibsHelper;
        $this->orderRepository = $orderRepository;

        SibsLog::setNameFile(SibsConstants::SIBS_LOG_WEBHOOK);
    }

    /**
     * Execute Received Webhook.
     *
     * @return \Magento\Framework\App\ResponseInterface
     */
    public function webhook()
    {
        try {
            SibsLog::info('WEBHOOK INIT');

            $decryptedBody = $this->getDecryptedBody();
            $notificationID = $this->validateDecryptedBody($decryptedBody);

            $this->responseSuccess($notificationID);
        } catch (SibsException $e) {
            $this->responseError(400, $e->getErrorMessage());
        } catch (Exception $e) {
            SibsLog::err($e->getMessage());
            $this->responseError(500, $e->getMessage());
        }
    }

    /**
     * Validate Decrypted Body.
     *
     * @param array $decryptedBody
     *
     * @return string
     */
    private function validateDecryptedBody($decryptedBody)
    {
        if (empty($decryptedBody['merchant']['transactonId'])) {
            SibsLog::warn('Missing [merchant][transactonId]');
        }

        // TODO: Put back when webhook fix transactonId to transactionId
        /*
        if (empty($decryptedBody['merchant']['transactionId'])) {
            SibsLog::warn('Missing [merchant][transactionId]');
        }
        */

        if (empty($decryptedBody['transactionID'])) {
            SibsLog::warn('Missing [transactionID]');
        }

        if (empty($decryptedBody['paymentMethod'])) {
            SibsLog::warn('Missing [paymentMethod]');
        }

        if (empty($decryptedBody['paymentType'])) {
            SibsLog::warn('Missing [paymentType]');
        }

        if (empty($decryptedBody['paymentStatus'])) {
            SibsLog::warn('Missing [paymentStatus]');
        }

        if (empty($decryptedBody['amount']['value'])) {
            SibsLog::warn('Missing [amount][value]');
        }

        if (empty($decryptedBody['notificationID'])) {
            throw new SibsException('Missing [notificationID]');
        }

        return $decryptedBody['notificationID'];
    }

    /**
     * Convert Enconding
     *
     * @return array|string|false
     */
    private function convert($string)
    {
        return mb_convert_encoding($string, self::TO_ENC, self::FROM_ENC);
    }

    /**
     * Get and Validate Request.
     *
     * @param array $body
     * @throws SibsException
     *
     * @return string
     */
    private function getAndValidateRequest()
    {
        $url     = $this->request->getRequestUri();
        $initVec = $this->request->getHeader('X-Initialization-Vector');
        $authTag = $this->request->getHeader('X-Authentication-Tag');
        $payload = $this->request->getContent();

        if (empty($initVec)) {
            throw new SibsException('X-Initialization-Vector not present in header');
        }

        if (empty($authTag)) {
            throw new SibsException('X-Authentication-Tag not present in header');
        }

        if (empty($payload)) {
            throw new SibsException('Body is empty');
        }

        return [$url, $initVec, $authTag, $payload];
    }

    /**
     * Get Decrypted Body
     *
     * @return array
     */
    private function getDecryptedBody()
    {
        list($url, $initVec, $authTag, $payload) = $this->getAndValidateRequest();

        SibsLog::info('WEBHOOK HEADER X-Initialization-Vector: ' . $initVec);
        SibsLog::info('WEBHOOK HEADER X-Authentication-Tag: ' . $authTag);

        $webhookCall = $this->getWebhookByPayload($payload);

        $webhookCallID = empty($webhookCall) ?
            $this->createWebhookCall($url, $initVec, $authTag, $payload) :
            $this->updateWebhookCall($webhookCall['id'], [
                'arrived' => ($webhookCall['arrived'] + 1)
            ]);

        try {
            $initVecEnc = $this->convert($initVec);
            $cipherText = $this->convert($payload) . $this->convert($authTag);

            $webhookBody = $this->decrypt($initVecEnc, $cipherText);

            $notificationID = $webhookBody['notificationID'] ?? '';

            $this->updateWebhookCall($webhookCallID, [
                'notification_id' => $notificationID,
                'decoded_payload' => json_encode($webhookBody ?? [])
            ]);

            SibsLog::info('WEBHOOK saved on DB');

            return $webhookBody;
        } catch (Exception $e) {
            SibsLog::warn('WEBHOOK partialy saved on DB because of errors: ' . $e->getMessage());

            return [];
        }
    }

    /**
     * Decrypt a cipherText
     *
     * @param string $initVecEnc
     * @param string $cipherText
     *
     * @return string|false
     */
    private function decrypt($initVecEnc, $cipherText)
    {
        if (! sodium_crypto_aead_aes256gcm_is_available()) {
            throw new SibsException('Sodium lib not available');
        }

        $allWebhookSecrets = $this->_sibsHelper->getAllWebhookSecrets();

        foreach ($allWebhookSecrets as $webhookSecret) {
            $storeIdentification = $this->getStoreIdentification($webhookSecret);

            if (empty($webhookSecret['secret'])) {
                SibsLog::warn("WEBHOOK - Secret is empty on Store: $storeIdentification");
                continue;
            }

            $secret = $this->convert($webhookSecret['secret']);

            try {
                $result = sodium_crypto_aead_aes256gcm_decrypt($cipherText, '', $initVecEnc, $secret);
            } catch (SodiumException $e) {
                SibsLog::warn("WEBHOOK - Decrypt Exception (" . $e->getMessage() . ") on Store: $storeIdentification");
                continue;
            }

            if ($result === false) {
                SibsLog::warn("WEBHOOK - Decrypt Error on Store: $storeIdentification");
                continue;
            }

            $json_result = json_decode($result, true);

            if ($json_result === null) {
                SibsLog::warn("WEBHOOK - Bad CipherText on Store: $storeIdentification");
                continue;
            }

            SibsLog::info("WEBHOOK - Webhook Decrypted in Store: $storeIdentification");
            SibsLog::info("WEBHOOK - Body Decrypted: " . SibsLog::prettify($json_result));

            return $json_result;
        }

        throw new SibsException('Bad CipherText on All Stores');
    }

    /**
     * Get Store Identification
     *
     * @param string $webhookSecret
     *
     * @return string|false
     */
    private function getStoreIdentification($webhookSecret)
    {
        return $webhookSecret['storeCode'] . ' - ' . $webhookSecret['storeName'];
    }

    /**
     * Create Webhook Call
     *
     * @param string $url
     * @param string $initVec
     * @param string $authTag
     * @param string $payload
     *
     * @return int
     */
    private function createWebhookCall($url, $initVec, $authTag, $payload)
    {
        $now       = $this->_sibsHelper->getDateFromMagento();
        $tableName = SibsDB::getTableName('sibs_webhook_calls');
        SibsDB::insert($tableName, [
            'url'        => $url,
            'headers'    => json_encode([
                'HTTP_X_INITIALIZATION_VECTOR' => $initVec,
                'HTTP_X_AUTHENTICATION_TAG'    => $authTag
            ]),
            'payload'    => $payload,
            'created_at' => $now,
            'updated_at' => $now
        ]);

        return SibsDB::lastInsertId($tableName);
    }

    /**
     * Update Webhook Call
     *
     * @param int $webhookCallID
     * @param array $updateData
     *
     *
     * @return int
     */
    private function updateWebhookCall($webhookCallID, $updateData)
    {
        $tableName  = SibsDB::getTableName('sibs_webhook_calls');

        if (!isset($updateData['updated_at'])) {
            $updateData['updated_at'] = $this->_sibsHelper->getDateFromMagento();
        }

        SibsDB::update($tableName, $updateData, "`id` = " . $webhookCallID);

        return $webhookCallID;
    }

    /**
     * Get Webhook Call by payload
     *
     * @param string $payload
     *
     * @return array
     */
    private function getWebhookByPayload($payload)
    {
        $tableName  = SibsDB::getTableName('sibs_webhook_calls');
        $webhookCall = SibsDB::fetchRow("SELECT *
            FROM `$tableName`
            WHERE `payload` = '$payload';
        ");

        return $webhookCall ?? [];
    }

    /**
     * Response with Success.
     *
     * @param string $notificationID
     * @return \Magento\Framework\App\ResponseInterface
     */
    private function responseSuccess($notificationID)
    {
        $this->response->getHeaders()
            ->addHeaderLine('Content-Type', 'application/json');

        $this->response->setContent(json_encode([
            'statusCode'     => 200,
            'statusMsg'      => 'Success',
            'notificationID' => $notificationID,
        ]));

        $this->response->send();

        SibsLog::info('WEBHOOK END with success');
        die();
    }

    /**
     * Response with Error.
     *
     * @param string $errorCode
     * @param string $errorMessage
     * @return \Magento\Framework\App\ResponseInterface
     */
    private function responseError($errorCode, $errorMessage = 'Error')
    {
        $this->response->getHeaders()
            ->addHeaderLine('Content-Type', 'application/json');

        $this->response->setContent(json_encode([
            'statusCode' => $errorCode,
            'statusMsg'  => $errorMessage,
        ]));

        $this->response->send();

        SibsLog::info('WEBHOOK END with error');
        die();
    }
}
