<?php

declare(strict_types=1);

namespace App\Models;

use App\Enums\BookingStatus;
use App\Enums\BookingType;
use App\Enums\SessionType;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;

class Booking extends Model
{
    protected $guarded = [];

    protected function casts(): array
    {
        return [
            'type' => BookingType::class,
            'session_type' => SessionType::class,
            'status' => BookingStatus::class,
            'starts_at' => 'immutable_datetime',
            'ends_at' => 'immutable_datetime',
            'hold_expires_at' => 'immutable_datetime',
            'confirmed_at' => 'immutable_datetime',
            'completed_at' => 'immutable_datetime',
            'cancelled_at' => 'immutable_datetime',
            'price_total' => 'decimal:2',
            'room_subtotal' => 'decimal:2',
            'addons_subtotal' => 'decimal:2',
            'refund_due' => 'decimal:2',
        ];
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function room(): BelongsTo
    {
        return $this->belongsTo(Room::class);
    }

    /** Set when this booking is a fixed-price product, e.g. the birthday. */
    public function experience(): BelongsTo
    {
        return $this->belongsTo(Experience::class);
    }

    public function slots(): HasMany
    {
        return $this->hasMany(BookingSlot::class);
    }

    public function addons(): HasMany
    {
        return $this->hasMany(BookingAddon::class);
    }

    public function packagePurchase(): BelongsTo
    {
        return $this->belongsTo(PackagePurchase::class);
    }

    public function preferredEngineer(): BelongsTo
    {
        return $this->belongsTo(Engineer::class, 'preferred_engineer_id');
    }

    public function assignedEngineer(): BelongsTo
    {
        return $this->belongsTo(Engineer::class, 'assigned_engineer_id');
    }

    public function payments(): MorphMany
    {
        return $this->morphMany(Payment::class, 'payable');
    }

    /**
     * Bookings that currently occupy their slots.
     *
     * This is the read-time expiry rule, and the reason slot release does not
     * depend on cron. A pending-payment booking holds its slot only while its
     * hold_expires_at is still in the future; the moment it lapses the slot is
     * free to everyone reading availability, whether or not the scheduler has
     * run. Cron only tidies up statuses and sends the release email.
     *
     * $now is bound from PHP rather than using SQL NOW(), so the result never
     * depends on the database session's timezone.
     */
    public function scopeLive(Builder $query, ?CarbonImmutable $now = null): Builder
    {
        $now ??= CarbonImmutable::now('UTC');

        return $query->where(function (Builder $outer) use ($now) {
            $outer
                ->whereIn('status', BookingStatus::unconditionallyHolding())
                ->orWhere(function (Builder $held) use ($now) {
                    $held
                        ->where('status', BookingStatus::PendingPayment->value)
                        ->where('hold_expires_at', '>', $now);
                });
        });
    }

    /** Pending bookings whose hold has run out but which cron has not tidied. */
    public function scopeLapsed(Builder $query, ?CarbonImmutable $now = null): Builder
    {
        $now ??= CarbonImmutable::now('UTC');

        return $query
            ->where('status', BookingStatus::PendingPayment->value)
            ->where('hold_expires_at', '<=', $now);
    }

    public function isHeld(?CarbonImmutable $now = null): bool
    {
        $now ??= CarbonImmutable::now('UTC');

        return $this->status === BookingStatus::PendingPayment
            && $this->hold_expires_at !== null
            && $this->hold_expires_at->greaterThan($now);
    }

    public function isPaidByPackage(): bool
    {
        return $this->package_purchase_id !== null;
    }

    /** Seconds left on the payment countdown, floored at zero. */
    public function holdSecondsRemaining(?CarbonImmutable $now = null): int
    {
        if ($this->hold_expires_at === null) {
            return 0;
        }

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

        return max(0, (int) $now->diffInSeconds($this->hold_expires_at, false));
    }
}
