package backup

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	path "path/filepath"
	"runtime/debug"
	"strings"
	"sync"
	"time"

	"github.com/apache/cloudberry-backup/filepath"
	"github.com/apache/cloudberry-backup/history"
	"github.com/apache/cloudberry-backup/options"
	"github.com/apache/cloudberry-backup/report"
	"github.com/apache/cloudberry-backup/toc"
	"github.com/apache/cloudberry-backup/utils"
	"github.com/apache/cloudberry-go-libs/cluster"
	"github.com/apache/cloudberry-go-libs/dbconn"
	"github.com/apache/cloudberry-go-libs/gplog"
	"github.com/apache/cloudberry-go-libs/operating"
	"github.com/spf13/cobra"
)

// This function handles setup that can be done before parsing flags.
func DoInit(cmd *cobra.Command) {
	CleanupGroup = &sync.WaitGroup{}
	CleanupGroup.Add(1)
	gplog.InitializeLogging("gpbackup", "")
	SetCmdFlags(cmd.Flags())
	_ = cmd.MarkFlagRequired(options.DBNAME)
	utils.InitializeSignalHandler(DoCleanup, "backup process", &wasTerminated)
	objectCounts = make(map[string]int)
}

func DoFlagValidation(cmd *cobra.Command) {
	validateFlagCombinations(cmd.Flags())
	validateFlagValues()
}

// This function handles setup that must be done after parsing flags.
func DoSetup() {
	SetLoggerVerbosity()
	gplog.Verbose("Backup Command: %s", os.Args)
	gplog.Info("gpbackup version = %s", GetVersion())

	utils.CheckGpexpandRunning(utils.BackupPreventedByGpexpandMessage)
	timestamp := history.CurrentTimestamp()
	createBackupLockFile(timestamp)
	initializeConnectionPool(timestamp)
	gplog.Info("Apache Cloudberry Version = %s", connectionPool.Version.VersionString)

	gplog.Info("Starting backup of database %s", MustGetFlagString(options.DBNAME))
	opts, err := options.NewOptions(cmdFlags)
	gplog.FatalOnError(err)

	ValidateAndProcessFilterLists(opts)
	includeOids := GetOidsFromRelationList(IncludedRelationFqns)
	err = ExpandIncludesForPartitions(connectionPool, opts, includeOids, cmdFlags)
	gplog.FatalOnError(err)

	clusterConfigConn := dbconn.NewDBConnFromEnvironment(MustGetFlagString(options.DBNAME))
	clusterConfigConn.MustConnect(1)

	segConfig := cluster.MustGetSegmentConfiguration(clusterConfigConn)
	globalCluster = cluster.NewCluster(segConfig)
	segPrefix := ""
	if !MustGetFlagBool(options.SINGLE_BACKUP_DIR) {
		segPrefix = filepath.GetSegPrefix(clusterConfigConn)
	}
	clusterConfigConn.Close()

	globalFPInfo = filepath.NewFilePathInfo(globalCluster, MustGetFlagString(options.BACKUP_DIR), timestamp, segPrefix, MustGetFlagBool(options.SINGLE_BACKUP_DIR))
	if MustGetFlagBool(options.METADATA_ONLY) {
		_, err = globalCluster.ExecuteLocalCommand(fmt.Sprintf("mkdir -p %s", globalFPInfo.GetDirForContent(-1)))
		gplog.FatalOnError(err)
	} else {
		createBackupDirectoriesOnAllHosts()
	}
	globalTOC = &toc.TOC{}
	globalTOC.InitializeMetadataEntryMap()
	utils.InitializePipeThroughParameters(!MustGetFlagBool(options.NO_COMPRESSION), MustGetFlagString(options.COMPRESSION_TYPE), MustGetFlagInt(options.COMPRESSION_LEVEL))
	getQuotedRoleNames(connectionPool)

	pluginConfigFlag := MustGetFlagString(options.PLUGIN_CONFIG)

	if pluginConfigFlag != "" {
		pluginConfig, err = utils.ReadPluginConfig(pluginConfigFlag)
		gplog.FatalOnError(err)
		configFilename := path.Base(pluginConfig.ConfigPath)
		configDirname := path.Dir(pluginConfig.ConfigPath)
		pluginConfig.ConfigPath = path.Join(configDirname, timestamp+"_"+configFilename)
		_ = cmdFlags.Set(options.PLUGIN_CONFIG, pluginConfig.ConfigPath)
		gplog.Debug("Plugin config path: %s", pluginConfig.ConfigPath)
	}

	initializeBackupReport(*opts)

	if pluginConfigFlag != "" {
		backupReport.PluginVersion = pluginConfig.CheckPluginExistsOnAllHosts(globalCluster)
		pluginConfig.CopyPluginConfigToAllHosts(globalCluster)
		pluginConfig.SetupPluginForBackup(globalCluster, globalFPInfo)
	}
}

