<?php

namespace FiloBlu\Flow\Model;

use Exception;
use FiloBlu\Flow\Api\StockAdjustmentManagerInterface;
use FiloBlu\Flow\Helper\Data;
use FiloBlu\Flow\Model\From\StockAdjustment;
use FiloBlu\Flow\Model\From\StockAdjustmentFactory;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Exception\LocalizedException;
use Psr\Log\LoggerInterface;
use Throwable;

/**
 * Class StockManagerAdjustment
 * @package FiloBlu\Flow\Model
 */
class StockAdjustmentManager implements StockAdjustmentManagerInterface {

    /**
     * Constants
     */
    const SKU_FIELD = 'sku';
    const WAREHOUSE_FIELD = 'warehouse';
    const QUANTITY_FIELD = 'adjustment';
    const SOURCE_FIELD = 'source';
    const DETAIL_FIELD = 'detail';

    /**
     * @var LoggerInterface
     */
    protected $logger;
    /**
     * @var Processor
     */
    protected $processor;
    /**
     * @var Channel
     */
    protected $channel;
    /**
     * @var Data
     */
    protected $helperData;
    /**
     * @var StockAdjustment
     */
    protected $stockAdjustment;
    /**
     * @var InboundFlowFactory
     */
    protected $inboundFlowFactory;
    /**
     * @var array
     */
    protected $errors = [];
    /**
     * @var ResourceConnection
     */
    protected $resourceConnection;
    /**
     * @var
     */
    protected $config_data;

    /**
     * @param InboundflowFactory $inboundFlowFactory
     * @param Channel $channel
     * @param Data $helperData
     * @param LoggerInterface $logger
     * @param StockAdjustmentFactory $stockAdjustment
     * @param Processor $processor
     * @param ResourceConnection $resourceConnection
     */
    public function __construct(
        InboundflowFactory $inboundFlowFactory,
        Channel $channel,
        Data $helperData,
        LoggerInterface $logger,
        StockAdjustmentFactory $stockAdjustment,
        Processor $processor,
        ResourceConnection $resourceConnection
    ) {
        $this->inboundFlowFactory = $inboundFlowFactory;
        $this->logger = $logger;
        $this->channel = $channel;
        $this->helperData = $helperData;
        $this->stockAdjustment = $stockAdjustment;
        $this->processor = $processor;
        $this->resourceConnection = $resourceConnection;
    }

    /**
     * @param mixed $stock
     * @return mixed|void
     * @throws Exception
     */
    public function save($stock)
    {
        if(!is_array($stock)){
            throw new LocalizedException(__("Please provide an array of stock"));
        }

        if(isset($stock["sku"]) || isset($stock["warehouse"]) || isset($stock["adjustment"])){
            throw new LocalizedException(__("Please provide an array of stock"));
        }

        //Count stock posted
        $stockRecords = (int)count($stock);

        //Check if there is at least one stock to import
        if($stockRecords == 0) {
            throw new LocalizedException(__("Please provide at least one stock to import"));
        }

        //Create inbound
        $inbound = $this->createInbound();
        //Populate inbound
        $this->populateInbound($inbound->getId(), $stock);
        $errorCount = (int)count($this->errors);

        $notExistingSkus = $this->checkNotExistingSkus($inbound);
        $countNotExistingSkus = count($notExistingSkus);
        if($countNotExistingSkus>0) {
            $nsSkus = [];
            $nsIds = [];
            foreach($notExistingSkus as $ns) {
                $nsSkus[] = $ns['sku'];
                $nsIds[] = $ns['meta_id'];
            }
            $this->updateMetaProcessedStatus($nsIds,2);
            $this->errors[] = ["sku" => implode(',',$nsSkus), "message" => "Not existing skus"];
        }

        $errorCount += $countNotExistingSkus;

        if($this->config_data->config->discard_already_processed_element) {
            $alreadyProcessedSkus = $this->checkAlreadyProcessedElements($inbound);
            $countAlreadyProcessedSkus = count($alreadyProcessedSkus);
            if($countAlreadyProcessedSkus>0) {
                $apSkus = [];
                $apIds = [];
                foreach($alreadyProcessedSkus as $ap) {
                    $apSkus[] = $ap['sku'];
                    $apIds[] = $ap['meta_id'];
                }
                $this->updateMetaProcessedStatus($apIds,2);
                $this->errors[] = ["sku" => implode(',',$apSkus), "message" => "Already processed skus for this order"];
            }
            $errorCount += $countAlreadyProcessedSkus;
        }

        //Get error count from populate inbound
        $recordInserted = $stockRecords - $errorCount;
        $operation = "inserted";

        $this->executeProcess($inbound);
        $operation = "processed";

        //Return message
        $return = [
            "results" => [
                "ref_id" => $inbound->getName(),
                "message" => "{$operation} {$recordInserted} records"
            ]
        ];

        //If record inserted = 0 -> set inbound in error
        if($recordInserted==0){
            $this->updateInbound($inbound,['status' => Inboundflow::STATUS_ERROR, 'comment' => "Please provide at least one valid product\n\n".json_encode($this->errors)]);
        }
        //If record inserted > 0 and error count > 0 log errors in inbound
        if($recordInserted>0 && $errorCount>0){
            $this->updateInbound($inbound,['comment' => "Some product won't be updated\n\n".json_encode($this->errors)]);
        }
        //If there are errors show them in the response
        if($errorCount){
            $return["results"]["errors"] = $this->errors;
        }
        return $return;
    }

