<?php

namespace App\Libraries;

use CodeIgniter\Config\Services;

/**
 * Client for NOWPayments API (https://api.nowpayments.io/v1).
 * Payments (x-api-key), crypto payouts (x-api-key + Bearer JWT for create/verify),
 * fiat payouts (Bearer JWT; account + request also send x-api-key per docs).
 *
 * JWT: use nowpayments.bearer_token, or set nowpayments.auth_email + nowpayments.auth_password
 * to obtain a token via POST /auth (cached ~4 minutes; upstream tokens expire in 5 minutes).
 */
class NowPaymentsService
{
    private const JWT_CACHE_KEY = 'nowpayments_jwt';

    /** Seconds; NOWPayments JWT lifetime is 5 minutes */
    private const JWT_CACHE_TTL = 240;

    private string $baseUrl;
    private string $apiKey;
    private string $bearerToken;
    private string $authEmail;
    private string $authPassword;
    private string $ipnSecret;

    public function __construct()
    {
        $this->baseUrl = rtrim($_ENV['nowpayments.base_url'] ?? 'https://api.nowpayments.io/v1', '/');
        $this->apiKey = $_ENV['nowpayments.api_key'] ?? '';
        $this->bearerToken = $_ENV['nowpayments.bearer_token'] ?? '';
        $this->authEmail = $_ENV['nowpayments.auth_email'] ?? '';
        $this->authPassword = $_ENV['nowpayments.auth_password'] ?? '';
        $this->ipnSecret = $_ENV['nowpayments.ipn_secret'] ?? '';
    }

    public function isConfigured(): bool
    {
        return $this->apiKey !== '';
    }

    public function isBearerConfigured(): bool
    {
        return trim($this->bearerToken) !== ''
            || (trim($this->authEmail) !== '' && $this->authPassword !== '');
    }

    public function isBearerStaticConfigured(): bool
    {
        return trim($this->bearerToken) !== '';
    }

    public function isAuthPasswordConfigured(): bool
    {
        return trim($this->authEmail) !== '' && $this->authPassword !== '';
    }

    public function isIpnSecretConfigured(): bool
    {
        return trim($this->ipnSecret) !== '';
    }

