<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\PersonBundle\Export\Filter\SocialWorkFilters;

use Chill\MainBundle\Export\ExportDataNormalizerTrait;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\SocialWork\Goal;
use Chill\PersonBundle\Entity\SocialWork\Result;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Templating\Entity\SocialActionRender;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;

class SocialWorkTypeFilter implements FilterInterface
{
    use ExportDataNormalizerTrait;

    public function __construct(
        private readonly SocialActionRender $socialActionRender,
        private readonly TranslatableStringHelper $translatableStringHelper,
        private readonly EntityManagerInterface $em,
    ) {}

    public function addRole(): ?string
    {
        return null;
    }

    public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void
    {
        if (\count($data['actionType']) > 0) {
            $qb
                ->andWhere($qb->expr()->in('acpw.socialAction', ':actionType'))
                ->setParameter('actionType', $data['actionType']);
        }

        if (\count($data['goal']) > 0) {
            if (!\in_array('acpw_goal', $qb->getAllAliases(), true)) {
                $qb->join('acpw.goals', 'acpw_goal');
            }

            $orX = $qb->expr()->orX();
            foreach ($data['goal'] as $goal) {
                /** @var Goal $goal */
                $andX = $qb->expr()->andX();
                $andX->add($qb->expr()->eq('acpw_goal.goal', $goalId = ':goal_'.uniqid()));
                $qb->setParameter($goalId, $goal);

                if (\count($data['result']) > 0) {
                    $orXResult = $qb->expr()->orX();
                    foreach ($data['result'] as $result) {
                        /* @var Result $result */
                        $orXResult->add($qb->expr()->isMemberOf($resultId = ':result_'.uniqid(), 'acpw_goal.results'));
                        $qb->setParameter($resultId, $result);
                    }
                    $andX->add($orXResult);
                }
                $orX->add($andX);
            }
            $qb->andWhere($orX);
        }
    }

    public function applyOn(): string
    {
        return Declarations::SOCIAL_WORK_ACTION_TYPE;
    }

    public function buildForm(FormBuilderInterface $builder): void
    {
        $builder
            ->add('actionType', HiddenType::class)
            ->get('actionType')
            ->addModelTransformer(
                $this->iterableToIdTransformer(SocialAction::class)
            );
        $builder
            ->add('goal', HiddenType::class)
            ->get('goal')
            ->addModelTransformer(
                $this->iterableToIdTransformer(Goal::class)
            );
        $builder
            ->add('result', HiddenType::class)
            ->get('result')
            ->addModelTransformer(
                $this->iterableToIdTransformer(Result::class)
            );
    }

    public function getNormalizationVersion(): int
    {
        return 1;
    }

    public function normalizeFormData(array $formData): array
    {
        return [
            'actionType' => $this->normalizeDoctrineEntity($formData['actionType']),
            'goal'  => $this->normalizeDoctrineEntity($formData['goal']),
            'result' => $this->normalizeDoctrineEntity($formData['result']),
        ];
    }

    public function denormalizeFormData(array $formData, int $fromVersion): array
    {
        return [
            'actionType' => $this->denormalizeDoctrineEntity($this->denormalizeStringRepresentation($formData['actionType']), $this->em->getRepository(SocialAction::class)),
            'goal'  => $this->denormalizeDoctrineEntity($this->denormalizeStringRepresentation($formData['goal']), $this->em->getRepository(Goal::class)),
            'result' => $this->denormalizeDoctrineEntity($this->denormalizeStringRepresentation($formData['result']), $this->em->getRepository(Result::class)),
        ];
    }

    private function denormalizeStringRepresentation(array|string $ids): array
    {
        if (is_array($ids)) {
            return $ids;
        }

        return array_map(fn (string $id) => (int) $id, explode(',', $ids));
    }

    public function getFormDefaultData(): array
    {
        return ['action_type' => [], 'goal' => [], 'result' => []];
    }

    public function describeAction($data, ExportGenerationContext $context): array
    {
        $actionTypes = [];
        $goals = [];
        $results = [];

        foreach ($data['actionType'] as $at) {
            $actionTypes[] = $this->translatableStringHelper->localize(
                $at->getTitle()
            );
        }

        foreach ($data['goal'] as $g) {
            $goals[] = $this->translatableStringHelper->localize(
                $g->getTitle()
            );
        }

        foreach ($data['result'] as $r) {
            $results[] = $this->translatableStringHelper->localize(
                $r->getTitle()
            );
        }

        return ['Filtered actions by type, goals and results: %selected%', [
            '%selected%' => implode(', ', [...$actionTypes, ...$goals, ...$results]),
        ]];
    }

    public function getTitle(): string
    {
        return 'Filter by type of action, goals and results';
    }

    private function iterableToIdTransformer(string $entity): CallbackTransformer
    {
        return new CallbackTransformer(
            static function (?iterable $asIterable): string {
                if (null === $asIterable) {
                    return '';
                }
                $ids = [];

                foreach ($asIterable as $value) {
                    if (null === $value) {
                        continue;
                    }
                    $ids[] = $value->getId();
                }

                return implode(',', $ids);
            },
            function (?string $asString) use ($entity): array {
                if (null === $asString) {
                    return [];
                }

                return array_map(
                    fn (string $id) => $this->em
                        ->getRepository($entity)
                        ->findOneBy(['id' => (int) $id]),
                    explode(',', $asString)
                );
            }
        );
    }
}
