/*
 * Copyright 2025 Vsevolod Stakhov
 *
 * Licensed 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.
 */
#include "config.h"
#include "logger.h"
#include "expression.h"
#include "task.h"
#include "utlist.h"
#include "scan_result.h"
#include "composites.h"
#include "contrib/libev/ev.h"
#include "libutil/util.h"

#include <cmath>
#include <vector>
#include <variant>
#include "libutil/cxx/util.hxx"
#include "contrib/ankerl/unordered_dense.h"

#include "composites_internal.hxx"

#define msg_err_composites(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL,                   \
															"composites", task->task_pool->tag.uid, \
															RSPAMD_LOG_FUNC,                        \
															__VA_ARGS__)
#define msg_warn_composites(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING,                    \
															 "composites", task->task_pool->tag.uid, \
															 RSPAMD_LOG_FUNC,                        \
															 __VA_ARGS__)
#define msg_info_composites(...) rspamd_default_log_function(G_LOG_LEVEL_INFO,                       \
															 "composites", task->task_pool->tag.uid, \
															 RSPAMD_LOG_FUNC,                        \
															 __VA_ARGS__)

#define msg_debug_composites(...) rspamd_conditional_debug_fast(NULL, task->from_addr,                                            \
																rspamd_composites_log_id, "composites", task->task_pool->tag.uid, \
																RSPAMD_LOG_FUNC,                                                  \
																__VA_ARGS__)

INIT_LOG_MODULE(composites)


namespace rspamd::composites {
static rspamd_expression_atom_t *rspamd_composite_expr_parse(const char *line, gsize len,
															 rspamd_mempool_t *pool,
															 gpointer ud, GError **err);
static double rspamd_composite_expr_process(void *ud, rspamd_expression_atom_t *atom);
static int rspamd_composite_expr_priority(rspamd_expression_atom_t *atom);
static void rspamd_composite_expr_destroy(rspamd_expression_atom_t *atom);
static void composites_foreach_callback(gpointer key, gpointer value, void *data);

const struct rspamd_atom_subr composite_expr_subr = {
	.parse = rspamd::composites::rspamd_composite_expr_parse,
	.process = rspamd::composites::rspamd_composite_expr_process,
	.priority = rspamd::composites::rspamd_composite_expr_priority,
	.destroy = rspamd::composites::rspamd_composite_expr_destroy};
}// namespace rspamd::composites

namespace rspamd::composites {

static constexpr const double epsilon = 0.00001;

struct symbol_remove_data {
	const char *sym;
	struct rspamd_composite *comp;
	GNode *parent;
	std::uint8_t action;
};

struct composites_data {
	struct rspamd_task *task;
	struct rspamd_composite *composite;
	struct rspamd_scan_result *metric_res;
	ankerl::unordered_dense::map<std::string_view,
								 std::vector<symbol_remove_data>>
		symbols_to_remove;
	std::vector<bool> checked;
	/*
	 * Pinned snapshot of the composites generation that was current when
	 * this task started evaluating composites. All evaluation reads happen
	 * through this pointer, so a concurrent dynamic-map reload cannot
	 * change the world under our feet mid-task.
	 *
	 * Composite ids are globally monotonic across reloads; sizing `checked`
	 * by gen->all_composites.size() means we have a slot for every
	 * composite in this snapshot.  Composites not in this generation are
	 * never evaluated, so their ids never index `checked`.
	 */
	std::shared_ptr<composites_generation> gen;
	bool is_second_pass;      /**< true if we're in COMPOSITES_POST stage */
	uint64_t matched_count{}; /**< number of matched composites */

	explicit composites_data(struct rspamd_task *task, struct rspamd_scan_result *mres)
		: task(task), composite(nullptr), metric_res(mres),
		  gen(COMPOSITE_MANAGER_FROM_PTR(task->cfg->composites_manager)->snapshot_generation()),
		  matched_count(0)
	{
		/*
		 * Size `checked` by the largest composite id this generation could
		 * possibly produce + 1 (ids are 0-based and monotonic across
		 * reloads). For an empty generation we allocate a 2-slot dummy
		 * vector so indexing never overflows even if a stale id sneaks in.
		 */
		int max_id = -1;
		for (const auto &c: gen->all_composites) {
			if (c->id > max_id) {
				max_id = c->id;
			}
		}
		checked.resize((max_id + 1) * 2, false);
		/* Determine if we're in second pass by checking if POST_FILTERS stage has been processed */
		is_second_pass = (task->processed_stages & RSPAMD_TASK_STAGE_POST_FILTERS) != 0;
	}
};

struct rspamd_composite_option_match {
	rspamd_regexp_t *re;
	std::string match;

