<?php
/**
 * Copyright 2012-2014 Rackspace US, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace OpenCloud;

use Guzzle\Http\Url;
use OpenCloud\Common\Exceptions;
use OpenCloud\Common\Http\Client;
use OpenCloud\Common\Http\Message\Formatter;
use OpenCloud\Common\Http\Message\RequestSubscriber;
use OpenCloud\Common\Lang;
use OpenCloud\Common\Log\Logger;
use OpenCloud\Common\Service\Catalog;
use OpenCloud\Common\Service\ServiceBuilder;
use OpenCloud\Identity\Resource\Tenant;
use OpenCloud\Identity\Resource\Token;
use OpenCloud\Identity\Resource\User;
use OpenCloud\Identity\Service as IdentityService;
use Psr\Log\LoggerInterface;

define('RACKSPACE_US', 'https://identity.api.rackspacecloud.com/v2.0/');
define('RACKSPACE_UK', 'https://lon.identity.api.rackspacecloud.com/v2.0/');

/**
 * The main client of the library. This object is the central point of negotiation between your application and the
 * API because it handles all of the HTTP transactions required to perform operations. It also manages the services
 * for your application through convenient factory methods.
 */
class OpenStack extends Client
{
    /**
     * @var array Credentials passed in by the user
     */
    private $secret = array();

    /**
     * @var string The token produced by the API
     */
    private $token;

    /**
     * @var string The unique identifier for who's accessing the API
     */
    private $tenant;

    /**
     * @var \OpenCloud\Common\Service\Catalog The catalog of services which are provided by the API
     */
    private $catalog;

    /**
     * @var LoggerInterface The object responsible for logging output
     */
    private $logger;

    /**
     * @var string The endpoint URL used for authentication
     */
    private $authUrl;

    /**
     * @var \OpenCloud\Identity\Resource\User
     */
    private $user;

    public function __construct($url, array $secret, array $options = array())
    {
        if (isset($options['logger']) && $options['logger'] instanceof LoggerInterface) {
            $this->setLogger($options['logger']);
        }

        $this->setSecret($secret);
        $this->setAuthUrl($url);

        parent::__construct($url, $options);

        $this->addSubscriber(RequestSubscriber::getInstance());
        $this->setDefaultOption('headers/Accept', 'application/json');
    }

    /**
     * Set the credentials for the client
     *
     * @param array $secret
     * @return $this
     */
    public function setSecret(array $secret = array())
    {
        $this->secret = $secret;

        return $this;
    }

    /**
     * Get the secret.
     *
     * @return array
     */
    public function getSecret()
    {
        return $this->secret;
    }

    /**
     * Set the token. If a string is passed in, the SDK assumes you want to set the ID of the full Token object
     * and sets this property accordingly. For any other data type, it assumes you want to populate the Token object.
     * This ambiguity arises due to backwards compatibility.
     *
     * @param  string $token
     * @return $this
     */
    public function setToken($token)
    {
        $identity = IdentityService::factory($this);

        if (is_string($token)) {
            if (!$this->token) {
                $this->setTokenObject($identity->resource('Token'));
            }
            $this->token->setId($token);
        } else {
            $this->setTokenObject($identity->resource('Token', $token));
        }

        return $this;
    }

    /**
     * Get the token ID for this client.
     *
     * @return string
     */
    public function getToken()
    {
        return ($this->getTokenObject()) ? $this->getTokenObject()->getId() : null;
    }

    /**
     * Set the full token object
     */
    public function setTokenObject(Token $token)
    {
        $this->token = $token;
    }

    /**
     * Get the full token object.
     */
    public function getTokenObject()
    {
        return $this->token;
    }

    /**
     * @deprecated
     */
    public function setExpiration($expiration)
    {
        $this->getLogger()->warning(Logger::deprecated(__METHOD__, '::getTokenObject()->setExpires()'));
        if ($this->getTokenObject()) {
            $this->getTokenObject()->setExpires($expiration);
        }

        return $this;
    }

    /**
     * @deprecated
     */
    public function getExpiration()
    {
        $this->getLogger()->warning(Logger::deprecated(__METHOD__, '::getTokenObject()->getExpires()'));
        if ($this->getTokenObject()) {
            return $this->getTokenObject()->getExpires();
        }
    }

    /**
     * Set the tenant. If an integer or string is passed in, the SDK assumes you want to set the ID of the full
     * Tenant object and sets this property accordingly. For any other data type, it assumes you want to populate
     * the Tenant object. This ambiguity arises due to backwards compatibility.
     *
     * @param  mixed $tenant
     * @return $this
     */
    public function setTenant($tenant)
    {
        $identity = IdentityService::factory($this);

        if (is_numeric($tenant) || is_string($tenant)) {
            if (!$this->tenant) {
                $this->setTenantObject($identity->resource('Tenant'));
            }
            $this->tenant->setId($tenant);
        } else {
            $this->setTenantObject($identity->resource('Tenant', $tenant));
        }

        return $this;
    }

