<?php

namespace PortalPeople\OrderWebhook\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\HTTP\Client\Curl;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Store\Model\ScopeInterface;
use Psr\Log\LoggerInterface;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Sales\Model\Order\Status\HistoryFactory;

/**
 * Webhook sender service
 */
class WebhookSender
{
    const CONFIG_PATH_ENABLED = 'order_webhook/general/enabled';
    const CONFIG_PATH_API_URL = 'order_webhook/general/api_url';
    const CONFIG_PATH_API_KEY = 'order_webhook/general/api_key';
    const CONFIG_PATH_TIMEOUT = 'order_webhook/general/timeout';
    const CONFIG_PATH_RETRY_ATTEMPTS = 'order_webhook/general/retry_attempts';
    const CONFIG_PATH_LOG_ENABLED = 'order_webhook/general/log_enabled';
    const CONFIG_PATH_NGAUGE_BASE_URL = 'order_webhook/ngauge/base_url';
    const CONFIG_PATH_COMMENT_ONLY    = 'order_webhook/ngauge/comment_only';

    /**
     * @var ScopeConfigInterface
     */
    private $scopeConfig;

    /**
     * @var Curl
     */
    private $curl;

    /**
     * @var Json
     */
    private $jsonSerializer;

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

    /**
     * @var OrderDataBuilder
     */
    private $orderDataBuilder;

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

    /**
     * @var HistoryFactory
     */
    private $historyFactory;

    /**
     * Re-entrancy guard – prevents observer-triggered saves from firing the webhook again.
     * A single PHP process can only hold one in-flight send at a time.
     *
     * @var bool
     */
    private static bool $sending = false;

    /**
     * @param ScopeConfigInterface $scopeConfig
     * @param Curl $curl
     * @param Json $jsonSerializer
     * @param LoggerInterface $logger
     * @param OrderDataBuilder $orderDataBuilder
     * @param OrderRepositoryInterface $orderRepository
     * @param HistoryFactory $historyFactory
     */
    public function __construct(
        ScopeConfigInterface $scopeConfig,
        Curl $curl,
        Json $jsonSerializer,
        LoggerInterface $logger,
        OrderDataBuilder $orderDataBuilder,
        OrderRepositoryInterface $orderRepository,
        HistoryFactory $historyFactory
    ) {
        $this->scopeConfig = $scopeConfig;
        $this->curl = $curl;
        $this->jsonSerializer = $jsonSerializer;
        $this->logger = $logger;
        $this->orderDataBuilder = $orderDataBuilder;
        $this->orderRepository = $orderRepository;
        $this->historyFactory = $historyFactory;
    }

