package utils

import (
	"fmt"
	"io"
	path "path/filepath"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/apache/cloudberry-backup/filepath"
	"github.com/apache/cloudberry-go-libs/cluster"
	"github.com/apache/cloudberry-go-libs/gplog"
	"github.com/apache/cloudberry-go-libs/iohelper"
	"github.com/apache/cloudberry-go-libs/operating"
	"github.com/pkg/errors"
)

var helperMutex sync.Mutex

/*
 * Functions to run commands on entire cluster during both backup and restore
 */

/*
 * The reason that gprestore is in charge of creating the first pipe to ensure
 * that the first pipe is created before the first COPY FROM is issued.  If
 * gpbackup_helper was in charge of creating the first pipe, there is a
 * possibility that the COPY FROM commands start before gpbackup_helper is done
 * starting up and setting up the first pipe.
 */
func CreateSegmentPipeOnAllHostsForBackup(oid string, c *cluster.Cluster, fpInfo filepath.FilePathInfo) {
	remoteOutput := c.GenerateAndExecuteCommand("Creating segment data pipes", cluster.ON_SEGMENTS, func(contentID int) string {
		pipeName := fpInfo.GetSegmentPipeFilePath(contentID)
		pipeName = fmt.Sprintf("%s_%s", pipeName, oid)
		gplog.Debug("Creating pipe %s", pipeName)
		return fmt.Sprintf("mkfifo -m 0700 %s", pipeName)
	})
	c.CheckClusterError(remoteOutput, "Unable to create segment data pipes", func(contentID int) string {
		return "Unable to create segment data pipe"
	})
}

func CreateSegmentPipeOnAllHostsForRestore(oid string, c *cluster.Cluster, fpInfo filepath.FilePathInfo) {
	oidWithBatch := strings.Split(oid, ",")
	remoteOutput := c.GenerateAndExecuteCommand("Creating segment data pipes", cluster.ON_SEGMENTS, func(contentID int) string {
		pipeName := fpInfo.GetSegmentPipeFilePath(contentID)
		pipeName = fmt.Sprintf("%s_%s_%s", pipeName, oidWithBatch[0], oidWithBatch[1])
		gplog.Debug("Creating pipe %s", pipeName)
		return fmt.Sprintf("mkfifo %s", pipeName)
	})
	c.CheckClusterError(remoteOutput, "Unable to create segment data pipes", func(contentID int) string {
		return "Unable to create segment data pipe"
	})
}

func WriteOidListToSegments(oidList []string, c *cluster.Cluster, fpInfo filepath.FilePathInfo, fileSuffix string) {
	rsync_exists := CommandExists("rsync")
	if !rsync_exists {
		gplog.Fatal(errors.New("Failed to find rsync on PATH. Please ensure rsync is installed."), "")
	}

	localOidFile, err := operating.System.TempFile("", "gpbackup-oids")
	gplog.FatalOnError(err, "Cannot open temporary file to write oids")
	defer func() {
		err = operating.System.Remove(localOidFile.Name())
		if err != nil {
			gplog.Warn("Cannot remove temporary oid file: %s, Err: %s", localOidFile.Name(), err.Error())
		}
	}()

	WriteOidsToFile(localOidFile.Name(), oidList)

	generateScpCmd := func(contentID int) string {
		sourceFile := localOidFile.Name()
		hostname := c.GetHostForContent(contentID)
		dest := fpInfo.GetSegmentHelperFilePath(contentID, fileSuffix)

		return fmt.Sprintf(`rsync -e ssh %s %s:%s`, sourceFile, hostname, dest)
	}
	remoteOutput := c.GenerateAndExecuteCommand("rsync oid file to segments", cluster.ON_LOCAL|cluster.ON_SEGMENTS, generateScpCmd)

	errMsg := "Failed to rsync oid file"
	errFunc := func(contentID int) string {
		return "Failed to run rsync"
	}
	c.CheckClusterError(remoteOutput, errMsg, errFunc, false)
}

func WriteOidsToFile(filename string, oidList []string) {
	oidFp, err := iohelper.OpenFileForWriting(filename)
	gplog.FatalOnError(err, filename)
	defer func() {
		err = oidFp.Close()
		gplog.FatalOnError(err, filename)
	}()

	err = WriteOids(oidFp, oidList)
	gplog.FatalOnError(err, filename)
}

func WriteOids(writer io.Writer, oidList []string) error {
	var err error
	for _, oid := range oidList {
		_, err = writer.Write([]byte(oid + "\n"))
		if err != nil {
			// error logging handled in calling functions
			return err
		}
	}

	return nil
}

