<?php declare(strict_types=1); 
 
namespace Shopware\Core\System\Language; 
 
use Doctrine\DBAL\Connection; 
use Doctrine\DBAL\FetchMode; 
use Shopware\Core\Defaults; 
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\CascadeDeleteCommand; 
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand; 
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand; 
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand; 
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommand; 
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PostWriteValidationEvent; 
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent; 
use Shopware\Core\Framework\Uuid\Uuid; 
use Shopware\Core\Framework\Validation\WriteConstraintViolationException; 
use Symfony\Component\EventDispatcher\EventSubscriberInterface; 
use Symfony\Component\Validator\ConstraintViolation; 
use Symfony\Component\Validator\ConstraintViolationInterface; 
use Symfony\Component\Validator\ConstraintViolationList; 
 
class LanguageValidator implements EventSubscriberInterface 
{ 
    public const VIOLATION_PARENT_HAS_PARENT = 'parent_has_parent_violation'; 
 
    public const VIOLATION_CODE_REQUIRED_FOR_ROOT_LANGUAGE = 'code_required_for_root_language'; 
 
    public const VIOLATION_DELETE_DEFAULT_LANGUAGE = 'delete_default_language_violation'; 
 
    public const VIOLATION_DEFAULT_LANGUAGE_PARENT = 'default_language_parent_violation'; 
 
    public const DEFAULT_LANGUAGES = [Defaults::LANGUAGE_SYSTEM]; 
 
    /** 
     * @var Connection 
     */ 
    private $connection; 
 
    /** 
     * @internal 
     */ 
    public function __construct(Connection $connection) 
    { 
        $this->connection = $connection; 
    } 
 
    public static function getSubscribedEvents(): array 
    { 
        return [ 
            PreWriteValidationEvent::class => 'preValidate', 
            PostWriteValidationEvent::class => 'postValidate', 
        ]; 
    } 
 
    public function postValidate(PostWriteValidationEvent $event): void 
    { 
        $commands = $event->getCommands(); 
        $affectedIds = $this->getAffectedIds($commands); 
        if (\count($affectedIds) === 0) { 
            return; 
        } 
 
        $violations = new ConstraintViolationList(); 
        $violations->addAll($this->getInheritanceViolations($affectedIds)); 
        $violations->addAll($this->getMissingTranslationCodeViolations($affectedIds)); 
 
        if ($violations->count() > 0) { 
            $event->getExceptions()->add(new WriteConstraintViolationException($violations)); 
        } 
    } 
 
    public function preValidate(PreWriteValidationEvent $event): void 
    { 
        $commands = $event->getCommands(); 
 
        foreach ($commands as $command) { 
            $violations = new ConstraintViolationList(); 
 
            if ($command instanceof CascadeDeleteCommand || $command->getDefinition()->getClass() !== LanguageDefinition::class) { 
                continue; 
            } 
 
            $pk = $command->getPrimaryKey(); 
            $id = mb_strtolower(Uuid::fromBytesToHex($pk['id'])); 
 
            if ($command instanceof DeleteCommand && $id === Defaults::LANGUAGE_SYSTEM) { 
                $violations->add( 
                    $this->buildViolation( 
                        'The default language {{ id }} cannot be deleted.', 
                        ['{{ id }}' => $id], 
                        null, 
                        '/' . $id, 
                        $id, 
                        self::VIOLATION_DELETE_DEFAULT_LANGUAGE 
                    ) 
                ); 
            } 
 
            if ($command instanceof UpdateCommand && $id === Defaults::LANGUAGE_SYSTEM) { 
                $payload = $command->getPayload(); 
                if (\array_key_exists('parent_id', $payload) && $payload['parent_id'] !== null) { 
                    $violations->add( 
                        $this->buildViolation( 
                            'The default language {{ id }} cannot inherit from another language.', 
                            ['{{ id }}' => $id], 
                            null, 
                            '/parentId', 
                            $payload['parent_id'], 
                            self::VIOLATION_DEFAULT_LANGUAGE_PARENT 
                        ) 
                    ); 
                } 
            } 
 
            if ($violations->count() > 0) { 
                $event->getExceptions()->add(new WriteConstraintViolationException($violations, $command->getPath())); 
            } 
        } 
    } 
 
