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

use Ferienpass\CoreBundle\Entity\ActivityNotification;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\ConversationItem;
use Ferienpass\CoreBundle\Entity\Offer\Base as OfferBase;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant\BaseParticipant;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Notifier\Message\EmailMessage;
use Ferienpass\CoreBundle\Notifier\Mime\NotificationEmail;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Notifier\Message\EmailMessage as SymfonyEmailMessage;
use Symfony\Component\Notifier\Notification\EmailNotificationInterface;
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
use Symfony\Component\Notifier\Recipient\RecipientInterface;

/**
 * The email a host or parent receives when a comment notifies them. Replying to
 * it posts a threaded reply back into the conversation (see the reply-to token
 * set by the handler).
 */
class CommentNotification extends AbstractNotification implements NotificationInterface, EmailNotificationInterface
{
    use ActionUrlTrait;

    private ConversationItem $comment;

    private ?string $recipientKind = null;
    private ?string $recipientName = null;
    private ?string $recipientReason = null;
    private ?string $settingsUrl = null;

    /**
     * Signed link to the applicant-facing confirmation page. Set when the author
     * ticked "Anmelde-Details mitschicken", and the trigger for the application
     * summary in the template.
     */
    private ?string $attendanceUrl = null;

    public function __construct(
        #[Autowire(param: 'ferienpass.inbound_domain')]
        private readonly ?string $inboundDomain = null,
    ) {
        parent::__construct();
    }

    public static function getName(): string
    {
        return 'comment';
    }

    /**
     * Whether replying to this email posts a reply — i.e. inbound parsing is
     * configured. Drives the "reply above this line" hint in the template.
     */
    public function canReplyByEmail(): bool
    {
        return null !== $this->inboundDomain && '' !== $this->inboundDomain;
    }

    public function getChannels(RecipientInterface $recipient): array
    {
        return ['email'];
    }

    public function comment(ConversationItem $comment): static
    {
        $this->comment = $comment;

        return $this;
    }

    /**
     * Who this particular copy is going to, so the email can address them and
     * pick the right "why you got this" footer. Only external parties (host /
     * account) are named — an admin colleague shouldn't read "schrieb an <self>".
     */
    public function recipient(string $kind, ?string $name = null, ?string $reason = null): static
    {
        $this->recipientKind = $kind;
        $this->recipientName = \in_array($kind, ['account', 'host'], true) ? $name : null;
        $this->recipientReason = $reason;

        return $this;
    }

    public function settingsUrl(string $url): static
    {
        $this->settingsUrl = $url;

        return $this;
    }

    public function attendanceUrl(?string $url): static
    {
        $this->attendanceUrl = $url;

        return $this;
    }

    /**
     * The application this conversation is about, or null when the comment hangs
     * off something else (an offer, an account).
     */
    public function attendance(): ?Attendance
    {
        $subject = $this->comment->getSubject();

        return $subject instanceof Attendance ? $subject : null;
    }

    /**
     * The application to summarise in the e-mail — only when a link to it came
     * along. Naming a registration the recipient cannot open would be worse than
     * saying nothing: a parent holds no admin account, so the link IS the access.
     */
    public function attendanceSummary(): ?Attendance
    {
        return null !== $this->attendanceUrl ? $this->attendance() : null;
    }

    /**
     * Works without an admin-configured template: falls back to a default
     * subject when none is set, so the comment email always has a subject line.
     */
    public function getSubject(): string
    {
        $subject = parent::getSubject();

        return '' !== trim($subject) ? $subject : \sprintf('Neue Nachricht zu %s', $this->subjectLabel());
    }

    public function getContext(): array
    {
        return array_merge(parent::getContext(), [
            'comment' => $this->comment,
            'authorName' => $this->comment->getAuthorLabel(),
            'subjectLabel' => $this->subjectLabel(),
            'bodyHtml' => $this->bodyHtml(),
            'canReplyByEmail' => $this->canReplyByEmail(),
            'verb' => $this->verb(),
            'recipientName' => $this->recipientName,
            'reasonText' => $this->reasonText(),
            'action_url' => $this->actionUrl,
            'settingsUrl' => $this->settingsUrl,
            'attendance' => $this->attendanceSummary(),
            'attendanceUrl' => $this->attendanceUrl,
        ]);
    }

