<?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\Comment;

use Ferienpass\CoreBundle\Entity\ActivityNotification;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\ConversationItem;
use Ferienpass\CoreBundle\Entity\Host;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Repository\UserRepository;

/**
 * Knows, for a given comment subject, which audiences can be notified — each
 * host by name (reaching the whole host: its inbox or its members), the
 * account/family by name (a single recipient) and each admin colleague by name.
 * Drives the mention picker and turns the picked targets into concrete in-app
 * recipients and email addresses.
 *
 * The picker deliberately offers only concrete, named targets — no generic
 * "Veranstalter"/"Account" group entries — so it is always obvious who a mention
 * actually reaches.
 */
final readonly class CommentAudienceResolver
{
    public function __construct(private UserRepository $users)
    {
    }

    /**
     * The audiences offered in the mention picker for a subject.
     *
     * @return array{groups: array<int, array<string, mixed>>, people: array<int, array<string, mixed>>}
     */
    public function audiences(object $subject): array
    {
        $groups = [];
        $people = [];

        // Each host by its own name — one concrete tag that reaches the whole host
        // (its shared inbox, or every member when it has none). Grouped so the
        // picker flags it as a multi-recipient target. Carries the host id so a
        // pick on a multi-host offer only notifies the host actually chosen.
        foreach ($this->hostsFor($subject) as $host) {
            $name = $host->getName();
            if (null === $name || '' === $name) {
                continue;
            }
            $groups[] = ['type' => 'group', 'g' => 'host', 'id' => $host->getId(), 'label' => $name, 'sub' => 'Veranstalter', 'icon' => 'ph:buildings'];
        }

        // The account/family as a single, named recipient. Offered whenever there
        // is a contact e-mail — a participant can carry its own without a linked
        // user account, so gate on the e-mail, not on `accountFor()`. With a linked
        // account it is a concrete person; without one it stays reachable by that
        // contact e-mail, still labelled with the applicant's name.
        $accountEmail = $this->accountEmailFor($subject);
        if (null !== $accountEmail && '' !== $accountEmail) {
            $accountName = $this->accountNameFor($subject) ?? $accountEmail;
            $account = $this->accountFor($subject);
            $people[] = $account instanceof User
                ? ['type' => 'user', 'g' => 'account', 'id' => $account->getId(), 'label' => $accountName, 'sub' => 'Account']
                : ['type' => 'group', 'g' => 'account', 'label' => $accountName, 'sub' => 'Account'];
        }

        // Admin colleagues, individually by name — never a catch-all "Admin-Team"
        // group, so it is always clear who actually gets notified.
        foreach ($this->users->findAdmins() as $admin) {
            $people[] = ['type' => 'user', 'g' => 'admin', 'id' => $admin->getId(), 'label' => $admin->getName(), 'sub' => 'Verwaltung'];
        }

        return ['groups' => $groups, 'people' => $people];
    }

    /**
     * Resolves the stored notify descriptors of a comment into the people who
     * should be alerted.
     *
     * @return array{
     *     inApp: array<int, array{user: User, reason: string}>,
     *     emails: array<string, array{email: string, name: ?string, kind: string}>,
     * }
     */
    public function resolve(ConversationItem $comment): array
    {
        $subject = $comment->getSubject();
        $inApp = [];
        $emails = [];

        $addAdmin = static function (User $user, string $reason) use (&$inApp): void {
            $id = $user->getId();
            if (null === $id) {
                return;
            }
            // A direct mention outranks a group membership.
            if (!isset($inApp[$id]) || ActivityNotification::REASON_MENTION === $reason) {
                $inApp[$id] = ['user' => $user, 'reason' => $reason];
            }
        };

        $addEmail = static function (?string $email, ?string $name, string $kind) use (&$emails): void {
            $email = trim((string) $email);
            if ('' === $email) {
                return;
            }
            $emails[strtolower($email)] ??= ['email' => $email, 'name' => $name, 'kind' => $kind];
        };

        foreach ($comment->getNotify() as $target) {
            $type = $target['type'] ?? null;
            $group = $target['g'] ?? null;

            if ('group' === $type && 'admin' === $group) {
                foreach ($this->users->findAdmins() as $admin) {
                    $addAdmin($admin, ActivityNotification::REASON_GROUP);
                }
                continue;
            }

            if ('group' === $type && 'host' === $group) {
                // A picked host carries its id (notify only that one); older
                // id-less descriptors fall back to every host of the subject.
                foreach ($this->hostEmails($subject, $target['id'] ?? null) as $hostEmail) {
                    $addEmail($hostEmail['email'], $hostEmail['name'], 'host');
                }
                continue;
            }

            if ('group' === $type && 'account' === $group) {
                $addEmail($this->accountEmailFor($subject), $this->accountNameFor($subject), 'account');
                continue;
            }

            if ('user' === $type && isset($target['id'])) {
                $user = $this->users->find($target['id']);
                if (!$user instanceof User) {
                    continue;
                }

                if ('admin' === $group) {
                    // Direct colleague mention → in-app bell; the e-mail (when the
                    // colleague subscribes) is dispatched by CommentNotifier.
                    $addAdmin($user, ActivityNotification::REASON_MENTION);
                } else {
                    $addEmail($user->getEmail(), $user->getName(), \in_array($group, ['host', 'account'], true) ? $group : 'host');
                }
            }
        }

        return ['inApp' => $inApp, 'emails' => $emails];
    }

    /**
     * @return array<int, Host>
     */
    private function hostsFor(object $subject): array
    {
        return match (true) {
            $subject instanceof Host => [$subject],
            $subject instanceof OfferInterface => $subject->getHosts()->toArray(),
            $subject instanceof Attendance => $subject->getOffer()->getHosts()->toArray(),
            default => [],
        };
    }

    private function accountFor(object $subject): ?User
    {
        return match (true) {
            $subject instanceof User => $subject,
            $subject instanceof ParticipantInterface => $subject->getUser(),
            $subject instanceof Attendance => $subject->getParticipant()?->getUser(),
            default => null,
        };
    }

    /**
     * The e-mail to notify for the account/family audience. Unlike
     * {@see accountFor()} this needs no linked user: a participant carries its
     * own e-mail (falling back to the user's), so an account-less participant
     * with an e-mail is still reachable.
     */
    private function accountEmailFor(object $subject): ?string
    {
        return match (true) {
            $subject instanceof User => $subject->getEmail(),
            $subject instanceof ParticipantInterface => $subject->getEmail(),
            $subject instanceof Attendance => $subject->getParticipant()?->getEmail(),
            default => null,
        };
    }

    private function accountNameFor(object $subject): ?string
    {
        if (($user = $this->accountFor($subject)) instanceof User) {
            return $user->getName();
        }

        return match (true) {
            $subject instanceof ParticipantInterface => $subject->getName(),
            $subject instanceof Attendance => $subject->getParticipant()?->getName(),
            default => null,
        };
    }

    /**
     * Email targets for the host audience of a subject: the host's own address
     * when set, otherwise the host members' addresses. When $onlyHostId is given,
     * restrict to that single host (the concrete tag the picker offers).
     *
     * @return array<int, array{email: ?string, name: ?string}>
     */
    private function hostEmails(object $subject, int|string|null $onlyHostId = null): array
    {
        $result = [];
        foreach ($this->hostsFor($subject) as $host) {
            if (null !== $onlyHostId && (string) $host->getId() !== (string) $onlyHostId) {
                continue;
            }

            if (null !== $host->getEmail() && '' !== $host->getEmail()) {
                $result[] = ['email' => $host->getEmail(), 'name' => $host->getName()];
                continue;
            }

            foreach ($host->getMemberAssociations() as $association) {
                $member = $association->getUser();
                $result[] = ['email' => $member->getEmail(), 'name' => $member->getName()];
            }
        }

        return $result;
    }
}
