package helpers

import (
	"regexp"
	"strings"
	"sync"

	"github.com/jesseduffield/generics/set"
	"github.com/jesseduffield/lazygit/pkg/commands/models"
	"github.com/jesseduffield/lazygit/pkg/gui/types"
	"github.com/jesseduffield/lazygit/pkg/utils"
	"github.com/samber/lo"
)

type FixupHelper struct {
	c *HelperCommon
}

func NewFixupHelper(
	c *HelperCommon,
) *FixupHelper {
	return &FixupHelper{
		c: c,
	}
}

type deletedLineInfo struct {
	filename     string
	startLineIdx int
	numLines     int
}

func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error {
	diff, hasStagedChanges, err := self.getDiff()
	if err != nil {
		return err
	}
	if diff == "" {
		return self.c.ErrorMsg(self.c.Tr.NoChangedFiles)
	}

	deletedLineInfos, hasHunksWithOnlyAddedLines := self.parseDiff(diff)
	if len(deletedLineInfos) == 0 {
		return self.c.ErrorMsg(self.c.Tr.NoDeletedLinesInDiff)
	}

	shas := self.blameDeletedLines(deletedLineInfos)

	if len(shas) == 0 {
		// This should never happen
		return self.c.ErrorMsg(self.c.Tr.NoBaseCommitsFound)
	}
	if len(shas) > 1 {
		subjects, err := self.c.Git().Commit.GetShasAndCommitMessagesFirstLine(shas)
		if err != nil {
			return err
		}
		message := lo.Ternary(hasStagedChanges,
			self.c.Tr.MultipleBaseCommitsFoundStaged,
			self.c.Tr.MultipleBaseCommitsFoundUnstaged)
		return self.c.ErrorMsg(message + "\n\n" + subjects)
	}

	commit, index, ok := lo.FindIndexOf(self.c.Model().Commits, func(commit *models.Commit) bool {
		return commit.Sha == shas[0]
	})
	if !ok {
		commits := self.c.Model().Commits
		if commits[len(commits)-1].Status == models.StatusMerged {
			// If the commit is not found, it's most likely because it's already
			// merged, and more than 300 commits away. Check if the last known
			// commit is already merged; if so, show the "already merged" error.
			return self.c.ErrorMsg(self.c.Tr.BaseCommitIsAlreadyOnMainBranch)
		}
		// If we get here, the current branch must have more then 300 commits. Unlikely...
		return self.c.ErrorMsg(self.c.Tr.BaseCommitIsNotInCurrentView)
	}
	if commit.Status == models.StatusMerged {
		return self.c.ErrorMsg(self.c.Tr.BaseCommitIsAlreadyOnMainBranch)
	}

	doIt := func() error {
		if !hasStagedChanges {
			if err := self.c.Git().WorkingTree.StageAll(); err != nil {
				return err
			}
			_ = self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.FILES}})
		}

		self.c.Contexts().LocalCommits.SetSelection(index)
		return self.c.PushContext(self.c.Contexts().LocalCommits)
	}

	if hasHunksWithOnlyAddedLines {
		return self.c.Confirm(types.ConfirmOpts{
			Title:  self.c.Tr.FindBaseCommitForFixup,
			Prompt: self.c.Tr.HunksWithOnlyAddedLinesWarning,
			HandleConfirm: func() error {
				return doIt()
			},
		})
	}

	return doIt()
}

func (self *FixupHelper) getDiff() (string, bool, error) {
	args := []string{"-U0", "--ignore-submodules=all", "HEAD", "--"}

	// Try staged changes first
	hasStagedChanges := true
	diff, err := self.c.Git().Diff.DiffIndexCmdObj(append([]string{"--cached"}, args...)...).RunWithOutput()

	if err == nil && diff == "" {
		hasStagedChanges = false
		// If there are no staged changes, try unstaged changes
		diff, err = self.c.Git().Diff.DiffIndexCmdObj(args...).RunWithOutput()
	}

	return diff, hasStagedChanges, err
}

func (self *FixupHelper) parseDiff(diff string) ([]*deletedLineInfo, bool) {
	lines := strings.Split(strings.TrimSuffix(diff, "\n"), "\n")

	deletedLineInfos := []*deletedLineInfo{}
	hasHunksWithOnlyAddedLines := false

	hunkHeaderRegexp := regexp.MustCompile(`@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@`)

	var filename string
	var currentLineInfo *deletedLineInfo
	finishHunk := func() {
		if currentLineInfo != nil {
			if currentLineInfo.numLines > 0 {
				deletedLineInfos = append(deletedLineInfos, currentLineInfo)
			} else {
				hasHunksWithOnlyAddedLines = true
			}
		}
	}
	for _, line := range lines {
		if strings.HasPrefix(line, "diff --git") {
			finishHunk()
			currentLineInfo = nil
		} else if strings.HasPrefix(line, "--- ") {
			// For some reason, the line ends with a tab character if the file
			// name contains spaces
			filename = strings.TrimRight(line[6:], "\t")
		} else if strings.HasPrefix(line, "@@ ") {
			finishHunk()
			match := hunkHeaderRegexp.FindStringSubmatch(line)
			startIdx := utils.MustConvertToInt(match[1])
			currentLineInfo = &deletedLineInfo{filename, startIdx, 0}
		} else if currentLineInfo != nil && line[0] == '-' {
			currentLineInfo.numLines++
		}
	}
	finishHunk()

	return deletedLineInfos, hasHunksWithOnlyAddedLines
}

// returns the list of commit hashes that introduced the lines which have now been deleted
func (self *FixupHelper) blameDeletedLines(deletedLineInfos []*deletedLineInfo) []string {
	var wg sync.WaitGroup
	shaChan := make(chan string)

	for _, info := range deletedLineInfos {
		wg.Add(1)
		go func(info *deletedLineInfo) {
			defer wg.Done()

			blameOutput, err := self.c.Git().Blame.BlameLineRange(info.filename, "HEAD", info.startLineIdx, info.numLines)
			if err != nil {
				self.c.Log.Errorf("Error blaming file '%s': %v", info.filename, err)
				return
			}
			blameLines := strings.Split(strings.TrimSuffix(blameOutput, "\n"), "\n")
			for _, line := range blameLines {
				shaChan <- strings.Split(line, " ")[0]
			}
		}(info)
	}

	go func() {
		wg.Wait()
		close(shaChan)
	}()

	result := set.New[string]()
	for sha := range shaChan {
		result.Add(sha)
	}

	return result.ToSlice()
}
