/*
 * SPDX-FileCopyrightText: 2024 Jérémie Galarneau <jeremie.galarneau@efficios.com>
 *
 * SPDX-License-Identifier: GPL-2.0-only
 *
 */

#ifndef LTTNG_SESSIOND_DOMAIN_HPP
#define LTTNG_SESSIOND_DOMAIN_HPP

#include "channel-configuration.hpp"
#include "map-channel-configuration.hpp"
#include "process-attribute-tracker.hpp"
#include "recording-channel-configuration.hpp"

#include <common/container-wrapper.hpp>
#include <common/defaults.hpp>
#include <common/domain.hpp>
#include <common/exception.hpp>
#include <common/format.hpp>
#include <common/make-unique.hpp>
#include <common/string-utils/c-string-view.hpp>

#include <lttng/domain.h>

#include <vendor/optional.hpp>

#include <cstdint>
#include <memory>
#include <string>
#include <unordered_map>

namespace lttng {
namespace sessiond {
namespace config {

#define LTTNG_THROW_CHANNEL_NOT_FOUND_BY_NAME_ERROR(channel_name)           \
	throw lttng::sessiond::config::exceptions::channel_not_found_error( \
		channel_name, LTTNG_SOURCE_LOCATION())

#define LTTNG_THROW_MAP_CHANNEL_NOT_FOUND_BY_NAME_ERROR(channel_name)           \
	throw lttng::sessiond::config::exceptions::map_channel_not_found_error( \
		channel_name, LTTNG_SOURCE_LOCATION())

#define LTTNG_THROW_MAP_CHANNEL_ALREADY_EXISTS_ERROR(channel_name)                   \
	throw lttng::sessiond::config::exceptions::map_channel_already_exists_error( \
		channel_name, LTTNG_SOURCE_LOCATION())

namespace exceptions {
/*
 * @class channel_not_found_error
 * @brief Represents a channel-not-found error and provides the name of the channel looked-up
 * for use by error-reporting code.
 */
class channel_not_found_error : public lttng::runtime_error {
public:
	explicit channel_not_found_error(std::string channel_name,
					 const lttng::source_location& source_location);

	const std::string channel_name;
};

/*
 * @class map_channel_not_found_error
 * @brief Represents a map-channel-not-found error and provides the name of the map channel
 * looked-up for use by error-reporting code.
 */
class map_channel_not_found_error : public lttng::runtime_error {
public:
	explicit map_channel_not_found_error(std::string channel_name,
					     const lttng::source_location& source_location);

	const std::string channel_name;
};

/*
 * @class map_channel_already_exists_error
 * @brief Represents an attempt to add a map channel whose name is already in use within the
 * domain, and provides the name of that map channel for use by error-reporting code.
 */
class map_channel_already_exists_error : public lttng::runtime_error {
public:
	explicit map_channel_already_exists_error(std::string channel_name,
						  const lttng::source_location& source_location);

	const std::string channel_name;
};
} /* namespace exceptions */

namespace details {
/* Create a tracker if domain is kernel-only. */
template <typename TrackerType>
nonstd::optional<TrackerType> make_kernel_tracker(lttng::domain_class domain_class)
{
	if (domain_class == lttng::domain_class::KERNEL_SPACE) {
		return TrackerType();
	}

	return nonstd::nullopt;
}

/* Create a tracker if domain is kernel or user space. */
template <typename TrackerType>
nonstd::optional<TrackerType> make_kernel_or_user_space_tracker(lttng::domain_class domain_class)
{
	if (domain_class == lttng::domain_class::KERNEL_SPACE ||
	    domain_class == lttng::domain_class::USER_SPACE) {
		return TrackerType();
	}

	return nonstd::nullopt;
}

} /* namespace details */

/*
 * A domain holds the channel configurations for a specific tracing domain
 * (kernel, user space, agent, etc.) within a recording session.
 *
 * Process attribute trackers are domain-specific:
 * - Kernel domain: all 6 trackers (pid, vpid, uid, vuid, gid, vgid)
 * - User space domain: 3 virtual trackers (vpid, vuid, vgid)
 * - Agent domains: no trackers (use agent_domain class instead)
 */
class domain final {
public:
	explicit domain(lttng::domain_class domain_class) :
		domain_class_(domain_class),
		_metadata_channel(_make_default_metadata_channel_configuration()),
		_process_id_tracker(
			details::make_kernel_tracker<process_id_tracker_t>(domain_class)),
		_virtual_process_id_tracker(
			details::make_kernel_or_user_space_tracker<virtual_process_id_tracker_t>(
				domain_class)),
		_user_id_tracker(details::make_kernel_tracker<user_id_tracker_t>(domain_class)),
		_virtual_user_id_tracker(
			details::make_kernel_or_user_space_tracker<virtual_user_id_tracker_t>(
				domain_class)),
		_group_id_tracker(details::make_kernel_tracker<group_id_tracker_t>(domain_class)),
		_virtual_group_id_tracker(
			details::make_kernel_or_user_space_tracker<virtual_group_id_tracker_t>(
				domain_class))
	{
	}

