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

use Chill\CustomFieldsBundle\CustomFields\CustomFieldChoice;
use Chill\CustomFieldsBundle\Entity\CustomField;
use Chill\CustomFieldsBundle\Entity\CustomFieldsGroup;
use Chill\CustomFieldsBundle\Service\CustomFieldProvider;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\ListInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Export\Declarations;
use Chill\ReportBundle\Entity\Report;
use Chill\ReportBundle\Security\Authorization\ReportVoter;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NativeQuery;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
 * @template-implements ListInterface<QueryBuilder, array{fields: list<string>, address_date: \DateTime}>
 */
class ReportList implements ExportElementValidatedInterface, ListInterface
{
    use \Chill\MainBundle\Export\ExportDataNormalizerTrait;
    protected array $fields = [
        'person_id',  'person_firstName', 'person_lastName', 'person_birthdate',
        'person_placeOfBirth', 'person_gender', 'person_memo', 'person_email', 'person_phonenumber',
        'person_countryOfBirth', 'person_nationality', 'person_address_street_address_1',
        'person_address_street_address_2', 'person_address_valid_from', 'person_address_postcode_label',
        'person_address_postcode_code', 'person_address_country_name', 'person_address_country_code',
        'report_id', 'report_user', 'report_date', 'report_scope',
    ];

    protected array $slugs = [];

    public function __construct(protected CustomFieldsGroup $customfieldsGroup, protected TranslatableStringHelper $translatableStringHelper, protected TranslatorInterface $translator, protected CustomFieldProvider $customFieldProvider, protected EntityManagerInterface $em) {}

    public function buildForm(FormBuilderInterface $builder)
    {
        $choices = array_combine($this->fields, $this->fields);

        foreach ($this->getCustomFields() as $cf) {
            $choices[$this->translatableStringHelper->localize($cf->getName())]
                =
                $cf->getSlug();
        }

        $builder->add('fields', ChoiceType::class, [
            'multiple' => true,
            'expanded' => true,
            'choices' => $choices,
            'label' => 'Fields to include in export',
            'choice_attr' => static function ($val, $key, $index) {
                // add a 'data-display-target' for address fields
                if (str_starts_with((string) $val, 'address_')) {
                    return ['data-display-target' => 'address_date'];
                }

                return [];
            },
            'choice_label' => fn (string $key, string $label): string => match (substr($key, 0, 7)) {
                'person_' => $this->translator->trans(substr($key, 7, \strlen($key) - 7)).
                    ' ('.$this->translator->trans('Person').')',
                'report_' => $this->translator->trans(\ucfirst(substr($key, 7, \strlen($key) - 7))).
                    ' ('.$this->translator->trans('Report').')',
                default => $label.
                    ' ('.$this->translator->trans("Report's question").')',
            },
            'constraints' => [new Callback([
                'callback' => static function ($selected, ExecutionContextInterface $context) {
                    if (0 === \count($selected)) {
                        $context->buildViolation('You must select at least one element')
                            ->atPath('fields')
                            ->addViolation();
                    }
                },
            ])],
        ]);

        $builder->add('address_date', ChillDateType::class, [
            'label' => 'Address valid at this date',
            'required' => false,
            'block_name' => 'list_export_form_address_date',
        ]);
    }

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

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

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

    public function getFormDefaultData(): array
    {
        return ['address_date' => new \DateTime()];
    }

    public function getAllowedFormattersTypes()
    {
        return [FormatterInterface::TYPE_LIST];
    }

    public function getDescription()
    {
        return $this->translator->trans(
            "Generate list of report '%type%'",
            [
                '%type%' => $this->translatableStringHelper->localize($this->customfieldsGroup->getName()),
            ]
        );
    }

    public function getLabels($key, array $values, $data)
    {
        switch ($key) {
            case 'person_birthdate':
            case 'report_date':
                // for birthdate or report date, we have to transform the string into a date
                // to format the date correctly.
                return static function ($value) use ($key) {
                    if ('_header' === $value) {
                        return 'person_birthdate' === $key ? 'birthdate' : 'report_date';
                    }

                    if (empty($value)) {
                        return '';
                    }

                    if ('person_birthdate' === $key) {
                        $date = \DateTime::createFromFormat('Y-m-d', $value);
                    } else {
                        $date = \DateTime::createFromFormat('Y-m-d H:i:s', $value);
                    }
                    // check that the creation could occurs.
                    if (false === $date) {
                        throw new \Exception(sprintf('The value %s could not be converted to %s', $value, \DateTime::class));
                    }

                    return $date->format('d-m-Y');
                };

            case 'report_scope':
                $qb = $this->em->getRepository(Scope::class)
                    ->createQueryBuilder('s');
                $qb->addSelect('s.name')
                    ->addSelect('s.id')
                    ->where($qb->expr()->in('s.id', $values));
                $rows = $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);

                $scopes = [];

                foreach ($rows as $row) {
                    $scopes[$row['id']] = $this->translatableStringHelper->localize($row['name']);
                }

                return static function ($value) use ($scopes): string {
                    if ('_header' === $value) {
                        return 'circle';
                    }

                    return $scopes[$value];
                };

            case 'report_user':
                $qb = $this->em->getRepository(User::class)
                    ->createQueryBuilder('u');
                $qb->addSelect('u.username')
                    ->addSelect('u.id')
                    ->where($qb->expr()->in('u.id', $values));
                $rows = $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);

