// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"bytes"
	"fmt"
	"html/template"
	"io"
	"log"
	"net/http"
	"sort"
	"strings"
	"time"

	"golang.org/x/build/internal/foreach"
	"golang.org/x/build/internal/gophers"
	"golang.org/x/build/maintner"
)

type project struct {
	*maintner.GerritProject
	Changes []*change
}

// ReviewServer returns the hostname of the review server for a googlesource repo,
// e.g. "go-review.googlesource.com" for a "go.googlesource.com" server. For a
// non-googlesource.com server, it will return an empty string.
func (p *project) ReviewServer() string {
	const d = ".googlesource.com"
	s := p.Server()
	i := strings.Index(s, d)
	if i == -1 {
		return ""
	}
	return s[:i] + "-review" + d
}

type change struct {
	*maintner.GerritCL
	LastUpdate          time.Time
	FormattedLastUpdate string

	HasPlusTwo       bool
	HasPlusOne       bool
	HasMinusOne      bool
	HasMinusTwo      bool
	NoHumanComments  bool
	TryBotMinusOne   bool
	TryBotPlusOne    bool
	SearchTerms      string
	ReleaseMilestone string
}

type reviewsData struct {
	Projects     []*project
	TotalChanges int

	// dirty is set if this data needs to be updated due to a corpus change.
	dirty bool
}

// handleReviews serves dev.golang.org/reviews.
func (s *server) handleReviews(t *template.Template, w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	s.cMu.RLock()
	dirty := s.data.reviews.dirty
	s.cMu.RUnlock()
	if dirty {
		s.updateReviewsData()
	}

	s.cMu.RLock()
	defer s.cMu.RUnlock()

	ownerFilter := r.FormValue("owner")
	var (
		projects     []*project
		totalChanges int
	)
	if len(ownerFilter) > 0 {
		for _, p := range s.data.reviews.Projects {
			var cs []*change
			for _, c := range p.Changes {
				if o := c.Owner(); o != nil && o.Name() == ownerFilter {
					cs = append(cs, c)
					totalChanges++
				}
			}
			if len(cs) > 0 {
				projects = append(projects, &project{GerritProject: p.GerritProject, Changes: cs})
			}
		}
	} else {
		projects = s.data.reviews.Projects
		totalChanges = s.data.reviews.TotalChanges
	}

	var buf bytes.Buffer
	if err := t.Execute(&buf, struct {
		Projects     []*project
		TotalChanges int
	}{
		Projects:     projects,
		TotalChanges: totalChanges,
	}); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if _, err := io.Copy(w, &buf); err != nil {
		log.Printf("io.Copy(w, %+v) = %v", buf, err)
		return
	}
}

