package git_commands

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

type BisectCommands struct {
	*GitCommon
}

func NewBisectCommands(gitCommon *GitCommon) *BisectCommands {
	return &BisectCommands{
		GitCommon: gitCommon,
	}
}

// This command is pretty cheap to run so we're not storing the result anywhere.
// But if it becomes problematic we can chang that.
func (self *BisectCommands) GetInfo() *BisectInfo {
	var err error
	info := &BisectInfo{started: false, log: self.Log, newTerm: "bad", oldTerm: "good"}
	// we return nil if we're not in a git bisect session.
	// we know we're in a session by the presence of a .git/BISECT_START file

	bisectStartPath := filepath.Join(self.dotGitDir, "BISECT_START")
	exists, err := self.os.FileExists(bisectStartPath)
	if err != nil {
		self.Log.Infof("error getting git bisect info: %s", err.Error())
		return info
	}

	if !exists {
		return info
	}

	startContent, err := os.ReadFile(bisectStartPath)
	if err != nil {
		self.Log.Infof("error getting git bisect info: %s", err.Error())
		return info
	}

	info.started = true
	info.start = strings.TrimSpace(string(startContent))

	termsContent, err := os.ReadFile(filepath.Join(self.dotGitDir, "BISECT_TERMS"))
	if err != nil {
		// old git versions won't have this file so we default to bad/good
	} else {
		splitContent := strings.Split(string(termsContent), "\n")
		info.newTerm = splitContent[0]
		info.oldTerm = splitContent[1]
	}

	bisectRefsDir := filepath.Join(self.dotGitDir, "refs", "bisect")
	files, err := os.ReadDir(bisectRefsDir)
	if err != nil {
		self.Log.Infof("error getting git bisect info: %s", err.Error())
		return info
	}

	info.statusMap = make(map[string]BisectStatus)
	for _, file := range files {
		status := BisectStatusSkipped
		name := file.Name()
		path := filepath.Join(bisectRefsDir, name)

		fileContent, err := os.ReadFile(path)
		if err != nil {
			self.Log.Infof("error getting git bisect info: %s", err.Error())
			return info
		}

		sha := strings.TrimSpace(string(fileContent))

		if name == info.newTerm {
			status = BisectStatusNew
		} else if strings.HasPrefix(name, info.oldTerm+"-") {
			status = BisectStatusOld
		} else if strings.HasPrefix(name, "skipped-") {
			status = BisectStatusSkipped
		}

		info.statusMap[sha] = status
	}

	currentContent, err := os.ReadFile(filepath.Join(self.dotGitDir, "BISECT_EXPECTED_REV"))
	if err != nil {
		self.Log.Infof("error getting git bisect info: %s", err.Error())
		return info
	}
	currentSha := strings.TrimSpace(string(currentContent))
	info.current = currentSha

	return info
}

func (self *BisectCommands) Reset() error {
	return self.cmd.New("git bisect reset").StreamOutput().Run()
}

func (self *BisectCommands) Mark(ref string, term string) error {
	return self.cmd.New(
		fmt.Sprintf("git bisect %s %s", term, ref),
	).
		IgnoreEmptyError().
		StreamOutput().
		Run()
}

func (self *BisectCommands) Skip(ref string) error {
	return self.Mark(ref, "skip")
}

func (self *BisectCommands) Start() error {
	return self.cmd.New("git bisect start").StreamOutput().Run()
}

// tells us whether we've found our problem commit(s). We return a string slice of
// commit sha's if we're done, and that slice may have more that one item if
// skipped commits are involved.
func (self *BisectCommands) IsDone() (bool, []string, error) {
	info := self.GetInfo()
	if !info.Bisecting() {
		return false, nil, nil
	}

	newSha := info.GetNewSha()
	if newSha == "" {
		return false, nil, nil
	}

	// if we start from the new commit and reach the a good commit without
	// coming across any unprocessed commits, then we're done
	done := false
	candidates := []string{}

	err := self.cmd.New(fmt.Sprintf("git rev-list %s", newSha)).RunAndProcessLines(func(line string) (bool, error) {
		sha := strings.TrimSpace(line)

		if status, ok := info.statusMap[sha]; ok {
			switch status {
			case BisectStatusSkipped, BisectStatusNew:
				candidates = append(candidates, sha)
				return false, nil
			case BisectStatusOld:
				done = true
				return true, nil
			}
		} else {
			return true, nil
		}

		// should never land here
		return true, nil
	})
	if err != nil {
		return false, nil, err
	}

	return done, candidates, nil
}

// tells us whether the 'start' ref that we'll be sent back to after we're done
// bisecting is actually a descendant of our current bisect commit. If it's not, we need to
// render the commits from the bad commit.
func (self *BisectCommands) ReachableFromStart(bisectInfo *BisectInfo) bool {
	err := self.cmd.New(
		fmt.Sprintf("git merge-base --is-ancestor %s %s", bisectInfo.GetNewSha(), bisectInfo.GetStartSha()),
	).DontLog().Run()

	return err == nil
}