func DoBackup() {
	gplog.Info("Backup Timestamp = %s", globalFPInfo.Timestamp)
	gplog.Info("Backup Database = %s", connectionPool.DBName)
	gplog.Verbose("Backup Parameters: {%s}", strings.ReplaceAll(backupReport.BackupParamsString, "\n", ", "))

	pluginConfigFlag := MustGetFlagString(options.PLUGIN_CONFIG)
	targetBackupTimestamp := ""
	var targetBackupFPInfo filepath.FilePathInfo
	if MustGetFlagBool(options.INCREMENTAL) {
		targetBackupTimestamp = GetTargetBackupTimestamp()

		targetBackupFPInfo = filepath.NewFilePathInfo(globalCluster, globalFPInfo.UserSpecifiedBackupDir,
			targetBackupTimestamp, globalFPInfo.UserSpecifiedSegPrefix, globalFPInfo.SingleBackupDir)

		if pluginConfigFlag != "" {
			// These files need to be downloaded from the remote system into the local filesystem
			pluginConfig.MustRestoreFile(targetBackupFPInfo.GetConfigFilePath())
			pluginConfig.MustRestoreFile(targetBackupFPInfo.GetTOCFilePath())
			pluginConfig.MustRestoreFile(targetBackupFPInfo.GetPluginConfigPath())
		}
	}

	gplog.Info("Gathering table state information")
	metadataTables, dataTables := RetrieveAndProcessTables()
	dataTables, numExtOrForeignTables := GetBackupDataSet(dataTables)
	if len(dataTables) == 0 && !backupReport.MetadataOnly {
		gplog.Warn("No tables in backup set contain data. Performing metadata-only backup instead.")
		backupReport.MetadataOnly = true
	}
	// This must be a full backup with --leaf-parition-data to query for incremental metadata
	if !(MustGetFlagBool(options.METADATA_ONLY) || MustGetFlagBool(options.DATA_ONLY)) && MustGetFlagBool(options.LEAF_PARTITION_DATA) {
		backupIncrementalMetadata()
	} else {
		gplog.Verbose("Skipping query for incremental metadata.")
	}

	metadataFilename := globalFPInfo.GetMetadataFilePath()
	gplog.Info("Metadata will be written to %s", metadataFilename)
	metadataFile := utils.NewFileWithByteCountFromFile(metadataFilename)

	/*
	 * We check this in the backup report rather than the flag because we
	 * perform a metadata only backup if the database contains no tables
	 * or only external tables
	 */
	backupSetTables := dataTables
	if !backupReport.MetadataOnly {
		targetBackupRestorePlan := make([]history.RestorePlanEntry, 0)
		if targetBackupTimestamp != "" {
			gplog.Info("Basing incremental backup off of backup with timestamp = %s", targetBackupTimestamp)

			targetBackupTOC := toc.NewTOC(targetBackupFPInfo.GetTOCFilePath())
			targetBackupRestorePlan = history.ReadConfigFile(targetBackupFPInfo.GetConfigFilePath()).RestorePlan
			backupSetTables = FilterTablesForIncremental(targetBackupTOC, globalTOC, dataTables)
		}

		backupReport.RestorePlan = PopulateRestorePlan(backupSetTables, targetBackupRestorePlan, dataTables)
	}

	// As soon as all necessary data is available, capture the backup into history database
	if !MustGetFlagBool(options.NO_HISTORY) {
		historyDBName := globalFPInfo.GetBackupHistoryDatabasePath()
		historyDB, err := history.InitializeHistoryDatabase(historyDBName)
		if err != nil {
			gplog.FatalOnError(err)
		} else {
			err = history.StoreBackupHistory(historyDB, &backupReport.BackupConfig)
			historyDB.Close()
			gplog.FatalOnError(err)
		}
	}

	backupSessionGUC(metadataFile)
	if !MustGetFlagBool(options.DATA_ONLY) {
		isFullBackup := len(MustGetFlagStringArray(options.INCLUDE_RELATION)) == 0
		if isFullBackup && !MustGetFlagBool(options.WITHOUT_GLOBALS) {
			backupGlobals(metadataFile)
		}

		isFilteredBackup := !isFullBackup
		backupPredata(metadataFile, metadataTables, isFilteredBackup)
		backupPostdata(metadataFile)
	}

	if !backupReport.MetadataOnly {
		backupData(backupSetTables)
	}

	printDataBackupWarnings(numExtOrForeignTables)
	if MustGetFlagBool(options.WITH_STATS) {
		backupStatistics(metadataTables)
	}

	globalTOC.WriteToFileAndMakeReadOnly(globalFPInfo.GetTOCFilePath())
	for connNum := 0; connNum < connectionPool.NumConns; connNum++ {
		// COMMIT TRANSACTION
		// The transaction could have been rollbacked already
		// during COPY step due to deadlock handling.
		if connectionPool.Tx[connNum] != nil {
			connectionPool.MustCommit(connNum)
		}
	}
	metadataFile.Close()
	if pluginConfigFlag != "" {
		pluginConfig.MustBackupFile(metadataFilename)
		pluginConfig.MustBackupFile(globalFPInfo.GetTOCFilePath())
		if MustGetFlagBool(options.WITH_STATS) {
			pluginConfig.MustBackupFile(globalFPInfo.GetStatisticsFilePath())
		}
		_ = utils.CopyFile(pluginConfigFlag, globalFPInfo.GetPluginConfigPath())
		pluginConfig.MustBackupFile(globalFPInfo.GetPluginConfigPath())
	}
}

