<?php
/**
 * PayNow API Client
 * 
 * Official PayNow API integration for Zimbabwe
 * Documentation: https://developers.paynow.co.zw/
 * 
 * This class implements the official PayNow API for:
 * - Creating payment sessions
 * - Checking payment status
 * - Handling webhooks
 * - Signature verification
 * 
 * Based on the official Paynow-PHP-SDK (https://gitlab.com/paynow-developer-hub/Paynow-PHP-SDK)
 */

namespace PayNow;

class PayNowClient
{
    /**
     * API base URLs
     */
    const LIVE_URL = 'https://www.paynow.co.zw';
    const STAGING_URL = 'https://www.paynow.co.zw';

    /**
     * API endpoints
     */
    const ENDPOINT_INITIATE = '/interface/initiatetransaction';
    const ENDPOINT_INITIATE_MOBILE = '/interface/remotetransaction';
    const ENDPOINT_STATUS = '/interface/query';

    /**
     * Result statuses
     */
    const STATUS_PAID = 'Paid';
    const STATUS_PENDING = 'Pending';
    const STATUS_CANCELLED = 'Cancelled';
    const STATUS_CREATED = 'Created';
    const STATUS_FAILED = 'Failed';
    const STATUS_ERROR = 'Error';

    private $integrationId;
    private $integrationKey;
    private $merchantId;
    private $isStaging;
    private $baseUrl;
    private $email;
    private $mobile;
    private $returnUrl;
    private $resultUrl;

    /**
     * Initialize PayNow API client
     * 
     * @param string $integrationId Integration ID from PayNow dashboard
     * @param string $integrationKey Integration Key from PayNow dashboard
     * @param string $merchantId Merchant ID from PayNow dashboard
     * @param bool $isStaging Use staging/sandbox environment
     * @param string $merchantEmail Merchant email for payment notifications
     * @param string $merchantMobile Merchant mobile for payment notifications
     */
    public function __construct(
        string $integrationId,
        string $integrationKey,
        string $merchantId = '',
        bool $isStaging = true,
        string $merchantEmail = '',
        string $merchantMobile = ''
    ) {
        $this->integrationId = $integrationId;
        $this->integrationKey = strtolower($integrationKey);
        $this->merchantId = $merchantId;
        $this->isStaging = $isStaging;
        $this->baseUrl = self::LIVE_URL;
        $this->email = $merchantEmail;
        $this->mobile = $merchantMobile;

        // Validate credentials
        if (empty($this->integrationId) || empty($this->integrationKey)) {
            throw new \InvalidArgumentException('PayNow Integration ID and Key are required');
        }
    }

    /**
     * Set the return URL (where user is redirected after payment)
     * 
     * @param string $returnUrl
     * @return $this
     */
    public function setReturnUrl(string $returnUrl)
    {
        $this->returnUrl = $returnUrl;
        return $this;
    }

    /**
     * Set the result URL (webhook URL for payment status updates)
     * 
     * @param string $resultUrl
     * @return $this
     */
    public function setResultUrl(string $resultUrl)
    {
        $this->resultUrl = $resultUrl;
        return $this;
    }

    /**
     * Create a payment session/request
     * 
     * @param string $reference Unique transaction reference
     * @param float $amount Amount to charge
     * @param string $description Payment description
     * @param string $buyerEmail Customer email
     * @param string $buyerMobile Customer mobile (optional, for mobile payments)
     * @param array $additionalParams Additional payment parameters
     * @param string $returnUrl Return URL (optional, can be set via setReturnUrl)
     * @return array Response containing redirect URL and poll URL
     */
    public function createPayment(
        string $reference,
        float $amount,
        string $description,
        string $buyerEmail,
        string $buyerMobile = '',
        array $additionalParams = [],
        string $returnUrl = ''
    ): array {
        $params = $this->buildPaymentParams($reference, $amount, $description, $buyerEmail, $buyerMobile, $additionalParams);
        
        // Set return URL if provided or already set
        if ($returnUrl !== '') {
            $params['returnurl'] = $returnUrl;
        } elseif ($this->returnUrl) {
            $params['returnurl'] = $this->returnUrl;
        }
        
        // Set result URL (webhook) if set
        if ($this->resultUrl) {
            $params['resulturl'] = $this->resultUrl;
        }
        
        // Generate hash for authentication (must be after all params are set)
        $params['hash'] = $this->generateHash($params);
        
        $response = $this->makeRequest(self::ENDPOINT_INITIATE, $params);
        
        // Check if response indicates an error
        $success = isset($response['status']) && strtolower($response['status']) === 'ok';
        
        return [
            'success' => $success,
            'redirect_url' => $response['browserurl'] ?? null,
            'poll_url' => $response['pollurl'] ?? null,
            'status' => $response['status'] ?? null,
            'reference' => $reference,
            'transaction_id' => $response['transaction_id'] ?? $reference,
            'raw_response' => $response
        ];
    }

