<?php

namespace App\Livewire\Admin;

use App\Actions\GrantAddon;
use App\Actions\GrantSubscription;
use App\Models\Customer;
use App\Models\Datacenter;
use App\Models\Subscription;
use App\Models\SubscriptionAddon;
use App\Services\Billing\AddonCatalogue;
use App\Services\Billing\PlanCatalogue;
use App\Support\Money;
use Illuminate\Support\Carbon;
use Illuminate\Validation\Rule;
use LivewireUI\Modal\ModalComponent;
use RuntimeException;
use Throwable;

/**
 * Gives a customer a package, a module, or a discount — without a payment.
 *
 * Reuses GrantSubscription / GrantAddon for the actual mutation: this
 * component is only the form. What it grants goes through the exact Order →
 * Subscription/SubscriptionAddon rows a Stripe purchase would, so everything
 * downstream (provisioning, quotas, cancellation, revenue) reads it the same
 * way as a sale.
 */
class GrantPlan extends ModalComponent
{
    public string $customerUuid = '';

    public string $customerName = '';

    /** Which form is on screen: a whole package, or a single module. */
    public string $kind = 'package';

    public bool $hasSubscription = false;

    // Package fields.
    public string $plan = '';

    public string $term = Subscription::TERM_MONTHLY;

    public string $datacenter = '';

    public string $bonusQuotaGb = '';

    public string $bonusSeats = '';

    public string $bonusTrafficGb = '';

    // Add-on fields.
    public ?int $subscriptionId = null;

    public string $addonKey = '';

    public int $quantity = 1;

    // Shared.
    public string $priceEuros = '0';

    public string $note = '';

    /** A plain date — "until when", not "until what time". */
    public string $until = '';

    public function mount(string $uuid): void
    {
        $this->authorize('customers.grant_plan'); // modals are reachable without the route middleware
        $customer = Customer::query()->where('uuid', $uuid)->firstOrFail();

        $this->customerUuid = $uuid;
        $this->customerName = (string) $customer->name;
        $this->datacenter = (string) (Datacenter::query()->active()->orderBy('code')->value('code') ?? '');

        $subscription = Subscription::query()
            ->where('customer_id', $customer->id)
            ->where('status', 'active')
            ->latest('id')
            ->first();

        $this->hasSubscription = $subscription !== null;
        $this->subscriptionId = $subscription?->id;
        // A customer with nothing yet can only be given a package — there is
        // no contract to hang a module off.
        $this->kind = $this->hasSubscription ? 'addon' : 'package';
    }

    /** Today's price for whatever is picked, so a discount reads as one. */
    public function cataloguePriceCents(): ?int
    {
        try {
            if ($this->kind === 'package' && $this->plan !== '') {
                return app(PlanCatalogue::class)->currentVersion($this->plan)->priceFor($this->term)?->amount_cents;
            }

            if ($this->kind === 'addon' && $this->addonKey !== '') {
                return app(AddonCatalogue::class)->priceCents($this->addonKey);
            }
        } catch (Throwable) {
            return null;
        }

        return null;
    }

    public function grant(): void
    {
        $this->authorize('customers.grant_plan');

        $operator = auth('operator')->user();
        abort_if($operator === null, 403);

        $customer = Customer::query()->where('uuid', $this->customerUuid)->firstOrFail();

        if ($this->kind === 'package') {
            $this->grantPackage($customer, $operator);
        } else {
            $this->grantAddon($customer, $operator);
        }
    }

    private function grantPackage(Customer $customer, $operator): void
    {
        $priceRule = function (string $attribute, mixed $value, \Closure $fail): void {
            if (Money::toCents((string) $value) === null) {
                $fail(__('admin.grant.price_invalid'));
            }
        };

        $data = $this->validate([
            // grantable(), nicht sellable(): das interne Testpaket ist aus dem
            // Laden, aber genau dieses Formular ist der Weg, auf dem es einen
            // Kunden trotzdem erreicht.
            'plan' => ['required', Rule::in(array_keys(app(PlanCatalogue::class)->grantable()))],
            'term' => ['required', Rule::in([Subscription::TERM_MONTHLY, Subscription::TERM_YEARLY])],
            'datacenter' => ['required', Rule::exists('datacenters', 'code')->where('active', true)],
            'priceEuros' => ['required', 'string', $priceRule],
            'bonusQuotaGb' => 'nullable|integer|min:0|max:1000000',
            'bonusSeats' => 'nullable|integer|min:0|max:100000',
            'bonusTrafficGb' => 'nullable|integer|min:0|max:10000000',
            'note' => 'nullable|string|max:1000',
        ]);

        try {
            app(GrantSubscription::class)(
                customer: $customer,
                grantedBy: $operator,
                plan: $data['plan'],
                term: $data['term'],
                datacenter: $data['datacenter'],
                priceCents: Money::toCents($data['priceEuros']),
                bonus: array_filter([
                    'quota_gb' => $data['bonusQuotaGb'] !== null && $data['bonusQuotaGb'] !== '' ? (int) $data['bonusQuotaGb'] : null,
                    'seats' => $data['bonusSeats'] !== null && $data['bonusSeats'] !== '' ? (int) $data['bonusSeats'] : null,
                    'traffic_gb' => $data['bonusTrafficGb'] !== null && $data['bonusTrafficGb'] !== '' ? (int) $data['bonusTrafficGb'] : null,
                ], fn ($value) => $value !== null),
                note: $data['note'] !== null && $data['note'] !== '' ? $data['note'] : null,
                until: $this->parseUntil($this->until),
            );
        } catch (RuntimeException $e) {
            $this->addError('priceEuros', $e->getMessage());

            return;
        }

        $this->dispatch('notify', message: __('admin.grant.granted'));
        $this->closeModal();
    }

