<?php
/**
 * Copyright © Simplyin. All rights reserved.
 */
declare(strict_types=1);

namespace Simplyin\Payment\Controller\Webhook;

use Magento\Framework\App\CsrfAwareActionInterface;
use Magento\Framework\App\Request\InvalidRequestException;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Sales\Model\OrderFactory;
use Magento\Sales\Api\OrderRepositoryInterface;
use Simplyin\Payment\Model\Auth\WebhookAuthenticator;
use Psr\Log\LoggerInterface;

/**
 * Order update webhook controller
 *
 * Handles order status updates from SimplyIN
 * Webhook format from SimplyIN: { "title": "ORDER_NUMBER", "status": "PAID|REJECTED|PENDING" }
 */
class OrderUpdate implements CsrfAwareActionInterface
{
    /**
     * @var JsonFactory
     */
    private JsonFactory $resultJsonFactory;

    /**
     * @var OrderFactory
     */
    private OrderFactory $orderFactory;

    /**
     * @var OrderRepositoryInterface
     */
    private OrderRepositoryInterface $orderRepository;

    /**
     * @var LoggerInterface
     */
    private LoggerInterface $logger;

    /**
     * @var RequestInterface
     */
    private RequestInterface $request;

    /**
     * @var WebhookAuthenticator
     */
    private WebhookAuthenticator $authenticator;

    /**
     * Constructor
     *
     * @param JsonFactory $resultJsonFactory
     * @param OrderFactory $orderFactory
     * @param OrderRepositoryInterface $orderRepository
     * @param LoggerInterface $logger
     * @param RequestInterface $request
     * @param WebhookAuthenticator $authenticator
     */
    public function __construct(
        JsonFactory $resultJsonFactory,
        OrderFactory $orderFactory,
        OrderRepositoryInterface $orderRepository,
        LoggerInterface $logger,
        RequestInterface $request,
        WebhookAuthenticator $authenticator
    ) {
        $this->resultJsonFactory = $resultJsonFactory;
        $this->orderFactory = $orderFactory;
        $this->orderRepository = $orderRepository;
        $this->logger = $logger;
        $this->request = $request;
        $this->authenticator = $authenticator;
    }

    /**
     * Execute webhook
     *
     * @return \Magento\Framework\Controller\Result\Json
     */
    public function execute(): \Magento\Framework\Controller\Result\Json
    {
        $result = $this->resultJsonFactory->create();

        $this->logger->info('SimplyIN Webhook: Called', [
            'method' => $this->request->getMethod(),
            'ip' => $this->request->getClientIp()
        ]);

        try {
            // Handle GET request for testing webhook accessibility
            if ($this->request->getMethod() === 'GET') {
                return $this->handleGetRequest($result);
            }

            $rawContent = $this->request->getContent();
            $data = json_decode($rawContent, true);

            if (empty($data)) {
                return $this->emptyDataResponse($result);
            }

            if (!$this->authenticator->isAuthentic($this->request)) {
                return $result->setData([
                    'success' => false,
                    'message' => __('Unauthorized')
                ])->setHttpResponseCode(401);
            }

            return $this->processStatusUpdate($result, $data);

        } catch (\Exception $e) {
            $this->logger->error('SimplyIN Webhook Error: ' . $e->getMessage(), [
                'trace' => $e->getTraceAsString()
            ]);

            return $result->setData([
                'success' => false,
                'message' => __('Internal server error')
            ])->setHttpResponseCode(500);
        }
    }

    /**
     * Run the authenticated status-update flow, extracted from execute() to keep it within complexity limits.
     *
     * @param \Magento\Framework\Controller\Result\Json $result
     * @param array $data Decoded webhook payload
     * @return \Magento\Framework\Controller\Result\Json
     */
    private function processStatusUpdate(
        \Magento\Framework\Controller\Result\Json $result,
        array $data
    ): \Magento\Framework\Controller\Result\Json {
        [$title, $status, $orderId, $trackingNumber] = $this->parseWebhookData($data);

        $this->logger->info('SimplyIN Webhook: Processing', [
            'title' => $title,
            'status' => $status,
            'orderId' => $orderId
        ]);

        // Primary source is the ?order= param on the webhook URL the plugin registered with the
        // backend (the backend calls that exact URL back). Body title/orderId are only a fallback.
        // HMAC over the body remains the authenticity guarantee.
        $orderNumber = (string)$this->request->getParam('order');
        if ($orderNumber === '') {
            $orderNumber = (string)$this->extractOrderNumber($title, $orderId);
        }
        if ($orderNumber === '') {
            return $this->missingOrderNumberResponse($result);
        }

        $order = $this->loadOrderByIncrementId($orderNumber);
        $orderError = $this->verifyOrderFound($result, $order, $orderNumber);
        if ($orderError !== null) {
            return $orderError;
        }

        $this->logger->info('SimplyIN Webhook: Order found', [
            'order_id' => $order->getId(),
            'increment_id' => $order->getIncrementId()
        ]);

        // Verify the order was paid via SimplyIN payment method
        $payment = $order->getPayment();
        $paymentError = $this->verifyPaymentMethod($result, $payment, $orderNumber);
        if ($paymentError !== null) {
            return $paymentError;
        }

        // Security: verify customer ownership when the webhook carries an identifier
        $ownershipError = $this->verifyOrderOwnership($result, $order, $data);
        if ($ownershipError !== null) {
            return $ownershipError;
        }

        $this->applyStatusUpdate($order, $payment, $status);

        if ($trackingNumber) {
            $order->addStatusHistoryComment('SimplyIN Tracking Number: ' . $trackingNumber);
        }

        $this->orderRepository->save($order);

        $this->logger->info('SimplyIN Webhook: Order updated successfully', [
            'order_number' => $orderNumber,
            'status' => $status
        ]);

        return $result->setData([
            'success' => true,
            'message' => __('Order updated successfully')
        ]);
    }

