<?php

namespace App\Support\Readiness;

use App\Mail\Transport\MailboxTransport;
use App\Models\MailTemplate;
use App\Services\Mail\MailboxResolver;
use App\Services\Mail\MailLane;
use App\Services\Mail\MailPurpose;
use App\Services\Secrets\SecretVault;
use App\Support\MailDelivery;
use Illuminate\Support\Facades\Queue;

/**
 * What has to be in place for the customer to actually find out any of this
 * happened: a mail transport that leaves the server, an address to send
 * from, and — for support — a way to answer and to hear back.
 */
final class DeliveryChecks
{
    public const GROUP = 'delivery';

    /**
     * Länger als das her ist eine Spur, die nicht mehr fährt, sondern steht.
     *
     * Die langsamste Spur (mail-ruhig, Task 3) lässt standardmäßig 20
     * Aufträge je 10 Minuten durch — 120 in der Stunde. Ein Lauf dieser
     * Größe steht bei elf Kunden nicht eine ganze Stunde lang an. Schlägt die
     * Prüfung trotzdem an, klemmt etwas: der Arbeiter läuft nicht mehr, oder
     * die Spur ist aus der --queue-Liste in docker-compose.yml gefallen.
     */
    private const STUCK_AFTER_MINUTES = 60;

    /** @return array<int, Check> */
    public static function all(): array
    {
        return [
            new Check(
                key: 'delivery.mailer_not_log',
                group: self::GROUP,
                severity: Check::SEVERITY_BLOCKING,
                label: __('readiness.delivery.mailer_not_log'),
                breaks: __('readiness.delivery.mailer_not_log_breaks'),
                tab: 'mail',
                // Reads the transport's own definition of "does not really
                // send" rather than a second, narrower one. `!== 'log'` alone
                // called 'array' — the MAIL_MAILER this whole suite runs
                // under — delivered, which MailboxTransport::delegate() has
                // never agreed with.
                satisfied: MailDelivery::delivers(),
            ),
            new Check(
                key: 'delivery.mailbox',
                group: self::GROUP,
                severity: Check::SEVERITY_BLOCKING,
                label: __('readiness.delivery.mailbox'),
                breaks: __('readiness.delivery.mailbox_breaks'),
                tab: 'mail',
                // Nicht mehr `Mailbox::query()->exists()`. Dass IRGENDEIN
                // Postfach angelegt ist, entscheidet nichts: gesendet wird
                // ueber das Postfach, das dem Zweck `system` zugeordnet ist,
                // und das muss aktiv sein und Zugangsdaten haben. Die alte
                // Fassung stand gruen, waehrend jede Systemmail — Passwort
                // vergessen, Adresse bestaetigen, neues Geraet — in der
                // Warteschlange starb. `system` genuegt als Pruefgegenstand,
                // weil MailboxResolver jeden anderen Zweck darauf zurueckfallen
                // laesst.
                satisfied: app(MailboxResolver::class)->usableFor(MailPurpose::SYSTEM) !== null,
            ),
            new Check(
                key: 'delivery.mail_templates',
                group: self::GROUP,
                severity: Check::SEVERITY_WARNING,
                label: __('readiness.delivery.mail_templates'),
                breaks: __('readiness.delivery.mail_templates_breaks'),
                tab: 'templates',
                satisfied: MailTemplate::query()->exists(),
            ),
            new Check(
                // Named after the vault entry it checks (inbound_mail.password),
                // not a shorthand — see the identical reasoning on
                // onboarding.ssh_private_key in OnboardingChecks.php.
                key: 'delivery.inbound_mail_password',
                group: self::GROUP,
                severity: Check::SEVERITY_WARNING,
                label: __('readiness.delivery.inbound_password'),
                breaks: __('readiness.delivery.inbound_password_breaks'),
                // The inbound-mail card (host/port/user/folder + this
                // password) is on the 'services' tab — 'integrations' is not
                // a member of Integrations::TABS at all (Fix-Runde, Befund 1).
                tab: 'services',
                satisfied: filled(app(SecretVault::class)->get('inbound_mail.password')),
            ),
            new Check(
                key: 'delivery.mail_lanes_moving',
                group: self::GROUP,
                severity: Check::SEVERITY_BLOCKING,
                label: __('readiness.delivery.mail_lanes_moving'),
                breaks: __('readiness.delivery.mail_lanes_moving_breaks'),
                // Eigener Wert, keiner der bestehenden ('mail' zeigt auf
                // admin/mail — Server, Postfächer, Zwecke; die Spuren liegen
                // seit Task 5 auf der eigenen Seite admin/mail-pace). Der
                // ReadinessPageTest hält jeden tab-Wert gegen die Stelle, an
                // der sein Feld wirklich liegt — 'mail' wäre hier derselbe
                // falsche Verweis, den jene Prüfung an neun anderen Stellen
                // schon einmal gefangen hat.
                tab: 'mail-pace',
                // Eine Spur, die STEHT, sieht von außen aus wie eine, die
                // LEER ist — `Queue::size()` (Task 5, admin/mail-pace) meldet
                // in beiden Fällen dieselbe Art Zahl und unterscheidet sie
                // nicht. Das Alter des ältesten wartenden Auftrags trennt die
                // beiden: eine leere Spur hat keinen, eine verstopfte hat
                // einen, der immer älter wird.
                //
                // `connection('redis')` ausdrücklich, nicht die
                // Standardverbindung: dieselbe, mit der der Arbeiter in
                // docker-compose.yml tatsächlich gestartet wird
                // (`queue:work redis --queue=mail-direkt,mail-wichtig,
                // mail-ruhig,default`) — unabhängig davon, worauf
                // QUEUE_CONNECTION gerade zeigt. Unter der Testsuite zwingt
                // phpunit.xml die STANDARDverbindung auf `sync`
                // (SyncQueue::size() liefert dort immer 0, egal ob leer oder
                // verstopft — siehe die Anmerkung auf admin/mail-pace); die
                // NAMENTLICH angeforderte `redis`-Verbindung bleibt davon
                // unberührt und bleibt echtes Redis, in beiden Umgebungen
                // erreichbar (siehe tests/Feature/Admin/HostTerminalTest.php).
                // `creationTimeOfOldestPendingJob()` ist Teil des
                // Queue-Vertrags selbst (Illuminate\Contracts\Queue\Queue) —
                // jeder Treiber implementiert es, `sync` eingeschlossen, und
                // liefert dort ehrlich `null` statt einer erfundenen Zahl.
                satisfied: self::laneMoving(MailLane::DIRECT)
                    && self::laneMoving(MailLane::URGENT)
                    && self::laneMoving(MailLane::CALM),
            ),
        ];
    }

