/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
#ifndef PULSAR_AUTHENTICATION_H_
#define PULSAR_AUTHENTICATION_H_

#include <pulsar/Result.h>
#include <pulsar/defines.h>

#include <functional>
#include <map>
#include <memory>
#include <string>
#include <vector>

namespace pulsar {

class ClientConfiguration;
class Authentication;

class PULSAR_PUBLIC AuthenticationDataProvider {
   public:
    virtual ~AuthenticationDataProvider();

    /**
     * @return true if the authentication data contains data for TLS
     */
    virtual bool hasDataForTls();

    /**
     * @return a client certificate chain or “none” if the data is not available
     */
    virtual std::string getTlsCertificates();

    /**
     * @return a private key for the client certificate or “none” if the data is not available
     */
    virtual std::string getTlsPrivateKey();

    /**
     * @return true if this authentication data contains data for HTTP
     */
    virtual bool hasDataForHttp();

    /**
     * @return an authentication scheme or “none” if the request is not authenticated
     */
    virtual std::string getHttpAuthType();

    /**
     * @return the string of HTTP header or “none” if the request is not authenticated
     */
    virtual std::string getHttpHeaders();

    /**
     * @return true if authentication data contains data from Pulsar protocol
     */
    virtual bool hasDataFromCommand();

    /**
     * @return authentication data which is stored in a command
     */
    virtual std::string getCommandData();

   protected:
    AuthenticationDataProvider();
};

typedef std::shared_ptr<AuthenticationDataProvider> AuthenticationDataPtr;
typedef std::shared_ptr<Authentication> AuthenticationPtr;
typedef std::map<std::string, std::string> ParamMap;

class PULSAR_PUBLIC Authentication {
   public:
    virtual ~Authentication();

    /**
     * @return the authentication method name supported by this provider
     */
    virtual const std::string getAuthMethodName() const = 0;

    /**
     * Get AuthenticationData from the current instance
     *
     * @param[out] authDataContent the shared pointer of AuthenticationData. The content of AuthenticationData
     * is changed to the internal data of the current instance.
     * @return ResultOk or ResultAuthenticationError if authentication failed
     */
    virtual Result getAuthData(AuthenticationDataPtr& authDataContent) {
        authDataContent = authData_;
        return ResultOk;
    }

    /**
     * Parse the authentication parameter string to a map whose key and value are both strings
     *
     * The parameter string can have multiple lines. The format of each line is a comma-separated “key:value”
     * string.
     *
     * For example, “k1:v1,k2:v2” is parsed to two key-value pairs `(k1, v1)` and `(k2, v2)`.
     *
     * @param authParamsString the authentication parameter string to be parsed
     * @return the parsed map whose key and value are both strings
     */
    static ParamMap parseDefaultFormatAuthParams(const std::string& authParamsString);

   protected:
    Authentication();
    AuthenticationDataPtr authData_;
    friend class ClientConfiguration;
};

/**
 * AuthFactory is used to create instances of Authentication class when
 * configuring a Client instance. It loads the authentication from an
 * external plugin.
 *
 * To use authentication methods that are internally supported, you should
 * use `AuthTls::create("my-cert.pem", "my-private.key")` or similar.
 */
class PULSAR_PUBLIC AuthFactory {
   public:
    static AuthenticationPtr Disabled();

    /**
     * Create an AuthenticationPtr with an empty ParamMap
     *
     * @see create(const std::string&, const ParamMap&)
     */
    static AuthenticationPtr create(const std::string& pluginNameOrDynamicLibPath);

    /**
     * Create an AuthenticationPtr with a ParamMap that is converted from authParamsString
     *
     * @see Authentication::parseDefaultFormatAuthParams
     * @see create(const std::string&, const ParamMap&)
     */
    static AuthenticationPtr create(const std::string& pluginNameOrDynamicLibPath,
                                    const std::string& authParamsString);

