<?php


namespace BitPaySDK\Util\RESTcli;


use BitPayKeyUtils\KeyHelper\PrivateKey;
use BitPaySDK\Env;
use BitPaySDK\Exceptions\BitPayException;
use Exception;
use GuzzleHttp\Client as GuzzleHttpClient;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\InvalidArgumentException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Exception\TooManyRedirectsException;
use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\Psr7\Response as Response;
use GuzzleHttp\RequestOptions as RequestOptions;

class RESTcli
{
    /**
     * @var GuzzleHttpClient
     */
    protected $_client;
    /**
     * @var string
     */
    protected $_baseUrl;
    /**
     * @var PrivateKey
     */
    protected $_ecKey;
    /**
     * @var string
     */
    protected $_identity;

    /**
     * @var string
     */
    protected $_proxy;

    public function __construct(string $environment, PrivateKey $ecKey, ?string $proxy = null)
    {
        $this->_ecKey = $ecKey;
        $this->_baseUrl = $environment == Env::Test ? Env::TestUrl : Env::ProdUrl;
        $this->_proxy = $proxy !== null ? trim($proxy) : '';
        $this->init();
    }

    public function init()
    {
        try {
            $this->_identity = $this->_ecKey->getPublicKey()->__toString();
            $config = [
                'base_url' => $this->_baseUrl,
                'defaults' => [
                    'headers' => [
                        'x-accept-version'           => Env::BitpayApiVersion,
                        'x-bitpay-plugin-info'       => Env::BitpayPluginInfo,
                        'x-bitpay-api-frame'         => Env::BitpayApiFrame,
                        'x-bitpay-api-frame-version' => Env::BitpayApiFrameVersion,
                    ],
                ],
            ];

            if ($this->_proxy !== '') {
                $config['proxy'] = $this->_proxy;
            }

            $this->_client = new GuzzleHttpClient($config);
        } catch (Exception $e) {
            throw new BitPayException("RESTcli init failed : ".$e->getMessage());
        }
    }

    public function post($uri, array $formData = [], $signatureRequired = true): string
    {
        try {
            $fullURL = $this->_baseUrl.$uri;
            $headers = [
                'Content-Type'               => 'application/json',
                'x-accept-version'           => Env::BitpayApiVersion,
                'x-bitpay-plugin-info'       => Env::BitpayPluginInfo,
                'x-bitpay-api-frame'         => Env::BitpayApiFrame,
                'x-bitpay-api-frame-version' => Env::BitpayApiFrameVersion,
            ];

            if ($signatureRequired) {
                $headers['x-signature'] = $this->_ecKey->sign($fullURL.json_encode($formData));
                $headers['x-identity'] = $this->_identity;
            }

            /**
             * @var Response
             */
            $response = $this->_client->requestAsync(
                'POST', $fullURL, [
                $options[RequestOptions::SYNCHRONOUS] = false,
                'headers'            => $headers,
                RequestOptions::JSON => $formData,
            ])->wait();

            $responseJson = $this->responseToJsonString($response);

            return $responseJson;
        } catch (BadResponseException $e) {
            throw new BitPayException("POST failed : Guzzle/BadResponseException : ".$e->getMessage());
        } catch (ClientException $e) {
            throw new BitPayException("POST failed : Guzzle/ClientException : ".$e->getMessage());
        } catch (ConnectException $e) {
            throw new BitPayException("POST failed : Guzzle/ConnectException : ".$e->getMessage());
        } catch (GuzzleException $e) {
            throw new BitPayException("POST failed : Guzzle/GuzzleException : ".$e->getMessage());
        } catch (InvalidArgumentException $e) {
            throw new BitPayException("POST failed : Guzzle/InvalidArgumentException : ".$e->getMessage());
        } catch (RequestException $e) {
            throw new BitPayException("POST failed : Guzzle/RequestException : ".$e->getMessage());
        } catch (ServerException $e) {
            throw new BitPayException("POST failed : Guzzle/ServerException : ".$e->getMessage());
        } catch (TooManyRedirectsException $e) {
            throw new BitPayException("POST failed : Guzzle/TooManyRedirectsException : ".$e->getMessage());
        } catch (Exception $e) {
            throw new BitPayException("POST failed : ".$e->getMessage());
        }
    }