	~domain() = default;

	domain(domain&& other) noexcept :
		domain_class_(other.domain_class_),
		_metadata_channel(other._metadata_channel),
		_channels(std::move(other._channels)),
		_map_channels(std::move(other._map_channels)),
		_process_id_tracker(std::move(other._process_id_tracker)),
		_virtual_process_id_tracker(std::move(other._virtual_process_id_tracker)),
		_user_id_tracker(std::move(other._user_id_tracker)),
		_virtual_user_id_tracker(std::move(other._virtual_user_id_tracker)),
		_group_id_tracker(std::move(other._group_id_tracker)),
		_virtual_group_id_tracker(std::move(other._virtual_group_id_tracker))
	{
	}

	domain(const domain&) = delete;
	domain& operator=(const domain&) = delete;
	domain& operator=(domain&&) = delete;

	/* Metadata channel configuration, initialized with suitable defaults. */
	const metadata_channel_configuration& metadata_channel() const noexcept
	{
		return _metadata_channel;
	}

	/* Add a channel to the domain by constructing it in place. */
	template <typename... Args>
	recording_channel_configuration& add_channel(Args&&...args)
	{
		auto new_channel = lttng::make_unique<recording_channel_configuration>(
			std::forward<Args>(args)...);

		_validate_channel(*new_channel);

		auto result = _channels.emplace(new_channel->name, std::move(new_channel));
		if (!result.second) {
			LTTNG_THROW_ERROR(lttng::format(
				"Failed to add channel to domain, name already in use: channel_name=`{}`",
				result.first->first));
		}

		return *(result.first->second);
	}

	/* Lookup by name. */
	recording_channel_configuration& get_channel(lttng::c_string_view name)
	{
		const auto it = _channels.find(name.data());
		if (it == _channels.end()) {
			LTTNG_THROW_CHANNEL_NOT_FOUND_BY_NAME_ERROR(name);
		}

		return *(it->second);
	}

	const recording_channel_configuration& get_channel(lttng::c_string_view name) const
	{
		/* NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) */
		return const_cast<domain *>(this)->get_channel(name);
	}

	using recording_channels_view = lttng::utils::dereferenced_mapped_values_view<
		std::unordered_map<std::string, recording_channel_configuration::uptr>,
		recording_channel_configuration>;
	using const_recording_channels_view = lttng::utils::dereferenced_mapped_values_view<
		const std::unordered_map<std::string, recording_channel_configuration::uptr>,
		const recording_channel_configuration>;

	recording_channels_view recording_channels() noexcept
	{
		return recording_channels_view(_channels);
	}

	const_recording_channels_view recording_channels() const noexcept
	{
		return const_recording_channels_view(_channels);
	}

	std::size_t recording_channel_count() const noexcept
	{
		return _channels.size();
	}

	bool has_non_default_channel() const noexcept
	{
		const auto default_channel_count = _channels.count(DEFAULT_CHANNEL_NAME) ? 1 : 0;
		return _channels.size() > static_cast<std::size_t>(default_channel_count);
	}

	/*
	 * Remove a channel from the domain by name.
	 *
	 * This operation is only provided to roll back configuration changes
	 * on failure (e.g., when the orchestrator fails to create the channel
	 * in the tracer after the config was already added). Orchestrators are
	 * not expected to support the removal of a channel.
	 */
	void remove_channel(const std::string& name)
	{
		const auto it = _channels.find(name);
		if (it == _channels.end()) {
			LTTNG_THROW_CHANNEL_NOT_FOUND_BY_NAME_ERROR(name);
		}

		_channels.erase(it);
	}