    /**
     * Build response for empty/invalid JSON payload
     *
     * @param \Magento\Framework\Controller\Result\Json $result
     * @return \Magento\Framework\Controller\Result\Json
     */
    private function emptyDataResponse(
        \Magento\Framework\Controller\Result\Json $result
    ): \Magento\Framework\Controller\Result\Json {
        $this->logger->warning('SimplyIN Webhook: Empty or invalid JSON data received');
        return $result->setData([
            'success' => false,
            'message' => __('Invalid JSON data')
        ])->setHttpResponseCode(400);
    }

    /**
     * Build response for missing order number
     *
     * @param \Magento\Framework\Controller\Result\Json $result
     * @return \Magento\Framework\Controller\Result\Json
     */
    private function missingOrderNumberResponse(
        \Magento\Framework\Controller\Result\Json $result
    ): \Magento\Framework\Controller\Result\Json {
        $this->logger->warning('SimplyIN Webhook: No order number found in payload');
        return $result->setData([
            'success' => false,
            'message' => __('Order number is required (title or orderId)')
        ])->setHttpResponseCode(400);
    }

    /**
     * Build accessibility response for GET requests
     *
     * @param \Magento\Framework\Controller\Result\Json $result
     * @return \Magento\Framework\Controller\Result\Json
     */
    private function handleGetRequest(
        \Magento\Framework\Controller\Result\Json $result
    ): \Magento\Framework\Controller\Result\Json {
        $this->logger->info('SimplyIN Webhook: GET request - endpoint accessible');
        return $result->setData([
            'success' => true,
            'message' => 'SimplyIN webhook endpoint is accessible',
            'timestamp' => date('Y-m-d H:i:s')
        ]);
    }

    /**
     * Verify the order was found; return error result or null to continue
     *
     * @param \Magento\Framework\Controller\Result\Json $result
     * @param \Magento\Sales\Model\Order|null $order
     * @param string $orderNumber
     * @return \Magento\Framework\Controller\Result\Json|null
     */
    private function verifyOrderFound(
        \Magento\Framework\Controller\Result\Json $result,
        ?\Magento\Sales\Model\Order $order,
        string $orderNumber
    ): ?\Magento\Framework\Controller\Result\Json {
        if (!$order || !$order->getId()) {
            $this->logger->warning('SimplyIN Webhook: Order not found', ['orderNumber' => $orderNumber]);
            return $result->setData([
                'success' => false,
                'message' => __('Order not found: %1', $orderNumber)
            ])->setHttpResponseCode(404);
        }

        return null;
    }

    /**
     * Verify the order was paid via SimplyIN; return error result or null to continue
     *
     * @param \Magento\Framework\Controller\Result\Json $result
     * @param mixed $payment
     * @param string $orderNumber
     * @return \Magento\Framework\Controller\Result\Json|null
     */
    private function verifyPaymentMethod(
        \Magento\Framework\Controller\Result\Json $result,
        $payment,
        string $orderNumber
    ): ?\Magento\Framework\Controller\Result\Json {
        if (!$payment || $payment->getMethod() !== 'simplyin_payment') {
            $this->logger->warning('SimplyIN Webhook: Order not paid via SimplyIN', [
                'order_number' => $orderNumber,
                'payment_method' => $payment ? $payment->getMethod() : 'none'
            ]);
            return $result->setData([
                'success' => false,
                'message' => __('Order payment method mismatch')
            ])->setHttpResponseCode(400);
        }

        return null;
    }