    /**
     * Create an AuthenticationPtr
     *
     * When the first parameter represents the plugin name, the type of authentication can be one of the
     * following:
     * - AuthTls (if the plugin name is “tls”)
     * - AuthToken (if the plugin name is “token” or “org.apache.pulsar.client.impl.auth.AuthenticationToken”)
     * - AuthAthenz (if the plugin name is “athenz” or
     * “org.apache.pulsar.client.impl.auth.AuthenticationAthenz”)
     * - AuthOauth2 (if the plugin name is “oauth2token” or
     * “org.apache.pulsar.client.impl.auth.oauth2.AuthenticationOAuth2”)
     *
     * @param pluginNameOrDynamicLibPath the plugin name or the path or a dynamic library that contains the
     * implementation of Authentication
     * @param params the ParamMap that is passed to Authentication::create method
     */
    static AuthenticationPtr create(const std::string& pluginNameOrDynamicLibPath, ParamMap& params);

   protected:
    static bool isShutdownHookRegistered_;
    static std::vector<void*> loadedLibrariesHandles_;
    static void release_handles();
};

/**
 * TLS implementation of Pulsar client authentication
 */
class PULSAR_PUBLIC AuthTls : public Authentication {
   public:
    AuthTls(AuthenticationDataPtr&);
    ~AuthTls();

    /**
     * Create an AuthTls with a ParamMap
     *
     * It is equal to create(params[“tlsCertFile”], params[“tlsKeyFile”])
     * @see create(const std::string&, const std::string&)
     */
    static AuthenticationPtr create(ParamMap& params);

    /**
     * Create an AuthTls with an authentication parameter string
     *
     * @see Authentication::parseDefaultFormatAuthParams
     */
    static AuthenticationPtr create(const std::string& authParamsString);

    /**
     * Create an AuthTls with the required parameters
     *
     * @param certificatePath the file path for a client certificate
     * @param privateKeyPath the file path for a client private key
     */
    static AuthenticationPtr create(const std::string& certificatePath, const std::string& privateKeyPath);

    /**
     * @return “tls”
     */
    const std::string getAuthMethodName() const;

    /**
     * Get AuthenticationData from the current instance
     *
     * @param[out] authDataTls the shared pointer of AuthenticationData. The content of AuthenticationData is
     * changed to the internal data of the current instance.
     * @return ResultOk
     */
    Result getAuthData(AuthenticationDataPtr& authDataTls);

   private:
    AuthenticationDataPtr authDataTls_;
};

typedef std::function<std::string()> TokenSupplier;

/**
 * Token based implementation of Pulsar client authentication
 */
class PULSAR_PUBLIC AuthToken : public Authentication {
   public:
    AuthToken(AuthenticationDataPtr&);
    ~AuthToken();

    /**
     * Create an AuthToken with a ParamMap
     *
     * @param parameters it must contain a key-value, where key means how to get the token and value means the
     * token source
     *
     * If the key is “token”, the value is the token
     *
     * If the key is “file”, the value is the file that contains the token
     *
     * If the key is “env”, the value is the environment variable whose value is the token
     *
     * Otherwise, a `std::runtime_error` error is thrown.
     * @see create(const std::string& authParamsString)
     */
    static AuthenticationPtr create(ParamMap& params);

    /**
     * Create an AuthToken with an authentication parameter string
     *
     * @see Authentication::parseDefaultFormatAuthParams
     */
    static AuthenticationPtr create(const std::string& authParamsString);

    /**
     * Create an authentication provider for token based authentication
     *
     * @param token
     *            a string containing the auth token
     */
    static AuthenticationPtr createWithToken(const std::string& token);

    /**
     * Create an authentication provider for token based authentication
     *
     * @param tokenSupplier
     *            a supplier of the client auth token
     */
    static AuthenticationPtr create(const TokenSupplier& tokenSupplier);

    /**
     * @return “token”
     */
    const std::string getAuthMethodName() const;

    /**
     * Get AuthenticationData from the current instance
     *
     * @param[out] authDataToken the shared pointer of AuthenticationData. The content of AuthenticationData
     * is changed to the internal data of the current instance.
     * @return ResultOk
     */
    Result getAuthData(AuthenticationDataPtr& authDataToken);

   private:
    AuthenticationDataPtr authDataToken_;
};

/**
 * Basic based implementation of Pulsar client authentication
 */
class PULSAR_PUBLIC AuthBasic : public Authentication {
   public:
    explicit AuthBasic(AuthenticationDataPtr&);
    ~AuthBasic() override;

