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

use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\Exception\UnauthorizedGenerationException;
use Chill\MainBundle\Form\Type\Export\ExportType;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Service\Regroupement\CenterRegroupementResolver;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\QueryBuilder;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\Response;

/**
 * Generate a single export.
 */
final readonly class ExportGenerator
{
    private bool $filterStatsByCenters;

    public function __construct(
        private ExportManager $exportManager,
        private ExportConfigNormalizer $configNormalizer,
        private LoggerInterface $logger,
        private AuthorizationHelperInterface $authorizationHelper,
        private CenterRegroupementResolver $centerRegroupementResolver,
        private ExportConfigProcessor $exportConfigProcessor,
        ParameterBagInterface $parameterBag,
        private CenterRepositoryInterface $centerRepository,
    ) {
        $this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
    }

    public function generate(string $exportAlias, array $configuration, ?User $byUser = null): FormattedExportGeneration
    {
        $data = $this->configNormalizer->denormalizeConfig($exportAlias, $configuration);
        $export = $this->exportManager->getExport($exportAlias);

        $centers = $this->filterCenters($byUser, $data['centers']['centers'], $data['centers']['regroupments'], $export);

        $context = new ExportGenerationContext($byUser);

        if ($export instanceof DirectExportInterface) {
            $generatedExport = $export->generate(
                $this->buildCenterReachableScopes($centers),
                $data['export'],
                $context,
            );

            if ($generatedExport instanceof Response) {
                trigger_deprecation('chill-project/chill-bundles', '3.10', 'DirectExportInterface should not return a %s instance, but a %s instance', Response::class, FormattedExportGeneration::class);

                return new FormattedExportGeneration($generatedExport->getContent(), $generatedExport->headers->get('Content-Type'));
            }

            return $generatedExport;
        }

        $query = $export->initiateQuery(
            $this->retrieveUsedModifiers($data),
            $this->buildCenterReachableScopes($centers),
            $data['export'],
            $context,
        );

        if ($query instanceof \Doctrine\ORM\NativeQuery) {
            // throw an error if the export require other modifier, which is
            // not allowed when the export return a `NativeQuery`
            if (\count($export->supportsModifiers()) > 0) {
                throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`');
            }
        } elseif ($query instanceof QueryBuilder) {
            // handle filters
            $this->handleFilters($query, $data[ExportType::FILTER_KEY], $context);

            // handle aggregators
            $this->handleAggregators($query, $data[ExportType::AGGREGATOR_KEY], $context);

            $this->logger->notice('[export] will execute this qb in export', [
                'dql' => $query->getDQL(),
            ]);
            $this->logger->debug('[export] will execute this sql qb in export', [
                'sql' => $query->getQuery()->getSQL(),
            ]);
        } else {
            throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.');
        }

        $result = $export->getResult($query, $data['export'], $context);

        $formatter = $this->exportManager->getFormatter($data['pick_formatter']);
        $filtersData = [];
        $aggregatorsData = [];

        if ($query instanceof QueryBuilder) {
            foreach ($this->exportConfigProcessor->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]) as $alias => $aggregator) {
                $aggregatorsData[$alias] = $data[ExportType::AGGREGATOR_KEY][$alias]['form'];
            }
            foreach ($this->exportConfigProcessor->retrieveUsedFilters($data[ExportType::FILTER_KEY]) as $alias => $filter) {
                $filtersData[$alias] = $data[ExportType::FILTER_KEY][$alias]['form'];
            }
        }

        /* @phpstan-ignore-next-line the method "generate" is not yet implemented on all formatters */
        if (method_exists($formatter, 'generate')) {
            return $formatter->generate(
                $result,
                $data['formatter'],
                $exportAlias,
                $data['export'],
                $filtersData,
                $aggregatorsData,
                $context,
            );
        }

        trigger_deprecation('chill-project/chill-bundles', '3.10', '%s should implements the "generate" method', FormatterInterface::class);

        /* @phpstan-ignore-next-line this is a deprecated method that we must still call */
        $generatedExport =  $formatter->getResponse(
            $result,
            $data['formatter'],
            $exportAlias,
            $data['export'],
            $filtersData,
            $aggregatorsData,
            $context,
        );

        return new FormattedExportGeneration($generatedExport->getContent(), $generatedExport->headers->get('content-type'));
    }

    private function filterCenters(User $byUser, array $centers, array $regroupements, ExportInterface|DirectExportInterface $export): array
    {
        if (!$this->filterStatsByCenters) {
            return $this->centerRepository->findActive();
        }

        $authorizedCenters = new ArrayCollection($this->authorizationHelper->getReachableCenters($byUser, $export->requiredRole()));
        if ($authorizedCenters->isEmpty()) {
            throw new UnauthorizedGenerationException('No authorized centers');
        }

        $wantedCenters = $this->centerRegroupementResolver->resolveCenters($regroupements, $centers);

        $resolvedCenters = [];
        foreach ($wantedCenters as $wantedCenter) {
            if ($authorizedCenters->contains($wantedCenter)) {
                $resolvedCenters[] = $wantedCenter;
            }
        }

        if ([] == $resolvedCenters) {
            throw new UnauthorizedGenerationException('No common centers between wanted centers and authorized centers');
        }

        return $resolvedCenters;
    }

    /**
     * parse the data to retrieve the used filters and aggregators.
     *
     * @return list<string>
     */
    private function retrieveUsedModifiers(mixed $data): array
    {
        if (null === $data) {
            return [];
        }

        $usedTypes = array_merge(
            $this->retrieveUsedFiltersType($data[ExportType::FILTER_KEY]),
            $this->retrieveUsedAggregatorsType($data[ExportType::AGGREGATOR_KEY]),
        );

        return array_values(array_unique($usedTypes));
    }

    /**
     * Retrieve the filter used in this export.
     *
     * @return list<string> an array with types
     */
    private function retrieveUsedFiltersType(mixed $data): array
    {
        if (null === $data) {
            return [];
        }

        $usedTypes = [];

        foreach ($this->exportConfigProcessor->retrieveUsedFilters($data) as $filter) {
            if (!\in_array($filter->applyOn(), $usedTypes, true)) {
                $usedTypes[] = $filter->applyOn();
            }
        }

        return $usedTypes;
    }

    /**
     * @return string[]
     */
    private function retrieveUsedAggregatorsType(mixed $data): array
    {
        if (null === $data) {
            return [];
        }

        $usedTypes = [];

        foreach ($this->exportConfigProcessor->retrieveUsedAggregators($data) as $alias => $aggregator) {
            if (!\in_array($aggregator->applyOn(), $usedTypes, true)) {
                $usedTypes[] = $aggregator->applyOn();
            }
        }

        return $usedTypes;
    }

    /**
     * Alter the query with selected aggregators.
     */
    private function handleAggregators(
        QueryBuilder $qb,
        array $data,
        ExportGenerationContext $context,
    ): void {
        foreach ($this->exportConfigProcessor->retrieveUsedAggregators($data) as $alias => $aggregator) {
            $formData = $data[$alias];
            $aggregator->alterQuery($qb, $formData['form'], $context);
        }
    }

    /**
     * alter the query with selected filters.
     */
    private function handleFilters(
        QueryBuilder $qb,
        mixed $data,
        ExportGenerationContext $context,
    ): void {
        foreach ($this->exportConfigProcessor->retrieveUsedFilters($data) as $alias => $filter) {
            $formData = $data[$alias];

            $filter->alterQuery($qb, $formData['form'], $context);
        }
    }

    /**
     * build the array required for defining centers and circles in the initiate
     * queries of ExportElementsInterfaces.
     *
     * @param list<Center> $centers
     */
    private function buildCenterReachableScopes(array $centers)
    {
        return array_map(static fn (Center $center) => ['center' => $center, 'circles' => []], $centers);
    }
}