func VerifyHelperVersionOnSegments(version string, c *cluster.Cluster) {
	remoteOutput := c.GenerateAndExecuteCommand("Verifying gpbackup_helper version", cluster.ON_HOSTS, func(contentID int) string {
		gphome := operating.System.Getenv("GPHOME")
		return fmt.Sprintf("%s/bin/gpbackup_helper --version", gphome)
	})
	c.CheckClusterError(remoteOutput, "Could not verify gpbackup_helper version", func(contentID int) string {
		return "Could not verify gpbackup_helper version"
	})

	numIncorrect := 0
	for contentID, cmd := range remoteOutput.Commands {
		parsedSegVersion := ""
		segVersion := strings.TrimSpace(cmd.Stdout) // Expected format is "gpbackup_helper version [version string]"
		splitSegVersion := strings.Split(segVersion, " ")
		if len(splitSegVersion) == 3 {
			// Array access placed inside a length guard to keep error messages nice, instead of
			// spilling panics to the log
			parsedSegVersion = splitSegVersion[2]
		}
		if parsedSegVersion != version {
			gplog.Verbose("Version mismatch for gpbackup_helper on segment %d on host %s: Expected version %s, found version %s.", contentID, c.GetHostForContent(contentID), version, parsedSegVersion)
			numIncorrect++
		}
	}
	if numIncorrect > 0 {
		cluster.LogFatalClusterError("The version of gpbackup_helper must match the version of gpbackup/gprestore, but found gpbackup_helper binaries with invalid version", cluster.ON_HOSTS, numIncorrect)
	}
}

func StartGpbackupHelpers(c *cluster.Cluster, fpInfo filepath.FilePathInfo, operation string, pluginConfigFile string, compressStr string, onErrorContinue bool, isFilter bool, wasTerminated *bool, copyQueue int, isSingleDataFile bool, resizeCluster bool, origSize int, destSize int, verbosity int) {
	// A mutex lock for cleaning up and starting gpbackup helpers prevents a
	// race condition that causes gpbackup_helpers to be orphaned if
	// gpbackup_helper cleanup happens before they are started.
	helperMutex.Lock()
	if *wasTerminated {
		helperMutex.Unlock()
		select {} // Pause forever and wait for cleanup to exit program.
	}
	defer helperMutex.Unlock()

	gphomePath := operating.System.Getenv("GPHOME")
	envSourceCommand := SourceClusterEnvCommand(gphomePath)
	pluginStr := ""
	if pluginConfigFile != "" {
		_, configFilename := path.Split(pluginConfigFile)
		pluginStr = fmt.Sprintf(" --plugin-config /tmp/%s", configFilename)
	}
	onErrorContinueStr := ""
	if onErrorContinue {
		onErrorContinueStr = " --on-error-continue"
	}
	filterStr := ""
	if isFilter {
		filterStr = " --with-filters"
	}
	singleDataFileStr := ""
	if isSingleDataFile {
		singleDataFileStr = " --single-data-file"
	}
	resizeStr := ""
	if resizeCluster {
		resizeStr = fmt.Sprintf(" --resize-cluster --orig-seg-count %d --dest-seg-count %d", origSize, destSize)
	}

	remoteOutput := c.GenerateAndExecuteCommand("Starting gpbackup_helper agent", cluster.ON_SEGMENTS, func(contentID int) string {
		tocFile := fpInfo.GetSegmentTOCFilePath(contentID)
		oidFile := fpInfo.GetSegmentHelperFilePath(contentID, "oid")
		scriptFile := fpInfo.GetSegmentHelperFilePath(contentID, "script")
		pipeFile := fpInfo.GetSegmentPipeFilePath(contentID)
		backupFile := fpInfo.GetTableBackupFilePath(contentID, 0, GetPipeThroughProgram().Extension, true)
		helperCmdStr := fmt.Sprintf(`gpbackup_helper %s --toc-file %s --oid-file %s --pipe-file %s --data-file "%s" --content %d%s%s%s%s%s%s --copy-queue-size %d --verbosity %d`,
			operation, tocFile, oidFile, pipeFile, backupFile, contentID, pluginStr, compressStr, onErrorContinueStr, filterStr, singleDataFileStr, resizeStr, copyQueue, verbosity)
		// we run these commands in sequence to ensure that any failure is critical; the last command ensures the agent process was successfully started
		return fmt.Sprintf(`cat << HEREDOC > %[1]s && chmod +x %[1]s && ( nohup %[1]s &> /dev/null &)
#!/bin/bash
%[3]s
%[2]s/bin/%[4]s

HEREDOC

`, scriptFile, gphomePath, envSourceCommand, helperCmdStr)
	})
	c.CheckClusterError(remoteOutput, "Error starting gpbackup_helper agent", func(contentID int) string {
		return "Error starting gpbackup_helper agent"
	})
}

