<?php
/**
 * @author Amasty Team
 * @copyright Copyright (c) Amasty (https://www.amasty.com)
 * @package GeoIP Data for Magento 2 (System)
 */

namespace Amasty\Geoip\Model;

use Amasty\Geoip\Helper\Data;
use Exception;
use Magento\Backend\Model\Session;
use Magento\Framework\App\Config\ConfigResource\ConfigInterface;
use Magento\Framework\App\Config\ReinitableConfigInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\DeploymentConfig;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Config\ConfigOptionsListConstants;
use Magento\Framework\Filesystem\Driver\File;
use Magento\Framework\Model\AbstractModel;
use Magento\Framework\Stdlib\DateTime\DateTime;
use Magento\Framework\Message\ManagerInterface;

class Import extends AbstractModel
{
    /**
     * @var string
     */
    protected $_tablePrefix;

    /**
     * @var int
     */
    protected $_rowsPerTransaction = 10000;

    /**
     * Resource model of config data
     *
     * @var ConfigInterface
     */
    protected $configInterface;

    /**
     * Database write connection
     *
     * @var ResourceConnection
     */
    protected $resource;

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

    /**
     * @var DateTime
     */
    protected $coreDate;

    /**
     * @var Session
     */
    protected $backendSession;

    /**
     * @var DeploymentConfig $_deploymentConfig
     */
    protected $_deploymentConfig;

    /**
     * @var ReinitableConfigInterface $reinitableInterface
     */
    protected $reinitableInterface;

    /**
     * @var Data $helper
     */
    protected $helper;

    /**
     * @var bool
     */
    protected $_firstTime = true;

    /**
     * @var File
     */
    private $driverFile;

    /**
     * @var ManagerInterface
     */
    private $messageManager;

    /**
     * Import constructor.
     *
     * @param ConfigInterface $configInterface
     * @param ReinitableConfigInterface $reinitableConfig
     * @param ResourceConnection $resource
     * @param ScopeConfigInterface $scopeConfig
     * @param DateTime $coreDate
     * @param Session $backendSession
     * @param DeploymentConfig $deploymentConfig
     * @param Data $helper
     * @param File $driverFile
     * @param ManagerInterface $messageManager
     */
    public function __construct(
        ConfigInterface $configInterface,
        ReinitableConfigInterface $reinitableConfig,
        ResourceConnection $resource,
        ScopeConfigInterface $scopeConfig,
        DateTime $coreDate,
        Session $backendSession,
        DeploymentConfig $deploymentConfig,
        Data $helper,
        File $driverFile,
        ManagerInterface $messageManager
    ) {
        $this->configInterface = $configInterface;
        $this->reinitableInterface = $reinitableConfig;
        $this->resource = $resource;
        $this->scopeConfig = $scopeConfig;
        $this->coreDate = $coreDate;
        $this->backendSession = $backendSession;
        $this->_deploymentConfig = $deploymentConfig;
        $this->helper = $helper;
        $this->driverFile = $driverFile;
        $this->messageManager = $messageManager;
    }

