<?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\Utils\Rector\Rector;

use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\DirectExportInterface;
use Chill\MainBundle\Export\ExportDataNormalizerTrait;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\ListInterface;
use Chill\MainBundle\Form\Type\ChillDateTimeType;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Form\Type\PickUserLocationType;
use Chill\MainBundle\Form\Type\Select2CountryType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
use PhpParser\Node;
use PhpParser\Node\Expr\PropertyFetch;
use Rector\Rector\AbstractRector;
use Rector\Symfony\NodeAnalyzer\ClassAnalyzer;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

class ChillBundleAddNormalizationMethodsOnExportRector extends AbstractRector
{
    public function __construct(
        private ClassAnalyzer $classAnalyzer,
    ) {}

    public function getRuleDefinition(): RuleDefinition
    {
        return new RuleDefinition(
            'Add methods `normalizeFormData` and `denormalizeFormData` on Export, Filter, Aggregator, and Formatter',
            []
        );
    }

    public function getNodeTypes(): array
    {
        return [Node\Stmt\Class_::class];
    }

    public function refactor(Node $node)
    {
        if (!$node instanceof Node\Stmt\Class_) {
            return null;
        }

        if (
            !$this->classAnalyzer->hasImplements($node, FilterInterface::class)
            && !$this->classAnalyzer->hasImplements($node, AggregatorInterface::class)
            && !$this->classAnalyzer->hasImplements($node, ExportInterface::class)
            && !$this->classAnalyzer->hasImplements($node, DirectExportInterface::class)
            && !$this->classAnalyzer->hasImplements($node, ListInterface::class)
            && !$this->classAnalyzer->hasImplements($node, FormatterInterface::class)
        ) {
            return null;
        }

        $hasDenormalizeMethod = false;
        $hasNormalizedMethod = false;
        $hasGetVersionMethod = false;
        $buildFormStmtIndex = null;
        $buildForm = null;
        $hasTraitHelper = false;

        foreach ($node->stmts as $k => $stmt) {
            if ($stmt instanceof Node\Stmt\TraitUse) {
                $trait = $stmt->traits[0];

                if (str_contains($trait->toString(), ExportDataNormalizerTrait::class)) {
                    $hasTraitHelper = true;
                }

                continue;
            }
            if (!$stmt instanceof Node\Stmt\ClassMethod) {
                continue;
            }

            if ('buildForm' === $stmt->name->name) {
                $buildForm = $stmt;
                $buildFormStmtIndex = $k;
            } elseif ('normalizeFormData' === $stmt->name->name) {
                $hasNormalizedMethod = true;
            } elseif ('denormalizeFormData' === $stmt->name->name) {
                $hasDenormalizeMethod = true;
            } elseif ('getNormalizationVersion' === $stmt->name->name) {
                $hasGetVersionMethod = true;
            }
        }

        if ($hasDenormalizeMethod && $hasNormalizedMethod && $hasGetVersionMethod) {
            return null;
        }

        $toAddBefore = [];
        $toAdd = [];

        $stmtBefore = array_slice($node->stmts, 0, $buildFormStmtIndex, false);
        $stmtAfter = array_slice($node->stmts, $buildFormStmtIndex + 1);
        $propertiesOnForm = $this->getPropertiesOnForm($buildForm);

        // if the trait is not present, we add it into the statements
        if (
            !$hasTraitHelper
            && (!$hasNormalizedMethod || !$hasDenormalizeMethod)
            && array_reduce(
                $propertiesOnForm,
                function (bool $carry, string $item): bool {
                    if ('entity' === $item || 'date' === $item) {
                        return true;
                    }

                    return $carry;
                },
                false,
            )) {
            $toAddBefore[] = new Node\Stmt\TraitUse([
                new Node\Name('\\'.ExportDataNormalizerTrait::class),
            ]);
        }

        // if we do not have the `getNormalizerVersion` method
        if (!$hasGetVersionMethod) {
            $toAdd[] = $this->buildGetNormalizationVersionMethod();
        }

        // if we do not have the `normalizeFormData` method
        if (!$hasNormalizedMethod) {
            $toAdd[] = $this->buildNormalizeFormDataMethod($propertiesOnForm);
        }

        if (!$hasDenormalizeMethod) {
            $toAdd[] = $this->buildDenormalizeFormDataMethod($propertiesOnForm);
        }

        $node->stmts = [
            ...array_values($toAddBefore),
            ...array_values($stmtBefore),
            $buildForm,
            ...array_values($toAdd),
            ...array_values($stmtAfter),
        ];

        return $node;
    }