func backupGlobals(metadataFile *utils.FileWithByteCount) {
	gplog.Info("Writing global database metadata")

	backupResourceQueues(metadataFile)
	backupResourceGroups(metadataFile)
	backupRoles(metadataFile)
	backupRoleGrants(metadataFile)
	backupTablespaces(metadataFile)
	backupCreateDatabase(metadataFile)
	backupDatabaseGUCs(metadataFile)
	backupRoleGUCs(metadataFile)

	if connectionPool.Version.IsCBDB() {
		backupStorageServers(metadataFile)
		backupStorageUserMappings(metadataFile)
	}

	logCompletionMessage("Global database metadata backup")
}

func backupPredata(metadataFile *utils.FileWithByteCount, tables []Table, tableOnly bool) {
	if wasTerminated {
		return
	}
	gplog.Info("Writing pre-data metadata")

	var protocols []ExternalProtocol
	var functions []Function
	var funcInfoMap map[uint32]FunctionInfo
	objects := make([]Sortable, 0)
	metadataMap := make(MetadataMap)

	if !tableOnly {
		functions, funcInfoMap = retrieveFunctions(&objects, metadataMap)
	}
	objects = append(objects, convertToSortableSlice(tables)...)
	relationMetadata := GetMetadataForObjectType(connectionPool, TYPE_RELATION)
	addToMetadataMap(relationMetadata, metadataMap)

	if !tableOnly {
		protocols = retrieveProtocols(&objects, metadataMap)
		backupSchemas(metadataFile, createAlteredPartitionSchemaSet(tables))
		backupExtensions(metadataFile)
		backupCollations(metadataFile)
		retrieveAndBackupTypes(metadataFile, &objects, metadataMap)

		if len(MustGetFlagStringArray(options.INCLUDE_SCHEMA)) == 0 {
			backupProceduralLanguages(metadataFile, functions, funcInfoMap, metadataMap)
			retrieveTransforms(&objects)
			retrieveFDWObjects(&objects, metadataMap)
		}

		retrieveTSObjects(&objects, metadataMap)
		backupOperatorFamilies(metadataFile)
		retrieveOperatorObjects(&objects, metadataMap)
		retrieveAggregates(&objects, metadataMap)
		retrieveCasts(&objects, metadataMap)
		backupAccessMethods(metadataFile)
	}

	retrieveViews(&objects)
	sequences := retrieveAndBackupSequences(metadataFile, relationMetadata)
	domainConstraints, nonDomainConstraints, conMetadata := retrieveConstraints(&objects, metadataMap)

	viewsDependingOnConstraints := backupDependentObjects(metadataFile, tables, protocols, metadataMap, domainConstraints, objects, sequences, funcInfoMap, tableOnly)

	backupConversions(metadataFile)

	// These two are actually in postdata, but we print them here to avoid passing information around too much
	backupConstraints(metadataFile, nonDomainConstraints, conMetadata)
	backupViewsDependingOnConstraints(metadataFile, viewsDependingOnConstraints)

	logCompletionMessage("Pre-data metadata metadata backup")
}

