<?php

namespace FiloBlu\Rma\Model\ResourceModel\Carrier;

use FiloBlu\Rma\Api\Data\RateInterface;
use FiloBlu\Rma\Helper\RmaHelper;
use FiloBlu\Rma\Model\Carrier\CarrierRate;
use Magento\Directory\Model\Region;
use Magento\Directory\Model\ResourceModel\Region\Collection;
use Magento\Directory\Model\ResourceModel\Region\CollectionFactory;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\DataObject;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Directory\ReadFactory;
use Magento\Framework\Filesystem\DirectoryList;
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
use Magento\Framework\Model\ResourceModel\Db\Context;
use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Store\Model\StoreManagerInterface;
use Psr\Log\LoggerInterface;

/**
 *
 */
class Rate extends AbstractDb
{
    /**
     * Import table rates website ID
     *
     * @var int
     */
    protected $_importWebsiteId = 0;

    /**
     * Errors in import process
     *
     * @var array
     */
    protected $_importErrors = [];

    /**
     * Count of imported table rates
     *
     * @var int
     */
    protected $_importedRows = 0;

    /**
     * Array of unique table rate keys to protect from duplicates
     *
     * @var array
     */
    protected $_importUniqueHash = [];

    /**
     * Array of countries keyed by iso2 code
     *
     * @var array
     */
    protected $_importIso2Countries;

    /**
     * Array of countries keyed by iso3 code
     *
     * @var array
     */
    protected $_importIso3Countries;

    /**
     * Associative array of countries and regions
     * [country_id][region_code] = region_id
     *
     * @var array
     */
    protected $_importRegions;

    /**
     * Import Table Rate condition name
     *
     * @var string
     */
    protected $_importConditionName;

    /**
     * Array of condition full names
     *
     * @var array
     */
    protected $_conditionFullNames = [];

    /**
     * @var ScopeConfigInterface
     */
    protected $_coreConfig;

    /**
     * @var LoggerInterface
     */
    protected $_logger;

    /**
     * @var StoreManagerInterface
     */
    protected $_storeManager;

    /**
     * @var CarrierRate
     */
    protected $_rate;

    /**
     * @var \Magento\Directory\Model\ResourceModel\Country\CollectionFactory
     */
    protected $_countryCollectionFactory;

    /**
     * @var CollectionFactory
     */
    protected $_regionCollectionFactory;

    /**
     *   * @var Filesystem
     */
    protected $_filesystem;
    /**
     * @var RmaHelper
     */
    protected $_rmaHelper;

    /**
     *   * @var ReadFactory
     */
    private $_readFactory;

    /**
     * EasyReturnRate constructor.
     * @param Context $context
     * @param LoggerInterface $logger
     * @param ScopeConfigInterface $coreConfig
     * @param StoreManagerInterface $storeManager
     * @param CarrierRate $rate
     * @param \Magento\Directory\Model\ResourceModel\Country\CollectionFactory $countryCollectionFactory
     * @param CollectionFactory $regionCollectionFactory
     * @param ReadFactory $readFactory
     * @param Filesystem $filesystem
     * @param RmaHelper $rmaHelper
     * @param string $matrixRateSource
     * @param null $resourcePrefix
     */
    public function __construct(
        Context                                                          $context,
        LoggerInterface                                                  $logger,
        ScopeConfigInterface                                             $coreConfig,
        StoreManagerInterface                                            $storeManager,
        CarrierRate                                                      $rate,
        \Magento\Directory\Model\ResourceModel\Country\CollectionFactory $countryCollectionFactory,
        CollectionFactory                                                $regionCollectionFactory,
        ReadFactory                                                      $readFactory,
        Filesystem                                                       $filesystem,
        RmaHelper                                                        $rmaHelper,
        string                                                           $matrixRateSource,
                                                                         $resourcePrefix = null
    )
    {
        $this->_coreConfig = $coreConfig;
        $this->_logger = $logger;
        $this->_storeManager = $storeManager;
        $this->_rate = $rate;
        $this->_countryCollectionFactory = $countryCollectionFactory;
        $this->_regionCollectionFactory = $regionCollectionFactory;
        $this->_readFactory = $readFactory;
        $this->_filesystem = $filesystem;
        $this->_rmaHelper = $rmaHelper;

        parent::__construct($context, $resourcePrefix);

        $this->_setMainTable($this->_rmaHelper->getEasyReturnMatrixrateSource());

        if ($matrixRateSource == 'change_size') {
            $this->_setMainTable($this->_rmaHelper->getChangeSizeMatrixrateSource());
            $this->_mainTable = $this->_rmaHelper->getChangeSizeMatrixrateSource();
        }
    }