    public function get($uri, array $parameters = null, $signatureRequired = true): string
    {
        try {
            $fullURL = $this->_baseUrl.$uri;
            $headers = [
                'Content-Type'               => 'application/json',
                'x-accept-version'           => Env::BitpayApiVersion,
                'x-bitpay-plugin-info'       => Env::BitpayPluginInfo,
                'x-bitpay-api-frame'         => Env::BitpayApiFrame,
                'x-bitpay-api-frame-version' => Env::BitpayApiFrameVersion,
            ];

            if ($parameters) {
                $fullURL .= '?'.http_build_query($parameters);
            }

            if ($signatureRequired) {
                $headers['x-signature'] = $this->_ecKey->sign($fullURL);
                $headers['x-identity'] = $this->_identity;
            }

            /**
             * @var Response
             */
            $response = $this->_client->requestAsync(
                'GET', $fullURL, [
                $options[RequestOptions::SYNCHRONOUS] = false,
                'headers' => $headers,
                'query'   => $parameters,
            ])->wait();

            $responseJson = $this->responseToJsonString($response);

            return $responseJson;
        } catch (BadResponseException $e) {
            throw new BitPayException("GET failed : Guzzle/BadResponseException : ".$e->getMessage());
        } catch (ClientException $e) {
            throw new BitPayException("GET failed : Guzzle/ClientException : ".$e->getMessage());
        } catch (ConnectException $e) {
            throw new BitPayException("GET failed : Guzzle/ConnectException : ".$e->getMessage());
        } catch (GuzzleException $e) {
            throw new BitPayException("GET failed : Guzzle/GuzzleException : ".$e->getMessage());
        } catch (InvalidArgumentException $e) {
            throw new BitPayException("GET failed : Guzzle/InvalidArgumentException : ".$e->getMessage());
        } catch (RequestException $e) {
            throw new BitPayException("GET failed : Guzzle/RequestException : ".$e->getMessage());
        } catch (ServerException $e) {
            throw new BitPayException("GET failed : Guzzle/ServerException : ".$e->getMessage());
        } catch (TooManyRedirectsException $e) {
            throw new BitPayException("GET failed : Guzzle/TooManyRedirectsException : ".$e->getMessage());
        } catch (Exception $e) {
            throw new BitPayException("GET failed : ".$e->getMessage());
        }
    }

    public function delete($uri, array $parameters = null): string
    {
        try {
            $fullURL = $this->_baseUrl.$uri;
            if ($parameters) {
                $fullURL .= '?'.http_build_query($parameters);
            }

            $headers = [
                'x-accept-version'           => Env::BitpayApiVersion,
                'x-bitpay-plugin-info'       => Env::BitpayPluginInfo,
                'x-bitpay-api-frame'         => Env::BitpayApiFrame,
                'x-bitpay-api-frame-version' => Env::BitpayApiFrameVersion,
                'Content-Type'               => 'application/json',
                'x-signature'                => $this->_ecKey->sign($fullURL),
                'x-identity'                 => $this->_identity,
            ];

            /**
             * @var Response
             */
            $response = $this->_client->requestAsync(
                'DELETE', $fullURL, [
                $options[RequestOptions::SYNCHRONOUS] = false,
                'headers' => $headers,
                'query'   => $parameters,
            ])->wait();

            $responseJson = $this->responseToJsonString($response);

            return $responseJson;
        } catch (BadResponseException $e) {
            throw new BitPayException("DELETE failed : Guzzle/BadResponseException : ".$e->getMessage());
        } catch (ClientException $e) {
            throw new BitPayException("DELETE failed : Guzzle/ClientException : ".$e->getMessage());
        } catch (ConnectException $e) {
            throw new BitPayException("DELETE failed : Guzzle/ConnectException : ".$e->getMessage());
        } catch (GuzzleException $e) {
            throw new BitPayException("DELETE failed : Guzzle/GuzzleException : ".$e->getMessage());
        } catch (InvalidArgumentException $e) {
            throw new BitPayException("DELETE failed : Guzzle/InvalidArgumentException : ".$e->getMessage());
        } catch (RequestException $e) {
            throw new BitPayException("DELETE failed : Guzzle/RequestException : ".$e->getMessage());
        } catch (ServerException $e) {
            throw new BitPayException("DELETE failed : Guzzle/ServerException : ".$e->getMessage());
        } catch (TooManyRedirectsException $e) {
            throw new BitPayException("DELETE failed : Guzzle/TooManyRedirectsException : ".$e->getMessage());
        } catch (Exception $e) {
            throw new BitPayException("DELETE failed : ".$e->getMessage());
        }
    }

