<?php
/**
 * Copyright 2010-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 * http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 AwsWPTC\Common\Credentials;

use AwsWPTC\Common\Enum\ClientOptions as Options;
use AwsWPTC\Common\Exception\InvalidArgumentException;
use AwsWPTC\Common\Exception\RequiredExtensionNotLoadedException;
use AwsWPTC\Common\Exception\RuntimeException;
use Guzzle\Common\FromConfigInterface;
use Guzzle\Cache\CacheAdapterInterface;
use Guzzle\Cache\DoctrineCacheAdapter;
use Guzzle\Common\Collection;

/**
 * Basic implementation of the AWSCredentials interface that allows callers to
 * pass in the AWS access key and secret access in the constructor.
 */
class Credentials implements CredentialsInterface, FromConfigInterface
{
    const ENV_KEY = 'AWS_ACCESS_KEY_ID';
    const ENV_SECRET = 'AWS_SECRET_KEY';
    const ENV_SECRET_ACCESS_KEY = 'AWS_SECRET_ACCESS_KEY';
    const ENV_PROFILE = 'AWS_PROFILE';

    /** @var string AWS Access Key ID */
    protected $key;

    /** @var string AWS Secret Access Key */
    protected $secret;

    /** @var string AWS Security Token */
    protected $token;

    /** @var int Time to die of token */
    protected $ttd;

    /**
     * Get the available keys for the factory method
     *
     * @return array
     */
    public static function getConfigDefaults()
    {
        return array(
            Options::KEY                   => null,
            Options::SECRET                => null,
            Options::TOKEN                 => null,
            Options::TOKEN_TTD             => null,
            Options::PROFILE               => null,
            Options::CREDENTIALS_CACHE     => null,
            Options::CREDENTIALS_CACHE_KEY => null,
            Options::CREDENTIALS_CLIENT    => null
        );
    }

    /**
     * Factory method for creating new credentials.  This factory method will
     * create the appropriate credentials object with appropriate decorators
     * based on the passed configuration options.
     *
     * @param array $config Options to use when instantiating the credentials
     *
     * @return CredentialsInterface
     * @throws InvalidArgumentException If the caching options are invalid
     * @throws RuntimeException         If using the default cache and APC is disabled
     */
    public static function factory($config = array())
    {
        // Add default key values
        foreach (self::getConfigDefaults() as $key => $value) {
            if (!isset($config[$key])) {
                $config[$key] = $value;
            }
        }

        // Set up the cache
        $cache = $config[Options::CREDENTIALS_CACHE];
        $cacheKey = $config[Options::CREDENTIALS_CACHE_KEY] ?:
            'credentials_' . ($config[Options::KEY] ?: crc32(gethostname()));

        if (
            $cacheKey &&
            $cache instanceof CacheAdapterInterface &&
            $cached = self::createFromCache($cache, $cacheKey)
        ) {
            return $cached;
        }

        // Create the credentials object
        if (!$config[Options::KEY] || !$config[Options::SECRET]) {
            $credentials = self::createFromEnvironment($config);
        } else {
            // Instantiate using short or long term credentials
            $credentials = new static(
                $config[Options::KEY],
                $config[Options::SECRET],
                $config[Options::TOKEN],
                $config[Options::TOKEN_TTD]
            );
        }

        // Check if the credentials are refreshable, and if so, configure caching
        $cache = $config[Options::CREDENTIALS_CACHE];
        if ($cacheKey && $cache) {
            $credentials = self::createCache($credentials, $cache, $cacheKey);
        }

        return $credentials;
    }

    /**
     * Create credentials from the credentials ini file in the HOME directory.
     *
     * @param string|null $profile  Pass a specific profile to use. If no
     *                              profile is specified we will attempt to use
     *                              the value specified in the AWS_PROFILE
     *                              environment variable. If AWS_PROFILE is not
     *                              set, the "default" profile is used.
     * @param string|null $filename Pass a string to specify the location of the
     *                              credentials files. If null is passed, the
     *                              SDK will attempt to find the configuration
     *                              file at in your HOME directory at
     *                              ~/.aws/credentials.
     * @return CredentialsInterface
     * @throws \RuntimeException if the file cannot be found, if the file is
     *                           invalid, or if the profile is invalid.
     */
    public static function fromIni($profile = null, $filename = null)
    {
        if (!$filename) {
            $filename = self::getHomeDir() . '/.aws/credentials';
        }

        if (!$profile) {
            $profile = self::getEnvVar(self::ENV_PROFILE) ?: 'default';
        }

        if (!is_readable($filename) || ($data = parse_ini_file($filename, true)) === false) {
            throw new \RuntimeException("Invalid AWS credentials file: {$filename}.");
        }

        if (!isset($data[$profile]['aws_access_key_id']) || !isset($data[$profile]['aws_secret_access_key'])) {
            throw new \RuntimeException("Invalid AWS credentials profile {$profile} in {$filename}.");
        }

        return new self(
            $data[$profile]['aws_access_key_id'],
            $data[$profile]['aws_secret_access_key'],
            isset($data[$profile]['aws_security_token'])
                ? $data[$profile]['aws_security_token']
                : null
        );
    }

