/*******************************************************************************
 * libretroshare/src/retroshare: rsinit.h                                      *
 *                                                                             *
 * Copyright (C) 2004-2014  Robert Fernie <retroshare@lunamutt.com>            *
 * Copyright (C) 2016-2019  Gioacchino Mazzurco <gio@altermundi.net>           *
 *                                                                             *
 * This program is free software: you can redistribute it and/or modify        *
 * it under the terms of the GNU Lesser General Public License as              *
 * published by the Free Software Foundation, either version 3 of the          *
 * License, or (at your option) any later version.                             *
 *                                                                             *
 * This program is distributed in the hope that it will be useful,             *
 * but WITHOUT ANY WARRANTY; without even the implied warranty of              *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the                *
 * GNU Lesser General Public License for more details.                         *
 *                                                                             *
 * You should have received a copy of the GNU Lesser General Public License    *
 * along with this program. If not, see <https://www.gnu.org/licenses/>.       *
 *                                                                             *
 *******************************************************************************/
#pragma once

/// @file RetroShare initialization and login API

// Initialize ok, result >= 0
#define RS_INIT_OK              0 // Initialize ok
#define RS_INIT_HAVE_ACCOUNT    1 // Initialize ok, have account
// Initialize failed, result < 0
#define RS_INIT_AUTH_FAILED    -1 // AuthGPG::InitAuth failed
#define RS_INIT_BASE_DIR_ERROR -2 // AuthGPG::InitAuth failed
#define RS_INIT_NO_KEYRING     -3 // Keyring is empty. Need to import it.
#define RS_INIT_NO_EXECUTABLE  -4 // executable path hasn't been set in config options

#include <list>
#include <map>
#include <vector>
#include <cstdint>
#include <system_error>

#include "retroshare/rstypes.h"
#include "retroshare/rsversion.h"


class RsLoginHelper;

/**
 * Pointer to global instance of RsLoginHelper
 * @jsonapi{development}
 */
extern RsLoginHelper* rsLoginHelper;


enum class RsInitErrorNum : int32_t
{
	ALREADY_LOGGED_IN         = 6000,
	CANT_ACQUIRE_LOCK         = 6001,
	INVALID_LOCATION_NAME     = 6002,
	PGP_NAME_OR_ID_NEEDED     = 6003,
	PGP_KEY_CREATION_FAILED   = 6004,
	SSL_KEY_CREATION_FAILED   = 6005,
	INVALID_SSL_ID            = 6006,
    LOGIN_FAILED              = 6007
};

struct RsInitErrorCategory: std::error_category
{
	const char* name() const noexcept override
	{ return "RetroShare init"; }

	std::string message(int ev) const override
	{
		switch (static_cast<RsInitErrorNum>(ev))
		{
		case RsInitErrorNum::ALREADY_LOGGED_IN:
			return "Already logged in";
		case RsInitErrorNum::CANT_ACQUIRE_LOCK:
			return "Cannot aquire lock on location data. Another instance is "
			        "already running with this profile?";
		case RsInitErrorNum::INVALID_LOCATION_NAME:
			return "Invalid location name";
		case RsInitErrorNum::PGP_NAME_OR_ID_NEEDED:
			return "Either PGP name or PGP id is needed";
		case RsInitErrorNum::PGP_KEY_CREATION_FAILED:
			return "Failure creating PGP key";
		case RsInitErrorNum::SSL_KEY_CREATION_FAILED:
			return "Failure creating SSL key";
		case RsInitErrorNum::INVALID_SSL_ID:
			return "Invalid SSL id";
		case RsInitErrorNum::LOGIN_FAILED:
			return "Generic login failure";
		default:
			return rsErrorNotInCategory(ev, name());
		}
	}

	const static RsInitErrorCategory instance;
};


namespace std
{
/** Register RsJsonApiErrorNum as an error condition enum, must be in std
 * namespace */
template<> struct is_error_condition_enum<RsInitErrorNum> : true_type {};
}

/** Provide RsInitErrorNum conversion to std::error_condition, must be in
 * same namespace of RsInitErrorNum */
