package git

import (
	"bufio"
	"bytes"
	"fmt"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"

	Log "code.gitea.io/gitea/modules/log"
)

type RepoKPIStats struct {
	Contributors        int64
	KeyContributors     int64
	DevelopAge          int64
	ContributorsAdded   int64
	Commits             int64
	CommitsAdded        int64
	CommitLinesModified int64
	WikiPages           int64
	Authors             []*UserKPITypeStats
}

type UserKPIStats struct {
	Name        string
	Email       string
	Commits     int64
	CommitLines int64
}
type UserKPITypeStats struct {
	UserKPIStats
	isNewContributor bool //是否是4个月内的新增贡献者
}

func SetDevelopAge(repoPath string, stats *RepoKPIStats, fromTime time.Time) error {
	since := fromTime.Format(time.RFC3339)
	args := []string{"log", "--no-merges", "--branches=*", "--format=%cd", "--date=short", fmt.Sprintf("--since='%s'", since)}
	stdout, err := NewCommand(args...).RunInDirBytes(repoPath)
	if err != nil {
		return err
	}
	scanner := bufio.NewScanner(bytes.NewReader(stdout))
	scanner.Split(bufio.ScanLines)
	developMonth := make(map[string]struct{})
	for scanner.Scan() {
		l := strings.TrimSpace(scanner.Text())
		month := l[0:strings.LastIndex(l, "-")]
		if _, ok := developMonth[month]; !ok {
			developMonth[month] = struct{}{}
		}
	}

	stats.DevelopAge = int64(len(developMonth))
	return nil
}

func GetUserKPIStats(repoPath string, startTime time.Time, endTime time.Time) (map[string]*UserKPIStats, error) {

	after := startTime.Format(time.RFC3339)
	until := endTime.Format(time.RFC3339)
	args := []string{"log", "--numstat", "--no-merges", "--branches=*", "--pretty=format:---%n%h%n%an%n%ae%n", "--date=iso", fmt.Sprintf("--after='%s'", after), fmt.Sprintf("--until='%s'", until)}
	stdout, err := NewCommand(args...).RunInDirBytes(repoPath)
	if err != nil {
		return nil, err
	}
	scanner := bufio.NewScanner(bytes.NewReader(stdout))
	scanner.Split(bufio.ScanLines)
	usersKPIStatses := make(map[string]*UserKPIStats)
	var author string
	p := 0
	var email string
	for scanner.Scan() {
		l := strings.TrimSpace(scanner.Text())
		if l == "---" {
			p = 1
		} else if p == 0 {
			continue
		} else {
			p++
		}
		if p > 4 && len(l) == 0 {
			continue
		}
		switch p {
		case 1: // Separator
		case 2: // Commit sha-1
		case 3: // Author
			author = l
		case 4: // E-mail
			email = strings.ToLower(l)
			if _, ok := usersKPIStatses[email]; !ok {
				usersKPIStatses[email] = &UserKPIStats{
					Name:        author,
					Email:       email,
					Commits:     0,
					CommitLines: 0,
				}
			}

			usersKPIStatses[email].Commits++
		default: // Changed file
			if parts := strings.Fields(l); len(parts) >= 3 {
				if parts[0] != "-" {
					if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil {
						usersKPIStatses[email].CommitLines += c
					}
				}
				if parts[1] != "-" {
					if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil {
						usersKPIStatses[email].CommitLines += c
					}
				}

			}
		}
	}

	return usersKPIStatses, nil

}

//获取一天内的用户贡献指标
func getUserKPIStats(repoPath string) (map[string]*UserKPIStats, error) {
	timeUntil := time.Now()
	oneDayAgo := timeUntil.AddDate(0, 0, -1)

	return GetUserKPIStats(repoPath, oneDayAgo, oneDayAgo)
}

