/*-------------------------------------------------------------------------
 * Copyright (c) Microsoft Corporation.  All rights reserved.
 *
 * src/commands/drop_indexes.c
 *
 * Implementation of the drop index operation.
 *
 *-------------------------------------------------------------------------
 */
#include <postgres.h>
#include <access/xact.h>
#include <catalog/namespace.h>
#include <catalog/dependency.h>
#include <commands/sequence.h>
#include <executor/spi.h>
#include <fmgr.h>
#include <funcapi.h>
#include <lib/stringinfo.h>
#include <nodes/pg_list.h>
#include <utils/builtins.h>
#include <utils/lsyscache.h>

#include "api_hooks.h"
#include "io/bson_core.h"
#include "commands/parse_error.h"
#include "commands/commands_common.h"
#include "commands/diagnostic_commands_common.h"
#include "commands/drop_indexes.h"
#include "metadata/collection.h"
#include "metadata/metadata_cache.h"
#include "metadata/index.h"
#include "utils/error_utils.h"
#include "utils/documentdb_errors.h"
#include "utils/query_utils.h"
#include "utils/index_utils.h"


typedef enum
{
	DROP_INDEX_MODE_INVALID = 0,
	DROP_INDEX_BY_NAME_LIST,
	DROP_INDEX_BY_SPEC_DOCUMENT
} DropIndexMode;


/* Represents whole "arg" document passed to dbcommand/dropIndexes */
typedef struct
{
	/* represents value of "dropIndexes" field */
	char *collectionName;

	/* represents value of "index" field */
	DropIndexMode dropIndexMode;
	union
	{
		List *nameList;   /* DROP_INDEX_BY_NAME_LIST */
		pgbson *document; /* DROP_INDEX_BY_SPEC_DOCUMENT */
	} index;


	/* TODO: other things such as writeConcern, comment ... */
} DropIndexesArg;

/*
 * Contains the data used when building the bson object that needs to be
 * sent to the client after a dropIndexes() command.
 */
typedef struct
{
	bool ok;
	int64 nIndexesWas;

	/* error reporting; valid only when "ok" is false */
	char *errmsg;
	int errcode;
} DropIndexesResult;


#define DROP_INDEX_ARG_FIELD_NOT_IMPL \
	"\"%s\" field for drop index arg is not implemented"


PG_FUNCTION_INFO_V1(command_drop_indexes);
PG_FUNCTION_INFO_V1(command_drop_indexes_concurrently);
PG_FUNCTION_INFO_V1(command_drop_indexes_concurrently_internal);

static DropIndexesArg ParseDropIndexesArg(pgbson *options, Datum *databaseNameDatum);
static void DropIndexesArgExpandIndexNameList(uint64 collectionId,
											  DropIndexesArg *dropIndexesArg);
static DropIndexesResult * DropIndexesConcurrentlyInternal(char *dbName, pgbson *arg);
static DropIndexesResult ProcessDropIndexesRequest(char *dbName, DropIndexesArg
												   dropIndexesArg,
												   bool dropIndexConcurrently);
static pgbson * MakeDropIndexesMsg(DropIndexesResult *result);
static void ExecuteDropIndexCommand(char *cmd, bool unique, bool concurrently, bool
									forceReadWrite);
static char * CreateDropIndexCommand(uint64 collectionId, int indexId, bool unique, bool
									 concurrently,
									 bool missingOk);
static void HandleDropIndexConcurrently(uint64 collectionId, int indexId, bool unique,
										bool concurrently,
										bool missingOk, DropIndexesResult *result,
										MemoryContext oldMemContext);
static void CancelIndexBuildRequest(int indexId);

/*
 * command_drop_indexes is the implementation of the internal logic for
 * dbcommand/dropIndexes.
 */
Datum
command_drop_indexes(PG_FUNCTION_ARGS)
{
	if (PG_ARGISNULL(1))
	{
		ereport(ERROR, (errmsg("Argument value must not be NULL")));
	}
	pgbson *arg = PG_GETARG_PGBSON(1);

	Datum dbNameDatum = PG_ARGISNULL(0) ? (Datum) 0 : PG_GETARG_DATUM(0);
	DropIndexesArg dropIndexesArg = ParseDropIndexesArg(arg, &dbNameDatum);

	if (dbNameDatum == (Datum) 0)
	{
		ereport(ERROR, (errmsg("dbName cannot be NULL")));
	}
	char *dbName = TextDatumGetCString(dbNameDatum);

	bool dropIndexConcurrently = false;
	DropIndexesResult dropIndexResult = ProcessDropIndexesRequest(dbName, dropIndexesArg,
																  dropIndexConcurrently);
	Datum values[1] = { 0 };
	bool isNulls[1] = { false };
	values[0] = PointerGetDatum(MakeDropIndexesMsg(&dropIndexResult));

	/* fetch TupleDesc for result, not interested in resultTypeId */
	Oid *resultTypeId = NULL;
	TupleDesc resultTupDesc = NULL;
	if (get_call_result_type(fcinfo, resultTypeId, &resultTupDesc) == TYPEFUNC_SCALAR)
	{
		PG_RETURN_DATUM(values[0]);
	}

	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(resultTupDesc, values, isNulls)));
}


