<?php

/**
 * PHP version 7.1
 *
 * @category Integration
 * @package  Intaro\RetailCrm\Service
 * @author   RetailCRM <integration@retailcrm.ru>
 * @license  MIT
 * @link     http://retailcrm.ru
 * @see      http://retailcrm.ru/docs
 */

namespace Intaro\RetailCrm\Service;

use Bitrix\Main\Loader;
use Bitrix\Sale\BasketItemBase;
use Bitrix\Sale\Order;
use Exception;
use Intaro\RetailCrm\Component\ConfigProvider;
use Intaro\RetailCrm\Component\Constants;
use Intaro\RetailCrm\Component\Factory\ClientFactory;
use Intaro\RetailCrm\Component\ServiceLocator;
use Intaro\RetailCrm\Model\Api\LoyaltyAccount;
use Intaro\RetailCrm\Model\Api\OrderProduct;
use Intaro\RetailCrm\Model\Api\PriceType;
use Intaro\RetailCrm\Model\Api\Request\Loyalty\Account\LoyaltyAccountRequest;
use Intaro\RetailCrm\Model\Api\Request\Loyalty\LoyaltyCalculateRequest;
use Intaro\RetailCrm\Model\Api\Request\Order\Loyalty\OrderLoyaltyApplyRequest;
use Intaro\RetailCrm\Model\Api\Response\Loyalty\LoyaltyCalculateResponse;
use Intaro\RetailCrm\Model\Api\Response\Order\Loyalty\OrderLoyaltyApplyResponse;
use Intaro\RetailCrm\Model\Api\SerializedOrder;
use Intaro\RetailCrm\Model\Api\SerializedOrderProduct;
use Intaro\RetailCrm\Model\Api\SerializedOrderProductOffer;
use Intaro\RetailCrm\Model\Api\SerializedOrderReference;
use Intaro\RetailCrm\Model\Api\SerializedRelationCustomer;
use Intaro\RetailCrm\Model\Bitrix\OrderLoyaltyData;
use Intaro\RetailCrm\Repository\CurrencyRepository;
use Intaro\RetailCrm\Repository\OrderLoyaltyDataRepository;
use Intaro\RetailCrm\Service\Exception\LpAccountsUnavailableException;
use Logger;

/**
 * Class LoyaltyService
 *
 * @package Intaro\RetailCrm\Service
 */
class LoyaltyService
{
    /**
     * @var \Intaro\RetailCrm\Component\ApiClient\ClientAdapter
     */
    private $client;

    /**
     * @var mixed
     */
    private $site;

    /**
     * @var \Logger
     */
    private $logger;

    /**
     * LoyaltyService constructor.
     *
     * @throws \Bitrix\Main\LoaderException
     */
    public function __construct()
    {
        IncludeModuleLangFile(__FILE__);

        $this->logger = Logger::getInstance();
        $this->client = ClientFactory::createClientAdapter();
        $this->site = ConfigProvider::getSitesAvailable();

        Loader::includeModule('Catalog');
    }

    /**
     * Выполняет запрос на применение бонусов по программе лояльности
     *
     * @link https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-orders-loyalty-apply
     *
     * @param int   $orderId    ID заказа
     * @param float $bonusCount количество бонусов для списания
     *
     * @return \Intaro\RetailCrm\Model\Api\Response\Order\Loyalty\OrderLoyaltyApplyResponse|mixed|null
     */
    public function sendBonusPayment(int $orderId, float $bonusCount): ?OrderLoyaltyApplyResponse
    {
        $request = new OrderLoyaltyApplyRequest();
        $request->order = new SerializedOrderReference();
        $request->order->externalId = $orderId;
        $request->bonuses = $bonusCount;
        $request->site = $this->site;
        $result = $this->client->loyaltyOrderApply($request);

        Utils::handleApiErrors($result);

        return $result;
    }

