/**
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

#include <aws/core/config/AWSProfileConfigLoader.h>
#include <aws/core/internal/AWSHttpResourceClient.h>
#include <aws/core/auth/AWSCredentialsProvider.h>
#include <aws/core/utils/memory/stl/AWSList.h>
#include <aws/core/utils/memory/stl/AWSStreamFwd.h>
#include <aws/core/utils/StringUtils.h>
#include <aws/core/utils/logging/LogMacros.h>
#include <aws/core/utils/json/JsonSerializer.h>
#include <fstream>

namespace Aws
{
    namespace Config
    {
        using namespace Aws::Utils;
        using namespace Aws::Auth;

        static const char* const CONFIG_LOADER_TAG = "Aws::Config::AWSProfileConfigLoader";
        #ifdef _MSC_VER
            // VS2015 compiler's bug, warning s_CoreErrorsMapper: symbol will be dynamically initialized (implementation limitation)
            AWS_SUPPRESS_WARNING(4592,
                static Aws::UniquePtr<ConfigAndCredentialsCacheManager> s_configManager(nullptr);
            )
        #else
            static Aws::UniquePtr<ConfigAndCredentialsCacheManager> s_configManager(nullptr);
        #endif

        static const char CONFIG_CREDENTIALS_CACHE_MANAGER_TAG[] = "ConfigAndCredentialsCacheManager";

        bool AWSProfileConfigLoader::Load()
        {
            if(LoadInternal())
            {
                AWS_LOGSTREAM_INFO(CONFIG_LOADER_TAG, "Successfully reloaded configuration.");
                m_lastLoadTime = DateTime::Now();
                AWS_LOGSTREAM_TRACE(CONFIG_LOADER_TAG, "reloaded config at "
                        << m_lastLoadTime.ToGmtString(DateFormat::ISO_8601));
                return true;
            }

            AWS_LOGSTREAM_INFO(CONFIG_LOADER_TAG, "Failed to reload configuration.");
            return false;
        }

        bool AWSProfileConfigLoader::PersistProfiles(const Aws::Map<Aws::String, Profile>& profiles)
        {
            if(PersistInternal(profiles))
            {
                AWS_LOGSTREAM_INFO(CONFIG_LOADER_TAG, "Successfully persisted configuration.");
                m_profiles = profiles;
                m_lastLoadTime = DateTime::Now();
                AWS_LOGSTREAM_TRACE(CONFIG_LOADER_TAG, "persisted config at "
                        << m_lastLoadTime.ToGmtString(DateFormat::ISO_8601));
                return true;
            }

            AWS_LOGSTREAM_WARN(CONFIG_LOADER_TAG, "Failed to persist configuration.");
            return false;
        }

        static const char REGION_KEY[]                       = "region";
        static const char ACCESS_KEY_ID_KEY[]                = "aws_access_key_id";
        static const char SECRET_KEY_KEY[]                   = "aws_secret_access_key";
        static const char SESSION_TOKEN_KEY[]                = "aws_session_token";
        static const char SSO_START_URL_KEY[]                = "sso_start_url";
        static const char SSO_REGION_KEY[]                   = "sso_region";
        static const char SSO_ACCOUNT_ID_KEY[]               = "sso_account_id";
        static const char SSO_ROLE_NAME_KEY[]                = "sso_role_name";
        static const char ROLE_ARN_KEY[]                     = "role_arn";
        static const char EXTERNAL_ID_KEY[]                  = "external_id";
        static const char CREDENTIAL_PROCESS_COMMAND[]       = "credential_process";
        static const char SOURCE_PROFILE_KEY[]               = "source_profile";
        static const char PROFILE_PREFIX[]                   = "profile ";
        static const char EQ                                 = '=';
        static const char LEFT_BRACKET                       = '[';
        static const char RIGHT_BRACKET                      = ']';
        static const char PARSER_TAG[]                       = "Aws::Config::ConfigFileProfileFSM";

        class ConfigFileProfileFSM
        {
        public:
            ConfigFileProfileFSM() : m_parserState(START) {}

            const Aws::Map<String, Profile>& GetProfiles() const { return m_foundProfiles; }

            void ParseStream(Aws::IStream& stream)
            {
                static const size_t ASSUME_EMPTY_LEN = 3;

                Aws::String line;
                while(std::getline(stream, line) && m_parserState != FAILURE)
                {
                    if (line.empty() || line.length() < ASSUME_EMPTY_LEN)
                    {
                        continue;
                    }

                    auto openPos = line.find(LEFT_BRACKET);
                    auto closePos = line.find(RIGHT_BRACKET);

                    switch(m_parserState)
                    {

                    case START:
                        if(openPos != std::string::npos && closePos != std::string::npos)
                        {
                            FlushProfileAndReset(line, openPos, closePos);
                            m_parserState = PROFILE_FOUND;
                        }
                        break;

                    //fallthrough here is intentional to reduce duplicate logic
                    case PROFILE_KEY_VALUE_FOUND:
                        if(openPos != std::string::npos && closePos != std::string::npos)
                        {
                            m_parserState = PROFILE_FOUND;
                            FlushProfileAndReset(line, openPos, closePos);
                            break;
                        }
                        // fall through
                    case PROFILE_FOUND:
                    {
                        auto equalsPos = line.find(EQ);
                        if (equalsPos != std::string::npos)
                        {
                            auto key = line.substr(0, equalsPos);
                            auto value = line.substr(equalsPos + 1);
                            m_profileKeyValuePairs[StringUtils::Trim(key.c_str())] =
                                    StringUtils::Trim(value.c_str());
                            m_parserState = PROFILE_KEY_VALUE_FOUND;
                        }

                        break;
                    }
                    default:
                        m_parserState = FAILURE;
                        break;
                    }
                }

                FlushProfileAndReset(line, std::string::npos, std::string::npos);
            }

        private:

            void FlushProfileAndReset(Aws::String& line, size_t openPos, size_t closePos)
            {
                if(!m_currentWorkingProfile.empty() && !m_profileKeyValuePairs.empty())
                {
                    Profile profile;
                    profile.SetName(m_currentWorkingProfile);

                    auto regionIter = m_profileKeyValuePairs.find(REGION_KEY);
                    if (regionIter != m_profileKeyValuePairs.end())
                    {
                        AWS_LOGSTREAM_DEBUG(PARSER_TAG, "found region " << regionIter->second);
                        profile.SetRegion(regionIter->second);
                    }

                    auto accessKeyIdIter = m_profileKeyValuePairs.find(ACCESS_KEY_ID_KEY);
                    Aws::String accessKey, secretKey, sessionToken;
                    if (accessKeyIdIter != m_profileKeyValuePairs.end())
                    {
                        accessKey = accessKeyIdIter->second;
                        AWS_LOGSTREAM_DEBUG(PARSER_TAG, "found access key " << accessKey);

                        auto secretAccessKeyIter = m_profileKeyValuePairs.find(SECRET_KEY_KEY);
                        auto sessionTokenIter = m_profileKeyValuePairs.find(SESSION_TOKEN_KEY);
                        if (secretAccessKeyIter != m_profileKeyValuePairs.end())
                        {
                            secretKey = secretAccessKeyIter->second;
                        }
                        else
                        {
                            AWS_LOGSTREAM_ERROR(PARSER_TAG, "No secret access key found even though an access key was specified. This will cause all signed AWS calls to fail.");
                        }

                        if (sessionTokenIter != m_profileKeyValuePairs.end())
                        {
                            sessionToken = sessionTokenIter->second;
                        }

                        profile.SetCredentials(Aws::Auth::AWSCredentials(accessKey, secretKey, sessionToken));
                    }

                    auto ssoStartUrlIter = m_profileKeyValuePairs.find(SSO_START_URL_KEY);
                    auto ssoRegionIter = m_profileKeyValuePairs.find(SSO_REGION_KEY);
                    auto ssoRoleNameIter = m_profileKeyValuePairs.find(SSO_ROLE_NAME_KEY);
                    auto ssoAccountIdIter = m_profileKeyValuePairs.find(SSO_ACCOUNT_ID_KEY);
                    if (ssoStartUrlIter != m_profileKeyValuePairs.end()
                        || ssoRegionIter != m_profileKeyValuePairs.end()
                        || ssoRoleNameIter != m_profileKeyValuePairs.end()
                        || ssoAccountIdIter != m_profileKeyValuePairs.end())
                    {
                        if (ssoStartUrlIter != m_profileKeyValuePairs.end()
                            && ssoRegionIter != m_profileKeyValuePairs.end()
                            && ssoRoleNameIter != m_profileKeyValuePairs.end()
                            && ssoAccountIdIter != m_profileKeyValuePairs.end())
                        {
                            AWS_LOGSTREAM_DEBUG(PARSER_TAG, "found sso_start_url " << ssoStartUrlIter->second);
                            profile.SetSsoStartUrl(ssoStartUrlIter->second);
                            AWS_LOGSTREAM_DEBUG(PARSER_TAG, "found sso_region " << ssoRegionIter->second);
                            profile.SetSsoRegion(ssoRegionIter->second);
                            AWS_LOGSTREAM_DEBUG(PARSER_TAG, "found sso_account_id " << ssoAccountIdIter->second);
                            profile.SetSsoAccountId(ssoAccountIdIter->second);
                            AWS_LOGSTREAM_DEBUG(PARSER_TAG, "found sso_role_name " << ssoRoleNameIter->second);
                            profile.SetSsoRoleName(ssoRoleNameIter->second);
                        } else {
                            AWS_LOGSTREAM_ERROR(PARSER_TAG, "invalid configuration for sso profile " << profile.GetName());
                        }
                    }

                    auto assumeRoleArnIter = m_profileKeyValuePairs.find(ROLE_ARN_KEY);
                    if (assumeRoleArnIter != m_profileKeyValuePairs.end())
                    {
                        AWS_LOGSTREAM_DEBUG(PARSER_TAG, "found role arn " << assumeRoleArnIter->second);
                        profile.SetRoleArn(assumeRoleArnIter->second);
                    }

                    auto externalIdIter = m_profileKeyValuePairs.find(EXTERNAL_ID_KEY);
                    if (externalIdIter != m_profileKeyValuePairs.end())
                    {
                        AWS_LOGSTREAM_DEBUG(PARSER_TAG, "found external id " << externalIdIter->second);
                        profile.SetExternalId(externalIdIter->second);
                    }

                    auto sourceProfileIter = m_profileKeyValuePairs.find(SOURCE_PROFILE_KEY);
                    if (sourceProfileIter != m_profileKeyValuePairs.end())
                    {
                        AWS_LOGSTREAM_DEBUG(PARSER_TAG, "found source profile " << sourceProfileIter->second);
                        profile.SetSourceProfile(sourceProfileIter->second);
                    }

                    auto credentialProcessIter = m_profileKeyValuePairs.find(CREDENTIAL_PROCESS_COMMAND);
                    if (credentialProcessIter != m_profileKeyValuePairs.end())
                    {
                        AWS_LOGSTREAM_DEBUG(PARSER_TAG, "found credential process " << credentialProcessIter->second);
                        profile.SetCredentialProcess(credentialProcessIter->second);
                    }
                    profile.SetAllKeyValPairs(m_profileKeyValuePairs);

                    m_foundProfiles[profile.GetName()] = std::move(profile);
                    m_currentWorkingProfile.clear();
                    m_profileKeyValuePairs.clear();
                }

                if(!line.empty() && openPos != std::string::npos && closePos != std::string::npos)
                {
                    m_currentWorkingProfile = StringUtils::Trim(line.substr(openPos + 1, closePos - openPos - 1).c_str());
                    StringUtils::Replace(m_currentWorkingProfile, PROFILE_PREFIX, "");
                    AWS_LOGSTREAM_DEBUG(PARSER_TAG, "found profile " << m_currentWorkingProfile);
                }
            }

            enum State
            {
                START = 0,
                PROFILE_FOUND,
                PROFILE_KEY_VALUE_FOUND,
                FAILURE
            };

            Aws::String m_currentWorkingProfile;
            Aws::Map<String, String> m_profileKeyValuePairs;
            State m_parserState;
            Aws::Map<String, Profile> m_foundProfiles;
        };

        static const char* const CONFIG_FILE_LOADER = "Aws::Config::AWSConfigFileProfileConfigLoader";

        AWSConfigFileProfileConfigLoader::AWSConfigFileProfileConfigLoader(const Aws::String& fileName, bool useProfilePrefix) :
                m_fileName(fileName), m_useProfilePrefix(useProfilePrefix)
        {
            AWS_LOGSTREAM_INFO(CONFIG_FILE_LOADER, "Initializing config loader against fileName "
                    << fileName << " and using profilePrefix = " << useProfilePrefix);
        }

        bool AWSConfigFileProfileConfigLoader::LoadInternal()
        {
            m_profiles.clear();

            Aws::IFStream inputFile(m_fileName.c_str());
            if(inputFile)
            {
                ConfigFileProfileFSM parser;
                parser.ParseStream(inputFile);
                m_profiles = parser.GetProfiles();
                return m_profiles.size() > 0;
            }

            AWS_LOGSTREAM_INFO(CONFIG_FILE_LOADER, "Unable to open config file " << m_fileName << " for reading.");

            return false;
        }

        bool AWSConfigFileProfileConfigLoader::PersistInternal(const Aws::Map<Aws::String, Profile>& profiles)
        {
            Aws::OFStream outputFile(m_fileName.c_str(), std::ios_base::out | std::ios_base::trunc);
            if(outputFile)
            {
                for(auto& profile : profiles)
                {
                    Aws::String prefix = m_useProfilePrefix ? PROFILE_PREFIX : "";

                    AWS_LOGSTREAM_DEBUG(CONFIG_FILE_LOADER, "Writing profile " << profile.first << " to disk.");

                    outputFile << LEFT_BRACKET << prefix << profile.second.GetName() << RIGHT_BRACKET << std::endl;
                    const Aws::Auth::AWSCredentials& credentials = profile.second.GetCredentials();
                    outputFile << ACCESS_KEY_ID_KEY << EQ << credentials.GetAWSAccessKeyId() << std::endl;
                    outputFile << SECRET_KEY_KEY << EQ << credentials.GetAWSSecretKey() << std::endl;

                    if(!credentials.GetSessionToken().empty())
                    {
                        outputFile << SESSION_TOKEN_KEY << EQ << credentials.GetSessionToken() << std::endl;
                    }

                    if(!profile.second.GetRegion().empty())
                    {
                        outputFile << REGION_KEY << EQ << profile.second.GetRegion() << std::endl;
                    }

                    if(!profile.second.GetRoleArn().empty())
                    {
                        outputFile << ROLE_ARN_KEY << EQ << profile.second.GetRoleArn() << std::endl;
                    }

                    if(!profile.second.GetSourceProfile().empty())
                    {
                        outputFile << SOURCE_PROFILE_KEY << EQ << profile.second.GetSourceProfile() << std::endl;
                    }

                    outputFile << std::endl;
                }

                AWS_LOGSTREAM_INFO(CONFIG_FILE_LOADER, "Profiles written to config file " << m_fileName);

                return true;
            }

            AWS_LOGSTREAM_WARN(CONFIG_FILE_LOADER, "Unable to open config file " << m_fileName << " for writing.");

            return false;
        }

        static const char* const EC2_INSTANCE_PROFILE_LOG_TAG = "Aws::Config::EC2InstanceProfileConfigLoader";

        EC2InstanceProfileConfigLoader::EC2InstanceProfileConfigLoader(const std::shared_ptr<Aws::Internal::EC2MetadataClient>& client)
            : m_ec2metadataClient(client == nullptr ? Aws::MakeShared<Aws::Internal::EC2MetadataClient>(EC2_INSTANCE_PROFILE_LOG_TAG) : client)
        {
        }

        bool EC2InstanceProfileConfigLoader::LoadInternal()
        {
            auto credentialsStr = m_ec2metadataClient->GetDefaultCredentialsSecurely();
            if(credentialsStr.empty()) return false;

            Json::JsonValue credentialsDoc(credentialsStr);
            if (!credentialsDoc.WasParseSuccessful())
            {
                AWS_LOGSTREAM_ERROR(EC2_INSTANCE_PROFILE_LOG_TAG,
                        "Failed to parse output from EC2MetadataService.");
                return false;
            }
            const char* accessKeyId = "AccessKeyId";
            const char* secretAccessKey = "SecretAccessKey";
            Aws::String accessKey, secretKey, token;

            auto credentialsView = credentialsDoc.View();
            accessKey = credentialsView.GetString(accessKeyId);
            AWS_LOGSTREAM_INFO(EC2_INSTANCE_PROFILE_LOG_TAG,
                    "Successfully pulled credentials from metadata service with access key " << accessKey);

            secretKey = credentialsView.GetString(secretAccessKey);
            token = credentialsView.GetString("Token");

            auto region = m_ec2metadataClient->GetCurrentRegion();

            Profile profile;
            profile.SetCredentials(AWSCredentials(accessKey, secretKey, token));
            profile.SetRegion(region);
            profile.SetName(INSTANCE_PROFILE_KEY);

            m_profiles[INSTANCE_PROFILE_KEY] = profile;

            return true;
        }

        ConfigAndCredentialsCacheManager::ConfigAndCredentialsCacheManager() :
            m_credentialsFileLoader(Aws::Auth::ProfileConfigFileAWSCredentialsProvider::GetCredentialsProfileFilename()),
            m_configFileLoader(Aws::Auth::GetConfigProfileFilename(), true/*use profile prefix*/)
        {
            ReloadCredentialsFile();
            ReloadConfigFile();
        }

        void ConfigAndCredentialsCacheManager::ReloadConfigFile()
        {
            Aws::Utils::Threading::WriterLockGuard guard(m_configLock);
            m_configFileLoader.SetFileName(Aws::Auth::GetConfigProfileFilename());
            m_configFileLoader.Load();
        }

        void ConfigAndCredentialsCacheManager::ReloadCredentialsFile()
        {
            Aws::Utils::Threading::WriterLockGuard guard(m_credentialsLock);
            m_credentialsFileLoader.SetFileName(Aws::Auth::ProfileConfigFileAWSCredentialsProvider::GetCredentialsProfileFilename());
            m_credentialsFileLoader.Load();
        }

        bool ConfigAndCredentialsCacheManager::HasConfigProfile(const Aws::String& profileName) const
        {
            Aws::Utils::Threading::ReaderLockGuard guard(m_configLock);
            return (m_configFileLoader.GetProfiles().count(profileName) == 1);
        }

        Aws::Config::Profile ConfigAndCredentialsCacheManager::GetConfigProfile(const Aws::String& profileName) const
        {
            Aws::Utils::Threading::ReaderLockGuard guard(m_configLock);
            const auto& profiles = m_configFileLoader.GetProfiles();
            const auto &iter = profiles.find(profileName);
            if (iter == profiles.end())
            {
                return {};
            }
            return iter->second;
        }

        Aws::Map<Aws::String, Aws::Config::Profile> ConfigAndCredentialsCacheManager::GetConfigProfiles() const
        {
            Aws::Utils::Threading::ReaderLockGuard guard(m_configLock);
            return m_configFileLoader.GetProfiles();
        }

        Aws::String ConfigAndCredentialsCacheManager::GetConfig(const Aws::String& profileName, const Aws::String& key) const
        {
            Aws::Utils::Threading::ReaderLockGuard guard(m_configLock);
            const auto& profiles = m_configFileLoader.GetProfiles();
            const auto &iter = profiles.find(profileName);
            if (iter == profiles.end())
            {
                return {};
            }
            return iter->second.GetValue(key);
        }

        bool ConfigAndCredentialsCacheManager::HasCredentialsProfile(const Aws::String& profileName) const
        {
            Aws::Utils::Threading::ReaderLockGuard guard(m_credentialsLock);
            return (m_credentialsFileLoader.GetProfiles().count(profileName) == 1);
        }

        Aws::Config::Profile ConfigAndCredentialsCacheManager::GetCredentialsProfile(const Aws::String& profileName) const
        {
            Aws::Utils::Threading::ReaderLockGuard guard(m_credentialsLock);
            const auto &profiles = m_credentialsFileLoader.GetProfiles();
            const auto &iter = profiles.find(profileName);
            if (iter == profiles.end())
            {
                return {};
            }
            return iter->second;
        }

        Aws::Map<Aws::String, Aws::Config::Profile> ConfigAndCredentialsCacheManager::GetCredentialsProfiles() const
        {
            Aws::Utils::Threading::ReaderLockGuard guard(m_credentialsLock);
            return m_credentialsFileLoader.GetProfiles();
        }

        Aws::Auth::AWSCredentials ConfigAndCredentialsCacheManager::GetCredentials(const Aws::String& profileName) const
        {
            Aws::Utils::Threading::ReaderLockGuard guard(m_credentialsLock);
            const auto& profiles = m_credentialsFileLoader.GetProfiles();
            const auto &iter = profiles.find(profileName);
            if (iter == profiles.end())
            {
                return {};
            }
            return iter->second.GetCredentials();
        }

        void InitConfigAndCredentialsCacheManager()
        {
            if (s_configManager)
            {
                return;
            }
            s_configManager = Aws::MakeUnique<ConfigAndCredentialsCacheManager>(CONFIG_CREDENTIALS_CACHE_MANAGER_TAG);
        }

        void CleanupConfigAndCredentialsCacheManager()
        {
            if (!s_configManager)
            {
                return;
            }
            s_configManager = nullptr;
        }

        void ReloadCachedConfigFile()
        {
            assert(s_configManager);
            s_configManager->ReloadConfigFile();
        }

        void ReloadCachedCredentialsFile()
        {
            assert(s_configManager);
            s_configManager->ReloadCredentialsFile();
        }

        bool HasCachedConfigProfile(const Aws::String& profileName)
        {
            assert(s_configManager);
            return s_configManager->HasConfigProfile(profileName);
        }

        Aws::Config::Profile GetCachedConfigProfile(const Aws::String& profileName)
        {
            assert(s_configManager);
            return s_configManager->GetConfigProfile(profileName);
        }

        Aws::Map<Aws::String, Aws::Config::Profile> GetCachedConfigProfiles()
        {
            assert(s_configManager);
            return s_configManager->GetConfigProfiles();
        }

        Aws::String GetCachedConfigValue(const Aws::String &profileName, const Aws::String &key)
        {
            assert(s_configManager);
            return s_configManager->GetConfig(profileName, key);
        }

        Aws::String GetCachedConfigValue(const Aws::String &key)
        {
            assert(s_configManager);
            return s_configManager->GetConfig(Aws::Auth::GetConfigProfileName(), key);
        }

        bool HasCachedCredentialsProfile(const Aws::String& profileName)
        {
            assert(s_configManager);
            return s_configManager->HasCredentialsProfile(profileName);
        }

        Aws::Config::Profile GetCachedCredentialsProfile(const Aws::String &profileName)
        {
            assert(s_configManager);
            return s_configManager->GetCredentialsProfile(profileName);
        }

        Aws::Map<Aws::String, Aws::Config::Profile> GetCachedCredentialsProfiles()
        {
            assert(s_configManager);
            return s_configManager->GetCredentialsProfiles();
        }

        Aws::Auth::AWSCredentials GetCachedCredentials(const Aws::String &profileName)
        {
            assert(s_configManager);
            return s_configManager->GetCredentials(profileName);
        }
    } // Config namespace
} // Aws namespace