    /**
     * @param RateRequest $request
     * @param bool $zipRangeSet
     * @return array
     * @throws LocalizedException
     */
    public function getRate(RateRequest $request, $zipRangeSet = false)
    {
        $adapter = $this->getConnection();
        $shippingData = [];
        $postcode = $request->getDestPostcode();
        if ($zipRangeSet && is_numeric($postcode)) {
            #  Want to search for postcodes within a range
            $zipSearchString = ' AND :postcode BETWEEN dest_zip AND dest_zip_to ';
        } else {
            $zipSearchString = ' AND dest_zip LIKE :postcode  ';
        }

        for ($j = 0; $j < 8; $j++) {
            $select = $adapter->select()->from(
                $this->getMainTable()
            )->where(
                'website_id = :website_id'
            )->order(
                ['dest_country_id DESC', 'dest_region_id DESC', 'dest_zip DESC', 'condition_from_value DESC']
            );

            $zoneWhere = '';
            $bind = [];
            switch ($j) {
                case 0: // country, region, city, postcode
                    $zoneWhere = 'dest_country_id = :country_id AND dest_region_id = :region_id AND STRCMP(LOWER(dest_city),LOWER(:city))= 0 ' . $zipSearchString;  // TODO Add city
                    $bind = [
                        ':country_id' => $request->getDestCountryId(),
                        ':region_id' => (int)$request->getDestRegionId(),
                        ':city' => $request->getDestCity(),
                        ':postcode' => $request->getDestPostcode(),
                    ];
                    break;
                case 1: // country, region, no city, postcode
                    $zoneWhere = "dest_country_id = :country_id AND dest_region_id = :region_id AND dest_city='' " . $zipSearchString;
                    $bind = [
                        ':country_id' => $request->getDestCountryId(),
                        ':region_id' => (int)$request->getDestRegionId(),
                        ':postcode' => $request->getDestPostcode(),
                    ];
                    break;
                case 2: // country, state, city, no postcode
                    $zoneWhere = "dest_country_id = :country_id AND dest_region_id = :region_id AND STRCMP(LOWER(dest_city),LOWER(:city))= 0 AND dest_zip ='*'"; // TODO Add city search
                    $bind = [
                        ':country_id' => $request->getDestCountryId(),
                        ':region_id' => (int)$request->getDestRegionId(),
                        ':city' => $request->getDestCity(),
                    ];
                    break;
                case 3: //country, city, no region, no postcode
                    $zoneWhere = "dest_country_id = :country_id AND dest_region_id = '0' AND STRCMP(LOWER(dest_city),LOWER(:city))= 0 AND dest_zip ='*'"; // TODO Add city
                    $bind = [
                        ':country_id' => $request->getDestCountryId(),
                        ':city' => $request->getDestCity(),
                    ];
                    break;
                case 4: // country, postcode
                    $zoneWhere = "dest_country_id = :country_id AND dest_region_id = '0' AND dest_city ='*' " . $zipSearchString;
                    $bind = [
                        ':country_id' => $request->getDestCountryId(),
                        ':postcode' => $request->getDestPostcode(),
                    ];
                    break;
                case 5: // country, region
                    $zoneWhere = "dest_country_id = :country_id AND dest_region_id = :region_id  AND dest_city ='*' AND dest_zip ='*'";
                    $bind = [
                        ':country_id' => $request->getDestCountryId(),
                        ':region_id' => (int)$request->getDestRegionId(),
                    ];
                    break;
                case 6: // country
                    $zoneWhere = "dest_country_id = :country_id AND dest_region_id = '0' AND dest_city ='*' AND dest_zip ='*'";
                    $bind = [
                        ':country_id' => $request->getDestCountryId(),
                    ];
                    break;
                case 7: // nothing
                    $zoneWhere = "dest_country_id = '0' AND dest_region_id = '0' AND dest_city ='*' AND dest_zip ='*'";
                    break;
            }

            $select->where($zoneWhere);

            $bind[':website_id'] = (int)$request->getWebsiteId();
            $bind[':condition_name'] = $request->getConditionMRName();
            $bind[':condition_value'] = $request->getData($request->getConditionMRName()) ?: '*';

            $select->where('condition_name = :condition_name');
            $select->where('condition_from_value <= :condition_value');
            $select->where('condition_to_value >= :condition_value');


            $results = $adapter->fetchAll($select, $bind);

            if (!empty($results)) {
                foreach ($results as $data) {
                    $shippingData[] = $data;
                }
                break;
            }
        }

        return $shippingData;
    }