/*
 * ProcessDropIndexesRequest is the implementation of the internal logic for
 * dropIndexes. If dropIndexConcurrently is set to true, then drop index concurrently.
 */
static DropIndexesResult
ProcessDropIndexesRequest(char *dbName, DropIndexesArg dropIndexesArg, bool
						  dropIndexConcurrently)
{
	char *collectionName = dropIndexesArg.collectionName;
	MongoCollection *collection =
		GetMongoCollectionByNameDatum(PointerGetDatum(cstring_to_text(dbName)),
									  PointerGetDatum(cstring_to_text(collectionName)),
									  AccessShareLock);
	if (collection == NULL)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_NAMESPACENOTFOUND),
						errmsg("Namespace %s.%s could not be located", dbName,
							   collectionName)));
	}

	uint64 collectionId = collection->collectionId;

	DropIndexesResult result = {
		.ok = true,
		.nIndexesWas = CollectionIdGetIndexCount(collectionId)
	};

	if (dropIndexesArg.dropIndexMode == DROP_INDEX_BY_NAME_LIST)
	{
		/* expand "*" to a list of index names to be dropped, if provided */
		DropIndexesArgExpandIndexNameList(collectionId, &dropIndexesArg);

		/* TODO: maybe sort dropIndexNameList before moving forward ? */
		ListCell *indexNameCell = NULL;
		List *indexesDetailsList = NIL;
		foreach(indexNameCell, dropIndexesArg.index.nameList)
		{
			char *indexName = lfirst(indexNameCell);

			if (strcmp(indexName, ID_INDEX_NAME) == 0)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INVALIDOPTIONS),
								errmsg("cannot drop _id index")));
			}

			IndexDetails *indexDetails = IndexNameGetIndexDetails(collectionId,
																  indexName);
			if (indexDetails == NULL)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INDEXNOTFOUND),
								errmsg("index not found with name [%s]", indexName)));
			}

			bool missingOk = true;
			bool concurrently = false;
			bool forceReadWrite = false;
			if (!dropIndexConcurrently)
			{
				DropPostgresIndex(collectionId, indexDetails->indexId,
								  indexDetails->indexSpec.indexUnique ==
								  BoolIndexOption_True,
								  concurrently, forceReadWrite, missingOk);
				DeleteCollectionIndexRecord(collectionId, indexDetails->indexId);
			}
			else
			{
				/* Mark index entries as skippable before starting the clean up process
				 * This is to avoid accidental pick up of request by the cron job while we are cleaning up index.
				 * BugId 2858365
				 */
				pgbson_writer writer;
				PgbsonWriterInit(&writer);
				PgbsonWriterAppendUtf8(&writer, ErrMsgKey, ErrMsgLength,
									   "DropIndexes is requested for the index");
				PgbsonWriterAppendInt32(&writer, ErrCodeKey, ErrCodeLength,
										ERRCODE_DOCUMENTDB_CANNOTCREATEINDEX);
				pgbson *newComment = PgbsonWriterGetPgbson(&writer);
				MarkIndexRequestStatus(indexDetails->indexId, CREATE_INDEX_COMMAND_TYPE,
									   IndexCmdStatus_Skippable, newComment, NULL, 1);
			}
			indexesDetailsList = lappend(indexesDetailsList, indexDetails);
		}

		/* We need to do all prevalidation on all given index names and
		 * if none of them fails then only we should start dropping indexes. */
		if (dropIndexConcurrently && indexesDetailsList != NIL)
		{
			ListCell *indexDetailsCell = NULL;
			bool missingOk = true;
			bool concurrently = true;

			/* save the memory context before committing the transaction */
			MemoryContext oldMemContext = CurrentMemoryContext;

			/* commit here so that MarkIndexRequestStatus is visible */
			PopAllActiveSnapshots();
			CommitTransactionCommand();
			StartTransactionCommand();

			foreach(indexDetailsCell, indexesDetailsList)
			{
				const IndexDetails *indexDetails = (IndexDetails *) lfirst(
					indexDetailsCell);
				HandleDropIndexConcurrently(collectionId, indexDetails->indexId,
											indexDetails->indexSpec.indexUnique ==
											BoolIndexOption_True,
											concurrently, missingOk, &result,
											oldMemContext);
				if (!result.ok)
				{
					/* break foreach */
					break;
				}
			}
		}
	}
	else if (dropIndexesArg.dropIndexMode == DROP_INDEX_BY_SPEC_DOCUMENT)
	{
		/*
		 * IndexKeyGetMatchingValidIndexes returns indexes in the order of their
		 * ids, so we don't need to worry about regression tests that report
		 * conflicting indexes becoming flaky.
		 */
		List *matchingIndexDetailsList =
			IndexKeyGetMatchingIndexes(collectionId,
									   dropIndexesArg.index.document);
		int nmatchingIndexes = list_length(matchingIndexDetailsList);
		if (nmatchingIndexes == 0)
		{
			const char *keyDocumentStr =
				PgbsonToJsonForLogging(dropIndexesArg.index.document);

			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INDEXNOTFOUND),
							errmsg("Unable to locate index using the provided key: %s",
								   keyDocumentStr)));
		}
		else if (nmatchingIndexes > 1)
		{
			const char *keyDocumentStr =
				PgbsonToJsonForLogging(dropIndexesArg.index.document);

			IndexDetails *firstMatchingIndexDetails =
				linitial(matchingIndexDetailsList);
			const char *firstMatchingIndexSpecStr = PgbsonToJsonForLogging(
				IndexSpecAsBson(&firstMatchingIndexDetails->indexSpec));

			IndexDetails *secondMatchingIndexDetails =
				lsecond(matchingIndexDetailsList);
			const char *secondMatchingIndexSpecStr = PgbsonToJsonForLogging(
				IndexSpecAsBson(&secondMatchingIndexDetails->indexSpec));

			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_AMBIGUOUSINDEXKEYPATTERN),
							errmsg("%d indexes found for key: %s, identify by "
								   "name instead. Conflicting indexes: %s, %s",
								   nmatchingIndexes, keyDocumentStr,
								   firstMatchingIndexSpecStr,
								   secondMatchingIndexSpecStr)));
		}

		IndexDetails *matchingIndexDetails = linitial(matchingIndexDetailsList);
		if (strcmp(matchingIndexDetails->indexSpec.indexName, ID_INDEX_NAME) == 0)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INVALIDOPTIONS),
							errmsg("cannot drop _id index")));
		}

		bool missingOk = true;
		bool concurrently = false;
		bool forceReadWrite = false;
		if (dropIndexConcurrently)
		{
			concurrently = true;

			/* Mark index entries as skippable before starting the clean up process
			 * This is to avoid accidental pick up of request by the cron job while we are cleaning up index.
			 * BugId 2858365
			 */
			pgbson_writer writer;
			PgbsonWriterInit(&writer);
			PgbsonWriterAppendUtf8(&writer, ErrMsgKey, ErrMsgLength,
								   "DropIndexes is requested for the index");
			PgbsonWriterAppendInt32(&writer, ErrCodeKey, ErrCodeLength,
									ERRCODE_DOCUMENTDB_CANNOTCREATEINDEX);
			pgbson *newComment = PgbsonWriterGetPgbson(&writer);

			MarkIndexRequestStatus(matchingIndexDetails->indexId,
								   CREATE_INDEX_COMMAND_TYPE,
								   IndexCmdStatus_Skippable, newComment, NULL, 1);

			/* save the memory context before committing the transaction */
			MemoryContext oldMemContext = CurrentMemoryContext;

			/* commit here so that MarkIndexRequestStatus is visible */
			PopAllActiveSnapshots();
			CommitTransactionCommand();
			StartTransactionCommand();

			HandleDropIndexConcurrently(collectionId, matchingIndexDetails->indexId,
										matchingIndexDetails->indexSpec.indexUnique ==
										BoolIndexOption_True,
										concurrently, missingOk, &result,
										oldMemContext);
		}
		else
		{
			DropPostgresIndex(collectionId, matchingIndexDetails->indexId,
							  matchingIndexDetails->indexSpec.indexUnique ==
							  BoolIndexOption_True,
							  concurrently, forceReadWrite, missingOk);
			DeleteCollectionIndexRecord(collectionId, matchingIndexDetails->indexId);
		}
	}
	else
	{
		ereport(ERROR, (errmsg("invalid dropIndex mode")));
	}

	return result;
}


