/*
** Copyright (C) 2023-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
** Free Software Foundation; either version 3, 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 General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software Foundation,
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**
*/

#ifndef MU_CONFIG_HH__
#define MU_CONFIG_HH__

#include <cstdint>
#include <cinttypes>
#include <string_view>
#include <string>
#include <array>
#include <vector>
#include <variant>
#include <unordered_map>

#include "mu-xapian-db.hh"

#include <glib.h>
#include <gmime/gmime.h>

#include <utils/mu-utils.hh>
#include <utils/mu-result.hh>
#include <utils/mu-option.hh>

#include <config.h>

namespace Mu {

struct Property {
        enum struct Id {
                // store properties, i.e. properties associated with
                // some store object (and store in the database), either
                // at init time or updated during runtime.
                BatchSize,         /**< Xapian batch-size */
                Contacts,          /**< Cache of contact information */
                Created,           /**< Time of creation */
                IgnoredAddresses,  /**< Email addresses ignored for the
                                      contacts-cache */
                Labels,            /**< Serialized label information. */
                LastChange,        /**< Time of last change */
                LastIndex,         /**< Time of last index */
                MaxMessageSize,    /**< Maximum message size (in bytes) */
                PersonalAddresses, /**< List of personal e-mail addresses */
                RootMaildir,       /**< Root maildir path */
                SchemaVersion,     /**< Xapian DB schema version */
                NgramsEnabled,     /**< Support ngrams for indexing & querying
                                    *  for e.g. CJK languages */
                // build/system properties
                MuVersion,     /**< Mu version */
                XapianVersion, /**< Xapian runtime version */
                GlibVersion,   /**< GLib version */
		GmimeVersion,  /**< GMime version */
                ScmEnabled,    /**< Built with SCM (Guile) support */
                LanguageEnabled /**< Built with language-detection support */
	};

        enum struct Flags {
                None     = 0,      /**< Nothing in particular */
                ReadOnly = 1 << 0, /**< Property is read-only for external use
                                    * (but can change from within the store) */
                Configurable = 1 << 1, /**< A user-configurable parameter; name
                                        * starts with 'conf-' */
                Internal = 1 << 2,     /**< Mu-internal field */
                Runtime  = 1 << 3,     /**< May change at runtime */
                System = 1 << 4,     /**< System property; read-only */
	};
	enum struct Type {
		Boolean,        /**< Some boolean value */
		Number,		/**< Some number */
		Timestamp,      /**< Timestamp number */
		Path,           /**< Path string */
		String,		/**< A string */
		StringList,	/**< A list of strings */
	};

	using Value = std::variant<int64_t, std::string, std::vector<std::string> >;
	using DefaultValue = std::string(*)(); /**< Function that returns a string_view */

	Id			id;
	Type			type;
	Flags			flags;
	std::string_view	name;
	DefaultValue            default_val;
	std::string_view	description;
};

MU_ENABLE_BITOPS(Property::Flags);

class Config {
public:
	using	Id    = Property::Id;
	using	Type  = Property::Type;
	using	Flags = Property::Flags;
	using	Value = Property::Value;