    /**
     * @param $table
     * @param $filePath
     * @param $action
     * @param int $ignoredLines
     *
     * @return array
     *
     * @throws Exception
     */
    public function startProcess($table, $filePath, $action, $ignoredLines = 0)
    {
        $allowedTableTypes = ['block', 'location', 'block_v6'];
        if (!in_array($table, $allowedTableTypes)) {
            throw new \Magento\Framework\Exception\LocalizedException('Invalid table type');
        }

        $importProcess = [
            'position'    => 0,
            'tmp_table'   => null,
            'rows_count'  => $this->_getRowsCount($filePath),
            'current_row' => 0
        ];
        $write = $this->resource->getConnection();
        $columns = $write->getTables('amasty_geoip_' . $table . '_');
        foreach ($columns as $key => $column) {
            if ($table == 'block' && preg_match('/.+_block_v6/', $column)) {
                unset($columns[$key]);
            }
        }
        $oldTemporary = implode(', ', $columns);

        if (!empty($oldTemporary)) {
            $write->dropTable($oldTemporary);
        }

        if (($handle = $this->driverFile->fileOpen($filePath, "r")) !== false) {
            $tmpTableName = $this->_prepareImport($table);

            while ($ignoredLines > 0 && ($data = $this->driverFile->fileGetCsv($handle, 0, ",")) !== false) {
                $ignoredLines--;
            }

            $importProcess['position'] = $this->driverFile->fileTell($handle);
            $importProcess['tmp_table'] = $tmpTableName;
        }

        $write->truncateTable($this->resource->getTableName('amasty_geoip_block'));
        $write->truncateTable($this->resource->getTableName('amasty_geoip_block_v6'));
        $write->truncateTable($this->resource->getTableName('amasty_geoip_location'));

        $this->helper->flushConfigCache();
        $this->_saveInDb($table, $importProcess);

        return $importProcess;
    }

    /**
     * @return bool
     */
    public function importTableHasData()
    {
        $write = $this->resource->getConnection();
        $blockTable = $this->resource->getTableName('amasty_geoip_block');
        $blockV6Table = $this->resource->getTableName('amasty_geoip_block_v6');
        $locationTable = $this->resource->getTableName('amasty_geoip_location');

        $countBlock = $write->select('count(block_id)')->from($blockTable)->limit(1);
        $countBlockV6 = $write->select('count(block_id)')->from($blockV6Table)->limit(1);
        $countLocation = $write->select('count(location_id)')->from($locationTable)->limit(1);

        $block = $write->fetchCol($countBlock);
        $blockV6 = $write->fetchCol($countBlockV6);
        $location = $write->fetchCol($countLocation);

        return array_key_exists(0, $block)
            && array_key_exists(0, $blockV6)
            && array_key_exists(0, $location)
            && $block[0]
            && $location[0];
    }

    /**
     * @param $filePath
     *
     * @return int
     */
    protected function _getRowsCount($filePath)
    {
        $lineCount = 0;
        $handle = $this->driverFile->fileOpen($filePath, "r");
        while (!$this->driverFile->endOfFile($handle)) {
            $this->driverFile->fileGetCsv($handle);
            $lineCount++;
        }
        $this->driverFile->fileClose($handle);

        return $lineCount;
    }

    /**
     * @param $table
     *
     * @return string
     */
    protected function _prepareImport($table)
    {
        $write = $this->resource->getConnection();

        $targetTable = $this->resource->getTableName('amasty_geoip_' . $table);

        $tmpTableName = uniqid($targetTable . '_');

        //@codingStandardsIgnoreStart
        $query = 'create table ' . $tmpTableName . ' like ' . $targetTable;
        //@codingStandardsIgnoreEnd
        $write->query($query);

        $write->changeTableEngine($tmpTableName, 'innodb');

        return $tmpTableName;
    }