/*
 * command_drop_indexes_concurrently calls command_drop_indexes_concurrently_internal only on coordinator.
 */
Datum
command_drop_indexes_concurrently(PG_FUNCTION_ARGS)
{
	if (PG_ARGISNULL(1))
	{
		ereport(ERROR, (errmsg("Argument value must not be NULL")));
	}
	pgbson *spec = PG_GETARG_PGBSON(1);

	Datum dbNameDatum = PG_ARGISNULL(0) ? (Datum) 0 : PG_GETARG_DATUM(0);

	/* Parse to extract $db if needed (result is unused but side-effects dbNameDatum) */
	ParseDropIndexesArg(spec, &dbNameDatum);

	if (dbNameDatum == (Datum) 0)
	{
		ereport(ERROR, (errmsg("dbName cannot be NULL")));
	}
	text *databaseDatum = DatumGetTextP(dbNameDatum);
	bool ok = false;
	pgbson *respose;

	if (IsMetadataCoordinator())
	{
		DropIndexesResult *result = DropIndexesConcurrentlyInternal(text_to_cstring(
																		databaseDatum),
																	spec);
		respose = MakeDropIndexesMsg(result);
	}
	else
	{
		StringInfo query = makeStringInfo();
		appendStringInfo(query,
						 "CALL %s.drop_indexes_concurrently_internal(%s,%s)",
						 ApiInternalSchemaName,
						 quote_literal_cstr(text_to_cstring(databaseDatum)),
						 quote_literal_cstr(PgbsonToHexadecimalString(spec)));

		DistributedRunCommandResult result = RunCommandOnMetadataCoordinator(query->data);
		if (!result.success)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INTERNALERROR),
							errmsg(
								"Error submitting background index/ drop index %s",
								text_to_cstring(result.response)),
							errdetail_log(
								"Error submitting index request/drop index %s",
								text_to_cstring(result.response))));
		}

		char *responseChar = text_to_cstring(result.response);
		if (IsBsonHexadecimalString(responseChar))
		{
			respose = PgbsonInitFromHexadecimalString(responseChar);
		}
		else
		{
			/* It's a json string use json deserialization */
			respose = PgbsonInitFromJson(responseChar);
		}
	}

	bson_iter_t documentIter;
	PgbsonInitIterator(respose, &documentIter);
	while (bson_iter_next(&documentIter))
	{
		const char *key = bson_iter_key(&documentIter);
		if (strcmp(key, "ok") == 0)
		{
			const bson_value_t *value = bson_iter_value(&documentIter);
			ok = value->value.v_bool;
			break;
		}
	}

	Datum values[2] = { 0 };
	bool isNulls[2] = { false, false };
	values[0] = PointerGetDatum(respose);
	values[1] = BoolGetDatum(ok);

	/* fetch TupleDesc for result, not interested in resultTypeId */
	Oid *resultTypeId = NULL;
	TupleDesc resultTupDesc = NULL;
	get_call_result_type(fcinfo, resultTypeId, &resultTupDesc);

	return HeapTupleGetDatum(heap_form_tuple(resultTupDesc, values, isNulls));
}