func backupData(tables []Table) {
	if wasTerminated {
		return
	}
	if len(tables) == 0 {
		// No incremental data changes to backup
		gplog.Info("No tables to backup")
		gplog.Info("Data backup complete")
		return
	}
	if MustGetFlagBool(options.SINGLE_DATA_FILE) {
		gplog.Verbose("Initializing pipes and gpbackup_helper on segments for single data file backup")
		utils.VerifyHelperVersionOnSegments(version, globalCluster)
		oidList := make([]string, 0, len(tables))
		for _, table := range tables {
			oidList = append(oidList, fmt.Sprintf("%d", table.Oid))
		}
		utils.WriteOidListToSegments(oidList, globalCluster, globalFPInfo, "oid")
		compressStr := fmt.Sprintf(" --compression-level %d --compression-type %s", MustGetFlagInt(options.COMPRESSION_LEVEL), MustGetFlagString(options.COMPRESSION_TYPE))
		if MustGetFlagBool(options.NO_COMPRESSION) {
			compressStr = " --compression-level 0"
		}
		initialPipes := CreateInitialSegmentPipes(oidList, globalCluster, connectionPool, globalFPInfo)
		// Do not pass through the --on-error-continue flag or the resizeClusterMap because neither apply to gpbackup
		utils.StartGpbackupHelpers(globalCluster, globalFPInfo, "--backup-agent",
			MustGetFlagString(options.PLUGIN_CONFIG), compressStr, false, false, &wasTerminated, initialPipes, true, false, 0, 0, gplog.GetVerbosity())
	}
	gplog.Info("Writing data to file")
	rowsCopiedMaps := BackupDataForAllTables(tables)
	AddTableDataEntriesToTOC(tables, rowsCopiedMaps)
	if MustGetFlagBool(options.SINGLE_DATA_FILE) && MustGetFlagString(options.PLUGIN_CONFIG) != "" {
		pluginConfig.BackupSegmentTOCs(globalCluster, globalFPInfo)
	}
	logCompletionMessage("Data backup")
}

func backupPostdata(metadataFile *utils.FileWithByteCount) {
	if wasTerminated {
		return
	}
	gplog.Info("Writing post-data metadata")

	backupIndexes(metadataFile)
	backupRules(metadataFile)
	backupTriggers(metadataFile)
	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("6")) || connectionPool.Version.IsCBDB() {
		backupDefaultPrivileges(metadataFile)
		if len(MustGetFlagStringArray(options.INCLUDE_SCHEMA)) == 0 {
			backupEventTriggers(metadataFile)
		}
	}
	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() {
		backupRowLevelSecurityPolicies(metadataFile)
		backupExtendedStatistic(metadataFile)
	}

	logCompletionMessage("Post-data metadata backup")
}

func backupStatistics(tables []Table) {
	if wasTerminated {
		return
	}
	statisticsFilename := globalFPInfo.GetStatisticsFilePath()
	gplog.Info("Writing query planner statistics to %s", statisticsFilename)
	statisticsFile := utils.NewFileWithByteCountFromFile(statisticsFilename)
	defer statisticsFile.Close()
	backupTableStatistics(statisticsFile, tables)

	logCompletionMessage("Query planner statistics backup")
}