    /**
     * Create an AuthBasic with a ParamMap
     *
     * It is equal to create(params[“username”], params[“password”])
     * @see create(const std::string&, const std::string&)
     */
    static AuthenticationPtr create(ParamMap& params);

    /**
     * Create an AuthBasic with an authentication parameter string
     *
     * @param authParamsString the JSON format string: {"username": "admin", "password": "123456"}
     */
    static AuthenticationPtr create(const std::string& authParamsString);

    /**
     * Create an AuthBasic with the required parameters
     */
    static AuthenticationPtr create(const std::string& username, const std::string& password);

    /**
     * Create an AuthBasic with the required parameters
     */
    static AuthenticationPtr create(const std::string& username, const std::string& password,
                                    const std::string& method);

    /**
     * @return “basic”
     */
    const std::string getAuthMethodName() const override;

    /**
     * Get AuthenticationData from the current instance
     *
     * @param[out] authDataBasic the shared pointer of AuthenticationData. The content of AuthenticationData
     * is changed to the internal data of the current instance.
     * @return ResultOk
     */
    Result getAuthData(AuthenticationDataPtr& authDataBasic) override;

   private:
    AuthenticationDataPtr authDataBasic_;
};

/**
 * Athenz implementation of Pulsar client authentication
 */
class PULSAR_PUBLIC AuthAthenz : public Authentication {
   public:
    AuthAthenz(AuthenticationDataPtr&);
    ~AuthAthenz();

    /**
     * Create an AuthAthenz with a ParamMap
     *
     * The required parameter keys are “tenantDomain”, “tenantService”, “providerDomain”, “privateKey”, and
     * “ztsUrl”
     *
     * @param params the key-value to construct ZTS client
     * @see http://pulsar.apache.org/docs/en/security-athenz/
     */
    static AuthenticationPtr create(ParamMap& params);

    /**
     * Create an AuthAthenz with an authentication parameter string
     *
     * @see Authentication::parseDefaultFormatAuthParams
     */
    static AuthenticationPtr create(const std::string& authParamsString);

    /**
     * @return “athenz”
     */
    const std::string getAuthMethodName() const;

    /**
     * Get AuthenticationData from the current instance
     *
     * @param[out] authDataAthenz the shared pointer of AuthenticationData. The content of AuthenticationData
     * is changed to the internal data of the current instance.
     * @return ResultOk
     */
    Result getAuthData(AuthenticationDataPtr& authDataAthenz);

   private:
    AuthenticationDataPtr authDataAthenz_;
};

// OAuth 2.0 token and associated information.
// currently mainly works for access token
class Oauth2TokenResult {
   public:
    static constexpr int undefined_expiration = -1;

    Oauth2TokenResult();
    ~Oauth2TokenResult();

    /**
     * Set the access token string
     *
     * @param accessToken the access token string
     */
    Oauth2TokenResult& setAccessToken(const std::string& accessToken);

    /**
     * Set the ID token
     *
     * @param idToken the ID token
     */
    Oauth2TokenResult& setIdToken(const std::string& idToken);

    /**
     * Set the refresh token which can be used to obtain new access tokens using the same authorization grant
     * or null for none
     *
     * @param refreshToken the refresh token
     */
    Oauth2TokenResult& setRefreshToken(const std::string& refreshToken);

    /**
     * Set the token lifetime
     *
     * @param expiresIn the token lifetime
     */
    Oauth2TokenResult& setExpiresIn(const int64_t expiresIn);

    /**
     * @return the access token string
     */
    const std::string& getAccessToken() const;

    /**
     * @return the ID token
     */
    const std::string& getIdToken() const;

    /**
     * @return the refresh token which can be used to obtain new access tokens using the same authorization
     * grant or null for none
     */
    const std::string& getRefreshToken() const;

    /**
     * @return the token lifetime in milliseconds
     */
    int64_t getExpiresIn() const;