    /**
     * Constructs a new BasicAWSCredentials object, with the specified AWS
     * access key and AWS secret key
     *
     * @param string $accessKeyId     AWS access key ID
     * @param string $secretAccessKey AWS secret access key
     * @param string $token           Security token to use
     * @param int    $expiration      UNIX timestamp for when credentials expire
     */
    public function __construct($accessKeyId, $secretAccessKey, $token = null, $expiration = null)
    {
        $this->key = trim($accessKeyId);
        $this->secret = trim($secretAccessKey);
        $this->token = $token;
        $this->ttd = $expiration;
    }

    public function serialize()
    {
        return json_encode(array(
            Options::KEY       => $this->key,
            Options::SECRET    => $this->secret,
            Options::TOKEN     => $this->token,
            Options::TOKEN_TTD => $this->ttd
        ));
    }

    public function unserialize($serialized)
    {
        $data = json_decode($serialized, true);
        $this->key    = $data[Options::KEY];
        $this->secret = $data[Options::SECRET];
        $this->token  = $data[Options::TOKEN];
        $this->ttd    = $data[Options::TOKEN_TTD];
    }

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

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

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

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

    public function isExpired()
    {
        return $this->ttd !== null && time() >= $this->ttd;
    }

    public function setAccessKeyId($key)
    {
        $this->key = $key;

        return $this;
    }

    public function setSecretKey($secret)
    {
        $this->secret = $secret;

        return $this;
    }

    public function setSecurityToken($token)
    {
        $this->token = $token;

        return $this;
    }

    public function setExpiration($timestamp)
    {
        $this->ttd = $timestamp;

        return $this;
    }

    /**
     * When no keys are provided, attempt to create them based on the
     * environment or instance profile credentials.
     *
     * @param array|Collection $config
     *
     * @return CredentialsInterface
     */
    private static function createFromEnvironment($config)
    {
        // Get key and secret from ENV variables
        $envKey = self::getEnvVar(self::ENV_KEY);
        if (!($envSecret = self::getEnvVar(self::ENV_SECRET))) {
            // Use AWS_SECRET_ACCESS_KEY if AWS_SECRET_KEY was not set
            $envSecret = self::getEnvVar(self::ENV_SECRET_ACCESS_KEY);
        }

        // Use credentials from the environment variables if available
        if ($envKey && $envSecret) {
            return new static($envKey, $envSecret);
        }

        try {
            // Use credentials from the INI file in HOME directory if available
            return self::fromIni($config[Options::PROFILE]);
        } catch (\RuntimeException $e) {
            // Otherwise, try using instance profile credentials (available on EC2 instances)
            return new RefreshableInstanceProfileCredentials(
                new static('', '', '', 1),
                $config[Options::CREDENTIALS_CLIENT]
            );
        }
    }

    private static function createFromCache(CacheAdapterInterface $cache, $cacheKey)
    {
        $cached = $cache->fetch($cacheKey);
        if ($cached instanceof CredentialsInterface && !$cached->isExpired()) {
            return new CacheableCredentials($cached, $cache, $cacheKey);
        }

        return null;
    }

    private static function createCache(CredentialsInterface $credentials, $cache, $cacheKey)
    {
        if ($cache === 'true' || $cache === true) {
            // If no cache adapter was provided, then create one for the user
            // @codeCoverageIgnoreStart
            if (!extension_loaded('apc')) {
                throw new RequiredExtensionNotLoadedException('PHP has not been compiled with APC. Unable to cache '
                    . 'the credentials.');
            } elseif (!class_exists('Doctrine\Common\Cache\ApcCache')) {
                throw new RuntimeException(
                    'Cannot set ' . Options::CREDENTIALS_CACHE . ' to true because the Doctrine cache component is '
                    . 'not installed. Either install doctrine/cache or pass in an instantiated '
                    . 'Guzzle\Cache\CacheAdapterInterface object'
                );
            }
            // @codeCoverageIgnoreEnd
            $cache = new DoctrineCacheAdapter(new \Doctrine\Common\Cache\ApcCache());
        } elseif (!($cache instanceof CacheAdapterInterface)) {
            throw new InvalidArgumentException('Unable to utilize caching with the specified options');
        }

        // Decorate the credentials with a cache
        return new CacheableCredentials($credentials, $cache, $cacheKey);
    }

    private static function getHomeDir()
    {
        // On Linux/Unix-like systems, use the HOME environment variable
        if ($homeDir = self::getEnvVar('HOME')) {
            return $homeDir;
        }

        // Get the HOMEDRIVE and HOMEPATH values for Windows hosts
        $homeDrive = self::getEnvVar('HOMEDRIVE');
        $homePath = self::getEnvVar('HOMEPATH');

        return ($homeDrive && $homePath) ? $homeDrive . $homePath : null;
    }

    /**
     * Fetches the value of an environment variable by checking $_SERVER and getenv().
     *
     * @param string $var Name of the environment variable
     *
     * @return mixed|null
     */
    private static function getEnvVar($var)
    {
        return isset($_SERVER[$var]) ? $_SERVER[$var] : getenv($var);
    }
}