    /**
     * Ob der älteste wartende Auftrag einer Spur jünger als die Grenze ist.
     *
     * Zählt nur die unverzögerte Liste (`creationTimeOfOldestPendingJob`
     * schließt `:delayed` ausdrücklich aus) — ein von der Drossel
     * zurückgelegter Auftrag dort ist die Drossel bei der Arbeit, kein
     * Klemmen. Was hier erkannt wird, ist der Fall, in dem niemand mehr
     * abholt: der Arbeiter tot, oder die Spur aus seiner --queue-Liste
     * gefallen — dann wächst die unverzögerte Liste, weil neue Mail dort
     * ungedrosselt eintrifft (`RidesALane::queue()` reiht ohne Verzögerung
     * ein) und nie wieder verschwindet.
     */
    private static function laneMoving(string $lane): bool
    {
        try {
            $oldest = Queue::connection('redis')->creationTimeOfOldestPendingJob($lane);
        } catch (\Throwable) {
            // Wie OperationChecks::isFresh(): eine Seite, deren einziger
            // Zweck ist, ruhig zu melden, was fehlt, darf nicht selbst
            // ausfallen, weil Redis gerade nicht erreichbar ist. Nicht
            // erreichbar heißt nicht erfüllt — nicht "wirft".
            return false;
        }

        if ($oldest === null) {
            // Noch nie etwas eingereiht, oder gerade leer geräumt — beides
            // ist gesund. Kein Auftrag heißt: nichts, das stehen könnte.
            return true;
        }

        return $oldest > now()->subMinutes(self::STUCK_AFTER_MINUTES)->getTimestamp();
    }
}