	explicit rspamd_composite_option_match(const char *start, std::size_t len) noexcept
		: re(nullptr), match(start, len)
	{
	}

	explicit rspamd_composite_option_match(rspamd_regexp_t *re) noexcept
		: re(rspamd_regexp_ref(re))
	{
	}

	rspamd_composite_option_match(const rspamd_composite_option_match &other) noexcept
	{
		if (other.re) {
			re = rspamd_regexp_ref(other.re);
		}
		else {
			match = other.match;
			re = nullptr;
		}
	}
	rspamd_composite_option_match &operator=(const rspamd_composite_option_match &other) noexcept
	{
		if (other.re) {
			if (re) {
				rspamd_regexp_unref(re);
			}
			re = rspamd_regexp_ref(other.re);
		}
		else {
			if (re) {
				rspamd_regexp_unref(re);
			}
			re = nullptr;
			match = other.match;
		}

		return *this;
	}

	rspamd_composite_option_match(rspamd_composite_option_match &&other) noexcept
	{
		if (other.re) {
			re = other.re;
			other.re = nullptr;
		}
		else {
			re = nullptr;
			match = std::move(other.match);
		}
	}
	rspamd_composite_option_match &operator=(rspamd_composite_option_match &&other) noexcept
	{
		if (other.re) {
			if (re) {
				rspamd_regexp_unref(re);
			}
			re = other.re;
			other.re = nullptr;
		}
		else {
			if (re) {
				rspamd_regexp_unref(re);
			}
			re = nullptr;
			match = std::move(other.match);
		}

		return *this;
	}

	~rspamd_composite_option_match()
	{
		if (re) {
			rspamd_regexp_unref(re);
		}
	}

	auto match_opt(const std::string_view &data) const -> bool
	{
		if (re) {
			return rspamd_regexp_search(re,
										data.data(), data.size(),
										nullptr, nullptr, false, nullptr);
		}
		else {
			return data == match;
		}
	}