    /**
     * Send order data to webhook endpoint
     *
     * @param OrderInterface $order
     * @param string $eventType
     * @return bool
     */
    public function sendOrderData(OrderInterface $order, string $eventType): bool
    {
        $incrementId = (string)$order->getIncrementId();
        $storeId = (int)$order->getStoreId();

        // Re-entrancy guard: saveNgaugeResponseFields and addOrderComment both call
        // orderRepository->save(), which re-fires sales_order_save_after. Block any
        // nested call so we never loop-send the webhook.
        if (self::$sending) {
            $this->trace("Blocked re-entrant webhook call for order {$incrementId} ({$eventType})");
            $this->log("Re-entrant webhook call blocked for order {$incrementId}", $storeId);
            return false;
        }

        self::$sending = true;

        try {
            $this->trace("START sendOrderData order={$incrementId} event={$eventType} store={$storeId}");

            if (!$this->isEnabled($storeId)) {
                $this->trace("SKIP sendOrderData order={$incrementId} event={$eventType} reason=disabled");
                return false;
            }

            $apiUrl = $this->getApiUrl($storeId);
            if (empty($apiUrl)) {
                $this->trace("SKIP sendOrderData order={$incrementId} event={$eventType} reason=missing_api_url");
                $this->log('API URL is not configured');
                return false;
            }

            $orderData = $this->orderDataBuilder->build($order, $eventType);
            $jsonData = $this->jsonSerializer->serialize($orderData);

            $retryAttempts = (int)$this->getRetryAttempts($storeId);
            $attempt = 0;
            $lastResponse = [];

            while ($attempt <= $retryAttempts) {
                try {
                    $attemptNumber = $attempt + 1;
                    $this->trace(
                        "POST attempt={$attemptNumber} order={$incrementId} event={$eventType} url={$apiUrl} payload_bytes=" . strlen($jsonData)
                    );
                    $response = $this->sendRequest($apiUrl, $jsonData, $storeId, $incrementId, $eventType, $attemptNumber);

                    if ($response['success']) {
                        $this->trace("SUCCESS order={$incrementId} event={$eventType} attempt={$attemptNumber} http_code=" . $response['http_code']);
                        $this->log("Webhook sent successfully for order {$incrementId} on attempt " . $attemptNumber, $storeId);
                        $commentOnly = $this->scopeConfig->isSetFlag(self::CONFIG_PATH_COMMENT_ONLY, ScopeInterface::SCOPE_STORE, $storeId);
                        $saved = [];
                        if (!$commentOnly) {
                            // Sets NGauge fields on the order object — does NOT save yet
                            $saved = $this->setNgaugeResponseFields($order, $response['response']);
                        }
                        // Attaches history comment to the order object — does NOT save yet
                        $this->attachOrderComment($order, $this->buildSuccessComment($eventType, $response['http_code'], $response['response'], $saved));
                        // Single save for both NGauge fields + comment
                        $this->persistOrder($order);
                        $this->trace("FINISH order={$incrementId} event={$eventType} result=success");
                        return true;
                    }

                    $this->trace("FAIL order={$incrementId} event={$eventType} attempt={$attemptNumber} error=" . $response['error']);
                    $this->log("Webhook failed for order {$incrementId} on attempt " . $attemptNumber . ": " . $response['error'], $storeId);
                    $lastResponse = $response;

                } catch (\Exception $e) {
                    $attemptNumber = $attempt + 1;
                    $this->trace("EXCEPTION order={$incrementId} event={$eventType} attempt={$attemptNumber} message=" . $e->getMessage());
                    $this->log("Exception sending webhook for order {$incrementId} on attempt " . $attemptNumber . ": " . $e->getMessage(), $storeId);
                    $lastResponse = ['http_code' => 0, 'error' => $e->getMessage(), 'response' => ''];
                }

                $attempt++;

                if ($attempt <= $retryAttempts) {
                    sleep(pow(2, $attempt)); // Exponential backoff
                }
            }

            $this->trace("FINISH order={$incrementId} event={$eventType} result=failure attempts=" . ($retryAttempts + 1));
            $this->log("All retry attempts failed for order {$incrementId}", $storeId);
            // Attaches failure comment — does NOT save yet
            $this->attachOrderComment($order, $this->buildFailureComment($eventType, $retryAttempts, $lastResponse ?? []));
            // Single save for the failure comment
            $this->persistOrder($order);
            return false;

        } finally {
            $this->trace("END sendOrderData order={$incrementId} event={$eventType}");
            self::$sending = false;
        }
    }

    /**
     * Send HTTP request to webhook endpoint
     *
     * @param string $url
     * @param string $jsonData
     * @param int $storeId
     * @return array
     */
    private function sendRequest(string $url, string $jsonData, int $storeId, string $incrementId = '', string $eventType = '', int $attemptNumber = 0): array
    {
        $timeout = (int)$this->getTimeout($storeId);
        $apiKeyEncrypted = $this->getApiKey($storeId);
        /** @var \Magento\Framework\Encryption\EncryptorInterface $encryptor */
        $encryptor = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Encryption\EncryptorInterface::class);
        $apiKey = $encryptor->decrypt($apiKeyEncrypted);
        
        $this->curl->setTimeout($timeout);

        // Build headers array and set them once so we don't overwrite Content-Type
        $headers = [
            'Content-Type' => 'application/json',
            'User-Agent' => 'Magento2-OrderWebhook/1.0.0'
        ];

        if (!empty($apiKey)) {
            $headers['Authorization'] = 'Bearer ' . $apiKey;
        }

        $this->curl->setHeaders($headers);

        $this->trace(
            "HTTP POST firing order={$incrementId} event={$eventType} attempt={$attemptNumber} timeout={$timeout}s url={$url}"
        );
        $this->curl->post($url, $jsonData);

        $httpCode = $this->curl->getStatus();
        $response = $this->curl->getBody();
        $this->trace(
            "HTTP POST completed order={$incrementId} event={$eventType} attempt={$attemptNumber} http_code={$httpCode} response_bytes=" . strlen((string)$response)
        );

        if ($httpCode >= 200 && $httpCode < 300) {
            return ['success' => true, 'response' => $response, 'http_code' => $httpCode];
        }