    /**
     * Verify NOWPayments IPN: HMAC-SHA512 of JSON with keys sorted recursively (see IPN setup in dashboard).
     * Header: x-nowpayments-sig (hex). If ipn_secret is not set, returns true and does not verify (not recommended for production).
     */
    public function verifyIpnSignature(string $rawBody, ?string $signatureHeader): bool
    {
        $secret = trim($this->ipnSecret);
        if ($secret === '') {
            log_message('warning', 'NOWPayments IPN: nowpayments.ipn_secret is empty; signature not verified');

            return true;
        }

        $sig = strtolower(trim((string) $signatureHeader));
        if ($sig === '') {
            return false;
        }

        $decoded = json_decode($rawBody, true);
        if (!is_array($decoded)) {
            return false;
        }

        $sorted = $this->sortArrayKeysRecursiveForIpn($decoded);
        $payload = json_encode($sorted, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($payload === false) {
            return false;
        }

        $expected = hash_hmac('sha512', $payload, $secret);

        return hash_equals(strtolower($expected), $sig);
    }

    /**
     * @param array<mixed> $data
     * @return array<mixed>
     */
    private function sortArrayKeysRecursiveForIpn(array $data): array
    {
        if ($data === []) {
            return [];
        }
        if (array_is_list($data)) {
            return array_map(function ($item) {
                return is_array($item) ? $this->sortArrayKeysRecursiveForIpn($item) : $item;
            }, $data);
        }
        ksort($data);
        foreach ($data as $k => $v) {
            $data[$k] = is_array($v) ? $this->sortArrayKeysRecursiveForIpn($v) : $v;
        }

        return $data;
    }

    /**
     * Resolve Authorization Bearer value: static env token, or JWT from POST /auth using dashboard credentials.
     */
    private function resolveBearerToken(): string
    {
        $static = trim($this->bearerToken);
        if ($static !== '') {
            return $static;
        }

        $email = trim($this->authEmail);
        if ($email === '' || $this->authPassword === '') {
            return '';
        }

        $cache = Services::cache();
        $cached = $cache->get(self::JWT_CACHE_KEY);
        if (is_string($cached) && $cached !== '') {
            return $cached;
        }

        $authResult = $this->requestAuthLogin($email, $this->authPassword);
        if (empty($authResult['success']) || !is_array($authResult['data'])) {
            log_message('error', 'NOWPayments /auth failed: ' . ($authResult['message'] ?? 'unknown'));

            return '';
        }

        $token = $this->extractJwtFromAuthResponse($authResult['data']);
        if ($token === '') {
            log_message('error', 'NOWPayments /auth: no token in response');

            return '';
        }

        $cache->save(self::JWT_CACHE_KEY, $token, self::JWT_CACHE_TTL);

        return $token;
    }

    /**
     * NOWPayments POST /auth JSON includes a top-level string field "token" (the JWT).
     *
     * @param array<string, mixed> $data
     */
    private function extractJwtFromAuthResponse(array $data): string
    {
        foreach (['token', 'access_token', 'jwt', 'accessToken'] as $key) {
            if (!empty($data[$key]) && is_string($data[$key])) {
                return $data[$key];
            }
        }
        if (isset($data['data']) && is_array($data['data'])) {
            return $this->extractJwtFromAuthResponse($data['data']);
        }

        return '';
    }

    /** POST /auth (dashboard email + password); no API key. */
    private function requestAuthLogin(string $email, string $password): array
    {
        return $this->request('POST', '/auth', [
            'json' => [
                'email' => $email,
                'password' => $password,
            ],
        ], 'none');
    }

    /** Clear cached JWT (e.g. after password change). */
    public function clearJwtCache(): void
    {
        Services::cache()->delete(self::JWT_CACHE_KEY);
    }

    /**
     * @param array<string, mixed> $options merged into curl request (json, query, etc.)
     * @return array{success: bool, message: string, data: mixed, status_code?: int}
     */
    private function request(string $method, string $path, array $options = [], string $authMode = 'api_key'): array
    {
        $authError = $this->authError($authMode);
        if ($authError !== null) {
            return [
                'success' => false,
                'message' => $authError,
                'data' => [],
            ];
        }

        $url = $this->baseUrl . (str_starts_with($path, '/') ? $path : '/' . $path);
        $hasJsonBody = isset($options['json']);
        $headers = $this->buildHeaders($authMode, $hasJsonBody);

        $client = Services::curlrequest();
        $defaults = [
            'headers' => $headers,
            'timeout' => 60,
            'http_errors' => false,
        ];
        $options = array_merge($defaults, $options);
        $options['headers'] = array_merge($headers, $options['headers'] ?? []);

        try {
            $response = $client->request($method, $url, $options);
            $body = $response->getBody();
            $data = $this->decodeResponseBody($body);
            $statusCode = $response->getStatusCode();

            if ($statusCode >= 200 && $statusCode < 300) {
                return [
                    'success' => true,
                    'message' => 'OK',
                    'data' => $data,
                ];
            }

            $message = is_array($data)
                ? ($data['message'] ?? $data['error'] ?? $data['detail'] ?? 'Request failed')
                : 'Request failed';
            if (is_array($message)) {
                $message = json_encode($message);
            }

            return [
                'success' => false,
                'message' => (string) $message,
                'data' => $data,
                'status_code' => $statusCode,
            ];
        } catch (\Throwable $e) {
            log_message('error', 'NOWPayments API request failed: ' . $e->getMessage());

            return [
                'success' => false,
                'message' => $e->getMessage(),
                'data' => [],
            ];
        }
    }

    private function authError(string $authMode): ?string
    {
        if ($authMode === 'none') {
            return null;
        }
        if ($authMode === 'api_key' && $this->apiKey === '') {
            return 'NOWPayments API key is not configured (nowpayments.api_key)';
        }
        if ($authMode === 'bearer' && $this->resolveBearerToken() === '') {
            return 'NOWPayments Bearer JWT: set nowpayments.bearer_token OR nowpayments.auth_email + nowpayments.auth_password';
        }
        if ($authMode === 'api_key_bearer') {
            if ($this->apiKey === '') {
                return 'NOWPayments API key is not configured (nowpayments.api_key)';
            }
            if ($this->resolveBearerToken() === '') {
                return 'NOWPayments Bearer JWT: set nowpayments.bearer_token OR nowpayments.auth_email + nowpayments.auth_password';
            }
        }

        return null;
    }

    /**
     * @return array<string, string>
     */
    private function buildHeaders(string $authMode, bool $jsonBody): array
    {
        $h = ['Accept' => 'application/json'];
        if ($jsonBody) {
            $h['Content-Type'] = 'application/json';
        }
        if ($this->apiKey !== '' && ($authMode === 'api_key' || $authMode === 'api_key_bearer')) {
            $h['x-api-key'] = $this->apiKey;
        }
        $bearer = ($authMode === 'bearer' || $authMode === 'api_key_bearer') ? $this->resolveBearerToken() : '';
        if ($bearer !== '') {
            $h['Authorization'] = 'Bearer ' . $bearer;
        }

        return $h;
    }

    /**
     * @return array<mixed>|array<string, mixed>
     */
    private function decodeResponseBody(string $body): array
    {
        $trim = trim($body);
        if ($trim === '') {
            return [];
        }
        $decoded = json_decode($trim, true);
        if (json_last_error() === JSON_ERROR_NONE) {
            return is_array($decoded) ? $decoded : ['value' => $decoded];
        }

        return ['raw' => $trim];
    }

    public function createPayment(array $body): array
    {
        return $this->request('POST', '/payment', ['json' => $body], 'api_key');
    }

    public function getPaymentStatus(string $paymentId): array
    {
        $id = rawurlencode($paymentId);

        return $this->request('GET', '/payment/' . $id, [], 'api_key');
    }

    public function getBalance(): array
    {
        return $this->request('GET', '/balance', [], 'api_key');
    }

    public function validatePayoutAddress(array $body): array
    {
        return $this->request('POST', '/payout/validate-address', ['json' => $body], 'api_key');
    }

    public function createPayout(array $body): array
    {
        return $this->request('POST', '/payout', ['json' => $body], 'api_key_bearer');
    }

    public function verifyPayout(string $batchWithdrawalId, array $body): array
    {
        $id = rawurlencode($batchWithdrawalId);

        return $this->request('POST', '/payout/' . $id . '/verify', ['json' => $body], 'api_key_bearer');
    }

    public function getPayoutStatus(string $payoutId): array
    {
        $id = rawurlencode($payoutId);

        return $this->request('GET', '/payout/' . $id, [], 'api_key');
    }

    public function fiatPayoutProviders(): array
    {
        return $this->request('GET', '/fiat-payouts/providers', [], 'bearer');
    }

    public function fiatPayoutFiatCurrencies(): array
    {
        return $this->request('GET', '/fiat-payouts/fiat-currencies', [], 'bearer');
    }

    public function fiatPayoutCryptoCurrencies(array $query): array
    {
        $query = $this->filterQuery($query);

        return $this->request('GET', '/fiat-payouts/crypto-currencies', ['query' => $query], 'bearer');
    }

    public function fiatPayoutPaymentMethods(array $query): array
    {
        $query = $this->filterQueryAllowNullString($query);

        return $this->request('GET', '/fiat-payouts/payment-methods', ['query' => $query], 'bearer');
    }

    public function fiatPayoutCreateAccount(array $body): array
    {
        return $this->request('POST', '/fiat-payouts/account', ['json' => $body], 'api_key_bearer');
    }

    public function fiatPayoutRequest(array $body): array
    {
        return $this->request('POST', '/fiat-payouts', ['json' => $body], 'api_key_bearer');
    }

    public function fiatPayoutList(array $query): array
    {
        $allowed = [
            'id', 'provider', 'requestId', 'fiatCurrency', 'cryptoCurrency', 'status', 'filter',
            'provider_payout_id', 'limit', 'page', 'orderBy', 'sortBy', 'dateFrom', 'dateTo',
        ];
        $filtered = [];
        foreach ($allowed as $key) {
            if (array_key_exists($key, $query) && $query[$key] !== null && $query[$key] !== '') {
                $filtered[$key] = $query[$key];
            }
        }

        return $this->request('GET', '/fiat-payouts', ['query' => $filtered], 'bearer');
    }

    private function filterQuery(array $query): array
    {
        $out = [];
        foreach ($query as $k => $v) {
            if ($v !== null && $v !== '') {
                $out[$k] = $v;
            }
        }

        return $out;
    }

    private function filterQueryAllowNullString(array $query): array
    {
        $out = [];
        foreach (['provider', 'currency'] as $k) {
            if (!array_key_exists($k, $query)) {
                continue;
            }
            $v = $query[$k];
            if ($v === null) {
                continue;
            }
            $out[$k] = $v;
        }

        return $out;
    }
}
