<?php

namespace App\Modules\Payment\AmazonPay;

use App\Core\Payment\AbstractPayment;

/**
 * Amazon Pay — Checkout v2 (redirect wallet).
 *
 * Refs: official amazon-pay-api-sdk-php (SignatureHelper — RSASSA-PSS-V2 salt=32,
 * canonical request + x-amz-pay-date/host/region/idempotency-key headers), Amazon Pay
 * Checkout v2 docs (/checkoutSessions, /charges/{id}/refund, region→host + sandbox/live
 * env path); WiseCP IpnHandler used only for the fail-closed callback-verify contract.
 */
class AmazonPay extends AbstractPayment
{
    /** RSASSA-PSS-V2 uses salt length 32 (SDK default). */
    protected string $signAlgorithm = 'AMZN-PAY-RSASSA-PSS-V2';

    protected int $signSaltLength = 32;

    /**
     * Amazon Pay checkout-session/charge state → internal status.
     * v2 states: Completed/Open/Declined/Canceled (session), Captured/Authorized/Pending (charge).
     */
    protected $statusMap = [
        'Completed' => 'success',
        'Captured' => 'success',
        'Authorized' => 'success',
        'Open' => 'pending',
        'Pending' => 'pending',
        'AuthorizationInitiated' => 'pending',
        'Declined' => 'failed',
        'Canceled' => 'failed',
    ];

    public function __construct(array $config = [])
    {
        parent::__construct($config);
    }

