<?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\JobBundle\Export;

use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\ListInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\PersonBundle\Entity\Person;
use Chill\JobBundle\Security\Authorization\ExportsJobVoter;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

/**
 * Class ListCV.
 *
 * @author Mathieu Jaumotte mathieu.jaumotte@champs-libres.coop
 */
class ListCV implements ListInterface, ExportElementValidatedInterface
{
    use \Chill\MainBundle\Export\ExportDataNormalizerTrait;
    /**
     * @var array
     */
    public const FIELDS = [
        'id' => 'integer',
        'firstname' => 'string',
        'lastname' => 'string',
        'gender' => 'string',
        'birthdate' => 'date',
        'placeofbirth' => 'string',
        'countryofbirth' => 'json',
        'formationlevel' => 'string',
    ];

    /**
     * @var array
     */
    protected $personIds = [];

    /**
     * @var EntityManagerInterface
     */
    protected $entityManager;

    /**
     * ListAcquisition constructor.
     */
    public function __construct(EntityManagerInterface $em)
    {
        $this->entityManager = $em;
    }

    /**
     * validate the form's data and, if required, build a contraint
     * violation on the data.
     *
     * @param mixed $data the data, as returned by the user
     */
    public function validateForm($data, ExecutionContextInterface $context): void {}

    /**
     * get a title, which will be used in UI (and translated).
     *
     * @return string
     */
    public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface
    {
        return 'Liste des CVs par personne';
    }

    /**
     * Add a form to collect data from the user.
     */
    public function buildForm(FormBuilderInterface $builder)
    {
        $builder
            ->add('fields', ChoiceType::class, [
                'multiple' => true,
                'expanded' => true,
                //                'choices_as_values' => true,
                'label' => 'Fields to include in export',
                'choices' => array_combine($this->getFields(), $this->getFields()),
                'data' => array_combine($this->getFields(), $this->getFields()),
                'choice_attr' => [],
                'attr' => ['class' => ''],
                'constraints' => [new Callback([
                    'callback' => function ($selected, ExecutionContextInterface $context) {
                        if (0 === count($selected)) {
                            $context
                                ->buildViolation('You must select at least one element')
                                ->atPath('fields')
                                ->addViolation();
                        }
                    },
                ])],
            ])
            ->add('reportdate_min', ChillDateType::class, [
                'label' => 'Date du rapport après le',
                'required' => false,
                'label_attr' => [
                    'class' => 'reportdate_range',
                ],
                'attr' => [
                    'class' => 'reportdate_range',
                ],
            ])
            ->add('reportdate_max', ChillDateType::class, [
                'label' => 'Date du rapport avant le',
                'required' => false,
                'label_attr' => [
                    'class' => 'report_date_range',
                ],
                'attr' => [
                    'class' => 'report_date_range',
                ],
            ])
        ;
    }

    public function getNormalizationVersion(): int
    {
        return 1;
    }

    public function normalizeFormData(array $formData): array
    {
        return ['fields' => $formData['fields'], 'reportdate_min' => $this->normalizeDate($formData['reportdate_min']), 'reportdate_max' => $this->normalizeDate($formData['reportdate_max'])];
    }

    public function denormalizeFormData(array $formData, int $fromVersion): array
    {
        return ['fields' => $formData['fields'], 'reportdate_min' => $this->denormalizeDate($formData['reportdate_min']), 'reportdate_max' => $this->denormalizeDate($formData['reportdate_max'])];
    }

    /**
     * Return the Export's type. This will inform _on what_ export will apply.
     * Most of the type, it will be a string which references an entity.
     *
     * Example of types : Chill\PersonBundle\Export\Declarations::PERSON_TYPE
     */
    public function getType(): string
    {
        return Person::class;
    }

    /**
     * A description, which will be used in the UI to explain what the export does.
     * This description will be translated.
     *
     * @return string
     */
    public function getDescription()
    {
        return 'Crée une liste des CVs en fonction de différents paramètres.';
    }

