/*-------------------------------------------------------------------------
 *
 * recovery_gen.c
 *		Generator for recovery configuration
 *
 * Portions Copyright (c) 2011-2025, PostgreSQL Global Development Group
 *
 *-------------------------------------------------------------------------
 */
#include "postgres_fe.h"

#include "common/logging.h"
#include "fe_utils/recovery_gen.h"
#include "fe_utils/string_utils.h"

static char *escape_quotes(const char *src);
static char *FindDbnameInConnOpts(PQconninfoOption *conn_opts);

/*
 * Write recovery configuration contents into a fresh PQExpBuffer, and
 * return it.
 *
 * This accepts the dbname which will be appended to the primary_conninfo.
 * The dbname will be ignored by walreceiver process but slotsync worker uses
 * it to connect to the primary server.
 */
PQExpBuffer
GenerateRecoveryConfig(PGconn *pgconn, const char *replication_slot,
					   char *dbname)
{
	PQconninfoOption *connOptions;
	PQExpBufferData conninfo_buf;
	char	   *escaped;
	PQExpBuffer contents;

	Assert(pgconn != NULL);

	contents = createPQExpBuffer();
	if (!contents)
		pg_fatal("out of memory");

	/*
	 * In PostgreSQL 12 and newer versions, standby_mode is gone, replaced by
	 * standby.signal to trigger a standby state at recovery.
	 */
	if (PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC)
		appendPQExpBufferStr(contents, "standby_mode = 'on'\n");

	connOptions = PQconninfo(pgconn);
	if (connOptions == NULL)
		pg_fatal("out of memory");

	initPQExpBuffer(&conninfo_buf);
	for (PQconninfoOption *opt = connOptions; opt && opt->keyword; opt++)
	{
		/* Omit empty settings and those libpqwalreceiver overrides. */
		if (strcmp(opt->keyword, "replication") == 0 ||
			strcmp(opt->keyword, "dbname") == 0 ||
			strcmp(opt->keyword, "fallback_application_name") == 0 ||
			(opt->val == NULL) ||
			(opt->val != NULL && opt->val[0] == '\0'))
			continue;

		/* Separate key-value pairs with spaces */
		if (conninfo_buf.len != 0)
			appendPQExpBufferChar(&conninfo_buf, ' ');

		/*
		 * Write "keyword=value" pieces, the value string is escaped and/or
		 * quoted if necessary.
		 */
		appendPQExpBuffer(&conninfo_buf, "%s=", opt->keyword);
		appendConnStrVal(&conninfo_buf, opt->val);
	}

	if (dbname)
	{
		/*
		 * If dbname is specified in the connection, append the dbname. This
		 * will be used later for logical replication slot synchronization.
		 */
		if (conninfo_buf.len != 0)
			appendPQExpBufferChar(&conninfo_buf, ' ');

		appendPQExpBuffer(&conninfo_buf, "%s=", "dbname");
		appendConnStrVal(&conninfo_buf, dbname);
	}

	if (PQExpBufferDataBroken(conninfo_buf))
		pg_fatal("out of memory");

	/*
	 * Escape the connection string, so that it can be put in the config file.
	 * Note that this is different from the escaping of individual connection
	 * options above!
	 */
	escaped = escape_quotes(conninfo_buf.data);
	termPQExpBuffer(&conninfo_buf);
	appendPQExpBuffer(contents, "primary_conninfo = '%s'\n", escaped);
	free(escaped);

	if (replication_slot)
	{
		/* unescaped: ReplicationSlotValidateName allows [a-z0-9_] only */
		appendPQExpBuffer(contents, "primary_slot_name = '%s'\n",
						  replication_slot);
	}

	if (PQExpBufferBroken(contents))
		pg_fatal("out of memory");

	PQconninfoFree(connOptions);

	return contents;
}

/*
 * Write the configuration file in the directory specified in target_dir,
 * with the contents already collected in memory appended.  Then write
 * the signal file into the target_dir.  If the server does not support
 * recovery parameters as GUCs, the signal file is not necessary, and
 * configuration is written to recovery.conf.
 */
void
WriteRecoveryConfig(PGconn *pgconn, const char *target_dir, PQExpBuffer contents)
{
	char		filename[MAXPGPATH];
	FILE	   *cf;
	bool		use_recovery_conf;

	Assert(pgconn != NULL);

	use_recovery_conf =
		PQserverVersion(pgconn) < MINIMUM_VERSION_FOR_RECOVERY_GUC;

	snprintf(filename, MAXPGPATH, "%s/%s", target_dir,
			 use_recovery_conf ? "recovery.conf" : "postgresql.auto.conf");

	cf = fopen(filename, use_recovery_conf ? "w" : "a");
	if (cf == NULL)
		pg_fatal("could not open file \"%s\": %m", filename);

	if (fwrite(contents->data, contents->len, 1, cf) != 1)
		pg_fatal("could not write to file \"%s\": %m", filename);

	fclose(cf);

	if (!use_recovery_conf)
	{
		snprintf(filename, MAXPGPATH, "%s/%s", target_dir, "standby.signal");
		cf = fopen(filename, "w");
		if (cf == NULL)
			pg_fatal("could not create file \"%s\": %m", filename);

		fclose(cf);
	}
}

/*
 * Escape a string so that it can be used as a value in a key-value pair
 * a configuration file.
 */
static char *
escape_quotes(const char *src)
{
	char	   *result = escape_single_quotes_ascii(src);

	if (!result)
		pg_fatal("out of memory");
	return result;
}

/*
 * FindDbnameInConnOpts
 *
 * This is a helper function for GetDbnameFromConnectionOptions(). Extract
 * the value of dbname from PQconninfoOption parameters, if it's present.
 * Returns a strdup'd result or NULL.
 */
static char *
FindDbnameInConnOpts(PQconninfoOption *conn_opts)
{
	for (PQconninfoOption *conn_opt = conn_opts;
		 conn_opt->keyword != NULL;
		 conn_opt++)
	{
		if (strcmp(conn_opt->keyword, "dbname") == 0 &&
			conn_opt->val != NULL && conn_opt->val[0] != '\0')
			return pg_strdup(conn_opt->val);
	}
	return NULL;
}

/*
 * GetDbnameFromConnectionOptions
 *
 * This is a special purpose function to retrieve the dbname from either the
 * 'connstr' specified by the caller or from the environment variables.
 *
 * Returns NULL, if dbname is not specified by the user in the given
 * connection options.
 */
char *
GetDbnameFromConnectionOptions(const char *connstr)
{
	PQconninfoOption *conn_opts;
	char	   *err_msg = NULL;
	char	   *dbname;

	/* First try to get the dbname from connection string. */
	if (connstr)
	{
		conn_opts = PQconninfoParse(connstr, &err_msg);
		if (conn_opts == NULL)
			pg_fatal("%s", err_msg);

		dbname = FindDbnameInConnOpts(conn_opts);

		PQconninfoFree(conn_opts);
		if (dbname)
			return dbname;
	}

	/*
	 * Next try to get the dbname from default values that are available from
	 * the environment.
	 */
	conn_opts = PQconndefaults();
	if (conn_opts == NULL)
		pg_fatal("out of memory");

	dbname = FindDbnameInConnOpts(conn_opts);

	PQconninfoFree(conn_opts);
	return dbname;
}