    /**
     * Extract webhook fields from decoded payload
     *
     * @param array $data
     * @return array{0: string|null, 1: string|null, 2: string|null, 3: string|null}
     */
    private function parseWebhookData(array $data): array
    {
        $title = $data['title'] ?? null;
        $status = $data['status'] ?? null;
        $orderId = $data['orderId'] ?? null;
        $trackingNumber = $data['trackingNumber'] ?? null;

        return [$title, $status, $orderId, $trackingNumber];
    }

    /**
     * Apply SimplyIN payment status to the order/payment
     *
     * @param \Magento\Sales\Model\Order $order
     * @param mixed $payment
     * @param string|null $status
     * @return void
     */
    private function applyStatusUpdate(\Magento\Sales\Model\Order $order, $payment, ?string $status): void
    {
        if ($status) {
            $statusUpper = strtoupper($status);

            if ($statusUpper === 'PAID') {
                $order->setState(\Magento\Sales\Model\Order::STATE_PROCESSING);
                $order->setStatus('processing');
                $order->addStatusHistoryComment(
                    __('SimplyIN Payment confirmed. Status: %1', $status)
                );
                if ($payment) {
                    $payment->setAdditionalInformation('simplyin_payment_status', $statusUpper);
                    $payment->setAdditionalInformation('simplyin_webhook_received', date('Y-m-d H:i:s'));
                }
                $this->registerExternalCapture($order, $payment);
            } elseif (in_array($statusUpper, ['REJECTED', 'FAILED', 'CANCELLED', 'CANCELED'], true)) {
                $order->setState(\Magento\Sales\Model\Order::STATE_CANCELED);
                $order->setStatus('canceled');
                $order->addStatusHistoryComment(
                    __('SimplyIN Payment failed/rejected. Status: %1', $status)
                );
                if ($payment) {
                    $payment->setAdditionalInformation('simplyin_payment_status', $statusUpper);
                    $payment->setAdditionalInformation('simplyin_webhook_received', date('Y-m-d H:i:s'));
                }
            } elseif ($statusUpper === 'PENDING') {
                if ($payment) {
                    $payment->setAdditionalInformation('simplyin_payment_status', $statusUpper);
                    $payment->setAdditionalInformation('simplyin_webhook_received', date('Y-m-d H:i:s'));
                }
            }
        }
    }

    /**
     * Register the externally-captured SimplyIN payment so Magento creates an invoice and marks the order paid.
     *
     * The payment is captured on SimplyIN's side; registerCaptureNotification records that capture, creates the
     * invoice, sets total_paid and moves the order to processing. Idempotent via canInvoice() (false once an
     * invoice exists → repeated PAID webhooks won't double-invoice). Any invoice/capture hiccup is logged but
     * never breaks the webhook. The invoice is attached to $order as a related object and is persisted by the
     * orderRepository->save($order) call in processStatusUpdate().
     *
     * @param \Magento\Sales\Model\Order $order
     * @param mixed $payment
     * @return void
     */
    private function registerExternalCapture(\Magento\Sales\Model\Order $order, $payment): void
    {
        if (!$payment || !$order->canInvoice()) {
            return;
        }

        try {
            $txnId = (string)($payment->getAdditionalInformation('simplyin_transaction_id')
                ?: $payment->getAdditionalInformation('simplyin_provider_payment_id') ?: '');
            if ($txnId !== '') {
                $payment->setTransactionId($txnId);
            }
            $payment->setIsTransactionClosed(true);
            $payment->registerCaptureNotification($order->getGrandTotal(), true);
        } catch (\Throwable $e) {
            $this->logger->error(
                'SimplyIN Webhook: invoice/capture registration failed: ' . $e->getMessage(),
                ['exception' => $e]
            );
        }
    }

    /**
     * Extract order number from title or orderId
     *
     * @param string|null $title
     * @param string|null $orderId
     * @return string|null
     */
    private function extractOrderNumber(?string $title, ?string $orderId): ?string
    {
        if ($title) {
            if (preg_match('/Order\s+(\d+)/i', $title, $matches)) {
                return $matches[1];
            }
            if (preg_match('/^(\d+)$/', trim($title), $matches)) {
                return $matches[1];
            }
            if (preg_match('/(\d{6,})/', $title, $matches)) {
                return $matches[1];
            }
            return trim($title);
        }

        return $orderId;
    }

    /**
     * Load order by increment ID, trying with and without leading zeros
     *
     * @param string $orderNumber
     * @return \Magento\Sales\Model\Order|null
     */
    private function loadOrderByIncrementId(string $orderNumber): ?\Magento\Sales\Model\Order
    {
        /** @var \Magento\Sales\Model\Order $order */
        $order = $this->orderFactory->create()->loadByIncrementId($orderNumber);

        if ($order->getId()) {
            return $order;
        }

        $orderNumberNoZeros = ltrim($orderNumber, '0');
        if ($orderNumberNoZeros !== $orderNumber) {
            $order = $this->orderFactory->create()->loadByIncrementId($orderNumberNoZeros);
            if ($order->getId()) {
                return $order;
            }
        }

        return null;
    }