func DoTeardown() {
	backupFailed := false
	defer func() {
		// If the backup was terminated, the signal handler will handle cleanup
		if wasTerminated {
			CleanupGroup.Wait()
		} else {
			DoCleanup(backupFailed)
		}

		errorCode := gplog.GetErrorCode()
		if errorCode == 0 {
			gplog.Info("Backup completed successfully")
		}
		os.Exit(errorCode)
	}()

	errStr := ""
	if err := recover(); err != nil {
		// gplog's Fatal will cause a panic with error code 2
		if gplog.GetErrorCode() != 2 {
			gplog.Error(fmt.Sprintf("%v: %s", err, debug.Stack()))
			gplog.SetErrorCode(2)
		} else {
			errStr = fmt.Sprintf("%v", err)
		}
		backupFailed = true
	}
	if wasTerminated {
		/*
		 * Don't print an error or create a report file if the backup was canceled,
		 * as the signal handler will take care of cleanup and return codes.  Just
		 * wait until the signal handler's DoCleanup completes so the main goroutine
		 * doesn't exit while cleanup is still in progress.
		 */
		CleanupGroup.Wait()
		backupFailed = true
		return
	}
	if errStr != "" {
		fmt.Println(errStr)
	}
	errMsg := report.ParseErrorMessage(errStr)

	/*
	 * Only create a report file if we fail after the cluster is initialized
	 * and a backup directory exists in which to create the report file.
	 */
	if globalFPInfo.Timestamp != "" {
		_, statErr := os.Stat(globalFPInfo.GetDirForContent(-1))
		if statErr != nil { // Even if this isn't os.IsNotExist, don't try to write a report file in case of further errors
			return
		}
		historyFileLegacyName := globalFPInfo.GetBackupHistoryFilePath()
		reportFilename := globalFPInfo.GetBackupReportFilePath()
		configFilename := globalFPInfo.GetConfigFilePath()

		time.Sleep(time.Second) // We sleep for 1 second to ensure multiple backups do not start within the same second.

		// Check if legacy history file is still present, log warning if so. Only log if we're planning to use history db.
		var err error
		if _, err = os.Stat(historyFileLegacyName); err == nil && !MustGetFlagBool(options.NO_HISTORY) {
			gplog.Warn("Legacy gpbackup_history file %s is still present. Please run 'gpbackup_manager migrate-history' to add entries from that file to the history database.", historyFileLegacyName)
		}

		if backupReport != nil {
			if backupFailed {
				backupReport.BackupConfig.Status = history.BackupStatusFailed
			} else {
				backupReport.BackupConfig.Status = history.BackupStatusSucceed
			}
			backupReport.ConstructBackupParamsString()

			history.WriteConfigFile(&backupReport.BackupConfig, configFilename)
			// We always want to override the initial end time set by the call to StoreBackupHistory
			backupReport.BackupConfig.EndTime = history.CurrentTimestamp()
			endtime, _ := time.ParseInLocation("20060102150405", backupReport.BackupConfig.EndTime, operating.System.Local)
			backupReport.WriteBackupReportFile(reportFilename, globalFPInfo.Timestamp, endtime, objectCounts, errMsg)
			report.EmailReport(globalCluster, globalFPInfo.Timestamp, reportFilename, "gpbackup", !backupFailed, backupReport.BackupConfig.DatabaseName)
			if pluginConfig != nil {
				err = pluginConfig.BackupFile(configFilename)
				if err != nil {
					gplog.Error(fmt.Sprintf("%v", err))
					return
				}
				err = pluginConfig.BackupFile(reportFilename)
				if err != nil {
					gplog.Error(fmt.Sprintf("%v", err))
					return
				}
			}
		}
		if pluginConfig != nil {
			pluginConfig.CleanupPluginForBackup(globalCluster, globalFPInfo)
			pluginConfig.DeletePluginConfigWhenEncrypting(globalCluster)
		}
	}
}

