<?php

namespace Webkul\AkeneoHistoryManagementBundle\Controller;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

$classCompatibilityListener = new \Webkul\AkeneoHistoryManagementBundle\Listener\ClassDefinationForCompatibility();
$classCompatibilityListener->checkVersionAndCreateClassAliases();

class ProductHistoryController
{
    /** @var \VersionRepositoryInterface */
    protected $versionRepository;

    /** @var \FQCNResolver */
    protected $FQCNResolver;

    /** @var NormalizerInterface */
    protected $normalizer;

    /** @var \UserContext */
    protected $userContext;

    /** @var \ProductRepositoryInterface */
    protected $productRepository;

    /** @var \ProductModelRepositoryInterface */
    protected $productModelRepository;

    /** @var \ObjectFilterInterface */
    protected $objectFilter;

    /** @var \CollectionFilterInterface */
    protected $productEditDataFilter;

    /** @var ValidatorInterface */
    protected $validator;

    /** @var \AttributeConverterInterface */
    protected $localizedConverter;

    /** @var \SaverInterface */
    protected $productSaver;

    /** @var \SaverInterface */
    protected $productModelSaver;

    /** @var \FilterInterface */
    protected $emptyValuesFilter;

    /** @var \EntityWithValuesFilter */
    protected $productModelEmptyValuesFilter;

    /** @var \ConverterInterface */
    protected $productValueConverter;

    /** @var NormalizerInterface */
    protected $constraintViolationNormalizer;

    /** @var \AttributeFilterInterface */
    protected $productAttributeFilter;

    /** @var \AttributeFilterInterface */
    protected $productModelAttributeFilter;

    /** @var \ObjectUpdaterInterface */
    protected $productUpdater;

    /** @var ObjectUpdaterInterface */
    protected $productModelUpdater;

    /** @var \ArrayConverterInterface **/
    protected $productArrayConverter;

    /** @var \ArrayConverterInterface **/
    protected $productModelArrayConverter;

    /** @var \AttributeRepositoryInterface */
    protected $attributeRepository;

    /** @var \AssociationColumnsResolver */
    protected $associationColumnsResolver;

    /** @var array */
    protected $defaultFields = [
        'family',
        'family_variant',
        'groups',
        'categories',
        'parent',
        'sku',
        'code',
        'enabled',
    ];

    /**
     * @param \VersionRepositoryInterface      $versionRepository
     * @param \FQCNResolver                    $FQCNResolver
     * @param NormalizerInterface              $normalizer
     * @param \UserContext                     $userContext
     * @param \ProductRepositoryInterface      $productRepository
     * @param \ProductModelRepositoryInterface $productModelRepository
     * @param \ObjectFilterInterface           $objectFilter
     * @param \CollectionFilterInterface       $productEditDataFilter
     * @param ValidatorInterface               $validator
     * @param \AttributeConverterInterface     $localizedConverter
     * @param \SaverInterface                  $productSaver
     * @param \SaverInterface                  $productModelSaver
     * @param \FilterInterface                 $emptyValuesFilter
     * @param \EntityWithValuesFilter          $productModelEmptyValuesFilter
     * @param \ConverterInterface              $productValueConverter
     * @param NormalizerInterface              $constraintViolationNormalizer
     * @param \AttributeFilterInterface        $productAttributeFilter
     * @param \AttributeFilterInterface        $productModelAttributeFilter
     * @param \ObjectUpdaterInterface          $productUpdater
     * @param \ObjectUpdaterInterface          $productModelUpdater
     * @param \ArrayConverterInterface         $productArrayConverter,
     * @param \ArrayConverterInterface         $productModelArrayConverter
     * @param \AttributeRepositoryInterface    $attributeRepository
     * @param \AssociationColumnsResolver      $associationColumnsResolver
     */
    public function __construct(
        \VersionRepositoryInterface $versionRepository,
        \FQCNResolver $FQCNResolver,
        NormalizerInterface $normalizer,
        \UserContext $userContext,
        \ProductRepositoryInterface $productRepository,
        \ProductModelRepositoryInterface $productModelRepository,
        \ObjectFilterInterface $objectFilter,
        \CollectionFilterInterface $productEditDataFilter,
        ValidatorInterface $validator,
        \AttributeConverterInterface $localizedConverter,
        \SaverInterface $productSaver,
        \SaverInterface $productModelSaver,
        \FilterInterface $emptyValuesFilter,
        \EntityWithValuesFilter $productModelEmptyValuesFilter,
        \ConverterInterface $productValueConverter,
        NormalizerInterface $constraintViolationNormalizer,
        \AttributeFilterInterface $productAttributeFilter,
        \AttributeFilterInterface $productModelAttributeFilter,
        \ObjectUpdaterInterface $productUpdater,
        \ObjectUpdaterInterface $productModelUpdater,
        \ArrayConverterInterface $productArrayConverter,
        \ArrayConverterInterface $productModelArrayConverter,
        \AttributeRepositoryInterface $attributeRepository,
        \AssociationColumnsResolver $associationColumnsResolver
    ) {
        $this->versionRepository = $versionRepository;
        $this->FQCNResolver = $FQCNResolver;
        $this->normalizer = $normalizer;
        $this->userContext = $userContext;
        $this->productRepository = $productRepository;
        $this->productModelRepository = $productModelRepository;
        $this->objectFilter = $objectFilter;
        $this->productEditDataFilter = $productEditDataFilter;
        $this->validator = $validator;
        $this->localizedConverter = $localizedConverter;
        $this->productSaver = $productSaver;
        $this->productModelSaver = $productModelSaver;
        $this->emptyValuesFilter = $emptyValuesFilter;
        $this->productModelEmptyValuesFilter = $productModelEmptyValuesFilter;
        $this->productValueConverter = $productValueConverter;
        $this->constraintViolationNormalizer = $constraintViolationNormalizer;
        $this->productAttributeFilter = $productAttributeFilter;
        $this->productModelAttributeFilter = $productModelAttributeFilter;
        $this->productUpdater = $productUpdater;
        $this->productModelUpdater = $productModelUpdater;
        $this->productArrayConverter = $productArrayConverter;
        $this->productModelArrayConverter = $productModelArrayConverter;
        $this->attributeRepository = $attributeRepository;
        $this->associationColumnsResolver = $associationColumnsResolver;
    }

