<?php

namespace App\Services\Payment;

use App\Services\BaseService;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Log;

class NoPayService extends BaseService
{
    public const CHANNEL_SCAN = 'NOpay12';
    public const CHANNEL_BALANCE = 'NOpay13';
    public const WITHDRAW_CHANNEL = 'NOwithdraw';

    public const STATE_SUCCESS = '9';
    public const STATE_FAIL = '10';

    public static function isRechargeChannel(?string $channel): bool
    {
        return in_array(strtolower((string)$channel), [
            strtolower(self::CHANNEL_SCAN),
            strtolower(self::CHANNEL_BALANCE),
        ], true);
    }

    public static function isWithdrawChannel(?string $channel): bool
    {
        return strtolower((string)$channel) === strtolower(self::WITHDRAW_CHANNEL);
    }

    public static function canUserRecharge($userId): bool
    {
        $allowedUserIds = array_values(array_filter(array_map(
            'trim',
            explode(',', (string)config('app.no_pay_recharge_user_ids', ''))
        )));

        if (empty($allowedUserIds)) {
            return true;
        }

        return in_array((string)$userId, $allowedUserIds, true);
    }

    public static function paymentMethod(string $channel): int
    {
        return strtolower($channel) === strtolower(self::CHANNEL_BALANCE) ? 13 : 12;
    }

    public static function amount($amount): string
    {
        return number_format((float)$amount, 2, '.', '');
    }

    public static function getDepositMerchantId(): string
    {
        return (string)config('app.no_pay_deposit_mch_id');
    }

    public static function getWithdrawMerchantId(): string
    {
        return (string)config('app.no_pay_withdraw_mch_id');
    }

    public static function getDepositNotifyUrl(): string
    {
        return rtrim(config('app.url'), '/') . '/api/pay/harvest';
    }

    public static function getWithdrawNotifyUrl(): string
    {
        return rtrim(config('app.url'), '/') . '/api/pay/notify';
    }

    public static function signature(array $params, string $key): string
    {
        unset($params['sign']);
        $params = array_filter($params, static function ($value) {
            return $value !== null && $value !== '';
        });
        ksort($params, SORT_STRING);

        $parts = [];
        foreach ($params as $name => $value) {
            $parts[] = $name . '=' . $value;
        }
        $parts[] = 'key=' . $key;

        return hash('sha256', implode('&', $parts));
    }

    public static function pay($amount, string $orderNo, string $memberNo, string $channel): array
    {
        $data = self::signedData([
            'appId' => self::getDepositMerchantId(),
            'merchantMemberNo' => $memberNo,
            'merchantOrderNo' => $orderNo,
            'amount' => self::amount($amount),
            'paymentMethod' => self::paymentMethod($channel),
            'notifyUrl' => self::getDepositNotifyUrl(),
            'timestamp' => time(),
            'version' => 'v1',
        ], (string)config('app.no_pay_deposit_key'));

        return self::post((string)config('app.no_pay_deposit_gateway'), $data, self::getDepositMerchantId());
    }

    public static function queryPayOrder(string $orderNo, string $memberNo): array
    {
        $data = self::signedData([
            'appId' => self::getDepositMerchantId(),
            'merchantOrderNo' => $orderNo,
            'merchantMemberNo' => $memberNo,
            'timestamp' => time(),
            'version' => 'v1',
        ], (string)config('app.no_pay_deposit_key'));

        return self::post((string)config('app.no_pay_deposit_query_gateway'), $data, self::getDepositMerchantId());
    }

    public static function withdraw($amount, string $orderNo, string $memberNo, string $accountName, string $qAccount): array
    {
        $data = self::signedData([
            'appId' => self::getWithdrawMerchantId(),
            'merchantOrderNo' => $orderNo,
            'merchantMemberNo' => $memberNo,
            'amount' => self::amount($amount),
            'accountName' => $accountName,
            'notifyUrl' => self::getWithdrawNotifyUrl(),
            'qAccount' => $qAccount,
            'timestamp' => time(),
            'version' => 'v1',
        ], (string)config('app.no_pay_withdraw_key'));

        return self::post((string)config('app.no_pay_withdraw_gateway'), $data, self::getWithdrawMerchantId());
    }