    /**
     * Build payment request parameters according to PayNow API spec
     * 
     * @param string $reference Unique transaction reference
     * @param float $amount Amount to charge
     * @param string $description Payment description
     * @param string $buyerEmail Customer email
     * @param string $buyerMobile Customer mobile
     * @param array $additionalParams Additional parameters
     * @return array Formatted parameters
     */
    private function buildPaymentParams(
        string $reference,
        float $amount,
        string $description,
        string $buyerEmail,
        string $buyerMobile,
        array $additionalParams
    ): array {
        // Format amount to 2 decimal places
        $formattedAmount = number_format($amount, 2, '.', '');

        $params = [
            'id' => $this->integrationId,
            'reference' => $reference,
            'amount' => $formattedAmount,
            'additionalinfo' => $description,
            'authemail' => $buyerEmail,
            'status' => 'Message'
        ];

        // Add mobile if provided (for mobile money payments)
        if (!empty($buyerMobile)) {
            $params['phone'] = $this->formatMobileNumber($buyerMobile);
        }

        // Add merchant info
        if (!empty($this->email)) {
            $params['merchantemail'] = $this->email;
        }

        // Add any additional parameters
        foreach ($additionalParams as $key => $value) {
            if (!isset($params[$key])) {
                $params[$key] = $value;
            }
        }

        return $params;
    }

    /**
     * Generate hash for transaction authentication
     * 
     * PayNow uses SHA-512 hash of all parameter values concatenated
     * in the order they appear in the request, followed by the integration key.
     * 
     * @param array $params Payment parameters
     * @return string SHA512 hash (uppercase)
     */
    private function generateHash(array $params): string
    {
        // Remove hash from params if present
        unset($params['hash']);

        // Build hash string: concatenation of ALL values in request order
        $hashString = '';
        
        foreach ($params as $key => $value) {
            if (strtoupper($key) !== 'HASH') {
                $hashString .= $value;
            }
        }

        // Append integration key
        $hashString .= $this->integrationKey;

        return strtoupper(hash('sha512', $hashString));
    }

    /**
     * Verify hash from PayNow response
     * 
     * @param array $response Response data from PayNow
     * @param string $hash Received hash
     * @return bool
     */
    public function verifyHash(array $response, string $hash): bool
    {
        // Rebuild hash string from response - concatenate all values in order
        $hashString = '';
        
        foreach ($response as $key => $value) {
            if (strtoupper($key) !== 'HASH') {
                $hashString .= $value;
            }
        }

        $hashString .= $this->integrationKey;
        $calculatedHash = strtoupper(hash('sha512', $hashString));

        return $calculatedHash === strtoupper($hash);
    }

    /**
     * Check payment status using poll URL
     * 
     * @param string $pollUrl Poll URL from payment initiation
     * @return array Payment status details
     */
    public function checkStatus(string $pollUrl): array
    {
        // The poll URL from PayNow already contains the necessary parameters
        // We just need to make a GET request to it
        $response = $this->makeCurlRequest($pollUrl, [], 'GET');
        
        return [
            'success' => true,
            'status' => $response['status'] ?? null,
            'paid' => isset($response['status']) && strtolower($response['status']) === strtolower(self::STATUS_PAID),
            'amount' => $response['amount'] ?? null,
            'reference' => $response['reference'] ?? null,
            'raw_response' => $response
        ];
    }

    /**
     * Verify webhook signature
     * 
     * @param string $signature Raw signature from header
     * @param string $payload Raw request body
     * @return bool
     */
    public function verifyWebhookSignature(string $signature, string $payload): bool
    {
        // Generate expected signature
        $expectedSignature = base64_encode(
            hash_hmac('sha256', $payload, $this->integrationKey, true)
        );

        return hash_equals($expectedSignature, $signature);
    }

    /**
     * Format mobile number to PayNow format
     * 
     * @param string $mobile Mobile number
     * @return string Formatted number
     */
    private function formatMobileNumber(string $mobile): string
    {
        // Remove spaces and special characters
        $mobile = preg_replace('/[^0-9]/', '', $mobile);

        // Handle Zimbabwe mobile numbers
        if (strlen($mobile) === 10 && substr($mobile, 0, 2) === '07') {
            return '263' . substr($mobile, 1);
        }

        if (strlen($mobile) === 9 && substr($mobile, 0, 1) === '7') {
            return '263' . $mobile;
        }

        if (strlen($mobile) === 12 && substr($mobile, 0, 3) === '263') {
            return $mobile;
        }

        return $mobile;
    }