inline std::error_condition make_error_condition(RsInitErrorNum e) noexcept
{
	return std::error_condition(
	            static_cast<int>(e), RsInitErrorCategory::instance );
};



/**
 * @brief The RsInitConfig struct
 * This class contains common configuration options, that executables using libretroshare may want to
 * set using e.g. commandline options. To be passed to RsInit::InitRetroShare().
 */
struct RsConfigOptions
{
	RsConfigOptions();

    // required

	std::string main_executable_path;/* this should be set to argv[0] */

    // Optional. Only change if needed.

	bool autoLogin;                  /* try auto-login */

	bool udpListenerOnly;			 /* only listen to udp */
    std::string forcedInetAddress; 	 /* inet address to use.*/
    uint16_t    forcedPort; 	     /* port to listen to */

	bool outStderr;
	int  debugLevel;
    std::string logfname;			/* output filename for log */

	std::string opModeStr;          /* operating mode. Acceptable values: "Full", "NoTurtle", "Gaming", "Minimal" */
	std::string optBaseDir;			/* base directory where to find profiles, etc */
    std::string userSuppliedTorExecutable; /* allows the user to supply his own Tor executable, or to tell RS where to find Tor */

	uint16_t    jsonApiPort;		/* port to use fo Json API */
	std::string jsonApiBindAddress; /* bind address for Json API */
};


/*!
 * Initialisation Class (not publicly disclosed to RsIFace)
 */
class RsInit
{
public:
    enum LoadCertificateStatus : uint8_t
	{
        OK                          = 0x00,	/// Everything go as expected, no error occurred
        ERR_ALREADY_RUNNING         = 0x01,	/// Another istance is running already
        ERR_CANT_ACQUIRE_LOCK       = 0x02,	/// Another istance is already running?
        ERR_NO_AVAILABLE_ACCOUNT    = 0x03,	/// Used in retroshare-service -U list when no account is available
        ERR_CANNOT_CONFIGURE_TOR    = 0x04,	/// cannot start/configure Tor for an auto-tor node
        ERR_NO_ACCOUNT_SELECTED     = 0x05,	/// cannot start/configure Tor for an auto-tor node
        ERR_MISSING_ACCOUNT_PATH    = 0x06,	/// cannot start/configure Tor for an auto-tor node
        ERR_MISSING_PASSPHRASE      = 0x07,	/// cannot start/configure Tor for an auto-tor node

        // Errors directly reported by AuthSSL::InitImpl()

        ERR_CERT_CRYPTO_IS_TOO_WEAK = 0x08,	/// Certificate is based on some weak settings.
        ERR_CANNOT_INIT_TLS_LIB     = 0x09, /// TLS initialization failed.
        ERR_MISSING_PARAMETERS      = 0x0a, /// Some params are null, including public/private cert filenames, and login passwd.
        ERR_MISSING_CERT_FILE       = 0x0b, /// Missing public key file (user_cert.pem)
        ERR_MISSING_PKEY_FILE       = 0x0c, /// Missing private key file (user_pk.pem)
        ERR_CORRUPTED_CERT_FILE     = 0x0d, /// Public node key file is corrupted
        ERR_CORRUPTED_PKEY_FILE     = 0x0e, /// Private node key file is corrupted
        ERR_PUBLIC_PKEY_MISMATCH    = 0x0f, /// Public key doesn't match private key. May be due to file corruption between user_pk.pem and user_cert.pem.
        ERR_CERT_VALIDATION_FAILED  = 0x10, /// Profile signature validation failed on node certificate
        ERR_WRONG_CERT_FORMAT       = 0x11, /// Certificate format is not recognised (e.g. signature missing, etc)
        ERR_CERT_REJECTED_BY_SSL    = 0x12,	/// Certificate doesn't pass OpenSSL internal checks.

        ERR_UNKNOWN                 = 0xff, /// Unkown error, maybe password is wrong?
	};

	/* reorganised RsInit system */