    public static function verifyDepositNotify(array $params): bool
    {
        return self::verifyNotify($params, self::getDepositMerchantId(), (string)config('app.no_pay_deposit_key'));
    }

    public static function verifyWithdrawNotify(array $params): bool
    {
        return self::verifyNotify($params, self::getWithdrawMerchantId(), (string)config('app.no_pay_withdraw_key'));
    }

    public static function depositNotifySignatureDiagnostics(array $params): array
    {
        return self::signatureDiagnostics($params, (string)config('app.no_pay_deposit_key'));
    }

    public static function withdrawNotifySignatureDiagnostics(array $params): array
    {
        return self::signatureDiagnostics($params, (string)config('app.no_pay_withdraw_key'));
    }

    private static function signedData(array $data, string $key): array
    {
        $data['sign'] = self::signature($data, $key);
        return $data;
    }

    private static function verifyNotify(array $params, string $merchantId, string $key): bool
    {
        if ($merchantId === '' || ($params['appId'] ?? '') !== $merchantId || empty($params['sign'])) {
            return false;
        }

        $receivedSign = strtolower((string)$params['sign']);
        foreach (self::signatureCandidates($params, $key) as $candidate) {
            if (hash_equals($candidate, $receivedSign)) {
                return true;
            }
        }

        return false;
    }

    private static function signatureCandidates(array $params, string $key): array
    {
        $candidates = [self::signature($params, $key)];

        // Some NO environments sign callbacks with the implicit default version
        // without including the version field in the callback payload.
        if (!isset($params['version'])) {
            $params['version'] = 'v1';
            $candidates[] = self::signature($params, $key);
        }

        return array_values(array_unique($candidates));
    }

    private static function signatureDiagnostics(array $params, string $key): array
    {
        $fields = $params;
        unset($fields['sign']);

        return [
            'received_sign' => strtolower((string)($params['sign'] ?? '')),
            'calculated_signs' => self::signatureCandidates($params, $key),
            'signing_fields' => array_keys(array_filter($fields, static function ($value) {
                return $value !== null && $value !== '';
            })),
        ];
    }

    private static function post(string $url, array $data, string $merchantId): array
    {
        $logData = $data;
        unset($logData['sign'], $logData['password']);

        Log::info('NO钱包接口请求', [
            'url' => $url,
            'merchant_id' => $merchantId,
            'data' => $logData,
        ]);

        try {
            $response = (new Client(['timeout' => 10.0]))->post($url, [
                'json' => $data,
                'headers' => [
                    'Accept' => 'application/json',
                    'appId' => $merchantId,
                    'language' => 'zh_CN',
                ],
            ]);
            $body = $response->getBody()->getContents();

            Log::info('NO钱包接口响应', [
                'url' => $url,
                'merchant_id' => $merchantId,
                'http_status' => $response->getStatusCode(),
                'body' => $body,
            ]);

            return json_decode($body, true) ?: [];
        } catch (RequestException $e) {
            $response = $e->getResponse();
            $body = $response ? $response->getBody()->getContents() : '';

            Log::error('NO钱包接口请求失败', [
                'url' => $url,
                'merchant_id' => $merchantId,
                'http_status' => $response ? $response->getStatusCode() : null,
                'body' => $body,
                'error' => $e->getMessage(),
            ]);

            throw $e;
        } catch (\Throwable $e) {
            Log::error('NO钱包接口异常', [
                'url' => $url,
                'merchant_id' => $merchantId,
                'error' => $e->getMessage(),
            ]);

            throw $e;
        }
    }

    public static function getWhere(array $search = []): array
    {
        return [];
    }
}