func findCommandStr(c *cluster.Cluster, fpInfo filepath.FilePathInfo, contentID int) string {
	var cmdString string
	if runtime.GOOS == "linux" {
		cmdString = fmt.Sprintf(`find %s -regextype posix-extended -regex ".*gpbackup_%d_%s_(oid|script|pipe)_%d.*"`,
			c.GetDirForContent(contentID), contentID, fpInfo.Timestamp, fpInfo.PID)
	} else if runtime.GOOS == "darwin" {
		cmdString = fmt.Sprintf(`find -E %s -regex ".*gpbackup_%d_%s_(oid|script|pipe)_%d.*"`,
			c.GetDirForContent(contentID), contentID, fpInfo.Timestamp, fpInfo.PID)
	}
	return cmdString
}

func GetHelperFileCount(c *cluster.Cluster, fpInfo filepath.FilePathInfo) int {
	totalFiles := 0
	remoteOutput := c.GenerateAndExecuteCommand("Checking for leftover gpbackup_helper files on segments", cluster.ON_SEGMENTS, func(contentID int) string {
		return fmt.Sprintf("%s %s", findCommandStr(c, fpInfo, contentID), "| wc -l")
	})

	if remoteOutput.NumErrors > 0 {
		gplog.Error("Unable to check for leftover gpbackup_helper files on segments")
	} else {
		for _, cmd := range remoteOutput.Commands {
			numFiles, _ := strconv.Atoi(strings.TrimSpace(cmd.Stdout))
			if numFiles > 0 {
				totalFiles = totalFiles + numFiles
			}
		}
	}
	return totalFiles
}

// Removes all gpbackup_helper files and pipes from the segment data directories.
// It's expected that gpbackup_helper cleans up the files on exit, but that is not guaranteed,
// so this function is used to clean up any leftover files.
func RemoveHelperFiles(c *cluster.Cluster, fpInfo filepath.FilePathInfo) {
	remoteOutput := c.GenerateAndExecuteCommand("Removing gpbackup_helper files from segment data directories", cluster.ON_SEGMENTS, func(contentID int) string {
		return fmt.Sprintf("%s %s", findCommandStr(c, fpInfo, contentID), `-exec rm -f {} \;`)
	})

	errMsg := fmt.Sprintf("Unable to remove gpbackup_helper file(s). See %s for a complete list of segments with errors and remove manually.",
		gplog.GetLogFilePath())
	c.CheckClusterError(remoteOutput, errMsg, func(contentID int) string {
		return fmt.Sprintf("Unable to remove gpbackup_helper file(s)\n\t%s", findCommandStr(c, fpInfo, contentID))
	}, true)
}

func CleanUpHelperFilesOnAllHosts(c *cluster.Cluster, fpInfo filepath.FilePathInfo, timeout time.Duration) {
	var fileCount int
	helperMutex.Lock()
	defer helperMutex.Unlock()
	tickerCleanup := time.NewTicker(1 * time.Second)

	for {
		fileCount = GetHelperFileCount(c, fpInfo)
		select {
		case <-tickerCleanup.C:
			if fileCount == 0 {
				return
			}
			RemoveHelperFiles(c, fpInfo)
		case <-time.After(timeout):
			gplog.Warn("Timeout of %ds reached while waiting for %d gpbackup_helper file(s) to be removed.", int(timeout.Seconds()), fileCount)
			return
		}
	}
}

// Checks for gpbackup_helper processes on segments.
// Returns a boolean indicating whether any PIDs were found and a map of hostnames to PIDs.
func CheckHelperPids(c *cluster.Cluster, fpInfo filepath.FilePathInfo, operation string) (map[string][]int, bool) {
	var foundPid bool = false
	remoteOutput := c.GenerateAndExecuteCommand("Checking for leftover gpbackup_helper processes", cluster.ON_SEGMENTS, func(contentID int) string {
		tocFile := fpInfo.GetSegmentTOCFilePath(contentID)
		procPattern := fmt.Sprintf("gpbackup_helper --%s-agent --toc-file %s*", operation, tocFile)
		return fmt.Sprintf(`ps ux | grep "%s" | grep -v grep | awk '{print $2}'`, procPattern)
	})
	pidMap := make(map[string][]int)
	for _, cmd := range remoteOutput.Commands {
		pids := strings.Split(strings.TrimSpace(cmd.Stdout), "\n")
		host := c.GetHostForContent(cmd.Content)
		for _, pid := range pids {
			if pid != "" {
				foundPid = true
				gplog.Debug("Found gpbackup_helper process %s for segment %d on host %s", pid, cmd.Content, host)
				pidInt, _ := strconv.Atoi(pid)
				pidMap[host] = append(pidMap[host], pidInt)
			}
		}
	}
	return pidMap, foundPid
}

