package utils

import (
	"bytes"
	"fmt"
	"io"
	"os/exec"
	"strings"
	"sync"

	"github.com/juju/errors"
	log "github.com/sirupsen/logrus"
)

func PrintWithPrefix(prefix string) func(string) {
	return InsertPrefixPerLine(prefix, func(a ...any) { fmt.Print(a...) })
}

func LogWithPrefix(level log.Level, prefix string) func(string) {
	previous := ""
	return func(allLines string) {
		arr := strings.Split(allLines, "\n")
		lines := len(arr)
		for i, line := range arr {
			if i < lines-1 {
				if i == 0 {
					log.StandardLogger().Log(level, prefix, previous, line)
				} else {
					log.StandardLogger().Log(level, prefix, line)
				}
			} else {
				previous = line
			}
		}
	}
}

func InsertPrefixPerLine(prefix string, printer func(a ...any)) func(string) {
	firstPrint := false
	return func(allLines string) {
		arr := strings.Split(allLines, "\n")
		lines := len(arr)
		for i, line := range arr {
			if i > 0 || !firstPrint {
				firstPrint = true
				printer(prefix)
			}
			printer(line)
			if i < lines-1 {
				printer("\n")
			}
		}
	}
}

func OutputString(buf *bytes.Buffer) func(string) {
	return func(s string) {
		buf.WriteString(s)
	}
}

func RunCommandAndGetOutput(exe string, args ...string) (string, error) {
	return RunCommandAndGetOutputIgnoreStderr(exe, false, args...)
}

func RunCommandAndGetOutputIgnoreStderr(exe string, ignoreStderr bool, args ...string) (string, error) {
	var buf bytes.Buffer
	exitCode, err := RunCommandIgnoreStderr(exe, ignoreStderr, OutputString(&buf), args...)
	if err != nil {
		log.WithFields(log.Fields{
			"cmd":    exe,
			"args":   args,
			"output": buf.String(),
			"error":  err.Error(),
		}).Error("Failed to run command")
		return "", err
	}
	if exitCode != 0 {
		log.WithFields(log.Fields{
			"cmd":    exe,
			"args":   args,
			"output": buf.String(),
			"exit":   exitCode,
		}).Error("Failed to run command")
		return "", errors.Errorf("Failed to run command, exe=%s, args=%s, exitCode=%d", exe, args, exitCode)
	}
	output := strings.Trim(buf.String(), " \r\n\t") // trim blank
	log.WithFields(log.Fields{
		"cmd":  exe,
		"args": args,
		// "output": output, // output may contain sensitive data
	}).Debug("Get command output")
	return output, nil
}

func RunCommand(exe string, outputFn func(string), args ...string) (int, error) {
	return RunCommandIgnoreStderr(exe, false, outputFn, args...)
}

func RunCommandIgnoreStderr(exe string, ignoreStderr bool, outputFn func(string), args ...string) (int, error) {
	log.WithFields(log.Fields{
		"cmd":  exe,
		"args": args,
	}).Debug("Execute command")
	cmd := exec.Command(exe, args...)
	stderr, err := cmd.StderrPipe()
	if err != nil {
		return -1, errors.Annotatef(err, "Failed to get std error pipe")
	}
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return -1, errors.Annotatef(err, "Failed to get std output pipe")
	}
	var wg sync.WaitGroup
	if !ignoreStderr {
		wg.Add(1)
		go func() {
			log.Debug("Start to read stderr")
			buf := make([]byte, 1024)
			for {
				len, err := stderr.Read(buf)
				if len > 0 {
					outputFn(string(buf[:len]))
				}
				if err != nil {
					if err != io.EOF {
						log.WithField("err", err).Error("Get an error when read stderr")
					}
					break
				}
			}
			log.Debug("Finished to read stderr")
			wg.Done()
		}()
	}
	wg.Add(1)
	go func() {
		log.Debug("Start to read stdout")
		buf := make([]byte, 1024)
		for {
			len, err := stdout.Read(buf)
			if len > 0 {
				outputFn(string(buf[:len]))
			}
			if err != nil {
				if err != io.EOF {
					log.WithField("err", err).Error("Get an error when read stderr")
				}
				break
			}
		}
		log.Debug("finished to read stdout")
		wg.Done()
	}()

	log.Debug("Start to execute command")
	if err = cmd.Start(); err != nil {
		return -1, errors.Annotatef(err, "Failed to start command")
	}
	wg.Wait()
	err = cmd.Wait()
	log.WithFields(log.Fields{
		"cmd":  exe,
		"args": args,
	}).Debug("Finished to execute command")
	if err != nil {
		if exitError, ok := err.(*exec.ExitError); ok {
			// 如果命令执行失败，获取退出码
			exitCode := exitError.ExitCode()
			outputFn(err.Error())
			return exitCode, errors.Annotatef(err, "Failed to run command")
		} else {
			return -1, errors.Annotatef(err, "Failed to run command")
		}
	}
	return cmd.ProcessState.ExitCode(), nil
}