    public function update($uri, array $formData = []): string
    {
        try {
            $fullURL = $this->_baseUrl.$uri;
            $headers = [
                'x-accept-version'           => Env::BitpayApiVersion,
                'x-bitpay-plugin-info'       => Env::BitpayPluginInfo,
                'x-bitpay-api-frame'         => Env::BitpayApiFrame,
                'x-bitpay-api-frame-version' => Env::BitpayApiFrameVersion,
                'Content-Type'               => 'application/json',
                'x-signature'                => $this->_ecKey->sign($fullURL.json_encode($formData)),
                'x-identity'                 => $this->_identity,
            ];

            /**
             * @var Response
             */
            $response = $this->_client->requestAsync(
                'PUT', $fullURL, [
                $options[RequestOptions::SYNCHRONOUS] = false,
                'headers'            => $headers,
                RequestOptions::JSON => $formData,
            ])->wait();

            $responseJson = $this->responseToJsonString($response);

            return $responseJson;
        } catch (BadResponseException $e) {
            throw new BitPayException("UPDATE failed : Guzzle/BadResponseException : ".$e->getMessage());
        } catch (ClientException $e) {
            throw new BitPayException("UPDATE failed : Guzzle/ClientException : ".$e->getMessage());
        } catch (ConnectException $e) {
            throw new BitPayException("UPDATE failed : Guzzle/ConnectException : ".$e->getMessage());
        } catch (GuzzleException $e) {
            throw new BitPayException("UPDATE failed : Guzzle/GuzzleException : ".$e->getMessage());
        } catch (InvalidArgumentException $e) {
            throw new BitPayException("UPDATE failed : Guzzle/InvalidArgumentException : ".$e->getMessage());
        } catch (RequestException $e) {
            throw new BitPayException("UPDATE failed : Guzzle/RequestException : ".$e->getMessage());
        } catch (ServerException $e) {
            throw new BitPayException("UPDATE failed : Guzzle/ServerException : ".$e->getMessage());
        } catch (TooManyRedirectsException $e) {
            throw new BitPayException("UPDATE failed : Guzzle/TooManyRedirectsException : ".$e->getMessage());
        } catch (Exception $e) {
            throw new BitPayException("UPDATE failed : ".$e->getMessage());
        }
    }

    public function responseToJsonString(Response $response): string
    {
        if ($response == null) {
            throw new Exception("Error: HTTP response is null");
        }

        try {
            $body = json_decode($response->getBody()->getContents(), true);
            if ($this->_proxy !== '' && !is_array($body)) {
                throw new BitPayException("Please check your proxy settings, HTTP Code:".$response->getStatusCode().", failed to decode json: ".json_last_error_msg());
            }
            $error_message = false;
            $error_message = (!empty($body['error'])) ? $body['error'] : $error_message;
            $error_message = (!empty($body['errors'])) ? $body['errors'] : $error_message;
            $error_message = (is_array($error_message)) ? implode("\n", $error_message) : $error_message;
            if (false !== $error_message) {
                throw new BitpayException($error_message);
            }
            if (!empty($body['success'])) {
                return json_encode($body);
            }

            // TODO Temporary fix for legacy response
            if (!array_key_exists('data', $body)) {
                return json_encode($body);
            }

            return json_encode($body['data']);

        } catch (Exception $e) {
            throw new BitPayException("failed to retrieve HTTP response body : ".$e->getMessage());
        }
    }

}