	/**
	 * Properties specific to the store.
	 */
        static constexpr auto properties = std::to_array<Property>({
		// store-properties
		{
			Id::BatchSize,
			Type::Number,
			Flags::Configurable,
			"batch-size",
			[]()->std::string {return "50000";},
			"Maximum number of changes in a database transaction"
		},
		{
			Id::Contacts,
			Type::String,
			Flags::Internal,
			"contacts",
			{},
			"Serialized contact information"
		},
		{
			Id::Created,
			Type::Timestamp,
			Flags::ReadOnly,
			MetadataIface::created_key,
			{},
			"Database creation time"
		},
		{
			Id::IgnoredAddresses,
			Type::StringList,
			Flags::Configurable,
			"ignored-addresses",
			{},
			"E-mail addresses ignored  for the contacts-cache, "
			"literal or /regexp/"
		},
		{
			Id::Labels,
			Type::String,
			Flags::Internal,
			"labels",
			{},
			"Serialized labels information"
		},

		{
			Id::LastChange,
			Type::Timestamp,
			Flags::ReadOnly | Flags::Runtime,
			MetadataIface::last_change_key,
			{},
			"Time when last change occurred"
		},
		{
			Id::LastIndex,
			Type::Timestamp,
			Flags::ReadOnly | Flags::Runtime,
			"last-index",
			{},
			"Time when last indexing operation was completed"
		},
		{
			Id::MaxMessageSize,
			Type::Number,
			Flags::Configurable,
			"max-message-size",
			[]()->std::string {return "100000000";}, // default max: 100M bytes
			"Maximum message size (in bytes); bigger messages are skipped"
		},
		{
			Id::PersonalAddresses,
			Type::StringList,
			Flags::Configurable,
			"personal-addresses",
			{},
			"Personal e-mail addresses, literal or /regexp/"
		},
		{
			Id::RootMaildir,
			Type::Path,
			Flags::ReadOnly,
			"root-maildir",
			{},
			"Absolute path of the top of the Maildir tree"
		},
		{
			Id::SchemaVersion,
			Type::Number,
			Flags::ReadOnly,
			"schema-version",
			{},
			"Version of the Xapian database schema"
		},
		{
			Id::NgramsEnabled,
			Type::Boolean,
			Flags::Configurable,
			"ngrams-enabled",
			{},
			"Support n-grams for working with CJK and other languages"
		},

		// system properties
		{
			Id::MuVersion,
			Type::String,
			Flags::System,
			"mu-version",
			[]()->std::string {return VERSION;},
			"mu version string"
		},
		{
			Id::XapianVersion,
			Type::String,
			Flags::System,
			"xapian-version",
			[]()->std::string {return Xapian::version_string();},
			"Xapian runtime version string"
		},
		{
			Id::GlibVersion,
			Type::String,
			Flags::System,
			"glib-version",
			[]()->std::string{return mu_format("{}.{}.{}", glib_major_version, glib_minor_version,
							   glib_micro_version);},
			"GLib runtime version string"
		},
		{
			Id::GmimeVersion,
			Type::String,
			Flags::System,
			"gmime-version",
			[]()->std::string {return mu_format("{}.{}.{}", gmime_major_version, gmime_minor_version,
								  gmime_micro_version);},
			"GMime runtime version string"
		},
		{
			Id::ScmEnabled,
			Type::Boolean,
			Flags::System,
			"scm-enabled",
			[]()->std::string { return mu_format("{}", BUILD_SCM); },
			"Is SCM/Guile supported?"
		},
		{
			Id::LanguageEnabled,
			Type::Boolean,
			Flags::System,
			"language-enabled",
			[]()->std::string { return mu_format("{}", HAVE_CLD2); },
			"Is language detection/searching supported?"
		},
	});

	/**
	 * Construct a new Config object.
	 *
	 * @param db The config-store (database); must stay valid for the
	 * lifetime of this config.
	 */
	Config(MetadataIface& cstore): cstore_{cstore}{}

	/**
	 * Get the property by its id
	 *
	 * @param id a property id
	 *
	 * @return the property
	 */
	template <Id ID>
	constexpr static const Property& property() {
		return properties[static_cast<size_t>(ID)];
	}

	/**
	 * Get a Property by its name.
	 *
	 * @param name The name
	 *
	 * @return the property or Nothing if not found
	 */
	static Option<const Property&> property(const std::string& name) {
		const auto pname{std::string_view(name.data(), name.size())};
		for (auto&& prop: properties)
			if (prop.name == pname)
				return prop;
		return Nothing;
	}


	/**
	 * Get the property value decoded based on the type
	 *
	 * @param str the raw string value
	 *
	 * @return the value
	 */
	template<Type type>
	static auto decode(const std::string& str)  {
		if constexpr (type == Type::Number)
			return static_cast<size_t>(str.empty() ? 0 : std::atoll(str.c_str()));
		else if constexpr (type == Type::Boolean)
			return static_cast<bool>(str.empty() ? false :
						   std::atol(str.c_str()) != 0);
		else if constexpr (type == Type::Timestamp)
			return static_cast<int64_t>(str.empty() ? 0 : std::atoll(str.c_str()));
		else if constexpr (type == Type::Path || type == Type::String)
			return str;
		else if constexpr (type == Type::StringList)
			return split(str, SepaChar1);
		throw std::logic_error("invalid type");
	}