    /**
     * @param $table
     * @param $filePath
     * @param $action
     *
     * @return array
     * @throws Exception
     */
    public function doProcess($table, $filePath, $action)
    {
        $ret = [];
        if (($handle = $this->driverFile->fileOpen($filePath, "r")) !== false) {
            $this->helper->flushConfigCache();
            $importProcess = $this->_getFromDb($table);

            if (!$importProcess) {
                throw new \Magento\Framework\Exception\LocalizedException('run start before');
            }
            $tmpTableName = $importProcess['tmp_table'];
            try {
                $position = $importProcess['position'];
                $this->driverFile->fileSeek($handle, $position);
                $transactionIterator = 0;
                if ($action != 'import') {
                    $this->_tablePrefix = (string)$this->_deploymentConfig->get(
                        ConfigOptionsListConstants::CONFIG_PATH_DB_PREFIX
                    );
                }
                $dataForImport = [];

                while (($data = $this->driverFile->fileGetCsv($handle, 0, ",")) !== false) {
                    $dataForImport[] = $data;
                    $transactionIterator++;

                    if ($transactionIterator >= $this->_rowsPerTransaction) {
                        break;
                    }
                }

                if (count($dataForImport)) {
                    $this->_importItem($table, $tmpTableName, $dataForImport);
                }

                if ($this->_rowsPerTransaction > $importProcess['rows_count']) {
                    $importProcess['current_row'] = $importProcess['rows_count'];
                }
                $importProcess['current_row'] += $transactionIterator;
                $importProcess['position'] = $this->driverFile->fileTell($handle);
                $this->_saveInDb($table, $importProcess);
                $ret = $importProcess;
            } catch (Exception $e) {
                if ($action == 'import') {
                    $this->_destroyImport($tmpTableName);
                }
                throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()));
            }
        }

        return $ret;
    }

    /**
     * @param $table
     * @param bool $isDownload
     *
     * @return bool
     * @throws Exception
     */
    public function commitProcess($table, $isDownload = false)
    {
        $ret = false;
        $this->helper->flushConfigCache();
        $importProcess = $this->_getFromDb($table);

        if ($importProcess) {
            $tmpTableName = $importProcess['tmp_table'];

            if ($isDownload) {
                $configDate = 'date_download';
            } else {
                $configDate = 'date';
            }

            try {
                $this->configInterface
                    ->saveConfig('amgeoip/import/' . $table, 1, 'default', 0)
                    ->saveConfig('amgeoip/import/' . $configDate, $this->coreDate->gmtDate(), 'default', 0);

                $this->reinitableInterface->reinit();

                $this->_doneImport($table, $tmpTableName);

            } catch (Exception $e) {
                $this->messageManager->addErrorMessage($e->getMessage());
            }

            $ret = true;
        } else {
            throw new \Magento\Framework\Exception\LocalizedException(__('run start before'));
        }

        return $ret;
    }

    /**
     * @param $table
     * @param $tmpTableName
     */
    protected function _doneImport($table, $tmpTableName)
    {
        $write = $this->resource->getConnection();

        $targetTable = $this->resource->getTableName('amasty_geoip_' . $table);

        if ($write->isTableExists($tmpTableName)) {
            $write->dropTable($targetTable);

            $write->renameTable($tmpTableName, $targetTable);
        }
    }

    /**
     * @param $table
     * @param $tmpTableName
     * @param $dataForImport
     */
    protected function _importItem($table, $tmpTableName, &$dataForImport)
    {
        $dataForInsert = [];
        $write = $this->resource->getConnection();

        foreach ($dataForImport as $data) {
            if ($table == 'block' && is_array($data) && isset($data[0])) {
                list($ip, $mask) = explode('/', $data[0]);
                $ip2long = ip2long($ip);
                $min = ($ip2long >> (32 - $mask)) << (32 - $mask);
                $max = $ip2long | ~(-1 << (32 - $mask));

                $dataForInsert[] =  [
                    'start_ip_num' => $min,
                    'end_ip_num'   => $max,
                    'geoip_loc_id' => $data[1],
                    'postal_code'  => $data[6],
                    'latitude'     => $data[7],
                    'longitude'    => $data[8]
                ];
            } elseif ($table == 'location' && is_array($data)) {
                $dataForInsert[] = [
                    'geoip_loc_id' => $data[0],
                    'country'      => $data[4],
                    'city'         => $data[10]
                ];
            } elseif ($table == 'block_v6' && is_array($data) && isset($data[0])) {
                list($ip, $mask) = explode('/', $data[0]);

                $firstAddrBin = inet_pton($ip);
                //@codingStandardsIgnoreStart
                $elem = unpack('H*', $firstAddrBin);
                //@codingStandardsIgnoreEnd
                $firstAddrHex = reset($elem);
                $firstAddrStr = inet_ntop($firstAddrBin);
                $flexBits = 128 - $mask;
                $lastAddrHex = $firstAddrHex;
                $pos = 31;

                while ($flexBits > 0) {
                    $orig = substr($lastAddrHex, $pos, 1);
                    $origVal = hexdec($orig);
                    $newVal = $origVal | (pow(2, min(4, $flexBits)) - 1);
                    $new = dechex($newVal);
                    $lastAddrHex = substr_replace($lastAddrHex, $new, $pos, 1);
                    $flexBits -= 4;
                    $pos--;
                }

                $lastAddrBin = pack('H*', $lastAddrHex);
                $lastAddrStr = inet_ntop($lastAddrBin);

                $dataForInsert[] = [
                    'start_ip_num' => $this->helper->getLongIpV6($firstAddrStr),
                    'end_ip_num'   => $this->helper->getLongIpV6($lastAddrStr),
                    'geoip_loc_id' => $data[1],
                    'postal_code'  => $data[6],
                    'latitude'     => $data[7],
                    'longitude'    => $data[8]
                ];
            }
        }

        $write->insertMultiple($tmpTableName, $dataForInsert);
    }

    /**
     * @param $tmpTableName
     */
    protected function _destroyImport($tmpTableName)
    {
        $write = $this->resource->getConnection();

        $write->dropTable($tmpTableName);

        $this->_clearDb();
    }

    /**
     * @param $table
     * @param $importProcess
     */
    protected function _saveInDb($table, $importProcess)
    {
        $this->configInterface->saveConfig(
            'amgeoip/import/position/' . $table,
            $importProcess['position'],
            'default',
            0
        );
        $this->configInterface->saveConfig(
            'amgeoip/import/tmp_table/' . $table,
            $importProcess['tmp_table'],
            'default',
            0
        );
        $this->configInterface->saveConfig(
            'amgeoip/import/rows_count/' . $table,
            $importProcess['rows_count'],
            'default',
            0
        );
        $this->configInterface->saveConfig(
            'amgeoip/import/current_row/' . $table,
            $importProcess['current_row'],
            'default',
            0
        );
    }

    /**
     * @param $table
     *
     * @return array
     */
    protected function _getFromDb($table)
    {
        $importProcess = [];
        $importProcess['position'] = $this->scopeConfig->getValue(
            'amgeoip/import/position/' . $table,
            'default',
            0
        );
        $importProcess['tmp_table'] = $this->scopeConfig->getValue(
            'amgeoip/import/tmp_table/' . $table,
            'default',
            0
        );
        $importProcess['rows_count'] = $this->scopeConfig->getValue(
            'amgeoip/import/rows_count/' . $table,
            'default',
            0
        );
        $importProcess['current_row'] = $this->scopeConfig->getValue(
            'amgeoip/import/current_row/' . $table,
            'default',
            0
        );

        return $importProcess;
    }

    /**
     *
     */
    protected function _clearDb()
    {
        $this->configInterface->deleteConfig('amgeoip/import/position/location', 'default', 0);
        $this->configInterface->deleteConfig('amgeoip/import/position/block', 'default', 0);
        $this->configInterface->deleteConfig('amgeoip/import/position/block_v6', 'default', 0);
        $this->configInterface->deleteConfig('amgeoip/import/tmp_table/location', 'default', 0);
        $this->configInterface->deleteConfig('amgeoip/import/tmp_table/block', 'default', 0);
        $this->configInterface->deleteConfig('amgeoip/import/tmp_table/block_v6', 'default', 0);
        $this->configInterface->deleteConfig('amgeoip/import/rows_count/location', 'default', 0);
        $this->configInterface->deleteConfig('amgeoip/import/rows_count/block', 'default', 0);
        $this->configInterface->deleteConfig('amgeoip/import/rows_count/block_v6', 'default', 0);
        $this->configInterface->deleteConfig('amgeoip/import/current_row/location', 'default', 0);
        $this->configInterface->deleteConfig('amgeoip/import/current_row/block', 'default', 0);
        $this->configInterface->deleteConfig('amgeoip/import/current_row/block_v6', 'default', 0);
    }
}
