<?php

namespace BeSimple\SoapClient\Curl;

use BeSimple\SoapClient\Curl\Http\HttpAuthenticationBasicOptions;
use BeSimple\SoapClient\Curl\Http\HttpAuthenticationDigestOptions;
use Exception;

class Curl
{
    const CURL_SUCCESS = true;
    const CURL_FAILED = false;

    private $curlSession;
    private $options;

    /**
     * @param CurlOptions $options
     */
    public function __construct(CurlOptions $options)
    {
        $this->curlSession = $this->acquireNewCurlSession($options);
        $this->options = $options;
    }

    public function __destruct()
    {
        $this->closeCurlSession($this->curlSession);
    }

    /**
     * @param string $location       HTTP location
     * @param string $request        Request body
     * @param array  $requestHeaders Request header strings
     * @return CurlResponse
     */
    public function executeCurlWithCachedSession($location, $request = null, $requestHeaders = [])
    {
        return $this->executeCurlSession($this->curlSession, $this->options, $location, $request, $requestHeaders);
    }

    /**
     * @param CurlOptions   $options
     * @param string        $location           HTTP location
     * @param string        $request            Request body
     * @param array         $requestHeaders     Request header strings
     * @return CurlResponse
     */
    public function executeCurl(CurlOptions $options, $location, $request = null, $requestHeaders = [])
    {
        $curlSession = $this->acquireNewCurlSession($options);
        $curlResponse = $this->executeCurlSession($curlSession, $options, $location, $request, $requestHeaders);
        $this->closeCurlSession($curlSession);

        return $curlResponse;
    }

    private function acquireNewCurlSession(CurlOptions $options)
    {
        $curlSession = curl_init();
        curl_setopt_array($curlSession, [
            CURLOPT_ENCODING => '',
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_FAILONERROR => false,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_HEADER => true,
            CURLOPT_USERAGENT => $options->getUserAgent(),
            CURLINFO_HEADER_OUT => true,
            CURLOPT_CONNECTTIMEOUT => $options->getConnectionTimeout()
        ]);

        return $curlSession;
    }

    private function closeCurlSession($curlSession)
    {
        curl_close($curlSession);
    }

    /**
     * @param mixed         $curlSession        Result of curl_init() handle
     * @param CurlOptions   $options
     * @param string        $location           HTTP location
     * @param string        $request            Request body
     * @param array         $requestHeaders     Request header strings
     * @return CurlResponse
     */
    private function executeCurlSession($curlSession, CurlOptions $options, $location, $request = null, $requestHeaders = [])
    {
        curl_setopt($curlSession, CURLOPT_URL, $location);
        curl_setopt($curlSession, CURLOPT_HEADER, true);
        curl_setopt($curlSession, CURLOPT_RETURNTRANSFER, true);
        if ($request !== null) {
            curl_setopt($curlSession, CURLOPT_POST, true);
            curl_setopt($curlSession, CURLOPT_POSTFIELDS, $request);
        } else {
            curl_setopt($curlSession, CURLOPT_POST, false);
        }
        if (count($requestHeaders) > 0) {
            curl_setopt($curlSession, CURLOPT_HTTPHEADER, $requestHeaders);
        }
        if (!$options->getSoapCompression() & SOAP_COMPRESSION_ACCEPT) {
            curl_setopt($curlSession, CURLOPT_ENCODING, 'identity');
        }
        if ($options->hasProxy()) {
            $proxyHost = $options->getProxy()->getHost() . $options->getProxy()->getPort();
            curl_setopt($curlSession, CURLOPT_PROXY, $proxyHost);
            if ($options->getProxy()->hasCredentials()) {
                curl_setopt($curlSession, CURLOPT_PROXYUSERPWD, $options->getProxy()->getLogin() . ':' . $options->getProxy()->getPassword());
                if ($options->getProxy()->hasAuthenticationType()) {
                    curl_setopt($curlSession, CURLOPT_PROXYAUTH, $options->getProxy()->getAuthenticationType());
                }
            }
        }
        if ($options->hasHttpAuthentication()) {
            if ($options->hasHttpAuthenticationBasic()) {
                /** @var HttpAuthenticationBasicOptions $httpAuthenticationBasic */
                $httpAuthenticationBasic = $options->getHttpAuthentication();
                curl_setopt($curlSession, CURLOPT_HTTPAUTH, $httpAuthenticationBasic->getAuthenticationType());
                curl_setopt($curlSession, CURLOPT_USERPWD, $httpAuthenticationBasic->getUsername() . ':' . $httpAuthenticationBasic->getPassword());
            } elseif ($options->hasHttpAuthenticationDigest()) {
                /** @var HttpAuthenticationDigestOptions $httpAuthenticationDigest */
                $httpAuthenticationDigest = $options->getHttpAuthentication();
                curl_setopt($curlSession, CURLOPT_HTTPAUTH, $httpAuthenticationDigest->getAuthenticationType());
            } else {
                throw new Exception('Unresolved authentication type: '.get_class($options->getHttpAuthentication()));
            }
        }
        if ($options->hasSslCertificateOptions()) {
            $sslCertificateOptions = $options->getSslCertificateOptions();
            curl_setopt($curlSession, CURLOPT_SSLCERT, $sslCertificateOptions->getCertificateLocalPath());
            if ($sslCertificateOptions->hasCertificatePassPhrase()) {
                curl_setopt($curlSession, CURLOPT_SSLCERTPASSWD, $sslCertificateOptions->getCertificatePassPhrase());
            }
            if ($sslCertificateOptions->hasCertificateAuthorityInfo()) {
                curl_setopt($curlSession, CURLOPT_CAINFO, $sslCertificateOptions->getCertificateAuthorityInfo());
            }
            if ($sslCertificateOptions->hasCertificateAuthorityPath()) {
                curl_setopt($curlSession, CURLOPT_CAPATH, $sslCertificateOptions->hasCertificateAuthorityPath());
            }
        }

        if ($options->hasSslVersion()) {
            curl_setopt($curlSession, CURLOPT_SSLVERSION, $options->getSslVersion());
        }

        $executeSoapCallResponse = $this->executeHttpCall($curlSession, $options);

        $httpRequestHeadersAsString = curl_getinfo($curlSession, CURLINFO_HEADER_OUT);
        $headerSize = curl_getinfo($curlSession, CURLINFO_HEADER_SIZE);
        $httpResponseCode = curl_getinfo($curlSession, CURLINFO_HTTP_CODE);
        $httpResponseContentType = curl_getinfo($curlSession, CURLINFO_CONTENT_TYPE);;
        $responseBody = substr($executeSoapCallResponse, $headerSize);
        $responseHeaders = substr($executeSoapCallResponse, 0, $headerSize);
        preg_match('/HTTP\/(1\.[0-1]+) ([0-9]{3}) (.*)/', $executeSoapCallResponse, $httpResponseMessages);
        $httpResponseMessage = trim(array_pop($httpResponseMessages));
        $curlErrorMessage = sprintf(
            'Curl error "%s" with message: %s occurred while connecting to %s with HTTP response code %s',
            curl_errno($curlSession),
            curl_error($curlSession),
            $location,
            $httpResponseCode
        );

        if (!is_int($httpResponseCode) || $httpResponseCode >= 400 || $httpResponseCode === 0) {

            return new CurlResponse(
                $this->normalizeStringOrFalse($httpRequestHeadersAsString),
                $httpResponseCode,
                $httpResponseMessage,
                $httpResponseContentType,
                self::CURL_FAILED,
                $this->normalizeStringOrFalse($responseHeaders),
                $this->normalizeStringOrFalse($responseBody),
                $curlErrorMessage
            );
        }

        return new CurlResponse(
            $this->normalizeStringOrFalse($httpRequestHeadersAsString),
            $httpResponseCode,
            $httpResponseMessage,
            $httpResponseContentType,
            self::CURL_SUCCESS,
            $this->normalizeStringOrFalse($responseHeaders),
            $this->normalizeStringOrFalse($responseBody)
        );
    }