func (s *server) updateReviewsData() {
	log.Println("Updating reviews data ...")
	s.cMu.Lock()
	defer s.cMu.Unlock()
	var (
		projects     []*project
		totalChanges int
	)
	s.corpus.Gerrit().ForeachProjectUnsorted(filterProjects(func(p *maintner.GerritProject) error {
		proj := &project{GerritProject: p}
		p.ForeachOpenCL(withoutDeletedCLs(p, func(cl *maintner.GerritCL) error {
			if cl.WorkInProgress() ||
				cl.Owner() == nil ||
				strings.Contains(cl.Commit.Msg, "DO NOT REVIEW") {
				return nil
			}
			var searchTerms []string
			tags := cl.Meta.Hashtags()
			if tags.Contains("wait-author") ||
				tags.Contains("wait-release") ||
				tags.Contains("wait-issue") {
				return nil
			}
			c := &change{GerritCL: cl}
			searchTerms = append(searchTerms, "repo:"+p.Project())
			searchTerms = append(searchTerms, cl.Owner().Name())
			searchTerms = append(searchTerms, "owner:"+cl.Owner().Email())
			searchTerms = append(searchTerms, "involves:"+cl.Owner().Email())
			searchTerms = append(searchTerms, fmt.Sprint(cl.Number))
			searchTerms = append(searchTerms, cl.Subject())

			c.NoHumanComments = !hasHumanComments(cl)
			if c.NoHumanComments {
				searchTerms = append(searchTerms, "t:attn")
			}

			const releaseMilestonePrefix = "Go"
			for _, ref := range cl.GitHubIssueRefs {
				issue := ref.Repo.Issue(ref.Number)
				if issue != nil &&
					issue.Milestone != nil &&
					strings.HasPrefix(issue.Milestone.Title, releaseMilestonePrefix) {
					c.ReleaseMilestone = issue.Milestone.Title[len(releaseMilestonePrefix):]
				}
			}
			if c.ReleaseMilestone != "" {
				searchTerms = append(searchTerms, "release:"+c.ReleaseMilestone)
			}

			searchTerms = append(searchTerms, searchTermsFromReviewerFields(cl)...)
			for label, votes := range cl.Metas[len(cl.Metas)-1].LabelVotes() {
				for _, val := range votes {
					if label == "Code-Review" {
						switch val {
						case -2:
							c.HasMinusTwo = true
							searchTerms = append(searchTerms, "t:-2")
						case -1:
							c.HasMinusOne = true
							searchTerms = append(searchTerms, "t:-1")
						case 1:
							c.HasPlusOne = true
							searchTerms = append(searchTerms, "t:+1")
						case 2:
							c.HasPlusTwo = true
							searchTerms = append(searchTerms, "t:+2")
						}
					}
					if label == "TryBot-Result" {
						switch val {
						case -1:
							c.TryBotMinusOne = true
							searchTerms = append(searchTerms, "trybot:-1")
						case 1:
							c.TryBotPlusOne = true
							searchTerms = append(searchTerms, "trybot:+1")
						}
					}
				}
			}

			c.LastUpdate = cl.Commit.CommitTime
			if len(cl.Messages) > 0 {
				c.LastUpdate = cl.Messages[len(cl.Messages)-1].Date
			}
			c.FormattedLastUpdate = c.LastUpdate.Format("2006-01-02")
			searchTerms = append(searchTerms, c.FormattedLastUpdate)
			c.SearchTerms = strings.ToLower(strings.Join(searchTerms, " "))
			proj.Changes = append(proj.Changes, c)
			totalChanges++
			return nil
		}))
		sort.Slice(proj.Changes, func(i, j int) bool {
			return proj.Changes[i].LastUpdate.Before(proj.Changes[j].LastUpdate)
		})
		projects = append(projects, proj)
		return nil
	}))
	sort.Slice(projects, func(i, j int) bool {
		return projects[i].Project() < projects[j].Project()
	})
	s.data.reviews.Projects = projects
	s.data.reviews.TotalChanges = totalChanges
	s.data.reviews.dirty = false
}

// hasHumanComments reports whether cl has any comments from a human on it.
func hasHumanComments(cl *maintner.GerritCL) bool {
	const (
		gobotID     = "5976@62eb7196-b449-3ce5-99f1-c037f21e1705"
		gerritbotID = "12446@62eb7196-b449-3ce5-99f1-c037f21e1705"
	)

	for _, m := range cl.Messages {
		if email := m.Author.Email(); email != gobotID && email != gerritbotID {
			return true
		}
	}
	return false
}

// searchTermsFromReviewerFields returns a slice of terms generated from
// the reviewer and cc fields of a Gerrit change.
func searchTermsFromReviewerFields(cl *maintner.GerritCL) []string {
	var searchTerms []string
	reviewers := make(map[string]bool)
	ccs := make(map[string]bool)
	for _, m := range cl.Metas {
		if !strings.Contains(m.Commit.Msg, "Reviewer:") &&
			!strings.Contains(m.Commit.Msg, "CC:") &&
			!strings.Contains(m.Commit.Msg, "Removed:") {
			continue
		}
		foreach.LineStr(m.Commit.Msg, func(ln string) error {
			if !strings.HasPrefix(ln, "Reviewer:") &&
				!strings.HasPrefix(ln, "CC:") &&
				!strings.HasPrefix(ln, "Removed:") {
				return nil
			}
			gerritID := ln[strings.LastIndexByte(ln, '<')+1 : strings.LastIndexByte(ln, '>')]
			if strings.HasPrefix(ln, "Removed:") {
				delete(reviewers, gerritID)
				delete(ccs, gerritID)
			} else if strings.HasPrefix(ln, "Reviewer:") {
				delete(ccs, gerritID)
				reviewers[gerritID] = true
			} else if strings.HasPrefix(ln, "CC:") {
				delete(reviewers, gerritID)
				ccs[gerritID] = true
			}
			return nil
		})
	}
	for r := range reviewers {
		if p := gophers.GetPerson(r); p != nil && p.Gerrit != cl.Owner().Email() {
			searchTerms = append(searchTerms, "involves:"+p.Gerrit)
			searchTerms = append(searchTerms, "reviewer:"+p.Gerrit)
		}
	}
	for r := range ccs {
		if p := gophers.GetPerson(r); p != nil && p.Gerrit != cl.Owner().Email() {
			searchTerms = append(searchTerms, "involves:"+p.Gerrit)
			searchTerms = append(searchTerms, "cc:"+p.Gerrit)
		}
	}
	return searchTerms
}
