<?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\GenericDoc\Providers;

use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;

final readonly class AccompanyingPeriodWorkEvaluationGenericDocProvider implements GenericDocForAccompanyingPeriodProviderInterface, GenericDocForPersonProviderInterface
{
    public const KEY = 'accompanying_period_work_evaluation_document';

    public function __construct(
        private Security $security,
        private EntityManagerInterface $entityManager,
        private AccompanyingPeriodWorkEvaluationDocumentRepository $accompanyingPeriodWorkEvaluationDocumentRepository,
    ) {}

    public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject
    {
        return $this->accompanyingPeriodWorkEvaluationDocumentRepository->find($genericDocDTO->identifiers['id'])?->getStoredObject();
    }

    public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool
    {
        return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers);
    }

    public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool
    {
        return self::KEY === $key;
    }

    public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO
    {
        if (null === $document = $this->accompanyingPeriodWorkEvaluationDocumentRepository->find($identifiers['id'])) {
            return null;
        }

        return new GenericDocDTO(
            $key,
            $identifiers,
            $document->getAccompanyingPeriodWorkEvaluation()->getCreatedAt(),
            $document->getAccompanyingPeriodWorkEvaluation()->getAccompanyingPeriodWork()->getAccompanyingPeriod()
        );
    }

    public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
    {
        $accompanyingPeriodWorkMetadata = $this->entityManager->getClassMetadata(AccompanyingPeriod\AccompanyingPeriodWork::class);
        $query = $this->buildBaseQuery();

        $query->addWhereClause(
            sprintf('action.%s = ?', $accompanyingPeriodWorkMetadata->getAssociationMapping('accompanyingPeriod')['joinColumns'][0]['name']),
            [$accompanyingPeriod->getId()],
            [Types::INTEGER]
        );

        return $this->addWhereClausesToQuery($query, $startDate, $endDate, $content);
    }

    public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool
    {
        return $this->security->isGranted(AccompanyingPeriodWorkVoter::SEE, $accompanyingPeriod);
    }

    private function addWhereClausesToQuery(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
    {
        $storedObjectMetadata = $this->entityManager->getClassMetadata(StoredObject::class);

        if (null !== $startDate) {
            $query->addWhereClause(
                sprintf('doc_store.%s >= ?', $storedObjectMetadata->getColumnName('createdAt')),
                [$startDate],
                [Types::DATE_IMMUTABLE]
            );
        }

        if (null !== $endDate) {
            $query->addWhereClause(
                sprintf('doc_store.%s < ?', $storedObjectMetadata->getColumnName('createdAt')),
                [$endDate],
                [Types::DATE_IMMUTABLE]
            );
        }

        if (null !== $content) {
            $query->addWhereClause(
                sprintf('doc_store.%s ilike ?', $storedObjectMetadata->getColumnName('title')),
                ['%'.$content.'%'],
                [Types::STRING]
            );
        }

        return $query;
    }

    public function buildFetchQueryForPerson(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
    {
        $storedObjectMetadata = $this->entityManager->getClassMetadata(StoredObject::class);
        $accompanyingPeriodWorkMetadata = $this->entityManager->getClassMetadata(AccompanyingPeriod\AccompanyingPeriodWork::class);
        $query = $this->buildBaseQuery();

        // we loop over each accompanying period participation, to check of the user is allowed to see them
        $or = [];
        $orParams = [];
        $orTypes = [];
        foreach ($person->getAccompanyingPeriodParticipations() as $participation) {
            if (!$this->security->isGranted(AccompanyingPeriodWorkVoter::SEE, $participation->getAccompanyingPeriod())) {
                continue;
            }

            $or[] = sprintf(
                '(action.%s = ? AND apwed.%s BETWEEN ?::date AND COALESCE(?::date, \'infinity\'::date))',
                $accompanyingPeriodWorkMetadata->getSingleAssociationJoinColumnName('accompanyingPeriod'),
                $storedObjectMetadata->getColumnName('createdAt')
            );
            $orParams = [...$orParams, $participation->getAccompanyingPeriod()->getId(),
                \DateTimeImmutable::createFromInterface($participation->getStartDate()),
                null === $participation->getEndDate() ? null : \DateTimeImmutable::createFromInterface($participation->getEndDate())];
            $orTypes = [...$orTypes, Types::INTEGER, Types::DATE_IMMUTABLE, Types::DATE_IMMUTABLE];
        }

        if ([] === $or) {
            $query->addWhereClause('TRUE = FALSE');

            return $query;
        }

        $query->addWhereClause(sprintf('(%s)', implode(' OR ', $or)), $orParams, $orTypes);

        return $this->addWhereClausesToQuery($query, $startDate, $endDate, $content);
    }

    public function isAllowedForPerson(Person $person): bool
    {
        // this will be filtered during query
        return true;
    }

    private function buildBaseQuery(): FetchQuery
    {
        $classMetadata = $this->entityManager->getClassMetadata(AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument::class);
        $storedObjectMetadata = $this->entityManager->getClassMetadata(StoredObject::class);
        $evaluationMetadata = $this->entityManager->getClassMetadata(AccompanyingPeriod\AccompanyingPeriodWorkEvaluation::class);
        $accompanyingPeriodWorkMetadata = $this->entityManager->getClassMetadata(AccompanyingPeriod\AccompanyingPeriodWork::class);

        $query = new FetchQuery(
            self::KEY,
            sprintf("jsonb_build_object('id', apwed.%s)", $classMetadata->getColumnName('id')),
            sprintf('apwed.'.$storedObjectMetadata->getColumnName('createdAt')),
            $classMetadata->getTableName().' AS apwed'
        );
        $query->addJoinClause(sprintf(
            'JOIN %s doc_store ON doc_store.%s = apwed.%s',
            $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName(),
            $storedObjectMetadata->getColumnName('id'),
            $classMetadata->getSingleAssociationJoinColumnName('storedObject')
        ));
        $query->addJoinClause(sprintf(
            'JOIN %s evaluation ON apwed.%s = evaluation.%s',
            $evaluationMetadata->getTableName(),
            $classMetadata->getAssociationMapping('accompanyingPeriodWorkEvaluation')['joinColumns'][0]['name'],
            $evaluationMetadata->getColumnName('id')
        ));
        $query->addJoinClause(sprintf(
            'JOIN %s action ON evaluation.%s = action.%s',
            $accompanyingPeriodWorkMetadata->getTableName(),
            $evaluationMetadata->getAssociationMapping('accompanyingPeriodWork')['joinColumns'][0]['name'],
            $accompanyingPeriodWorkMetadata->getColumnName('id')
        ));

        return $query;
    }
}