    /**
     * Upload table rate file and import data from it
     *
     * @param DataObject $object
     * @return Matrixrate
     * @throws LocalizedException
     * @TODO: this method should be refactored as soon as updated design will be provided
     * @see https://wiki.corp.x.com/display/MCOMS/Magento+Filesystem+Decisions
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     * @SuppressWarnings(PHPMD.NPathComplexity)
     */
    public function uploadAndImport(DataObject $object)
    {
        if (empty($_FILES['groups']['tmp_name']['easy_return_matrixrate_group']['fields']['import']['value']) && empty($_FILES['groups']['tmp_name']['change_size_matrixrate_group']['fields']['import']['value'])) {
            return $this;
        }

        $this->_importConditionName = 'package_weight';

        if (!empty($_FILES['groups']['tmp_name']['easy_return_matrixrate_group']['fields']['import']['value'])) {
            $csvFile = $_FILES['groups']['tmp_name']['easy_return_matrixrate_group']['fields']['import']['value'];
            $this->_setMainTable('filoblu_easy_return_matrixrate');
            $this->_importConditionName = $this->_rmaHelper->getEasyReturnMatrixrateConditionName();
        }
        if (!empty($_FILES['groups']['tmp_name']['change_size_matrixrate_group']['fields']['import']['value'])) {
            $csvFile = $_FILES['groups']['tmp_name']['change_size_matrixrate_group']['fields']['import']['value'];
            $this->_setMainTable('filoblu_change_size_matrixrate');
            $this->_importConditionName = $this->_rmaHelper->getChangeSizeMatrixrateConditionName();
        }

        $website = $this->_storeManager->getWebsite($object->getScopeId());

        $this->_importWebsiteId = (int)$website->getId();
        $this->_importUniqueHash = [];
        $this->_importErrors = [];
        $this->_importedRows = 0;

        //M2-20
        $tmpDirectory = ini_get('upload_tmp_dir') ? $this->_readFactory->create(ini_get('upload_tmp_dir'))
            : $this->_filesystem->getDirectoryRead(DirectoryList::SYS_TMP);
        $path = $tmpDirectory->getRelativePath($csvFile);
        $stream = $tmpDirectory->openFile($path);

        // check and skip headers
        $headers = $stream->readCsv();
        if ($headers === false || count($headers) < 5) {
            $stream->close();
            throw new LocalizedException(__('Please correct Matrix Rates File Format.'));
        }

        $adapter = $this->getConnection();
        $adapter->beginTransaction();

        try {
            $rowNumber = 1;
            $importData = [];

            $this->_loadDirectoryCountries();
            $this->_loadDirectoryRegions();

            // delete old data by website and condition name
            $condition = [
                'website_id = ?' => $this->_importWebsiteId,
                'condition_name = ?' => $this->_importConditionName,
            ];
            $adapter->delete($this->getMainTable(), $condition);

            while (false !== ($csvLine = $stream->readCsv())) {
                $rowNumber++;

                if (empty($csvLine)) {
                    continue;
                }

                $row = $this->_getImportRow($csvLine, $rowNumber);
                if ($row !== false) {
                    $importData[] = $row;
                }

                if (count($importData) == 5000) {
                    $this->_saveImportData($importData);
                    $importData = [];
                }
            }
            $this->_saveImportData($importData);
            $stream->close();
        } catch (LocalizedException $e) {
            $adapter->rollBack();
            $stream->close();
            throw new LocalizedException(__($e->getMessage()));
        } catch (Exception $e) {
            $adapter->rollback();
            $stream->close();
            $this->_logger->critical($e);
            throw new LocalizedException(
                __('Something went wrong while importing matrix rates.')
            );
        }

        $adapter->commit();

        if ($this->_importErrors) {
            $error = __(
                'We couldn\'t import this file because of these errors: %1',
                implode(" \n", $this->_importErrors)
            );
            throw new LocalizedException($error);
        }

        return $this;
    }