    /**
     * Возвращает расчет привилегий на основе корзины и количества бонусов для списания
     *
     * @link https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-loyalty-calculate
     *
     * @param array $basketItems корзина
     * @param float $bonuses количество бонусов для списания
     *
     * @return \Intaro\RetailCrm\Model\Api\Response\Loyalty\LoyaltyCalculateResponse|mixed|null
     */
    public function getLoyaltyCalculate(array $basketItems, float $bonuses = 0): ?LoyaltyCalculateResponse
    {
        global $USER;

        $request = new LoyaltyCalculateRequest();
        $request->order = new SerializedOrder();
        $request->order->customer = new SerializedRelationCustomer();
        $request->order->customer->id = $USER->GetID();
        $request->order->customer->externalId = $USER->GetID();

        $request->site = $this->site;
        $request->bonuses = $bonuses;

        foreach ($basketItems as $item) {
            $product = new SerializedOrderProduct();

            $fullPrice = $item['BASE_PRICE'] ?? $item['FULL_PRICE'];
            $product->initialPrice = $fullPrice; //цена без скидки

            if ($fullPrice > 0) {
                $product->discountManualAmount = $fullPrice - $item['PRICE'];
            }

            $product->offer = new SerializedOrderProductOffer();
            $product->offer->externalId = $item['ID'];
            $product->offer->id = $item['ID'];
            $product->offer->xmlId = $item['XML_ID'];
            $product->quantity = $item['QUANTITY'];

            $prices = ConfigProvider::getCrmPrices();
            $product->priceType = new PriceType();
            $serializePrice = unserialize($prices);

            if (isset($serializePrice[$item['PRICE_TYPE_ID']])) {
                $product->priceType->code = $serializePrice[$item['PRICE_TYPE_ID']];
            }

            $request->order->items[] = $product;
        }

        $result = $this->client->loyaltyCalculate($request);

        if (isset($result->errorMsg) && !empty($result->errorMsg)) {
            $this->logger->write($result->errorMsg, Constants::LOYALTY_ERROR);
        }

        return $result;
    }

    //TODO доделать метод проверки регистрации в ПЛ

    /**
     * Возвращает список участий в программе лояльности
     *
     * @link https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#get--api-v5-loyalty-accounts
     *
     * @param int $idInLoyalty ID участия в программе лояльности
     *
     * @return null|\Intaro\RetailCrm\Model\Api\LoyaltyAccount
     * @throws \Intaro\RetailCrm\Service\Exception\LpAccountsUnavailableException
     */
    public function getLoyaltyAccounts(int $idInLoyalty): ?LoyaltyAccount
    {
        $request = new LoyaltyAccountRequest();
        $request->filter->id = $idInLoyalty;
        $request->filter->sites = $this->site;

        $response = $this->client->getLoyaltyAccounts($request);

        if ($response !== null && $response->success) {
            if (!isset($response->loyaltyAccounts[0])) {
                throw new LpAccountsUnavailableException();
            }

            return $response->loyaltyAccounts[0];
        }

        Utils::handleApiErrors($response);

        return null;
    }

    /**
     * Повторно отправляет бонусную оплату
     *
     * Используется при необходимости еще раз отправить смс
     *
     * @param int $orderId
     *
     * @return \Intaro\RetailCrm\Model\Bitrix\SmsCookie|bool
     */
    public function resendBonusPayment(int $orderId)
    {
        /** @var CookieService $service */
        $service = ServiceLocator::get(CookieService::class);
        $bonusCount = $this->getTotalBonusCount($orderId);

        if ($bonusCount === false || $bonusCount === 0) {
            return false;
        }

        /** @var  OrderLoyaltyApplyResponse $response */
        $response = $this->sendBonusPayment($orderId, $bonusCount);

        if ($response === null || !($response instanceof OrderLoyaltyApplyResponse)) {
            return false;
        }

        if (
            isset($response->verification, $response->verification->checkId)
            && empty($response->verification->verifiedAt)
        ) {
            return $service->setSmsCookie('lpOrderBonusConfirm', $response->verification);
        }

        if (!empty($response->verification->verifiedAt)) {
            $this->saveBonusDiscounts(Order::load($orderId), $response);
            $this->setDebitedStatus($orderId, true);

            return true;
        }

        return false;
    }

    /**
     * @param int  $orderId
     * @param bool $newStatus
     */
    public function setDebitedStatus(int $orderId, bool $newStatus): void
    {
        $repository = new OrderLoyaltyDataRepository();
        $products = $repository->getProductsByOrderId($orderId);

        if (is_array($products)) {
            /** @var OrderLoyaltyData $product */
            foreach ($products as $product) {
                $product->isDebited = $newStatus;
                $repository->edit($product);
            }
        }
    }

