<?php

namespace Modules\Payment\Actions;

use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Modules\Booking\Enums\BookingStatus;
use Modules\Booking\Models\Booking;
use Modules\Payment\Data\PaymentRequestData;
use Modules\Payment\Enums\PaymentMethod;
use Modules\Payment\Enums\PaymentStatus;
use Modules\Payment\Exceptions\PaymentInitiationNotAllowedException;
use Modules\Payment\Models\Payment;
use Modules\Payment\Services\PaymentService;

/**
 * Starts a payment attempt for a booking — calls the resolved gateway via
 * PaymentService, then persists the attempt as a `payments` row regardless
 * of outcome (a failed precreate is still a recorded attempt, domain.md §6).
 *
 * Booking status only ever flips to `confirmed` once the gateway confirms
 * success via the webhook/verify path (T5.9/T5.10) — never here.
 *
 * Idempotent per booking: KBZ's precreate rejects a second call tied to an
 * order that's still in flight, so a repeat call (double-tap on "Pay", the
 * customer re-opening the payment screen) must not blindly precreate again.
 * If the latest attempt is still `pending`, it's re-verified against the
 * gateway (via ConfirmPaymentAction, the same logic the webhook path uses)
 * and reused instead of starting a new one.
 */
class InitiatePaymentAction
{
    private const CURRENCY = 'MMK';

    public function __construct(
        private PaymentService $paymentService,
        private ConfirmPaymentAction $confirmPayment,
    ) {}

    public function handle(Booking $booking, PaymentMethod $method = PaymentMethod::KbzMiniApp): Payment
    {
        if ($booking->status !== BookingStatus::PendingPayment) {
            throw PaymentInitiationNotAllowedException::notPendingPayment($booking);
        }

        if ($booking->is_return_leg) {
            throw PaymentInitiationNotAllowedException::isReturnLeg($booking);
        }

        return DB::transaction(function () use ($booking, $method) {
            $booking = Booking::whereKey($booking->id)->lockForUpdate()->first();

            $latest = $booking->payments()->latest('id')->first();

            if ($latest !== null) {
                // No-op for an already-terminal payment (ConfirmPaymentAction
                // only re-verifies `pending` ones), so this is cheap even for
                // a Failed/Completed latest attempt — and it guards against a
                // narrow race where a webhook already completed the payment
                // but the queued booking-status listener hasn't run yet.
                $reverified = $this->confirmPayment->handle($latest->gateway, $latest->gateway_transaction_id);

                if ($reverified !== null && $reverified->status !== PaymentStatus::Failed) {
                    return $reverified;
                }
            }

            $merchantOrderId = $this->merchantOrderId($booking);
            $amount = $this->amount($booking);

            $result = $this->paymentService->initiate(new PaymentRequestData(
                bookingId: $booking->id,
                merchantOrderId: $merchantOrderId,
                amount: $amount,
                currency: self::CURRENCY,
                method: $method,
                notifyUrl: $this->notifyUrl($booking, $method),
            ));

            return Payment::create([
                'booking_id' => $booking->id,
                'gateway' => $method,
                'status' => $result->status,
                'amount' => $amount,
                'currency' => self::CURRENCY,
                'gateway_transaction_id' => $result->gatewayTransactionId ?? $merchantOrderId,
                'gateway_payload' => $result->gatewayPayload,
                'initiated_at' => now(),
            ]);
        });
    }

    /**
     * A round trip's payment is combined on the outbound leg — covers both
     * legs' price, since the return leg never gets its own Payment
     * (domain.md §2b). A plain one-way booking just pays its own price.
     */
    private function amount(Booking $booking): string
    {
        if ($booking->linked_booking_id === null) {
            return (string) $booking->price;
        }

        return bcadd((string) $booking->price, (string) $booking->linkedBooking->price, 2);
    }

    /**
     * A booking can have more than one payment attempt (retry after
     * failure), so the merchant order id must be unique per attempt, not
     * just per booking — suffixed with the attempt number.
     */
    private function merchantOrderId(Booking $booking): string
    {
        $attempt = $booking->payments()->count() + 1;

        return "{$booking->booking_ref}-{$attempt}";
    }

    /**
     * Embeds the booking id (encrypted, so the URL doesn't leak a raw
     * sequential id) as an optional path segment on the webhook URL —
     * mirrors bnf_event's `{encryptOrderId?}` on `paymentComplete`, giving
     * the webhook a direct way to locate the booking as a redundant check
     * alongside `merch_order_id` in the signed payload. Uses Laravel's
     * Crypt facade rather than porting bnf_event's hand-rolled openssl
     * helper (BNFEventEncryption) — same idea, standard implementation.
     */
    private function notifyUrl(Booking $booking, PaymentMethod $method): string
    {
        return route('payment.webhooks.handle', [
            'method' => $method->value,
            'encryptBookingId' => Crypt::encryptString((string) $booking->id),
        ]);
    }
}
