<?php

namespace Modules\Payment\Gateways;

use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Modules\Payment\Contracts\PaymentGatewayInterface;
use Modules\Payment\Data\PaymentRequestData;
use Modules\Payment\Data\PaymentResultData;
use Modules\Payment\Data\RefundResultData;
use Modules\Payment\Enums\PaymentMethod;
use Modules\Payment\Enums\PaymentStatus;
use Modules\Payment\Enums\RefundStatus;
use Modules\Payment\Exceptions\InvalidWebhookSignatureException;
use Modules\Payment\Support\KbzSignature;

/**
 * KBZ Mini App gateway strategy, ported from bnf_event's
 * `App\Strategies\Payments\KBZMiniApp`/`KBZPay` (domain.md §6).
 *
 * Reads credentials from config('services.kbz') by default; a config array
 * may be injected directly (used by tests / the factory).
 */
class KbzMiniAppGateway implements PaymentGatewayInterface
{
    private readonly string $appId;

    private readonly string $merchantCode;

    private readonly string $merchantKey;

    private readonly string $baseUrl;

    private readonly string $createOrderUrl;

    private readonly string $queryOrderUrl;

    private readonly string $refundOrderUrl;

    private readonly ?string $notifyUrl;

    private readonly ?string $certPath;

    private readonly ?string $certKeyPath;

    private readonly ?string $caPath;

    private readonly ?string $certPassword;

    /**
     * @param  array<string, mixed>|null  $config
     */
    public function __construct(?array $config = null)
    {
        $config ??= (array) config('services.kbz');

        $this->appId = (string) ($config['app_id'] ?? '');
        $this->merchantCode = (string) ($config['merchant_code'] ?? '');
        $this->merchantKey = (string) ($config['merchant_key'] ?? '');
        $this->baseUrl = (string) ($config['base_url'] ?? '');
        // Falls back to base_url for gateways/environments that haven't
        // configured per-operation endpoints yet.
        $this->createOrderUrl = (string) ($config['create_order_url'] ?? $this->baseUrl);
        $this->queryOrderUrl = (string) ($config['query_order_url'] ?? $this->baseUrl);
        $this->refundOrderUrl = (string) ($config['refund_order_url'] ?? $this->baseUrl);
        $this->notifyUrl = $config['notify_url'] ?? null;
        $this->certPath = $config['cert_path'] ?? null;
        $this->certKeyPath = $config['cert_key_path'] ?? null;
        $this->caPath = $config['ca_path'] ?? null;
        $this->certPassword = $config['cert_password'] ?? null;
    }

    public function initiate(PaymentRequestData $data): PaymentResultData
    {
        $params = $this->buildPrecreateParams($data);
        logger($params);
        try {
            $response = Http::post($this->createOrderUrl, ['Request' => $params]);

            logger($response);
        } catch (ConnectionException $exception) {
            \Log::error('KBZ Mini App precreate connection error: '.$exception->getMessage(), [
                'merchant_order_id' => $data->merchantOrderId,
                'amount' => $data->amount,
                'currency' => $data->currency,
            ]);

            return new PaymentResultData(
                status: PaymentStatus::Failed,
                gatewayTransactionId: null,
                gatewayPayload: [],
                message: $exception->getMessage(),
            );
        }

        /** @var array<string, mixed> $body */
        $body = $response->json('Response', []);

        if (! $response->successful() || ($body['result'] ?? null) !== 'SUCCESS') {
            \Log::error('KBZ Mini App precreate failed: '.($body['msg'] ?? 'Unknown error'), [
                'merchant_order_id' => $data->merchantOrderId,
                'amount' => $data->amount,
                'currency' => $data->currency,
                'http_status' => $response->status(),
                'raw_body' => $response->body(),
            ]);

            return new PaymentResultData(
                status: PaymentStatus::Failed,
                gatewayTransactionId: $body['prepay_id'] ?? null,
                gatewayPayload: $body,
                message: $body['msg'] ?? 'KBZ precreate failed.',
            );
        }

        $orderInfo = $this->createOrderInfo($body['prepay_id'] ?? '');

        return new PaymentResultData(
            // KBZ's queryorder/refund calls both key off our own merch_order_id,
            // not their prepay_id — so that's what gets stored/passed forward as
            // the gateway transaction id (prepay_id still lives in the payload).
            status: PaymentStatus::Pending,
            gatewayTransactionId: $data->merchantOrderId,
            gatewayPayload: [
                'prepayId' => $body['prepay_id'] ?? null,
                'orderInfo' => KbzSignature::joinKeyVal($orderInfo),
                'signature' => KbzSignature::sign($orderInfo, $this->merchantKey),
            ],
        );
    }