	auto get_pat() const -> std::string_view
	{
		if (re) {
			return std::string_view(rspamd_regexp_get_pattern(re));
		}
		else {
			return match;
		}
	}
};

struct rspamd_composite_atom {
	std::string symbol;
	std::string_view norm_symbol;
	std::vector<rspamd_composite_option_match> opts;
};

enum rspamd_composite_action : std::uint8_t {
	RSPAMD_COMPOSITE_UNTOUCH = 0,
	RSPAMD_COMPOSITE_REMOVE_SYMBOL = (1u << 0),
	RSPAMD_COMPOSITE_REMOVE_WEIGHT = (1u << 1),
	RSPAMD_COMPOSITE_REMOVE_FORCED = (1u << 2)
};

static GQuark
rspamd_composites_quark(void)
{
	return g_quark_from_static_string("composites");
}

static auto
rspamd_composite_atom_dtor(void *ptr)
{
	auto *atom = reinterpret_cast<rspamd_composite_atom *>(ptr);

	delete atom;
}

static rspamd_expression_atom_t *
rspamd_composite_expr_parse(const char *line, gsize len,
							rspamd_mempool_t *pool,
							gpointer ud, GError **err)
{
	gsize clen = 0;
	const char *p, *end;
	enum composite_expr_state {
		comp_state_read_symbol = 0,
		comp_state_read_obrace,
		comp_state_read_option,
		comp_state_read_regexp,
		comp_state_read_regexp_end,
		comp_state_read_comma,
		comp_state_read_ebrace,
		comp_state_read_end
	} state = comp_state_read_symbol;

	end = line + len;
	p = line;

	/* Find length of the atom using a reduced state machine */
	while (p < end) {
		if (state == comp_state_read_end) {
			break;
		}

		switch (state) {
		case comp_state_read_symbol:
			clen = rspamd_memcspn(p, len, "[; \t()><!|&\n", 12);
			p += clen;

			if (*p == '[') {
				state = comp_state_read_obrace;
			}
			else {
				state = comp_state_read_end;
			}
			break;
		case comp_state_read_obrace:
			p++;

			if (*p == '/') {
				p++;
				state = comp_state_read_regexp;
			}
			else {
				state = comp_state_read_option;
			}
			break;
		case comp_state_read_regexp:
			if (*p == '\\' && p + 1 < end) {
				/* Escaping */
				p++;
			}
			else if (*p == '/') {
				/* End of regexp, possible flags */
				state = comp_state_read_regexp_end;
			}
			p++;
			break;
		case comp_state_read_option:
		case comp_state_read_regexp_end:
			if (*p == ',') {
				p++;
				state = comp_state_read_comma;
			}
			else if (*p == ']') {
				state = comp_state_read_ebrace;
			}
			else {
				p++;
			}
			break;
		case comp_state_read_comma:
			if (!g_ascii_isspace(*p)) {
				if (*p == '/') {
					state = comp_state_read_regexp;
				}
				else if (*p == ']') {
					state = comp_state_read_ebrace;
				}
				else {
					state = comp_state_read_option;
				}
			}
			else {
				/* Skip spaces after comma */
				p++;
			}
			break;
		case comp_state_read_ebrace:
			p++;
			state = comp_state_read_end;
			break;
		case comp_state_read_end:
			g_assert_not_reached();
		}
	}

	if (state != comp_state_read_end) {
		g_set_error(err, rspamd_composites_quark(), 100, "invalid composite: %s;"
														 "parser stopped in state %d",
					line, state);
		return NULL;
	}

	clen = p - line;
	p = line;
	state = comp_state_read_symbol;

	auto *atom = new rspamd_composite_atom;
	auto *res = rspamd_mempool_alloc0_type(pool, rspamd_expression_atom_t);
	res->len = clen;
	res->str = line;

	/* Full state machine to fill a composite atom */
	const char *opt_start = nullptr;

	while (p < end) {
		if (state == comp_state_read_end) {
			break;
		}

		switch (state) {
		case comp_state_read_symbol: {
			clen = rspamd_memcspn(p, len, "[; \t()><!|&\n", 12);
			p += clen;

			if (*p == '[') {
				state = comp_state_read_obrace;
			}
			else {
				state = comp_state_read_end;
			}

			atom->symbol = std::string{line, clen};
			auto norm_start = std::find_if(atom->symbol.begin(), atom->symbol.end(),
										   [](char c) { return g_ascii_isalnum(c); });
			if (norm_start == atom->symbol.end()) {
				msg_err_pool("invalid composite atom: %s", atom->symbol.c_str());
			}
			atom->norm_symbol = make_string_view_from_it(norm_start, atom->symbol.end());
			break;
		}
		case comp_state_read_obrace:
			p++;

			if (*p == '/') {
				opt_start = p;
				p++; /* Starting slash */
				state = comp_state_read_regexp;
			}
			else {
				state = comp_state_read_option;
				opt_start = p;
			}

			break;
		case comp_state_read_regexp:
			if (*p == '\\' && p + 1 < end) {
				/* Escaping */
				p++;
			}
			else if (*p == '/') {
				/* End of regexp, possible flags */
				state = comp_state_read_regexp_end;
			}
			p++;
			break;
		case comp_state_read_option:
			if (*p == ',' || *p == ']') {
				/* Plain match, copy option to ensure string_view validity */
				int opt_len = p - opt_start;
				auto *opt_buf = rspamd_mempool_alloc_buffer(pool, opt_len + 1);
				rspamd_strlcpy(opt_buf, opt_start, opt_len + 1);
				opt_buf = g_strstrip(opt_buf);
				atom->opts.emplace_back(opt_buf, strlen(opt_buf));

				if (*p == ',') {
					p++;
					state = comp_state_read_comma;
				}
				else {
					state = comp_state_read_ebrace;
				}
			}
			else {
				p++;
			}
			break;
		case comp_state_read_regexp_end:
			if (*p == ',' || *p == ']') {
				auto opt_len = p - opt_start;
				rspamd_regexp_t *re;
				GError *re_err = nullptr;

				re = rspamd_regexp_new_len(opt_start, opt_len, nullptr, &re_err);

				if (re == nullptr) {
					msg_err_pool("cannot create regexp from string %*s: %e",
								 (int) opt_len, opt_start, re_err);

					g_error_free(re_err);
				}
				else {
					atom->opts.emplace_back(re);
					rspamd_regexp_unref(re);
				}

				if (*p == ',') {
					p++;
					state = comp_state_read_comma;
				}
				else {
					state = comp_state_read_ebrace;
				}
			}
			else {
				p++;
			}
			break;
		case comp_state_read_comma:
			if (!g_ascii_isspace(*p)) {
				if (*p == '/') {
					state = comp_state_read_regexp;
					opt_start = p;
				}
				else if (*p == ']') {
					state = comp_state_read_ebrace;
				}
				else {
					opt_start = p;
					state = comp_state_read_option;
				}
			}
			else {
				/* Skip spaces after comma */
				p++;
			}
			break;
		case comp_state_read_ebrace:
			p++;
			state = comp_state_read_end;
			break;
		case comp_state_read_end:
			g_assert_not_reached();
		}
	}

	res->data = atom;

	return res;
}

static auto
process_symbol_removal(rspamd_expression_atom_t *atom,
					   struct composites_data *cd,
					   struct rspamd_symbol_result *ms,
					   const std::string &beg) -> void
{
	struct rspamd_task *task = cd->task;

	if (ms == nullptr) {
		return;
	}

	/*
	 * At this point we know that we need to do something about this symbol,
	 * however, we don't know whether we need to delete it unfortunately,
	 * that depends on the later decisions when the complete expression is
	 * evaluated.
	 */
	auto rd_it = cd->symbols_to_remove.find(ms->name);

	auto fill_removal_structure = [&](symbol_remove_data &nrd) {
		nrd.sym = ms->name;

		/* By default remove symbols */
		switch (cd->composite->policy) {
		case rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_REMOVE_ALL:
		default:
			nrd.action = (RSPAMD_COMPOSITE_REMOVE_SYMBOL | RSPAMD_COMPOSITE_REMOVE_WEIGHT);
			break;
		case rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_REMOVE_SYMBOL:
			nrd.action = RSPAMD_COMPOSITE_REMOVE_SYMBOL;
			break;
		case rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_REMOVE_WEIGHT:
			nrd.action = RSPAMD_COMPOSITE_REMOVE_WEIGHT;
			break;
		case rspamd_composite_policy::RSPAMD_COMPOSITE_POLICY_LEAVE:
			nrd.action = 0;
			break;
		}

		for (auto t: beg) {
			if (t == '~') {
				nrd.action &= ~RSPAMD_COMPOSITE_REMOVE_SYMBOL;
			}
			else if (t == '-') {
				nrd.action &= ~(RSPAMD_COMPOSITE_REMOVE_WEIGHT |
								RSPAMD_COMPOSITE_REMOVE_SYMBOL);
			}
			else if (t == '^') {
				nrd.action |= RSPAMD_COMPOSITE_REMOVE_FORCED;
			}
			else {
				break;
			}
		}

		nrd.comp = cd->composite;
		nrd.parent = atom->parent;
	};

	if (rd_it != cd->symbols_to_remove.end()) {
		fill_removal_structure(rd_it->second.emplace_back());
		msg_debug_composites("%s: added symbol %s to removal: %d policy, from composite %s",
							 cd->metric_res->name,
							 ms->name, rd_it->second.back().action,
							 cd->composite->sym.c_str());
	}
	else {
		std::vector<symbol_remove_data> nrd;
		fill_removal_structure(nrd.emplace_back());
		msg_debug_composites("%s: added symbol %s to removal: %d policy, from composite %s",
							 cd->metric_res->name,
							 ms->name, nrd.front().action,
							 cd->composite->sym.c_str());
		cd->symbols_to_remove[ms->name] = std::move(nrd);
	}
}

static auto
process_single_symbol(struct composites_data *cd,
					  std::string_view sym,
					  struct rspamd_symbol_result **pms,
					  struct rspamd_composite_atom *atom) -> double
{
	struct rspamd_symbol_result *ms = nullptr;
	double rc = 0;
	struct rspamd_task *task = cd->task;