        return [
            'success'   => false,
            'error'     => "HTTP {$httpCode}: {$response}",
            'response'  => $response,
            'http_code' => $httpCode,
        ];
    }

    /**
     * Check if webhook is enabled
     *
     * @param int $storeId
     * @return bool
     */
    private function isEnabled(int $storeId): bool
    {
        return $this->scopeConfig->isSetFlag(
            self::CONFIG_PATH_ENABLED,
            ScopeInterface::SCOPE_STORE,
            $storeId
        );
    }

    /**
     * Get API URL
     *
     * @param int $storeId
     * @return string
     */
    private function getApiUrl(int $storeId): string
    {
        return (string)$this->scopeConfig->getValue(
            self::CONFIG_PATH_API_URL,
            ScopeInterface::SCOPE_STORE,
            $storeId
        );
    }

    /**
     * Get API key
     *
     * @param int $storeId
     * @return string
     */
    private function getApiKey(int $storeId): string
    {
        return (string)$this->scopeConfig->getValue(
            self::CONFIG_PATH_API_KEY,
            ScopeInterface::SCOPE_STORE,
            $storeId
        );
    }

    /**
     * Get timeout value
     *
     * @param int $storeId
     * @return int
     */
    private function getTimeout(int $storeId): int
    {
        return (int)$this->scopeConfig->getValue(
            self::CONFIG_PATH_TIMEOUT,
            ScopeInterface::SCOPE_STORE,
            $storeId
        ) ?: 30;
    }

    /**
     * Get retry attempts
     *
     * @param int $storeId
     * @return int
     */
    private function getRetryAttempts(int $storeId): int
    {
        return (int)$this->scopeConfig->getValue(
            self::CONFIG_PATH_RETRY_ATTEMPTS,
            ScopeInterface::SCOPE_STORE,
            $storeId
        ) ?: 3;
    }

    /**
     * Return configuration details for display (e.g. CLI info command)
     *
     * @param int $storeId
     * @return array
     */
    public function getConfigInfo(int $storeId = 0): array
    {
        $apiKeyEncrypted = $this->getApiKey($storeId);
        $apiKey = '';
        if (!empty($apiKeyEncrypted)) {
            try {
                /** @var \Magento\Framework\Encryption\EncryptorInterface $encryptor */
                $encryptor = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Encryption\EncryptorInterface::class);
                $decrypted = $encryptor->decrypt($apiKeyEncrypted);
                $apiKey = strlen($decrypted) > 4
                    ? str_repeat('*', strlen($decrypted) - 4) . substr($decrypted, -4)
                    : str_repeat('*', strlen($decrypted));
            } catch (\Exception $e) {
                $apiKey = '(error decrypting)';
            }
        }

        return [
            'enabled'        => $this->isEnabled($storeId) ? 'Yes' : 'No',
            'api_url'        => $this->getApiUrl($storeId) ?: '(not set)',
            'api_key'        => $apiKey ?: '(not set)',
            'timeout'        => $this->getTimeout($storeId) . 's',
            'retry_attempts' => $this->getRetryAttempts($storeId),
            'ngauge_base_url'=> (string)$this->scopeConfig->getValue(self::CONFIG_PATH_NGAUGE_BASE_URL, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $storeId) ?: '(not set)',
            'log_enabled'    => $this->scopeConfig->isSetFlag(self::CONFIG_PATH_LOG_ENABLED, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $storeId) ? 'Yes' : 'No',
        ];
    }

    /**
     * Parse webhook response and save NGauge order fields back to the Magento order.
     * Returns array of saved fields for use in debug comments.
     *
     * @param OrderInterface $order
     * @param string $responseBody
     * @return array
     */
    /**
     * Sets NGauge order fields on the order object from the webhook response.
     * Does NOT persist — caller is responsible for a single orderRepository::save().
     * Renamed from saveNgaugeResponseFields to reflect the no-save contract.
     *
     * @param OrderInterface $order
     * @param string $responseBody
     * @return array
     */
    private function setNgaugeResponseFields(OrderInterface $order, string $responseBody): array
    {
        try {
            $data = $this->jsonSerializer->unserialize($responseBody);

            $no   = $data['ngauge_order_no']   ?? $data['orderref']   ?? null;
            $id   = $data['ngauge_order_id']   ?? $data['id']         ?? null;
            $link = $data['ngauge_order_link'] ?? $data['order_link'] ?? null;

            if ($no === null && $id === null && $link === null) {
                return [];
            }

            // Build link from id if not supplied directly
            if ($link === null && $id !== null) {
                $baseUrl = rtrim(
                    (string)$this->scopeConfig->getValue(self::CONFIG_PATH_NGAUGE_BASE_URL, ScopeInterface::SCOPE_STORE, $order->getStoreId()),
                    '/'
                );
                $link = $baseUrl . '/cart/view?id=' . $id;
            } elseif ($link !== null && !preg_match('#^https?://#i', $link)) {
                // If link is relative, prepend base URL
                $baseUrl = rtrim(
                    (string)$this->scopeConfig->getValue(self::CONFIG_PATH_NGAUGE_BASE_URL, ScopeInterface::SCOPE_STORE, $order->getStoreId()),
                    '/'
                );
                $link = $baseUrl . '/' . ltrim($link, '/');
            }

            if ($no   !== null) { $order->setData('ngauge_order_no',   $no); }
            if ($id   !== null) { $order->setData('ngauge_order_id',   (string)$id); }
            if ($link !== null) { $order->setData('ngauge_order_link', $link); }

            $this->log("Queued NGauge fields for order {$order->getIncrementId()} — no:{$no} id:{$id} (will be persisted by caller)");

            return ['order_no' => $no, 'order_id' => $id, 'order_link' => $link];
        } catch (\Exception $e) {
            $this->log("Failed to save NGauge response fields for order {$order->getIncrementId()}: " . $e->getMessage());
            return [];
        }
    }

    /**
     * Build a readable success comment including HTTP code, pretty JSON and extracted NGauge fields
     *
     * @param string $eventType
     * @param int $httpCode
     * @param string $responseBody
     * @param array $saved
     * @return string
     */
    private function buildSuccessComment(string $eventType, int $httpCode, string $responseBody, array $saved): string
    {
        // Pretty-print JSON if possible
        try {
            $decoded = $this->jsonSerializer->unserialize($responseBody);
            $pretty  = json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        } catch (\Exception $e) {
            $pretty = $responseBody;
        }

        $lines = [
            '[NGauge] Webhook sent successfully',
            'Event: ' . $eventType . ' | HTTP: ' . $httpCode,
            str_repeat('-', 40),
            'Response:',
            $pretty ?: '(empty)',
        ];

        if (!empty($saved)) {
            $lines[] = str_repeat('-', 40);
            $lines[] = 'NGauge fields saved:';
            if (!empty($saved['order_no']))   { $lines[] = '  Order No : ' . $saved['order_no']; }
            if (!empty($saved['order_id']))   { $lines[] = '  Order ID : ' . $saved['order_id']; }
            if (!empty($saved['order_link'])) { $lines[] = '  Link     : ' . $saved['order_link']; }
        } else {
            $lines[] = str_repeat('-', 40);
            $lines[] = 'No NGauge fields found in response.';
        }

        return implode("\n", $lines);
    }

    /**
     * Build a readable failure comment including HTTP code and response body
     *
     * @param string $eventType
     * @param int $retryAttempts
     * @param array $lastResponse
     * @return string
     */
    private function buildFailureComment(string $eventType, int $retryAttempts, array $lastResponse): string
    {
        $httpCode = $lastResponse['http_code'] ?? 0;
        $body     = $lastResponse['response']  ?? '';

        // Pretty-print if JSON
        if (!empty($body)) {
            try {
                $decoded = $this->jsonSerializer->unserialize($body);
                $body    = json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            } catch (\Exception $e) {
                // leave as-is
            }
        }

        $lines = [
            '[NGauge] Webhook FAILED',
            'Event: ' . $eventType . ' | HTTP: ' . ($httpCode ?: 'N/A') . ' | Attempts: ' . $retryAttempts,
        ];

        if (!empty($body)) {
            $lines[] = str_repeat('-', 40);
            $lines[] = 'Response:';
            $lines[] = $body;
        }

        return implode("\n", $lines);
    }

    /**
     * Attaches a status-history comment to the order object.
     * Does NOT persist — caller is responsible for a single orderRepository::save().
     * Renamed from addOrderComment to reflect the no-save contract.
     *
     * @param OrderInterface $order
     * @param string $comment
     * @return void
     */
    private function attachOrderComment(OrderInterface $order, string $comment): void
    {
        try {
            $history = $this->historyFactory->create();
            $history->setParentId($order->getId())
                ->setComment($comment)
                ->setIsCustomerNotified(false)
                ->setIsVisibleOnFront(false)
                ->setStatus($order->getStatus())
                ->setEntityName('order');

            $order->addStatusHistory($history);
        } catch (\Exception $e) {
            $this->log('Failed to attach order comment: ' . $e->getMessage());
        }
    }

    /**
     * Persist the order exactly once. Centralised here so there is only ever
     * a single orderRepository::save() call per webhook send, which avoids
     * triggering sales_order_save_after multiple times.
     *
     * @param OrderInterface $order
     * @return void
     */
    private function persistOrder(OrderInterface $order): void
    {
        try {
            $this->orderRepository->save($order);
        } catch (\Exception $e) {
            $this->log('Failed to persist order after webhook: ' . $e->getMessage());
        }
    }

    /**
     * Log message if logging is enabled
     *
     * @param string $message
     * @param int $storeId
     * @return void
     */
    private function log(string $message, int $storeId = 0): void
    {
        if ($this->scopeConfig->isSetFlag(self::CONFIG_PATH_LOG_ENABLED, ScopeInterface::SCOPE_STORE, $storeId)) {
            $this->logger->info('[OrderWebhook] ' . $message);
        }
    }

    /**
     * Always emit operational trace logs for webhook lifecycle debugging.
     * This bypasses the optional module log toggle so webhook execution can be confirmed.
     *
     * @param string $message
     * @return void
     */
    private function trace(string $message): void
    {
        $this->logger->info('[OrderWebhook] ' . $message);
    }
}