    /**
     * @param $nsIds
     * @param $status
     * @return void
     */
    protected function updateMetaProcessedStatus($nsIds,$status)
    {
        $connection = $this->resourceConnection->getConnection();
        $FlowFromStockAdjustmentTable = $connection->getTableName('flow_from_stock_adjustment');

        $where = "meta_id IN (".implode(',',$nsIds).")";

        $connection->update(
            $FlowFromStockAdjustmentTable,
            [
                'meta_processed' => $status
            ],
            $where
        );
    }

    /**
     * @param $inbound
     * @param $update
     */
    protected function updateInbound($inbound, $update){
        foreach ($update as $k => $v) {
            $inbound->setData($k, $v);
        }
        $inbound->save();
    }

    /**
     * @param $flow
     * @throws LocalizedException
     */
    public function executeProcess($flow) {
        //Not execute if is in processing
        if ($flow->getStatus() === Inboundflow::STATUS_PROCESSING) {
            return;
        }
        //Check if can run
        if (!$this->processor->canRun($flow)) {
            return;
        }
        //Set processing
        $this->updateInbound($flow,['status' => Inboundflow::STATUS_PROCESSING]);

        try {
            //Process
            $this->processor->processFile($flow);
            //Update processed
            $this->updateInbound($flow, ['status' => Inboundflow::STATUS_PROCESSED]);
        }
        catch (Exception $e) {
            $log = __METHOD__ . ': ' . $e->getMessage() . "\n\n" . $e->getTraceAsString();
            $this->updateInbound($flow, ['status' => Inboundflow::STATUS_ERROR, 'log' => $log]);
        }
        catch (Throwable $t) {
            $log = __METHOD__ . ': ' . $t->getMessage() . "\n\n" . $t->getTraceAsString();
            $this->updateInbound($flow, ['status' => Inboundflow::STATUS_ERROR, 'log' => $log]);
        }
    }

    /**
     * @return false|mixed
     */
    public function init(){
        $collection = $this->channel->getCollection();

        foreach ($collection as $channel) {
            $config_data = json_decode($channel->getData('data'), false);
            $channel_model_class = $this->helperData->getChannelModels($config_data->config->flow);

            if($channel_model_class=='stockadjustment'){
                return $channel;
            }
        }

        return false;
    }

    /**
     * @return Inboundflow
     * @throws Exception
     */
    protected function createInbound(){
        $channel = $this->init();

        if($channel===false) {
            throw new Exception("There is no channel stock adjustment configured");
        }

        $this->config_data = json_decode($channel->getData('data'), false);

        $model = $this->inboundFlowFactory->create();
        $model->setName("api_".$channel->getName().'_'.date('YmdHis'));
        $model->setFlow($this->config_data->config->flow);
        $model->setType($this->config_data->config->type);
        $model->setChannel($channel->getName());
        $model->setRetry(0);
        $model->setStatus(Inboundflow::STATUS_PARSED);
        $model->setPriority($this->config_data->config->priority);
        $model->save();
        return $model;
    }

