<?php

namespace App\Actions;

use App\Models\Order;
use App\Models\Subscription;
use App\Models\SubscriptionRecord;
use App\Services\Billing\TaxTreatment;
use Illuminate\Support\Carbon;

/**
 * Writes one row into the proof register.
 *
 * Every commercial event goes through here so that the flat columns are filled
 * the same way each time — an evidence table where the amount means one thing
 * in one row and another in the next is not evidence.
 *
 * Amounts are stated three ways on purpose. Net is what the catalogue and the
 * contract deal in; gross is what was actually charged; the tax between them
 * depends on the customer and on the day, and recomputing it later from a rate
 * that has since changed would produce a different answer to the one on the
 * invoice.
 */
class RecordCommercialEvent
{
    /**
     * @param  array<string, mixed>  $extra  merged into the JSON snapshot
     * @param  array<string, string|null>  $stripe  event / invoice / subscription ids
     */
    public function __invoke(
        string $event,
        Subscription $subscription,
        int $netCents,
        ?Carbon $at = null,
        array $extra = [],
        array $stripe = [],
        ?int $chargedGrossCents = null,
        ?Order $order = null,
        ?string $eventKey = null,
    ): SubscriptionRecord {
        $at ??= now();
        $customer = $subscription->customer;
        $tax = TaxTreatment::for($customer);

        $agreedNet = $netCents;

        // What the till would take from THIS customer for this catalogue figure:
        // the domestic gross for everybody who is charged VAT, the bare net for a
        // reverse-charge business, because that is the Stripe Price their checkout
        // used. Asked of the treatment rather than of a single domestic figure,
        // which is what it used to be — and every reverse-charge sale, charged
        // exactly the amount it should have been, was then reported as a mismatch
        // by the one flag whose job is "somebody must look at this".
        //
        // chargeCents() is the single place the amount taken is formed, and it is
        // what the Stripe Price carries — so this is the figure the charge can
        // honestly be held against.
        $expectedGross = $tax->chargeCents($agreedNet);

        // The flat columns describe the TRANSACTION, not the contract: gross is
        // what was taken from the customer, and net and tax are that gross
        // split by the rate that applied on the day.
        //
        // Split, not subtracted from the agreed price. A discount lowers the
        // taxable amount; it does not create negative VAT, and recording
        // 14900 charged against a 179,00 contract as "minus 30,00 tax" would
        // make the register wrong in exactly the case someone audits.
        if ($chargedGrossCents !== null) {
            $gross = $chargedGrossCents;
            $net = (int) round($gross / (1 + $tax->rate));
        } else {
            // Nothing was charged at this event. The columns still state what the
            // terms are worth, split by this customer's own rate — a
            // reverse-charge contract owes no VAT and must not have any invented
            // for it here.
            $net = $agreedNet;
            $gross = $tax->grossCents($agreedNet);
        }

        $version = $subscription->planVersion;

        return SubscriptionRecord::create([
            'event' => $event,
            // What makes this event "the same one" if it arrives again. Unique
            // where set, so a duplicate delivery collides in the database
            // rather than in a check that two of them can both pass.
            'event_key' => $eventKey,

            'customer_id' => $subscription->customer_id,
            'subscription_id' => $subscription->id,
            // The order this event belongs to — a booked module has its own,
            // and pointing it at the original plan purchase would file every
            // add-on under the wrong transaction.
            'order_id' => $order?->id ?? $subscription->order_id,
            'plan_version_id' => $subscription->plan_version_id,

            // Copied rather than joined: these have to still answer the question
            // after the customer, the plan or the version have gone.
            'customer_name' => $customer?->name,
            'plan_key' => $subscription->plan,
            'plan_version' => $version?->version,
            'term' => $subscription->term,

            'net_cents' => $net,
            'tax_cents' => $gross - $net,
            'gross_cents' => $gross,
            'currency' => $subscription->currency,
            'tax_rate' => round($tax->rate * 100, 2),
            'reverse_charge' => $tax->reverseCharge,

            'stripe_event_id' => $stripe['event'] ?? null,
            'stripe_invoice_id' => $stripe['invoice'] ?? null,
            'stripe_subscription_id' => $stripe['subscription'] ?? null,

            'occurred_at' => $at,

            'snapshot_version' => SubscriptionRecord::SNAPSHOT_VERSION,
            // The long tail: everything nobody has thought to ask about yet.
            // The flat columns above are what gets queried.
            'snapshot' => array_merge([
                'subscription' => $subscription->only(Subscription::FROZEN),
                'plan' => [
                    'key' => $subscription->plan,
                    'version' => $version?->version,
                    'version_id' => $subscription->plan_version_id,
                    'family' => $version?->family?->name,
                    'capabilities' => $version?->capabilities(),
                ],
                'period' => [
                    'start' => $subscription->current_period_start?->toIso8601String(),
                    'end' => $subscription->current_period_end?->toIso8601String(),
                ],
                'addons' => $subscription->addons()->active()->get()
                    ->map(fn ($addon) => [
                        'key' => $addon->addon_key,
                        'price_cents' => $addon->price_cents,
                        'quantity' => $addon->quantity,
                        'booked_at' => $addon->booked_at?->toIso8601String(),
                    ])->all(),
                'customer' => [
                    'name' => $customer?->name,
                    'email' => $customer?->email,
                    'vat_id' => $customer?->vat_id,
                ],
                // Kept even when they agree. A charge that differs from the
                // catalogue plus tax is exactly the thing someone will ask
                // about later, and reconciling it silently would erase the
                // question along with the answer.
                'amounts' => [
                    // What was agreed, beside what was actually taken. The
                    // flat columns state the transaction; this states whether
                    // it came out at the contract price, which is the question
                    // someone asks a year later.
                    'agreed_net_cents' => $agreedNet,
                    'expected_gross_cents' => $expectedGross,
                    // Null where no money moved at this event, and the flag with
                    // it. Stamping the expected figure into the charged column
                    // made a plan change — where the terms move and Stripe raises
                    // its proration invoice afterwards — claim it had been paid
                    // for, and then agree with itself. A row that answers "was
                    // the right amount taken?" with "yes" when nothing was taken
                    // is worse than one that says it does not know.
                    'charged_gross_cents' => $chargedGrossCents,
                    'matches_catalogue' => $chargedGrossCents === null
                        ? null
                        : $chargedGrossCents === $expectedGross,
                ],
            ], $extra),
        ]);
    }
}