    /**
     * Добавляет данные о программе лояльности  в массив корзины
     *
     * @param array                                                                 $basketData
     * @param \Intaro\RetailCrm\Model\Api\Response\Loyalty\LoyaltyCalculateResponse $calculate
     *
     * @return array
     */
    public function addLoyaltyToBasket(array $basketData, LoyaltyCalculateResponse $calculate): array
    {
        $totalRenderData = &$basketData['TOTAL_RENDER_DATA'];
        $basketData['LP_CALCULATE_SUCCESS'] = $calculate->success;
        $totalRenderData['WILL_BE_CREDITED'] = $calculate->order->bonusesCreditTotal;

        foreach ($calculate->calculations as $privilege) {
            if ($privilege->maximum && $privilege->creditBonuses === 0.0) {
                $totalRenderData['LOYALTY_DISCOUNT']
                    = round($privilege->discount - $basketData['DISCOUNT_PRICE_ALL'], 2);
                $totalRenderData['LOYALTY_DISCOUNT_FORMATED'] = $totalRenderData['LOYALTY_DISCOUNT']
                    . ' ' . GetMessage($totalRenderData['CURRENCY']);
                $totalRenderData['PRICE'] -= $totalRenderData['LOYALTY_DISCOUNT'];//общая сумма со скидкой
                $totalRenderData['PRICE_FORMATED'] = $totalRenderData['PRICE']
                    . ' ' . GetMessage($totalRenderData['CURRENCY']); //отформатированная сумма со скидкой
                $totalRenderData['SUM_WITHOUT_VAT_FORMATED'] = $totalRenderData['PRICE_FORMATED'];
                $basketData['allSum_FORMATED'] = $totalRenderData['PRICE_FORMATED'];
                $basketData['allSum_wVAT_FORMATED'] = $totalRenderData['PRICE_FORMATED'];
                $basketData['allSum'] = $totalRenderData['PRICE'];
                $totalRenderData['DISCOUNT_PRICE_FORMATED'] = $privilege->discount
                    . ' ' . GetMessage($totalRenderData['CURRENCY']);
                $totalRenderData['LOYALTY_DISCOUNT_DEFAULT'] = $basketData['DISCOUNT_PRICE_ALL']
                    . ' ' . GetMessage($totalRenderData['CURRENCY']);
            }
        }

        foreach ($basketData['BASKET_ITEM_RENDER_DATA'] as $key => &$item) {
            $item['WILL_BE_CREDITED_BONUS'] = $calculate->order->items[$key]->bonusesCreditTotal;

            if ($calculate->order->items[$key]->bonusesCreditTotal === 0.0) {
                $item['PRICE'] -= $calculate->order->items[$key]->discountTotal
                    - ($item['SUM_DISCOUNT_PRICE'] / $item['QUANTITY']);
                $item['SUM_PRICE'] = $item['PRICE'] * $item['QUANTITY'];
                $item['PRICE_FORMATED'] = $item['PRICE'] . ' ' . GetMessage($item['CURRENCY']);
                $item['SUM_PRICE_FORMATED'] = $item['SUM_PRICE'] . ' ' . GetMessage($item['CURRENCY']);
                $item['SHOW_DISCOUNT_PRICE'] = true;
                $item['SUM_DISCOUNT_PRICE'] = $calculate->order->items[$key]->discountTotal
                    * $item['QUANTITY'];
                $item['SUM_DISCOUNT_PRICE_FORMATED'] = $item['SUM_DISCOUNT_PRICE']
                    . ' '
                    . GetMessage($item['CURRENCY']);
                $item['DISCOUNT_PRICE_PERCENT'] = round($item['SUM_DISCOUNT_PRICE']
                    / (($item['FULL_PRICE'] * $item['QUANTITY']) / 100));
                $item['DISCOUNT_PRICE_PERCENT_FORMATED'] = $item['DISCOUNT_PRICE_PERCENT'] . '%';

                if (isset($item['COLUMN_LIST'])) {
                    foreach ($item['COLUMN_LIST'] as &$column) {
                        $column['VALUE'] = $column['CODE'] === 'DISCOUNT'
                            ? $item['DISCOUNT_PRICE_PERCENT_FORMATED'] : $column['VALUE'];
                    }

                    unset($column);
                }
            }
        }

        unset($item);

        return $basketData;
    }