    /**
     * Verify order ownership to prevent order-number enumeration and unauthorized updates.
     *
     * When the webhook payload includes a customerEmail or customerIdentifier,
     * we verify it matches the order's customer email or customer ID.
     * This prevents an attacker from guessing order numbers and modifying orders
     * belonging to other customers.
     *
     * @param \Magento\Framework\Controller\Result\Json $result
     * @param \Magento\Sales\Model\Order $order
     * @param array $data Webhook payload
     * @return \Magento\Framework\Controller\Result\Json|null Error response or null to continue
     */
    private function verifyOrderOwnership(
        \Magento\Framework\Controller\Result\Json $result,
        \Magento\Sales\Model\Order $order,
        array $data
    ): ?\Magento\Framework\Controller\Result\Json {
        $webhookEmail = $data['customerEmail'] ?? $data['email'] ?? $data['shopUserEmail'] ?? null;
        $webhookCustomerId = $data['customerId'] ?? $data['customerIdentifier'] ?? null;

        // No identifier in the payload: ownership relies on the HMAC signature (already
        // validated). Under IP-only fallback, log this case but allow through for backward
        // compatibility.
        if (empty($webhookEmail) && empty($webhookCustomerId)) {
            if (!$this->authenticator->hasSigningSecret()) {
                $this->logger->warning(
                    'SimplyIN Webhook: No customer identifier in payload and no HMAC configured. '
                    . 'Order-number enumeration risk. Configure webhook_secret for production.',
                    ['order_number' => $order->getIncrementId()]
                );
            }
            return null;
        }

        return $this->verifyEmailOwnership($result, $order, $webhookEmail)
            ?? $this->verifyCustomerIdOwnership($result, $order, $webhookCustomerId);
    }

    /**
     * Reject the webhook (403) when it carries a customer email that doesn't match the order.
     *
     * @param \Magento\Framework\Controller\Result\Json $result
     * @param \Magento\Sales\Model\Order $order
     * @param string|null $webhookEmail
     * @return \Magento\Framework\Controller\Result\Json|null
     */
    private function verifyEmailOwnership(
        \Magento\Framework\Controller\Result\Json $result,
        \Magento\Sales\Model\Order $order,
        ?string $webhookEmail
    ): ?\Magento\Framework\Controller\Result\Json {
        if (empty($webhookEmail)) {
            return null;
        }

        $orderEmail = strtolower(trim($order->getCustomerEmail() ?? ''));
        $providedEmail = strtolower(trim($webhookEmail));

        if (empty($orderEmail) || hash_equals($orderEmail, $providedEmail)) {
            return null;
        }

        $this->logger->warning('SimplyIN Webhook: Customer email mismatch', [
            'order_number' => $order->getIncrementId(),
            'order_email_hash' => hash('sha256', $orderEmail),
            'provided_email_hash' => hash('sha256', $providedEmail)
        ]);

        return $result->setData([
            'success' => false,
            'message' => __('Order ownership verification failed')
        ])->setHttpResponseCode(403);
    }

    /**
     * Reject the webhook (403) when it carries a customer ID that doesn't match the order (guests skipped).
     *
     * @param \Magento\Framework\Controller\Result\Json $result
     * @param \Magento\Sales\Model\Order $order
     * @param mixed $webhookCustomerId
     * @return \Magento\Framework\Controller\Result\Json|null
     */
    private function verifyCustomerIdOwnership(
        \Magento\Framework\Controller\Result\Json $result,
        \Magento\Sales\Model\Order $order,
        $webhookCustomerId
    ): ?\Magento\Framework\Controller\Result\Json {
        if (empty($webhookCustomerId) || !$order->getCustomerId()) {
            return null;
        }

        $orderCustomerId = (string) $order->getCustomerId();
        $providedCustomerId = (string) $webhookCustomerId;

        if ($orderCustomerId === $providedCustomerId) {
            return null;
        }

        $this->logger->warning('SimplyIN Webhook: Customer ID mismatch', [
            'order_number' => $order->getIncrementId(),
            'order_customer_id' => $orderCustomerId,
            'provided_customer_id' => $providedCustomerId
        ]);

        return $result->setData([
            'success' => false,
            'message' => __('Order ownership verification failed')
        ])->setHttpResponseCode(403);
    }

    /**
     * Create CSRF validation exception
     *
     * @param RequestInterface $request
     * @return InvalidRequestException|null
     */
    public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException
    {
        return null;
    }

    /**
     * Validate for CSRF
     *
     * @param RequestInterface $request
     * @return bool|null
     */
    public function validateForCsrf(RequestInterface $request): ?bool
    {
        return true;
    }
}