    /**
     * @param Request $request
     * @param string  $id
     *
     * @throws NotFoundHttpException     If product is not found or the user cannot see it
     * @throws AccessDeniedHttpException If the user does not have right to edit the product
     *
     * @return Response
     */
    public function postAction(Request $request)
    {
        if (!$request->isXmlHttpRequest()) {
            return new RedirectResponse('/');
        }

        $requestData = json_decode($request->getContent(), true);
        $id = $requestData['resource_id'];
        $versionId = $requestData['id'];
        $version = $requestData['version'];
        $productType = $requestData['document_type'];
        $pending = $requestData['pending'];
        $productLogEntry = $this->versionRepository->findOneBy(
            [
                'resourceName' => $this->FQCNResolver->getFQCN($productType),
                'resourceId' => $id,
                'pending' => (int) $pending,
                'version' => $version
            ]
        );
       
        if ($productLogEntry) {
            $data = $this->formatProductData($productLogEntry, $productType);
            $product = $this->findProductOr404($id, $productType);
            if ($productType == 'product') {
                try {
                    $data = $this->productEditDataFilter->filterCollection($data, null, ['product' => $product]);
                } catch (\ObjectNotFoundException $e) {
                    throw new BadRequestHttpException($e->getMessage());
                }

                $this->updateProduct($product, $data);
            } else {
                $this->updateProductModel($product, $data);
            }

            $violations = $this->validator->validate($product);
            $violations->addAll($this->localizedConverter->getViolations());
    
            if (0 === $violations->count()) {
                if ($productType == 'product') {
                    $this->productSaver->save($product);
                } else {
                    $this->productModelSaver->save($product);
                }
                $normalizedProduct = $this->normalizer->normalize(
                    $product,
                    'internal_api',
                    $this->getNormalizationContext()
                );
    
                return new JsonResponse(['message' => 'Product values successfully restored'], 200);
            }

            $normalizedViolations = [];
            foreach ($violations as $violation) {
                $normalizedViolations[] = $this->constraintViolationNormalizer->normalize(
                    $violation,
                    'internal_api',
                    ['product' => $product]
                );
            }
            
            return new JsonResponse(['message' => 'Violation occured during product normalization'], 400);
        }

        return new JsonResponse(['message' => 'No version found'], 400);
    }

    /**
     * Find a product by its id or return a 404 response
     *
     * @param string $id the product id
     * @param string $productType the product type
     *
     * @throws NotFoundHttpException
     *
     * @return \ProductInterface|\ProductModelInterface
     */
    protected function findProductOr404($id, $productType)
    {
        if ($productType == 'product') {
            $product = $this->productRepository->find($id);
            if ($this->objectFilter->filterObject($product, 'pim.internal_api.product.edit')) {
                throw new AccessDeniedHttpException();
            }
        } else {
            $product = $this->productModelRepository->find($id);
            $product = $this->objectFilter->filterObject($product, 'pim.internal_api.product.view') ? null : $product;
        }

        if (null === $product) {
            throw new NotFoundHttpException(
                sprintf('Product with id %s could not be found.', $id)
            );
        }

        return $product;
    }

