/*-------------------------------------------------------------------------
 *
 * cdc_decoder_utils.c
 *		CDC Decoder plugin utility functions for Citus
 *
 * Copyright (c) Citus Data, Inc.
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "cdc_decoder_utils.h"
#include "fmgr.h"
#include "miscadmin.h"

#include "access/genam.h"
#include "access/heapam.h"
#include "catalog/pg_namespace.h"
#include "commands/extension.h"
#include "common/hashfn.h"
#include "common/string.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/typcache.h"

#include "distributed/pg_dist_partition.h"
#include "distributed/pg_dist_shard.h"
#include "distributed/relay_utility.h"

static int32 LocalGroupId = -1;
static Oid PgDistLocalGroupRelationId = InvalidOid;
static Oid PgDistShardRelationId = InvalidOid;
static Oid PgDistShardShardidIndexId = InvalidOid;
static Oid PgDistPartitionRelationId = InvalidOid;
static Oid PgDistPartitionLogicalrelidIndexId = InvalidOid;
static bool IsCitusExtensionLoaded = false;

#define COORDINATOR_GROUP_ID 0
#define InvalidRepOriginId 0
#define Anum_pg_dist_local_groupid 1
#define GROUP_ID_UPGRADING -2


static Oid DistLocalGroupIdRelationId(void);
static int32 CdcGetLocalGroupId(void);
static HeapTuple CdcPgDistPartitionTupleViaCatalog(Oid relationId);

/*
 * DistLocalGroupIdRelationId returns the relation id of the pg_dist_local_group
 */
static Oid
DistLocalGroupIdRelationId(void)
{
	if (PgDistLocalGroupRelationId == InvalidOid)
	{
		PgDistLocalGroupRelationId = get_relname_relid("pg_dist_local_group",
													   PG_CATALOG_NAMESPACE);
	}
	return PgDistLocalGroupRelationId;
}


/*
 * DistShardRelationId returns the relation id of the pg_dist_shard
 */
static Oid
DistShardRelationId(void)
{
	if (PgDistShardRelationId == InvalidOid)
	{
		PgDistShardRelationId = get_relname_relid("pg_dist_shard", PG_CATALOG_NAMESPACE);
	}
	return PgDistShardRelationId;
}


/*
 * DistShardShardidIndexId returns the relation id of the pg_dist_shard_shardid_index
 */
static Oid
DistShardShardidIndexId(void)
{
	if (PgDistShardShardidIndexId == InvalidOid)
	{
		PgDistShardShardidIndexId = get_relname_relid("pg_dist_shard_shardid_index",
													  PG_CATALOG_NAMESPACE);
	}
	return PgDistShardShardidIndexId;
}


/*
 * DistPartitionRelationId returns the relation id of the pg_dist_partition
 */
static Oid
DistPartitionRelationId(void)
{
	if (PgDistPartitionRelationId == InvalidOid)
	{
		PgDistPartitionRelationId = get_relname_relid("pg_dist_partition",
													  PG_CATALOG_NAMESPACE);
	}
	return PgDistPartitionRelationId;
}


static Oid
DistPartitionLogicalRelidIndexId(void)
{
	if (PgDistPartitionLogicalrelidIndexId == InvalidOid)
	{
		PgDistPartitionLogicalrelidIndexId = get_relname_relid(
			"pg_dist_partition_logicalrelid_index", PG_CATALOG_NAMESPACE);
	}
	return PgDistPartitionLogicalrelidIndexId;
}


/*
 * CdcIsCoordinator function returns true if this node is identified as the
 * schema/coordinator/master node of the cluster.
 */
bool
CdcIsCoordinator(void)
{
	return (CdcGetLocalGroupId() == COORDINATOR_GROUP_ID);
}


/*
 * CdcCitusHasBeenLoaded function returns true if the citus extension has been loaded.
 */
bool
CdcCitusHasBeenLoaded()
{
	if (!IsCitusExtensionLoaded)
	{
		IsCitusExtensionLoaded = (get_extension_oid("citus", true) != InvalidOid);
	}

	return IsCitusExtensionLoaded;
}


/*
 * ExtractShardIdFromTableName tries to extract shard id from the given table name,
 * and returns the shard id if table name is formatted as shard name.
 * Else, the function returns INVALID_SHARD_ID.
 */