    /**
     * Returns the tenant ID only (backwards compatibility).
     *
     * @return string
     */
    public function getTenant()
    {
        return ($this->getTenantObject()) ? $this->getTenantObject()->getId() : null;
    }

    /**
     * Set the full Tenant object for this client.
     *
     * @param OpenCloud\Identity\Resource\Tenant $tenant
     */
    public function setTenantObject(Tenant $tenant)
    {
        $this->tenant = $tenant;
    }

    /**
     * Get the full Tenant object for this client.
     *
     * @return OpenCloud\Identity\Resource\Tenant
     */
    public function getTenantObject()
    {
        return $this->tenant;
    }

    /**
     * Set the service catalog.
     *
     * @param  mixed $catalog
     * @return $this
     */
    public function setCatalog($catalog)
    {
        $this->catalog = Catalog::factory($catalog);

        return $this;
    }

    /**
     * Get the service catalog.
     *
     * @return array
     */
    public function getCatalog()
    {
        return $this->catalog;
    }

    /**
     * @param LoggerInterface $logger
     *
     * @return $this
     */
    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;

        return $this;
    }

    /**
     * @return LoggerInterface
     */
    public function getLogger()
    {
        if (null === $this->logger) {
            $this->setLogger(new Common\Log\Logger);
        }

        return $this->logger;
    }

    /**
     * @return bool
     */
    public function hasLogger()
    {
        return (null !== $this->logger);
    }

    /**
     * @deprecated
     */
    public function hasExpired()
    {
        $this->getLogger()->warning(Logger::deprecated(__METHOD__, 'getTokenObject()->hasExpired()'));

        return $this->getTokenObject() && $this->getTokenObject()->hasExpired();
    }

    /**
     * Formats the credentials array (as a string) for authentication
     *
     * @return string
     * @throws Common\Exceptions\CredentialError
     */
    public function getCredentials()
    {
        if (!empty($this->secret['username']) && !empty($this->secret['password'])) {
            $credentials = array('auth' => array(
                'passwordCredentials' => array(
                    'username' => $this->secret['username'],
                    'password' => $this->secret['password']
                )
            ));

            if (!empty($this->secret['tenantName'])) {
                $credentials['auth']['tenantName'] = $this->secret['tenantName'];
            } elseif (!empty($this->secret['tenantId'])) {
                $credentials['auth']['tenantId'] = $this->secret['tenantId'];
            }

            return json_encode($credentials);
        } else {
            throw new Exceptions\CredentialError(
                Lang::translate('Unrecognized credential secret')
            );
        }
    }

    /**
     * @param $url
     * @return $this
     */
    public function setAuthUrl($url)
    {
        $this->authUrl = Url::factory($url);

        return $this;
    }

    /**
     * @return Url
     */
    public function getAuthUrl()
    {
        return $this->authUrl;
    }

    /**
     * Sets the current user based on the generated token.
     *
     * @param $data Object of user data
     */
    public function setUser(User $user)
    {
        $this->user = $user;
    }

    /**
     * @return \OpenCloud\Identity\Resource\User
     */
    public function getUser()
    {
        return $this->user;
    }

    /**
     * Authenticate the tenant using the supplied credentials
     *
     * @return void
     * @throws AuthenticationError
     */
    public function authenticate()
    {
        // OpenStack APIs will return a 401 if an expired X-Auth-Token is sent,
        // so we need to reset the value before authenticating for another one.
        $this->updateTokenHeader('');

        $identity = IdentityService::factory($this);
        $response = $identity->generateToken($this->getCredentials());

        $body = Formatter::decode($response);

        $this->setCatalog($body->access->serviceCatalog);
        $this->setTokenObject($identity->resource('Token', $body->access->token));
        $this->setUser($identity->resource('User', $body->access->user));

        if (isset($body->access->token->tenant)) {
            $this->setTenantObject($identity->resource('Tenant', $body->access->token->tenant));
        }

        // Set X-Auth-Token HTTP request header
        $this->updateTokenHeader($this->getToken());
    }

    /**
     * @deprecated
     */
    public function getUrl()
    {
        return $this->getBaseUrl();
    }

    /**
     * Convenience method for exporting current credentials. Useful for local caching.
     * @return array
     */
    public function exportCredentials()
    {
        if ($this->hasExpired()) {
            $this->authenticate();
        }

        return array(
            'token'      => $this->getToken(),
            'expiration' => $this->getExpiration(),
            'tenant'     => $this->getTenant(),
            'catalog'    => $this->getCatalog()
        );
    }

    /**
     * Convenience method for importing credentials. Useful for local caching because it reduces HTTP traffic.
     *
     * @param array $values
     */
    public function importCredentials(array $values)
    {
        if (!empty($values['token'])) {
            $this->setToken($values['token']);
            $this->updateTokenHeader($this->getToken());
        }
        if (!empty($values['expiration'])) {
            $this->setExpiration($values['expiration']);
        }
        if (!empty($values['tenant'])) {
            $this->setTenant($values['tenant']);
        }
        if (!empty($values['catalog'])) {
            $this->setCatalog($values['catalog']);
        }
    }

    /**
     * Sets the X-Auth-Token header. If no value is explicitly passed in, the current token is used.
     *
     * @param  string $token Value of header.
     * @return void
     */
    private function updateTokenHeader($token)
    {
        $this->setDefaultOption('headers/X-Auth-Token', (string) $token);
    }

    /**
     * Creates a new ObjectStore object (Swift/Cloud Files)
     *
     * @param string $name    The name of the service as it appears in the Catalog
     * @param string $region  The region (DFW, IAD, ORD, LON, SYD)
     * @param string $urltype The URL type ("publicURL" or "internalURL")
     * @return \OpenCloud\ObjectStore\Service
     */
    public function objectStoreService($name = null, $region = null, $urltype = null)
    {
        return ServiceBuilder::factory($this, 'OpenCloud\ObjectStore\Service', array(
            'name'    => $name,
            'region'  => $region,
            'urlType' => $urltype
        ));
    }

    /**
     * Creates a new Compute object (Nova/Cloud Servers)
     *
     * @param string $name    The name of the service as it appears in the Catalog
     * @param string $region  The region (DFW, IAD, ORD, LON, SYD)
     * @param string $urltype The URL type ("publicURL" or "internalURL")
     * @return \OpenCloud\Compute\Service
     */
    public function computeService($name = null, $region = null, $urltype = null)
    {
        return ServiceBuilder::factory($this, 'OpenCloud\Compute\Service', array(
            'name'    => $name,
            'region'  => $region,
            'urlType' => $urltype
        ));
    }

    /**
     * Creates a new Orchestration (Heat) service object
     *
     * @param string $name    The name of the service as it appears in the Catalog
     * @param string $region  The region (DFW, IAD, ORD, LON, SYD)
     * @param string $urltype The URL type ("publicURL" or "internalURL")
     * @return \OpenCloud\Orchestration\Service
     * @codeCoverageIgnore
     */
    public function orchestrationService($name = null, $region = null, $urltype = null)
    {
        return ServiceBuilder::factory($this, 'OpenCloud\Orchestration\Service', array(
            'name'    => $name,
            'region'  => $region,
            'urlType' => $urltype
        ));
    }

    /**
     * Creates a new Volume (Cinder) service object
     *
     * @param string $name    The name of the service as it appears in the Catalog
     * @param string $region  The region (DFW, IAD, ORD, LON, SYD)
     * @param string $urltype The URL type ("publicURL" or "internalURL")
     * @return \OpenCloud\Volume\Service
     */
    public function volumeService($name = null, $region = null, $urltype = null)
    {
        return ServiceBuilder::factory($this, 'OpenCloud\Volume\Service', array(
            'name'    => $name,
            'region'  => $region,
            'urlType' => $urltype
        ));
    }

    /**
     * Creates a new Rackspace "Cloud Identity" service.
     *
     * @return \OpenCloud\Identity\Service
     */
    public function identityService()
    {
        $service = IdentityService::factory($this);
        $this->authenticate();

        return $service;
    }

    /**
     * Creates a new Glance service
     *
     * @param string $name    The name of the service as it appears in the Catalog
     * @param string $region  The region (DFW, IAD, ORD, LON, SYD)
     * @param string $urltype The URL type ("publicURL" or "internalURL")
     * @return Common\Service\ServiceInterface
     */
    public function imageService($name = null, $region = null, $urltype = null)
    {
        return ServiceBuilder::factory($this, 'OpenCloud\Image\Service', array(
            'name'    => $name,
            'region'  => $region,
            'urlType' => $urltype
        ));
    }

    /**
     * Creates a new Networking (Neutron) service object
     *
     * @param string $name    The name of the service as it appears in the Catalog
     * @param string $region  The region (DFW, IAD, ORD, LON, SYD)
     * @param string $urltype The URL type ("publicURL" or "internalURL")
     * @return \OpenCloud\Networking\Service
     * @codeCoverageIgnore
     */
    public function networkingService($name = null, $region = null, $urltype = null)
    {
        return ServiceBuilder::factory($this, 'OpenCloud\Networking\Service', array(
            'name'    => $name,
            'region'  => $region,
            'urlType' => $urltype
        ));
    }
}