    public function verify(string $gatewayTransactionId): PaymentResultData
    {
        $params = $this->buildQueryOrderParams($gatewayTransactionId);

        try {
            $response = Http::asJson()->post($this->queryOrderUrl, ['Request' => $params]);
        } catch (ConnectionException $exception) {
            \Log::error('KBZ Mini App verify connection error: '.$exception->getMessage(), [
                'gateway_transaction_id' => $gatewayTransactionId,
            ]);

            return new PaymentResultData(
                status: PaymentStatus::Failed,
                gatewayTransactionId: $gatewayTransactionId,
                gatewayPayload: [],
                message: $exception->getMessage(),
            );
        }

        /** @var array<string, mixed> $body */
        $body = $response->json('Response', []);

        return new PaymentResultData(
            status: $this->mapTradeStatus($body['trade_status'] ?? null),
            gatewayTransactionId: $gatewayTransactionId,
            gatewayPayload: $body,
            message: $body['trade_status'] ?? null,
        );
    }

    public function refund(string $gatewayTransactionId, string $amount, string $reason): RefundResultData
    {
        $params = $this->buildRefundParams($gatewayTransactionId, $amount, $reason);

        try {
            $response = Http::asJson()
                ->withOptions($this->mtlsOptions())
                ->post($this->refundOrderUrl, ['Request' => $params]);
        } catch (ConnectionException $exception) {
            return new RefundResultData(
                status: RefundStatus::Failed,
                gatewayRefundId: null,
                gatewayPayload: [],
                message: $exception->getMessage(),
            );
        }

        /** @var array<string, mixed> $body */
        $body = $response->json('Response', []);

        if (! $response->successful() || ($body['result'] ?? null) !== 'SUCCESS') {
            return new RefundResultData(
                status: RefundStatus::Failed,
                gatewayRefundId: $body['refund_order_id'] ?? null,
                gatewayPayload: $body,
                message: $body['msg'] ?? 'KBZ refund failed.',
            );
        }

        return new RefundResultData(
            status: RefundStatus::Completed,
            gatewayRefundId: $body['refund_order_id'] ?? null,
            gatewayPayload: $body,
        );
    }

    /**
     * bnf_event's equivalent (`OrderController::paymentComplete` /
     * `KBZMiniApp::save`) trusted the raw `trade_status` from the POST body
     * and only re-verified via `queryorder` afterward — it never checked
     * `sign` on the inbound payload at all. This closes that gap: KBZ signs
     * webhook notifications with the same scheme as our outbound calls
     * (confirmed against KBZ's "6 Callback Interface" spec), so the
     * signature is checked first, before any of the payload is trusted.
     *
     * @param  array<string, mixed>  $payload
     */
    public function handleWebhook(array $payload): PaymentResultData
    {
        /** @var array<string, mixed> $notification */
        $notification = (array) ($payload['Request'] ?? []);

        if (! $this->hasValidSignature($notification)) {
            throw InvalidWebhookSignatureException::forGateway(PaymentMethod::KbzMiniApp);
        }

        return new PaymentResultData(
            status: $this->mapTradeStatus($notification['trade_status'] ?? null),
            gatewayTransactionId: $notification['merch_order_id'] ?? null,
            gatewayPayload: $notification,
            message: $notification['trade_status'] ?? null,
        );
    }

    /**
     * @param  array<string, mixed>  $notification
     */
    private function hasValidSignature(array $notification): bool
    {
        if (! isset($notification['sign']) || ! is_string($notification['sign']) || $this->merchantKey === '') {
            return false;
        }

        return hash_equals(KbzSignature::sign($notification, $this->merchantKey), strtoupper($notification['sign']));
    }