uint64
CdcExtractShardIdFromTableName(const char *tableName, bool missingOk)
{
	char *shardIdStringEnd = NULL;

	/* find the last underscore and increment for shardId string */
	char *shardIdString = strrchr(tableName, SHARD_NAME_SEPARATOR);
	if (shardIdString == NULL && !missingOk)
	{
		ereport(ERROR, (errmsg("could not extract shardId from table name \"%s\"",
							   tableName)));
	}
	else if (shardIdString == NULL && missingOk)
	{
		return INVALID_SHARD_ID;
	}

	shardIdString++;

	errno = 0;
	uint64 shardId = strtoull(shardIdString, &shardIdStringEnd, 0);

	if (errno != 0 || (*shardIdStringEnd != '\0'))
	{
		if (!missingOk)
		{
			ereport(ERROR, (errmsg("could not extract shardId from table name \"%s\"",
								   tableName)));
		}
		else
		{
			return INVALID_SHARD_ID;
		}
	}

	return shardId;
}


/*
 * CdcGetLocalGroupId returns the group identifier of the local node. The
 * function assumes that pg_dist_local_group has exactly one row and has at
 * least one column. Otherwise, the function errors out.
 */
static int32
CdcGetLocalGroupId(void)
{
	ScanKeyData scanKey[1];
	int scanKeyCount = 0;
	int32 groupId = 0;

	/*
	 * Already set the group id, no need to read the heap again.
	 */
	if (LocalGroupId != -1)
	{
		return LocalGroupId;
	}

	Oid localGroupTableOid = DistLocalGroupIdRelationId();
	if (localGroupTableOid == InvalidOid)
	{
		return 0;
	}

	Relation pgDistLocalGroupId = table_open(localGroupTableOid, AccessShareLock);

	SysScanDesc scanDescriptor = systable_beginscan(pgDistLocalGroupId,
													InvalidOid, false,
													NULL, scanKeyCount, scanKey);

	TupleDesc tupleDescriptor = RelationGetDescr(pgDistLocalGroupId);

	HeapTuple heapTuple = systable_getnext(scanDescriptor);

	if (HeapTupleIsValid(heapTuple))
	{
		bool isNull = false;
		Datum groupIdDatum = heap_getattr(heapTuple,
										  Anum_pg_dist_local_groupid,
										  tupleDescriptor, &isNull);

		groupId = DatumGetInt32(groupIdDatum);

		/* set the local cache variable */
		LocalGroupId = groupId;
	}
	else
	{
		/*
		 * Upgrade is happening. When upgrading postgres, pg_dist_local_group is
		 * temporarily empty before citus_finish_pg_upgrade() finishes execution.
		 */
		groupId = GROUP_ID_UPGRADING;
	}

	systable_endscan(scanDescriptor);
	table_close(pgDistLocalGroupId, AccessShareLock);

	return groupId;
}


/*
 * CdcLookupShardRelationFromCatalog returns the logical relation oid a shard belongs to.
 *
 * Errors out if the shardId does not exist and missingOk is false.
 * Returns InvalidOid if the shardId does not exist and missingOk is true.
 */
Oid
CdcLookupShardRelationFromCatalog(int64 shardId, bool missingOk)
{
	ScanKeyData scanKey[1];
	int scanKeyCount = 1;
	Form_pg_dist_shard shardForm = NULL;
	Relation pgDistShard = table_open(DistShardRelationId(), AccessShareLock);
	Oid relationId = InvalidOid;

	ScanKeyInit(&scanKey[0], Anum_pg_dist_shard_shardid,
				BTEqualStrategyNumber, F_INT8EQ, Int64GetDatum(shardId));

	SysScanDesc scanDescriptor = systable_beginscan(pgDistShard,
													DistShardShardidIndexId(), true,
													NULL, scanKeyCount, scanKey);

	HeapTuple heapTuple = systable_getnext(scanDescriptor);
	if (!HeapTupleIsValid(heapTuple) && !missingOk)
	{
		ereport(ERROR, (errmsg("could not find valid entry for shard "
							   UINT64_FORMAT, shardId)));
	}

	if (!HeapTupleIsValid(heapTuple))
	{
		relationId = InvalidOid;
	}
	else
	{
		shardForm = (Form_pg_dist_shard) GETSTRUCT(heapTuple);
		relationId = shardForm->logicalrelid;
	}

	systable_endscan(scanDescriptor);
	table_close(pgDistShard, NoLock);

	return relationId;
}


/*
 * CdcPgDistPartitionTupleViaCatalog is a helper function that searches
 * pg_dist_partition for the given relationId. The caller is responsible
 * for ensuring that the returned heap tuple is valid before accessing
 * its fields.
 */