   private:
    // map to json "access_token"
    std::string accessToken_;
    // map to json "id_token"
    std::string idToken_;
    // map to json "refresh_token"
    std::string refreshToken_;
    // map to json "expires_in"
    int64_t expiresIn_;
};

typedef std::shared_ptr<Oauth2TokenResult> Oauth2TokenResultPtr;

class Oauth2Flow {
   public:
    virtual ~Oauth2Flow();

    /**
     * Initializes the authorization flow.
     */
    virtual void initialize() = 0;

    /**
     * Acquires an access token from the OAuth 2.0 authorization server.
     * @return a token result including an access token.
     */
    virtual Oauth2TokenResultPtr authenticate() = 0;

    /**
     * Closes the authorization flow.
     */
    virtual void close() = 0;

   protected:
    Oauth2Flow();
};

typedef std::shared_ptr<Oauth2Flow> FlowPtr;

class CachedToken {
   public:
    virtual ~CachedToken();

    /**
     * @return true if the token has expired
     */
    virtual bool isExpired() = 0;

    /**
     * Get AuthenticationData from the current instance
     *
     * @return ResultOk or ResultAuthenticationError if authentication failed
     */
    virtual AuthenticationDataPtr getAuthData() = 0;

   protected:
    CachedToken();
};

typedef std::shared_ptr<CachedToken> CachedTokenPtr;

/**
 * Oauth2 based implementation of Pulsar client authentication.
 * Passed in parameter would be like:
 * ```
 *   "type": "client_credentials",
 *   "tokenEndpointAuthMethod": "client_secret_post",
 *   "issuer_url": "https://accounts.google.com",
 *   "client_id": "d9ZyX97q1ef8Cr81WHVC4hFQ64vSlDK3",
 *   "client_secret": "on1uJ...k6F6R",
 *   "audience": "https://broker.example.com"
 *  ```
 *
 * For `tokenEndpointAuthMethod = "tls_client_auth"`:
 * ```
 *   "type": "client_credentials",
 *   "tokenEndpointAuthMethod": "tls_client_auth",
 *   "issuer_url": "https://accounts.google.com",
 *   "client_id": "d9ZyX97q1ef8Cr81WHVC4hFQ64vSlDK3",
 *   "tls_cert_file": "/path/to/cert.pem",
 *   "tls_key_file": "/path/to/key.pem"
 * ```
 *  If passed in as std::string, it should be in Json format.
 */
class PULSAR_PUBLIC AuthOauth2 : public Authentication {
   public:
    AuthOauth2(ParamMap& params);
    ~AuthOauth2();

    /**
     * Create an AuthOauth2 with a ParamMap
     *
     * For `tokenEndpointAuthMethod = "client_secret_post"` (default), the required parameter
     * keys are “issuer_url”, “private_key”, and “audience”.
     * Optional keys: `scope`, `tls_cert_file`, `tls_key_file`.
     *
     * For `tokenEndpointAuthMethod = "tls_client_auth"`, the required parameter keys are
     * `issuer_url`, `tls_cert_file`, and `tls_key_file`.
     * Optional keys: `client_id`, `audience`, `scope`. If `client_id` is omitted, the client
     * uses `pulsar-client`.
     *
     * @param parameters the key-value to create OAuth 2.0 client credentials
     * @see http://pulsar.apache.org/docs/en/security-oauth2/#client-credentials
     */
    static AuthenticationPtr create(ParamMap& params);

    /**
     * Create an AuthOauth2 with an authentication parameter string
     *
     * @see Authentication::parseDefaultFormatAuthParams
     */
    static AuthenticationPtr create(const std::string& authParamsString);

    /**
     * @return “token”
     */
    const std::string getAuthMethodName() const;

    /**
     * Get AuthenticationData from the current instance
     *
     * @param[out] authDataOauth2 the shared pointer of AuthenticationData. The content of AuthenticationData
     * is changed to the internal data of the current instance.
     * @return ResultOk or ResultAuthenticationError if authentication failed
     */
    Result getAuthData(AuthenticationDataPtr& authDataOauth2);

   private:
    FlowPtr flowPtr_;
    CachedTokenPtr cachedTokenPtr_;
};

}  // namespace pulsar

#endif /* PULSAR_AUTHENTICATION_H_ */