    /**
     * Validate payment parameters and return ready status.
     */
    public function attemptPayment(array $params)
    {
        $requiredParams = ['amount', 'currency'];

        foreach ($requiredParams as $param) {
            if (! isset($params[$param]) || empty($params[$param])) {
                return [
                    'status' => 'error',
                    'message' => __('admin/modules.amazonpay_missing_parameter', ['param' => $param]),
                ];
            }
        }

        if ((float) $params['amount'] <= 0) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.amazonpay_invalid_amount'),
            ];
        }

        return [
            'status' => 'ready',
            'message' => __('admin/modules.amazonpay_ready'),
            'data' => [
                'amount' => $params['amount'],
                'currency' => $params['currency'],
            ],
        ];
    }

    /**
     * Create an Amazon Pay checkout session and return redirect URL.
     */
    public function makePayment(array $params)
    {
        return $this->createCheckoutSession($params, 'redirect');
    }

    /**
     * Generate a payment link — delegates to the same CreateCheckoutSession flow (DRY).
     */
    public function generatePaymentLink(array $params)
    {
        $result = $this->createCheckoutSession($params, 'success');

        if ($result['status'] === 'success') {
            return [
                'status' => 'success',
                'payment_url' => $result['redirect_url'] ?? '',
                'transaction_id' => $result['transaction_id'] ?? null,
            ];
        }

        return [
            'status' => 'error',
            'message' => $result['message'] ?? __('admin/modules.amazonpay_payment_link_failed'),
        ];
    }

    /**
     * Shared CreateCheckoutSession (v2 POST /checkoutSessions) used by makePayment + generatePaymentLink.
     *
     * @param  string  $successStatus  'redirect' (makePayment) | 'success' (link)
     */
    protected function createCheckoutSession(array $params, string $successStatus): array
    {
        $currency = $params['currency'] ?? $this->getConfig('currency', 'USD');
        // v2 amount = decimal string (NOT minor-unit): "12.99".
        $amount = number_format((float) $params['amount'], 2, '.', '');

        $checkoutData = [
            'webCheckoutDetails' => [
                'checkoutReviewReturnUrl' => $params['success_url'] ?? $params['redirect_url'] ?? '',
                'checkoutResultReturnUrl' => $params['callback_url'] ?? $params['webhook_url'] ?? '',
            ],
            'storeId' => $this->getConfig('store_id', ''),
            'paymentDetails' => [
                'paymentIntent' => 'AuthorizeWithCapture',
                'chargeAmount' => [
                    'amount' => $amount,
                    'currencyCode' => strtoupper($currency),
                ],
            ],
            'merchantMetadata' => [
                'merchantReferenceId' => (string) ($params['order_id'] ?? ''),
                'merchantStoreName' => $params['store_name'] ?? config('app.name', 'ProCP'),
                'noteToBuyer' => $params['description'] ?? __('admin/modules.amazonpay_payment_description', ['order_id' => $params['order_id'] ?? '']),
            ],
        ];

        // CreateCheckoutSession is a resource-creating op → idempotency key mandatory (v2).
        $response = $this->apiCall('POST', '/checkoutSessions', $checkoutData, $this->idempotencyKey());

        if ($this->isSuccessHttp($response['http_code']) && isset($response['response']['checkoutSessionId'])) {
            $session = $response['response'];
            $redirectUrl = $session['webCheckoutDetails']['amazonPayRedirectUrl']
                ?? $session['forwardToInfo']['paymentServiceUrl']
                ?? '';

            return [
                'status' => $successStatus,
                'message' => __('admin/modules.amazonpay_payment_created'),
                'transaction_id' => $session['checkoutSessionId'],
                'redirect_url' => $redirectUrl,
                'data' => [
                    'checkout_session_id' => $session['checkoutSessionId'],
                    'amazon_pay_redirect_url' => $redirectUrl,
                    'amount' => $params['amount'],
                    'currency' => $currency,
                ],
            ];
        }

        return [
            'status' => 'error',
            'message' => $this->errorMessage($response, 'amazonpay_payment_failed'),
        ];
    }

    /**
     * Handle payment callback (checkoutResultReturnUrl return / webhook).
     *
     * FAIL-CLOSED: the buyer-controlled checkoutSessionId is re-fetched server-to-server
     * from Amazon (a signed request attacker cannot forge). Before treating the session as
     * paid we additionally verify the returned session belongs to THIS invoice/amount
     * (merchantReferenceId + chargeAmount) to block cross-order replay.
     */
    public function handlePaymentCallback(array $params)
    {
        $checkoutSessionId = $params['amazonCheckoutSessionId']
            ?? $params['checkoutSessionId']
            ?? $params['checkout_session_id']
            ?? $params['transaction_id']
            ?? null;

        if (! $checkoutSessionId) {
            return [
                'status' => 'failed',
                'message' => __('admin/modules.amazonpay_missing_session_id'),
            ];
        }

        // Step 1: authoritative server-to-server fetch of the session.
        $response = $this->apiCall('GET', '/checkoutSessions/'.rawurlencode($checkoutSessionId));

        if ($response['http_code'] !== 200 || ! isset($response['response']['statusDetails']['state'])) {
            return [
                'status' => 'failed',
                'message' => __('admin/modules.amazonpay_status_check_failed'),
            ];
        }

        $session = $response['response'];
        $sessionState = $session['statusDetails']['state'];

        // Step 2: if buyer confirmed (Open), complete the checkout → creates the charge.
        if ($sessionState === 'Open') {
            $completeData = [
                'chargeAmount' => [
                    'amount' => $session['paymentDetails']['chargeAmount']['amount'] ?? '0',
                    'currencyCode' => $session['paymentDetails']['chargeAmount']['currencyCode'] ?? 'USD',
                ],
            ];

            $completeResponse = $this->apiCall('POST', '/checkoutSessions/'.rawurlencode($checkoutSessionId).'/complete', $completeData);

            // v2 complete returns 200 or 202 (AuthorizationInitiated).
            if (($completeResponse['http_code'] === 200 || $completeResponse['http_code'] === 202)
                && isset($completeResponse['response']['statusDetails']['state'])) {
                $session = $completeResponse['response'];
                $sessionState = $session['statusDetails']['state'];
            } else {
                return [
                    'status' => 'failed',
                    'message' => $this->errorMessage($completeResponse, 'amazonpay_complete_failed'),
                ];
            }
        }

        // Step 3: bind session to this invoice/amount (fail-closed against replay).
        $returnedRef = (string) ($session['merchantMetadata']['merchantReferenceId'] ?? '');
        $expectedRef = (string) ($params['order_id'] ?? $params['merchant_reference_id'] ?? '');
        if ($expectedRef !== '' && ! $this->secureCompare($expectedRef, $returnedRef)) {
            return [
                'status' => 'failed',
                'message' => __('admin/modules.amazonpay_reference_mismatch'),
            ];
        }

        $returnedAmount = (string) ($session['paymentDetails']['chargeAmount']['amount'] ?? '');
        if (isset($params['amount']) && $params['amount'] !== '' && $params['amount'] !== null) {
            $expectedAmount = number_format((float) $params['amount'], 2, '.', '');
            if ($returnedAmount === '' || ! $this->secureCompare($expectedAmount, $returnedAmount)) {
                return [
                    'status' => 'failed',
                    'message' => __('admin/modules.amazonpay_amount_mismatch'),
                ];
            }
        }

        $mappedStatus = $this->mapAmazonPayStatus($sessionState);

        return [
            'status' => $mappedStatus,
            'message' => __('admin/modules.amazonpay_payment_status_'.$mappedStatus),
            'transaction_id' => $checkoutSessionId,
            'data' => [
                'amazon_status' => $sessionState,
                'checkout_session_id' => $checkoutSessionId,
                'charge_id' => $session['chargeId'] ?? null,
                'charge_permission_id' => $session['chargePermissionId'] ?? null,
                'amount' => $returnedAmount ?: null,
                'currency' => $session['paymentDetails']['chargeAmount']['currencyCode'] ?? null,
                'merchant_reference' => $returnedRef ?: null,
            ],
        ];
    }

    /**
     * Amazon Pay is a redirect wallet — 3DS is handled by Amazon, not this gateway.
     * Inherit AbstractPayment::complete3DPayment which throws payment_function_not_supported.
     */

    /**
     * Refund a charge — v2 POST /refunds { chargeId, refundAmount{amount,currencyCode}, softDescriptor }.
     */
    public function refundPayment(array $params)
    {
        $chargeId = $params['charge_id'] ?? $params['transaction_id'] ?? null;

        if (! $chargeId) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.amazonpay_missing_charge_id'),
            ];
        }

        $currency = $params['currency'] ?? $this->getConfig('currency', 'USD');
        $amount = number_format((float) ($params['amount'] ?? 0), 2, '.', '');

        $refundData = [
            'chargeId' => $chargeId,
            'refundAmount' => [
                'amount' => $amount,
                'currencyCode' => strtoupper($currency),
            ],
            'softDescriptor' => $params['description'] ?? $params['note'] ?? __('admin/modules.amazonpay_refund_description'),
        ];

        // CreateRefund is a resource-creating op → idempotency key mandatory (v2).
        $response = $this->apiCall('POST', '/refunds', $refundData, $this->idempotencyKey());

        if ($this->isSuccessHttp($response['http_code']) && isset($response['response']['refundId'])) {
            return [
                'status' => 'success',
                'message' => __('admin/modules.amazonpay_refund_success'),
                'refund_id' => $response['response']['refundId'],
                'data' => [
                    'refund_id' => $response['response']['refundId'],
                    'charge_id' => $chargeId,
                    'refund_amount' => $amount,
                    'currency' => $currency,
                    'state' => $response['response']['statusDetails']['state'] ?? null,
                ],
            ];
        }

        return [
            'status' => 'error',
            'message' => $this->errorMessage($response, 'amazonpay_refund_failed'),
        ];
    }

    /**
     * Test API connection — signed GET of the base checkoutSessions resource.
     *
     * Only a real 2xx proves the signature/credentials are accepted. 400/401/403 mean the
     * signed request was rejected → error (NO fail-open; a signature failure must not be
     * reported as a successful connection).
     */
    public function testConnection(array $params = [])
    {
        $config = $this->validateConfig();
        if (($config['status'] ?? '') === 'error') {
            return $config;
        }

        $response = $this->apiCall('GET', '/checkoutSessions');

        if ($this->isSuccessHttp($response['http_code'])) {
            return [
                'status' => 'success',
                'message' => __('admin/modules.amazonpay_connection_success'),
                'data' => ['http_code' => $response['http_code']],
            ];
        }

        if (in_array($response['http_code'], [400, 401, 403], true)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.amazonpay_auth_failed'),
            ];
        }

        return [
            'status' => 'error',
            'message' => $this->errorMessage($response, 'amazonpay_connection_failed'),
        ];
    }

    /**
     * Validate module configuration.
     */
    public function validateConfig()
    {
        $missing = [];

        foreach (['merchant_id', 'store_id', 'public_key_id', 'private_key'] as $key) {
            if (empty($this->config[$key])) {
                $missing[] = $key;
            }
        }

        if (! empty($missing)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.amazonpay_missing_config', ['fields' => implode(', ', $missing)]),
            ];
        }

        return [
            'status' => 'success',
            'message' => __('admin/modules.amazonpay_config_valid'),
        ];
    }

    /**
     * Supported features.
     *
     * recurring=false: v2 recurring needs a charge-permission/saved-token flow that is
     * NOT implemented here — declaring true would be misleading.
     */
    public function getFeatures(): array
    {
        return [
            'refund' => true,
            '3d_secure' => false,
            'save_card' => false,
            'recurring' => false,
            'fraud_check' => false,
            'installment' => false,
        ];
    }

    /**
     * Get configuration fields for admin panel.
     */
    public function getConfigFields(): array
    {
        return array_merge(parent::getConfigFields(), [
            'merchant_id' => [
                'type' => 'text',
                'label' => 'Merchant ID',
                'description' => 'Your Amazon Pay Merchant ID from Seller Central',
                'required' => true,
                'value' => '',
                'placeholder' => 'AXXXXXXXXXXXXXXXXXX',
            ],
            'store_id' => [
                'type' => 'text',
                'label' => 'Store ID',
                'description' => 'Amazon Pay Store ID (Client ID) from Seller Central',
                'required' => true,
                'value' => '',
                'placeholder' => 'amzn1.application-oa2-client.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
            ],
            'public_key_id' => [
                'type' => 'text',
                'label' => 'Public Key ID',
                'description' => 'Public Key ID from Amazon Pay integration keys',
                'required' => true,
                'value' => '',
                'placeholder' => 'SANDBOX-XXXXXXXXXXXXXXXXXXXXXXXX',
            ],
            'private_key' => [
                'type' => 'textarea',
                'label' => 'Private Key',
                'description' => 'Private key (PEM format) downloaded from Amazon Pay integration',
                'required' => true,
                'value' => '',
                'placeholder' => '-----BEGIN RSA PRIVATE KEY-----',
            ],
            'region' => [
                'type' => 'select',
                'label' => 'Region',
                'description' => 'Amazon Pay region (affects endpoint host and supported currencies)',
                'required' => true,
                'value' => 'eu',
                'options' => [
                    'eu' => 'EU/UK - Europe',
                    'na' => 'NA - North America',
                    'jp' => 'JP - Japan',
                ],
            ],
        ]);
    }

    // =========================================================================
    //  Transport + signing (Checkout v2)
    // =========================================================================

    /** Region → API host (na→.com, eu→.eu, jp→.jp). */
    protected function apiHost(): string
    {
        $region = strtolower((string) $this->getConfig('region', 'eu'));

        return match ($region) {
            'na', 'us' => 'pay-api.amazon.com',
            'jp' => 'pay-api.amazon.jp',
            default => 'pay-api.amazon.eu',
        };
    }

    /** Region code normalized to the value Amazon expects in x-amz-pay-region (na/eu/jp). */
    protected function regionCode(): string
    {
        $region = strtolower((string) $this->getConfig('region', 'eu'));

        return match ($region) {
            'na', 'us' => 'na',
            'jp' => 'jp',
            default => 'eu',
        };
    }

    /** Base path segment: /{sandbox|live}/v2. */
    protected function basePath(): string
    {
        return ($this->isTestMode() ? '/sandbox' : '/live').'/v2';
    }

    /** v2 idempotency key: hex, no dashes. */
    protected function idempotencyKey(): string
    {
        return bin2hex(random_bytes(16));
    }

    /**
     * Perform a signed Amazon Pay v2 API call.
     *
     * Does NOT override the global AbstractPayment::callApi (kept intact for other gateways);
     * this is a gateway-private transport with TLS verification + timeouts + protocol restriction.
     *
     * @param  string  $method  HTTP method
     * @param  string  $path  path under basePath, e.g. '/checkoutSessions'
     * @param  array  $data  JSON body (empty for GET)
     * @param  string|null  $idempotencyKey  set for resource-creating POSTs
     * @return array{http_code:int,response:mixed}
     */
    protected function apiCall(string $method, string $path, array $data = [], ?string $idempotencyKey = null): array
    {
        $host = $this->apiHost();
        $uri = $this->basePath().$path;
        $url = 'https://'.$host.$uri;
        $payload = ! empty($data) ? json_encode($data) : '';

        $headers = $this->signRequest($method, $host, $uri, '', $payload, $idempotencyKey);

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
            CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS,
        ]);

        if ($payload !== '' && $method !== 'GET') {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
        }

        $response = curl_exec($ch);
        $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        // NOTE: raw response is intentionally NOT returned/logged (may carry buyer PII).
        return [
            'http_code' => $httpCode,
            'response' => json_decode((string) $response, true),
        ];
    }

    /**
     * Build the signed header set per Amazon Pay v2 (RSASSA-PSS).
     *
     * Canonical request = METHOD \n URI \n QUERY \n canonicalHeaders \n signedHeaders \n hex(sha256(payload)).
     * String to sign    = algorithm \n hex(sha256(canonicalRequest)).
     * Signature         = base64( RSASSA-PSS(SHA256, salt=32) over stringToSign ).
     * Signed headers: accept, content-type, x-amz-pay-date, x-amz-pay-host, x-amz-pay-region,
     * and (create ops) x-amz-pay-idempotency-key.
     */
    protected function signRequest(string $method, string $host, string $uri, string $query, string $payload, ?string $idempotencyKey = null): array
    {
        $publicKeyId = (string) $this->getConfig('public_key_id', '');
        $date = gmdate('Ymd\THis\Z');

        // Headers that participate in the signature (lowercase names).
        $signed = [
            'accept' => 'application/json',
            'content-type' => 'application/json',
            'x-amz-pay-date' => $date,
            'x-amz-pay-host' => $host,
            'x-amz-pay-region' => $this->regionCode(),
        ];
        if ($idempotencyKey !== null) {
            $signed['x-amz-pay-idempotency-key'] = $idempotencyKey;
        }

        ksort($signed);

        $canonicalHeaders = '';
        foreach ($signed as $name => $value) {
            $canonicalHeaders .= $name.':'.$value."\n";
        }
        $signedHeaders = implode(';', array_keys($signed));

        $canonicalRequest = implode("\n", [
            strtoupper($method),
            $uri,
            $query,
            $canonicalHeaders,
            $signedHeaders,
            hash('sha256', $payload),
        ]);

        $stringToSign = $this->signAlgorithm."\n".hash('sha256', $canonicalRequest);
        $signature = $this->rsaPssSign($stringToSign);

        $authorization = sprintf(
            '%s PublicKeyId=%s, SignedHeaders=%s, Signature=%s',
            $this->signAlgorithm,
            $publicKeyId,
            $signedHeaders,
            $signature
        );

        // Emit as 'Name: value' curl headers.
        $out = [
            'Accept: application/json',
            'Content-Type: application/json',
            'User-Agent: ProCP/1.0 (Language=PHP)',
            'X-Amz-Pay-Date: '.$date,
            'X-Amz-Pay-Host: '.$host,
            'X-Amz-Pay-Region: '.$this->regionCode(),
            'Authorization: '.$authorization,
        ];
        if ($idempotencyKey !== null) {
            $out[] = 'X-Amz-Pay-Idempotency-Key: '.$idempotencyKey;
        }

        return $out;
    }

    /**
     * RSASSA-PSS sign (SHA256, salt=32) using native openssl.
     *
     * PHP's openssl_sign() only emits PKCS#1 v1.5; PSS is produced by EMSA-PSS-encoding the
     * digest and applying raw RSA (openssl_private_encrypt + OPENSSL_NO_PADDING). Returns
     * base64 signature, or '' if the key is invalid (Amazon then rejects — fail-closed, no
     * silent PKCS#1-v1.5 fallback which Amazon would also reject).
     */
    protected function rsaPssSign(string $message): string
    {
        $privateKey = (string) $this->getConfig('private_key', '');
        $key = openssl_pkey_get_private($privateKey);
        if ($key === false) {
            return '';
        }

        $details = openssl_pkey_get_details($key);
        if ($details === false || ($details['type'] ?? null) !== OPENSSL_KEYTYPE_RSA) {
            return '';
        }

        $emBits = $details['bits'] - 1;
        $mHash = hash('sha256', $message, true);
        $em = $this->emsaPssEncode($mHash, $emBits, $this->signSaltLength);
        if ($em === false) {
            return '';
        }

        $signature = '';
        if (! openssl_private_encrypt($em, $signature, $key, OPENSSL_NO_PADDING)) {
            return '';
        }

        return base64_encode($signature);
    }

    /**
     * EMSA-PSS-ENCODE (RFC 8017 §9.1.1) with SHA-256 and MGF1-SHA256.
     *
     * @param  string  $mHash  raw SHA-256 digest of the message
     * @param  int  $emBits  modulus bits - 1
     * @param  int  $sLen  salt length
     * @return string|false  encoded message octet string, or false on parameter error
     */
    protected function emsaPssEncode(string $mHash, int $emBits, int $sLen)
    {
        $hLen = 32; // SHA-256 output length
        $emLen = (int) ceil($emBits / 8);

        if ($emLen < $hLen + $sLen + 2) {
            return false;
        }

        $salt = $sLen > 0 ? random_bytes($sLen) : '';
        $mPrime = str_repeat("\x00", 8).$mHash.$salt;
        $h = hash('sha256', $mPrime, true);
        $ps = str_repeat("\x00", $emLen - $sLen - $hLen - 2);
        $db = $ps."\x01".$salt;
        $dbMask = $this->mgf1($h, $emLen - $hLen - 1);
        $maskedDb = $db ^ $dbMask;

        // Zero the leftmost (8*emLen - emBits) bits of maskedDB.
        $zeroBits = 8 * $emLen - $emBits;
        if ($zeroBits > 0) {
            $maskedDb[0] = chr(ord($maskedDb[0]) & (0xFF >> $zeroBits));
        }

        return $maskedDb.$h."\xbc";
    }

    /** MGF1 mask generation function with SHA-256. */
    protected function mgf1(string $seed, int $length): string
    {
        $mask = '';
        $counter = 0;
        while (strlen($mask) < $length) {
            $mask .= hash('sha256', $seed.pack('N', $counter), true);
            $counter++;
        }

        return substr($mask, 0, $length);
    }

    // =========================================================================
    //  Helpers
    // =========================================================================

    protected function isSuccessHttp(int $code): bool
    {
        return $code === 200 || $code === 201;
    }

    /** Extract a safe error message (never leaks raw response body). */
    protected function errorMessage(array $response, string $fallbackKey): string
    {
        return $response['response']['message']
            ?? $response['response']['reasonCode']
            ?? __('admin/modules.'.$fallbackKey);
    }

    private function mapAmazonPayStatus(string $amazonStatus): string
    {
        return $this->statusMap[$amazonStatus] ?? 'failed';
    }

    public function getSupportedFeatures(): array
    {
        return $this->getFeatures();
    }
}
