<?php

namespace App\Livewire\Admin;

use App\Models\DunningCase;
use App\Models\FailedCheckout;
use App\Services\Billing\DunningSchedule;
use App\Support\Settings;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Component;

/**
 * Wo Geld ausbleibt — beide Arten an einer Stelle.
 *
 * 1. **Mahnfälle.** Ein laufender Vertrag, dessen Abbuchung gescheitert ist.
 *    Der Fall wandert durch die Stufen des DunningSchedule bis zur Begleichung
 *    oder zur Sperre.
 * 2. **Geplatzte Käufe.** Bei SEPA oder Sofortüberweisung klärt die Zahlung
 *    erst nach dem Abschluss der Kasse. Klärt sie nicht, entsteht kein Auftrag
 *    — richtig so — und bis zum 31.7.2026 auch sonst nichts. Ein verlorener
 *    Verkauf, von dem niemand hörte.
 *
 * Eine Seite und nicht zwei: beide Male ist die Frage dieselbe — wer schuldet
 * was, und was ist der nächste Schritt. Zwei Seiten hiessen zwei Orte, an denen
 * man nachsehen muss, und einer davon wird vergessen.
 *
 * Hinter `billing.manage`, nicht hinter `hosts.manage`: hier stehen Kundennamen,
 * Beträge und offene Schulden. Die Rolle `Billing` hat genau dieses Recht und
 * sonst nur `console.view` — die Buchhaltung sieht das, ohne Admin zu sein.
 */
#[Layout('layouts.admin')]
class PaymentProblems extends Component
{
    /** @var array<int, int> Die fünf Fristen in Tagen, Stufe 0 bis 4. */
    public array $days = [];

    public int|string $feeFromLevel = 2;

    /** @var array<int, int> Stufe => Gebühr in Cent */
    public array $fees = [];

    public function mount(): void
    {
        abort_unless(Gate::allows('billing.manage'), 403);

        $this->days = DunningSchedule::days();
        $this->feeFromLevel = DunningSchedule::feeFromLevel();
        // In EURO ins Formular, in Cent in die Datenbank.
        //
        // Hier stand Cent, und im Feld stand „500". Wer eine Mahngebühr
        // einträgt, denkt in Euro — und tippt „5". Aus fünf Euro wurden damit
        // fünf Cent, ohne dass irgendetwas widersprochen hätte.
        $this->fees = [
            2 => self::centsToEuro(DunningSchedule::feeCents(2)),
            3 => self::centsToEuro(DunningSchedule::feeCents(3)),
        ];
    }

    /**
     * Fristen und Gebühren speichern.
     *
     * Rückwärts laufende Fristen werden ABGELEHNT, nicht still sortiert. Der
     * Zeitplan selbst sortiert beim Lesen, damit ein Altbestand nichts kaputt
     * macht — aber wer hier 20/5/12 eintippt, hat sich vertan und soll es
     * sehen, statt eine andere Reihenfolge gespeichert zu bekommen als die,
     * die er gelesen hat.
     */
    /** Cent → das, was im Feld stehen soll: „5" statt „5,00", aber „5,50" wenn nötig. */
    private static function centsToEuro(int $cents): string
    {
        return $cents % 100 === 0
            ? (string) intdiv($cents, 100)
            : number_format($cents / 100, 2, ',', '');
    }

    /** Was im Feld steht → Zahl. Komma und Punkt gelten beide als Dezimaltrenner. */
    private static function euroToFloat(string $eingabe): float
    {
        return (float) str_replace(',', '.', trim($eingabe));
    }

