<?php

/**
 * Http_client is based on Pest.
 * Pest is a REST client for PHP.
 *
 * See http://github.com/educoder/pest for details.
 *
 * This code is licensed for use, modification, and distribution
 * under the terms of the MIT License (see http://en.wikipedia.org/wiki/MIT_License)
 */

interface HttpClientInterface
{
    public function get(string $url, array $headers = [], bool $htmlSpecialCharsEncode = true);

    public function post(string $url, array $data = [], array $headers = []);

    public function put(string $url, array $data = [], array $headers = []);

    public function patch(string $url, array $data = [], array $headers = []);

    public function delete(string $url, $data = false, array $headers = []);

    public function setupOauthAuth(string $token = '');

    public function lastStatus();
}

class Http_client implements HttpClientInterface
{
    protected $params = [];
    protected $client;
    protected $lastStatus;
    public $baseURL;

    public function __construct(array $params = [])
    {
        $this->baseURL = isset($params['base_url']) ? $params['base_url'] : '';

        // if there is no verify parameter then skip ssl verification for local URLs
        if (!isset($params['verify']) || !is_bool($params['verify'])) {
            $params['verify'] = !$this->isLocalIpAddressByUrl($this->baseURL);
        }

        if (isset($params['headers'])) {
            $this->params['headers'] = $params['headers'];
        }
        $this->client = new GuzzleHttp\Client([
            'base_uri' => $this->baseURL,
            'verify' => $params['verify'],
            'headers' => $params['headers'] ?? [],
            'proxy' => '',
            'curl' => ($params['curl'] ?? [])
        ]);
    }

    public function get(string $url, array $headers = [], bool $htmlSpecialCharsEncode = true, $processBody = true)
    {
        $this->params['headers'] = $this->combineHeaders($headers);

        $response = $this->sendRequest($url, 'GET', $this->params);

        return $processBody ? $this->processBody(
            $response->getBody()->getContents(),
            $htmlSpecialCharsEncode
        ) : $response->getBody()->getContents();
    }

    public function post(string $url, array $data = [], array $headers = [])
    {
        $this->params['json'] = $data;
        $this->params['headers'] = $this->combineHeaders($headers);
        $response = $this->sendRequest($url, 'POST', $this->params);
        return $this->processBody($response->getBody()->getContents());
    }

    public function put(string $url, array $data = [], array $headers = [])
    {
        $this->params['json'] = $data;
        $this->params['headers'] = $this->combineHeaders($headers);
        $response = $this->sendRequest($url, 'PUT', $this->params);
        return $this->processBody($response->getBody()->getContents());
    }

    public function patch(string $url, array $data = [], array $headers = [])
    {
        $this->params['json'] = $data;
        $this->params['headers'] = $this->combineHeaders($headers);
        $response = $this->sendRequest($url, 'PATCH', $this->params);
        return $this->processBody($response->getBody()->getContents());
    }

    public function delete(string $url, $data = false, $headers = [])
    {
        $this->params['json'] = $data;
        $this->params['headers'] = $this->combineHeaders($headers);
        $response = $this->sendRequest($url, 'DELETE', $this->params);
        return $this->processBody($response->getBody()->getContents());
    }

    public function lastStatus()
    {
        return $this->lastStatus;
    }

    public function setupOauthAuth(?string $token = '')
    {
        if (isset($this->params['auth'])) {
            unset($this->params['auth']);
        }

        $this->params['headers']['Authorization'] = 'Bearer ' . $token;
    }

    public function setupAuth($user = '', $pass = '', $auth = 'basic')
    {
        if (isset($this->params['headers']['Authorization'])) {
            unset($this->params['headers']['Authorization']);
        }

        if ($user === false) {
            unset($this->params['auth']);
        } else {
            $this->params['auth'] = [$user, $pass, $auth];
        }
    }

    private function sendRequest($url, $method, $params)
    {
        try {
            $response = $this->client->request(
                $method,
                $this->prepareURL($url),
                $params
            );
            $this->lastStatus = $response->getStatusCode();
            log_message('debug', "HTTP request $method::$url, parameters: " . json_encode($params));
            return $response;
        } catch (\GuzzleHttp\Exception\ClientException $exception) {
            $this->processError($exception->getResponse());
        }
    }

