/*
 * Copyright 2024 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.
 */

#ifndef UPSTREAM_H
#define UPSTREAM_H

#include "config.h"
#include "util.h"
#include "rdns.h"
#include "ucl.h"

#ifdef __cplusplus
extern "C" {
#endif

/* Forward declaration */
struct ev_loop;

enum rspamd_upstream_rotation {
	RSPAMD_UPSTREAM_RANDOM = 0,
	RSPAMD_UPSTREAM_HASHED,
	RSPAMD_UPSTREAM_ROUND_ROBIN,
	RSPAMD_UPSTREAM_MASTER_SLAVE,
	RSPAMD_UPSTREAM_SEQUENTIAL,
	RSPAMD_UPSTREAM_TOKEN_BUCKET, /* Token bucket weighted balancing */
	/*
	 * Power of Two Choices: pick two alive upstreams at random, choose the
	 * one with lower load (inflight + recent errors). Provably within a
	 * constant factor of optimal max-load. RANDOM callers are silently
	 * upgraded to P2C since it strictly dominates uniform random.
	 */
	RSPAMD_UPSTREAM_P2C,
	RSPAMD_UPSTREAM_UNDEF
};

enum rspamd_upstream_flag {
	RSPAMD_UPSTREAM_FLAG_NORESOLVE = (1 << 0),
	/*
	 * Upstream is an SRV "parent": a placeholder that owns SRV-derived
	 * member upstreams. Never put into the alive list, never selected
	 * directly; its only job is to host the lazy SRV re-resolve timer
	 * and a hash table of members keyed by "fqdn:port".
	 */
	RSPAMD_UPSTREAM_FLAG_SRV_RESOLVE = (1 << 1),
	RSPAMD_UPSTREAM_FLAG_DNS = (1 << 2),
	/*
	 * Upstream was created with a hostname that could not be resolved at
	 * config-parse time. It is kept out of the `alive` list until a later
	 * lazy/forced resolution succeeds, at which point the flag is cleared
	 * and the upstream becomes selectable.
	 */
	RSPAMD_UPSTREAM_FLAG_PENDING_RESOLVE = (1 << 3),
	/*
	 * SRV-derived member: a first-class upstream created by expanding a
	 * single SRV target into its own struct upstream. Holds its own error
	 * budget, latency EWMA, weight, addresses, and is selectable via the
	 * normal rotation algorithms. Lifetime is bound to the parent's SRV
	 * member table; on graceful drain (target gone from re-resolve), the
	 * member is removed from selection and released after its inflight
	 * refs drop.
	 */
	RSPAMD_UPSTREAM_FLAG_SRV_MEMBER = (1 << 4),
};

struct rspamd_config;
/* Opaque upstream structures */
struct upstream;
struct upstream_list;
struct upstream_ctx;

/**
 * Init upstreams library
 * @param resolver
 */
struct upstream_ctx *rspamd_upstreams_library_init(void);

/**
 * Remove reference from upstreams library
 */
void rspamd_upstreams_library_unref(struct upstream_ctx *ctx);

/**
 * Configure attributes of upstreams library
 * @param cfg
 */
void rspamd_upstreams_library_config(struct rspamd_config *cfg,
									 struct upstream_ctx *ctx, struct ev_loop *event_loop,
									 struct rdns_resolver *resolver);

/**
 * Upstream error logic
 * 1. During error time we count upstream_ok and upstream_fail
 * 2. If failcount is more then maxerrors then we mark upstream as unavailable for dead time
 * 3. After dead time we mark upstream as alive and go to the step 1
 * 4. If all upstreams are dead, marks every upstream as alive
 */

/**
 * Add an error to an upstream
 */
void rspamd_upstream_fail(struct upstream *upstream, gboolean addr_failure, const char *reason);

/**
 * Increase upstream successes count
 */
void rspamd_upstream_ok(struct upstream *up);

/**
 * Retire an upstream selection without affecting error counters or latency.
 * Use this when neither success nor failure semantics apply: message-copy
 * failures after a successful selection, fire-and-forget address lookups,
 * or hand-off paths where success/failure is signalled by a different
 * layer. Decrements the inflight counter so P2C load comparisons stay
 * accurate; otherwise abandoned selections would skew selection forever.
 */
void rspamd_upstream_release(struct upstream *up);

/**
 * Set weight for an upstream
 * @param up
 */
void rspamd_upstream_set_weight(struct upstream *up, unsigned int weight);

/**
 * Create new list of upstreams
 * @return
 */
struct upstream_list *rspamd_upstreams_create(struct upstream_ctx *ctx);

/**
 * Sets specific flag to the upstream list
 * @param ups
 * @param flags
 */
void rspamd_upstreams_set_flags(struct upstream_list *ups,
								enum rspamd_upstream_flag flags);

/**
 * Sets custom limits for upstreams
 * This function allocates memory from the upstreams ctx pool and should
 * not be called in cycles/constantly as this memory is likely persistent
 * @param ups
 * @param revive_time
 * @param revive_jitter
 * @param error_time
 * @param dns_timeout
 * @param max_errors
 * @param dns_retransmits
 */
void rspamd_upstreams_set_limits(struct upstream_list *ups,
								 double revive_time,
								 double revive_jitter,
								 double error_time,
								 double dns_timeout,
								 unsigned int max_errors,
								 unsigned int dns_retransmits);

/**
 * Sets rotation policy for upstreams list
 * @param ups
 * @param rot
 */
void rspamd_upstreams_set_rotation(struct upstream_list *ups,
								   enum rspamd_upstream_rotation rot);

/**
 * Destroy list of upstreams
 * @param ups
 */
void rspamd_upstreams_destroy(struct upstream_list *ups);

/**
 * Returns count of upstreams in a list. SRV parent placeholders are excluded
 * (they are not selectable themselves); use this when sizing the dispatchable
 * cluster (e.g. weight calculations, output table preallocation).
 * @param ups
 * @return
 */
gsize rspamd_upstreams_count(struct upstream_list *ups);

/**
 * Returns the total count of configured upstreams, including SRV parents that
 * have not yet been resolved into members. Use this for "is anything configured
 * at all" checks at config-load time, before async SRV resolution has run.
 * @param ups
 * @return
 */
gsize rspamd_upstreams_count_total(struct upstream_list *ups);

/**
 * Returns the number of upstreams in the list
 * @param ups
 * @return
 */
gsize rspamd_upstreams_alive(struct upstream_list *ups);

enum rspamd_upstream_parse_type {
	RSPAMD_UPSTREAM_PARSE_DEFAULT = 0,
	RSPAMD_UPSTREAM_PARSE_NAMESERVER,
};

/**
 * Add upstream from the string
 * @param ups upstream list
 * @param str string in format "name[:port[:priority]]"
 * @param def_port default port number
 * @param data optional userdata
 * @return TRUE if upstream has been added
 */
gboolean rspamd_upstreams_add_upstream(struct upstream_list *ups, const char *str,
									   uint16_t def_port, enum rspamd_upstream_parse_type parse_type,
									   void *data);

/**
 * Add multiple upstreams from comma, semicolon or space separated line
 * @param ups upstream list
 * @param str string in format "(<ups>([<sep>+]<ups>)*)+"
 * @param def_port default port number
 * @param data optional userdata
 * @return TRUE if **any** of upstreams has been added
 */
gboolean rspamd_upstreams_parse_line(struct upstream_list *ups,
									 const char *str, uint16_t def_port, void *data);


gboolean rspamd_upstreams_parse_line_len(struct upstream_list *ups,
										 const char *str, gsize len,
										 uint16_t def_port,
										 void *data);

/**
 * Parse upstreams list from the UCL object
 * @param ups
 * @param in
 * @param def_port
 * @param data
 * @return
 */
gboolean rspamd_upstreams_from_ucl(struct upstream_list *ups,
								   const ucl_object_t *in, uint16_t def_port, void *data);


typedef void (*rspamd_upstream_traverse_func)(struct upstream *up, unsigned int idx,
											  void *ud);

/**
 * Traverse upstreams list calling the function specified
 * @param ups
 * @param cb
 * @param ud
 */
void rspamd_upstreams_foreach(struct upstream_list *ups,
							  rspamd_upstream_traverse_func cb, void *ud);

enum rspamd_upstreams_watch_event {
	RSPAMD_UPSTREAM_WATCH_SUCCESS = 1u << 0,
	RSPAMD_UPSTREAM_WATCH_FAILURE = 1u << 1,
	RSPAMD_UPSTREAM_WATCH_OFFLINE = 1u << 2,
	RSPAMD_UPSTREAM_WATCH_ONLINE = 1u << 3,
	RSPAMD_UPSTREAM_WATCH_ALL = (1u << 0) | (1u << 1) | (1u << 2) | (1u << 3),
};

typedef void (*rspamd_upstream_watch_func)(struct upstream *up,
										   enum rspamd_upstreams_watch_event event,
										   unsigned int cur_errors,
										   void *ud);

/**
 * Adds new watcher to the upstreams list
 * @param ups
 * @param events
 * @param func
 * @param ud
 */
void rspamd_upstreams_add_watch_callback(struct upstream_list *ups,
										 enum rspamd_upstreams_watch_event events,
										 rspamd_upstream_watch_func func,
										 GFreeFunc free_func,
										 gpointer ud);

/**
 * Returns the next IP address of the upstream (internal rotation)
 * @param up
 * @return
 */
rspamd_inet_addr_t *rspamd_upstream_addr_next(struct upstream *up);

/**
 * Returns the current IP address of the upstream
 * @param up
 * @return
 */
rspamd_inet_addr_t *rspamd_upstream_addr_cur(const struct upstream *up);

/**
 * Add custom address for an upstream (ownership of addr is transferred to upstream)
 * @param up
 * @return
 */
gboolean rspamd_upstream_add_addr(struct upstream *up,
								  rspamd_inet_addr_t *addr);

/**
 * Returns the symbolic name of the upstream
 * @param up
 * @return
 */
const char *rspamd_upstream_name(struct upstream *up);

/**
 * Returns the port of the current address for the upstream
 * @param up
 * @return
 */
int rspamd_upstream_port(struct upstream *up);

/**
 * Sets opaque user data associated with this upstream
 * @param up
 * @param data
 * @return old data
 */
gpointer rspamd_upstream_set_data(struct upstream *up, gpointer data);

/**
 * Gets opaque user data associated with this upstream
 * @param up
 * @return
 */
gpointer rspamd_upstream_get_data(struct upstream *up);

/**
 * Get new upstream from the list
 * @param ups upstream list
 * @param type type of rotation algorithm, for `RSPAMD_UPSTREAM_HASHED` it is required to specify `key` and `keylen` as arguments
 * @return
 */
struct upstream *rspamd_upstream_get(struct upstream_list *ups,
									 enum rspamd_upstream_rotation default_type,
									 const unsigned char *key, gsize keylen);

/**
 * Get new upstream from the list
 * @param ups upstream list
 * @param type type of rotation algorithm, for `RSPAMD_UPSTREAM_HASHED` it is required to specify `key` and `keylen` as arguments
 * @return
 */
struct upstream *rspamd_upstream_get_forced(struct upstream_list *ups,
											enum rspamd_upstream_rotation forced_type,
											const unsigned char *key, gsize keylen);

/**
 * Get new upstream from the list excepting the upstream specified
 * @param ups upstream list
 * @param type type of rotation algorithm, for `RSPAMD_UPSTREAM_HASHED` it is required to specify `key` and `keylen` as arguments
 * @return
 */
struct upstream *rspamd_upstream_get_except(struct upstream_list *ups,
											struct upstream *except,
											enum rspamd_upstream_rotation default_type,
											const unsigned char *key, gsize keylen);

/**
 * Re-resolve addresses for all upstreams registered
 */
void rspamd_upstream_reresolve(struct upstream_ctx *ctx);

/**
 * Share ownership on upstream
 * @param up
 * @return
 */
struct upstream *rspamd_upstream_ref(struct upstream *up);
/**
 * Unshare ownership on upstream
 * @param up
 */
void rspamd_upstream_unref(struct upstream *up);

/**
 * Get the current rotation algorithm for an upstream list
 * @param ups upstream list
 * @return rotation algorithm
 */
enum rspamd_upstream_rotation rspamd_upstreams_get_rotation(struct upstream_list *ups);

/**
 * Configure token bucket parameters for an upstream list
 * @param ups upstream list
 * @param max_tokens maximum tokens per upstream (default: 10000)
 * @param scale_factor bytes per token (default: 1024)
 * @param min_tokens minimum tokens for selection (default: 1)
 * @param base_cost base cost per request (default: 10)
 */
void rspamd_upstreams_set_token_bucket(struct upstream_list *ups,
									   gsize max_tokens,
									   gsize scale_factor,
									   gsize min_tokens,
									   gsize base_cost);

/**
 * Configure slow-start window for revived upstreams.
 * When set, a freshly revived upstream's effective weight ramps linearly
 * from 0 to its configured weight over the given window. Avoids the
 * thundering herd that would otherwise hit the just-revived backend.
 * @param ups upstream list
 * @param slow_start_ms ramp duration in milliseconds (0 = disabled)
 */
void rspamd_upstreams_set_slow_start(struct upstream_list *ups,
									 unsigned int slow_start_ms);

/**
 * Record a per-request latency observation for the upstream.
 * Updates a time-weighted EWMA that decays old samples on a
 * configurable half-life. The EWMA feeds into P2C selection so
 * faster backends are preferred when load is otherwise comparable.
 * Cheap to call; no allocation.
 * @param up upstream
 * @param seconds observed latency (e.g. request RTT)
 */
void rspamd_upstream_record_latency(struct upstream *up, double seconds);

/**
 * Read the current latency EWMA in seconds. Zero if no samples yet.
 */
double rspamd_upstream_get_latency(const struct upstream *up);

/**
 * Configure latency EWMA half-life. Defaults to 60s; setting to 0
 * disables time-weighting (becomes a flat moving average).
 * @param ups upstream list
 * @param half_life_s decay half-life in seconds
 */
void rspamd_upstreams_set_latency_half_life(struct upstream_list *ups,
											double half_life_s);

/**
 * Get upstream using token bucket algorithm.
 * Selects upstream with lowest inflight tokens (weighted by message size).
 * Falls back to round-robin if heap initialization fails.
 * Token cost is calculated as: base_cost + (message_size / scale).
 * @param ups upstream list
 * @param except upstream to exclude (for retries), or NULL
 * @param message_size size of the message being processed
 * @param reserved_tokens output: tokens reserved for this request (must be returned later)
 * @return selected upstream or NULL if none available
 */
struct upstream *rspamd_upstream_get_token_bucket(struct upstream_list *ups,
												  struct upstream *except,
												  gsize message_size,
												  gsize *reserved_tokens);

/**
 * Return tokens to upstream after request completion.
 * Must be called exactly once for each successful rspamd_upstream_get_token_bucket call.
 * On success, tokens are restored to the available pool.
 * On failure, tokens are NOT restored - this penalises failing backends.
 * @param up upstream to return tokens to
 * @param tokens number of tokens to return (from rspamd_upstream_get_token_bucket)
 * @param success TRUE if request succeeded, FALSE if failed (penalty)
 */
void rspamd_upstream_return_tokens(struct upstream *up, gsize tokens, gboolean success);

#ifdef __cplusplus
}
#endif

#endif /* UPSTREAM_H */
