<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

/**
 * Die neue Leiter.
 *
 * Die alte war entworfen, bevor jemand ausgerechnet hatte, was ein Server
 * trägt: ein Host mit 456 GB und 15 % Reserve vergibt 388 GB, und darauf
 * passen bei 120 GB Platte je Startkunde genau drei Kunden — während 64 GB RAM
 * acht bis sechzehn getragen hätten. Sechs RAM-Plätze verfielen. Neun Kunden à
 * 40 GB lasten beides gleichzeitig aus und bringen 351 € statt 147 € je Server.
 *
 * **Warum eine Migration und kein Befehl.** Der Katalog wird von einer
 * Migration gesetzt (`…_create_plan_catalogue_tables`). Ein Befehl liefe nur
 * dort, wo jemand ihn aufruft — jede Neuinstallation und die gesamte Testsuite
 * blieben auf einer Leiter stehen, die nicht mehr verkauft wird.
 *
 * **Warum die Werte als Zahlen dastehen** und nicht aus der Konfiguration
 * kommen: aus demselben Grund, aus dem die setzende Migration sie eingetragen
 * hat. Eine Migration muss in fünf Jahren dieselben Zeilen schreiben wie heute.
 * Zöge sie ihre Zahlen aus `config`, schriebe sie beim nächsten Deploy etwas
 * anderes — und niemand könnte am Datenbestand ablesen, was am 1. August 2026
 * beschlossen wurde. Nicht kaufmännisch sind nur zwei Dinge, und die werden
 * deshalb übernommen statt eingetippt: `template_vmid` (wovon geklont wird) und
 * die Währung (worin diese Installation handelt).
 *
 * **Warum Beenden und Nachfolgen statt Ändern.** Eine veröffentlichte Version
 * ist unveränderlich — `PlanVersion` verbietet es, und zwar zu Recht: ein
 * Kunde hat eine Version gekauft, keinen Namen. Umgeschaltet wird deshalb durch
 * Handreichung: die laufende Version bekommt ein `available_until`, die neue
 * ein `available_from` auf dieselbe Sekunde. Das Fenster ist halboffen, also
 * überschneidet sich nichts, es entsteht keine Lücke, und es gibt keinen Job,
 * der laufen müsste.
 *
 * Laufende Verträge merken davon nichts — sie tragen ihren eigenen
 * eingefrorenen Schnappschuss, und seit dem Katalogumbau liest kein Vertrag
 * mehr den Katalog.
 */