// Checks for gpbackup_helper processes on segments and sends a USR1 signal to them to terminate.
// If the processes are not terminated within the timeout, a warning is logged.
// Ideally, the termination requests would only be sent to the segment hosts reported by CheckHelperPids,
// but the current design of cluster.GenerateAndExecuteCommand does not allow for that, so we a
// command to check for pids and send the termination signal to all segments if CheckHelperPids reports
// that there are still processes running.
func CleanUpSegmentHelperProcesses(c *cluster.Cluster, fpInfo filepath.FilePathInfo, operation string, timeout time.Duration) {
	helperMutex.Lock()
	defer helperMutex.Unlock()
	tickerCleanup := time.NewTicker(1 * time.Second)

	for {
		helperPids, found := CheckHelperPids(c, fpInfo, operation)
		select {
		case <-tickerCleanup.C:
			if !found {
				return
			}
			c.GenerateAndExecuteCommand("Cleaning up gpbackup_helper processes", cluster.ON_SEGMENTS, func(contentID int) string {
				tocFile := fpInfo.GetSegmentTOCFilePath(contentID)
				procPattern := fmt.Sprintf("gpbackup_helper --%s-agent --toc-file %s", operation, tocFile)
				return fmt.Sprintf("PIDS=`ps ux | grep \"%s\" | grep -v grep | awk '{print $2}'`; if [[ ! -z \"$PIDS\" ]]; then kill -USR1 $PIDS; fi", procPattern)
			})
		case <-time.After(timeout):
			for host, pids := range helperPids {
				if len(pids) > 0 {
					gplog.Warn("Unable to terminate segment helper process(es) on host %s. ", host)
				}
			}
			gplog.Warn("See %s for a complete list of segments with errors and terminate manually.", gplog.GetLogFilePath())
			return
		}
	}
}

func CheckAgentErrorsOnSegments(c *cluster.Cluster, fpInfo filepath.FilePathInfo) error {
	remoteOutput := c.GenerateAndExecuteCommand("Checking whether segment agents had errors", cluster.ON_SEGMENTS, func(contentID int) string {
		errorFile := fmt.Sprintf("%s_error", fpInfo.GetSegmentPipeFilePath(contentID))
		/*
		 * If an error file exists we want to indicate an error, as that means
		 * the agent errored out.  If no file exists, the agent was successful.
		 */
		return fmt.Sprintf("if [[ -f %s ]]; then echo 'error'; fi; rm -f %s", errorFile, errorFile)
	})

	numErrors := 0
	for contentID, cmd := range remoteOutput.Commands {
		if strings.TrimSpace(cmd.Stdout) == "error" {
			gplog.Verbose("Error occurred with helper agent on segment %d on host %s.", contentID, c.GetHostForContent(contentID))
			numErrors++
		}
	}
	if numErrors > 0 {
		helperLogName := fpInfo.GetHelperLogPath()
		return errors.Errorf("Encountered errors with %d helper agent(s).  See %s for a complete list of segments with errors, and see %s on the corresponding hosts for detailed error messages.",
			numErrors, gplog.GetLogFilePath(), helperLogName)
	}
	return nil
}

func CreateSkipFileOnSegments(oid string, tableName string, c *cluster.Cluster, fpInfo filepath.FilePathInfo) {
	createSkipFileLogMsg := fmt.Sprintf("Creating skip file on segments for restore entry %s (%s)", oid, tableName)
	remoteOutput := c.GenerateAndExecuteCommand(createSkipFileLogMsg, cluster.ON_SEGMENTS, func(contentID int) string {
		return fmt.Sprintf("touch %s_skip_%s", fpInfo.GetSegmentPipeFilePath(contentID), oid)
	})
	c.CheckClusterError(remoteOutput, "Error while creating skip file on segments", func(contentID int) string {
		return fmt.Sprintf("Could not create skip file %s_skip_%s on segments", fpInfo.GetSegmentPipeFilePath(contentID), oid)
	})
}
