<?php

namespace App\Services\Mail;

use App\Models\Customer;
use App\Models\InboundMail;
use App\Models\SupportRequest;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Support\Facades\Log;

/**
 * Turning fetched messages into rows an operator can work through.
 *
 * Three decisions, and only three: is this one already here, whose is it, and
 * does it belong to a question already open.
 *
 * Whose it is, is decided by the sender's ADDRESS and nothing else. Not by a
 * name in the subject line and not by anything in the body: a mail is easy to
 * write and this decision attaches a stranger's words to a real customer's
 * file. An address nobody recognises stays unassigned, which is a state to work
 * in and not an error — a new enquiry starts exactly that way.
 */
class IngestInboundMail
{
    public function __construct(
        private readonly InboundMailbox $mailbox,
        private readonly InboundMailStatus $status,
    ) {}

    /**
     * Fetch, store, and flag what was stored.
     *
     * @return int how many new messages were taken in
     */
    public function __invoke(int $limit = 50): int
    {
        if (! $this->mailbox->isConfigured()) {
            return 0;
        }

        // The scheduled run records its own outcome, so the "last checked" line
        // in the console means the last time the mailbox was actually reached —
        // not the last time somebody pressed a button. A mailbox that stopped
        // answering at three in the morning is the one worth seeing, and nobody
        // presses a button at three in the morning.
        $check = $this->mailbox->check();
        $this->status->record($check['ok'], $check['message'], $check['unseen']);

        if (! $check['ok']) {
            return 0;
        }

        $taken = 0;

        foreach ($this->mailbox->fetch($limit) as $mail) {
            $stored = $this->store($mail);

            if ($stored) {
                $taken++;
            }

            // Flagged only once the row is safely written — including for a
            // duplicate, which IS safely written, just earlier. Flagging first
            // and failing after would lose the message with nothing to show
            // for it, and there is no second copy anywhere.
            $this->mailbox->markSeen($mail->uid);
        }

        return $taken;
    }

    private function store(FetchedMail $mail): bool
    {
        $customer = Customer::query()->where('email', $mail->fromEmail)->first();

        try {
            InboundMail::create([
                'customer_id' => $customer?->id,
                'support_request_id' => $customer === null ? null : $this->openRequestFor($customer)?->id,
                'message_id' => $mail->messageId,
                'from_email' => $mail->fromEmail,
                'from_name' => $mail->fromName,
                'subject' => $mail->subject,
                'body' => $mail->body,
                'attachments' => $mail->attachments === [] ? null : $mail->attachments,
                'received_at' => $mail->receivedAt,
            ]);
        } catch (UniqueConstraintViolationException) {
            // Already here. The fetch runs every few minutes and a flag that
            // did not stick on the server costs one re-read — which is exactly
            // what this catch turns into nothing.
            return false;
        } catch (\Throwable $e) {
            Log::error('Could not store an inbound mail', ['exception' => $e]);

            return false;
        }

        return true;
    }

    /**
     * The question this is probably an answer to.
     *
     * The customer's most recent request that is still open. A guess, and a
     * shallow one on purpose — it only pre-fills a link an operator can undo,
     * and threading by References/In-Reply-To would need the whole outgoing
     * side to carry stable ids first.
     */
    private function openRequestFor(Customer $customer): ?SupportRequest
    {
        return SupportRequest::query()
            ->where('customer_id', $customer->id)
            ->whereNotIn('status', SupportRequest::CLOSED)
            ->latest('id')
            ->first();
    }
}
