<?php

namespace App\Http\Controllers\Api;

use App\Exceptions\PaymentGatewayException;
use App\Http\Controllers\Controller;
use App\Models\Order;
use App\Services\CapturedPaymentRecoveryService;
use App\Services\InventoryService;
use App\Services\RazorpayService;
use App\Support\PaymentLogger;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class RazorpayWebhookController extends Controller
{
    public function __construct(
        protected RazorpayService $razorpayService,
        protected InventoryService $inventoryService,
        protected CapturedPaymentRecoveryService $capturedPaymentRecovery
    ) {
    }

    /**
     * Handle Razorpay webhook events
     * 
     * @param Request $request
     * @return JsonResponse
     */
    public function handle(Request $request): JsonResponse
    {
        $payload = $request->all();
        $event = $payload['event'] ?? null;
        $entity = $payload['payload']['payment']['entity'] ?? null;

        if (!$event || !$entity) {
            Log::warning('Razorpay webhook: Invalid payload structure', ['payload' => $payload]);
            return response()->json(['error' => 'Invalid payload'], 400);
        }

        // Verify webhook signature (mandatory in production)
        // Try to get from Settings table first, fallback to config/env
        $webhookSecret = \App\Models\Setting::get('payment_razorpay_webhook_secret');
        if (empty($webhookSecret)) {
            $webhookSecret = config('services.razorpay.webhook_secret');
        }
        $isProduction = config('app.env') === 'production';
        
        if ($isProduction && !$webhookSecret) {
            Log::error('Razorpay webhook: Webhook secret not configured in production');
            return response()->json(['error' => 'Webhook not configured'], 500);
        }
        
        if ($webhookSecret) {
            $signature = $request->header('X-Razorpay-Signature');
            if (!$signature) {
                Log::warning('Razorpay webhook: Missing signature header', [
                    'event' => $event,
                ]);
                return response()->json(['error' => 'Missing signature'], 401);
            }
            
            if (!$this->verifyWebhookSignature($request->getContent(), $signature, $webhookSecret)) {
                Log::warning('Razorpay webhook: Signature verification failed', [
                    'event' => $event,
                    'signature' => $signature,
                ]);
                return response()->json(['error' => 'Invalid signature'], 401);
            }
        } elseif ($isProduction) {
            // In production, signature verification is mandatory
            Log::error('Razorpay webhook: Signature verification required but secret not configured');
            return response()->json(['error' => 'Webhook authentication required'], 401);
        }

        try {
            DB::beginTransaction();

            $paymentId = $entity['id'] ?? null;
            $orderId = $entity['order_id'] ?? null;

            if (!$paymentId || !$orderId) {
                Log::warning('Razorpay webhook: Missing payment or order ID', [
                    'event' => $event,
                    'entity' => $entity,
                ]);
                DB::rollBack();
                return response()->json(['error' => 'Missing required data'], 400);
            }

            // Find order by Razorpay order ID
            $order = Order::where('razorpay_order_id', $orderId)->first();

            // Fallback: if the razorpay_order_id was cleared/changed, recover by fetching Razorpay order and mapping via receipt.
            // Receipt is created as: 'order_' . $order->id
            if (!$order) {
                $rzpOrder = $this->razorpayService->getOrderDetails($orderId);
                $receipt = (string) ($rzpOrder['receipt'] ?? '');
                if ($receipt !== '' && preg_match('/^order_(\d+)$/', $receipt, $m) === 1) {
                    $internalOrderId = (int) $m[1];
                    $order = Order::whereKey($internalOrderId)->first();
                    if ($order) {
                        // Self-heal mapping for future webhooks.
                        if (empty($order->razorpay_order_id)) {
                            $order->update(['razorpay_order_id' => $orderId]);
                        }

                        PaymentLogger::info('Razorpay webhook: Order recovered via receipt', [
                            'order_id' => $order->id,
                            'razorpay_order_id' => $orderId,
                            'receipt' => $receipt,
                            'failure_bucket' => 'webhook_order_recovered_via_receipt',
                        ]);
                    }
                }
            }

            if (!$order) {
                PaymentLogger::warning('Razorpay webhook: Order not found', [
                    'razorpay_order_id' => $orderId,
                    'payment_id' => $paymentId,
                    'event' => $event,
                    'failure_bucket' => 'webhook_order_not_found',
                ]);
                Log::warning('Razorpay webhook: Order not found', [
                    'razorpay_order_id' => $orderId,
                    'event' => $event,
                ]);
                DB::rollBack();
                return response()->json(['error' => 'Order not found'], 404);
            }

            // Handle different webhook events
            switch ($event) {
                case 'payment.authorized':
                    $this->handlePaymentAuthorized($order, $entity);
                    break;

                case 'payment.captured':
                    $this->handlePaymentCaptured($order, $entity);
                    break;

                case 'payment.failed':
                    $this->handlePaymentFailed($order, $entity);
                    break;

                case 'payment.refunded':
                case 'refund.created':
                    $this->handlePaymentRefunded($order, $entity);
                    break;

                default:
                    Log::info('Razorpay webhook: Unhandled event', [
                        'event' => $event,
                        'order_id' => $order->id,
                    ]);
            }

            DB::commit();

            return response()->json(['status' => 'success'], 200);
        } catch (\Exception $e) {
            DB::rollBack();
            Log::error('Razorpay webhook: Error processing webhook', [
                'event' => $event,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);

            return response()->json(['error' => 'Internal server error'], 500);
        }
    }

    /**
     * Handle payment authorized event.
     * Optionally performs backup capture when frontend verify did not run (e.g. user closed tab).
     */
    protected function handlePaymentAuthorized(Order $order, array $entity): void
    {
        PaymentLogger::info('Razorpay webhook: Payment authorized', [
            'order_id' => $order->id,
            'payment_id' => $entity['id'] ?? null,
            'razorpay_order_id' => $entity['order_id'] ?? null,
        ]);
        $order = $this->lockOrder($order->id);
        if ($this->isOrderTerminalPaid($order)) {
            return;
        }
        if (!$this->isOrderPayable($order)) {
            return;
        }

        $paymentId = (string) ($entity['id'] ?? '');
        if ($paymentId === '') {
            return;
        }

        if (!$order->razorpay_payment_id) {
            $order->update([
                'razorpay_payment_id' => $paymentId,
                'payment_status' => Order::PAYMENT_PENDING,
            ]);
        }

        if (!$this->hasActiveReservations($order->id)) {
            PaymentLogger::warning('Razorpay webhook: Backup capture skipped (reservations expired or none)', [
                'order_id' => $order->id,
                'payment_id' => $paymentId,
                'failure_bucket' => 'backend_state_race',
            ]);
            return;
        }

        $gatewayState = $this->razorpayService->resolvePaymentState($paymentId);
        if (($gatewayState['status'] ?? RazorpayService::PAYMENT_STATUS_UNKNOWN) === RazorpayService::PAYMENT_STATUS_CAPTURED) {
            $this->finalizeCapturedOrder($order, $paymentId, 'webhook_authorized_pre_captured');
            return;
        }

        try {
            $this->confirmStockReservations($order);
            $this->razorpayService->capturePayment($paymentId, (float) $order->total_payable);
            $this->finalizeCapturedOrder($order, $paymentId, 'webhook_authorized_capture');
        } catch (PaymentGatewayException $e) {
            if ($e->reasonCode() === PaymentGatewayException::CODE_CAPTURE_ALREADY_CAPTURED) {
                $this->finalizeCapturedOrder($order, $paymentId, 'webhook_authorized_already_captured');
                return;
            }

            $this->inventoryService->releaseByOrderId($order->id, true);
            $order->update([
                'payment_status' => Order::PAYMENT_FAILED,
                'status' => Order::STATUS_CANCELLED,
            ]);

            PaymentLogger::error('Razorpay webhook: Backup capture failed', [
                'order_id' => $order->id,
                'payment_id' => $paymentId,
                'error' => $e->getMessage(),
                'reason_code' => $e->reasonCode(),
                'failure_bucket' => 'backend_state_race',
            ]);

            throw $e;
        }
    }

    /**
     * Handle payment captured event
     */
    protected function handlePaymentCaptured(Order $order, array $entity): void
    {
        PaymentLogger::info('Razorpay webhook: Payment captured', [
            'order_id' => $order->id,
            'payment_id' => $entity['id'] ?? null,
            'razorpay_order_id' => $entity['order_id'] ?? null,
        ]);
        $order = $this->lockOrder($order->id);
        if ($this->isOrderTerminalPaid($order)) {
            return;
        }

        $paymentId = (string) ($entity['id'] ?? '');

        try {
            $this->finalizeCapturedOrder($order, $paymentId, 'webhook_captured');
        } catch (\Exception $e) {
            if ($paymentId !== '' && $this->capturedPaymentRecovery->recoverCapturedPayment($order, $paymentId, 'webhook_captured_stock_recovery')) {
                return;
            }

            if ($paymentId !== '' && $this->capturedPaymentRecovery->isGatewayPaymentCaptured($paymentId)) {
                PaymentLogger::error('Razorpay webhook: capture processed but stock confirmation failed', [
                    'order_id' => $order->id,
                    'payment_id' => $paymentId,
                    'error' => $e->getMessage(),
                    'failure_bucket' => 'backend_state_race',
                ]);

                return;
            }

            $this->inventoryService->releaseByOrderId($order->id);
            $order->update([
                'payment_status' => Order::PAYMENT_FAILED,
                'status' => Order::STATUS_CANCELLED,
            ]);

            PaymentLogger::error('Razorpay webhook: capture processed but stock confirmation failed', [
                'order_id' => $order->id,
                'payment_id' => $paymentId ?: null,
                'error' => $e->getMessage(),
                'failure_bucket' => 'backend_state_race',
            ]);
        }
    }

    /**
     * Handle payment failed event
     */
    protected function handlePaymentFailed(Order $order, array $entity): void
    {
        $failureReason = $entity['error_description'] ?? 'Unknown error';
        $failureBucket = $this->classifyFailureBucket($failureReason);
        PaymentLogger::warning('Razorpay webhook: Payment failed', [
            'order_id' => $order->id,
            'payment_id' => $entity['id'] ?? null,
            'razorpay_order_id' => $entity['order_id'] ?? null,
            'error' => $failureReason,
            'failure_bucket' => $failureBucket,
        ]);

        $order = $this->lockOrder($order->id);
        $paymentId = (string) ($entity['id'] ?? '');

        // Ignore stale/late failed events after order is already finalized as paid.
        if ($this->isOrderTerminalPaid($order)) {
            PaymentLogger::info('Razorpay webhook: Ignored failed event for paid order', [
                'order_id' => $order->id,
                'payment_id' => $paymentId ?: null,
                'failure_bucket' => 'backend_state_race',
            ]);
            return;
        }

        // UPI/bank often sends failed first, then captured for the same payment_id.
        if ($paymentId !== '' && $this->capturedPaymentRecovery->recoverCapturedPayment($order, $paymentId, 'webhook_failed_already_captured')) {
            return;
        }

        $order->update([
            'payment_status' => Order::PAYMENT_FAILED,
            'razorpay_payment_id' => $paymentId !== '' ? $paymentId : $order->razorpay_payment_id,
        ]);

        PaymentLogger::info('Razorpay webhook: Payment failed recorded without releasing stock (grace window)', [
            'order_id' => $order->id,
            'payment_id' => $paymentId ?: null,
            'grace_seconds' => config('services.razorpay.failed_webhook_grace_seconds', 300),
            'failure_bucket' => $failureBucket,
        ]);
    }

    /**
     * Handle payment refunded event
     */
    protected function handlePaymentRefunded(Order $order, array $entity): void
    {
        Log::info('Razorpay webhook: Payment refunded', [
            'order_id' => $order->id,
            'payment_id' => $entity['id'],
        ]);

        $order->update([
            'payment_status' => Order::PAYMENT_REFUNDED,
        ]);

        // Optionally release stock or handle refund logic
        // Note: Stock is usually not released on refund as the order was already fulfilled
    }

    /**
     * Confirm stock reservations for an order
     */
    protected function confirmStockReservations(Order $order): void
    {
        $reservations = \App\Models\StockReservation::where('order_id', $order->id)
            ->where('status', 'pending')
            ->where('expires_at', '>', now())
            ->get();

        if ($reservations->isEmpty()) {
            throw new \RuntimeException('No active stock reservations found for captured payment.');
        }

        foreach ($reservations as $reservation) {
            $this->inventoryService->confirmReservation($reservation->reservation_token);
        }
    }

    protected function finalizeCapturedOrder(Order $order, string $paymentId, string $source): void
    {
        if ($paymentId === '') {
            throw new \RuntimeException('Missing payment ID for captured finalization.');
        }

        $order = $this->lockOrder($order->id);
        if ($this->isOrderTerminalPaid($order)) {
            return;
        }

        $recoveredFromNonPayable = !$this->isOrderPayable($order);

        if (!$this->capturedPaymentRecovery->tryConfirmStockForOrder($order)) {
            throw new \RuntimeException('No active stock reservations found for captured payment.');
        }
        $order->update([
            'razorpay_payment_id' => $paymentId,
            'payment_status' => Order::PAYMENT_PAID,
            'status' => Order::STATUS_PLACED,
            'placed_at' => $order->placed_at ?? now(),
        ]);

        PaymentLogger::info('Razorpay webhook: Payment finalized as captured', [
            'order_id' => $order->id,
            'payment_id' => $paymentId,
            'source' => $source,
            'failure_bucket' => $recoveredFromNonPayable ? 'backend_state_race' : null,
        ]);
    }

    protected function isOrderTerminalPaid(Order $order): bool
    {
        return $order->payment_status === Order::PAYMENT_PAID
            && $order->status === Order::STATUS_PLACED;
    }

    protected function isOrderPayable(Order $order): bool
    {
        return $order->status === Order::STATUS_AWAITING_PAYMENT
            && in_array($order->payment_status, [Order::PAYMENT_PENDING, Order::PAYMENT_FAILED], true);
    }

    protected function lockOrder(int $orderId): Order
    {
        return Order::whereKey($orderId)->lockForUpdate()->firstOrFail();
    }

    protected function hasActiveReservations(int $orderId): bool
    {
        return \App\Models\StockReservation::where('order_id', $orderId)
            ->where('status', 'pending')
            ->where('expires_at', '>', now())
            ->exists();
    }

    protected function classifyFailureBucket(string $errorDescription): string
    {
        $message = strtolower($errorDescription);
        if (str_contains($message, 'in time') || str_contains($message, 'delay')) {
            return 'gateway_timeout';
        }

        return 'customer_decline';
    }

    /**
     * Verify webhook signature
     */
    protected function verifyWebhookSignature(string $payload, ?string $signature, string $secret): bool
    {
        if (!$signature) {
            return false;
        }

        $expectedSignature = hash_hmac('sha256', $payload, $secret);

        return hash_equals($expectedSignature, $signature);
    }
}