func DoCleanup(backupFailed bool) {
	cleanupTimeout := 60 * time.Second

	defer func() {
		if err := recover(); err != nil {
			gplog.Warn("Encountered error during cleanup: %v", err)
		}
		if connectionPool != nil {
			connectionPool.Close()
		}
		gplog.Info("Cleanup complete")
		CleanupGroup.Done()
	}()

	gplog.Info("Beginning cleanup")
	if connectionPool != nil {
		cancelBlockedQueries(globalFPInfo.Timestamp)
	}
	if globalFPInfo.Timestamp != "" && MustGetFlagBool(options.SINGLE_DATA_FILE) {
		// Copy sessions must be terminated before cleaning up gpbackup_helper processes to avoid a potential deadlock
		// If the terminate query is sent via a connection with an active COPY command, and the COPY's pipe is cleaned up, the COPY query will hang.
		// This results in the DoCleanup function passed to the signal handler to never return, blocking the os.Exit call

		// All COPY commands should end on their own for a successful restore, however we cleanup any hanging COPY sessions here as a precaution
		utils.TerminateHangingCopySessions(globalFPInfo, fmt.Sprintf("gpbackup_%s", globalFPInfo.Timestamp), cleanupTimeout, 5*time.Second)

		// Ensure we don't leave anything behind on the segments
		utils.CleanUpSegmentHelperProcesses(globalCluster, globalFPInfo, "backup", cleanupTimeout)
		utils.CleanUpHelperFilesOnAllHosts(globalCluster, globalFPInfo, cleanupTimeout)

		// Check gpbackup_helper errors here if backup was terminated
		if wasTerminated {
			err := utils.CheckAgentErrorsOnSegments(globalCluster, globalFPInfo)
			if err != nil {
				gplog.Error(err.Error())
			}
		}
	}

	// The gpbackup_history entry is written to the DB with an "In Progress" status and a preliminary EndTime value
	// very early on.  If we get to cleanup and the backup succeeded, mark it as a success, otherwise mark it as a
	// failure; in either case, update the end time to the actual value. Between our signal handler and recovering
	// panics, there should be no way for gpbackup to exit that leaves the entry in the initial status.

	if !MustGetFlagBool(options.NO_HISTORY) {
		var statusString string
		if backupFailed {
			statusString = history.BackupStatusFailed
		} else {
			statusString = history.BackupStatusSucceed
		}
		historyDBName := globalFPInfo.GetBackupHistoryDatabasePath()
		historyDB, err := history.InitializeHistoryDatabase(historyDBName)
		if err != nil {
			gplog.Error(fmt.Sprintf("Unable to update history database.  Error: %v", err))
		} else {
			_, err := historyDB.Exec(fmt.Sprintf("UPDATE backups SET status='%s', end_time='%s' WHERE timestamp='%s'", statusString, backupReport.BackupConfig.EndTime, globalFPInfo.Timestamp))
			historyDB.Close()
			if err != nil {
				gplog.Error(fmt.Sprintf("Unable to update history database.  Error: %v", err))
			}
		}
	}

	err := backupLockFile.Unlock()
	if err != nil && backupLockFile != "" {
		gplog.Warn("Failed to remove lock file %s.", backupLockFile)
	}
}

// Cancel blocked gpbackup queries waiting for locks.
func cancelBlockedQueries(timestamp string) {
	conn := dbconn.NewDBConnFromEnvironment(MustGetFlagString(options.DBNAME))
	conn.MustConnect(1)
	defer conn.Close()

	// Query for all blocked queries
	pids := make([]int64, 0)
	var findBlockedQuery string
	if conn.Version.IsGPDB() && conn.Version.Before("6") {
		findBlockedQuery = fmt.Sprintf("SELECT procpid from pg_stat_activity WHERE application_name='gpbackup_%s' AND waiting='t' AND waiting_reason='lock';", timestamp)
	}
	if conn.Version.IsGPDB() && conn.Version.Is("6") {
		findBlockedQuery = fmt.Sprintf("SELECT pid from pg_stat_activity WHERE application_name='gpbackup_%s' AND waiting='t' AND waiting_reason='lock';", timestamp)
	} else if (conn.Version.IsGPDB() && conn.Version.AtLeast("7")) || conn.Version.IsCBDB() {
		findBlockedQuery = fmt.Sprintf("SELECT pid from pg_stat_activity WHERE application_name='gpbackup_%s' AND wait_event_type='Lock';", timestamp)
	}
	err := conn.Select(&pids, findBlockedQuery)
	gplog.FatalOnError(err)

	if len(pids) == 0 {
		return
	}

	gplog.Info(fmt.Sprintf("Canceling %d blocked queries", len(pids)))
	// Cancel all gpbackup queries waiting for a lock
	for _, pid := range pids {
		conn.MustExec(fmt.Sprintf("SELECT pg_cancel_backend(%d)", pid))
	}

	// Wait for the cancel queries to finish
	tickerCheckCanceled := time.NewTicker(500 * time.Millisecond)
	var count string
	for {
		select {
		case <-tickerCheckCanceled.C:
			blockedQueryCount := fmt.Sprintf("SELECT count(*) from pg_stat_activity WHERE application_name='gpbackup_%s' AND waiting='t' AND  waiting_reason='lock';", timestamp)
			if (conn.Version.IsGPDB() && conn.Version.AtLeast("7")) || conn.Version.IsCBDB() {
				blockedQueryCount = fmt.Sprintf("SELECT count(*) from pg_stat_activity WHERE application_name='gpbackup_%s' AND wait_event_type='Lock';", timestamp)
			}
			count = dbconn.MustSelectString(conn, blockedQueryCount)
			if count == "0" {
				return
			}
		case <-time.After(20 * time.Second):
			tickerCheckCanceled.Stop()
			gplog.FatalOnError(errors.New("Timeout attempting to cancel blocked queries"))
		}
	}
}