    public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): QueryBuilder
    {
        return $this->entityManager->createQueryBuilder()
            ->from('ChillPersonBundle:Person', 'person');
    }

    /**
     * Inform which ModifiersInterface (i.e. AggregatorInterface, FilterInterface)
     * are allowed. The modifiers should be an array of types the _modifier_ apply on
     * (@see ModifiersInterface::applyOn()).
     *
     * @return string[]
     */
    public function supportsModifiers()
    {
        return ['cv', 'person'];
    }

    /**
     * Return the required Role to execute the Export.
     */
    public function requiredRole(): string
    {
        return ExportsJobVoter::EXPORT;
    }

    /**
     * Return which formatter type is allowed for this report.
     *
     * @return string[]
     */
    public function getAllowedFormattersTypes()
    {
        return [FormatterInterface::TYPE_LIST];
    }

    /**
     * give the list of keys the current export added to the queryBuilder in
     * self::initiateQuery.
     *
     * Example: if your query builder will contains `SELECT count(id) AS count_id ...`,
     * this function will return `array('count_id')`.
     *
     * @param mixed[] $data the data from the export's form (added by self::buildForm)
     */
    public function getQueryKeys($data): array
    {
        return array_keys($data['fields']);
    }

    /**
     * Return array FIELDS keys only.
     *
     * @return array
     */
    private function getFields()
    {
        return array_keys(self::FIELDS);
    }

    /**
     * Return the results of the query builder.
     *
     * @param QueryBuilder|\Doctrine\ORM\NativeQuery $qb
     * @param mixed[]                                $data the data from the export's form (added by self::buildForm)
     */
    public function getResult($qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array
    {
        $qb->select('person.id');

        $ids = $qb->getQuery()->getResult(Query::HYDRATE_SCALAR);
        $this->personIds = array_map(fn ($e) => $e['id'], $ids);
        $personIdsParameters = '?'.\str_repeat(', ?', count($this->personIds) - 1);
        $query = \str_replace('%person_ids%', $personIdsParameters, self::QUERY);

        $rsm = new Query\ResultSetMapping();

        foreach (self::FIELDS as $name => $type) {
            if (null !== $data['fields'][$name]) {
                $rsm->addScalarResult($name, $name, $type);
            }
        }

        $nq = $this->entityManager->createNativeQuery($query, $rsm);

        $idx = 1;
        for ($i = 1; $i <= count($this->personIds); ++$i) {
            ++$idx;
            $nq->setParameter($i, $this->personIds[$i - 1]);
        }
        $nq->setParameter($idx++, $data['reportdate_min'], 'date');
        $nq->setParameter($idx, $data['reportdate_max'], 'date');

        return $nq->getResult();
    }

    /**
     * transform the results to viewable and understable string.
     *
     * The callable will have only one argument: the `value` to translate.
     *
     * The callable should also be able to return a key `_header`, which
     * will contains the header of the column.
     *
     * The string returned **must** be already translated if necessary,
     * **with an exception** for the string returned for `_header`.
     *
     * Example :
     *
     * ```
     * protected $translator;
     *
     * public function getLabels($key, array $values, $data)
     * {
     *      return function($value) {
     *          case $value
     *          {
     *              case '_header' :
     *                  return 'my header not translated';
     *              case true:
     *                  return $this->translator->trans('true');
     *              case false:
     *                  return $this->translator->trans('false');
     *              default:
     *                  // this should not happens !
     *                  throw new \LogicException();
     *          }
     * }
     * ```
     *
     * **Note:** Why each string must be translated with an exception for
     * the `_header` ? For performance reasons: most of the value will be number
     * which do not need to be translated, or value already translated in
     * database. But the header must be, in every case, translated.
     *
     * @param string  $key    The column key, as added in the query
     * @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR')
     * @param mixed   $data   The data from the export's form (as defined in `buildForm`
     */
    public function getLabels($key, array $values, $data)
    {
        return match ($key) {
            'birthdate' => function ($value) use ($key) {
                if ('_header' === $value) {
                    return $key;
                }
                if (null === $value || '' === $value) {
                    return '';
                }

                return $value->format('d-m-Y');
            },
            'countryofbirth' => function ($value) use ($key) {
                if ('_header' === $value) {
                    return $key;
                }

                return $value['fr'];
            },
            'gender' => function ($value) use ($key) {
                $gend_array = ['man' => 'Homme', 'woman' => 'Femme', 'both' => 'Indéterminé'];
                if ('_header' === $value) {
                    return $key;
                }
                if (null === $value || '' === $value) {
                    return '';
                }

                return $gend_array[$value];
            },
            default => function ($value) use ($key) {
                if ('_header' === $value) {
                    return $key;
                }

                return $value;
            },
        };
    }

    /**
     * Native Query SQL.
     */
    public const QUERY = <<<'SQL'

SELECT
  p.id as id,
  p.firstname as firstname,
  p.lastname as lastname,
  p.gender as gender,
  p.birthdate as birthdate,
  p.place_of_birth as placeofbirth,
  cn.name as countryofbirth,
  cv.formationlevel as formationlevel
FROM
  public.chill_person_person AS p
LEFT JOIN chill_job.cv AS cv
  ON p.id = cv.person_id
LEFT JOIN public.country AS cn
  ON p.countryofbirth_id = cn.id

-- condition 1
WHERE p.id IN (%person_ids%)

-- condition 2
AND (
  cv.reportdate
  BETWEEN COALESCE(?::date, '1900-01-01')
      AND COALESCE(?::date, '2100-01-01')
)

ORDER BY cv.reportdate DESC

SQL;

    public function getFormDefaultData(): array
    {
        return [];
    }
}