	/*!
	 * PreLogin
	 * Call before init retroshare, initialises rsinitconfig's public attributes
	 */
	static void InitRsConfig();

	/*!
	 * Should be called to load up ssl cert and private key, and intialises gpg
	 * this must be called before accessing rsserver (e.g. ::startupretroshare)
	 * @param argc passed from executable
	 * @param argv commandline arguments passed to executable
	 * @param strictCheck set to true if you want rs to continue executing if
	 *	invalid argument passed and vice versa
	 * @return RS_INIT_...
	 */
	static int InitRetroShare(const RsConfigOptions&);

	static bool isPortable();
	static bool isWindowsXP();
	static bool collectEntropy(uint32_t bytes) ;
    static bool startAutoTor();

    /*!
     * \brief lockFilePath
     * \return
     * 		full path for the lock file. Can be used to warn the user about a non deleted lock that would prevent to start.
     */
	static std::string lockFilePath();

	/*
	 * Setup Hidden Location;
	 */
	static void SetHiddenLocation(const std::string& hiddenaddress, uint16_t port, bool useI2p);

	static bool LoadPassword(const std::string& passwd) ;

	/*
	 * Final Certificate load. This can be called if:
	 * a) InitRetroshare() returns RS_INIT_HAVE_ACCOUNT -> autoLoad/password Set.
	 * b) or LoadPassword()
	 *
	 * This uses the preferredId from RsAccounts.
	 * This wrapper also locks the profile before finalising the login
	 */
	static LoadCertificateStatus LockAndLoadCertificates(
	        bool autoLoginNT, std::string& lockFilePath );

	// Post Login Options
	static bool getStartMinimised();

	static int getSslPwdLen();
	static bool getAutoLogin();
	static void setAutoLogin(bool autoLogin);
	static bool RsClearAutoLogin() ;

    static std::string executablePath() ;
private:
	/** @brief Lock profile directory
	 * param[in] accountDir account directory to lock
	 * param[out] lockFilePath path of the created lock-file
	 */
	static LoadCertificateStatus LockConfigDirectory(
	        const std::string& accountDir, std::string& lockFilePath);

	/// @brief Unlock profile directory
	static void UnlockConfigDirectory();

    static bool LoadCertificates(bool autoLoginNT,LoadCertificateStatus& error_code);
};



/* Seperate static Class for dealing with Accounts */

class RsAccountsDetail;

class RsAccounts
{
public:
	/// Should be called once before everything else.
	static bool init(const std::string &opt_base_dir, int& error_code);

	/**
	 * @brief ConfigDirectory (usually ~/.retroshare) you can call this method
	 * even before initialisation (you can't with some other methods)
	 * @see RsAccountsDetail::PathBaseDirectory()
	 * @jsonapi{development,unauthenticated}
	 * @return a string containing the path
	 */
	static std::string ConfigDirectory();

	/**
	 * @brief Get current account id. Beware that an account may be selected
	 *	without actually logging in.
	 * @jsonapi{development,unauthenticated}
	 * @param[out] id storage for current account id
	 * @return false if account hasn't been selected yet, true otherwise
	 */
	static bool getCurrentAccountId(RsPeerId &id);

	/**
	 * @brief DataDirectory
	 * you can call this method even before initialisation (you can't with some other methods)
	 * @param check if set to true and directory does not exist, return empty string
	 * @return path where global platform independent files are stored, like bdboot.txt or webinterface files
	 */
	static std::string systemDataDirectory(bool check = true);
	static std::string PGPDirectory();

	/**
	 * @brief Get available PGP identities id list
	 * @jsonapi{development,unauthenticated}
	 * @param[out] pgpIds storage for PGP id list
	 * @return true on success, false otherwise
	 */
	static int GetPGPLogins(std::list<RsPgpId> &pgpIds);
	static int     GetPGPLoginDetails(const RsPgpId& id, std::string &name, std::string &email);
	static bool    GeneratePGPCertificate(const std::string&, const std::string& email, const std::string& passwd, RsPgpId &pgpId, const int keynumbits, std::string &errString);