    /**
     * @param array                                                                 $orderArResult
     * @param \Intaro\RetailCrm\Model\Api\Response\Loyalty\LoyaltyCalculateResponse $calculate
     *
     * @return array
     */
    public function calculateOrderBasket(array $orderArResult, LoyaltyCalculateResponse $calculate): array
    {
        /** @var \Intaro\RetailCrm\Model\Api\LoyaltyCalculation $privilege */
        foreach ($calculate->calculations as $privilege) {
            if ($privilege->maximum) {
                $orderArResult['AVAILABLE_BONUSES'] = $privilege->maxChargeBonuses;

                $jsDataTotal = &$orderArResult['JS_DATA']['TOTAL'];

                //если уровень скидочный
                if ($privilege->maximum && $privilege->discount > 0) {
                    //Персональная скидка
                    $jsDataTotal['LOYALTY_DISCOUNT'] = $orderArResult['LOYALTY_DISCOUNT_INPUT']
                        = round($privilege->discount - $jsDataTotal['DISCOUNT_PRICE'], 2);

                    //общая стоимость
                    $jsDataTotal['ORDER_TOTAL_PRICE'] -= $jsDataTotal['LOYALTY_DISCOUNT'];

                    //обычная скидка
                    $jsDataTotal['DEFAULT_DISCOUNT'] = $jsDataTotal['DISCOUNT_PRICE'];

                    $jsDataTotal['ORDER_TOTAL_PRICE_FORMATED'] = round($jsDataTotal['ORDER_TOTAL_PRICE'], 2)
                        . ' ' . GetMessage($orderArResult['BASE_LANG_CURRENCY']);

                    $jsDataTotal['DISCOUNT_PRICE'] += $jsDataTotal['LOYALTY_DISCOUNT'];

                    $jsDataTotal['DISCOUNT_PRICE_FORMATED'] = $jsDataTotal['DISCOUNT_PRICE']
                        . ' ' . GetMessage($orderArResult['BASE_LANG_CURRENCY']);

                    $jsDataTotal['ORDER_PRICE'] -= $jsDataTotal['LOYALTY_DISCOUNT'];

                    $jsDataTotal['ORDER_PRICE_FORMATED'] = $jsDataTotal['ORDER_PRICE']
                        . ' ' . GetMessage($orderArResult['BASE_LANG_CURRENCY']);

                    $iterator = 0;

                    foreach ($orderArResult['JS_DATA']['GRID']['ROWS'] as $key => &$item) {
                        $item['data']['SUM_NUM'] = $orderArResult['CALCULATE_ITEMS_INPUT'][$key]['SUM_NUM']
                            = $item['data']['SUM_BASE']
                            - ($calculate->order->items[$iterator]->discountTotal
                                * $item['data']['QUANTITY']);

                        $orderArResult['CALCULATE_ITEMS_INPUT'][$key]['QUANTITY'] = $item['data']['QUANTITY'];
                        $orderArResult['CALCULATE_ITEMS_INPUT'][$key]['SHOP_ITEM_DISCOUNT']
                            = round($item['data']['BASE_PRICE'] - $item['data']['PRICE'], 2);
                        $orderArResult['CALCULATE_ITEMS_INPUT'][$key]['BASE_PRICE']
                            = $item['data']['BASE_PRICE'];

                        $item['data']['SUM'] = $item['data']['SUM_NUM']
                            . ' ' . GetMessage($orderArResult['BASE_LANG_CURRENCY']);

                        $item['data']['DISCOUNT_PRICE'] = $calculate->order->items[$iterator]->discountTotal;

                        $iterator++;
                    }

                    unset($item);

                    $orderArResult['CALCULATE_ITEMS_INPUT']
                        = htmlspecialchars(json_encode($orderArResult['CALCULATE_ITEMS_INPUT']));
                }

                break;
            }
        }

        $orderArResult['CHARGERATE'] = $calculate->loyalty->chargeRate;
        $orderArResult['TOTAL_BONUSES_COUNT'] = $calculate->order->loyaltyAccount->amount;
        $orderArResult['LP_CALCULATE_SUCCESS'] = $calculate->success;
        $orderArResult['WILL_BE_CREDITED'] = $calculate->order->bonusesCreditTotal;

        $currencyRepository = new CurrencyRepository();
        $orderArResult['BONUS_CURRENCY'] = html_entity_decode($currencyRepository->getCurrencyFormatString());

        return $orderArResult;
    }