/*
 * command_drop_indexes_concurrently_internal drop index concurrently and also cancels the running create index request for the same index.
 */
Datum
command_drop_indexes_concurrently_internal(PG_FUNCTION_ARGS)
{
	if (PG_ARGISNULL(1))
	{
		ereport(ERROR, (errmsg("Argument value must not be NULL")));
	}
	pgbson *arg = PG_GETARG_PGBSON(1);

	Datum dbNameDatum = PG_ARGISNULL(0) ? (Datum) 0 : PG_GETARG_DATUM(0);

	/* Parse to extract $db if needed */
	ParseDropIndexesArg(arg, &dbNameDatum);

	if (dbNameDatum == (Datum) 0)
	{
		ereport(ERROR, (errmsg("dbName cannot be NULL")));
	}
	char *dbName = TextDatumGetCString(dbNameDatum);

	DropIndexesResult *dropIndexResult = DropIndexesConcurrentlyInternal(dbName, arg);

	Datum values[1] = { 0 };
	bool isNulls[1] = { false };
	values[0] = PointerGetDatum(MakeDropIndexesMsg(dropIndexResult));

	/* fetch TupleDesc for result, not interested in resultTypeId */
	Oid *resultTypeId = NULL;
	TupleDesc resultTupDesc = NULL;
	get_call_result_type(fcinfo, resultTypeId, &resultTupDesc);

	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(resultTupDesc, values,
													  isNulls)));
}


/* DropIndexesConcurrentlyInternal drops index concurrently and also cancels the running create index request for the same index. */
static DropIndexesResult *
DropIndexesConcurrentlyInternal(char *dbName, pgbson *arg)
{
	DropIndexesResult *dropIndexResult = palloc0(sizeof(DropIndexesResult));

	MemoryContext savedMemoryContext = CurrentMemoryContext;
	PG_TRY();
	{
		Datum databaseNameDatum = CStringGetTextDatum(dbName);
		DropIndexesArg dropIndexesArg = ParseDropIndexesArg(arg, &databaseNameDatum);
		bool dropIndexConcurrently = true;
		*dropIndexResult = ProcessDropIndexesRequest(dbName, dropIndexesArg,
													 dropIndexConcurrently);
	}
	PG_CATCH();
	{
		/* Since the distributed caller does not return error code in case of failure inside the called function
		 * i.e. ApiInternalSchema.drop_indexes_concurrently_internal in this case.
		 * The way we are solving it by adding 'errmsg' and 'code' in the DropIndexesResult and
		 * setting them (with ok->false) when there is any exception in here.
		 */
		MemoryContextSwitchTo(savedMemoryContext);
		dropIndexResult->ok = false;
		dropIndexResult->nIndexesWas = 0;

		ErrorData *edata = CopyErrorDataAndFlush();
		dropIndexResult->errcode = edata->sqlerrcode;
		dropIndexResult->errmsg = edata->message;

		/* Since we no longer have an active transcation we need to restart the transaction */
		PopAllActiveSnapshots();
		AbortCurrentTransaction();
		StartTransactionCommand();
	}
	PG_END_TRY();


	/* Post process if there's an error to massage the message */
	if (dropIndexResult->errcode == ERRCODE_INSUFFICIENT_PRIVILEGE)
	{
		ereport(LOG, (errmsg(
						  "Insufficient privileges to execute drop indexes - inner error %s",
						  dropIndexResult->errmsg)));
		dropIndexResult->errmsg = "Insufficient privileges to execute drop indexes";
	}

	return dropIndexResult;
}