return new class extends Migration
{
    /** @var array<string, array<string, int|string>> */
    private const LADDER = [
        'test' => ['quota_gb' => 5, 'disk_gb' => 20, 'ram_mb' => 4096, 'cores' => 2, 'seats' => 3,
            'traffic_gb' => 1000, 'performance' => 'standard', 'price_cents' => 100, 'free_months' => 2,
            'features' => '["managed_updates","daily_backups","monitoring","subdomain","custom_domain"]'],
        'start' => ['quota_gb' => 30, 'disk_gb' => 40, 'ram_mb' => 6144, 'cores' => 2, 'seats' => 10,
            'traffic_gb' => 1000, 'performance' => 'standard', 'price_cents' => 3900, 'free_months' => 2,
            'features' => '["managed_updates","daily_backups","monitoring","subdomain"]'],
        'team' => ['quota_gb' => 85, 'disk_gb' => 100, 'ram_mb' => 8192, 'cores' => 4, 'seats' => 25,
            'traffic_gb' => 2000, 'performance' => 'enhanced', 'price_cents' => 7900, 'free_months' => 2,
            'features' => '["managed_updates","daily_backups","monitoring","subdomain","office","branding","priority_support"]'],
        'business' => ['quota_gb' => 175, 'disk_gb' => 200, 'ram_mb' => 12288, 'cores' => 6, 'seats' => 50,
            'traffic_gb' => 4000, 'performance' => 'high', 'price_cents' => 13900, 'free_months' => 2,
            'features' => '["managed_updates","daily_backups","monitoring","custom_domain","office","branding","priority_support","extended_retention","audit_log"]'],
    ];

    /**
     * Das interne Paket — die einzige Familie, die diese Migration anlegen darf.
     *
     * Sie hat bisher in keiner Migration gestanden: der Betreiber hat sie von
     * Hand über die Konsole angelegt, weshalb sie auf jeder Neuinstallation und
     * in der Testsuite fehlte. Sie gehört in den Katalog, den eine frische
     * Installation mitbringt, denn ohne sie kann niemand eine Abnahme fahren,
     * ohne vorher ein Paket zu erfinden. Der Schlüssel bleibt `test` — das
     * Modell verbietet Umbenennen, und Bestellungen verweisen darauf; nur der
     * Anzeigename wird zu „Intern".
     */
    private const INTERNAL_FAMILY = [
        'key' => 'test',
        'name' => 'Intern',
        'tier' => 0,
        'audience' => 'Nur für interne Tests',
        'note' => 'Nicht im Verkauf. Wird von Hand vergeben.',
    ];

    public function up(): void
    {
        $now = now();

        DB::transaction(function () use ($now) {
            $this->settleInternalFamily($now);

            foreach (self::LADDER as $key => $spec) {
                $family = DB::table('plan_families')->where('key', $key)->first();

                // Eine Familie, die es nicht gibt, wird nicht erfunden: dann ist
                // dieser Bestand anders aufgebaut, und das gehört gesehen.
                if ($family === null) {
                    continue;
                }

                $current = DB::table('plan_versions')
                    ->where('plan_family_id', $family->id)
                    ->whereNotNull('published_at')
                    ->where('available_from', '<=', $now)
                    ->where(fn ($q) => $q->whereNull('available_until')->orWhere('available_until', '>', $now))
                    ->orderByDesc('version')
                    ->first();

                // Schon umgeschaltet (die laufende Version trägt die neuen
                // Zahlen): nichts zu tun, und wiederholbar bleibt es damit auch.
                // Geprüft an dem, was die Version TRÄGT, nicht an ihrer Nummer —
                // eine Nummer sagt nur, wie oft jemand etwas veröffentlicht hat.
                //
                // Geprüft werden ALLE neun Vorgaben, nicht nur Kontingent und
                // Platte: bei Intern waren genau diese beiden schon vorher
                // richtig (5 GB / 20 GB, von Hand über die Konsole angelegt),
                // eine Prüfung auf nur zwei Felder hielt das Paket deshalb für
                // erledigt und ließ RAM, Kerne und Plätze auf ihren alten Werten
                // stehen.
                if ($current !== null && $this->matchesLadder($current, $spec)) {
                    continue;
                }

                if ($current !== null) {
                    DB::table('plan_versions')->where('id', $current->id)
                        ->update(['available_until' => $now, 'updated_at' => $now]);
                }

                $next = (int) DB::table('plan_versions')->where('plan_family_id', $family->id)->max('version') + 1;

                $versionId = DB::table('plan_versions')->insertGetId([
                    'uuid' => (string) Str::uuid(),
                    'plan_family_id' => $family->id,
                    'version' => $next,
                    'quota_gb' => $spec['quota_gb'],
                    'traffic_gb' => $spec['traffic_gb'],
                    'seats' => $spec['seats'],
                    'free_months' => $spec['free_months'],
                    'ram_mb' => $spec['ram_mb'],
                    'cores' => $spec['cores'],
                    'disk_gb' => $spec['disk_gb'],
                    'performance' => $spec['performance'],
                    // Übernommen, nicht gesetzt: wovon geklont wird, ist keine
                    // kaufmännische Entscheidung dieser Umschaltung. 9000 nur,
                    // wenn es nichts zu übernehmen gibt — sonst ginge eine
                    // Version ohne Vorlage in den Verkauf, und die Bereitstellung
                    // bräche bei CloneVirtualMachine, nachdem der Kunde gezahlt
                    // hat.
                    'template_vmid' => $current->template_vmid ?? 9000,
                    'features' => $spec['features'],
                    'available_from' => $now,
                    'available_until' => null,
                    'published_at' => $now,
                    'created_at' => $now,
                    'updated_at' => $now,
                ]);

                // Der Jahrespreis wird gerechnet, nicht getippt: zwölf Monate
                // minus die freien, zum Monatspreis. Zwei Zahlen für dieselbe
                // Sache driften auseinander, und die Zeile "zwei Monate frei"
                // muss stimmen, was auch immer Stripe abbucht.
                $yearly = (int) $spec['price_cents'] * (12 - (int) $spec['free_months']);

                foreach ([['monthly', (int) $spec['price_cents']], ['yearly', $yearly]] as [$term, $amount]) {
                    DB::table('plan_prices')->insert([
                        'uuid' => (string) Str::uuid(),
                        'plan_version_id' => $versionId,
                        'term' => $term,
                        'amount_cents' => $amount,
                        // Übernommen wie die Vorlage: in welcher Währung diese
                        // Installation handelt, entscheidet nicht diese
                        // Migration. Stünde hier fest "EUR", fiele auf einem
                        // Bestand mit anderer Währung jedes Paket aus dem
                        // Verkauf — PlanCatalogue zeigt nur, was in der
                        // Katalogwährung ausgepreist ist.
                        'currency' => $this->currencyOf($current),
                        'stripe_price_id' => null,
                        'created_at' => $now,
                        'updated_at' => $now,
                    ]);
                }
            }

            // Enterprise wird angefragt, nicht gekauft: 2000 GB finden auf 388 GB
            // vergebbarem Platz keinen Host, und der Kunde erführe das nach der
            // Zahlung. Die Familie bleibt samt Version stehen — laufende
            // Verträge verweisen darauf.
            DB::table('plan_families')->where('key', 'enterprise')->update([
                'sales_enabled' => false,
                'updated_at' => $now,
            ]);
        });
    }

    /**
     * Kein Rückweg.
     *
     * Eine veröffentlichte Version zu löschen hieße, Verträge zu löschen, die
     * darauf verweisen — `plan_version_id` liegt auf jedem Vertrag und jeder
     * Bestellung, und `PlanVersion::deleting()` verbietet es aus genau diesem
     * Grund. Wer zurück will, schaltet die alte Version über die Konsole wieder
     * in den Verkauf (`PlanCatalogue::reopen()`); das ist eine kaufmännische
     * Entscheidung und keine, die ein `migrate:rollback` nebenbei trifft.
     */
    public function down(): void {}

    /**
     * Das Testpaket heißt Intern, verlässt den Laden und bleibt verschenkbar.
     *
     * `sales_enabled` bleibt an: es ist der Hebel, der auch das Verschenken
     * abschaltet (`currentVersion()` wirft bei einer nicht verkäuflichen
     * Familie). Aus dem Laden nimmt es `internal`, und genau dafür gibt es das
     * zweite Feld.
     */
    private function settleInternalFamily(Carbon $now): void
    {
        $shared = [
            'name' => self::INTERNAL_FAMILY['name'],
            'internal' => true,
            'audience' => self::INTERNAL_FAMILY['audience'],
            'note' => self::INTERNAL_FAMILY['note'],
            'updated_at' => $now,
        ];

        if (DB::table('plan_families')->where('key', self::INTERNAL_FAMILY['key'])->exists()) {
            DB::table('plan_families')->where('key', self::INTERNAL_FAMILY['key'])->update($shared);

            return;
        }

        DB::table('plan_families')->insert([
            ...$shared,
            'uuid' => (string) Str::uuid(),
            'key' => self::INTERNAL_FAMILY['key'],
            // Rang 0: unter allem, was verkauft wird. Der Rang entscheidet, ob
            // ein Paketwechsel ein Aufstieg ist, und aus dem internen Paket
            // heraus ist jeder Wechsel einer.
            'tier' => self::INTERNAL_FAMILY['tier'],
            'sales_enabled' => true,
            'is_recommended' => false,
            'created_at' => $now,
        ]);
    }

    /**
     * Trägt die laufende Version bereits genau das, was die neue Leiter für
     * diesen Schlüssel vorschreibt — Kontingent, Platte, RAM, Kerne, Plätze,
     * Traffic, Leistungsklasse, Monatspreis und Freimonate?
     *
     * Alle neun, nicht nur zwei: der Defekt, den dieser Vergleich behebt, kam
     * genau daher, dass eine frühere Fassung nur Kontingent und Platte prüfte
     * und ein Paket deshalb für erledigt hielt, dessen übrige Werte gar nicht
     * geprüft wurden.
     */
    private function matchesLadder(object $current, array $spec): bool
    {
        if ((int) $current->quota_gb !== (int) $spec['quota_gb']
            || (int) $current->disk_gb !== (int) $spec['disk_gb']
            || (int) $current->ram_mb !== (int) $spec['ram_mb']
            || (int) $current->cores !== (int) $spec['cores']
            || (int) $current->seats !== (int) $spec['seats']
            || (int) $current->traffic_gb !== (int) $spec['traffic_gb']
            || (string) $current->performance !== (string) $spec['performance']
            || (int) $current->free_months !== (int) $spec['free_months']) {
            return false;
        }

        $monthly = DB::table('plan_prices')
            ->where('plan_version_id', $current->id)
            ->where('term', 'monthly')
            ->value('amount_cents');

        return (int) $monthly === (int) $spec['price_cents'];
    }

    /**
     * Die Währung, in der diese Familie schon ausgepreist ist — sonst die der
     * Installation.
     */
    private function currencyOf(?object $current): string
    {
        $existing = $current === null ? null : DB::table('plan_prices')
            ->where('plan_version_id', $current->id)
            ->value('currency');

        return $existing ?? strtoupper((string) config('provisioning.currency', 'EUR'));
    }
};
