<?php

namespace App\Support\Readiness;

use App\Models\InvoiceSeries;
use App\Models\PlanPrice;
use App\Services\Secrets\SecretVault;
use App\Support\CompanyProfile;
use App\Support\OperatingMode;
use App\Support\StripeCatalogueMode;
use App\Support\StripeWebhookSecret;

/**
 * Was gesetzt sein muss, damit überhaupt ein Auftrag entstehen und ein Beleg
 * herauskommen kann.
 */
final class BillingChecks
{
    public const GROUP = 'billing';

    /** @return array<int, Check> */
    public static function all(): array
    {
        $mode = OperatingMode::current();

        // get() löst den aktiven Modus selbst auf UND ist für Stripe strikt —
        // ein Live-Schlüssel im LIVE-Platz zählt im Testbetrieb nicht als
        // Nachweis, sonst widerspräche diese Seite der Kassensperre.
        $secret = app(SecretVault::class)->get('stripe.secret');

        // Was im Platz des aktiven Modus liegt, kann trotzdem der Schlüssel des
        // anderen Kontos sein — eingetippt, nicht zurückgefallen. Am Präfix
        // ohne Netzverkehr entscheidbar; `null` heißt „nicht entscheidbar" und
        // wird hier bewusst NICHT als Widerspruch gemeldet: diese Prüfung sagt
        // nur, was sie beweisen kann.
        $keyMode = OperatingMode::ofStripeKey($secret);
        $wrongSlot = $keyMode !== null && $keyMode !== $mode;

        // stripe:sync-catalogue (SyncStripeCatalogue::handle()) only ever
        // touches PUBLISHED versions — "a draft has promised nothing, and a
        // Product for it would be a price list entry for something that may
        // never exist" (its own header comment). A draft's own unsynced price
        // is therefore not a gap; counting it anyway turns this check into an
        // alarm that never clears on any installation that has ever drafted a
        // next version, which stops it being read at all.
        $published = PlanPrice::query()
            ->whereHas('version', fn ($query) => $query->whereNotNull('published_at'))
            // Eine interne Familie (`test`, `enterprise`) überspringt
            // stripe:sync-catalogue absichtlich — Produkt UND Preise, im
            // Trockenlauf wie im echten Lauf (Aufgabe 12: ein Stripe-Preis
            // lässt sich nicht löschen, und was nie belastet wird, wird nicht
            // angelegt). Ihre Preiszeilen bleiben deshalb für immer ohne
            // stripe_price_id, ganz gleich wie oft der Abgleich läuft. Ohne
            // diese Ausnahme wäre diese Prüfung auf jeder Installation mit dem
            // internen Testpaket oder Enterprise im Katalog auf Dauer rot —
            // eine Bereitschaftsseite, die niemals grün werden kann, ist so
            // gut wie keine.
            ->whereHas('version.family', fn ($query) => $query->where('internal', false));

        $unsyncedPrices = (clone $published)->whereNull('stripe_price_id')->exists();

        // Eine gefüllte Spalte beweist nur, dass IRGENDEIN Konto diese ID
        // ausgestellt hat. Welches, steht seit dem Abgleich in einer eigenen
        // Zeile (StripeCatalogueMode) — ohne sie meldete diese Prüfung nach
        // dem Umschalten auf Live weiter „erfüllt", und die erste echte
        // Bestellung bekam von Stripe *No such price*. Ohne Netzverkehr
        // entschieden: die Bereitschaftsseite erreicht beim Aufruf nichts.
        //
        // ZWEI Fälle, nicht einer. „Steht fest: anderes Konto" verlangt das
        // Leeren der Zeiger und Register; „Herkunft nie festgehalten" verlangt
        // nur einen neuen Lauf. Der zweite entsteht ohne jeden Kontowechsel —
        // ein abgebrochener Abgleich, oder eine Bestellung, die sich ihren
        // fehlenden Preis selbst geholt hat — und die Leeranweisung wäre dort
        // die Aufforderung, einen Katalog zu löschen, an dem laufende Verträge
        // abgerechnet werden.
        $somethingSynced = (clone $published)->whereNotNull('stripe_price_id')->exists();
        $wrongAccount = $somethingSynced && StripeCatalogueMode::belongsToAnotherMode();
        $unknownOrigin = $somethingSynced && StripeCatalogueMode::recorded() === null;

        return [
            new Check(
                key: 'billing.stripe_secret',
                group: self::GROUP,
                severity: Check::SEVERITY_BLOCKING,
                label: __('readiness.billing.stripe_secret', ['mode' => __('readiness.mode.'.$mode->value)]),
                // Zwei Ausfälle, zwei Folgen — und „ohne ihn nimmt die Kasse
                // keine Bestellung an" wäre über einen Schlüssel, der DA ist,
                // schlicht falsch.
                breaks: $wrongSlot
                    ? __('readiness.billing.stripe_secret_'.$mode->value.'_slot_breaks')
                    : __('readiness.billing.stripe_secret_breaks'),
                tab: 'services',
                satisfied: filled($secret) && ! $wrongSlot,
            ),
            new Check(
                key: 'billing.stripe_webhook_secret',
                group: self::GROUP,
                severity: Check::SEVERITY_BLOCKING,
                label: __('readiness.billing.stripe_webhook_secret'),
                breaks: __('readiness.billing.stripe_webhook_secret_breaks'),
                // Seit dem Umzug in den Tresor ein Feld auf der Zahlungskarte,
                // nicht mehr eine Zeile im Roheditor.
                tab: 'services',
                // Die Auswahl nach Modus liegt schon in StripeWebhookSecret —
                // hier noch einmal per config() nach Modus zu unterscheiden
                // wäre eine zweite Stelle, die dieselbe Entscheidung trifft.
                satisfied: filled(StripeWebhookSecret::current()),
            ),
            new Check(
                key: 'billing.company_details',
                group: self::GROUP,
                severity: Check::SEVERITY_BLOCKING,
                label: __('readiness.billing.company_details'),
                breaks: __('readiness.billing.company_details_breaks'),
                tab: 'company',
                // Die vorhandene Liste wird gerufen, nicht verdoppelt: zwei
                // Quellen für eine Frage ist der Weg, auf dem sie auseinanderlaufen.
                satisfied: CompanyProfile::missingForInvoicing() === [],
            ),
            new Check(
                key: 'billing.invoice_series',
                group: self::GROUP,
                severity: Check::SEVERITY_BLOCKING,
                label: __('readiness.billing.invoice_series'),
                breaks: __('readiness.billing.invoice_series_breaks'),
                tab: 'company',
                // IssueInvoice draws a series with `where('kind', $kind)
                // ->where('active', true)` (IssueInvoice.php:373, :444) — a
                // deactivated row is invisible to it exactly like a missing
                // one, so the same `active` filter belongs here too. Without
                // it, an installation with a switched-off 'invoice' series
                // still counted as ready, the false-green this page exists to
                // rule out.
                satisfied: InvoiceSeries::query()
                    ->whereIn('kind', ['invoice', 'credit_note', 'cancellation'])
                    ->where('active', true)
                    ->distinct()
                    ->count('kind') === 3,
            ),
            new Check(
                key: 'billing.catalogue_synced',
                group: self::GROUP,
                severity: Check::SEVERITY_BLOCKING,
                label: __('readiness.billing.catalogue_synced'),
                // Der bewiesene Widerspruch zuerst: „noch nicht abgeglichen"
                // wäre über einen Katalog, der abgeglichen IST — nur im anderen
                // Konto — die falsche Anweisung, und es ist die teure Richtung
                // (der Betreiber ruft den Befehl, bekommt „already in step"
                // und glaubt, es sei erledigt). Danach die unbekannte Herkunft,
                // deren Kur ein Lauf ist und kein Löschen. Zuletzt die Lücke.
                breaks: match (true) {
                    $wrongAccount => __('readiness.billing.catalogue_account_breaks', [
                        'mode' => __('readiness.mode.'.$mode->value),
                    ]),
                    $unknownOrigin => __('readiness.billing.catalogue_origin_breaks'),
                    default => __('readiness.billing.catalogue_synced_breaks'),
                },
                tab: 'services',
                satisfied: ! $unsyncedPrices && ! $wrongAccount && ! $unknownOrigin,
            ),
        ];
    }
}
