#ifndef PROXYSQL_PLUGIN_MANAGER_H
#define PROXYSQL_PLUGIN_MANAGER_H

// Plugin chassis is a v4.0 feature.  Including this header from a v3.x
// (no PROXYSQL40) translation unit is a no-op: no class, no free
// functions, nothing.  Callers that intend to use the plugin manager
// must guard their own code on PROXYSQL40 too.
#ifdef PROXYSQL40

#include "ProxySQL_Plugin.h"

#include <cstddef>
#include <deque>
#include <memory>
#include <string>
#include <vector>

class ProxySQL_PluginManager {
public:
	ProxySQL_PluginManager();
	~ProxySQL_PluginManager();

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

	bool load(const std::string &path, std::string &err);
#ifdef PROXYSQL40
	// Phase B of the four-phase plugin lifecycle: after all plugins have
	// been dlopen'd but BEFORE admin module bootstrap.  Invokes each
	// plugin's optional register_schemas callback with a services struct
	// that has register_table live but DB-handle getters stubbed to
	// nullptr.  Plugins that left the descriptor field null are skipped.
	// Returns false on the first register_schemas callback that fails.
	bool invoke_register_schemas_phase(std::string &err);
#endif /* PROXYSQL40 */
	bool init_all(std::string &err);
	bool start_all(std::string &err);
	bool stop_all();
	const std::vector<ProxySQL_PluginTableDef>& tables(ProxySQL_PluginDBKind kind) const;
	bool dispatch_admin_command(const ProxySQL_PluginCommandContext& ctx, const std::string& sql, ProxySQL_PluginCommandResult& result) const;

	void register_table_for_test(const ProxySQL_PluginTableDef& def);
	bool register_command_for_test(const std::string& sql);
	bool has_command_for_test(const std::string& sql) const;
	bool register_table(const ProxySQL_PluginTableDef& def);
	bool register_command(const char* sql, proxysql_plugin_admin_command_cb cb);
#ifdef PROXYSQL40
	// Register an alternate spelling (alias) of an already-registered
	// command.  Returns true on successful registration, false if the
	// canonical SQL isn't registered, if the alias is empty, or if the
	// alias would collide with another (canonical or alias) of a
	// different command. Duplicate registrations of the same
	// (canonical, alias) pair are idempotent and return true.
	bool register_command_alias(const char* canonical_sql, const char* alias_sql);
	// Resolve an incoming admin-command spelling to its canonical form.
	// Returns an owned copy of the canonical SQL if the query matches a
	// registered command or any of its aliases; an empty string
	// otherwise.  Whitespace and case are normalized on both sides.
	// Returns by value (not const char*) so callers can release the
	// manager lock before dispatching without risking pointer
	// invalidation on concurrent reload.
	std::string resolve_alias_to_canonical(const std::string& sql) const;
	bool register_query_hook(ProxySQL_PluginProtocol proto, proxysql_plugin_query_hook_cb cb);
	bool has_query_hook(ProxySQL_PluginProtocol proto) const;
	bool dispatch_query_hook(ProxySQL_PluginProtocol proto,
	                         const ProxySQL_PluginQueryHookPayload& payload,
	                         ProxySQL_PluginQueryHookResult& result) const;

	// Runtime-view (admin-side projection of module state) plumbing.
	// register_runtime_view returns false if the table is already
	// registered or if the refresh callback is null.
	bool register_runtime_view(const ProxySQL_PluginRuntimeView& view);

	// Refresh every registered view whose table_name appears as a
	// case-insensitive substring of `sql`. Each refresh callback is
	// invoked exactly once per call, regardless of how many times its
	// table is mentioned. Best-effort: a callback that throws or
	// otherwise misbehaves is logged but does not stop other views from
	// refreshing. Caller supplies all three DB handles so the chassis
	// does not have to reach into the global admin module.
	void refresh_runtime_views_for_query(const std::string& sql,
		SQLite3DB* admindb, SQLite3DB* configdb, SQLite3DB* statsdb) const;
#endif /* PROXYSQL40 */

	size_t size() const;

private:
	struct plugin_handle_t {
		void *handle{nullptr};
		const ProxySQL_PluginDescriptor *descriptor{nullptr};
		std::string path {};
#ifdef PROXYSQL40
		bool schemas_registered{false};
#endif /* PROXYSQL40 */
		bool initialized{false};
		bool started{false};
		bool stopped{false};
	};

	struct registered_command_t {
		std::string sql {};
		proxysql_plugin_admin_command_cb cb { nullptr };
#ifdef PROXYSQL40
		// User-friendly alternate spellings for this canonical command.
		// Admin's dispatcher resolves any of these to `sql` before
		// invoking `cb`. Normalized (case + whitespace) on insertion.
		std::vector<std::string> aliases {};
#endif /* PROXYSQL40 */
	};