    /**
     * @param \Bitrix\Sale\Order $order
     * @param array              $calculateItemsInput
     */
    public function saveDiscounts(Order $order, array $calculateItemsInput): void
    {
        try {
            /** @var BasketItemBase $basketItem */
            foreach ($order->getBasket() as $basketItem) {
                $calcItemPosition = $calculateItemsInput[$basketItem->getId()];
                $calculateItem = $calcItemPosition['SUM_NUM'] / $calcItemPosition['QUANTITY'];

                $basketItem->setField('CUSTOM_PRICE', 'Y');
                $basketItem->setField('DISCOUNT_PRICE', $basketItem->getBasePrice() - $calculateItem);
                $basketItem->setField('PRICE', $calculateItem);
            }

            $order->save();
        } catch (Exception $exception) {
            $this->logger->write($exception->getMessage(), Constants::LOYALTY_ERROR);
        }
    }


    /**
     * @param \Bitrix\Sale\Order                                                           $order
     * @param \Intaro\RetailCrm\Model\Api\Response\Order\Loyalty\OrderLoyaltyApplyResponse $response
     *
     * @return void|null
     */
    public function saveBonusDiscounts(Order $order, OrderLoyaltyApplyResponse $response): void
    {
        try {
            $basketItems = $order->getBasket();

            if ($basketItems === null) {
                $this->logger->write('No item in basket', Constants::LOYALTY_ERROR);
            }

            /** @var BasketItemBase $basketItem */
            foreach ($basketItems as $key => $basketItem) {
                /** @var OrderProduct $item */
                $item = $response->order->items[$key];
                $basePrice = $basketItem->getField('BASE_PRICE');
                $basketItem->setField('CUSTOM_PRICE', 'Y');
                $basketItem->setField('DISCOUNT_PRICE', $item->discountTotal);
                $basketItem->setField('PRICE', $basePrice - $item->discountTotal);
            }

            $order->save();
        } catch (Exception $exception) {
            $this->logger->write($exception->getMessage(), Constants::LOYALTY_ERROR);
        }
    }

    /**
     * @param int $externalId
     *
     * @return float|null
     */
    public function getInitialDiscount(int $externalId): ?float
    {
        $repository = new OrderLoyaltyDataRepository();

        return $repository->getDefDiscountByProductPosition($externalId);
    }

    /**
     * Списаны ли бонусы в заказе
     *
     * @param $orderId
     *
     * @return bool|null
     */
    public function isBonusDebited($orderId): ?bool
    {
        $repository = new OrderLoyaltyDataRepository();
        $products = $repository->getProductsByOrderId($orderId);

        if ($products === null || count($products) === 0) {
            return null;
        }

        foreach ($products as $product) {
            if (!empty($product->checkId) && $product->isDebited === false) {
                return false;
            }
        }

        return true;
    }

    /**
     * Добавляет оплату бонусами в заказ Битрикс (устанавливает кастомные цены)
     *
     * @param \Bitrix\Sale\Order $order
     * @param float              $bonusCount /бонусная скидка в рублях
     *
     * @return \Intaro\RetailCrm\Model\Api\Response\Order\Loyalty\OrderLoyaltyApplyResponse|null
     */
    public function applyBonusesInOrder(Order $order, float $bonusCount): ?OrderLoyaltyApplyResponse
    {
        $response = $this->sendBonusPayment($order->getId(), $bonusCount);

        if ($response->success) {
           $this->saveBonusDiscounts($order, $response);
        } else {
            Utils::handleApiErrors($response);
            return null;
        }

        return $response;
    }

    /**
     * @param $orderId
     *
     * @return int|null
     */
    private function getTotalBonusCount($orderId): ?int
    {
        $repository = new OrderLoyaltyDataRepository();
        $products = $repository->getProductsByOrderId($orderId);

        if ($products === null || count($products) === 0) {
            return null;
        }

        foreach ($products as $product) {
            if ($product->bonusCountTotal > 0) {
                return $product->bonusCountTotal;
            }
        }

        return null;
    }
}