<?php

namespace App\Livewire;

use App\Models\Customer;
use App\Models\Subscription;
use App\Services\Billing\PlanCatalogue;
use App\Services\Billing\SetupFee;
use App\Services\Billing\TaxTreatment;
use App\Services\Provisioning\HostCapacity;
use App\Support\CompanyProfile;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Component;
use Throwable;

/**
 * The order, before it is placed.
 *
 * There was no such page. The order page posted straight to Stripe, so the last
 * thing a customer saw in this product's own design was a grid of packages, and
 * the first thing they saw after pressing was somebody else's payment form
 * asking for a card. What they were buying, what it costs in total, what happens
 * afterwards and what they were agreeing to were never stated in one place.
 *
 * This is that place: one package, one term, every figure that will be charged,
 * and the terms acceptance directly above the button that spends money — which
 * is also where it belongs and where the owner asked for it.
 *
 * ## It decides nothing
 *
 * Every figure here comes from the same three sources the checkout itself uses —
 * the catalogue, TaxTreatment, SetupFee — and none of them is recomputed. A
 * summary that works out its own total is a second answer waiting to disagree
 * with the first, and the one that matters is the one Stripe is handed.
 */
#[Layout('layouts.portal-app')]
class Checkout extends Component
{
    /** Which package. In the address, so this page can be reloaded and shared. */
    #[Url]
    public string $plan = '';

    /** Monthly or yearly — carried from the choice on the order page. */
    #[Url]
    public string $term = Subscription::TERM_MONTHLY;

    public function mount(): void
    {
        if (! in_array($this->term, [Subscription::TERM_MONTHLY, Subscription::TERM_YEARLY], true)) {
            $this->term = Subscription::TERM_MONTHLY;
        }
    }

    public function render()
    {
        $user = Auth::user();
        $customer = $user === null ? null : Customer::query()->where('email', $user->email)->first();

        // Someone who already has a contract does not buy a second one by
        // accident: changing package is a plan change, which prorates and keeps
        // their data. Same rule as the order page, checked again here because a
        // link into this page skips that one.
        $contract = $customer === null ? null : Subscription::query()
            ->where('customer_id', $customer->id)
            ->whereIn('status', ['active', 'past_due'])
            ->first();

        $tax = TaxTreatment::for($customer);
        // NOT called `plan` in the view: the public property of that name is a
        // key, a string, and Livewire merges its properties into the view data —
        // so a package array under the same name is shadowed by it and every
        // `$plan['name']` in the template reads an offset off a string.
        $package = $this->plan === '' ? null : $this->planOf($this->plan);

        return view('livewire.checkout', [
            'package' => $package,
            'contract' => $contract,
            'tax' => $tax,
            'lines' => $package === null ? [] : $this->lines($package, $tax),
            'vat' => rtrim(rtrim(number_format(CompanyProfile::taxRate(), 1, ',', '.'), '0'), ','),
        ]);
    }

    /**
     * The chosen package as the shop describes it, or null when the key names
     * nothing on sale — a URL is a string somebody can retype.
     *
     * @return array<string, mixed>|null
     */
    private function planOf(string $key): ?array
    {
        try {
            $sellable = app(PlanCatalogue::class)->sellable();
        } catch (Throwable) {
            // The catalogue fails loudly by design; a customer's page is not
            // the place to argue about it. No package, one sentence.
            return null;
        }

        if (! array_key_exists($key, $sellable)) {
            return null;
        }

        $plan = $sellable[$key];

        return $plan + [
            'key' => $key,
            'delivery' => app(HostCapacity::class)->deliveryFor((int) ($plan['disk_gb'] ?? 0)),
        ];
    }

    /**
     * What will be charged, line by line, in the order an invoice states it.
     *
     * Net, tax and gross come from TaxTreatment rather than from a
     * multiplication here: a business with a valid EU VAT number outside Austria
     * pays no Austrian tax at all, and a summary that "adds 20 %" would quote
     * that customer a total they will never be charged.
     *
     * @param  array<string, mixed>  $plan
     * @return array<string, mixed>
     */
    private function lines(array $plan, TaxTreatment $tax): array
    {
        $yearly = $this->term === Subscription::TERM_YEARLY;

        $recurringNet = (int) ($yearly ? $plan['yearly_price_cents'] : $plan['price_cents']);
        $recurringGross = $tax->chargeCents($recurringNet);
        $setupGross = SetupFee::chargedCents($tax);
        $setupNet = SetupFee::netCents();

        return [
            'currency' => (string) $plan['currency'],
            'recurring_net' => $recurringNet,
            'recurring_gross' => $recurringGross,
            'setup_net' => $setupNet,
            'setup_gross' => $setupGross,
            // What leaves the account on the first charge: the term plus the
            // one-off. Every figure afterwards is the term alone.
            'today_net' => $recurringNet + $setupNet,
            'today_gross' => $recurringGross + $setupGross,
            'today_tax' => ($recurringGross + $setupGross) - ($recurringNet + $setupNet),
            'yearly' => $yearly,
            'free_months' => (int) ($plan['free_months'] ?? 0),
            // Only on the yearly view, and only when there is something to
            // compare against: twelve monthly charges at the monthly price.
            'twelve_months_gross' => $tax->chargeCents((int) $plan['price_cents']) * 12,
            'per_month_gross' => $yearly ? intdiv($recurringGross, 12) : $recurringGross,
        ];
    }
}