	struct registered_table_storage_t {
		std::string table_name {};
		std::string table_def {};
	};

	std::vector<plugin_handle_t> plugins_;
	ProxySQL_PluginServices services_;
#ifdef PROXYSQL40
	// Phase-B variant handed to register_schemas; DB-handle getters are
	// stubbed, everything else mirrors services_.  See the contract in
	// ProxySQL_Plugin.h next to ProxySQL_PluginServices.
	ProxySQL_PluginServices services_phase_b_;
#endif /* PROXYSQL40 */
	std::vector<ProxySQL_PluginTableDef> tables_admin_;
	std::vector<ProxySQL_PluginTableDef> tables_config_;
	std::vector<ProxySQL_PluginTableDef> tables_stats_;
	std::deque<registered_table_storage_t> table_storage_;
	std::vector<registered_command_t> commands_;
#ifdef PROXYSQL40
	// At most one hook per protocol; nullptr means "no hook".
	proxysql_plugin_query_hook_cb mysql_query_hook_ { nullptr };
	proxysql_plugin_query_hook_cb pgsql_query_hook_ { nullptr };

	// Runtime-view registry: one entry per admin-side projection of
	// module state. Stored alongside an owned table_name copy so
	// callers may free the input string after registration. The
	// refresh callback pointer and opaque are plugin-owned with
	// static lifetime (the .so isn't unloaded while a view is live).
	struct registered_runtime_view_t {
		ProxySQL_PluginDBKind db_kind { ProxySQL_PluginDBKind::admin_db };
		std::string table_name {};
		void (*refresh)(SQLite3DB*, void*) { nullptr };
		void* opaque { nullptr };
	};
	std::vector<registered_runtime_view_t> runtime_views_ {};
#endif /* PROXYSQL40 */
};

ProxySQL_PluginManager* proxysql_get_plugin_manager();
bool proxysql_dispatch_configured_plugin_admin_command(
	const ProxySQL_PluginCommandContext& ctx,
	const std::string& sql,
	ProxySQL_PluginCommandResult& result
);
bool proxysql_dispatch_configured_plugin_query_hook(
	ProxySQL_PluginProtocol proto,
	const ProxySQL_PluginQueryHookPayload& payload,
	ProxySQL_PluginQueryHookResult& result
);
// Fast path for hot code: returns true when the active manager has a hook
// registered for the given protocol.  No locks taken.  Callers should still
// invoke proxysql_dispatch_configured_plugin_query_hook to actually run the
// hook (which takes the manager lock).  Use this to elide the dispatch call
// entirely on the no-plugin path.
bool proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol proto);
// Admin-side helper: consult the active plugin manager's command table and
// return the canonical spelling of `sql` if it's a registered command or
// alias, or an empty string otherwise.  Returns by value so callers can
// release the manager lock before dispatching without risking pointer
// invalidation on concurrent reload.
std::string proxysql_resolve_configured_plugin_admin_alias(const std::string& sql);

// Admin-side helper: invoke every plugin runtime-view refresh callback
// whose registered table is referenced by `sql`. Used by Admin's
// pre-SELECT path, mirroring the way runtime_mysql_users is refreshed
// before its SELECTs. No-op if no plugin manager is active or no views
// match. Caller supplies all three DB handles; the chassis dispatches
// the correct one based on each view's registered db_kind.
void proxysql_refresh_configured_plugin_runtime_views(const std::string& sql,
	SQLite3DB* admindb, SQLite3DB* configdb, SQLite3DB* statsdb);
// Phase A + B of the four-phase lifecycle: dlopen() each module, read its
// descriptor, then call register_schemas() on plugins that opted in. On
// success, `manager` is populated AND installed as the active manager so
// that ProxySQL_Admin::init() can see the declared tables and merge them
// into tables_defs_{admin,config,stats} for the existing
// check_and_build_standard_tables DDL pass. Phase D (init) must be
// invoked separately — after admin module bootstrap — via
// proxysql_init_configured_plugins.
bool proxysql_load_configured_plugins(
	std::unique_ptr<ProxySQL_PluginManager>& manager,
	const std::vector<std::string>& plugin_modules,
	std::string& err
);
// Phase D: call each plugin's init() with full services (live DB handles).
// Must run after ProxySQL_Main_init_Admin_module so init() sees live
// admindb/configdb/statsdb with plugin-owned tables already materialized.
bool proxysql_init_configured_plugins(
	ProxySQL_PluginManager* manager,
	std::string& err
);
bool proxysql_start_configured_plugins(
	ProxySQL_PluginManager* manager,
	std::string& err
);
bool proxysql_stop_configured_plugins(
	std::unique_ptr<ProxySQL_PluginManager>& manager,
	std::string& err
);

#endif /* PROXYSQL40 */
#endif /* PROXYSQL_PLUGIN_MANAGER_H */