static HeapTuple
CdcPgDistPartitionTupleViaCatalog(Oid relationId)
{
	const int scanKeyCount = 1;
	ScanKeyData scanKey[1];
	bool indexOK = true;

	Relation pgDistPartition = table_open(DistPartitionRelationId(), AccessShareLock);

	ScanKeyInit(&scanKey[0], Anum_pg_dist_partition_logicalrelid,
				BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(relationId));

	SysScanDesc scanDescriptor = systable_beginscan(pgDistPartition,
													DistPartitionLogicalRelidIndexId(),
													indexOK, NULL, scanKeyCount, scanKey);

	HeapTuple partitionTuple = systable_getnext(scanDescriptor);

	if (HeapTupleIsValid(partitionTuple))
	{
		/* callers should have the tuple in their memory contexts */
		partitionTuple = heap_copytuple(partitionTuple);
	}

	systable_endscan(scanDescriptor);
	table_close(pgDistPartition, AccessShareLock);

	return partitionTuple;
}


/*
 * CdcIsReferenceTableViaCatalog gets a relationId and returns true if the relation
 * is a reference table and false otherwise.
 */
char
CdcIsReferenceTableViaCatalog(Oid relationId)
{
	HeapTuple partitionTuple = CdcPgDistPartitionTupleViaCatalog(relationId);
	if (!HeapTupleIsValid(partitionTuple))
	{
		return false;
	}

	Relation pgDistPartition = table_open(DistPartitionRelationId(), AccessShareLock);

	TupleDesc tupleDescriptor = RelationGetDescr(pgDistPartition);
	Datum *datumArray = (Datum *) palloc(tupleDescriptor->natts * sizeof(Datum));
	bool *isNullArray = (bool *) palloc(tupleDescriptor->natts * sizeof(bool));

	heap_deform_tuple(partitionTuple, tupleDescriptor, datumArray, isNullArray);

	if (isNullArray[Anum_pg_dist_partition_partmethod - 1] ||
		isNullArray[Anum_pg_dist_partition_repmodel - 1])
	{
		/*
		 * partition method and replication model cannot be NULL,
		 * still let's make sure
		 */
		heap_freetuple(partitionTuple);
		table_close(pgDistPartition, NoLock);
		pfree(datumArray);
		pfree(isNullArray);
		return false;
	}

	Datum partitionMethodDatum = datumArray[Anum_pg_dist_partition_partmethod - 1];
	char partitionMethodChar = DatumGetChar(partitionMethodDatum);

	Datum replicationModelDatum = datumArray[Anum_pg_dist_partition_repmodel - 1];
	char replicationModelChar = DatumGetChar(replicationModelDatum);

	heap_freetuple(partitionTuple);
	table_close(pgDistPartition, NoLock);
	pfree(datumArray);
	pfree(isNullArray);

	/*
	 * A table is a reference table when its partition method is 'none'
	 * and replication model is 'two phase commit'
	 */
	return partitionMethodChar == DISTRIBUTE_BY_NONE &&
		   replicationModelChar == REPLICATION_MODEL_2PC;
}


/*
 * RemoveCitusDecodersFromPaths removes a path ending in citus_decoders
 * from the given input paths.
 */
char *
RemoveCitusDecodersFromPaths(char *paths)
{
	if (strlen(paths) == 0)
	{
		/* dynamic_library_path is empty */
		return paths;
	}

	StringInfo newPaths = makeStringInfo();

	char *remainingPaths = paths;

	for (;;)
	{
		int pathLength = 0;

		char *pathStart = first_path_var_separator(remainingPaths);
		if (pathStart == remainingPaths)
		{
			/*
			 * This will error out in find_in_dynamic_libpath, return
			 * original value here.
			 */
			return paths;
		}
		else if (pathStart == NULL)
		{
			/* final path */
			pathLength = strlen(remainingPaths);
		}
		else
		{
			/* more paths remaining */
			pathLength = pathStart - remainingPaths;
		}

		char *currentPath = palloc(pathLength + 1);
		strlcpy(currentPath, remainingPaths, pathLength + 1);
		canonicalize_path(currentPath);

		if (!pg_str_endswith(currentPath, "/citus_decoders"))
		{
			appendStringInfo(newPaths, "%s%s", newPaths->len > 0 ? ":" : "", currentPath);
		}

		if (remainingPaths[pathLength] == '\0')
		{
			/* end of string */
			break;
		}

		remainingPaths += pathLength + 1;
	}

	return newPaths->data;
}