/*
 * ParseDropIndexesArg returns a DropIndexesArg object by parsing given
 * pgbson object that represents the "arg" document passed to
 * dbCommand/dropIndexes.
 */
static DropIndexesArg
ParseDropIndexesArg(pgbson *arg, Datum *databaseNameDatum)
{
	DropIndexesArg dropIndexesArg = { 0 };

	/*
	 * Distinguish "index: []" from not specifying "index" field at all.
	 * While the former one is ok, the later one is not.
	 */
	bool gotIndexObject = false;

	bson_iter_t argIter;
	PgbsonInitIterator(arg, &argIter);
	while (bson_iter_next(&argIter))
	{
		/*
		 * we don't throw an error if we encounter with
		 * definition of a field more than once.
		 *
		 * TODO: We should also not throw an error if there is a syntax error
		 *       in the preceding definitions. That means, even if the first
		 *       definiton of "index" uses an integer, we should not throw an
		 *       error for the following, since later definition is valid:
		 *       {"index": 1, "index": "my_index_name"}
		 */

		const char *argKey = bson_iter_key(&argIter);

		/*
		 * The "deleteIndexes" command is deprecated but still accepted as an alias for "dropIndexes".
		 * The input and error handling are consistent with "dropIndexes", so no special handling is required.
		 */
		if (strcmp(argKey, "dropIndexes") == 0 ||
			strcmp(argKey, "deleteIndexes") == 0)
		{
			EnsureTopLevelFieldType("dropIndexes.dropIndexes", &argIter, BSON_TYPE_UTF8);

			const bson_value_t *dropIndexesVal = bson_iter_value(&argIter);
			dropIndexesArg.collectionName = pstrdup(dropIndexesVal->value.v_utf8.str);
		}
		else if (strcmp(argKey, "index") == 0)
		{
			gotIndexObject = true;
			dropIndexesArg.dropIndexMode = DROP_INDEX_MODE_INVALID;

			if (BSON_ITER_HOLDS_UTF8(&argIter))
			{
				const bson_value_t *indexVal = bson_iter_value(&argIter);
				dropIndexesArg.index.nameList =
					list_make1(pstrdup(indexVal->value.v_utf8.str));
				dropIndexesArg.dropIndexMode = DROP_INDEX_BY_NAME_LIST;
			}
			else if (BSON_ITER_HOLDS_DOCUMENT(&argIter))
			{
				dropIndexesArg.index.document = PgbsonInitFromIterDocumentValue(&argIter);
				dropIndexesArg.dropIndexMode = DROP_INDEX_BY_SPEC_DOCUMENT;
			}
			else if (BSON_ITER_HOLDS_ARRAY(&argIter))
			{
				dropIndexesArg.index.nameList = NIL;
				dropIndexesArg.dropIndexMode = DROP_INDEX_BY_NAME_LIST;

				bson_iter_t indexArrayIter;
				bson_iter_recurse(&argIter, &indexArrayIter);
				while (bson_iter_next(&indexArrayIter))
				{
					EnsureTopLevelFieldType("dropIndexes.index.item", &indexArrayIter,
											BSON_TYPE_UTF8);

					const bson_value_t *indexArrayVal = bson_iter_value(&indexArrayIter);
					dropIndexesArg.index.nameList =
						lappend(dropIndexesArg.index.nameList,
								pstrdup(indexArrayVal->value.v_utf8.str));
				}
			}
			else
			{
				ThrowTopLevelTypeMismatchError("dropIndexes.index",
											   BsonIterTypeName(&argIter),
											   "[string, object]");
			}
		}
		else if (strcmp(argKey, "$db") == 0)
		{
			ValidateOrExtractDatabaseNameFromSpec(&argIter, databaseNameDatum);
		}
		else if (IsCommonSpecIgnoredField(argKey))
		{
			elog(DEBUG1, "Command field not recognized: dropIndexes.%s", argKey);

			/*
			 *  Silently ignore now, so that clients don't break
			 *  TODO: implement me
			 *      writeConcern
			 *      comment
			 */
		}
		else
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg(
								"The BSON field 'dropIndexes.%s' is not recognized as a valid field.",
								argKey)));
		}
	}

	/* verify that all non-optional fields are given */

	if (*databaseNameDatum == (Datum) 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
						errmsg("$db cannot be NULL")));
	}

	if (dropIndexesArg.collectionName == NULL)
	{
		ThrowTopLevelMissingFieldError("dropIndexes.dropIndexes");
	}

	if (!gotIndexObject)
	{
		ThrowTopLevelMissingFieldError("dropIndexes.index");
	}
	else if (dropIndexesArg.dropIndexMode == DROP_INDEX_BY_NAME_LIST &&
			 list_length(dropIndexesArg.index.nameList) == 0)
	{
		/* Passing an empty array is allowed, so this is ok */
	}

	return dropIndexesArg;
}


/*
 * DropIndexesArgExpandIndexNameList rewrites index.indexNameList so
 * that it contains names of all the indexes that given collection has
 * (except _id) if first element of the list is "*".
 *
 * However, if "*" is placed in a different position, then we won't do so.
 * This is because, in that case we should throw an error complaining that
 * there is no such index since it's already not allowed to define an index
 * with name "*".
 */
