<?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\BudgetBundle\Service\Summary;

use Chill\BudgetBundle\Entity\ChargeKind;
use Chill\BudgetBundle\Entity\ResourceKind;
use Chill\BudgetBundle\Repository\ChargeKindRepositoryInterface;
use Chill\BudgetBundle\Repository\ResourceKindRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\ResultSetMapping;

/**
 * Helps to find a summary of the budget: the sum of resources and charges.
 */
final readonly class SummaryBudget implements SummaryBudgetInterface
{
    private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, charge_id AS kind_id FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY charge_id';

    private const QUERY_CHARGE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, charge_id AS kind_id FROM chill_budget.charge WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY charge_id';

    private const QUERY_RESOURCE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, resource_id AS kind_id FROM chill_budget.resource WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY resource_id';

    private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, resource_id AS kind_id FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY resource_id';

    public function __construct(private EntityManagerInterface $em, private TranslatableStringHelperInterface $translatableStringHelper, private ResourceKindRepositoryInterface $resourceKindRepository, private ChargeKindRepositoryInterface $chargeKindRepository) {}

    public function getSummaryForHousehold(?Household $household): array
    {
        if (null === $household) {
            return [
                'resources' => $this->getEmptyResourceArray(),
                'charges' => $this->getEmptyChargeArray(),
            ];
        }

        $personIds = $household->getCurrentPersons()->map(static fn (Person $p) => $p->getId());
        $ids = implode(', ', array_fill(0, \count($personIds), '?'));

        $parameters = [...$personIds, $household->getId()];

        $rsm = $this->buildRsm();

        $resources = $this->em->createNativeQuery(strtr(self::QUERY_RESOURCE_BY_HOUSEHOLD, ['_ids_' => $ids]), $rsm)
            ->setParameters($parameters)
            ->getResult();
        $charges = $this->em->createNativeQuery(strtr(self::QUERY_CHARGE_BY_HOUSEHOLD, ['_ids_' => $ids]), $rsm)
            ->setParameters($parameters)
            ->getResult();

        return [
            'resources' => array_merge($this->getEmptyResourceArray(), $this->rowToArray($resources, 'resource')),
            'charges' => array_merge($this->getEmptyChargeArray(), $this->rowToArray($charges, 'charge')),
        ];
    }

    public function getSummaryForPerson(?Person $person): array
    {
        if (null === $person) {
            return [
                'resources' => $this->getEmptyResourceArray(),
                'charges' => $this->getEmptyChargeArray(),
            ];
        }

        $rsm = $this->buildRsm();

        $resources = $this->em->createNativeQuery(self::QUERY_RESOURCE_BY_PERSON, $rsm)
            ->setParameters([$person->getId()])
            ->getResult();
        $charges = $this->em->createNativeQuery(self::QUERY_CHARGE_BY_PERSON, $rsm)
            ->setParameters([$person->getId()])
            ->getResult();

        return [
            'resources' => array_merge($this->getEmptyResourceArray(), $this->rowToArray($resources, 'resource')),
            'charges' => array_merge($this->getEmptyChargeArray(), $this->rowToArray($charges, 'charge')),
        ];
    }

    private function buildRsm(): ResultSetMapping
    {
        $rsm = new ResultSetMapping();
        $rsm
            ->addScalarResult('sum', 'sum')
            ->addScalarResult('kind_id', 'kind_id')
            ->addScalarResult('comment', 'comment');

        return $rsm;
    }

    private function getEmptyChargeArray(): array
    {
        $keys = array_map(static fn (ChargeKind $kind) => $kind->getKind(), $this->chargeKindRepository->findAll());

        return array_combine($keys, array_map(fn ($kind) => ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->chargeKindRepository->findOneByKind($kind)->getName()), 'comment' => ''], $keys));
    }

    private function getEmptyResourceArray(): array
    {
        $keys = array_map(static fn (ResourceKind $kind) => $kind->getKind(), $this->resourceKindRepository->findAll());

        return array_combine($keys, array_map(fn ($kind) => ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->resourceKindRepository->findOneByKind($kind)->getName()), 'comment' => ''], $keys));
    }

    private function rowToArray(array $rows, string $kind): array
    {
        $result = [];

        switch ($kind) {
            case 'charge':
                foreach ($rows as $row) {
                    $chargeKind = $this->chargeKindRepository->find($row['kind_id']);

                    if (null === $chargeKind) {
                        throw new \RuntimeException('charge kind not found: '.$row['kind_id']);
                    }
                    $result[$chargeKind->getKind()] = [
                        'sum' => (float) $row['sum'],
                        'label' => $this->translatableStringHelper->localize($chargeKind->getName()),
                        'comment' => (string) $row['comment'],
                    ];
                }

                return $result;

            case 'resource':
                foreach ($rows as $row) {
                    $resourceKind = $this->resourceKindRepository->find($row['kind_id']);

                    if (null === $resourceKind) {
                        throw new \RuntimeException('charge kind not found: '.$row['kind_id']);
                    }

                    $result[$resourceKind->getKind()] = [
                        'sum' => (float) $row['sum'],
                        'label' => $this->translatableStringHelper->localize($resourceKind->getName()),
                        'comment' => (string) $row['comment'],
                    ];
                }

                return $result;

            default:
                throw new \LogicException();
        }
    }
}
