<?php
// Copyright (c) Pickware GmbH. All rights reserved.
// This file is part of software that is released under a proprietary license.
// You must not copy, modify, distribute, make publicly available, or execute
// its contents or parts thereof without express permission by the copyright
// holder, unless otherwise permitted by law.

namespace Shopware\Plugins\ViisonSetArticles\Components\SwagImportExportIntegration\DbAdapters;

use Doctrine\ORM\AbstractQuery;
use Enlight_Components_Snippet_Namespace;
use Shopware\Components\Model\ModelManager;
use Shopware\Plugins\ViisonCommon\Classes\Plugins\SwagImportExport\SwagImportExportVersion2\SwagAdapterException;
use Shopware\CustomModels\ViisonSetArticles\SetArticle;
use Shopware\Models\Article\Article;
use Shopware\Models\Article\Detail as ArticleDetail;
use Shopware\Plugins\ViisonCommon\Classes\Plugins\SwagImportExport\AbstractDbAdapter;
use Shopware\Plugins\ViisonCommon\Classes\Plugins\SwagImportExport\DbAdapterValidator;
use Shopware\Plugins\ViisonSetArticles\Util;
use Shopware_Components_Config;
use Shopware_Components_Snippet_Manager;

class ViisonSetArticlesCompositionsDbAdapter extends AbstractDbAdapter
{
    /**
     * @var DbAdapterValidator
     */
    protected $validator;

    /**
     * @param Shopware_Components_Config $shopwareConfig
     * @param ModelManager $entityManager
     * @param Shopware_Components_Snippet_Manager $snippetManager
     */
    public function __construct(
        Shopware_Components_Config $shopwareConfig,
        ModelManager $entityManager,
        Shopware_Components_Snippet_Manager $snippetManager
    ) {
        parent::__construct($shopwareConfig, $entityManager, $snippetManager);

        $this->validator = new DbAdapterValidator(
            $snippetManager,
            [
                'string' => [
                    'setArticleOrderNumber',
                    'subArticleOrderNumber',
                ],
                'unsignedInt' => [
                    'quantity',
                ],
            ],
            [
                'setArticleOrderNumber',
            ]
        );
    }

    /**
     * @inheritdoc
     */
    public function getSections()
    {
        return [
            [
                'setArticleOrderNumber' => 'default',
            ],
        ];
    }

    /**
     * @inheritdoc
     */
    public function getDefaultColumns()
    {
        return [
            'setArticleDetail.number as setArticleOrderNumber',
            'subArticleDetail.number as subArticleOrderNumber',
            'setArticle.quantity as quantity',
        ];
    }

    /**
     * @inheritdoc
     * @throws \Exception
     */
    public function read($ids, $columns)
    {
        if (!$ids) {
            throw new \Exception($this->getSnippetNamespace()->get('db_adapter/error/read/no_ids', 'Cannot read set article compositions without IDs.'));
        }

        // Build the main query
        $builder = $this->entityManager->createQueryBuilder();
        $builder
            ->select($columns)
            ->from('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle', 'setArticle')
            ->leftJoin('setArticle.setArticleDetail', 'setArticleDetail')
            ->leftJoin('setArticle.articleDetail', 'subArticleDetail')
            ->where('setArticle.id IN (:ids)')
            ->setParameter('ids', $ids)
            ->orderBy('setArticleDetail.number, subArticleDetail.number');

        // Fetch results
        $query = $builder->getQuery();
        $query->setHydrationMode(AbstractQuery::HYDRATE_ARRAY);
        $paginator = $this->entityManager->createPaginator($query);
        $result = $paginator->getIterator()->getArrayCopy();

        return [
            'default' => $result,
        ];
    }

    /**
     * @inheritdoc
     */
    public function readRecordIds($start, $limit, $filter)
    {
        // Prepare query for fetching set article composition IDs
        $builder = $this->entityManager
            ->createQueryBuilder()
            ->select('setArticle.id')
            ->from('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle', 'setArticle');

        if (!empty($filter)) {
            $builder->addFilter($filter);
        }
        if ($start) {
            $builder->setFirstResult($start);
        }
        if ($limit) {
            $builder->setMaxResults($limit);
        }

        return $builder->getQuery()->getResult();
    }

    /**
     * @inheritdoc
     * @throws \Exception
     */
    public function write($records)
    {
        if (!$records['default']) {
            throw new \Exception($this->getSnippetNamespace()->get('db_adapter/error/write/no_records', 'No set article compositions were found.'));
        }

        $changedEntities = [];
        foreach ($records['default'] as $record) {
            try {
                $changedEntity = $this->writeRecord($record);
                if ($changedEntity) {
                    $changedEntities[] = $changedEntity;
                }
            } catch (SwagAdapterException $e) {
                $this->saveMessage($e->getMessage());
            }

            // Flush changes in batches to improve performance
            if (count($changedEntities) === 50) {
                $this->entityManager->flush($changedEntities);
                $changedEntities = [];
            }
        }

        if (count($changedEntities) > 0) {
            $this->entityManager->flush($changedEntities);
        }
    }

    /**
     * @return Enlight_Components_Snippet_Namespace
     */
    private function getSnippetNamespace()
    {
        return $this->snippetManager->getNamespace('backend/plugins/swag_import_export/exceptions');
    }