func SetRepoKPIStats(repoPath string, fromTime time.Time, stats *RepoKPIStats, newContributers map[string]struct{}) error {
	since := fromTime.Format(time.RFC3339)
	args := []string{"log", "--numstat", "--no-merges", "HEAD", "--pretty=format:---%n%h%n%an%n%ae%n", "--date=iso", fmt.Sprintf("--since='%s'", since)}

	stdout, err := NewCommand(args...).RunInDirBytes(repoPath)
	if err != nil {
		return err
	}

	scanner := bufio.NewScanner(bytes.NewReader(stdout))
	scanner.Split(bufio.ScanLines)

	authors := make(map[string]*UserKPITypeStats)

	var author string
	p := 0
	var email string
	for scanner.Scan() {
		l := strings.TrimSpace(scanner.Text())
		if l == "---" {
			p = 1
		} else if p == 0 {
			continue
		} else {
			p++
		}
		if p > 4 && len(l) == 0 {
			continue
		}
		switch p {
		case 1: // Separator
		case 2: // Commit sha-1
			stats.CommitsAdded++
		case 3: // Author
			author = l
		case 4: // E-mail
			email = strings.ToLower(l)
			if _, ok := authors[email]; !ok {
				authors[email] = &UserKPITypeStats{
					UserKPIStats: UserKPIStats{
						Name:        author,
						Email:       email,
						Commits:     0,
						CommitLines: 0,
					},
					isNewContributor: false,
				}
			}
			if _, ok := newContributers[email]; ok {
				authors[email].isNewContributor = true
			}

			authors[email].Commits++
		default: // Changed file
			if parts := strings.Fields(l); len(parts) >= 3 {
				if parts[0] != "-" {
					if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil {
						stats.CommitLinesModified += c
						authors[email].CommitLines += c
					}
				}
				if parts[1] != "-" {
					if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil {
						stats.CommitLinesModified += c
						authors[email].CommitLines += c
					}
				}

			}
		}
	}

	a := make([]*UserKPITypeStats, 0, len(authors))
	for _, v := range authors {
		a = append(a, v)
	}
	// Sort authors descending depending on commit count
	sort.Slice(a, func(i, j int) bool {
		return a[i].Commits > a[j].Commits
	})

	stats.Authors = a
	return nil

}

func GetContributorsDetail(repoPath string, fromTime time.Time) ([]Contributor, error) {
	since := fromTime.Format(time.RFC3339)
	cmd := NewCommand("shortlog", "-sne", "HEAD", fmt.Sprintf("--since='%s'", since))
	stdout, err := cmd.RunInDir(repoPath)
	if err != nil {
		return nil, err
	}
	stdout = strings.Trim(stdout, "\n")
	contributorRows := strings.Split(stdout, "\n")
	if len(contributorRows) > 0 {
		contributorsInfo := make([]Contributor, len(contributorRows))
		for i := 0; i < len(contributorRows); i++ {
			var oneCount string = strings.Trim(contributorRows[i], " ")
			if strings.Index(oneCount, "\t") < 0 {
				continue
			}
			number := oneCount[0:strings.Index(oneCount, "\t")]
			commitCnt, _ := strconv.Atoi(number)
			committer := oneCount[strings.Index(oneCount, "\t")+1 : strings.LastIndex(oneCount, " ")]
			committer = strings.Trim(committer, " ")
			email := oneCount[strings.Index(oneCount, "<")+1 : strings.Index(oneCount, ">")]
			contributorsInfo[i] = Contributor{
				commitCnt, committer, email,
			}
		}
		return contributorsInfo, nil
	}
	return nil, nil
}

func SetWikiPages(wikiPath string, stats *RepoKPIStats) {
	wikiPages := 0

	if wikiPath == "" {
		stats.WikiPages = int64(wikiPages)
		return
	}

	wikiRepo, commit, err := findWikiRepoCommit(wikiPath)
	if err != nil {
		if !IsErrNotExist(err) {
			Log.Warn("GetBranchCommit", err)
		}
		stats.WikiPages = int64(wikiPages)
		return
	}

	// Get page list.
	entries, err := commit.ListEntries()
	if err != nil {
		if wikiRepo != nil {
			wikiRepo.Close()
		}
		Log.Warn("GetBranchCommit", err)
		stats.WikiPages = int64(wikiPages)
		return

	}

	for _, entry := range entries {
		if !entry.IsRegular() {
			continue
		}

		wikiName, err := filenameToName(entry.Name())
		if err != nil || wikiName == "_Sidebar" || wikiName == "_Footer" {
			continue
		}

		wikiPages += 1

	}
	//确保wikiRepo用完被关闭
	defer func() {
		if wikiRepo != nil {
			wikiRepo.Close()
		}
	}()
	stats.WikiPages = int64(wikiPages)
	return

}

func filenameToName(filename string) (string, error) {
	if !strings.HasSuffix(filename, ".md") {
		return "", fmt.Errorf("invalid file")
	}
	basename := filename[:len(filename)-3]
	unescaped, err := url.QueryUnescape(basename)
	if err != nil {
		return "", err
	}
	return strings.Replace(unescaped, "-", " ", -1), nil
}

func findWikiRepoCommit(wikiPath string) (*Repository, *Commit, error) {
	wikiRepo, err := OpenRepository(wikiPath)
	if err != nil {

		return nil, nil, err
	}

	commit, err := wikiRepo.GetBranchCommit("master")
	if err != nil {
		return wikiRepo, nil, err
	}
	return wikiRepo, commit, nil
}