    /**
     * Custom curl_exec wrapper that allows to follow redirects when specific
     * http response code is set. SOAP only allows 307.
     *
     * @param mixed         $curlSession        Result of curl_init() handle
     * @param CurlOptions   $options
     * @param int           $executedRedirects
     * @return string|null
     * @throws Exception
     */
    private function executeHttpCall($curlSession, CurlOptions $options, $executedRedirects = 0)
    {
        if ($executedRedirects > $options->getFollowLocationMaxRedirects()) {
            throw new Exception('Cannot executeHttpCall - too many redirects: ' . $executedRedirects);
        }
        $curlExecResponse = curl_exec($curlSession);
        $httpResponseCode = curl_getinfo($curlSession, CURLINFO_HTTP_CODE);
        if ($httpResponseCode === 307) {
            $newUrl = $this->getRedirectUrlFromResponseHeaders($curlSession, $curlExecResponse);
            curl_setopt($curlSession, CURLOPT_URL, $newUrl);

            return $this->executeHttpCall($curlSession, $options, ++$executedRedirects);
        }

        return $curlExecResponse;
    }

    private function getRedirectUrlFromResponseHeaders($curlSession, $curlExecResponse)
    {
        $curlExecResponseHeaders = substr($curlExecResponse, 0, curl_getinfo($curlSession, CURLINFO_HEADER_SIZE));
        $matches = [];
        preg_match('/Location:(.*?)\n/', $curlExecResponseHeaders, $matches);
        $url = trim(array_pop($matches));

        if (($url = @parse_url($url)) !== false) {
            $lastUrl = parse_url(curl_getinfo($curlSession, CURLINFO_EFFECTIVE_URL));
            if (!isset($url['scheme'])) {
                $url['scheme'] = $lastUrl['scheme'];
            }
            if (!isset($url['host'])) {
                $url['host'] = $lastUrl['host'];
            }
            if (!isset($url['path'])) {
                $url['path'] = $lastUrl['path'];
            }

            return $url['scheme'] . '://' . $url['host'] . $url['path'] . ($url['query'] ? '?' . $url['query'] : '');
        }

        throw new Exception('Cannot parse WSDL url redirect: ' . $url);
    }

    private function normalizeStringOrFalse($string)
    {
        if ($string === false || $string === '') {
            $string = null;
        }

        return $string;
    }
}
