<?php

namespace FiloBlu\Flow\Model\From;

use Exception;
use FiloBlu\Flow\Exception\BlockingException;
use FiloBlu\Flow\Exception\NonBlockingException;
use FiloBlu\Flow\Model\Channel;
use FiloBlu\Flow\Model\Inboundflow;
use FiloBlu\Flow\Model\Processor;
use Magento\Framework\Data\Collection\AbstractDb;
use Magento\Framework\DB\Adapter\DeadlockException;
use Magento\Framework\Model\AbstractModel;
use Magento\Framework\Model\Context;
use Magento\Framework\Model\ResourceModel\AbstractResource;
use Magento\Framework\Registry;
use RuntimeException;
use Zend_Db;

/**
 * Class AbstractFrom
 * @package FiloBlu\Flow\Model\From
 * @method setMetaProcessed(bool $true)
 */
abstract class AbstractFrom extends AbstractModel
{
    /**
     * @var int
     */
    const PRODUCT_NOT_FOUND_META_STATUS = -2;

    /**
     * @var string
     */
    const XML_PATH_EMAIL_ADDRESS_NOTIFY_ERROR = 'filoblu_flow/flow_email_notification/flow_email_notify_address';

    /**
     * @var string
     */
    const XML_PATH_STORE_NAME = 'general/store_information/name';

    /**
     * @var string
     */
    protected $_error_message;

    /**
     * Undocumented variable
     *
     * @var array
     */
    protected $_email_notify_errors = [];

    /**
     * @var Channel
     */
    protected $channel;

    /**
     * @param Context $context
     * @param Registry $registry
     * @param AbstractResource $resource
     * @param AbstractDb $resourceCollection
     * @param array $data
     */
    public function __construct(
        Context $context,
        Registry $registry,
        AbstractResource $resource = null,
        AbstractDb $resourceCollection = null,
        array $data = []
    ) {
        parent::__construct($context, $registry, $resource, $resourceCollection, $data);
    }

    /**
     * @return $this
     */
    public function setProcessed()
    {
        $now = date('Y-m-d H:i:s');
        $this->setMetaProcessed(true)
            ->setMetaProcessTime($now);
        return $this;
    }

    /**
     * @return Channel
     */
    public function getChannel()
    {
        return $this->channel;
    }

    /**
     * @param $channel
     * @return $this
     */
    public function setChannel($channel)
    {
        $this->channel = $channel;
        return $this;
    }

    /**
     * @param $msg
     */
    public function addEmailNotifyError($msg)
    {
        $this->_email_notify_errors[] = $msg;
    }

    /**
     * @return array
     */
    public function getEmailNotifyError()
    {
        return $this->_email_notify_errors;
    }

    /**
     * @return mixed
     */
    public function getErrorMessage()
    {
        return $this->_error_message;
    }

    /**
     * @param $msg
     */
    public function setErrorMessage($msg)
    {
        $this->_error_message = $msg;
    }

