<?php

declare(strict_types=1);

namespace App\Services\Packages;

use App\Exceptions\PackageUnavailableException;
use App\Models\Package;
use App\Models\PackagePurchase;
use App\Models\User;
use App\Services\Booking\ReferenceGenerator;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;

/**
 * Selling packages, and turning a paid one into spendable balances.
 *
 * The important decision here is when a package becomes usable. Balances are
 * written at the moment of purchase, but the row starts as pending_payment,
 * and PackagePurchase::scopeUsable() requires "active" — so an unpaid package
 * carries its hours without being able to spend them. That keeps the money
 * check in one place rather than scattering "but has it been paid for?"
 * through every balance calculation.
 *
 * The validity clock starts when the money lands, not when the customer
 * clicked buy. A 30 day package that spent three of them waiting for a bank
 * transfer to clear would be 27 days of studio time sold as 30.
 */
class PackageService
{
    public function __construct(
        private readonly ReferenceGenerator $references,
    ) {}

    /**
     * @throws PackageUnavailableException
     */
    public function purchase(
        User $user,
        Package $package,
        bool $asGroup = false,
        int $participants = 1,
        ?CarbonImmutable $now = null,
    ): PackagePurchase {
        $now ??= CarbonImmutable::now('UTC');

        $this->guard($user, $package, $asGroup, $participants, $now);

        $slotsPerHour = 60 / (int) config('studio.slot_minutes');

        return DB::transaction(fn () => PackagePurchase::create([
            'reference' => $this->references->forPackagePurchase($user, $now),
            'user_id' => $user->id,
            'package_id' => $package->id,
            'price_paid' => $this->priceFor($package, $asGroup),

            // Snapshotted, never re-read from the package. Repricing or
            // re-speccing a package next year must not change what somebody
            // already bought.
            'studio_hours_granted' => $package->studio_hours,
            'engineer_hours_granted' => $package->engineer_hours,
            'mixmaster_credits_granted' => $package->mixmaster_credits,
            'skips_lead_time' => $package->skips_lead_time,
            'staff_per_session' => $package->staff_per_session,

            'studio_slots_remaining' => (int) ($package->studio_hours * $slotsPerHour),
            'engineer_slots_remaining' => (int) ($package->engineer_hours * $slotsPerHour),
            'mixmaster_credits_remaining' => $package->mixmaster_credits,

            'participants' => $asGroup ? $participants : 1,

            // Not usable until paid — see the class comment.
            'status' => 'pending_payment',
            'activated_at' => null,
            'expires_at' => null,
        ]));
    }

    /**
     * Turn a paid purchase into a usable one and start its clock.
     *
     * Idempotent: a retried webhook and a reconciliation sweep can both land
     * here, and the second must not push the expiry date further out.
     */
    public function activate(PackagePurchase $purchase, ?CarbonImmutable $now = null): PackagePurchase
    {
        if ($purchase->status === 'active') {
            return $purchase;
        }

        $now ??= CarbonImmutable::now('UTC');

        $purchase->update([
            'status' => 'active',
            'activated_at' => $now,
            'expires_at' => $now->addDays($purchase->package->validity_days),
        ]);

        return $purchase->refresh();
    }

    public function priceFor(Package $package, bool $asGroup = false): string
    {
        $price = (string) $package->price;

        if ($asGroup && $package->allows_group && $package->group_surcharge !== null) {
            return bcadd($price, (string) $package->group_surcharge, 2);
        }

        return $price;
    }

    /**
     * @throws PackageUnavailableException
     */
    private function guard(
        User $user,
        Package $package,
        bool $asGroup,
        int $participants,
        CarbonImmutable $now,
    ): void {
        if (! $package->is_active) {
            throw PackageUnavailableException::because("{$package->name} is not currently on sale.");
        }

        // Young Creators is 16 to 25. Enforced against the stored date of
        // birth rather than trusted to the customer, because the studio finds
        // out at the door otherwise.
        if ($package->hasAgeLimit()) {
            $age = $user->profile?->age($now->setTimezone(config('studio.timezone')));

            if ($age === null) {
                throw PackageUnavailableException::because(
                    "{$package->name} has an age limit, so we need your date of birth first."
                );
            }

            if (! $package->acceptsAge($age)) {
                throw PackageUnavailableException::because(
                    $package->min_age && $package->max_age
                        ? "{$package->name} is for ages {$package->min_age} to {$package->max_age}."
                        : "You are not eligible for {$package->name}."
                );
            }
        }

        if ($asGroup) {
            if (! $package->allows_group) {
                throw PackageUnavailableException::because("{$package->name} cannot be shared.");
            }

            if ($participants < 2 || $participants > $package->max_participants) {
                throw PackageUnavailableException::because(
                    "The group version takes 2 to {$package->max_participants} people."
                );
            }
        }
    }
}