    /**
     * @return array<string, mixed>
     */
    private function buildPrecreateParams(PaymentRequestData $data): array
    {
        $params = [
            'timestamp' => (string) now()->timestamp,
            'method' => 'kbz.payment.precreate',
            'notify_url' => $data->notifyUrl ?? $this->notifyUrl,
            'nonce_str' => uniqid(),
            'version' => '1.0',
            'biz_content' => [
                'appid' => $this->appId,
                'merch_code' => $this->merchantCode,
                'merch_order_id' => $data->merchantOrderId,
                'trade_type' => 'MINIAPP',
                'total_amount' => $data->amount,
                'trans_currency' => $data->currency,
                'callback_info' => 'urlencode',
            ],
        ];

        $params['sign'] = KbzSignature::sign($params, $this->merchantKey);
        $params['sign_type'] = 'SHA256';

        return $params;
    }

    /**
     * @return array<string, mixed>
     */
    private function buildQueryOrderParams(string $merchantOrderId): array
    {
        $params = [
            'timestamp' => (string) now()->timestamp,
            'method' => 'kbz.payment.queryorder',
            'nonce_str' => uniqid(),
            'version' => '1.0',
            'biz_content' => [
                'appid' => $this->appId,
                'merch_code' => $this->merchantCode,
                'merch_order_id' => $merchantOrderId,
            ],
        ];

        $params['sign'] = KbzSignature::sign($params, $this->merchantKey);
        $params['sign_type'] = 'SHA256';

        return $params;
    }

    /**
     * KBZ's queryorder trade_status values — mapped conservatively: anything
     * not explicitly a success/pending state is treated as failed rather
     * than silently left as an unhandled status (domain.md §6).
     */
    private function mapTradeStatus(?string $tradeStatus): PaymentStatus
    {
        return match ($tradeStatus) {
            'PAY_SUCCESS' => PaymentStatus::Completed,
            'WAIT_PAY', 'USERPAYING' => PaymentStatus::Pending,
            default => PaymentStatus::Failed,
        };
    }

    /**
     * @return array<string, mixed>
     */
    private function buildRefundParams(string $merchantOrderId, string $amount, string $reason): array
    {
        $params = [
            'timestamp' => (string) now()->timestamp,
            'method' => 'kbz.payment.refund',
            'nonce_str' => uniqid(),
            'version' => '1.0',
            'biz_content' => [
                'appid' => $this->appId,
                'merch_code' => $this->merchantCode,
                'merch_order_id' => $merchantOrderId,
                'refund_request_no' => $this->refundRequestNo(),
                // Unlike bnf_event (refund_amount was commented out, full-refund
                // only), this is wired through to support partial refunds —
                // domain.md §6.
                'refund_amount' => $amount,
                'refund_reason' => $reason,
            ],
        ];

        $params['sign'] = KbzSignature::sign($params, $this->merchantKey);
        $params['sign_type'] = 'SHA256';

        return $params;
    }

    private function refundRequestNo(): string
    {
        return now()->format('YmdHi').strtoupper(Str::random(8));
    }

    /**
     * mTLS options for the refund call — KBZ requires a client cert/key +
     * CA bundle on `kbz.payment.refund` specifically (domain.md §6).
     *
     * @return array<string, mixed>
     */
    private function mtlsOptions(): array
    {
        $options = [];

        if ($this->certPath !== null) {
            $options['cert'] = $this->certPassword !== null
                ? [$this->certPath, $this->certPassword]
                : $this->certPath;
        }

        if ($this->certKeyPath !== null) {
            $options['ssl_key'] = $this->certPassword !== null
                ? [$this->certKeyPath, $this->certPassword]
                : $this->certKeyPath;
        }

        if ($this->caPath !== null) {
            $options['verify'] = $this->caPath;
        }

        return $options;
    }

    public function createOrderInfo($prepayId): array
    {
        return [
            'appid' => $this->appId,
            'merch_code' => $this->merchantCode,
            'nonce_str' => uniqid(),
            'prepay_id' => $prepayId,
            'timestamp' => (string)time()
        ];
    }
}