    private function buildDenormalizeFormDataMethod(array $propertiesOnForm): Node\Stmt\ClassMethod
    {
        $array = new Node\Expr\Array_([]);

        foreach ($propertiesOnForm as $name => $kind) {
            $arrayDimFetch = new Node\Expr\ArrayDimFetch(
                new Node\Expr\Variable('formData'),
                new Node\Scalar\String_($name),
            );

            $array->items[] = new Node\Expr\ArrayItem(
                match ($kind) {
                    'entity' => new Node\Expr\MethodCall(
                        new Node\Expr\Variable('this'),
                        new Node\Identifier('denormalizeDoctrineEntity'),
                        [
                            new Node\Arg($arrayDimFetch),
                            new Node\Arg(
                                new PropertyFetch(
                                    new Node\Expr\Variable('this'),
                                    'someRepository',
                                ),
                            ),
                        ],
                    ),
                    'rolling_date' => new Node\Expr\StaticCall(
                        new Node\Name\FullyQualified(RollingDate::class),
                        'fromNormalized',
                        [new Node\Arg($arrayDimFetch)],
                    ),
                    'date' => new Node\Expr\MethodCall(
                        new Node\Expr\Variable('this'),
                        new Node\Identifier('denormalizeDate'),
                        [
                            new Node\Arg($arrayDimFetch),
                        ],
                    ),
                    'scalar' => $arrayDimFetch,
                    default => $arrayDimFetch,
                },
                new Node\Scalar\String_($name),
            );
        }

        return new Node\Stmt\ClassMethod(
            'denormalizeFormData',
            [
                'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC,
                'returnType' => new Node\Identifier('array'),
                'params' => [
                    new Node\Param(new Node\Expr\Variable('formData'), type: new Node\Identifier('array')),
                    new Node\Param(new Node\Expr\Variable('fromVersion'), type: new Node\Identifier('int')),
                ],
                'stmts' => [
                    new Node\Stmt\Return_($array),
                ],
            ],
        );
    }

    private function buildNormalizeFormDataMethod(array $propertiesOnForm): Node\Stmt\ClassMethod
    {
        $array = new Node\Expr\Array_([]);

        foreach ($propertiesOnForm as $name => $kind) {
            $arrayDimFetch = new Node\Expr\ArrayDimFetch(
                new Node\Expr\Variable('formData'),
                new Node\Scalar\String_($name),
            );

            $array->items[] = new Node\Expr\ArrayItem(
                match ($kind) {
                    'entity' => new Node\Expr\MethodCall(
                        new Node\Expr\Variable('this'),
                        new Node\Identifier('normalizeDoctrineEntity'),
                        [
                            new Node\Arg($arrayDimFetch),
                        ],
                    ),
                    'rolling_date' => new Node\Expr\MethodCall(
                        $arrayDimFetch,
                        new Node\Identifier('normalize'),
                    ),
                    'date' => new Node\Expr\MethodCall(
                        new Node\Expr\Variable('this'),
                        new Node\Identifier('normalizeDate'),
                        [
                            new Node\Arg($arrayDimFetch),
                        ],
                    ),
                    'scalar' => $arrayDimFetch,
                    default => $arrayDimFetch,
                },
                new Node\Scalar\String_($name),
            );
        }

        return new Node\Stmt\ClassMethod(
            'normalizeFormData',
            [
                'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC,
                'returnType' => new Node\Identifier('array'),
                'params' => [
                    new Node\Param(new Node\Expr\Variable('formData'), type: new Node\Identifier('array')),
                ],
                'stmts' => [
                    new Node\Stmt\Return_($array),
                ],
            ],
        );
    }

    private function buildGetNormalizationVersionMethod(): Node\Stmt\ClassMethod
    {
        return new Node\Stmt\ClassMethod(
            'getNormalizationVersion',
            [
                'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC,
                'returnType' => new Node\Identifier('int'),
                'stmts' => [
                    new Node\Stmt\Return_(
                        new Node\Scalar\LNumber(1)
                    ),
                ],
            ],
        );
    }

    /**
     * @return array<string, 'entity'|'scalar'|'rolling_date'|'date'>
     */
    private function getPropertiesOnForm(Node\Stmt\ClassMethod $buildFormMethod): array
    {
        $builderName = $buildFormMethod->params[0]->var->name;
        $values = [];

        foreach ($buildFormMethod->stmts as $stmt) {
            if ($stmt instanceof Node\Stmt\Expression
                // it must be a method call
                && $stmt->expr instanceof Node\Expr\MethodCall
                && 'add' === $stmt->expr->name->toString()
            ) {
                $newValues = $this->handleMethodCallAdd($stmt->expr, $builderName);

                if (false === $newValues) {
                    continue;
                }

                $values = [...$values, ...$newValues];
            }
        }

        return $values;
    }

    private function handleMethodCallAdd(Node\Expr\MethodCall $call, string $builderName): array|false
    {
        if ($call->var instanceof Node\Expr\Variable) {
            // in this case, the call is done on the form builder: $formBuilder->add
            // or the last method call on the form builder, for instance $formBuilder->add( )->add( )
            if ($builderName !== $call->var->name) {
                return false;
            }
        } elseif ($call->var instanceof Node\Expr\MethodCall && 'add' === $call->var->name->toString()) {
            // in this case, we have a chained method call: something like $formbuilder->add()->add().
            // we have to go deeper into the call to get the information from them
            $previous = $this->handleMethodCallAdd($call->var, $builderName);

            if (false === $previous) {
                return false;
            }
        }

        $arg0 = $call->args[0] ?? null;

        if (null === $arg0) {
            throw new \UnexpectedValueException("The first argument of an 'add' call method is empty");
        }
        if (!$arg0->value instanceof Node\Scalar\String_) {
            throw new \UnexpectedValueException("The first argument of an 'add' call is not a string");
        }

        $key = $arg0->value->value;
        /** @var Node\Expr\ClassConstFetch $argType */
        $argType = $call->args[1]->value;

        return [
            ...$previous ?? [],
            $key => match($argType->class->toString()) {
                EntityType::class, PickUserDynamicType::class, PickUserLocationType::class, PickThirdpartyDynamicType::class, Select2CountryType::class => 'entity',
                PickRollingDateType::class => 'rolling_date',
                ChillDateType::class, ChillDateTimeType::class, DateTimeType::class, DateType::class => 'date',
                default => 'scalar',
            },
        ];
    }
}
