package sync

import (
	"fmt"
	"os"
	"os/exec"
	"syscall"
	"time"

	"git.csclub.uwaterloo.ca/public/merlin/config"
)

// SpawnProcess spawns a child process for the given repo. The process will
// be stopped early if the repo receives a stop signal, or if the process
// runs for longer than the repo's MaxTime.
// It returns a channel through which a Cmd will be sent once it has finished,
// or nil if it was unable to start a process.
func spawnProcess(repo *config.Repo, args []string) (ch <-chan *exec.Cmd) {
	if len(args) == 0 {
		repo.Logger.Error("command given is of zero length")
		return
	}

	repo.Logger.Debug(fmt.Sprintf("Running the command: %v", args))
	if repo.DryRun {
		repo.Logger.Debug("Dry running for 50 seconds")
		args = []string{"sleep", "50"}
	}

	cmd := exec.Command(args[0], args[1:]...)

	// This appears to be the only way to set env variables for exec.Command
	// The other env varaibles for rclone s3 were converted to arguments
	// but I was unable to find a way to convert this one.
	if repo.SyncType == "csc-sync-s3" {
		cmd.Env = append(os.Environ(), "RCLONE_CONFIG_S3_TYPE=s3")
	}

	repo.Logger.Debug("Starting process")

	if err := cmd.Start(); err != nil {
		repo.Logger.Error(fmt.Errorf("could not start process for %s: %w", repo.Name, err).Error())
		return
	}

	cmdChan := make(chan *exec.Cmd)
	ch = cmdChan
	cmdDoneChan := make(chan struct{})
	killProcess := func() {
		err := cmd.Process.Signal(syscall.SIGTERM)
		if err != nil {
			repo.Logger.Error("Could not send signal to process:", err)
			return
		}

		select {
		case <-time.After(30 * time.Second):
			repo.Logger.Warning("Process still hasn't stopped after 30 seconds; sending SIGKILL")
			cmd.Process.Signal(syscall.SIGKILL)

		case <-cmdDoneChan:
			repo.Logger.Debug("Process has been stopped.")
		}
	}

	go func() {
		cmd.Wait()
		close(cmdDoneChan)
	}()

	go func() {
		defer func() {
			cmdChan <- cmd
		}()
		select {
		case <-cmdDoneChan:
			if !cmd.ProcessState.Success() {
				repo.Logger.Warning("Process ended with status code", cmd.ProcessState.ExitCode())
				out, _ := cmd.CombinedOutput()
				repo.Logger.Debug(string(out))
			}

		case <-repo.StopChan:
			repo.Logger.Debug("Received signal to stop, killing process...")
			killProcess()

		case <-time.After(time.Duration(repo.MaxTime) * time.Second):
			repo.Logger.Warning("Process has exceeded its max time; killing now")
			killProcess()
		}
	}()
	return
}

// spawns a process and waits for it complete before parsing the exit code and returning it
func spawnProcessAndWait(repo *config.Repo, args []string) (status int) {
	status = config.FAILURE

	ch := spawnProcess(repo, args)
	if ch == nil {
		// spawnProcess will have already logged error
		return
	}
	cmd := <-ch

	switch cmd.ProcessState.ExitCode() {
	case 0:
		status = config.SUCCESS
	case -1:
		status = config.TERMINATED
		// default is already FAILURE
	}
	return
}

// returns true iff file contents are the same (for diff file contents, errors, empty return false)
func diffFileContent(repo *config.Repo, file1, file2 string) bool {
	readFile := func(file string) string {
		f, err := os.ReadFile(file)
		if err != nil {
			repo.Logger.Debug("Error while trying to read file: " + file)
			return ""
		}
		return string(f)
	}

	content1 := readFile(file1)
	content2 := readFile(file2)

	if content1 == "" || content2 == "" {
		return false
	}
	return content1 == content2
}

// returns true iff file times are the same (for diff file times and errors return false)
func diffFileTime(repo *config.Repo, file1, file2 string) bool {
	statFile := func(file string) int64 {
		f, err := os.Stat(file)
		if err != nil {
			repo.Logger.Debug("Error while trying to stat file: " + file)
			return 0
		}

		return f.ModTime().Unix()
	}

	time1 := statFile(file1)
	time2 := statFile(file2)

	if time1 == 0 || time2 == 0 {
		return false
	}
	return file1 == file2
}