    public static function getAvailableTokens(): array
    {
        return array_merge(parent::getAvailableTokens(), ['comment', 'authorName', 'subjectLabel']);
    }

    public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): ?SymfonyEmailMessage
    {
        return EmailMessage::fromFerienpassNotification($this, $recipient, function (NotificationEmail $email): void {
            $email->conversationItem($this->comment->getUuid()->toRfc4122());

            if (null !== $this->actionUrl) {
                $email->action('email.comment.open', $this->actionUrl);
            }
        });
    }

    public function createMock(string $offerClass = OfferBase::class, string $participantClass = BaseParticipant::class): static
    {
        $notification = parent::createMock($offerClass, $participantClass);

        $offer = new $offerClass();
        $offer->setName('Schwimmkurs für Anfänger');

        $author = new User();
        $author->setFirstname('Anna');
        $author->setLastname('Administratorin');

        $notification->comment(new ConversationItem($offer, $author, 'Hallo @Veranstalter, könnten Sie bitte die Begleitpersonen für den Ausflug bestätigen?', notify: [['type' => 'group', 'g' => 'host', 'label' => 'Veranstalter']]));
        $notification->recipient('host', 'Volkshochschule Beispielstadt');
        $notification->actionUrl('#');
        $notification->settingsUrl('#');

        return $notification;
    }

    private function subjectLabel(): string
    {
        $subject = $this->comment->getSubject();

        return match (true) {
            $subject instanceof Attendance => $subject->getOffer()->getName(),
            $subject instanceof OfferInterface => $subject->getName(),
            default => (string) (method_exists($subject, 'getName') ? $subject->getName() : $subject),
        };
    }

    /**
     * The gray "who did what" verb: an @mention reads "erwähnte Sie", a reply
     * "antwortete", otherwise "schrieb [an]" — the "an" only when we go on to
     * name the (external) recipient.
     */
    private function verb(): string
    {
        if (ActivityNotification::REASON_MENTION === $this->recipientReason) {
            return 'erwähnte Sie';
        }

        $replied = ActivityNotification::REASON_REPLY === $this->recipientReason || $this->comment->isReply();

        if (null !== $this->recipientName && '' !== $this->recipientName) {
            return $replied ? 'antwortete' : 'schrieb an';
        }

        return $replied ? 'antwortete' : 'schrieb';
    }

    private function reasonText(): string
    {
        return match ($this->recipientKind) {
            'host' => 'Sie erhalten diese E-Mail, weil Sie diese Unterhaltung betreuen.',
            'account' => 'Sie erhalten diese E-Mail, weil Sie an dieser Unterhaltung beteiligt sind.',
            default => ActivityNotification::REASON_MENTION === $this->recipientReason
                ? 'Sie erhalten diese E-Mail, weil Sie namentlich erwähnt wurden.'
                : 'Sie erhalten diese E-Mail, weil Sie an dieser Unterhaltung beteiligt sind.',
        };
    }

    /**
     * The comment body as safe HTML: escaped, newlines to <br>, and every.
     *
     * @mention that resolves to a notify target coloured by role (host / account
     * / admin) — the design's "mentions are just bold coloured text". strtr
     * replaces the longest token first and never re-scans inserted markup, so a
     * short "@Team" can't corrupt an already-wrapped "@Team Betreuung".
     */
    private function bodyHtml(): string
    {
        $escaped = htmlspecialchars($this->comment->getBody(), \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');

        $colors = ['host' => '#2563eb', 'account' => '#2f6f57', 'admin' => '#6b4a9a'];
        $pairs = [];
        foreach ($this->comment->getNotify() as $target) {
            $label = trim((string) ($target['label'] ?? ''));
            if ('' === $label) {
                continue;
            }

            $token = htmlspecialchars('@'.$label, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
            $color = $colors[$target['g'] ?? ''] ?? '#6b4a9a';
            $pairs[$token] = \sprintf('<strong style="color:%s;font-weight:600;">%s</strong>', $color, $token);
        }

        return nl2br($pairs ? strtr($escaped, $pairs) : $escaped, false);
    }
}