                $users = [];

                foreach ($rows as $row) {
                    $users[$row['id']] = $row['username'];
                }

                return static function ($value) use ($users): string {
                    if ('_header' === $value) {
                        return 'user';
                    }

                    return $users[$value];
                };

            case 'person_gender':
                // for gender, we have to translate men/women statement
                return function ($value) {
                    if ('_header' === $value) {
                        return 'gender';
                    }

                    return $this->translator->trans($value);
                };

            case 'person_countryOfBirth':
            case 'person_nationality':
                $countryRepository = $this->em
                    ->getRepository(\Chill\MainBundle\Entity\Country::class);

                // load all countries in a single query
                $countryRepository->findBy(['countryCode' => $values]);

                return function ($value) use ($key, $countryRepository) {
                    if ('_header' === $value) {
                        return \strtolower($key);
                    }

                    if (null === $value) {
                        return $this->translator->trans('no data');
                    }

                    $country = $countryRepository->find($value);

                    return $this->translatableStringHelper->localize(
                        $country->getName()
                    );
                };

            case 'person_address_country_name':
                return function ($value) use ($key) {
                    if ('_header' === $value) {
                        return \strtolower($key);
                    }

                    if (null === $value) {
                        return '';
                    }

                    return $this->translatableStringHelper->localize(json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR));
                };

            default:
                // for fields which are associated with person
                if (\in_array($key, $this->fields, true)) {
                    return static function ($value) use ($key) {
                        if ('_header' === $value) {
                            return \strtolower($key);
                        }

                        return $value;
                    };
                }

                return $this->getLabelForCustomField($key, $values, $data);
        }
    }

    public function getQueryKeys($data): array
    {
        $fields = [];

        foreach ($data['fields'] as $key) {
            if (\in_array($key, $this->fields, true)) {
                $fields[] = $key;
            }
        }

        // add the key from slugs and return
        return [...$fields, ...\array_keys($this->slugs)];
    }

    public function getResult(QueryBuilder|NativeQuery $query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array
    {
        return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
    }

    public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface
    {
        return $this->translator->trans(
            "List for report '%type%'",
            [
                '%type%' => $this->translatableStringHelper->localize($this->customfieldsGroup->getName()),
            ]
        );
    }

    public function getType(): string
    {
        return 'report';
    }

    public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): QueryBuilder
    {
        $centers = array_map(static fn ($el) => $el['center'], $acl);

        // throw an error if any fields are present
        if (!\array_key_exists('fields', $data)) {
            throw new \Doctrine\DBAL\Exception\InvalidArgumentException('any fields have been checked');
        }

        $qb = $this->em->createQueryBuilder();

        // process fields which are not custom fields
        foreach ($this->fields as $f) {
            // do not add fields which are not selected
            if (!\in_array($f, $data['fields'], true)) {
                continue;
            }

            // add a column to the query for each field
            switch ($f) {
                case 'person_countryOfBirth':
                case 'person_nationality':
                    $suffix = substr((string) $f, 7);
                    $qb->addSelect(sprintf('IDENTITY(person.%s) as %s', $suffix, $f));

                    break;

                case 'person_address_street_address_1':
                case 'person_address_street_address_2':
                case 'person_address_valid_from':
                case 'person_address_postcode_label':
                case 'person_address_postcode_code':
                case 'person_address_country_name':
                case 'person_address_country_code':
                    // remove 'person_'
                    $suffix = substr((string) $f, 7);

                    $qb->addSelect(sprintf(
                        'GET_PERSON_ADDRESS_%s(person.id, :address_date) AS %s',
                        // get the part after address_
                        strtoupper(substr($suffix, 8)),
                        $f
                    ));
                    $qb->setParameter('address_date', $data['address_date']);

                    break;

                case 'report_scope':
                    $qb->addSelect(sprintf('IDENTITY(report.scope) AS %s', 'report_scope'));

                    break;

                case 'report_user':
                    $qb->addSelect(sprintf('IDENTITY(report.user) AS %s', 'report_user'));

                    break;

                default:
                    $prefix = substr((string) $f, 0, 7);
                    $suffix = substr((string) $f, 7);

                    match ($prefix) {
                        'person_' => $qb->addSelect(sprintf('person.%s as %s', $suffix, $f)),
                        'report_' => $qb->addSelect(sprintf('report.%s as %s', $suffix, $f)),
                        // no break
                        default => throw new \LogicException("this prefix {$prefix} should not be encountered. Full field: {$f}"),
                    };
            }
        }

        // process fields which are custom fields
        foreach ($this->getCustomFields() as $cf) {
            // do not add custom fields which are not selected
            if (!\in_array($cf->getSlug(), $data['fields'], true)) {
                continue;
            }

            $cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType());

            // if is multiple, split into multiple columns
            if ($cfType instanceof CustomFieldChoice && $cfType->isMultiple($cf)) {
                foreach ($cfType->getChoices($cf) as $choiceSlug => $label) {
                    $slug = $this->slugToDQL($cf->getSlug(), 'choice', ['choiceSlug' => $choiceSlug]);
                    $qb->addSelect(
                        sprintf(
                            'GET_JSON_FIELD_BY_KEY(report.cFData, :slug%s) AS %s',
                            $slug,
                            $slug
                        )
                    );
                    $qb->setParameter(sprintf('slug%s', $slug), $cf->getSlug());
                }
            } else {
                // not multiple, add a single column
                $slug = $this->slugToDQL($cf->getSlug());
                $qb->addSelect(
                    sprintf(
                        'GET_JSON_FIELD_BY_KEY(report.cFData, :slug%s) AS %s',
                        $slug,
                        $slug
                    )
                );
                $qb->setParameter(sprintf('slug%s', $slug), $cf->getSlug());
            }
        }

        return $qb
            ->from(Report::class, 'report')
            ->leftJoin('report.person', 'person')
            ->join('person.center', 'center')
            ->andWhere($qb->expr()->eq('report.cFGroup', ':cFGroup'))
            ->setParameter('cFGroup', $this->customfieldsGroup)
            ->andWhere('center IN (:authorized_centers)')
            ->setParameter('authorized_centers', $centers);
    }

    public function requiredRole(): string
    {
        return ReportVoter::LISTS;
    }

    public function supportsModifiers()
    {
        return [Declarations::PERSON_TYPE, 'report'];
    }

    public function validateForm($data, ExecutionContextInterface $context): void
    {
        // get the field starting with address_
        $addressFields = array_filter(
            $this->fields,
            static fn (string $el): bool => str_starts_with($el, 'address_')
        );

        // check if there is one field starting with address in data
        if (\count(array_intersect($data['fields'], $addressFields)) > 0) {
            // if a field address is checked, the date must not be empty
            if (empty($data['address_date'])) {
                $context
                    ->buildViolation('You must set this date if an address is checked')
                    ->atPath('address_date')
                    ->addViolation();
            }
        }
    }

    private function DQLToSlug($cleanedSlug)
    {
        return $this->slugs[$cleanedSlug]['slug'];
    }

    private function extractInfosFromSlug($slug)
    {
        return $this->slugs[$slug];
    }

    /**
     * Get custom fields associated with person.
     *
     * @return CustomField[]
     */
    private function getCustomFields()
    {
        return array_filter($this->customfieldsGroup
            ->getCustomFields()->toArray(), static fn (CustomField $cf) => 'title' !== $cf->getType());
    }

    private function getLabelForCustomField($key, array $values, $data)
    {
        // for fields which are custom fields
        /** @var CustomField $cf */
        $cf = $this->em
            ->getRepository(CustomField::class)
            ->findOneBy(['slug' => $this->DQLToSlug($key)]);

        $cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType());
        $defaultFunction = function ($value) use ($cf) {
            if ('_header' === $value) {
                return $this->translatableStringHelper->localize($cf->getName());
            }

            return $this->customFieldProvider
                ->getCustomFieldByType($cf->getType())
                ->render(json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR), $cf, 'csv');
        };

        if ($cfType instanceof CustomFieldChoice && $cfType->isMultiple($cf)) {
            return function ($value) use ($cf, $cfType, $key) {
                $slugChoice = $this->extractInfosFromSlug($key)['additionnalInfos']['choiceSlug'];
                $decoded = json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR);

                if ('_header' === $value) {
                    $label = $cfType->getChoices($cf)[$slugChoice];

                    return $this->translatableStringHelper->localize($cf->getName())
                            .' | '.$label;
                }

                if ('_other' === $slugChoice && $cfType->isChecked($cf, $slugChoice, $decoded)) {
                    return $cfType->extractOtherValue($cf, $decoded);
                }

                return $cfType->isChecked($cf, $slugChoice, $decoded);
            };
        }

        return $defaultFunction;
    }

    private function slugToDQL($slug, $type = 'default', array $additionalInfos = [])
    {
        $uid = 'slug_'.\uniqid('', true);

        $this->slugs[$uid] = [
            'slug' => $slug,
            'type' => $type,
            'additionnalInfos' => $additionalInfos,
        ];

        return $uid;
    }
}