	/* Add a map channel to the domain by constructing it in place. */
	template <typename... Args>
	map_channel_configuration& add_map_channel(Args&&...args)
	{
		auto new_map_channel =
			lttng::make_unique<map_channel_configuration>(std::forward<Args>(args)...);

		_validate_map_channel(*new_map_channel);

		auto result =
			_map_channels.emplace(new_map_channel->name, std::move(new_map_channel));
		if (!result.second) {
			LTTNG_THROW_MAP_CHANNEL_ALREADY_EXISTS_ERROR(result.first->first);
		}

		return *(result.first->second);
	}

	/* Lookup by name. */
	map_channel_configuration& get_map_channel(lttng::c_string_view name)
	{
		const auto it = _map_channels.find(name.data());
		if (it == _map_channels.end()) {
			LTTNG_THROW_MAP_CHANNEL_NOT_FOUND_BY_NAME_ERROR(name);
		}

		return *(it->second);
	}

	const map_channel_configuration& get_map_channel(lttng::c_string_view name) const
	{
		/* NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) */
		return const_cast<domain *>(this)->get_map_channel(name);
	}

	using map_channels_view = lttng::utils::dereferenced_mapped_values_view<
		std::unordered_map<std::string, map_channel_configuration::uptr>,
		map_channel_configuration>;
	using const_map_channels_view = lttng::utils::dereferenced_mapped_values_view<
		const std::unordered_map<std::string, map_channel_configuration::uptr>,
		const map_channel_configuration>;

	map_channels_view map_channels() noexcept
	{
		return map_channels_view(_map_channels);
	}

	const_map_channels_view map_channels() const noexcept
	{
		return const_map_channels_view(_map_channels);
	}

	std::size_t map_channel_count() const noexcept
	{
		return _map_channels.size();
	}

	/*
	 * Generate a map channel name which isn't currently in use
	 * within this domain.
	 */
	std::string generate_map_channel_name() const
	{
		for (std::uint64_t candidate_index = 0;; ++candidate_index) {
			auto candidate_name = fmt::format("channel{}", candidate_index);

			if (_map_channels.count(candidate_name) == 0) {
				return candidate_name;
			}
		}
	}

	/* Intended for rollback on orchestrator failure. */
	void remove_map_channel(const std::string& name)
	{
		const auto it = _map_channels.find(name);
		if (it == _map_channels.end()) {
			LTTNG_THROW_MAP_CHANNEL_NOT_FOUND_BY_NAME_ERROR(name);
		}

		_map_channels.erase(it);
	}

	/*
	 * Process attribute tracker accessors.
	 *
	 * Kernel-only trackers (process_id, user_id, group_id) assert if accessed
	 * on a non-kernel domain.
	 *
	 * Virtual trackers (virtual_process_id, virtual_user_id, virtual_group_id)
	 * are available for both kernel and user space domains.
	 */

	/* Kernel-only: real process ID tracker. */
	process_id_tracker_t& process_id_tracker()
	{
		LTTNG_ASSERT(domain_class_ == lttng::domain_class::KERNEL_SPACE);
		return *_process_id_tracker;
	}

	const process_id_tracker_t& process_id_tracker() const
	{
		LTTNG_ASSERT(domain_class_ == lttng::domain_class::KERNEL_SPACE);
		return *_process_id_tracker;
	}

	/* Kernel and user space: virtual process ID tracker. */
	virtual_process_id_tracker_t& virtual_process_id_tracker()
	{
		LTTNG_ASSERT(_virtual_process_id_tracker);
		return *_virtual_process_id_tracker;
	}

	const virtual_process_id_tracker_t& virtual_process_id_tracker() const
	{
		LTTNG_ASSERT(_virtual_process_id_tracker);
		return *_virtual_process_id_tracker;
	}

	/* Kernel-only: real user ID tracker. */
	user_id_tracker_t& user_id_tracker()
	{
		LTTNG_ASSERT(domain_class_ == lttng::domain_class::KERNEL_SPACE);
		return *_user_id_tracker;
	}

	const user_id_tracker_t& user_id_tracker() const
	{
		LTTNG_ASSERT(domain_class_ == lttng::domain_class::KERNEL_SPACE);
		return *_user_id_tracker;
	}

	/* Kernel and user space: virtual user ID tracker. */
	virtual_user_id_tracker_t& virtual_user_id_tracker()
	{
		LTTNG_ASSERT(_virtual_user_id_tracker);
		return *_virtual_user_id_tracker;
	}