    /**
     * Load directory countries
     *
     * @return Matrixrate
     */
    protected function _loadDirectoryCountries()
    {
        if ($this->_importIso2Countries !== null && $this->_importIso3Countries !== null) {
            return $this;
        }

        $this->_importIso2Countries = [];
        $this->_importIso3Countries = [];

        /** @var \Magento\Directory\Model\ResourceModel\Country\Collection $collection */
        $collection = $this->_countryCollectionFactory->create();
        foreach ($collection->getData() as $row) {
            $this->_importIso2Countries[$row['iso2_code']] = $row['country_id'];
            $this->_importIso3Countries[$row['iso3_code']] = $row['country_id'];
        }

        return $this;
    }

    /**
     * Load directory regions
     *
     * @return Matrixrate
     */
    protected function _loadDirectoryRegions()
    {
        if ($this->_importRegions !== null) {
            return $this;
        }

        $this->_importRegions = [];

        /** @var Collection $collection */
        $collection = $this->_regionCollectionFactory->create();
        /** @var Region $row */
        foreach ($collection->getItems() as $row) {
            $this->_importRegions[$row->getCountryId()][$row->getCode()] = (int)$row->getregionId();
        }

        ksort($this->_importRegions);

        return $this;
    }

    /**
     * @param $row
     * @param int $rowNumber
     * @return array|bool
     * @throws LocalizedException
     */
    protected function _getImportRow($row, $rowNumber = 0)
    {
        // Validate row
        if (count($row) < 7) {
            $this->_importErrors[] = __(
                'Please correct Matrix Rates format in Row #%1. Invalid Number of Rows',
                $rowNumber
            );
            return false;
        }

        // Strip whitespace from the beginning and end of each row
        foreach ($row as $k => $v) {
            $row[$k] = trim($v);
        }

        // Validate country
        if (isset($this->_importIso2Countries[$row[0]])) {
            $countryId = $this->_importIso2Countries[$row[0]];
        } elseif (isset($this->_importIso3Countries[$row[0]])) {
            $countryId = $this->_importIso3Countries[$row[0]];
        } elseif ($row[0] == '*' || $row[0] == '') {
            $countryId = '0';
        } else {
            $this->_importErrors[] = __('Please correct Country "%1" in Row #%2.', $row[0], $rowNumber);
            return false;
        }

        // Validate region
        if (is_numeric($row[1]) && array_search($row[1], $this->_importRegions[$countryId])) {
            $regionId = $row[1];
        } elseif ($countryId != '0' && isset($this->_importRegions[$countryId][$row[1]])) {
            $regionId = $this->_importRegions[$countryId][$row[1]];
        } elseif ($row[1] == '*' || $row[1] == '') {
            $regionId = 0;
        } else {
            $this->_importErrors[] = __('Please correct Region/State "%1" in Row #%2.', $row[1], $rowNumber);
            return false;
        }

        // Detect city
        if ($row[2] == '*' || $row[2] == '') {
            $city = '*';
        } else {
            $city = $row[2];
        }


        // Detect zip code
        if ($row[3] == '*' || $row[3] == '') {
            $zipCode = '*';
        } else {
            $zipCode = $row[3];
        }

        // Zip from
        if ($row[4] == '*' || $row[4] == '') {
            $zip_to = '';
        } else {
            $zip_to = $row[4];
        }

        // Validate condition from value
        $valueFrom = $this->_parseDecimalValue($row[6]);
        if ($valueFrom === false) {
            $this->_importErrors[] = __(
                'Please correct %1 From "%2" in Row #%3.',
                $this->_getConditionFullName($this->_importConditionName),
                $row[6],
                $rowNumber
            );
            return false;
        }
        // Validate condition to value
        $valueTo = $this->_parseDecimalValue($row[7]);
        if ($valueTo === false) {
            $this->_importErrors[] = __(
                'Please correct %1 To "%2" in Row #%3.',
                $this->_getConditionFullName($this->_importConditionName),
                $row[7],
                $rowNumber
            );
            return false;
        }


        // validate price
        $price = $this->_parseDecimalValue($row[8]);
        if ($price === false) {
            $this->_importErrors[] = __('Please correct Shipping Price "%1" in Row #%2.', $row[8], $rowNumber);
            return false;
        }

        $cost = $this->_parseDecimalValue($row[9]);
        if ($cost === false) {
            $this->_importErrors[] = __('Please correct Cost "%1" in Row #%2.', $row[9], $rowNumber);
            return false;
        }

        // validate shipping method
        if ($row[10] == '*' || $row[10] == '') {
            $this->_importErrors[] = __('Please correct Shipping Method "%1" in Row #%2.', $row[10], $rowNumber);
            return false;
        } else {
            $shippingMethod = $row[10];
        }

        // protect from duplicate
        $hash = sprintf(
            '%s-%d-%s-%s-%F-%F-%s',
            $countryId,
            $city,
            $regionId,
            $zipCode,
            $valueFrom,
            $valueTo,
            $shippingMethod
        );
        if (isset($this->_importUniqueHash[$hash])) {
            $this->_importErrors[] = __(
                'Duplicate Row #%1 (Country "%2", Region/State "%3", City "%4", Zip from "%5", Zip to "%6", From Value "%7", To Value "%8", and Shipping Method "%9")',
                $rowNumber,
                $row[0],
                $row[1],
                $city,
                $zipCode,
                $zip_to,
                $valueFrom,
                $valueTo,
                $shippingMethod
            );
            return false;
        }
        $this->_importUniqueHash[$hash] = true;

        return [
            'website_id' => $this->_importWebsiteId,                    // website_id
            'dest_country_id' => $countryId,                            // dest_country_id
            'dest_region_id' => $regionId,                              // dest_region_id,
            'dest_city' => $city,                                       // city,
            'dest_zip' => $zipCode,                                     // dest_zip
            'dest_zip_to' => $zip_to,                                   // zip to
            'condition_name' => $this->_importConditionName,            // condition_name,
            'condition_from_value' => $valueFrom,      // condition_value From
            'condition_to_value' => $valueTo,                           // condition_value To
            'price' => $price,                                          // price
            'cost' => $cost,                                            // cost
            'shipping_method' => $shippingMethod
        ];
    }