	/**
	 * @brief Export full encrypted PGP identity to file
	 * @jsonapi{development}
	 * @param[in] filePath path of certificate file
	 * @param[in] pgpId PGP id to export
	 * @return true on success, false otherwise
	 */
	static bool ExportIdentity( const std::string& filePath,
	                            const RsPgpId& pgpId );

	/**
	 * @brief Import full encrypted PGP identity from file
	 * @jsonapi{development,unauthenticated}
	 * @param[in] filePath path of certificate file
	 * @param[out] pgpId storage for the PGP fingerprint of the imported key
	 * @param[out] errorMsg storage for eventual human readable error message
	 * @return true on success, false otherwise
	 */
	static bool ImportIdentity(
	        const std::string& filePath, RsPgpId& pgpId, std::string& errorMsg );

	/**
	 * @brief Import full encrypted PGP identity from string
	 * @jsonapi{development,unauthenticated}
	 * @param[in] data certificate string
	 * @param[out] pgpId storage for the PGP fingerprint of the imported key
	 * @param[out] errorMsg storage for eventual human readable error message
	 * @return true on success, false otherwise
	 */
	static bool importIdentityFromString(
	        const std::string& data, RsPgpId& pgpId,
	        std::string& errorMsg );

	/**
	 * @brief Export full encrypted PGP identity to string
	 * @jsonapi{development}
	 * @param[out] data storage for certificate string
	 * @param[in] pgpId PGP id to export
	 * @param[in] includeSignatures true to include signatures
	 * @param[out] errorMsg storage for eventual human readable error message
	 * @return true on success, false otherwise
	 */
	static bool exportIdentityToString(
	        std::string& data, const RsPgpId& pgpId, std::string& errorMsg,
	        bool includeSignatures = true );

	static void    GetUnsupportedKeys(std::map<std::string,std::vector<std::string> > &unsupported_keys);
	static bool    CopyGnuPGKeyrings() ;

	// Rs Accounts
	static bool SelectAccount(const RsPeerId& id);
	static bool	GetPreferredAccountId(RsPeerId &id);
	static bool GetAccountIds(std::list<RsPeerId> &ids);

	static bool	GetAccountDetails(const RsPeerId &id, RsPgpId &gpgId, std::string &gpgName, std::string &gpgEmail, std::string &location);

	static bool createNewAccount(
	        const RsPgpId& pgp_id, const std::string& org,
	        const std::string& loc, const std::string& country,
	        bool ishiddenloc, bool is_auto_tor, const std::string& passwd,
	        RsPeerId &sslId, std::string &errString );

    static void storeSelectedAccount() ;

    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    //                                 All methods bellow can only be called ones SelectAccount() as been called.                                       //
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    static bool getCurrentAccountOptions(bool& is_hidden,bool& is_tor_auto,bool& is_first_time) ;

	static bool checkCreateAccountDirectory();		// Generate the hierarchy of directories below ~/.retroshare/[SSL dir]/
    static bool isHiddenNode() ;                    // true if the running node is a hidden node. Used to choose which services to start.
    static bool isTorAuto() ;                       // true if the running node is a hidden node using automated Tor management

	static std::string AccountDirectory();			// linux: ~/.retroshare/[SSL dir]/
	static std::string AccountKeysDirectory();		// linux: ~/.retroshare/[SSL dir]/keys/
	static std::string AccountPathCertFile();		// linux: ~/.retroshare/[SSL dir]/keys/user_cert.pem
	static std::string AccountPathKeyFile();		// linux: ~/.retroshare/[SSL dir]/keys/user_pk.pem
	static std::string AccountLocationName();

    static bool lockPreferredAccount() ;	// are these methods any useful??
    static void unlockPreferredAccount() ;

private:
	static RsAccountsDetail* rsAccountsDetails;
};

/**
 * Pointer to global instance of RsAccounts needed to expose JSON API, as all
 * the members of this class are static you should call them directly without
 * using this pointer in the other parts of the code
 * @jsonapi{development}
 */