	const virtual_user_id_tracker_t& virtual_user_id_tracker() const
	{
		LTTNG_ASSERT(_virtual_user_id_tracker);
		return *_virtual_user_id_tracker;
	}

	/* Kernel-only: real group ID tracker. */
	group_id_tracker_t& group_id_tracker()
	{
		LTTNG_ASSERT(domain_class_ == lttng::domain_class::KERNEL_SPACE);
		return *_group_id_tracker;
	}

	const group_id_tracker_t& group_id_tracker() const
	{
		LTTNG_ASSERT(domain_class_ == lttng::domain_class::KERNEL_SPACE);
		return *_group_id_tracker;
	}

	/* Kernel and user space: virtual group ID tracker. */
	virtual_group_id_tracker_t& virtual_group_id_tracker()
	{
		LTTNG_ASSERT(_virtual_group_id_tracker);
		return *_virtual_group_id_tracker;
	}

	const virtual_group_id_tracker_t& virtual_group_id_tracker() const
	{
		LTTNG_ASSERT(_virtual_group_id_tracker);
		return *_virtual_group_id_tracker;
	}

	const lttng::domain_class domain_class_;

private:
	/*
	 * Validate that a channel configuration is compatible with this domain.
	 *
	 * Kernel domain constraints:
	 * - No blocking timeout: the kernel tracer does not support consumption
	 *   blocking.
	 * - Per-CPU allocation only: the kernel tracer uses one ring buffer per
	 *   CPU; per-channel allocation is not supported.
	 */
	void _validate_channel(const recording_channel_configuration& channel) const
	{
		if (domain_class_ != lttng::domain_class::KERNEL_SPACE) {
			return;
		}

		if (channel.consumption_blocking_policy_.mode_ !=
		    recording_channel_configuration::consumption_blocking_policy::mode::NONE) {
			LTTNG_THROW_INVALID_ARGUMENT_ERROR(lttng::format(
				"Blocking timeout is not supported by the kernel tracer: channel_name=`{}`",
				channel.name));
		}

		if (channel.buffer_allocation_policy !=
		    recording_channel_configuration::buffer_allocation_policy_t::PER_CPU) {
			LTTNG_THROW_INVALID_ARGUMENT_ERROR(lttng::format(
				"Only per-CPU buffer allocation is supported by the kernel tracer: channel_name=`{}`",
				channel.name));
		}
	}

	/*
	 * The kernel only supports SYSTEM ownership; user space only PER_PID or
	 * PER_UID. Reject the model that doesn't belong to this domain.
	 */
	void _validate_map_channel(const map_channel_configuration& map_channel) const
	{
		if (domain_class_ == lttng::domain_class::KERNEL_SPACE) {
			if (map_channel.buffer_ownership != ownership_model_t::SYSTEM) {
				LTTNG_THROW_INVALID_ARGUMENT_ERROR(lttng::format(
					"The kernel tracer only supports the system buffer ownership model: map_channel_name=`{}`, buffer_ownership={}",
					map_channel.name,
					map_channel.buffer_ownership));
			}
		} else if (map_channel.buffer_ownership == ownership_model_t::SYSTEM) {
			LTTNG_THROW_INVALID_ARGUMENT_ERROR(lttng::format(
				"The system buffer ownership model is only supported by the kernel tracer: map_channel_name=`{}`",
				map_channel.name));
		}
	}

	static metadata_channel_configuration _make_default_metadata_channel_configuration();

	const metadata_channel_configuration _metadata_channel;
	std::unordered_map<std::string, recording_channel_configuration::uptr> _channels;
	std::unordered_map<std::string, map_channel_configuration::uptr> _map_channels;

	/* Process attribute trackers (populated based on domain_class_). */
	nonstd::optional<process_id_tracker_t> _process_id_tracker;
	nonstd::optional<virtual_process_id_tracker_t> _virtual_process_id_tracker;
	nonstd::optional<user_id_tracker_t> _user_id_tracker;
	nonstd::optional<virtual_user_id_tracker_t> _virtual_user_id_tracker;
	nonstd::optional<group_id_tracker_t> _group_id_tracker;
	nonstd::optional<virtual_group_id_tracker_t> _virtual_group_id_tracker;
};
} /* namespace config */
} /* namespace sessiond */
} /* namespace lttng */

#endif /* LTTNG_SESSIOND_DOMAIN_HPP */