func GetVersion() string {
	return version
}

func logCompletionMessage(msg string) {
	if wasTerminated {
		gplog.Info("%s incomplete", msg)
	} else {
		gplog.Info("%s complete", msg)
	}
}

func CreateInitialSegmentPipes(oidList []string, c *cluster.Cluster, connectionPool *dbconn.DBConn, fpInfo filepath.FilePathInfo) int {
	// Create min(connections, tables) segment pipes on each host
	var maxPipes int
	if connectionPool.NumConns < len(oidList) {
		maxPipes = connectionPool.NumConns
	} else {
		maxPipes = len(oidList)
	}
	for i := 0; i < maxPipes; i++ {
		utils.CreateSegmentPipeOnAllHostsForBackup(oidList[i], c, fpInfo)
	}
	return maxPipes
}

type TableLocks struct {
	Oid         uint32
	Database    string
	Relation    string
	Mode        string
	Application string
	Granted     string
	User        string
	Pid         string
}

func getTableLocks(table Table) []TableLocks {
	conn := dbconn.NewDBConnFromEnvironment(MustGetFlagString(options.DBNAME))
	conn.MustConnect(1)
	var query string
	defer conn.Close()
	if conn.Version.IsGPDB() && conn.Version.Before("6") {
		query = fmt.Sprintf(`
		SELECT c.oid as oid,
		coalesce(a.datname, '') as database,
		n.nspname || '.' || c.relname as relation,
		l.mode,
		l.GRANTED as granted,
		coalesce(a.application_name, '') as application,
		coalesce(a.usename, '') as user,
		a.procpid as pid
		FROM pg_stat_activity a
		JOIN pg_locks l ON l.pid = a.procpid
		JOIN pg_class c on c.oid = l.relation
		JOIN pg_namespace n on n.oid=c.relnamespace
		WHERE (a.datname = '%s' OR a.datname IS NULL)
		AND NOT a.procpid = pg_backend_pid()
		AND relation = '%s'::regclass
		AND mode = 'AccessExclusiveLock'
		ORDER BY a.query_start;
		`, conn.DBName, table.FQN())
	} else {
		query = fmt.Sprintf(`
		SELECT c.oid as oid,
		coalesce(a.datname, '') as database,
		n.nspname || '.' || c.relname relation,
		l.mode,
		l.GRANTED as granted,
		coalesce(a.application_name, '') as application,
		coalesce(a.usename, '') as user,
		a.pid
		FROM pg_stat_activity a
		JOIN pg_locks l ON l.pid = a.pid
		JOIN pg_class c on c.oid = l.relation
		JOIN pg_namespace n on n.oid=c.relnamespace
		WHERE (a.datname = '%s' OR a.datname IS NULL)
		AND NOT a.pid = pg_backend_pid()
		AND relation = '%s'::regclass
		AND mode = 'AccessExclusiveLock'
		ORDER BY a.query_start;
		`, conn.DBName, table.FQN())
	}

	locksResults := make([]TableLocks, 0)
	err := conn.Select(&locksResults, query)
	if err != nil {
		gplog.FatalOnError(err)
	}

	return locksResults
}

func logTableLocks(table Table, whichConn int) {
	locks := getTableLocks(table)
	jsonData, _ := json.Marshal(&locks)
	gplog.Warn("Locks held on table %s: %s", table.FQN(), jsonData)
}