    /**
     * Format product version data into standard format
     *
     * @param \Version $productLogEntry
     * @param string   $productType
     *
     * @return array
     */
    protected function formatProductData(\Version $productLogEntry, string $productType)
    {
        $changeSet = $productLogEntry->getChangeset();
        $data = $productLogEntry->getSnapshot();
        if (!empty($changeSet)) {
            foreach ($changeSet as $changeSetKey => $changeSetValue) {
                $data[$changeSetKey] = $changeSetValue['old'];
            }
        }

        foreach ($data as $field => $value) {
            if (
                in_array($field, $this->associationColumnsResolver->resolveAssociationColumns())
                || in_array($field, $this->defaultFields)
            ) {
                continue;
            }
            $attributeField = explode('-', $field);
            $attribute = $this->findAttributeByCode($attributeField[0]);
            if (!$attribute) {
                unset($data[$field]);
                continue;
            }
        }

        if ($productType == 'product') {
            $data = $this->productArrayConverter->convert($data, []);
        } else {
            $data = $this->productModelArrayConverter->convert($data, []);
        }
        
        if ($data['parent'] == '') {
            $data['parent'] = null;
        }

        $data = $this->formatImageAndFileData($data);

        return $data;
    }

    /**
     * Format image and file attriute value
     *
     * @param array $item
     *
     * @return array
     */
    protected function formatImageAndFileData($item)
    {
        if (!empty($item['values'])) {
            foreach ($item['values'] as $attributeCode => $values) {
                $attribute = $this->findAttributeByCode($attributeCode);
                if ($attribute) {
                    if (in_array($attribute->getType(), ['pim_catalog_image', 'pim_catalog_file'])) {
                        foreach ($values as $valueKey => $value) {
                            $filePath = null;
                            $originalFilename = null;
                            if ($value['data']) {
                                $filePath = $value['data'];
                                $tempValue = explode('/', $value['data']);
                                $tempValue = end($tempValue);
                                $originalFilename = $tempValue;
                                $strPos = strpos($tempValue, '_');
                                if ($strPos >= 0) {
                                    $originalFilename = substr($tempValue, $strPos+1, strlen($tempValue));
                                }
                            }
                            $item['values'][$attributeCode][$valueKey]['data'] = [
                                'filePath' =>  $filePath,
                                'originalFilename' => $originalFilename
                            ];
                        }
                    } elseif ($attribute->getType() == 'pim_catalog_date') {
                        foreach ($values as $valueKey => $value) {
                            if ($value['data']) {
                                $date= new \DateTime($value['data']);
                                $item['values'][$attributeCode][$valueKey]['data'] = $date;
                            }
                        }
                    }
                }
            }
        }

        return $item;
    }

    /**
     * Find an attribute by its code
     *
     * @param string $code the attribute code
     *
     * @return \AttributeInterface
     */
    protected function findAttributeByCode($code)
    {
        return $this->attributeRepository->findOneByCode($code);
    }

    /**
     * Get the context used for product normalization
     *
     * @return array
     */
    protected function getNormalizationContext(): array
    {
        return $this->userContext->toArray() + ['filter_types' => []];
    }

    /**
     * Updates product with the provided request data
     *
     * @param \ProductInterface $product
     * @param array             $data
     */
    protected function updateProduct(\ProductInterface $product, array $data)
    {
        $values = $this->productValueConverter->convert($data['values']);

        $values = $this->localizedConverter->convertToDefaultFormats($values, [
            'locale' => $this->userContext->getUiLocale()->getCode()
        ]);

        $dataFiltered = $this->emptyValuesFilter->filter($product, ['values' => $values]);

        if (!empty($dataFiltered)) {
            $data = array_replace($data, $dataFiltered);
        } else {
            $data['values'] = [];
        }

        // don't filter during creation, because identifier is needed
        // but not sent by the frontend during creation (it sends the sku in the values)
        if (null !== $product->getId() && $product->isVariant()) {
            $data = $this->productAttributeFilter->filter($data);
        }

        $this->productUpdater->update($product, $data);
    }
    
    /**
     * Updates product with the provided request data
     *
     * @param \ProductModelInterface $productModel
     * @param array                  $data
     */
    private function updateProductModel(\ProductModelInterface $productModel, array $data): void
    {
        unset($data['parent']);
        $values = $this->productValueConverter->convert($data['values']);

        $values = $this->localizedConverter->convertToDefaultFormats($values, [
            'locale' => $this->userContext->getUiLocale()->getCode()
        ]);

        $dataFiltered = $this->productModelEmptyValuesFilter->filter($productModel, ['values' => $values]);

        if (!empty($dataFiltered)) {
            $data = array_replace($data, $dataFiltered);
        } else {
            $data['values'] = [];
        }

        if (!$productModel->isRoot()) {
            $data = $this->productModelAttributeFilter->filter($data);
        }

        $this->productModelUpdater->update($productModel, $data);
    }
}
