<?php

namespace OpenCloud\Common\Service;

use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Middleware as GuzzleMiddleware;
use OpenCloud\Common\Auth\Token;
use OpenCloud\Common\Transport\HandlerStack;
use OpenCloud\Common\Transport\Middleware;
use OpenCloud\Common\Transport\Utils;
use OpenCloud\Identity\v3\Service;

/**
 * A Builder for easily creating OpenCloud services.
 *
 * @package OpenCloud\Common\Service
 */
class Builder
{
    /**
     * Global options that will be applied to every service created by this builder.
     *
     * @var array
     */
    private $globalOptions = [];

    /** @var string */
    private $rootNamespace;

    /**
     * Defaults that will be applied to options if no values are provided by the user.
     *
     * @var array
     */
    private $defaults = ['urlType' => 'publicURL'];

    /**
     * @param array $globalOptions Options that will be applied to every service created by this builder.
     *                             Eventually they will be merged (and if necessary overridden) by the
     *                             service-specific options passed in.
     */
    public function __construct(array $globalOptions = [], $rootNamespace = 'OpenCloud')
    {
        $this->globalOptions = $globalOptions;
        $this->rootNamespace = $rootNamespace;
    }

    /**
     * Internal method which resolves the API and Service classes for a service.
     *
     * @param string $serviceName    The name of the service, e.g. Compute
     * @param int    $serviceVersion The major version of the service, e.g. 2
     *
     * @return array
     */
    private function getClasses($serviceName, $serviceVersion)
    {
        $rootNamespace = sprintf("%s\\%s\\v%d", $this->rootNamespace, $serviceName, $serviceVersion);

        return [
            sprintf("%s\\Api", $rootNamespace),
            sprintf("%s\\Service", $rootNamespace),
        ];
    }

    /**
     * This method will return an OpenCloud service ready fully built and ready for use. There is
     * some initial setup that may prohibit users from directly instantiating the service class
     * directly - this setup includes the configuration of the HTTP client's base URL, and the
     * attachment of an authentication handler.
     *
     * @param $serviceName          The name of the service as it appears in the OpenCloud\* namespace
     * @param $serviceVersion       The major version of the service
     * @param array $serviceOptions The service-specific options to use
     *
     * @return \OpenCloud\Common\Service\ServiceInterface
     *
     * @throws \Exception
     */
    public function createService($serviceName, $serviceVersion, array $serviceOptions = [])
    {
        $options = $this->mergeOptions($serviceOptions);

        $this->stockIdentityService($options);
        $this->stockAuthHandler($options);
        $this->stockHttpClient($options, $serviceName);

        list($apiClass, $serviceClass) = $this->getClasses($serviceName, $serviceVersion);

        return new $serviceClass($options['httpClient'], new $apiClass());
    }

    private function stockHttpClient(array &$options, $serviceName)
    {
        if (!isset($options['httpClient']) || !($options['httpClient'] instanceof ClientInterface)) {
            if (strcasecmp($serviceName, 'identity') === 0) {
                $baseUrl = $options['authUrl'];
                $stack = $this->getStack($options['authHandler']);
            } else {
                list($token, $baseUrl) = $options['identityService']->authenticate($options);
                $stack = $this->getStack($options['authHandler'], $token);
            }

            $this->addDebugMiddleware($options, $stack);

            $options['httpClient'] = $this->httpClient($baseUrl, $stack);
        }
    }

    /**
     * @codeCoverageIgnore
     */
    private function addDebugMiddleware(array $options, HandlerStack &$stack)
    {
        if (!empty($options['debugLog'])
            && !empty($options['logger'])
            && !empty($options['messageFormatter'])
        ) {
            $stack->push(GuzzleMiddleware::log($options['logger'], $options['messageFormatter']));
        }
    }

    private function stockIdentityService(array &$options)
    {
        if (!isset($options['identityService'])) {
            $httpClient = $this->httpClient($options['authUrl'], HandlerStack::create());
            $options['identityService'] = Service::factory($httpClient);
        }
    }

    /**
     * @param array $options
     * @codeCoverageIgnore
     */
    private function stockAuthHandler(array &$options)
    {
        if (!isset($options['authHandler'])) {
            $options['authHandler'] = function () use ($options) {
                return $options['identityService']->generateToken($options);
            };
        }
    }

    private function getStack(callable $authHandler, Token $token = null)
    {
        $stack = HandlerStack::create();
        $stack->push(Middleware::authHandler($authHandler, $token));
        return $stack;
    }

    private function httpClient($baseUrl, HandlerStack $stack)
    {
        return new Client([
            'base_uri' => Utils::normalizeUrl($baseUrl),
            'handler'  => $stack,
        ]);
    }

    private function mergeOptions(array $serviceOptions)
    {
        $options = array_merge($this->defaults, $this->globalOptions, $serviceOptions);

        if (!isset($options['authUrl'])) {
            throw new \InvalidArgumentException('"authUrl" is a required option');
        }

        return $options;
    }
}