package git

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"

	gitea_git "code.gitea.io/gitea/modules/git"
	"code.gitea.io/gitea/modules/process"
	hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs"
)

type BlameReader struct {
	cmd      *exec.Cmd
	output   io.ReadCloser
	reader   *bufio.Reader
	lastSha  *string
	cancel   context.CancelFunc   // Cancels the context that this reader runs in
	finished process.FinishedFunc // Tells the process manager we're finished and it can remove the associated process from the process table
}

var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")

func GetBlameCommit(repo *gitea_git.Repository, sha string) *hat_api.BlameCommit {
	commit, err := repo.GetCommit(sha)

	if err != nil {
		return &hat_api.BlameCommit{}
	}

	var apiParents []string
	for i := 0; i < commit.ParentCount(); i++ {
		sha, _ := commit.ParentID(i)
		apiParents = append(apiParents, sha.String())
	}

	return &hat_api.BlameCommit{
		ID:            sha,
		Author:        commit.Author,
		Commiter:      commit.Committer,
		CommitMessage: commit.CommitMessage,
		Parents:       apiParents,
		AuthoredTime:  commit.Author.When,
		CommittedTime: commit.Committer.When,
		CreatedTime:   commit.Committer.When,
	}
}

func (r *BlameReader) NextApiPart(repo *gitea_git.Repository) (*hat_api.BlamePart, error) {
	var blamePart *hat_api.BlamePart
	reader := r.reader
	effectLine := 0

	if r.lastSha != nil {
		blamePart = &hat_api.BlamePart{
			Sha:            *r.lastSha,
			Commit:         GetBlameCommit(repo, *r.lastSha),
			PreviousNumber: 0,
			CurrentNumber:  0,
			EffectLine:     effectLine,
			Lines:          make([]string, 0),
		}
	}

	var line []byte
	var isPrefix bool
	var err error

	for err != io.EOF {
		line, isPrefix, err = reader.ReadLine()
		if err != nil && err != io.EOF {
			return blamePart, err
		}

		if len(line) == 0 {
			continue
		}

		lines := shaLineRegex.FindSubmatch(line)
		if lines != nil {
			lineString := string(line)
			sha1 := string(lines[1])
			lineString = strings.Replace(lineString, sha1, "", 1)
			lineArray := strings.Split(lineString, " ")
			previousNumber, _ := strconv.Atoi(lineArray[1])
			if blamePart == nil {
				blamePart = &hat_api.BlamePart{
					Sha:            sha1,
					Commit:         GetBlameCommit(repo, sha1),
					PreviousNumber: previousNumber,
					CurrentNumber:  0,
					EffectLine:     effectLine,
					Lines:          make([]string, 0),
				}
			}

			if blamePart.Sha != sha1 {
				r.lastSha = &sha1
				// need to munch to end of line...
				for isPrefix {
					_, isPrefix, err = reader.ReadLine()
					if err != nil && err != io.EOF {
						return blamePart, err
					}
				}
				return blamePart, nil
			}
		} else if line[0] == '\t' {
			code := line[1:]
			effectLine += 1
			blamePart.Lines = append(blamePart.Lines, string(code))
		}
		blamePart.EffectLine = effectLine

		for isPrefix {
			_, isPrefix, err = reader.ReadLine()
			if err != nil && err != io.EOF {
				return blamePart, err
			}
		}
	}

	r.lastSha = nil

	return blamePart, nil
}

func (r *BlameReader) Close() error {
	defer r.finished() // Only remove the process from the process table when the underlying command is closed
	r.cancel()         // However, first cancel our own context early

	_ = r.output.Close()

	if err := r.cmd.Wait(); err != nil {
		return fmt.Errorf("Wait: %w", err)
	}

	return nil
}

func CreateBlameReader(ctx context.Context, repoPath, commitID, file string) (*BlameReader, error) {
	return createBlameReader(ctx, repoPath, gitea_git.GitExecutable, "blame", commitID, "--porcelain", "--", file)
}

func createBlameReader(ctx context.Context, dir string, command ...string) (*BlameReader, error) {
	// Here we use the provided context - this should be tied to the request performing the blame so that it does not hang around.
	ctx, cancel, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("GetBlame [repo_path: %s]", dir))

	cmd := exec.CommandContext(ctx, command[0], command[1:]...)
	cmd.Dir = dir
	cmd.Stderr = os.Stderr
	process.SetSysProcAttribute(cmd)

	stdout, err := cmd.StdoutPipe()
	if err != nil {
		defer finished()
		return nil, fmt.Errorf("StdoutPipe: %w", err)
	}

	if err = cmd.Start(); err != nil {
		defer finished()
		_ = stdout.Close()
		return nil, fmt.Errorf("Start: %w", err)
	}

	reader := bufio.NewReader(stdout)

	return &BlameReader{
		cmd:      cmd,
		output:   stdout,
		reader:   reader,
		cancel:   cancel,
		finished: finished,
	}, nil
}
