package cautils

import (
	"fmt"
	"path"
	"strings"
	"time"

	"github.com/armosec/go-git-url/apis"
	gitv5 "github.com/go-git/go-git/v5"
	configv5 "github.com/go-git/go-git/v5/config"
	plumbingv5 "github.com/go-git/go-git/v5/plumbing"
	git2go "github.com/libgit2/git2go/v33"
)

type LocalGitRepository struct {
	goGitRepo        *gitv5.Repository
	git2GoRepo       *git2go.Repository
	head             *plumbingv5.Reference
	config           *configv5.Config
	fileToLastCommit map[string]*git2go.Commit
}

func NewLocalGitRepository(path string) (*LocalGitRepository, error) {
	goGitRepo, err := gitv5.PlainOpenWithOptions(path, &gitv5.PlainOpenOptions{DetectDotGit: true})
	if err != nil {
		return nil, err
	}

	head, err := goGitRepo.Head()
	if err != nil {
		return nil, err
	}

	if !head.Name().IsBranch() {
		return nil, fmt.Errorf("current HEAD reference is not a branch")
	}

	config, err := goGitRepo.Config()
	if err != nil {
		return nil, err
	}

	if len(config.Remotes) == 0 {
		return nil, fmt.Errorf("no remotes found")
	}

	l := &LocalGitRepository{
		goGitRepo: goGitRepo,
		head:      head,
		config:    config,
	}

	if repoRoot, err := l.GetRootDir(); err == nil {
		git2GoRepo, err := git2go.OpenRepository(repoRoot)
		if err != nil {
			return l, err
		}
		l.git2GoRepo = git2GoRepo
	}

	return l, nil
}

// GetBranchName get current branch name
func (g *LocalGitRepository) GetBranchName() string {
	return g.head.Name().Short()
}

// GetRemoteUrl get default remote URL
func (g *LocalGitRepository) GetRemoteUrl() (string, error) {
	branchName := g.GetBranchName()
	if branchRef, branchFound := g.config.Branches[branchName]; branchFound {
		remoteName := branchRef.Remote
		if len(g.config.Remotes[remoteName].URLs) == 0 {
			return "", fmt.Errorf("expected to find URLs for remote '%s', branch '%s'", remoteName, branchName)
		}
		return g.config.Remotes[remoteName].URLs[0], nil
	}

	const defaultRemoteName string = "origin"
	if len(g.config.Remotes[defaultRemoteName].URLs) == 0 {
		return "", fmt.Errorf("expected to find URLs for remote '%s'", defaultRemoteName)
	}
	return g.config.Remotes[defaultRemoteName].URLs[0], nil
}

// GetName get origin name without the .git suffix
func (g *LocalGitRepository) GetName() (string, error) {
	originUrl, err := g.GetRemoteUrl()
	if err != nil {
		return "", err
	}
	baseName := path.Base(originUrl)
	// remove .git
	return strings.TrimSuffix(baseName, ".git"), nil
}

// GetLastCommit get latest commit object
func (g *LocalGitRepository) GetLastCommit() (*apis.Commit, error) {
	cIter, err := g.goGitRepo.Log(&gitv5.LogOptions{})
	if err != nil {
		return nil, err
	}

	commit, err := cIter.Next()
	defer cIter.Close()
	if err != nil {
		return nil, err
	}

	return &apis.Commit{
		SHA: commit.Hash.String(),
		Author: apis.Committer{
			Name:  commit.Author.Name,
			Email: commit.Author.Email,
			Date:  commit.Author.When,
		},
		Message:   commit.Message,
		Committer: apis.Committer{},
		Files:     []apis.Files{},
	}, nil
}

func (g *LocalGitRepository) getAllCommits() ([]*git2go.Commit, error) {
	logItr, itrErr := g.git2GoRepo.Walk()
	if itrErr != nil {

		return nil, itrErr
	}

	pushErr := logItr.PushHead()
	if pushErr != nil {
		return nil, pushErr
	}

	var allCommits []*git2go.Commit
	err := logItr.Iterate(func(commit *git2go.Commit) bool {
		if commit != nil {
			allCommits = append(allCommits, commit)
			return true
		}
		return false
	})

	if err != nil {
		return nil, err
	}

	if err != nil {
		return nil, err
	}

	return allCommits, nil
}

func (g *LocalGitRepository) GetFileLastCommit(filePath string) (*apis.Commit, error) {
	if len(g.fileToLastCommit) == 0 {
		filePathToCommitTime := map[string]time.Time{}
		filePathToCommit := map[string]*git2go.Commit{}
		allCommits, _ := g.getAllCommits()

		// builds a map of all files to their last commit
		for _, commit := range allCommits {
			// Ignore merge commits (2+ parents)
			if commit.ParentCount() <= 1 {
				tree, err := commit.Tree()
				if err != nil {
					continue
				}

				// ParentCount can be either 1 or 0 (initial commit)
				// In case it's the initial commit, prevTree is nil
				var prevTree *git2go.Tree
				if commit.ParentCount() == 1 {
					prevCommit := commit.Parent(0)
					prevTree, err = prevCommit.Tree()
					if err != nil {
						continue
					}
				}

				diff, err := g.git2GoRepo.DiffTreeToTree(prevTree, tree, nil)
				if err != nil {
					continue
				}

				numDeltas, err := diff.NumDeltas()
				if err != nil {
					continue
				}

				for i := 0; i < numDeltas; i++ {
					delta, err := diff.Delta(i)
					if err != nil {
						continue
					}

					deltaFilePath := delta.NewFile.Path
					commitTime := commit.Author().When

					// In case we have the commit information for the file which is not the latest - we override it
					if currentCommitTime, exists := filePathToCommitTime[deltaFilePath]; exists {
						if currentCommitTime.Before(commitTime) {
							filePathToCommitTime[deltaFilePath] = commitTime
							filePathToCommit[deltaFilePath] = commit
						}
					} else {
						filePathToCommitTime[deltaFilePath] = commitTime
						filePathToCommit[deltaFilePath] = commit
					}
				}
			}
		}
		g.fileToLastCommit = filePathToCommit
	}

	if relevantCommit, exists := g.fileToLastCommit[filePath]; exists {
		return g.getCommit(relevantCommit), nil
	}

	return nil, fmt.Errorf("failed to get commit information for file: %s", filePath)
}

func (g *LocalGitRepository) getCommit(commit *git2go.Commit) *apis.Commit {
	return &apis.Commit{
		SHA: commit.Id().String(),
		Author: apis.Committer{
			Name:  commit.Author().Name,
			Email: commit.Author().Email,
			Date:  commit.Author().When,
		},
		Message:   commit.Message(),
		Committer: apis.Committer{},
		Files:     []apis.Files{},
	}
}

func (g *LocalGitRepository) GetRootDir() (string, error) {
	wt, err := g.goGitRepo.Worktree()
	if err != nil {
		return "", fmt.Errorf("failed to get repo root")
	}

	return wt.Filesystem.Root(), nil
}