    /**
     * @param Inboundflow $file
     * @return mixed
     * @throws Exception
     */
    public function processFileRows($file)
    {
        if ($file->getFlow() == 'TableFlow') {
            return $file;
        }

        $connection = $this->getResourceCollection()->getConnection();

        $hasExecutedOnColumn = $connection->tableColumnExists($this->getResource()->getMainTable(), 'executed_on');
        // All query to execute by order
        $allQuery = [];

        // If I have the 'executed on' column then I'm inside the EAVS
        if ($hasExecutedOnColumn) {
            // 1st
            // All the EAVS actions (meta_ref_id = 0)
            // All the EAVS special actions executed on 'product' (executed_on = 'product')
            $rows = $this->getCollection()
                ->addFieldToFilter('meta_file', $file->getId())
                ->addFieldToFilter('meta_processed', 0)
                ->addFieldToFilter(
                    ['meta_ref_id', 'executed_on'],
                    [
                        ['eq' => '0'],
                        ['eq' => 'product']
                    ]
                );
            $rows->getSelect()
                ->order('e DESC')
                ->order('meta_ref_id ASC');
            $allQuery[] = $rows;

            // 2nd
            // All the EAVS special actions executed on 'attribute' (executed_on = 'attribute')
            $rows = $this->getCollection()
                ->addFieldToFilter('meta_file', $file->getId())
                ->addFieldToFilter('meta_processed', 0)
                // Greater than 0 are only special actions
                ->addFieldToFilter('meta_ref_id', ['gt' => '0'])
                ->addFieldToFilter('executed_on', 'attribute');
            $rows->getSelect()
                ->order('e DESC')
                ->order('meta_ref_id ASC');
            $allQuery[] = $rows;

            // 3rd
            // All the EAVS special actions remaining
            $rows = $this->getCollection()
                ->addFieldToFilter('meta_file', $file->getId())
                ->addFieldToFilter('meta_processed', 0)
                // Greater than 0 are only special actions
                ->addFieldToFilter('meta_ref_id', ['gt' => '0']);
            $rows->getSelect()
                ->order('e DESC')
                ->order('meta_ref_id ASC');
            // Fallback to the old default
        } else {
            $rows = $this->getCollection()
                ->addFieldToFilter('meta_file', $file->getId())
                ->addFieldToFilter('meta_processed', 0)
                ->setOrder('meta_ref_id', 'ASC');
        }
        $allQuery[] = $rows;

        $errorCount = 0;
        $lines = 1;
        // Running execution in order
        foreach ($allQuery as $rows) {
            $query = $connection->query((string)$rows->getSelect());
            $query = $this->preprocessFileRows($query, $file);
            $mainTable = $this->getResource()->getMainTable();
            $query_no_cache = str_replace('SELECT', 'SELECT SQL_NO_CACHE', $query->getDriverStatement()->queryString);

            foreach ($connection->fetchAll($query_no_cache, [], Zend_Db::FETCH_ASSOC) as $row) {
                $lines++;
                try {
                    $status = $this->tryExecute($row, 3);
                } catch (BlockingException $blockingException) {
                    $status = Processor::META_PROCESSED_ERROR;
                    throw new RuntimeException(
                        "processing item {$row['meta_id']} : {$blockingException->getMessage()}"
                    );
                } catch (NonBlockingException $nonBlockingException) {
                    $status = Processor::META_PROCESSED_ERROR;
                    $this->_logger->error("FLOW: processing item {$row['meta_id']} : {$nonBlockingException->getMessage()}");
                    $this->appendErrorMessage(
                        "processing item {$row['meta_id']} : {$nonBlockingException->getMessage()}"
                    );
                    $errorCount++;
                } catch (Exception $e) {
                    $this->_logger->error("FLOW: processing item {$row['meta_id']} :{$e->getMessage()}");
                    $this->appendErrorMessage("processing item {$row['meta_id']} : {$e->getMessage()}");
                    $status = Processor::META_PROCESSED_ERROR;
                } finally {
                    $now = date('Y-m-d H:i:s');
                    $connection->query(
                        "UPDATE $mainTable SET meta_processed = ?, meta_process_time = ? WHERE meta_id = ?",
                        [$status, $now, $row['meta_id']]
                    );

                    $table = $file->getResource()->getMainTable();
                    $connection->update($table, ['last_activity' => $now , 'errors' => $errorCount, 'lines' => $lines], ['id = ?'=> $file->getId()]);
                }
            }
        }

        $file->setErrorCount($errorCount);

        if ($query !== null) {
            $this->postprocessFileRows($query);
        }

        $this->postProcess($file);

        // Al termine della fase process dell'import vengono inviate eventuali notifiche di errore via email
        $this->sendErrorNotifications($file);

        if ($errorCount > 0) {
            throw new RuntimeException("Encountered $errorCount errors while processing flow");
        }

        return $file;
    }

    /**
     * @param $row
     * @param $times
     * @return int
     * @throws BlockingException
     * @throws NonBlockingException
     */
    protected function tryExecute($row, $times)
    {
        while ($times--) {
            try {
                $this->setData($row);
                $this->process();
                return Processor::META_PROCESSED_SUCCESS;
            } catch (DeadlockException $exception) {
                usleep(rand(500000, 3500000));
            }
        }

        return Processor::META_PROCESSED_ERROR;
    }

    /**
     * @param $query
     * @param $file
     * @return mixed
     */
    protected function preprocessFileRows($query, $file)
    {
        return $query;
    }

    /**
     * @return self
     * @throws \FiloBlu\Flow\Exception\BlockingException
     * @throws \FiloBlu\Flow\Exception\NonBlockingException
     */
    abstract public function process();

    /**
     * @param $blocking
     * @param $msg
     * @return bool
     * @throws Exception
     */
    protected function error($blocking, $msg)
    {
        if ($blocking) {
            throw new BlockingException($msg);
        }

        throw new NonBlockingException($msg);
        //$this->setErrorMessage("{$this->_error_message }\r\n{$msg}");
        //  return false;
    }

    /**
     * @param $message
     */
    protected function appendErrorMessage($message)
    {
        $this->_error_message .= PHP_EOL . $message;
    }

    /**
     * @param $query
     * @return bool
     */
    protected function postprocessFileRows($query)
    {
        return true;
    }

    /**
     * @param Inboundflow $inboundFlow
     * @return bool
     */
    protected function postProcess(Inboundflow $inboundFlow)
    {
        return true;
    }

    /**
     * @param $file
     * @return mixed
     */
    abstract public function sendErrorNotifications($file);

    /**
     * @param $file
     * @return bool
     * @throws Exception
     */
    public function clean($file)
    {
        if (!$file->getId()) {
            return false;
        }

        $connection = $this->getResource()->getConnection();

        try {
            $sql = 'DELETE FROM ' . $this->getResource()->getMainTable() . ' WHERE meta_file = ' . $file->getId();
            $connection->query($sql);
        } catch (Exception $e) {
            $message = 'processing item ' . $file->getName() . ': ' . $e->getMessage();
            throw new Exception($message);
        }
    }
}