    /**
     * @param $inboundId
     * @param $stock
     * @throws Exception
     */
    protected function populateInbound($inboundId, $stock)
    {
        foreach ($stock as $s){
            try {
                $this->checkProductStockRecord($s);

                $model = $this->stockAdjustment->create();
                $model->setData('meta_file', $inboundId);
                $model->setData('meta_processed', 0);
                $model->setData('sku', $s[self::SKU_FIELD]);
                $model->setData('warehouse', $s[self::WAREHOUSE_FIELD]);
                $model->setData('adjustment', $s[self::QUANTITY_FIELD]);
                if(isset($s[self::SOURCE_FIELD]) && !empty($s[self::SOURCE_FIELD])){
                    $model->setData('source', $s[self::SOURCE_FIELD]);
                }
                if(isset($s[self::DETAIL_FIELD]) && !empty($s[self::DETAIL_FIELD])){
                    $model->setData('detail', $s[self::DETAIL_FIELD]);
                }
                $model->setData('meta_insert_time', date('Y-m-d H:i:s'));
                $model->save();
                continue;
            }
            catch (Exception $exception) {
                $this->errors[] = ["sku" => $s[self::SKU_FIELD], "message" => $exception->getMessage()];
            }
            catch (Throwable $throwable) {
                $this->errors[] = ["sku" => $s[self::SKU_FIELD], "message" => $throwable->getMessage()];
            }
        }
    }

    /**
     * @param $item
     * @throws Exception
     */
    protected function checkProductStockRecord($item)
    {
        if(!isset($item[self::SKU_FIELD]) || empty($item[self::SKU_FIELD])){
            throw new LocalizedException(__("Please provide a valid sku"));
        }
        if(!isset($item[self::QUANTITY_FIELD]) || $item[self::QUANTITY_FIELD]===null || $item[self::QUANTITY_FIELD]==="" || !is_int($item[self::QUANTITY_FIELD])){
            throw new LocalizedException(__("Please provide a valid integer value for quantity"));
        }
        if(!isset($item[self::WAREHOUSE_FIELD]) || empty($item[self::WAREHOUSE_FIELD])){
            throw new LocalizedException(__("Please provide a valid stock source"));
        }
        if($item[self::WAREHOUSE_FIELD]!='default'){
            //todo check stock
            //throw new LocalizedException(__("Please provide a valid stock source"));
        }
    }

    /**
     * @param $inbound
     * @return array
     */
    protected function checkNotExistingSkus($inbound)
    {
        $connection = $this->resourceConnection->getConnection();
        $ffsa = $connection->getTableName('flow_from_stock_adjustment');
        $cpe = $connection->getTableName('catalog_product_entity');
        $query = "SELECT {$ffsa}.sku, {$ffsa}.meta_id FROM {$ffsa} WHERE {$ffsa}.sku NOT IN (SELECT sku FROM {$cpe}) AND {$ffsa}.meta_file = '" . $inbound->getId() . "'";
        return $connection->fetchAll($query);
    }

    /**
     * @param $inbound
     * @return array
     */
    protected function checkAlreadyProcessedElements($inbound){
        $connection = $this->resourceConnection->getConnection();
        $ffsa = $connection->getTableName('flow_from_stock_adjustment');
        $inboudId = $inbound->getId();
        $query = "SELECT
                        ffsa2.sku,
                        ffsa.meta_id
                    FROM
                        {$ffsa} ffsa
                    LEFT JOIN {$ffsa} ffsa2 ON
                        ffsa.sku = ffsa2.sku
                        AND ffsa.warehouse = ffsa2.warehouse
                        AND ffsa.source = ffsa2.source
                        AND ffsa.detail = ffsa2.detail
                        AND ffsa.source IS NOT NULL
                        AND ffsa.detail IS NOT NULL
                    WHERE ffsa.meta_file = {$inboudId}
                    AND ffsa2.meta_file <> {$inboudId}
                    AND ffsa.meta_processed = 0
                    Group BY ffsa.meta_id
                    ";
        return $connection->fetchAll($query);
    }

}
