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

use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
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\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;

final readonly class AccompanyingCourseDocumentGenericDocProvider implements GenericDocForAccompanyingPeriodProviderInterface, GenericDocForPersonProviderInterface
{
    public const KEY = 'accompanying_course_document';

    public function __construct(
        private Security $security,
        private EntityManagerInterface $entityManager,
        private AccompanyingCourseDocumentRepository $accompanyingCourseDocumentRepository,
    ) {}

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

    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 === $accompanyingCourseDocument = $this->accompanyingCourseDocumentRepository->find($identifiers['id'])) {
            return null;
        }

        return new GenericDocDTO(
            self::KEY,
            $identifiers,
            \DateTimeImmutable::createFromInterface($accompanyingCourseDocument->getDate()),
            $accompanyingCourseDocument->getCourse(),
        );
    }

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

        $query = new FetchQuery(
            self::KEY,
            sprintf('jsonb_build_object(\'id\', acc_course_document.%s)', $classMetadata->getIdentifierColumnNames()[0]),
            $classMetadata->getColumnName('date'),
            $classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS acc_course_document'
        );

        $query->addWhereClause(
            sprintf('%s = ?', $classMetadata->getSingleAssociationJoinColumnName('course')),
            [$accompanyingPeriod->getId()],
            [Types::INTEGER]
        );

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

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

    public function buildFetchQueryForPerson(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
    {
        $classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class);

        $query = new FetchQuery(
            self::KEY,
            sprintf('jsonb_build_object(\'id\', acc_course_document.%s)', $classMetadata->getIdentifierColumnNames()[0]),
            $classMetadata->getColumnName('date'),
            $classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS acc_course_document'
        );

        $atLeastOne = false;
        $or = [];
        $orParams = [];
        $orTypes = [];

        foreach ($person->getAccompanyingPeriodParticipations() as $participation) {
            if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $participation->getAccompanyingPeriod())) {
                continue;
            }

            $atLeastOne = true;

            $or[] = sprintf(
                "(acc_course_document.%s = ? AND acc_course_document.%s BETWEEN ? AND COALESCE(?, 'infinity'::date))",
                $classMetadata->getSingleAssociationJoinColumnName('course'),
                $classMetadata->getColumnName('date')
            );
            $orParams = [...$orParams, $participation->getAccompanyingPeriod()->getId(), $participation->getStartDate(), $participation->getEndDate()];
            $orTypes = [...$orTypes, Types::INTEGER, Types::DATE_MUTABLE, Types::DATE_MUTABLE];
        }

        if (!$atLeastOne) {
            // there aren't any period allowed to be seen. Add an unreachable condition
            $query->addWhereClause('TRUE = FALSE');

            return $query;
        }

        $query->addWhereClause('('.implode(' OR ', $or).')', $orParams, $orTypes);

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

    public function isAllowedForPerson(Person $person): bool
    {
        return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $person);
    }

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

        if (null !== $startDate) {
            $query->addWhereClause(
                sprintf('? <= %s', $classMetadata->getColumnName('date')),
                [$startDate],
                [Types::DATE_IMMUTABLE]
            );
        }

        if (null !== $endDate) {
            $query->addWhereClause(
                sprintf('? >= %s', $classMetadata->getColumnName('date')),
                [$endDate],
                [Types::DATE_IMMUTABLE]
            );
        }

        if (null !== $content and '' !== $content) {
            // add join clause to stored_object table
            $query->addJoinClause(
                sprintf(
                    'JOIN %s AS doc_store ON doc_store.%s = acc_course_document.%s',
                    $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName(),
                    $storedObjectMetadata->getSingleIdentifierColumnName(),
                    $classMetadata->getSingleAssociationJoinColumnName('object')
                )
            );

            $query->addWhereClause(
                sprintf(
                    '(doc_store.%s ilike ? OR acc_course_document.%s ilike ?)',
                    $classMetadata->getColumnName('title'),
                    $classMetadata->getColumnName('description')
                ),
                ['%'.$content.'%', '%'.$content.'%'],
                [Types::STRING, Types::STRING]
            );
        }

        return $query;
    }
}