    /**
     * When a relative URI is provided to a client,
     * the client will combine the base URI
     * with the relative URI using the rules described
     * in RFC 3986, section 2 https://tools.ietf.org/html/rfc3986#section-5.2
     *
     * To avoid errors we need to remove first `/` from relative URI
     * Otherwise relative url will become absolute
     */
    private function prepareURL(string $url): string
    {
        if ($url[0] == '/') {
            $url = substr($url, 1);
        }
        return $url;
    }

    private function processBody($body, $htmlSpecialCharsEncode = true)
    {
        $isJson = true;
        $result = @json_decode($body, JSON_OBJECT_AS_ARRAY);

        // json_decode returns null in case of bad json object, then the body is just a string
        if ($result === null) {
            $result = $body;
            $isJson = false;
        }

        if ($htmlSpecialCharsEncode === true) {
            if (isset($result) && is_array($result)) {
                htmlSpecialCharsArrayEncode($result);
            } else {
                $result = htmlspecialchars($result);
            }
        }

        return $isJson === true ? json_encode($result) : $result;
    }

    private function combineHeaders(array $headers = [])
    {
        if (
            (!array_key_exists(Cf_RestInstance::CF_2FA_TOKEN_HEADER, $headers) || empty($headers[Cf_RestInstance::CF_2FA_TOKEN_HEADER])) 
            && isset($_COOKIE['2FaCode'])
        ) {
            $headers[Cf_RestInstance::CF_2FA_TOKEN_HEADER] = $_COOKIE['2FaCode'];
        }
        
        return isset($this->params['headers']) && is_array($this->params['headers']) ?
            array_merge($this->params['headers'], $headers) :
            $headers;
    }

    private function isLocalIpAddressByUrl($url)
    {
        $host = parse_url($url, PHP_URL_HOST);
        return gethostbyname($host) == '127.0.0.1' || (isset($_SERVER['SERVER_NAME']) && $host === $_SERVER['SERVER_NAME']);
    }

    protected function processError($response)
    {
        $body = $response->getBody();
        $statusCode = $response->getStatusCode();
        switch ($statusCode) {
            case 400:
                throw new HttpClient_BadRequest($body);
                break;
            case 401:
                throw new HttpClient_Unauthorized($body);
                break;
            case 403:
                throw new HttpClient_Forbidden($body);
                break;
            case 404:
                throw new HttpClient_NotFound($body);
                break;
            case 405:
                throw new HttpClient_MethodNotAllowed($body);
                break;
            case 409:
                throw new HttpClient_Conflict($body);
                break;
            case 410:
                throw new HttpClient_Gone($body);
                break;
            case 422:
                throw new HttpClient_InvalidRecord($body);
                break;
            case 429:
                throw new HttpClient_TooManyRequests($body);
                break;
            default:
                if ($statusCode >= 400 && $statusCode <= 499) {
                    throw new HttpClient_ClientError($body);
                } elseif ($statusCode >= 500 && $statusCode <= 599) {
                    throw new HttpClient_ServerError($body);
                } elseif (!$statusCode || $statusCode >= 600) {
                    throw new HttpClient_UnknownResponse($body);
                }
        }
    }
}


class HttpClient_Exception extends Exception
{
}

class HttpClient_UnknownResponse extends HttpClient_Exception
{
}

/* 401-499 */
class HttpClient_ClientError extends HttpClient_Exception
{
}

/* 400 */
class HttpClient_BadRequest extends HttpClient_ClientError
{
}

/* 401 */
class HttpClient_Unauthorized extends HttpClient_ClientError
{
}

/* 403 */
class HttpClient_Forbidden extends HttpClient_ClientError
{
}

/* 404 */
class HttpClient_NotFound extends HttpClient_ClientError
{
}

/* 405 */
class HttpClient_MethodNotAllowed extends HttpClient_ClientError
{
}

/* 409 */
class HttpClient_Conflict extends HttpClient_ClientError
{
}

/* 410 */
class HttpClient_Gone extends HttpClient_ClientError
{
}

/* 422 */
class HttpClient_InvalidRecord extends HttpClient_ClientError
{
}

/* 429 */
class HttpClient_TooManyRequests extends HttpClient_ClientError
{
}

/* 500-599 */
class HttpClient_ServerError extends HttpClient_Exception
{
}
