<?php

namespace App\Filter\Doctrine\TimeConstraint;

use App\Entity\Access\Access;
use App\Service\Constraint\AbstractTimeConstraintUtils;
use App\Service\Constraint\ActivityYearConstraint;
use App\Service\Constraint\DateTimeConstraint;
use App\Service\Constraint\TimeConstraintInterface;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RequestStack;
use Doctrine\ORM\Mapping\ClassMetadata;
use Symfony\Contracts\Service\Attribute\Required;

/**
 * Applique les contraintes temporelles aux entités possédant l'annotation requise
 */
abstract class AbstractTimeFilter extends SQLFilter
{
    protected ?int $accessId = null;
    protected ?TimeConstraintInterface $timeConstraint = null;

    /**
     * Annotation expected, have to be re-defined in subclasses
     * @var string|null
     */
    protected static ?string $constraintAnnotation = null;

    /**
     * Parameter of the annotation that design the field containing the starting date of the period
     * Have to be re-defined in subclasses
     * @var string|null
     */
    protected static ?string $annotationStartField = null;

    /**
     * Parameter of the annotation that design the field containing the ending date of the period
     * Have to be re-defined in subclasses
     * @var string|null
     */
    protected static ?string $annotationEndField = null;

    public function setAccessId(int $accessId): void {
        $this->accessId = $accessId;
    }

    public function setTimeConstraint(TimeConstraintInterface $timeConstraint): void {
        $this->timeConstraint = $timeConstraint;
    }

    /**
     * Retourne les noms des champs contenant les dates de début et de fin de la période de la ressource testée.
     *
     * @param array<string, string> $arguments
     * @return array<string>
     */
    protected function getStartAndEndFields(array $arguments): array {
        if (static::$annotationStartField === null || static::$annotationEndField === null) {
            throw new \RuntimeException('Constraint annotation has not been properly configured');
        }

        $startField = $arguments[static::$annotationStartField] ?? null;
        $endField = $arguments[static::$annotationEndField] ?? null;

        return [$startField, $endField];
    }

    /**
     * Invoke the TimeConstraintUtils to retrieve the active constraints for the given user
     *
     * @param int $accessId
     * @return array<string, array<string, list<int>>>
     * @throws \Exception
     */
    protected function getConstraints(int $accessId): array
    {
        return $this->timeConstraint->invoke($accessId);
    }

    /**
     * Méthode surchargée de SQLFilter permettant d'appliquer un filtre supplémentaire aux requêtes SQL
     *
     * @param ClassMetadata $targetEntity
     * @param string $targetTableAlias
     * @return string
     */
    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
    {
        if (static::$constraintAnnotation === null) {
            throw new \RuntimeException('Constraint annotation has not been set');
        }
        if ($this->accessId === null) {
            throw new \RuntimeException('AccessId has not been set');
        }

        $constraintAnnotation = $targetEntity->getReflectionClass()->getAttributes(static::$constraintAnnotation)[0] ?? null;
        if (!$constraintAnnotation) {
            return '';
        }

        [$startFieldName, $endFieldName] = $this->getStartAndEndFields($constraintAnnotation->getArguments());
        if (!$startFieldName || !$endFieldName) {
            throw new \RuntimeException('Missing start and/or end field names in constraint annotation');
        }

        $constraints = $this->getConstraints($this->accessId);

        $fields = [
            ActivityYearConstraint::START_KEY => $startFieldName,
            ActivityYearConstraint::END_KEY => $endFieldName
        ];

        return $this->constructQuery($constraints, $targetTableAlias, $fields);
    }

    /**
     * Fonction permettant de construire la requête SQL correspondante aux contraintes
     *
     * @param array<string, array<string, int[]>> $constraints
     * @param string $targetTableAlias
     * @param array<string, mixed> $fields
     * @return string
     */
    protected function constructQuery(array $constraints, string $targetTableAlias, array $fields): string {
        $queryConditionsAND = [];
        foreach ($constraints as $key => $constraint) {
            $queryConditionsOR = [];
            foreach ($constraint as $date => $conditions){
                foreach ($conditions as $condition){
                    $arithmetic = $this->getArithmeticValue($condition);
                    if(!is_null($arithmetic)) {
                        $queryConditionsOR[] = sprintf("%s.%s %s '%s'", $targetTableAlias, $fields[$key], $arithmetic, $date);
                    }
                    else {
                        $queryConditionsOR[] = sprintf("%s.%s IS NULL", $targetTableAlias, $fields[$key]);
                    }
                }
            }
            if(!empty($queryConditionsOR)) {
                $queryConditionsAND[] = sprintf("(%s)", join(' OR ', $queryConditionsOR));
            }
        }

        return implode(" AND ", $queryConditionsAND);
    }

    /**
     * Fonction retournant la valeur arithmétique correspondant à la condition de la contrainte
     *
     * @param int $condition
     * @return string|null
     * @see DateTimeFilterTest::testGetArithmeticValue()
     */
    protected function getArithmeticValue(int $condition): ?string
    {
        return match ($condition) {
            DateTimeConstraint::INF => '<',
            DateTimeConstraint::EQUAL => '=',
            DateTimeConstraint::SUP => '>',
            DateTimeConstraint::INF + DateTimeConstraint::EQUAL => '<=',
            DateTimeConstraint::SUP + DateTimeConstraint::EQUAL => '>=',
            default => null,
        };
    }
}