    /**
     * @param array $record
     * @return SetArticle|null
     * @throws SwagAdapterException
     */
    private function writeRecord($record)
    {
        // Clean and validate the record
        $record = $this->validator->filterEmptyString($record);
        $this->validator->checkRequiredFields($record);
        $this->validator->validateFieldTypes($record);

        /** @var ArticleDetail $setArticleDetail */
        $setArticleDetail = $this->entityManager->getRepository('Shopware\\Models\\Article\\Detail')->findOneBy([
            'number' => $record['setArticleOrderNumber'],
        ]);
        if (!$setArticleDetail) {
            throw new SwagAdapterException(
                sprintf(
                    $this->getSnippetNamespace()->get(
                        'db_adapter/error/write/no_article_detail',
                        'No article detail with number %s could be found.'
                    ),
                    $record['setArticleOrderNumber']
                )
            );
        }

        if (empty($record['subArticleOrderNumber'])) {
            // The sub article identification (number) was set to null, '' or 0. Therefore remove the 'set article
            // active' flag from this article and all of it's variants. But keep existing compositions.
            $this->flagAllVariantsAsSetArticleActive($setArticleDetail->getArticle(), false);

            return null;
        }

        /** @var ArticleDetail $subArticleDetail */
        $subArticleDetail = $this->entityManager->getRepository('Shopware\\Models\\Article\\Detail')->findOneBy([
            'number' => $record['subArticleOrderNumber'],
        ]);
        if (!$subArticleDetail) {
            throw new SwagAdapterException(
                sprintf(
                    $this->getSnippetNamespace()->get(
                        'db_adapter/error/write/no_article_detail',
                        'No article detail with number %s could be found.'
                    ),
                    $record['subArticleOrderNumber']
                )
            );
        }

        if (Util::isSetArticleByArticle($subArticleDetail->getArticle())) {
            throw new SwagAdapterException(
                sprintf(
                    $this->getSnippetNamespace()->get(
                        'db_adapter/error/write/sub_article_is_set_article',
                        'Article variant with number "%s" is already a set article and cannot be assigned as a sub article.'
                    ),
                    $record['subArticleOrderNumber']
                )
            );
        }

        // Both the set and sub article detail are set and found. Check for an existing composition.
        /** @var SetArticle $setArticle */
        $setArticle = $this->entityManager->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle')
            ->findOneBy([
                'setId' => $setArticleDetail->getId(),
                'articleDetailId' => $subArticleDetail->getId(),
            ]);

        if (empty($record['quantity'])) {
            if ($setArticle) {
                $this->entityManager->remove($setArticle);
                $this->entityManager->flush();
            }

            // Check if there are any other set article compositions for the (main) article of this set article detail
            // exist. If the last set article composition was removed (or none existed in the first place) flag all
            // variants as not 'set article active'
            if (!$this->checkIfSetArticleCompositionsExistForArticle($setArticleDetail->getArticle())) {
                $this->flagAllVariantsAsSetArticleActive($setArticle->getSetArticleDetail()->getArticle(), false);
            }

            return null;
        }

        // This method adds or updates an existing composition. Flag all variants as 'set article active'
        $this->flagAllVariantsAsSetArticleActive($setArticleDetail->getArticle(), true);

        if (!$setArticle) {
            $setArticle = new SetArticle();
            $setArticle->setSetId($setArticleDetail->getId());
            $setArticle->setSetArticleDetail($setArticleDetail);
            $setArticle->setArticleDetailId($subArticleDetail->getId());
            $setArticle->setArticleDetail($subArticleDetail);
            $this->entityManager->persist($setArticle);
        }
        $setArticle->setQuantity($record['quantity']);

        return $setArticle;
    }

    /**
     * @param Article $article
     * @param bool $flag
     * @throws SwagAdapterException
     */
    private function flagAllVariantsAsSetArticleActive(Article $article, $flag)
    {
        if ($flag) {
            // Check if any article detail is a sub article to any set article. If so, the article and it's variants
            // cannot be flagged as a set article.
            $articleDetailIds = array_map(function (ArticleDetail $articleDetail) {
                return $articleDetail->getId();
            }, $article->getDetails()->toArray());

            /** @var SetArticle[] $setArticles */
            $setArticles = $this->entityManager->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle')
                ->findBy([
                    'articleDetailId' => $articleDetailIds,
                ]);

            if (count($setArticles) > 0) {
                throw new SwagAdapterException(
                    vsprintf(
                        $this->getSnippetNamespace()->get(
                            'db_adapter/error/write/set_article_is_sub_article',
                            'Article variant with number %s is already a sub article of a set article and therefore ' .
                            'article "%s" and it\'s variants cannot be flagged as a set article.'
                        ),
                        [
                            $setArticles[0]->getSetArticleDetail()->getNumber(),
                            $article->getName(),
                        ]
                    )
                );
            }
        }

        $changedEntities = [];
        foreach ($article->getDetails()->toArray() as $articleDetail) {
            if ($articleDetail->getAttribute()
                && $articleDetail->getAttribute()->getViisonSetarticleActive() !== $flag
            ) {
                $articleDetail->getAttribute()->setViisonSetarticleActive($flag);
                $changedEntities[] = $articleDetail->getAttribute();
            }
        }

        if (count($changedEntities) > 0) {
            $this->entityManager->flush($changedEntities);
        }
    }

    /**
     * @param Article $article
     * @return bool
     */
    private function checkIfSetArticleCompositionsExistForArticle(Article $article)
    {
        $articleDetailIds = array_map(function (ArticleDetail $articleDetail) {
            return $articleDetail->getId();
        }, $article->getDetails()->toArray());

        $setArticles = $this->entityManager->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle')
            ->findBy([
                'setId' => $articleDetailIds,
            ]);

        return (count($setArticles) > 0);
    }
}