static void
DropIndexesArgExpandIndexNameList(uint64 collectionId, DropIndexesArg *dropIndexesArg)
{
	Assert(dropIndexesArg->dropIndexMode == DROP_INDEX_BY_NAME_LIST);

	if (list_length(dropIndexesArg->index.nameList) != 0 &&
		strcmp(linitial(dropIndexesArg->index.nameList), "*") == 0)
	{
		/* Dropping the _id index is not permitted */
		bool excludeIdIndex = true;
		bool inProgressOnly = false;
		dropIndexesArg->index.nameList = CollectionIdGetIndexNames(collectionId,
																   excludeIdIndex,
																   inProgressOnly);
	}
}


/*
 * DropPostgresIndex drops the index with indexId.
 *
 * If `unique` is true, we check whether the physical index is actually backed
 * by a constraint (exclusion or primary key). If it is, we use ALTER TABLE
 * DROP CONSTRAINT via SPI. If not (e.g., a background unique index build that
 * failed before registering the constraint), we use DROP INDEX via the same
 * path as non-unique indexes.
 *
 * This mirrors the logic in DropPostgresIndexWithSuffix.
 */
void
DropPostgresIndex(uint64 collectionId, int indexId, bool unique, bool concurrently,
				  bool forceReadWrite, bool missingOk)
{
	bool isConstraint = false;

	if (unique)
	{
		char indexName[NAMEDATALEN] = { 0 };
		pg_sprintf(indexName, DOCUMENT_DATA_TABLE_INDEX_NAME_FORMAT, indexId);
		Oid indexOid = get_relname_relid(indexName, ApiDataNamespaceOid());

		if (OidIsValid(indexOid))
		{
			isConstraint = OidIsValid(get_index_constraint(indexOid));
		}
		else
		{
			if (missingOk)
			{
				/* Index doesn't exist — nothing to drop */
				return;
			}

			ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT),
							errmsg("index with id %d does not exist for "
								   "collection " UINT64_FORMAT, indexId, collectionId)));
		}
	}

	char *cmd = CreateDropIndexCommand(collectionId, indexId, isConstraint,
									   concurrently, missingOk);

	/*
	 * Drop statistics before the index so that the stats object still exists on
	 * the coordinator when the DROP STATISTICS command runs. This lets Citus
	 * propagate the drop to shard tables. If we dropped after the index,
	 * DEPENDENCY_AUTO would silently remove the coordinator stats first, making
	 * the explicit DROP STATISTICS a no-op that Citus cannot map to shards.
	 */
	DropIndexStatisticsForPlannerStatistics(collectionId, list_make1_int(indexId));

	ExecuteDropIndexCommand(cmd, isConstraint, concurrently, forceReadWrite);
}


/*
 * Drop indexes where names can have suffix and also considers default id index for drop
 *
 * Callers should be careful not to drop valid index, once an index is identifies as invalid
 * then only this should be called
 *
 * Currenlty used by re-index
 */
void
DropPostgresIndexWithSuffix(uint64 collectionId, IndexDetails *index, bool concurrently,
							bool missingOk, const char *suffix)
{
	Assert(suffix != NULL);

	StringInfo cmdStr = makeStringInfo();

	/*
	 * Determine the physical index name based on whether this is the _id index
	 * or a regular index. We use the index spec name rather than probing the
	 * catalog to avoid a race where a concurrent drop could cause us to
	 * accidentally resolve and drop the real primary key constraint of another
	 * collection's _id index.
	 */
	bool isIdIndex = (strncmp(index->indexSpec.indexName,
							  ID_INDEX_NAME, strlen(ID_INDEX_NAME)) == 0);

	char indexName[NAMEDATALEN] = { 0 };
	if (isIdIndex)
	{
		pg_sprintf(indexName, DOCUMENT_DATA_PRIMARY_KEY_FORMAT_PREFIX UINT64_FORMAT "%s",
				   collectionId, suffix);
	}
	else
	{
		pg_sprintf(indexName, DOCUMENT_DATA_TABLE_INDEX_NAME_FORMAT "%s",
				   index->indexId, suffix);
	}

	Oid indexOid = get_relname_relid(indexName, ApiDataNamespaceOid());
	if (!OidIsValid(indexOid))
	{
		if (!missingOk)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INTERNALERROR),
							errmsg(
								"Unexpected index \"%s\" does not exist when trying to drop it.",
								indexName)));
		}

		return;
	}

	/*
	 * Check whether the physical index is backed by a constraint. This is
	 * always true for _id indexes and can also be true for unique indexes
	 * (including background unique index builds that failed validation but
	 * already registered the constraint).
	 */
	bool isConstraint = isIdIndex || OidIsValid(get_index_constraint(indexOid));

	if (isConstraint)
	{
		appendStringInfo(cmdStr,
						 "ALTER TABLE %s." DOCUMENT_DATA_TABLE_NAME_FORMAT
						 " DROP CONSTRAINT %s %s", ApiDataSchemaName, collectionId,
						 missingOk ? "IF EXISTS" : "", indexName);

		bool isNull = true;
		bool readOnly = false;
		ExtensionExecuteQueryViaSPI(cmdStr->data, readOnly, SPI_OK_UTILITY, &isNull);
		Assert(isNull);
	}
	else
	{
		/* These are indexes */
		appendStringInfo(cmdStr,
						 "DROP INDEX %s %s %s.%s%s",
						 concurrently ? "CONCURRENTLY" : "",
						 missingOk ? "IF EXISTS" : "", ApiDataSchemaName, indexName,
						 concurrently ? "" : " CASCADE");

		if (concurrently)
		{
			ExtensionExecuteQueryOnLocalhostViaLibPQ(cmdStr->data);
		}
		else
		{
			bool isNull = true;
			bool readOnly = false;
			ExtensionExecuteQueryViaSPI(cmdStr->data, readOnly, SPI_OK_UTILITY, &isNull);
			Assert(isNull);
		}
	}
}


