<?php

namespace App\Listeners;

use App\Models\Customer;
use App\Models\SentMail;
use Illuminate\Mail\Events\MessageSent;
use Illuminate\Support\Facades\Log;
use Throwable;

/**
 * Write every outgoing mail into the register.
 *
 * On MessageSent rather than MessageSending: a row here means the transport
 * accepted the message. Recording the intention instead would fill the register
 * with mails that never left, which is worse than no register — an operator
 * would show a customer a line proving something was sent that was not.
 *
 * One listener for every mailable in the application, rather than a call in
 * each of them. A register that has to be remembered at each call site is a
 * register with holes in it, and the holes are exactly the mails nobody thought
 * about.
 */
class RecordSentMail
{
    /**
     * Mails that are not worth a row.
     *
     * The verification and reset mails go to an address that has not been
     * confirmed to belong to anybody yet, and both are sent in bulk by anyone
     * who can type an address into a form. Recording them would let a stranger
     * write lines into the operator's register at will.
     */
    private const IGNORED = [
        \App\Mail\VerifyEmailMail::class,
        \App\Mail\ResetPasswordMail::class,
    ];

    public function handle(MessageSent $event): void
    {
        try {
            $this->record($event);
        } catch (Throwable $e) {
            // A register that cannot be written must never be the reason a mail
            // fails. The send already happened by the time this runs, and
            // throwing here would turn a delivered message into a failed job
            // that gets delivered a second time on retry.
            Log::error('Could not record a sent mail', ['exception' => $e]);
        }
    }

    private function record(MessageSent $event): void
    {
        $mailable = $event->data['__laravel_mailable'] ?? null;

        if ($mailable !== null && in_array($mailable, self::IGNORED, true)) {
            return;
        }

        $message = $event->message;
        $to = collect($message->getTo())->map(fn ($address) => $address->getAddress())->all();

        if ($to === []) {
            return;
        }

        // One row per recipient. A mail to three addresses is three deliveries,
        // and an operator asking "what did this customer get" must not have to
        // read someone else's row to find out.
        foreach ($to as $address) {
            SentMail::create([
                'customer_id' => $this->customerFor($address)?->id,
                'to' => $address,
                'subject' => (string) $message->getSubject(),
                'mailable' => $mailable,
                // Set by the console when an operator writes one themselves;
                // see Admin\CustomerMessage, which fills the body in afterwards.
                'from_operator' => false,
                'sent_at' => now(),
            ]);
        }
    }

    /** Whose mail this was, where the address belongs to somebody we know. */
    private function customerFor(string $address): ?Customer
    {
        return Customer::query()->where('email', $address)->first();
    }
}