    private function grantAddon(Customer $customer, $operator): void
    {
        $priceRule = function (string $attribute, mixed $value, \Closure $fail): void {
            if (Money::toCents((string) $value) === null) {
                $fail(__('admin.grant.price_invalid'));
            }
        };

        $data = $this->validate([
            'addonKey' => ['required', Rule::in(array_keys((array) config('provisioning.addons')))],
            'quantity' => 'required|integer|min:1|max:100',
            'priceEuros' => ['required', 'string', $priceRule],
            'note' => 'nullable|string|max:1000',
        ]);

        // Re-resolved and re-checked, never trusted from the hydrated
        // property: a forged request must not be able to book a module onto
        // someone else's contract.
        $subscription = Subscription::query()
            ->where('customer_id', $customer->id)
            ->whereKey($this->subscriptionId)
            ->first();

        if ($subscription === null) {
            $this->addError('addonKey', __('admin.grant.no_subscription'));

            return;
        }

        try {
            app(GrantAddon::class)(
                subscription: $subscription,
                grantedBy: $operator,
                addonKey: $data['addonKey'],
                priceCents: Money::toCents($data['priceEuros']),
                quantity: $data['quantity'],
                note: $data['note'] !== null && $data['note'] !== '' ? $data['note'] : null,
                until: $this->parseUntil($this->until),
            );
        } catch (RuntimeException $e) {
            $this->addError('priceEuros', $e->getMessage());

            return;
        }

        $this->dispatch('notify', message: __('admin.grant.granted'));
        $this->closeModal();
    }

    /**
     * A date, read in the operator's own zone — the same pairing LocalTime
     * keeps for a datetime-local field, minus the time of day this form does
     * not ask for. "Until 15 August" means through the end of that day where
     * the operator is sitting, not UTC midnight.
     */
    private function parseUntil(string $value): ?Carbon
    {
        if (trim($value) === '') {
            return null;
        }

        try {
            return Carbon::parse($value, config('app.display_timezone'))->endOfDay()->utc();
        } catch (Throwable) {
            return null;
        }
    }

    /** @return array<int, array<string, mixed>> */
    private function existingGrants(int $customerId): array
    {
        $fromSubscriptions = Subscription::query()
            ->where('customer_id', $customerId)
            ->whereNotNull('granted_at')
            ->with('grantedBy')
            ->get()
            ->map(fn (Subscription $s) => [
                'kind' => 'package',
                'label' => __('billing.plan.'.$s->plan),
                'price_cents' => $s->price_cents,
                'catalogue_price_cents' => $s->catalogue_price_cents,
                'granted_by' => $s->grantedBy?->name ?? '—',
                'granted_at' => $s->granted_at,
                'granted_until' => $s->granted_until,
                'note' => $s->grant_note,
            ]);

        $fromAddons = SubscriptionAddon::query()
            ->whereHas('subscription', fn ($q) => $q->where('customer_id', $customerId))
            ->whereNotNull('granted_at')
            ->with('grantedBy')
            ->get()
            ->map(fn (SubscriptionAddon $a) => [
                'kind' => 'addon',
                'label' => __('billing.addon.'.$a->addon_key.'.name'),
                'price_cents' => $a->price_cents,
                'catalogue_price_cents' => $a->catalogue_price_cents,
                'granted_by' => $a->grantedBy?->name ?? '—',
                'granted_at' => $a->granted_at,
                'granted_until' => $a->granted_until,
                'note' => $a->grant_note,
            ]);

        return $fromSubscriptions->concat($fromAddons)
            ->sortByDesc('granted_at')
            ->values()
            ->all();
    }

    public function render()
    {
        // Read again rather than off the mounted properties: a second grant
        // can have happened while this modal sat open (EditDatacenter's
        // pattern), and the existing-grants list has to show it.
        $customer = Customer::query()->where('uuid', $this->customerUuid)->firstOrFail();

        return view('livewire.admin.grant-plan', [
            'plans' => app(PlanCatalogue::class)->grantable(),
            'addons' => (array) config('provisioning.addons'),
            'datacenters' => Datacenter::query()->active()->orderBy('code')->pluck('name', 'code'),
            'cataloguePriceCents' => $this->cataloguePriceCents(),
            'grants' => $this->existingGrants($customer->id),
        ]);
    }
}