/*
 * MakeDropIndexesMsg returns a bson object that encapsulates given
 * DropIndexesResult object.
 */
static pgbson *
MakeDropIndexesMsg(DropIndexesResult *result)
{
	pgbson_writer writer;
	PgbsonWriterInit(&writer);
	PgbsonWriterAppendBool(&writer, "ok", strlen("ok"), result->ok);

	if (result->ok)
	{
		PgbsonWriterAppendInt64(&writer, "nIndexesWas", strlen("nIndexesWas"),
								result->nIndexesWas);
	}
	else
	{
		PgbsonWriterAppendUtf8(&writer, "errmsg", strlen("errmsg"), result->errmsg);
		PgbsonWriterAppendInt32(&writer, "code", strlen("code"), result->errcode);
	}
	return PgbsonWriterGetPgbson(&writer);
}


/*
 * Prepares Postgres Drop Index command based on the parameters like unique, concurrently.
 */
static char *
CreateDropIndexCommand(uint64 collectionId, int indexId, bool unique, bool concurrently,
					   bool missingOk)
{
	StringInfo cmdStr = makeStringInfo();

	if (unique)
	{
		appendStringInfo(cmdStr,
						 "ALTER TABLE %s." DOCUMENT_DATA_TABLE_NAME_FORMAT
						 " DROP CONSTRAINT %s " DOCUMENT_DATA_TABLE_INDEX_NAME_FORMAT
						 " CASCADE",
						 ApiDataSchemaName, collectionId, missingOk ? "IF EXISTS" : "",
						 indexId);
	}
	else
	{
		appendStringInfo(cmdStr,
						 "DROP INDEX %s %s %s."
						 DOCUMENT_DATA_TABLE_INDEX_NAME_FORMAT " %s",
						 concurrently ? "CONCURRENTLY" : "",
						 missingOk ? "IF EXISTS" : "", ApiDataSchemaName, indexId,
						 concurrently ? "" : "CASCADE");
	}
	return cmdStr->data;
}


/*
 * Actually executes DROP INDEX postgres command.
 */
static void
ExecuteDropIndexCommand(char *cmd, bool unique, bool concurrently, bool forceReadWrite)
{
	if (unique)
	{
		bool isNull = true;
		bool readOnly = false;
		ExtensionExecuteQueryViaSPI(cmd, readOnly, SPI_OK_UTILITY, &isNull);
		Assert(isNull);
	}
	else
	{
		if (concurrently)
		{
			if (forceReadWrite)
			{
				ExtensionExecuteForcedReadWriteQueryOnLocalhostViaLibPQ(cmd);
			}
			else
			{
				ExtensionExecuteQueryOnLocalhostViaLibPQ(cmd);
			}
		}
		else
		{
			bool isNull = true;
			bool readOnly = false;
			ExtensionExecuteQueryViaSPI(cmd, readOnly, SPI_OK_UTILITY, &isNull);
			Assert(isNull);
		}
	}
}


/*
 * HandleDropIndexConcurrently handles dropping index with concurrently
 */