	/**
	 * Get default value for the property, or Nothing if it does not have one.
	 *
	 * @param prop some property
	 *
	 * @return string or nothing
	 */
	static Option<std::string> default_value(const Property& prop) {
		if (!prop.default_val)
			return Nothing;
		else
			return prop.default_val();
	}

	/**
	 * Get the raw string-value for some property
	 *
	 * @param prop some property
	 *
	 * @return a string
	 */
	std::string as_raw_string(const Property& prop) const {
		// system properties are _not_ stored in the db.
		if (any_of(prop.flags & Property::Flags::System)) {
			return default_value(prop).value_or("");
		} else {
			if (const auto str = cstore_.metadata(std::string{prop.name}); !str.empty())
				return str;
			else
				return default_value(prop).value_or("");
		}
	}

	/**
	 * Get the string-value for some property (for display)
	 *
	 * @param prop some property
	 *
	 * @return a string
	 */
	std::string as_display_string(const Property& prop) const {
		// system properties are _not_ stored in the db.
		const auto str{as_raw_string(prop)};
		switch (prop.type) {
		case Type::Boolean:
			return decode<Type::Boolean>(str) ? "yes" : "no";
		case Type::StringList:
			return mu_format("{}", mu_join(decode<Type::StringList>(str), ", "));
		case Type::Timestamp: {
			const auto t{decode<Type::Timestamp>(str)};
			return t == 0 ? "never" : mu_format("{:%c}", mu_time(t));
		}
		default:
			return str;
		}
	}


	/**
	 * Get the property value of the correct type
	 *
	 * @param prop_id a property id
	 *
	 * @return the value or Nothing
	 */
	template<Id ID>
	auto get() const {
		constexpr auto& prop{property<ID>()};
		return decode<prop.type>(as_raw_string(prop));
	}

	/**
	 * Set a new value for some property
	 *
	 * @param prop_id property-id
	 * @param val the new value (of the correct type)
	 *
	 * @return Ok() or some error
	 */
	template<Id ID, typename T>
	Result<void> set(const T& val) {
		constexpr auto&& prop{property<ID>()};
		if (any_of(prop.flags & Property::Flags::System))
			return Err(Error::Code::AccessDenied, "cannot write system property");
		if (read_only())
			return Err(Error::Code::AccessDenied, "cannot write to read-only db");

		const auto strval = std::invoke([&]{
			if constexpr (prop.type == Type::Number || prop.type == Type::Timestamp)
				return mu_format("{}", static_cast<int64_t>(val));
			if constexpr (prop.type == Type::Boolean)
				return val ? "1" : "0";
			else if constexpr (prop.type == Type::Path || prop.type == Type::String)
				return std::string{val};
			else if constexpr (prop.type == Type::StringList)
				return join(val, SepaChar1);
			else
				throw std::logic_error("invalid prop " + std::string{prop.name});
		});

		cstore_.set_metadata(std::string{prop.name}, strval);
		return Ok();
	}

	/**
	 * Is this a read-only Config?
	 *
	 *
	 * @return true or false
	 */
	bool read_only() const { return cstore_.read_only();};

	/**
	 * Import configurable settings to some other MetadataIface
	 *
	 * @param target some other metadata interface
	 */
	void import_configurable(const Config& src) const {
		for (auto&& prop: properties) {
			if (any_of(prop.flags & Flags::Configurable)) {
				const auto&& key{std::string{prop.name}};
				if (const auto& val{src.cstore_.metadata(key)}; !val.empty())
					cstore_.set_metadata(key, std::string{val});
			}
		}
	}

private:
	MetadataIface& cstore_;
};


} // namespace Mu

#endif /* MU_CONFIG_DB_HH__ */