    /**
     * Parse and validate positive decimal value
     * Return false if value is not decimal or is not positive
     *
     * @param string $value
     * @return bool|float
     */
    protected function _parseDecimalValue($value)
    {
        if (!is_numeric($value)) {
            return false;
        }
        $value = (double)sprintf('%.4F', $value);
        if ($value < 0.0000) {
            return false;
        }
        return $value;
    }

    /**
     * @param $conditionName
     * @return mixed
     * @throws LocalizedException
     */
    protected function _getConditionFullName($conditionName)
    {
        if (!isset($this->_conditionFullNames[$conditionName])) {
            $name = $this->_rate->getCode('condition_name_short', $conditionName);
            $this->_conditionFullNames[$conditionName] = $name;
        }

        return $this->_conditionFullNames[$conditionName];
    }

    /**
     * @param array $data
     * @return $this
     * @throws LocalizedException
     */
    protected function _saveImportData(array $data)
    {
        if (!empty($data)) {
            $this->getConnection()->insertOnDuplicate($this->getMainTable(), $data);
            $this->_importedRows += count($data);
        }

        return $this;
    }

    /**
     * Define main table and id field name
     *
     * @return void
     */
    protected function _construct()
    {
        $this->_init($this->_rmaHelper->getEasyReturnMatrixrateSource(), 'pk');
    }

    protected function _doesCountryExists($row)
    {
        return isset($this->_importRegions[$row[0]]);
    }
}