    public function saveSchedule(): void
    {
        $this->authorize('billing.manage');

        $this->validate([
            'days' => ['required', 'array', 'size:5'],
            'days.*' => ['required', 'integer', 'min:0', 'max:365'],
            'feeFromLevel' => ['required', 'integer', 'min:1', 'max:3'],
            // Euro, mit Komma oder Punkt, höchstens zwei Nachkommastellen.
            // 1000,00 € ist die Obergrenze — dieselbe wie vorher, nur lesbar.
            'fees.*' => ['required', 'regex:/^\\d{1,4}([.,]\\d{1,2})?$/'],
        ]);

        $tage = array_map('intval', array_values($this->days));

        if ($tage !== array_values(collect($tage)->sort()->all())) {
            $this->addError('days', __('payment_problems.days_out_of_order'));

            return;
        }

        Settings::set(DunningSchedule::DAYS, $tage);
        Settings::set(DunningSchedule::FEE_FROM_LEVEL, (int) $this->feeFromLevel);
        // Gerundet, nicht abgeschnitten: (int) (19.99 * 100) ist in PHP 1998,
        // weil 19.99 als Gleitkommazahl knapp darunter liegt. Ein Cent, der bei
        // jedem Speichern verschwindet, fällt erst nach Monaten auf.
        Settings::set(DunningSchedule::FEES, array_map(
            fn ($euro) => (int) round(self::euroToFloat((string) $euro) * 100),
            $this->fees,
        ));

        $this->dispatch('notify', message: __('payment_problems.schedule_saved'));
    }

    /** ExtendDunningDeadline reicht das zurück (R23). */
    #[On('dunning-deadline-extended')]
    public function onExtended(int $id, int $days, string $note = ''): void
    {
        $this->authorize('billing.manage');

        $case = DunningCase::query()->findOr($id, fn () => abort(404));

        // Ab der Sperre gibt es keine nächste Frist mehr: die Cloud steht, und
        // was jetzt hilft, ist Bezahlen oder das Schliessen von Hand.
        if ($case->next_step_at === null || $case->isSuspended()) {
            $this->addError('days', __('payment_problems.extend_impossible'));

            return;
        }

        $case->update([
            'next_step_at' => $case->next_step_at->copy()->addDays(max(1, $days)),
            'note' => $this->stamp($case->note, $note),
        ]);

        $this->dispatch('notify', message: __('payment_problems.extended'));
    }

    /** CloseDunningCase reicht das zurück (R23). */
    #[On('dunning-case-closed')]
    public function onClosed(int $id, string $note = '', int $days = 0): void
    {
        $this->authorize('billing.manage');

        // Ohne Begründung nicht. Hier hängt Geld daran, das danach niemand
        // mehr eintreibt — und „erledigt" allein beantwortet beim nächsten
        // Nachfragen nichts.
        if (trim($note) === '') {
            $this->addError('note', __('payment_problems.close_needs_reason'));

            return;
        }

        DunningCase::query()->findOr($id, fn () => abort(404))->update([
            'settled_at' => Carbon::now(),
            'note' => $this->stamp(null, $note),
        ]);

        $this->dispatch('notify', message: __('payment_problems.closed'));
    }

    /** Begründung plus Person — wer und warum, nicht nur dass. */
    private function stamp(?string $bisher, string $note): string
    {
        $wer = Auth::guard('operator')->user()?->email ?? 'console';
        $neu = trim($note) !== '' ? trim($note).' — '.$wer : $wer;

        return trim(($bisher ? $bisher."\n" : '').$neu);
    }

    /**
     * Einen geplatzten Kauf als erledigt abhaken.
     *
     * Mit Begründung, nicht nur mit einem Haken: „erledigt" allein ist beim
     * nächsten Nachfragen wertlos — überwiesen? neu bestellt? nicht erreichbar?
     * Wer es war, steht mit dabei.
     */
    #[On('failed-checkout-resolved')]
    public function onResolved(int $id, string $note = ''): void
    {
        $this->authorize('billing.manage');

        $problem = FailedCheckout::query()->findOr($id, fn () => abort(404));

        $begruendung = trim($note);
        $wer = Auth::guard('operator')->user()?->email ?? 'console';

        $problem->update([
            'resolved_at' => Carbon::now(),
            'note' => $begruendung !== '' ? $begruendung.' — '.$wer : $wer,
        ]);

        $this->dispatch('notify', message: __('payment_problems.resolved'));
    }

    public function render()
    {
        return view('livewire.admin.payment-problems', [
            'cases' => DunningCase::query()
                ->whereNull('settled_at')
                ->with('subscription.customer')
                ->orderBy('next_step_at')
                ->get(),
            'failed' => FailedCheckout::query()
                ->whereNull('resolved_at')
                ->orderByDesc('failed_at')
                ->get(),
        ]);
    }
}
