<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\AdminBundle\Controller\Page;

use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\AdminBundle\Breadcrumb\Breadcrumb;
use Ferienpass\AdminBundle\Controller\TagsResponses;
use Ferienpass\CoreBundle\Entity\HostActivity;
use Ferienpass\CoreBundle\Entity\OfferLog;
use Ferienpass\CoreBundle\Entity\ParticipantLog;
use Ferienpass\CoreBundle\Facade\EraseDataFacade;
use Ferienpass\CoreBundle\HttpCache\CacheTag;
use Ferienpass\CoreBundle\Repository\EditionRepository;
use Ferienpass\CoreBundle\Repository\OfferRepositoryInterface;
use Ferienpass\CoreBundle\Repository\SentMessageRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

/**
 * The /admin dashboard. The whole page is rendered inline and cached as a single
 * object, partitioned per user-context hash (which folds in the host's UUIDs, so
 * hosts see their own offers) and tagged {@see CacheTag::dashboard()} — the
 * CacheInvalidationListener purges that tag on attendance and offer lifecycle
 * events. No ESI: the next-steps / heatmap / activity-feed sections used to be
 * separately-cached ESI fragments carrying the same dashboard tag, so they
 * always invalidated together anyway; folding them inline keeps one clear cache
 * entry and one invalidation path.
 */
#[Route('', name: 'admin_index')]
final class HomeController extends AbstractController
{
    use TagsResponses;

    public function __construct(#[Autowire(param: 'app.id')] private readonly string $appId)
    {
    }

    public function __invoke(Breadcrumb $breadcrumb, OfferRepositoryInterface $offers, EditionRepository $editions, SentMessageRepository $sentMessages, EraseDataFacade $eraseData, Connection $connection, EntityManagerInterface $entityManager): Response
    {
        $parameters = [
            'breadcrumb' => $breadcrumb->generate('Dashboard'),
            // Both roles' next-steps iterate the upcoming offers (hosts get the
            // "Teilnahmeliste"/"abschließen" rows, admins the edition prompts).
            'upcomingOffers' => $offers->findUpcoming(),
            'editions' => $editions,
        ];

        // The heatmap, activity feed and the admin next-steps buckets are
        // tenant-global aggregates only admins see.
        if ($this->isGranted('ROLE_ADMIN')) {
            $parameters += [
                'editionsWithUncommitedAttendances' => $editions->findWithUncommitedAttendances(),
                'messagesWithError' => $sentMessages->findWithError('30 days'),
                'editionsWithFinalizedOffers' => $editions->findWithFinalizedOffers(),
                'editionsWithWaitingAttendances' => $editions->findWithWaitingAttendances(),
                'expiredParticipants' => $eraseData->expiredParticipants(),
                'heatmapValues' => $this->countAttendancesByDay($connection),
                'activities' => $this->recentActivities($entityManager),
            ];
        }

        // One tag on the one cache entry: attendance + offer lifecycle events
        // purge it (see CacheInvalidationListener::invalidateDashboard).
        $this->tagResponse(CacheTag::dashboard($this->appId));

        return $this->render('@FerienpassAdmin/page/home.html.twig', $parameters);
    }

    /**
     * Attendances per day over the last year, keyed by unix timestamp — feeds
     * the heatmap chart.
     */
    private function countAttendancesByDay(Connection $connection): array
    {
        $days = $connection
            ->executeQuery(
                <<<'SQL'
                    SELECT DATE_FORMAT(a.createdAt, '%Y-%m-%d') AS day, COUNT(*)
                    FROM Attendance a
                    WHERE a.createdAt > (NOW() - INTERVAL 1 YEAR)
                    GROUP BY day
                    ORDER BY day
                    SQL
            )
            ->fetchAllNumeric();

        $return = [];
        foreach ($days as $row) {
            $return[strtotime((string) $row[0])] = (int) $row[1];
        }

        return $return;
    }

    /**
     * The 20 most recent offer / participant / host log entries, merged and
     * sorted newest-first — feeds the activity feed.
     */
    private function recentActivities(EntityManagerInterface $entityManager): array
    {
        $offerLogs = $entityManager->getRepository(OfferLog::class)->findBy([], ['createdAt' => 'DESC'], 20);
        $participantLogs = $entityManager->getRepository(ParticipantLog::class)->findBy([], ['createdAt' => 'DESC'], 20);
        $hostActivities = $entityManager->getRepository(HostActivity::class)->findBy([], ['createdAt' => 'DESC'], 20);

        $activities = array_merge($offerLogs, $participantLogs, $hostActivities);
        usort($activities, static fn ($a, $b) => $b->getCreatedAt() <=> $a->getCreatedAt());

        return \array_slice($activities, 0, 20);
    }
}
