package git

import (
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"os/user"
	"path/filepath"
	"strings"

	"coopcloud.tech/abra/pkg/log"
	"github.com/go-git/go-git/v5"
	gitConfigPkg "github.com/go-git/go-git/v5/config"
	"github.com/go-git/go-git/v5/plumbing/format/gitignore"
)

// IsClean checks if a repo has unstaged changes
func IsClean(repoPath string) (bool, error) {
	repo, err := git.PlainOpen(repoPath)
	if err != nil {
		if errors.Is(err, git.ErrRepositoryNotExists) {
			return false, git.ErrRepositoryNotExists
		}

		return false, fmt.Errorf("unable to open %s: %s", repoPath, err)
	}

	worktree, err := repo.Worktree()
	if err != nil {
		return false, fmt.Errorf("unable to open worktree of %s: %s", repoPath, err)
	}

	patterns, err := GetExcludesFiles()
	if err != nil {
		return false, err
	}

	if len(patterns) > 0 {
		worktree.Excludes = append(patterns, worktree.Excludes...)
	}

	status, err := worktree.Status()
	if err != nil {
		return false, fmt.Errorf("unable to query status of %s: %s", repoPath, err)
	}

	if status.String() != "" {
		noNewline := strings.TrimSuffix(status.String(), "\n")
		log.Debugf("git status: %s: %s", repoPath, noNewline)
	} else {
		log.Debugf("git status: %s: clean", repoPath)
	}

	return status.IsClean(), nil
}

// GetExcludesFiles reads the exlude files from a global gitignore
func GetExcludesFiles() ([]gitignore.Pattern, error) {
	var err error
	var patterns []gitignore.Pattern

	cfg, err := parseGitConfig()
	if err != nil {
		return patterns, err
	}

	excludesfile := getExcludesFile(cfg)
	patterns, err = parseExcludesFile(excludesfile)
	if err != nil {
		return patterns, err
	}

	return patterns, nil
}

func parseGitConfig() (*gitConfigPkg.Config, error) {
	cfg := gitConfigPkg.NewConfig()

	usr, err := user.Current()
	if err != nil {
		return nil, err
	}

	globalGitConfig := filepath.Join(usr.HomeDir, ".gitconfig")
	if _, err := os.Stat(globalGitConfig); err != nil {
		if os.IsNotExist(err) {
			log.Debugf("no %s exists, not reading any global gitignore config", globalGitConfig)
			return cfg, nil
		}
		return cfg, err
	}

	b, err := ioutil.ReadFile(globalGitConfig)
	if err != nil {
		return nil, err
	}

	if err := cfg.Unmarshal(b); err != nil {
		return nil, err
	}

	return cfg, err
}

func getExcludesFile(cfg *gitConfigPkg.Config) string {
	for _, sec := range cfg.Raw.Sections {
		if sec.Name == "core" {
			for _, opt := range sec.Options {
				if opt.Key == "excludesfile" {
					return opt.Value
				}
			}
		}
	}

	return "~/.gitignore"
}

func parseExcludesFile(excludesfile string) ([]gitignore.Pattern, error) {
	var ps []gitignore.Pattern

	excludesfile, err := expandTilde(excludesfile)
	if err != nil {
		return nil, err
	}

	if _, err := os.Stat(excludesfile); err != nil {
		if os.IsNotExist(err) {
			log.Debugf("no %s exists, skipping reading gitignore paths", excludesfile)
			return ps, nil
		}
		return ps, err
	}

	data, err := ioutil.ReadFile(excludesfile)
	if err != nil {
		return nil, err
	}

	var pathsRaw []string
	for _, s := range strings.Split(string(data), "\n") {
		if !strings.HasPrefix(s, "#") && len(strings.TrimSpace(s)) > 0 {
			pathsRaw = append(pathsRaw, s)
			ps = append(ps, gitignore.ParsePattern(s, nil))
		}
	}

	log.Debugf("read global ignore paths: %s", strings.Join(pathsRaw, " "))

	return ps, nil
}

func expandTilde(path string) (string, error) {
	if !strings.HasPrefix(path, "~") {
		return path, nil
	}

	var paths []string
	u, err := user.Current()
	if err != nil {
		return "", err
	}

	for _, p := range strings.Split(path, string(filepath.Separator)) {
		if p == "~" {
			paths = append(paths, u.HomeDir)
		} else {
			paths = append(paths, p)
		}
	}

	return filepath.Join(paths...), nil
}