	if ((ms = rspamd_task_find_symbol_result(cd->task, sym.data(), cd->metric_res)) == nullptr) {
		msg_debug_composites("not found symbol %s in composite %s", sym.data(),
							 cd->composite->sym.c_str());

		/*
		 * Resolve composite references against the task's pinned snapshot
		 * every time. We do not cache the resolution on the atom because
		 * the atom struct is shared across tasks that may be using
		 * different generations — caching would dangle on reload.
		 * Hashtable lookups are cheap and only happen for symbols that
		 * aren't already in the scan result.
		 */
		const struct rspamd_composite *ncomp = cd->gen->find(sym);

		if (ncomp != nullptr && !ncomp->disabled) {
			msg_debug_composites("symbol %s for composite %s is another composite",
								 sym.data(), cd->composite->sym.c_str());

			if (!cd->checked[ncomp->id * 2]) {
				msg_debug_composites("composite dependency %s for %s is not checked",
									 sym.data(), cd->composite->sym.c_str());
				/* Set checked for this symbol to avoid cyclic references */
				cd->checked[cd->composite->id * 2] = true;
				auto *saved = cd->composite; /* Save the current composite */
				composites_foreach_callback((gpointer) ncomp->sym.c_str(),
											(gpointer) ncomp, (gpointer) cd);
				/* Restore state */
				cd->composite = saved;
				cd->checked[cd->composite->id * 2] = false;

				ms = rspamd_task_find_symbol_result(cd->task, sym.data(),
													cd->metric_res);
			}
			else {
				/*
				 * XXX: in case of cyclic references this would return 0
				 */
				if (cd->checked[ncomp->id * 2 + 1]) {
					ms = rspamd_task_find_symbol_result(cd->task, sym.data(),
														cd->metric_res);
				}
			}
		}
	}

