<?php

namespace VendorDuplicator\Aws\Token;

use VendorDuplicator\Aws\Exception\TokenException;
use VendorDuplicator\GuzzleHttp\Promise;
/**
 * Token that comes from the SSO provider
 */
class SsoTokenProvider implements RefreshableTokenProviderInterface
{
    use ParsesIniTrait;
    const ENV_PROFILE = 'AWS_PROFILE';
    private $ssoProfileName;
    private $filename;
    private $ssoOidcClient;
    /**
     * Constructs a new SsoTokenProvider object, which will fetch a token from an authenticated SSO profile
     * @param string $ssoProfileName The name of the profile that contains the sso_session key
     * @param int    $filename Name of the config file to sso profile from
     */
    public function __construct($ssoProfileName, $filename = null, $ssoOidcClient = null)
    {
        $profileName = getenv(self::ENV_PROFILE) ?: 'default';
        $this->ssoProfileName = !empty($ssoProfileName) ? $ssoProfileName : $profileName;
        $this->filename = !empty($filename) ? $filename : self::getHomeDir() . '/.aws/config';
        $this->ssoOidcClient = $ssoOidcClient;
    }
    /*
     * Loads cached sso credentials
     *
     * @return PromiseInterface
     */
    public function __invoke()
    {
        return Promise\Coroutine::of(function () {
            if (!@is_readable($this->filename)) {
                throw new TokenException("Cannot read profiles from {$this->filename}");
            }
            $profiles = self::loadProfiles($this->filename);
            if (!isset($profiles[$this->ssoProfileName])) {
                throw new TokenException("Profile {$this->ssoProfileName} does not exist in {$this->filename}.");
            }
            $ssoProfile = $profiles[$this->ssoProfileName];
            if (empty($ssoProfile['sso_session'])) {
                throw new TokenException("Profile {$this->ssoProfileName} in {$this->filename} must contain an sso_session.");
            }
            $sessionProfileName = 'sso-session ' . $ssoProfile['sso_session'];
            if (empty($profiles[$sessionProfileName])) {
                throw new TokenException("Profile {$this->ssoProfileName} does not exist in {$this->filename}");
            }
            $sessionProfileData = $profiles[$sessionProfileName];
            if (empty($sessionProfileData['sso_start_url']) || empty($sessionProfileData['sso_region'])) {
                throw new TokenException("Profile {$this->ssoProfileName} in {$this->filename} must contain the following keys: " . "sso_start_url and sso_region.");
            }
            $tokenLocation = self::getTokenLocation($ssoProfile['sso_session']);
            if (!@is_readable($tokenLocation)) {
                throw new TokenException("Unable to read token file at {$tokenLocation}");
            }
            $tokenData = $this->getTokenData($tokenLocation);
            $this->validateTokenData($tokenLocation, $tokenData);
            yield new SsoToken($tokenData['accessToken'], $tokenData['expiresAt'], isset($tokenData['refreshToken']) ? $tokenData['refreshToken'] : null, isset($tokenData['clientId']) ? $tokenData['clientId'] : null, isset($tokenData['clientSecret']) ? $tokenData['clientSecret'] : null, isset($tokenData['registrationExpiresAt']) ? $tokenData['registrationExpiresAt'] : null, isset($tokenData['region']) ? $tokenData['region'] : null, isset($tokenData['startUrl']) ? $tokenData['startUrl'] : null);
        });
    }
    /**
     * Refreshes the token
     * @return mixed|null
     */
    public function refresh()
    {
        try {
            //try to reload from disk
            $token = $this();
            if ($token instanceof SsoToken && !$token->shouldAttemptRefresh()) {
                return $token;
            }
        } finally {
            //if reload from disk fails, try refreshing
            $tokenLocation = self::getTokenLocation($this->ssoProfileName);
            $tokenData = $this->getTokenData($tokenLocation);
            if (empty($this->ssoOidcClient) || empty($tokenData['startUrl'])) {
                throw new TokenException("Cannot refresh this token without an 'ssooidcClient' " . "and a 'start_url'");
            }
            $response = $this->ssoOidcClient->createToken([
                'clientId' => $tokenData['clientId'],
                'clientSecret' => $tokenData['clientSecret'],
                'grantType' => 'refresh_token',
                // REQUIRED
                'refreshToken' => $tokenData['refreshToken'],
            ]);
            if ($response['@metadata']['statusCode'] == 200) {
                $tokenData['accessToken'] = $response['accessToken'];
                $tokenData['expiresAt'] = time() + $response['expiresIn'];
                $tokenData['refreshToken'] = $response['refreshToken'];
                $token = new SsoToken($tokenData['accessToken'], $tokenData['expiresAt'], $tokenData['refreshToken'], isset($tokenData['clientId']) ? $tokenData['clientId'] : null, isset($tokenData['clientSecret']) ? $tokenData['clientSecret'] : null, isset($tokenData['registrationExpiresAt']) ? $tokenData['registrationExpiresAt'] : null, isset($tokenData['region']) ? $tokenData['region'] : null, isset($tokenData['startUrl']) ? $tokenData['startUrl'] : null);
                $this->writeNewTokenDataToDisk($tokenData, $tokenLocation);
                return $token;
            }
        }
    }
    public function shouldAttemptRefresh()
    {
        $tokenLocation = self::getTokenLocation($this->ssoProfileName);
        $tokenData = $this->getTokenData($tokenLocation);
        return strtotime("-10 minutes") >= strtotime($tokenData['expiresAt']);
    }
    /**
     * @param $sso_session
     * @return string
     */
    public static function getTokenLocation($sso_session)
    {
        return self::getHomeDir() . '/.aws/sso/cache/' . mb_convert_encoding(sha1($sso_session), "UTF-8") . ".json";
    }
    /**
     * @param $tokenLocation
     * @return array
     */
    function getTokenData($tokenLocation)
    {
        return json_decode(file_get_contents($tokenLocation), \true);
    }
    /**
     * @param $tokenData
     * @param $tokenLocation
     * @return mixed
     */
    private function validateTokenData($tokenLocation, $tokenData)
    {
        if (empty($tokenData['accessToken']) || empty($tokenData['expiresAt'])) {
            throw new TokenException("Token file at {$tokenLocation} must contain an access token and an expiration");
        }
        $expiration = strtotime($tokenData['expiresAt']);
        if ($expiration === \false) {
            throw new TokenException("Cached SSO token returned an invalid expiration");
        } elseif ($expiration < time()) {
            throw new TokenException("Cached SSO token returned an expired token");
        }
        return $tokenData;
    }
    /**
     * @param array $tokenData
     * @param string $tokenLocation
     * @return void
     */
    private function writeNewTokenDataToDisk(array $tokenData, $tokenLocation)
    {
        $tokenData['expiresAt'] = gmdate('Y-m-d\TH:i:s\Z', $tokenData['expiresAt']);
        file_put_contents($tokenLocation, json_encode(array_filter($tokenData)));
    }
}
