<?php

namespace AuthStack\UserDirectory\Service;

use AuthStack\UserDirectory\Exceptions\JsonDecodingException;
use AuthStack\UserDirectory\Exceptions\TransferException;
use AuthStack\UserDirectory\Exceptions\UserDirectoryException;
use Buckhill\Support\Traits\DotNotationAccessorTrait;
use GuzzleHttp\Client;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\RequestException;

abstract class RESTService
{
    use DotNotationAccessorTrait;

    protected $httpClient;
    protected $baseUrl;
    protected $config = [];
    protected $tz_offset = null;
    protected $tz_dst = null;

    public function __construct($baseUrl = '', array $options = [], array $routes = [], string $tz_offset = null, string $tz_dst = null)
    {
        $this->baseUrl = $baseUrl;

        $this->config = [
            'options' => $options,
            'routes' => $routes
        ];

        $this->tz_offset = $tz_offset;
        $this->tz_dst = $tz_dst;
    }

    public function setTimeZoneOffset(string $offset): self
    {
        $this->tz_offset = $offset;

        return $this;
    }

    public function setTimeZoneDST(string $dst): self
    {
        $this->tz_dst = $dst;

        return $this;
    }

    public function getHttpClient(): Client
    {
        if(!$this->httpClient instanceof Client)
        {
            $this->httpClient = new Client();
        }

        return $this->httpClient;
    }

    protected function section($section)
    {
        $verb       = $this->config(sprintf("routes.%s.method", $section));
        $path       = sprintf("%s%s", $this->getBaseUrl(), $this->config(sprintf("routes.%s.path", $section)));
        $verify     = $this->config(sprintf("routes.%s.verify", $section), false);

        return [
            'method' => $verb,
            'path' => $path,
            'verify' => $verify
        ];
    }

    protected function config($path, $default = null)
    {
        return $this->dotNotationFromArray($this->config, $path, $default);
    }

    /**
     * @param array $options
     * @return array
     */
    protected function options(array $options)
    {
        return array_merge($this->config('options', []), $options);
    }

    /**
     * @return string
     */
    protected function getBaseUrl()
    {
        return $this->baseUrl;
    }

    /**
     * @param callable $callback
     * @param $nextException
     * @param callable|null $afterException
     * @return mixed
     */
    protected function remote(callable $callback, $nextException = UserDirectoryException::class, callable $afterException = null)
    {
        try
        {
            return $callback();
        }
        catch(\Exception $e)
        {
            // Useful for error logging
            if(is_callable($afterException))
            {
                $exception = $this->getExceptionFromResponse($e, $nextException);

                $afterException($exception);
            }

            $this->handleException($e, $nextException);
        }
    }

    /**
     * @param $cfg_section
     * @param array|null $form_params
     * @param array|null $path_params
     * @param array|null $get_params
     * @return mixed|ResponseInterface
     */
    protected function request($cfg_section, array $form_params = null, array $path_params = null, array $get_params = null)
    {
        $client = $this->getHttpClient();
        $section = $this->section($cfg_section);

        $path = !empty($path_params) ? vsprintf($section['path'], $path_params) : $section['path'];

        $options = array_merge($this->config['options'], ['verify' => $section['verify']]);

        // Add time zone headers
        if(!is_null($this->tz_offset) && !is_null($this->tz_dst))
        {
            if(!isset($options['headers'])) $options['headers'] = [];

            $options['headers']['X-TimeZoneOffset'] = $this->tz_offset;
            $options['headers']['X-TimeZoneDST'] = $this->tz_dst;
        }

        if(!empty($form_params))
        {
            $options['form_params'] = $form_params;
        }

        if(!empty($get_params))
        {
            $options['query'] = $get_params;
        }

        return $client->request($section['method'], $path, $options);
    }

    /**
     * @param array ...$params
     * @return array
     * @throws JsonDecodingException
     */
    protected function json(...$params): array
    {
        $result = $this->request(...$params);

        $json = @json_decode($result->getBody(), true);

        if(json_last_error() !== JSON_ERROR_NONE)
        {
            throw new JsonDecodingException(json_last_error_msg(), json_last_error());
        }

        return $json;
    }

    /**
     * @param RequestException $e
     * @param $nextException
     * @throws TransferException
     * @throws UserDirectoryException
     */
    protected function handleException(\Exception $e, $nextException)
    {
        if(method_exists($e, 'getResponse'))
        {
            $response = $e->getResponse();

            if($this->isJson($response))
            {
                $body = @json_decode($e->getResponse()->getBody(), true);

                $message = isset($body) && is_array($body) ? $body : $e->getResponse()->getBody()->getContents();

                throw new $nextException($message, (int)$e->getCode());
            }

            throw new TransferException($e->getResponse()->getBody()->getContents(), $e->getCode());
        }

        throw new UserDirectoryException($e->getMessage(), $e->getCode());
    }

    /**
     * @param \Exception $e
     * @param $nextException
     * @return \Exception
     */
    protected function getExceptionFromResponse(\Exception $e, $nextException): \Exception
    {
        if(method_exists($e, 'getResponse'))
        {
            $response = $e->getResponse();

            if($this->isJson($response))
            {
                $body = @json_decode($e->getResponse()->getBody(), true);

                $message = isset($body) && is_array($body) ? $body : $e->getResponse()->getBody()->getContents();

                return new $nextException($message, (int)$e->getCode());
            }
        }

        return new $nextException(['message' => 'Uknown error during transfer'], 500);
    }

    /**
     * @param ResponseInterface $response
     * @return bool
     */
    protected function isJson(ResponseInterface $response)
    {
        $result = false;

        if($response->hasHeader('content-type'))
        {
            $contentType = $response->getHeader('content-type');

            if(is_string($contentType))
            {
                $result = 0 === strcmp($contentType, 'application/json');
            }

            if(is_array($contentType))
            {
                foreach($contentType as $ctype)
                {
                    $result = 0 === strcmp($ctype, 'application/json');

                    if($result)
                        break;
                }
            }
        }

        return $result;
    }
}