    /**
     * Make API request to PayNow
     * 
     * @param string $endpoint API endpoint
     * @param array $params Request parameters
     * @param string $method HTTP method
     * @return array Response
     */
    private function makeRequest(string $endpoint, array $params, string $method = 'POST'): array
    {
        $url = $this->baseUrl . $endpoint;

        if ($method === 'GET') {
            $url .= '?' . http_build_query($params);
        }

        $response = $this->makeCurlRequest($url, $params, $method);

        // Log the request
        $this->logRequest($endpoint, $params, $response);

        return $response;
    }

    /**
     * Execute cURL request
     * 
     * @param string $url Full URL
     * @param array $params Request data
     * @param string $method HTTP method
     * @return array Response
     */
    private function makeCurlRequest(string $url, array $params, string $method): array
    {
        $ch = curl_init();

        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => false, // PayNow uses valid SSL certs, but allow self-signed in dev
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_USERAGENT => 'PayNow-PHP-SDK/2.0'
        ]);

        if ($method === 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);

        curl_close($ch);

        if ($error) {
            throw new \RuntimeException('PayNow API request failed: ' . $error);
        }

        // Parse response
        $responseData = [];
        parse_str($response, $responseData);

        // Check for API errors
        if (isset($responseData['error'])) {
            // Return error in response rather than throwing
            return $responseData;
        }

        return $responseData;
    }

    /**
     * Log API requests for debugging
     * 
     * @param string $endpoint Endpoint called
     * @param array $params Request parameters
     * @param array $response Response received
     */
    private function logRequest(string $endpoint, array $params, array $response): void
    {
        // Mask sensitive data
        $logParams = $params;
        if (isset($logParams['hash'])) {
            $logParams['hash'] = '[MASKED]';
        }
        if (isset($logParams['authemail'])) {
            $logParams['authemail'] = '[MASKED]';
        }

        error_log(sprintf(
            "[PayNow] Endpoint: %s | Params: %s | Response: %s",
            $endpoint,
            json_encode($logParams),
            json_encode($response)
        ));
    }

    /**
     * Get whether using staging environment
     * 
     * @return bool
     */
    public function isStaging(): bool
    {
        return $this->isStaging;
    }

    /**
     * Get the base URL being used
     * 
     * @return string
     */
    public function getBaseUrl(): string
    {
        return $this->baseUrl;
    }
}

/**
 * PayNow Payment Response Handler
 */
class PayNowPaymentResponse
{
    private $status;
    private $transactionId;
    private $reference;
    private $amount;
    private $rawData;

    public function __construct(array $data)
    {
        $this->rawData = $data;
        $this->status = $data['status'] ?? null;
        $this->transactionId = $data['transaction_id'] ?? null;
        $this->reference = $data['reference'] ?? null;
        $this->amount = $data['amount'] ?? null;
    }

    /**
     * Check if payment was successful
     * 
     * @return bool
     */
    public function success(): bool
    {
        return isset($this->status) && 
               strtoupper($this->status) === strtoupper(PayNowClient::STATUS_PAID);
    }

    /**
     * Check if payment is pending
     * 
     * @return bool
     */
    public function pending(): bool
    {
        return isset($this->status) && 
               strtoupper($this->status) === strtoupper(PayNowClient::STATUS_PENDING);
    }

    /**
     * Check if payment was cancelled
     * 
     * @return bool
     */
    public function cancelled(): bool
    {
        return isset($this->status) && 
               strtoupper($this->status) === strtoupper(PayNowClient::STATUS_CANCELLED);
    }

    /**
     * Check if payment failed
     * 
     * @return bool
     */
    public function failed(): bool
    {
        return isset($this->status) && 
               in_array(strtoupper($this->status), [
                   strtoupper(PayNowClient::STATUS_FAILED),
                   strtoupper(PayNowClient::STATUS_ERROR)
               ]);
    }

    /**
     * Get status message
     * 
     * @return string|null
     */
    public function getStatus(): ?string
    {
        return $this->status;
    }

    /**
     * Get transaction ID
     * 
     * @return string|null
     */
    public function getTransactionId(): ?string
    {
        return $this->transactionId;
    }

    /**
     * Get reference
     * 
     * @return string|null
     */
    public function getReference(): ?string
    {
        return $this->reference;
    }

    /**
     * Get amount
     * 
     * @return float|null
     */
    public function getAmount(): ?float
    {
        return $this->amount ? (float) $this->amount : null;
    }

    /**
     * Get raw response data
     * 
     * @return array
     */
    public function getRawData(): array
    {
        return $this->rawData;
    }

    /**
     * Get human-readable status
     * 
     * @return string
     */
    public function getStatusMessage(): string
    {
        if ($this->success()) {
            return 'Payment successful';
        }
        
        if ($this->pending()) {
            return 'Payment is pending';
        }
        
        if ($this->cancelled()) {
            return 'Payment was cancelled';
        }
        
        if ($this->failed()) {
            return 'Payment failed';
        }

        return 'Unknown status: ' . ($this->status ?? 'No status');
    }
}