<?php

namespace Modules\Payment\Actions;

use Illuminate\Support\Facades\DB;
use Modules\Booking\Enums\BookingStatus;
use Modules\Booking\Models\Booking;
use Modules\Payment\Enums\PaymentStatus;
use Modules\Payment\Enums\RefundStatus;
use Modules\Payment\Events\RefundProcessed;
use Modules\Payment\Exceptions\RefundFailedException;
use Modules\Payment\Exceptions\RefundNotAllowedException;
use Modules\Payment\Models\Payment;
use Modules\Payment\Models\Refund;
use Modules\Payment\Services\PaymentService;

/**
 * Usable from both the API and Filament (T5.13) — resolves the gateway via
 * PaymentService/PaymentGatewayFactory, never a concrete gateway class.
 *
 * Unlike bnf_event (refund amount was effectively hardcoded to full-amount),
 * partial refunds are explicitly supported: `amount` is validated against
 * the Payment's remaining refundable balance, not just its original total
 * (domain.md §6).
 */
class RefundBookingAction
{
    public function __construct(
        private PaymentService $paymentService,
    ) {}

    public function handle(Booking $booking, string $amount, string $reason, ?int $requestedBy = null): Refund
    {
        if ($booking->status !== BookingStatus::Confirmed) {
            throw RefundNotAllowedException::notConfirmed($booking);
        }

        $payment = $this->resolveRefundablePayment($booking);

        if ($payment === null) {
            throw RefundNotAllowedException::noCompletedPayment($booking);
        }

        $this->assertWithinRefundableBalance($payment, $amount);

        $result = $this->paymentService->refund($payment->gateway, $payment->gateway_transaction_id, $amount, $reason);

        $refund = DB::transaction(function () use ($booking, $payment, $amount, $reason, $result, $requestedBy) {
            $refund = Refund::create([
                'payment_id' => $payment->id,
                'booking_id' => $booking->id,
                'status' => $result->status,
                'amount' => $amount,
                'reason' => $reason,
                'gateway_refund_id' => $result->gatewayRefundId,
                'gateway_payload' => $result->gatewayPayload,
                'requested_by' => $requestedBy,
                'requested_at' => now(),
                'completed_at' => now(),
            ]);

            if ($result->status === RefundStatus::Completed) {
                RefundProcessed::dispatch($refund);
            }

            return $refund;
        });

        if ($result->status === RefundStatus::Failed) {
            throw RefundFailedException::fromResult($result);
        }

        return $refund;
    }

    /**
     * The Completed Payment a refund against $booking would apply to.
     * Public so the Filament refund forms can look up the same Payment to
     * surface its refundable balance before staff submit an amount.
     *
     * Round trip: payment is combined on the outbound leg, so a return leg
     * has no Payment of its own — resolve against its linked leg's Payment
     * instead (domain.md §2b). The Confirmed check in handle() still applies
     * to $booking itself, not the payment holder, so each leg remains
     * independently cancellable/refundable.
     */
    public function resolveRefundablePayment(Booking $booking): ?Payment
    {
        $paymentBooking = $booking->is_return_leg ? ($booking->linkedBooking ?? $booking) : $booking;

        return $paymentBooking->payments()->where('status', PaymentStatus::Completed->value)->latest()->first();
    }

    private function assertWithinRefundableBalance(Payment $payment, string $amount): void
    {
        $remaining = $payment->refundableBalance();

        if (bccomp($amount, $remaining, 2) === 1) {
            throw RefundNotAllowedException::exceedsRefundableBalance($payment, $amount, $remaining);
        }
    }
}