extern RsAccounts* rsAccounts;


/**
 * This helper class have been implemented because there was not reasonable way
 * to login in the API that could be exposed via JSON API
 */
class RsLoginHelper
{
public:
	RsLoginHelper() = default;

	/**
	 * @brief Normal way to attempt login
	 * @jsonapi{development,unauthenticated}
	 * @param[in] account Id of the account to which attempt login
	 * @param[in] password Password for the given account
	 * @return RsInit::OK if login attempt success, error code otherwhise
	 */
	RsInit::LoadCertificateStatus attemptLogin(
	        const RsPeerId& account, const std::string& password );

	/**
	 * @brief Feed extra entropy to the crypto libraries
	 * @jsonapi{development,unauthenticated}
	 * @param[in] bytes number to feed to the entropy pool
	 * @return false if error occurred, true otherwise
	 */
	static bool collectEntropy(uint32_t bytes);

	struct Location : RsSerializable
	{
		RsPeerId mLocationId;
		RsPgpId mPgpId;
		std::string mLocationName;
		std::string mPgpName;

		/// @see RsSerializable::serial_process
		void serial_process( RsGenericSerializer::SerializeJob j,
		                     RsGenericSerializer::SerializeContext& ctx );
	};

	/**
	 * @brief Get locations and associated information
	 * @jsonapi{development,unauthenticated}
	 * @param[out] locations storage for the retrived locations
	 */
	void getLocations(std::vector<RsLoginHelper::Location>& locations);

	/**
	 * @brief Creates a new RetroShare location, and log in once is created
	 * @jsonapi{development,manualwrapper}
	 * @param[out] locationId storage for generated location SSL id
	 * @param[inout] pgpId specify PGP id to use to sign the location, if a null
	 *	id is passed the PGP key is created too and this param is used as
	 *	storage for its id.
	 * @param[in] password to protect and unlock the associated PGP key
	 * param[in] apiUser (JSON API only) string containing username for JSON API
	 *	so it can be later used to authenticate JSON API calls. It is passed
	 *	down to @see RsJsonApi::authorizeUser under the hood.
	 * param[in] apiPass (JSON API only) string containing password for JSON API
	 *	so it can be later used to authenticate JSON API calls. It is passed
	 *	down to @see RsJsonApi::authorizeUser under the hood.
	 *	To improve security we strongly advise to not use the same as the
	 *	password used for the PGP key.
	 * @return Success or error information
	 */
	std::error_condition createLocationV2(
	        RsPeerId& locationId,
	        RsPgpId& pgpId,
	        const std::string& locationName,
	        const std::string& pgpName,
	        const std::string& password
	        /* JSON API only
	         * const std::string& apiUser
	         * const std::string& apiPass */ );

	/**
	 * @brief Check if RetroShare is already logged in, this usually return true
	 *	after a successfull attemptLogin() and before closeSession()
	 * @jsonapi{development,unauthenticated}
	 * @return true if already logged in, false otherwise
	 */
	bool isLoggedIn();

#if !RS_VERSION_AT_LEAST(0,6,6)
	/**
	 * @deprecated Use @see createLocationV2 instead
	 * @brief Creates a new RetroShare location, and log in once is created
	 * @jsonapi{development,manualwrapper}
	 * @param[inout] location provide input information to generate the location
	 *	and storage to output the data of the generated location
	 * @param[in] password to protect and unlock the associated PGP key
	 * @param[out] errorMessage if some error occurred human readable error
	 *	message
	 * @param[in] makeHidden pass true to create an hidden location. UNTESTED!
	 * @param[in] makeAutoTor pass true to create an automatically configured
	 *	Tor hidden location. UNTESTED!
	 * @return true if success, false otherwise
	 */
	RS_DEPRECATED_FOR(createLocationV2)
	bool createLocation( RsLoginHelper::Location& location,
	                     const std::string& password, std::string& errorMessage,
	                     bool makeHidden = false, bool makeAutoTor = false );
#endif // !RS_VERSION_AT_LEAST(0,6,6)
};
