// Copyright 2019 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"
	"html/template"
	"io"
	"log"
	"math"
	"net/http"
	"strings"
	"time"

	"golang.org/x/build/maintner"
)

// handleStats serves dev.golang.org/stats.
func (s *server) handleStats(t *template.Template, w http.ResponseWriter, r *http.Request) {
	s.cMu.RLock()
	dirty := s.data.stats.dirty
	s.cMu.RUnlock()
	if dirty {
		s.updateStatsData()
	}

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	var buf bytes.Buffer
	s.cMu.RLock()
	defer s.cMu.RUnlock()
	data := struct {
		DataJSON interface{}
	}{
		DataJSON: s.data.stats,
	}
	if err := t.Execute(&buf, data); 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
	}
}

type statsData struct {
	Charts []*chart

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

// A chart holds data used by the Google Charts JavaScript API to render
// an interactive visualization.
type chart struct {
	Title   string          `json:"title"`
	Columns []*chartColumn  `json:"columns"`
	Data    [][]interface{} `json:"data"`
}

// A chartColumn is analogous to a Google Charts DataTable column.
type chartColumn struct {
	// Type is the data type of the values of the column.
	// Supported values are 'string', 'number', 'boolean',
	// 'timeofday', 'date', and 'datetime'.
	Type string `json:"type"`

	// Label is an optional label for the column.
	Label string `json:"label"`
}

func (s *server) updateStatsData() {
	log.Println("Updating stats data ...")
	s.cMu.Lock()
	defer s.cMu.Unlock()

	var (
		windowStart = time.Now().Add(-1 * 365 * 24 * time.Hour)
		intervals   []*clInterval
	)
	s.corpus.Gerrit().ForeachProjectUnsorted(filterProjects(func(p *maintner.GerritProject) error {
		p.ForeachCLUnsorted(withoutDeletedCLs(p, func(cl *maintner.GerritCL) error {
			closed := cl.Status == "merged" || cl.Status == "abandoned"

			// Discard CL if closed and last updated before windowStart.
			if closed && cl.Meta.Commit.CommitTime.Before(windowStart) {
				return nil
			}
			intervals = append(intervals, newIntervalFromCL(cl))
			return nil
		}))
		return nil
	}))

	var chartData [][]interface{}
	for t0, t1 := windowStart, windowStart.Add(24*time.Hour); t0.Before(time.Now()); t0, t1 = t0.Add(24*time.Hour), t1.Add(24*time.Hour) {
		var (
			open       int
			withIssues int
		)

		for _, i := range intervals {
			if !i.intersects(t0, t1) {
				continue
			}
			open++
			if len(i.cl.GitHubIssueRefs) > 0 {
				withIssues++
			}
		}
		chartData = append(chartData, []interface{}{
			t0, open, withIssues,
		})
	}
	cols := []*chartColumn{
		{Type: "date", Label: "date"},
		{Type: "number", Label: "All CLs"},
		{Type: "number", Label: "With issues"},
	}
	var charts []*chart
	charts = append(charts, &chart{
		Title:   "Open CLs (1 Year)",
		Columns: cols,
		Data:    chartData,
	})
	charts = append(charts, &chart{
		Title:   "Open CLs (30 Days)",
		Columns: cols,
		Data:    chartData[len(chartData)-30:],
	})
	charts = append(charts, &chart{
		Title:   "Open CLs (7 Days)",
		Columns: cols,
		Data:    chartData[len(chartData)-7:],
	})
	s.data.stats.Charts = charts
}

// A clInterval describes a time period during which a CL is open.
// points on the interval are seconds since the epoch.
type clInterval struct {
	start, end int64 // seconds since epoch
	cl         *maintner.GerritCL
}

// returns true iff the interval contains any seconds
// in the timespan [t0,t1]. t0 must be before t1.
func (i *clInterval) intersects(t0, t1 time.Time) bool {
	if t1.Before(t0) {
		panic("t0 cannot be before t1")
	}
	return i.end >= t0.Unix() && i.start <= t1.Unix()
}

func newIntervalFromCL(cl *maintner.GerritCL) *clInterval {
	interval := &clInterval{
		start: cl.Created.Unix(),
		end:   math.MaxInt64,
		cl:    cl,
	}

	closed := cl.Status == "merged" || cl.Status == "abandoned"
	if closed {
		for i := len(cl.Metas) - 1; i >= 0; i-- {
			if !strings.Contains(cl.Metas[i].Commit.Msg, "autogenerated:gerrit") {
				continue
			}

			if strings.Contains(cl.Metas[i].Commit.Msg, "autogenerated:gerrit:merged") ||
				strings.Contains(cl.Metas[i].Commit.Msg, "autogenerated:gerrit:abandon") {
				interval.end = cl.Metas[i].Commit.CommitTime.Unix()
			}
		}
		if interval.end == math.MaxInt64 {
			log.Printf("Unable to determine close time of CL: %+v", cl)
		}
	}
	return interval
}
