<?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\CoreBundle\Repository;

use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\ConversationItem;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\Host;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Entity\User;

/**
 * @extends ServiceEntityRepository<ConversationItem>
 */
class ConversationItemRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, ConversationItem::class);
    }

    /**
     * All comments attached to a subject entity, oldest first.
     *
     * @return array<int, ConversationItem>
     */
    public function findForSubject(object $subject): array
    {
        $field = self::fieldForSubject($subject);

        return $this->createQueryBuilder('c')
            ->andWhere(\sprintf('c.%s = :subject', $field))
            ->setParameter('subject', $subject)
            ->orderBy('c.createdAt', 'ASC')
            ->getQuery()
            ->getResult()
        ;
    }

    /**
     * Comment counts for a whole page of list rows, in a single query.
     *
     * The list templates otherwise ask every row's EXTRA_LAZY `conversation`
     * collection for its size, which is one `SELECT COUNT(*)` per row.
     *
     * All subjects must be of the same kind — a list page renders one entity
     * type. Anything that is not a commentable entity (an array row, a Debtor,
     * …) is skipped, so callers can pass a raw list of rows.
     *
     * @param iterable<mixed> $subjects
     *
     * @return array<int, int> subject id => number of comments; subjects
     *                         without a comment are absent from the map
     */
    public function countForSubjects(iterable $subjects): array
    {
        $field = null;
        $ids = [];

        foreach ($subjects as $subject) {
            if (!\is_object($subject)) {
                continue;
            }

            try {
                $subjectField = self::fieldForSubject($subject);
            } catch (\InvalidArgumentException) {
                continue;
            }

            $field ??= $subjectField;
            if ($subjectField !== $field) {
                return [];
            }

            $id = $subject->getId();
            if (null !== $id) {
                $ids[] = $id;
            }
        }

        if (null === $field || [] === $ids) {
            return [];
        }

        $rows = $this->createQueryBuilder('c')
            ->select(\sprintf('IDENTITY(c.%s) AS subjectId', $field), 'COUNT(c.uuid) AS total')
            ->andWhere(\sprintf('c.%s IN (:subjects)', $field))
            ->setParameter('subjects', $ids)
            ->groupBy(\sprintf('c.%s', $field))
            ->getQuery()
            ->getScalarResult()
        ;

        $counts = [];
        foreach ($rows as $row) {
            $counts[(int) $row['subjectId']] = (int) $row['total'];
        }

        return $counts;
    }

    /**
     * The whole conversation thread the given item belongs to: its root (found
     * by walking up the parent chain) plus every descendant reply, oldest
     * first. Threads are shallow — an outbound item and the back-and-forth
     * replies to it — so walking the object graph stays cheap.
     *
     * @return array<int, ConversationItem>
     */
    public function findThread(ConversationItem $item): array
    {
        $root = $item;
        while (null !== $root->getParent()) {
            $root = $root->getParent();
        }

        $thread = [];
        $stack = [$root];
        while ([] !== $stack) {
            $current = array_pop($stack);
            $thread[] = $current;
            foreach ($current->getReplies() as $reply) {
                $stack[] = $reply;
            }
        }

        usort($thread, static fn (ConversationItem $a, ConversationItem $b): int => $a->getCreatedAt() <=> $b->getCreatedAt());

        return $thread;
    }

    /**
     * Distinct admin users who have already taken part in a subject's
     * conversation (authored a comment on it). Used to decide who to alert when
     * a host/parent replies by email.
     *
     * @return array<int, User>
     */
    public function findThreadParticipants(object $subject): array
    {
        $field = self::fieldForSubject($subject);

        $rows = $this->createQueryBuilder('c')
            ->select('DISTINCT IDENTITY(c.author) AS authorId')
            ->andWhere(\sprintf('c.%s = :subject', $field))
            ->andWhere('c.author IS NOT NULL')
            ->setParameter('subject', $subject)
            ->getQuery()
            ->getScalarResult()
        ;

        $ids = array_filter(array_map(static fn (array $r) => $r['authorId'], $rows));
        if ([] === $ids) {
            return [];
        }

        $authors = $this->getEntityManager()->getRepository(User::class)->findBy(['id' => $ids]);

        return array_values(array_filter($authors, static fn (User $u): bool => [] !== $u->getAdminRoles()));
    }

    private static function fieldForSubject(object $subject): string
    {
        return match (true) {
            $subject instanceof Attendance => 'attendance',
            $subject instanceof OfferInterface => 'offer',
            $subject instanceof ParticipantInterface => 'participant',
            $subject instanceof Host => 'host',
            $subject instanceof Edition => 'edition',
            $subject instanceof User => 'account',
            default => throw new \InvalidArgumentException(\sprintf('Unsupported comment subject "%s".', $subject::class)),
        };
    }
}
