package metrics

import (
	"context"

	"github.com/golang/protobuf/ptypes"
	structpb "github.com/golang/protobuf/ptypes/struct"
	"github.com/montanaflynn/stats"
	"google.golang.org/grpc/codes"

	pb "code.justin.tv/dta/rockpaperscissors/proto"
)

func init() {
	info := &pb.MetricInfo{
		MetricId:        "avg_pull_request_open_time",
		MetricName:      "Avg PR Open Time",
		Description:     "Average time pull requests are open",
		ValidForProject: true,
		KeyMetric:       true,
	}
	Registry().Register(info, NewAvgPullRequestOpenTimeCalculator)
}

// AvgPullRequestOpenTimeCalculator to calculate how many Jenkins builds a project made.
type AvgPullRequestOpenTimeCalculator struct {
	ProjectMetadataServer pb.ProjectMetadataServiceServer
	EventServer           pb.EventServiceServer
}

// NewAvgPullRequestOpenTimeCalculator factory for AvgPullRequestOpenTimeCalculator structs.
func NewAvgPullRequestOpenTimeCalculator(projectMetadataServer pb.ProjectMetadataServiceServer, eventServer pb.EventServiceServer) (Calculator, error) {
	return &AvgPullRequestOpenTimeCalculator{
		ProjectMetadataServer: projectMetadataServer,
		EventServer:           eventServer,
	}, nil
}

func (p *AvgPullRequestOpenTimeCalculator) getDataPointsForPullRequest(pullRequest *pullRequestOpenPeriod, timerange *pb.TimeRange) ([]float64, error) {
	isOpen, err := pullRequest.isOpenDuring(timerange)
	if err != nil {
		return nil, err
	}
	if isOpen {
		openDuration := pullRequest.Closed.Sub(pullRequest.Opened)
		return []float64{openDuration.Hours()}, nil
	}
	return nil, nil
}

func (p *AvgPullRequestOpenTimeCalculator) calculateEntry(ctx context.Context, req *pb.GetMetricRequest, entry *pb.GetMetricResponse_TimeSeriesEntry, pullRequests *pullRequestMap) error {
	var dataPoints []float64

	for pullRequest := range pullRequests.Iter() {
		pullRequestDataPoints, err := p.getDataPointsForPullRequest(pullRequest, entry.GetTimerange())
		if err != nil {
			return err
		}
		if pullRequestDataPoints != nil {
			dataPoints = append(dataPoints, pullRequestDataPoints...)
		}
	}

	if len(dataPoints) == 0 {
		return nil
	}

	avg, err := stats.Mean(dataPoints)
	if err != nil {
		return errf(codes.Internal, "Error calculating sum: %v", err)
	}

	entry.Value = &structpb.Value{
		Kind: &structpb.Value_NumberValue{
			NumberValue: RoundPlus(avg, 2),
		},
	}

	return nil
}

// Calculate the time series and fill out the response.
func (p *AvgPullRequestOpenTimeCalculator) Calculate(ctx context.Context, req *pb.GetMetricRequest, resp *pb.GetMetricResponse) error {
	timeSeries, err := makeTimeSeries(req.Timerange, req.BucketSize, req.IanaTimeZone)
	if err != nil {
		return errf(codes.InvalidArgument,
			"Found inappropriate time range: %v", err)
	}
	resp.TimeSeries = timeSeries
	resp.TimeSeriesUnits = "Hours"

	githubRepos, err := getGitHubReposForProject(ctx, p.ProjectMetadataServer, req.ProjectId)
	if err != nil {
		return err
	}
	if len(githubRepos) == 0 {
		return nil
	}

	pullRequests := newPullRequestMap(p.EventServer)
	pullRequestMapEndTime, err := ptypes.Timestamp(req.GetTimerange().GetEnd())
	if err != nil {
		return err
	}
	err = pullRequests.UpdateFromDatastore(ctx, githubRepos, pullRequestMapEndTime)
	if err != nil {
		return err
	}

	sem := make(chan error, len(timeSeries))
	for _, bucket := range timeSeries {
		go func(ctx context.Context, req *pb.GetMetricRequest, entry *pb.GetMetricResponse_TimeSeriesEntry, pullRequests *pullRequestMap) {
			sem <- p.calculateEntry(ctx, req, entry, pullRequests)
		}(ctx, req, bucket, pullRequests)
	}
	for i := 0; i < len(timeSeries); i++ {
		err := <-sem
		if err != nil {
			// TODO: cancel the context to stop any still-processing work?
			return err
		}
	}

	return nil
}