static void
HandleDropIndexConcurrently(uint64 collectionId, int indexId, bool unique, bool
							concurrently, bool missingOk, DropIndexesResult *result,
							MemoryContext oldMemContext)
{
	CancelIndexBuildRequest(indexId);

	/*
	 * We commit the transaction to prevent concurrent index creation
	 * getting blocked on that transaction due to any snapshots that
	 * we might have grabbed so far.
	 */
	PopAllActiveSnapshots();
	CommitTransactionCommand();
	StartTransactionCommand();

	/* declared volatile because of the longjmp in PG_CATCH */
	volatile bool indexDropped = false;

	/*
	 * Drop statistics before the index so that the stats object still
	 * exists on the coordinator when the DROP STATISTICS command runs.
	 * This lets Citus propagate the drop to shard tables.
	 *
	 * We must commit after dropping stats so that the locks acquired by
	 * the DROP STATISTICS (and its Citus propagation to shards) are
	 * released before the DROP INDEX CONCURRENTLY runs via a separate
	 * libpq connection. Otherwise the libpq connection blocks waiting
	 * for the locks held by this transaction, causing a hang.
	 *
	 * If the stats drop fails we must NOT proceed with the index drop,
	 * otherwise the index would be removed while its statistics remain,
	 * leaving stale stats behind.
	 */
	volatile bool statsDropped = false;
	PG_TRY();
	{
		DropIndexStatisticsForPlannerStatistics(collectionId, list_make1_int(indexId));
		PopAllActiveSnapshots();
		CommitTransactionCommand();
		StartTransactionCommand();
		statsDropped = true;
	}
	PG_CATCH();
	{
		MemoryContextSwitchTo(oldMemContext);
		ErrorData *edata = CopyErrorDataAndFlush();
		result->errcode = edata->sqlerrcode;
		result->errmsg = edata->message;
		result->ok = false;

		ereport(DEBUG1, (errmsg(
							 "Failed to drop statistics for index %d on collection "
							 UINT64_FORMAT, indexId, collectionId)));

		PopAllActiveSnapshots();
		AbortCurrentTransaction();
		StartTransactionCommand();
	}
	PG_END_TRY();

	if (!statsDropped)
	{
		return;
	}

	PG_TRY();
	{
		char *cmd = CreateDropIndexCommand(collectionId, indexId, unique, concurrently,
										   missingOk);

		bool forceReadWrite = false;
		ExecuteDropIndexCommand(cmd, unique, concurrently, forceReadWrite);

		indexDropped = true;
	}
	PG_CATCH();
	{
		/* save error info into right context */
		MemoryContextSwitchTo(oldMemContext);
		ErrorData *edata = CopyErrorDataAndFlush();
		result->errcode = edata->sqlerrcode;
		result->errmsg = edata->message;
		result->ok = false;

		ereport(DEBUG1, (errmsg(
							 "Failed to remove certain indexes from the specified collection")));

		PopAllActiveSnapshots();
		AbortCurrentTransaction();
		StartTransactionCommand();
	}
	PG_END_TRY();

	if (indexDropped)
	{
		DeleteCollectionIndexRecord(collectionId, indexId);

		/* Do not delete request from the queue (via RemoveRequestFromIndexQueue inside drop_indexes_concurrently) because check_build_index_status can think that request is finished successfully
		 * since it does not exist in the queue. Instead we have already marked the request Skippable before coming to
		 * HandleDropIndexConcurrently, and cron-job will evict Skippable request eventually after 20 mins (IndexQueueEvictionIntervalInSec) */
	}
}


/*
 * CancelIndexBuildRequest cancels an existing (if there is any) running Index Build request for the indexId.
 */
static void
CancelIndexBuildRequest(int indexId)
{
	char *cancelIndexBuildRequestQueryStr = NULL;
	cancelIndexBuildRequestQueryStr = TryGetCancelIndexBuildQuery(indexId,
																  CREATE_INDEX_COMMAND_TYPE);
	if (cancelIndexBuildRequestQueryStr == NULL)
	{
		StringInfo cmdStr = makeStringInfo();
		appendStringInfo(cmdStr,
						 "SELECT iq.global_pid AS pid, iq.start_time AS timestamp");
		appendStringInfo(cmdStr,
						 " FROM %s iq WHERE index_id = %d",
						 GetIndexQueueName(), indexId);

		cancelIndexBuildRequestQueryStr = cmdStr->data;
	}

	bool readOnly = true;
	int numValues = 2;
	bool isNull[2];
	Datum results[2];
	ExtensionExecuteMultiValueQueryViaSPI(cancelIndexBuildRequestQueryStr, readOnly,
										  SPI_OK_SELECT, results,
										  isNull, numValues);

	if (!isNull[0])
	{
		StringInfo cancelCmdStr = makeStringInfo();
		Datum pid = results[0];
		Datum startTime = results[1];
		appendStringInfo(cancelCmdStr,
						 " SELECT pg_cancel_backend(pid) FROM pg_stat_activity ");
		appendStringInfo(cancelCmdStr, " WHERE pid = $1 AND query_start = $2");

		int nargs = 2;
		Oid argTypes[2] = { INT4OID, TIMESTAMPTZOID };
		Datum argValues[2] = {
			pid, startTime
		};

		const char **parameterValues = (const char **) palloc(nargs * sizeof(char *));

		/* TODO Now we also have a provision to cancel create index only when authenticated user is same as user who has submitted the create index request if(GetAuthenticatedUserId == user_oid)*/
		for (int i = 0; i < nargs; i++)
		{
			bool isVarlen;
			Oid funcOutOid;
			getTypeOutputInfo(argTypes[i], &funcOutOid, &isVarlen);
			parameterValues[i] = OidOutputFunctionCall(funcOutOid, argValues[i]);
		}

		/* Run pg_cancel_backend as super user via libpq otherwise we will not be able to cancel cron-job (launched as super user) */
		ExtensionExecuteQueryWithArgsAsUserOnLocalhostViaLibPQ(cancelCmdStr->data,
															   DocumentDBApiExtensionOwner(),
															   nargs, argTypes,
															   parameterValues);

		pfree(parameterValues);
	}

	elog(LOG, "Cancel existing index build request for %d is completed.",
		 indexId);
}
