<?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\Service\Import;

use Chill\PersonBundle\Entity\SocialWork\Evaluation;
use Chill\PersonBundle\Entity\SocialWork\Goal;
use Chill\PersonBundle\Entity\SocialWork\Result;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Repository\SocialWork\EvaluationRepository;
use Chill\PersonBundle\Repository\SocialWork\GoalRepository;
use Chill\PersonBundle\Repository\SocialWork\ResultRepository;
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\Expr\Comparison;
use Doctrine\Persistence\ObjectRepository;

final readonly class SocialWorkMetadata implements SocialWorkMetadataInterface
{
    public function __construct(private SocialIssueRepository $socialIssueRepository, private SocialActionRepository $socialActionRepository, private GoalRepository $goalRepository, private ResultRepository $resultRepository, private EvaluationRepository $evaluationRepository, private EntityManagerInterface $entityManager) {}

    /**
     * @throws \Exception
     */
    public function import(iterable $dataset): bool
    {
        // Initialisation of the previous result row with the proper data structure.
        $result = [
            'socialIssues' => [
                'socialIssue' => null,
                'socialIssueChild' => null,
            ],
            'socialActions' => [
                'socialAction' => null,
                'socialActionChild' => null,
            ],
            'goal' => null,
            'result' => null,
            'eval' => null,
        ];

        foreach ($dataset as $key => $row) {
            $result = $this
                ->import1(
                    $key,
                    // Columns cleanup before importing data.
                    array_map(
                        static fn (string $column): ?string => '' === $column ? null : $column,
                        array_map('trim', $row)
                    ),
                    $result
                );
        }

        return true;
    }

    /**
     * @return array<int, object>
     */
    private function findByJson(ObjectRepository $repository, string $field, array $jsonCriterias): array
    {
        $qb = $this
            ->entityManager
            ->createQueryBuilder()
            ->select('s')
            ->from($repository->getClassName(), 's');

        $expr = $qb->expr();

        $temporaryJsonCriterias = [];

        foreach ($jsonCriterias as $key => $value) {
            $temporaryJsonCriterias[] = [$field, $key, $value, sprintf(':placeholder_%s_%s', $field, $key)];
        }

        $jsonParameters = array_reduce(
            $temporaryJsonCriterias,
            static function (array $carry, array $row): array {
                [,, $value, $placeholder] = $row;

                return array_merge(
                    $carry,
                    [
                        $placeholder => sprintf('"%s"', $value),
                    ]
                );
            },
            []
        );

        $jsonPredicates = array_map(
            static function (array $row) use ($expr): Comparison {
                [$field, $key,, $placeholder] = $row;

                $left = sprintf(
                    "GET_JSON_FIELD_BY_KEY(s.%s, '%s')",
                    $field,
                    $key
                );

                return $expr
                    ->eq(
                        $left,
                        $placeholder
                    );
            },
            $temporaryJsonCriterias
        );

        return $qb
            ->select('s')
            ->where(...$jsonPredicates)
            ->setParameters($jsonParameters)
            ->getQuery()
            ->getResult();
    }

    /**
     * @return object
     *
     * @throws \Exception
     */
    private function getOrCreateEntity(ObjectRepository $repository, string $field, array $jsonCriterias = [])
    {
        $results = $this
            ->findByJson(
                $repository,
                $field,
                $jsonCriterias
            );

        if ([] === $results) {
            $entity = $repository->getClassName();

            return new $entity();
        }

        if (1 === \count($results)) {
            return reset($results);
        }

        throw new \Exception('Unable to create entity.');
    }

    private function handleEvaluation(?string $evaluationTitle, SocialAction $socialAction): ?Evaluation
    {
        if (null === $evaluationTitle) {
            return null;
        }

        /** @var Evaluation $eval */
        $eval = $this->getOrCreateEntity($this->evaluationRepository, 'title', ['fr' => $evaluationTitle]);
        $eval->setTitle(['fr' => $evaluationTitle]);
        $socialAction->addEvaluation($eval);

        $this->entityManager->persist($eval);

        return $eval;
    }

    private function handleGoal(?string $goalTitle, SocialAction $socialAction): ?Goal
    {
        if (null === $goalTitle) {
            return null;
        }

        /** @var Goal $goal */
        $goal = $this->getOrCreateEntity($this->goalRepository, 'title', ['fr' => $goalTitle]);
        $goal->setTitle(['fr' => $goalTitle]);

        $socialAction->addGoal($goal);
        $goal->addSocialAction($socialAction);

        $this->entityManager->persist($socialAction);
        $this->entityManager->persist($goal);

        return $goal;
    }

    private function handleResult(?string $resultTitle, SocialAction $socialAction, ?Goal $goal): ?Result
    {
        if (null === $resultTitle) {
            return null;
        }

        /** @var Result $result */
        $result = $this->getOrCreateEntity($this->resultRepository, 'title', ['fr' => $resultTitle]);
        $result->setTitle(['fr' => $resultTitle]);

        if (null !== $goal) {
            $result->addGoal($goal);
            $goal->addResult($result);

            $this->entityManager->persist($goal);
        } else {
            $result->addSocialAction($socialAction);
            $socialAction->addResult($result);
        }

        $this->entityManager->persist($result);
        $this->entityManager->persist($socialAction);

        return $result;
    }

    /**
     * @return array<string, SocialAction|null>
     *
     * @throws \Exception
     */
    private function handleSocialAction(
        ?string $socialActionTitle,
        ?string $socialActionChildTitle,
        SocialIssue $socialIssue,
        float $orderingParent,
        float $orderingChild,
        ?SocialAction $previousSocialAction,
        ?SocialAction $previousSocialActionChild,
    ): array {
        if (null === $socialActionTitle) {
            return [
                'socialAction' => null,
                'socialActionChild' => null,
            ];
        }

        $return = [];

        if (null !== $previousSocialAction && ($previousSocialAction->getTitle()['fr'] === $socialActionTitle)) {
            $return = [
                'socialAction' => $parent = $previousSocialAction,
            ];
            $parentIsSame = true;
        } else {
            $return['socialAction'] = $parent = (new SocialAction())->setTitle(['fr' => $socialActionTitle])
                ->setOrdering($orderingParent);
            $parent->setIssue($socialIssue);
            $this->entityManager->persist($parent);
            $parentIsSame = false;
        }

        if (null === $socialActionChildTitle) {
            $return['socialActionChild'] = null;
        } elseif ($parentIsSame && null !== $previousSocialActionChild && $previousSocialActionChild->getTitle()['fr'] === $socialActionChildTitle) {
            $return['socialActionChild'] = $previousSocialActionChild;
        } else {
            $return['socialActionChild'] = $child = (new SocialAction())->setTitle(['fr' => $socialActionChildTitle]);
            $child->setParent($parent);
            $child->setIssue($socialIssue)->setOrdering($orderingChild);
            $this->entityManager->persist($child);
        }

        return $return;
    }

    /**
     * @return array<string, SocialIssue|null>
     *
     * @throws \Exception
     */
    private function handleSocialIssue(
        ?string $socialIssueTitle,
        ?string $socialIssueChildTitle,
        float $orderingParent,
        float $orderingChild,
        ?SocialIssue $previousSocialIssue,
        ?SocialIssue $previousSocialIssueChild,
    ): array {
        $return = [];

        if (null !== $previousSocialIssue && ($previousSocialIssue->getTitle()['fr'] === $socialIssueTitle)) {
            $return = [
                'socialIssue' => $parent = $previousSocialIssue,
            ];
            $parentIsSame = true;
        } elseif (null !== $socialIssueTitle) {
            $return['socialIssue'] = $parent = (new SocialIssue())->setTitle(['fr' => $socialIssueTitle])
                ->setOrdering($orderingParent);
            $this->entityManager->persist($parent);
            $parentIsSame = false;
        } else {
            return [
                'socialIssue' => null,
                'socialIssueChild' => null,
            ];
        }

        if ($parentIsSame && null !== $previousSocialIssueChild && ($previousSocialIssueChild->getTitle()['fr'] === $socialIssueChildTitle)) {
            $return['socialIssueChild'] = $previousSocialIssueChild;
        } elseif (null !== $socialIssueChildTitle) {
            $return['socialIssueChild'] = $child = (new SocialIssue())->setTitle(['fr' => $socialIssueChildTitle])
                ->setOrdering($orderingChild);
            $child->setParent($parent);
            $this->entityManager->persist($child);
        } else {
            $return['socialIssueChild'] = null;
        }

        return $return;
    }

    /**
     * Row Structure:.
     *
     * Index 0: Parent SocialIssue
     * Index 1: Child SocialIssue
     * Index 2: Parent SocialAction
     * Index 3: Child SocialAction
     * Index 4: Goal
     * Index 5: Result
     * Index 6: Evaluation
     *
     * @param array<int, ?string>                                                                                                                      $row
     * @param array{socialIssues: array<SocialIssue>, socialActions: array<SocialAction>, goal: Goal|null, result: Result|null, eval: Evaluation|null} $previousRow
     *
     * @return array{socialIssues: array<SocialIssue>, socialActions: array<SocialAction>, goal: Goal|null, result: Result|null, eval: Evaluation|null}
     *
     * @throws \Exception
     */
    private function import1(int $key, array $row, array $previousRow): array
    {
        $baseOrdering = $key * 10.0;

        $socialIssues = $this
            ->handleSocialIssue(
                $row[0],
                $row[1],
                $key + 1.0,
                $key + 3.0,
                $previousRow['socialIssues']['socialIssue'] ?? null,
                $previousRow['socialIssues']['socialIssueChild'] ?? null
            );

        $socialIssue = $socialIssues['socialIssueChild'] ?? $socialIssues['socialIssue'];

        if (null === $socialIssue) {
            throw new \Exception(sprintf("no social issue on row {$key}, values: %s", implode(', ', $row)));
        }

        $socialActions = $this
            ->handleSocialAction(
                $row[2],
                $row[3],
                $socialIssue,
                $key + 5.0,
                $key + 7.0,
                $previousRow['socialActions']['socialAction'] ?? null,
                $previousRow['socialActions']['socialActionChild'] ?? null
            );

        $socialAction = $socialActions['socialActionChild'] ?? $socialActions['socialAction'];

        if (null !== $socialAction) {
            $goal = $this->handleGoal($row[4], $socialAction);
            $result = $this->handleResult($row[5], $socialAction, $goal);
            $eval = $this->handleEvaluation($row[6], $socialAction);
        }

        $this->entityManager->flush();

        return [
            'socialIssues' => $socialIssues,
            'socialActions' => $socialActions,
            'goal' => $goal ?? null,
            'result' => $result ?? null,
            'eval' => $eval ?? null,
        ];
    }
}