    /** 
     * @param array<string> $affectedIds 
     */ 
    private function getInheritanceViolations(array $affectedIds): ConstraintViolationList 
    { 
        $statement = $this->connection->executeQuery( 
            'SELECT child.id 
             FROM language child 
             INNER JOIN language parent ON parent.id = child.parent_id 
             WHERE (child.id IN (:ids) OR child.parent_id IN (:ids)) 
             AND parent.parent_id IS NOT NULL', 
            ['ids' => $affectedIds], 
            ['ids' => Connection::PARAM_STR_ARRAY] 
        ); 
        $ids = $statement->fetchAll(FetchMode::COLUMN); 
 
        $violations = new ConstraintViolationList(); 
        foreach ($ids as $binId) { 
            $id = Uuid::fromBytesToHex($binId); 
            $violations->add( 
                $this->buildViolation( 
                    'Language inheritance limit for the child {{ id }} exceeded. A Language must not be nested deeper than one level.', 
                    ['{{ id }}' => $id], 
                    null, 
                    '/' . $id . '/parentId', 
                    $id, 
                    self::VIOLATION_PARENT_HAS_PARENT 
                ) 
            ); 
        } 
 
        return $violations; 
    } 
 
    /** 
     * @param array<string> $affectedIds 
     */ 
    private function getMissingTranslationCodeViolations(array $affectedIds): ConstraintViolationList 
    { 
        $statement = $this->connection->executeQuery( 
            'SELECT lang.id 
             FROM language lang 
             LEFT JOIN locale l ON lang.translation_code_id = l.id 
             WHERE l.id IS NULL # no translation code 
             AND lang.parent_id IS NULL # root 
             AND lang.id IN (:ids)', 
            ['ids' => $affectedIds], 
            ['ids' => Connection::PARAM_STR_ARRAY] 
        ); 
        $ids = $statement->fetchAll(FetchMode::COLUMN); 
 
        $violations = new ConstraintViolationList(); 
        foreach ($ids as $binId) { 
            $id = Uuid::fromBytesToHex($binId); 
            $violations->add( 
                $this->buildViolation( 
                    'Root language {{ id }} requires a translation code', 
                    ['{{ id }}' => $id], 
                    null, 
                    '/' . $id . '/translationCodeId', 
                    $id, 
                    self::VIOLATION_CODE_REQUIRED_FOR_ROOT_LANGUAGE 
                ) 
            ); 
        } 
 
        return $violations; 
    } 
 
    /** 
     * @param WriteCommand[] $commands 
     * 
     * @return array<string> 
     */ 
    private function getAffectedIds(array $commands): array 
    { 
        $ids = []; 
        foreach ($commands as $command) { 
            if ($command->getDefinition()->getClass() !== LanguageDefinition::class) { 
                continue; 
            } 
            if ($command instanceof InsertCommand || $command instanceof UpdateCommand) { 
                $ids[] = $command->getPrimaryKey()['id']; 
            } 
        } 
 
        return $ids; 
    } 
 
    private function buildViolation( 
        string $messageTemplate, 
        array $parameters, 
        $root = null, 
        ?string $propertyPath = null, 
        ?string $invalidValue = null, 
        ?string $code = null 
    ): ConstraintViolationInterface { 
        return new ConstraintViolation( 
            str_replace(array_keys($parameters), array_values($parameters), $messageTemplate), 
            $messageTemplate, 
            $parameters, 
            $root, 
            $propertyPath, 
            $invalidValue, 
            null, 
            $code 
        ); 
    } 
}