	if (ms) {
		msg_debug_composites("found symbol %s in composite %s, weight: %.3f",
							 sym.data(), cd->composite->sym.c_str(), ms->score);

		/* Now check options */
		for (const auto &cur_opt: atom->opts) {
			struct rspamd_symbol_option *opt;
			auto found = false;

			DL_FOREACH(ms->opts_head, opt)
			{
				if (cur_opt.match_opt({opt->option, opt->optlen})) {
					found = true;
					break;
				}
			}

			if (!found) {
				auto pat = cur_opt.get_pat();
				msg_debug_composites("symbol %s in composite %s misses required option %*s",
									 sym.data(),
									 cd->composite->sym.c_str(),
									 (int) pat.size(), pat.data());
				ms = nullptr;

				break;
			}
		}

		if (ms) {
			if (ms->score == 0) {
				rc = epsilon * 16.0; /* Distinguish from 0 */
			}
			else {
				rc = ms->score;
			}
		}
	}

	*pms = ms;
	return rc;
}

static auto
rspamd_composite_expr_process(void *ud, rspamd_expression_atom_t *atom) -> double
{
	struct composites_data *cd = (struct composites_data *) ud;
	struct rspamd_composite_atom *comp_atom = (struct rspamd_composite_atom *) atom->data;

	struct rspamd_symbol_result *ms = NULL;
	struct rspamd_task *task = cd->task;
	double rc = 0;

	if (cd->checked[cd->composite->id * 2]) {
		/* We have already checked this composite, so just return its value */
		if (cd->checked[cd->composite->id * 2 + 1]) {
			ms = rspamd_task_find_symbol_result(cd->task,
												comp_atom->norm_symbol.data(),
												cd->metric_res);
		}

		if (ms) {
			if (ms->score == 0) {
				rc = epsilon; /* Distinguish from 0 */
			}
			else {
				/* Treat negative and positive scores equally... */
				rc = fabs(ms->score);
			}
		}

		msg_debug_composites("composite %s is already checked, result: %.2f",
							 cd->composite->sym.c_str(), rc);

		return rc;
	}

	/* Note: sym is zero terminated as it is a view on std::string */
	auto sym = comp_atom->norm_symbol;
	auto group_process_functor = [&](auto cond, int sub_start) -> double {
		auto max = 0.;
		GHashTableIter it;
		gpointer k, v;
		struct rspamd_symbols_group *gr;

		gr = (struct rspamd_symbols_group *) g_hash_table_lookup(cd->task->cfg->groups,
																 sym.substr(sub_start).data());

		if (gr != nullptr) {
			g_hash_table_iter_init(&it, gr->symbols);

			while (g_hash_table_iter_next(&it, &k, &v)) {
				auto *sdef = (rspamd_symbol *) v;

				if (cond(sdef->score)) {
					rc = process_single_symbol(cd,
											   std::string_view(sdef->name),
											   &ms,
											   comp_atom);

					if (fabs(rc) > epsilon) {
						process_symbol_removal(atom,
											   cd,
											   ms,
											   comp_atom->symbol);

						if (fabs(rc) > max) {
							max = fabs(rc);
						}
					}
				}
			}
		}

		return max;
	};

	if (sym.size() > 2) {
		if (sym.substr(0, 2) == "g:") {
			rc = group_process_functor([](auto _) { return true; }, 2);
		}
		else if (sym.substr(0, 3) == "g+:") {
			/* Group, positive symbols only */
			rc = group_process_functor([](auto sc) { return sc > 0.; }, 3);
		}
		else if (sym.substr(0, 3) == "g-:") {
			rc = group_process_functor([](auto sc) { return sc < 0.; }, 3);
		}
		else {
			rc = process_single_symbol(cd, sym, &ms, comp_atom);

			if (fabs(rc) > epsilon) {
				process_symbol_removal(atom,
									   cd,
									   ms,
									   comp_atom->symbol);
			}
		}
	}
	else {
		rc = process_single_symbol(cd, sym, &ms, comp_atom);

		if (fabs(rc) > epsilon) {
			process_symbol_removal(atom,
								   cd,
								   ms,
								   comp_atom->symbol);
		}
	}

	msg_debug_composites("%s: result for atom %s in composite %s is %.4f",
						 cd->metric_res->name,
						 comp_atom->norm_symbol.data(),
						 cd->composite->sym.c_str(), rc);

	return rc;
}

/*
 * We don't have preferences for composites
 */
static int
rspamd_composite_expr_priority(rspamd_expression_atom_t *atom)
{
	return 0;
}

static void
rspamd_composite_expr_destroy(rspamd_expression_atom_t *atom)
{
	rspamd_composite_atom_dtor(atom->data);
}

static void
composites_foreach_callback(gpointer key, gpointer value, void *data)
{
	auto *cd = (struct composites_data *) data;
	auto *comp = (struct rspamd_composite *) value;
	auto *str_key = (const char *) key;
	struct rspamd_task *task;
	double rc;

	cd->composite = comp;
	task = cd->task;

	/* Skip composites that don't belong to current pass */
	if (cd->is_second_pass != comp->second_pass) {
		msg_debug_composites("skip composite %s (pass mismatch: current=%s, composite=%s)",
							 str_key,
							 cd->is_second_pass ? "second" : "first",
							 comp->second_pass ? "second" : "first");
		return;
	}

	msg_debug_composites("process composite %s", str_key);

	if (!cd->checked[cd->composite->id * 2]) {
		/* For second-pass composites during second pass, skip symcache check since they were
		 * already marked as checked during first pass but not actually evaluated */
		bool skip_symcache_check = (cd->is_second_pass && comp->second_pass);

		if (!skip_symcache_check &&
			rspamd_symcache_is_checked(cd->task, cd->task->cfg->cache, str_key)) {
			msg_debug_composites("composite %s is checked in symcache but not "
								 "in composites bitfield",
								 cd->composite->sym.c_str());
			cd->checked[comp->id * 2] = true;
			cd->checked[comp->id * 2 + 1] = false;
		}
		else {
			if (rspamd_task_find_symbol_result(cd->task, str_key,
											   cd->metric_res) != nullptr) {
				/* Already set, no need to check */
				msg_debug_composites("composite %s is already in metric "
									 "in composites bitfield",
									 cd->composite->sym.c_str());
				cd->checked[comp->id * 2] = true;
				cd->checked[comp->id * 2 + 1] = true;

				return;
			}

			msg_debug_composites("%s: start processing composite %s",
								 cd->metric_res->name,
								 cd->composite->sym.c_str());

			rc = rspamd_process_expression(comp->expr, RSPAMD_EXPRESSION_FLAG_NOOPT,
										   cd);

			/* Checked bit */
			cd->checked[comp->id * 2] = true;

			msg_debug_composites("%s: final result for composite %s is %.4f",
								 cd->metric_res->name,
								 cd->composite->sym.c_str(), rc);

			/* Result bit */
			if (fabs(rc) > epsilon) {
				cd->checked[comp->id * 2 + 1] = true;
				cd->matched_count++;
				rspamd_task_insert_result_full(cd->task, str_key, 1.0, NULL,
											   RSPAMD_SYMBOL_INSERT_SINGLE, cd->metric_res);
			}
			else {
				cd->checked[comp->id * 2 + 1] = false;
			}
		}
	}
}


static auto
remove_symbols(const composites_data &cd, const std::vector<symbol_remove_data> &rd) -> void
{
	struct rspamd_task *task = cd.task;
	gboolean skip = FALSE,
			 has_valid_op = FALSE,
			 want_remove_score = TRUE,
			 want_remove_symbol = TRUE,
			 want_forced = FALSE;
	const char *disable_score_reason = "no policy",
			   *disable_symbol_reason = "no policy";

	task = cd.task;

	for (const auto &cur: rd) {
		if (!cd.checked[cur.comp->id * 2 + 1]) {
			continue;
		}
		/*
		 * First of all exclude all elements with any parent that is negation:
		 * !A || B -> here we can have both !A and B matched, but we do *NOT*
		 * want to remove symbol in that case
		 */
		auto *par = cur.parent;
		skip = FALSE;

		while (par) {
			if (rspamd_expression_node_is_op(par, OP_NOT)) {
				skip = TRUE;
				break;
			}

			par = par->parent;
		}

		if (skip) {
			continue;
		}

		has_valid_op = TRUE;
		/*
		 * Now we can try to remove symbols/scores
		 *
		 * We apply the following logic here:
		 * - if no composites would like to save score then we remove score
		 * - if no composites would like to save symbol then we remove symbol
		 */
		if (!want_forced) {
			if (!(cur.action & RSPAMD_COMPOSITE_REMOVE_SYMBOL)) {
				want_remove_symbol = FALSE;
				disable_symbol_reason = cur.comp->sym.c_str();
			}

			if (!(cur.action & RSPAMD_COMPOSITE_REMOVE_WEIGHT)) {
				want_remove_score = FALSE;
				disable_score_reason = cur.comp->sym.c_str();
			}

			if (cur.action & RSPAMD_COMPOSITE_REMOVE_FORCED) {
				want_forced = TRUE;
				disable_symbol_reason = cur.comp->sym.c_str();
				disable_score_reason = cur.comp->sym.c_str();
			}
		}
	}

	auto *ms = rspamd_task_find_symbol_result(task, rd.front().sym, cd.metric_res);

	if (has_valid_op && ms && !(ms->flags & RSPAMD_SYMBOL_RESULT_IGNORED)) {

		if (want_remove_score || want_forced) {
			msg_debug_composites("%s: %s remove symbol weight for %s (was %.2f), "
								 "score removal affected by %s, symbol removal affected by %s",
								 cd.metric_res->name,
								 (want_forced ? "forced" : "normal"), rd.front().sym, ms->score,
								 disable_score_reason, disable_symbol_reason);
			cd.metric_res->score -= ms->score;
			ms->score = 0.0;
		}

		if (want_remove_symbol || want_forced) {
			ms->flags |= RSPAMD_SYMBOL_RESULT_IGNORED;
			msg_debug_composites("%s: %s remove symbol %s (score %.2f), "
								 "score removal affected by %s, symbol removal affected by %s",
								 cd.metric_res->name,
								 (want_forced ? "forced" : "normal"), rd.front().sym, ms->score,
								 disable_score_reason, disable_symbol_reason);
		}
	}
}

/* Sampling rate for timing measurements: 1 in 256 tasks */
constexpr uint64_t COMPOSITES_SAMPLING_MASK = 0xFF;

static void
composites_metric_callback(struct rspamd_task *task)
{
	std::vector<composites_data> comp_data_vec;
	struct rspamd_scan_result *mres;
	auto *cm = COMPOSITE_MANAGER_FROM_PTR(task->cfg->composites_manager);
	bool is_second_pass = (task->processed_stages & RSPAMD_TASK_STAGE_POST_FILTERS) != 0;
	bool use_fast_path = cm->use_inverted_index && !is_second_pass;

	/* Probabilistic sampling for timing measurements (unless always_sample is set) */
	bool do_sample = task->cfg->composites_stats_always ||
					 (rspamd_random_uint64_fast() & COMPOSITES_SAMPLING_MASK) == 0;
	ev_tstamp start_time = 0.0;

	if (do_sample && task->event_loop) {
		ev_now_update_if_cheap(task->event_loop);
		start_time = ev_now(task->event_loop);
	}

	uint64_t composites_checked = 0;

	comp_data_vec.reserve(1);

	DL_FOREACH(task->result, mres)
	{
		auto &cd = comp_data_vec.emplace_back(task, mres);

		/*
		 * All evaluation reads go through cd.gen — the snapshot we pinned
		 * when this composites_data was constructed. cm->use_inverted_index
		 * is read off the manager (it is config-time only and never
		 * changes after init).
		 */
		auto *gen = cd.gen.get();

		if (is_second_pass) {
			/* Second pass: process only second-pass composites from the snapshot */
			msg_debug_composites("processing second-pass composites (gen %L)",
								 (int64_t) gen->generation_id);
			for (auto *comp: gen->second_pass_composites) {
				composites_foreach_callback((gpointer) comp->sym.c_str(),
											(gpointer) comp,
											&cd);
			}
			composites_checked += gen->second_pass_composites.size();
		}
		else if (use_fast_path) {
			/* First pass with inverted index: fast lookup */
			ankerl::unordered_dense::set<rspamd_composite *> potentially_active;

			/* Callback data for collecting potentially active composites */
			struct collect_active_cbdata {
				composites_generation *gen;
				ankerl::unordered_dense::set<rspamd_composite *> *active;
			} collect_data{gen, &potentially_active};

			/* Collect composites that have at least one positive atom present */
			rspamd_task_symbol_result_foreach(task, mres, [](gpointer key, gpointer value, gpointer ud) {
												  auto *cbd = reinterpret_cast<collect_active_cbdata *>(ud);
												  std::string_view sym_name{reinterpret_cast<const char *>(key)};

												  auto it = cbd->gen->symbol_to_composites.find(sym_name);
												  if (it != cbd->gen->symbol_to_composites.end()) {
													  for (auto *comp: it->second) {
														  /* Only add first-pass composites */
														  if (!comp->second_pass) {
															  cbd->active->insert(comp);
														  }
													  }
												  } }, &collect_data);

			/* Always add NOT-only composites (they have no positive atoms) */
			for (auto *comp: gen->not_only_composites) {
				if (!comp->second_pass) {
					potentially_active.insert(comp);
				}
			}

			msg_debug_composites("processing %d potentially active composites (from %d first-pass, gen %L)",
								 (int) potentially_active.size(),
								 (int) gen->first_pass_composites.size(),
								 (int64_t) gen->generation_id);

			/* Process only potentially active composites */
			for (auto *comp: potentially_active) {
				composites_foreach_callback((gpointer) comp->sym.c_str(),
											(gpointer) comp,
											&cd);
			}
			composites_checked += potentially_active.size();
		}
		else {
			/* Slow path: check all first-pass composites */
			msg_debug_composites("processing all %d first-pass composites (slow path, gen %L)",
								 (int) gen->first_pass_composites.size(),
								 (int64_t) gen->generation_id);
			for (auto *comp: gen->first_pass_composites) {
				composites_foreach_callback((gpointer) comp->sym.c_str(),
											(gpointer) comp,
											&cd);
			}
			composites_checked += gen->first_pass_composites.size();
		}
	}

	/* Update statistics */
	uint64_t total_matched = 0;
	for (const auto &cd: comp_data_vec) {
		total_matched += cd.matched_count;
	}
	cm->stats.matched += total_matched;

	if (use_fast_path) {
		cm->stats.checked_fast += composites_checked;
	}
	else if (!is_second_pass) {
		cm->stats.checked_slow += composites_checked;
	}

	/* Record timing with EMA (probabilistic sampling unless always_sample is set) */
	if (do_sample && task->event_loop) {
		ev_now_update_if_cheap(task->event_loop);
		ev_tstamp elapsed_ms = (ev_now(task->event_loop) - start_time) * 1000.0;

		if (use_fast_path) {
			rspamd_set_counter_ema(&cm->stats.time_fast, elapsed_ms, 0.5);
		}
		else if (!is_second_pass) {
			rspamd_set_counter_ema(&cm->stats.time_slow, elapsed_ms, 0.5);
		}
	}

	for (const auto &cd: comp_data_vec) {
		/* Remove symbols that are in composites */
		for (const auto &srd_it: cd.symbols_to_remove) {
			remove_symbols(cd, srd_it.second);
		}
	}
}

}// namespace rspamd::composites


void rspamd_composites_process_task(struct rspamd_task *task)
{
	if (task->result && !RSPAMD_TASK_IS_SKIPPED(task)) {
		rspamd::composites::composites_metric_callback(task);
	}
}
