package metrics

import (
	"context"
	"strings"

	"github.com/golang/protobuf/proto"
	structpb "github.com/golang/protobuf/ptypes/struct"
	"github.com/montanaflynn/stats"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"gopkg.in/fatih/set.v0"

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

func init() {
	info := &pb.MetricInfo{
		MetricId:        "production_bugs",
		MetricName:      "Production Bugs",
		Description:     "Open Production Bugs from JIRA",
		ValidForProject: true,
		ValidForTeam:    true,
		ValidForOrg:     true,
		KeyMetric:       true,
	}
	Registry().Register(info, NewProductionBugsCalculator)
}

// ProductionBugsCalculator to calculate the metric.
type ProductionBugsCalculator struct {
	ProjectMetadataServer pb.ProjectMetadataServiceServer
	EventServer           pb.EventServiceServer
}

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

func (p *ProductionBugsCalculator) getMatchTerms(ctx context.Context, req *pb.GetMetricRequest) (*set.Set, error) {
	matchTerms := set.New()

	addProject := func(projectID string) error {
		metadataResp, err := p.ProjectMetadataServer.GetMetadata(ctx, &pb.GetMetadataRequest{
			ProjectId: projectID,
		})
		if err != nil {
			return err
		}
		if metadataResp.Project == nil {
			return errf(codes.Internal,
				"project_name for project %s is unset", projectID)
		}
		matchTerms.Add(*metadataResp.Project.ProjectName)
		return nil
	}

	addTeam := func(team string) error {
		matchTerms.Add(team)
		metadataResp, err := p.ProjectMetadataServer.GetTeam(ctx, &pb.GetTeamRequest{
			TeamName: team,
		})
		if err != nil {
			return err
		}
		for _, projectMetadata := range metadataResp.Projects {
			if err := addProject(projectMetadata.GetProjectId()); err != nil {
				return err
			}
		}
		return nil
	}

	addOrg := func(org string) error {
		matchTerms.Add(org)
		metadataResp, err := p.ProjectMetadataServer.GetOrg(ctx, &pb.GetOrgRequest{
			OrgName: org,
		})
		if err != nil {
			return err
		}
		for _, teamEntry := range metadataResp.Teams {
			if err := addTeam(teamEntry.TeamName); err != nil {
				return err
			}
		}
		return nil
	}

	if len(req.ProjectId) > 0 {
		if err := addProject(req.ProjectId); err != nil {
			return nil, err
		}
	} else if len(req.Team) > 0 {
		if err := addTeam(req.Team); err != nil {
			return nil, err
		}
	} else if len(req.Org) > 0 {
		if err := addOrg(req.Org); err != nil {
			return nil, err
		}
	}

	return matchTerms, nil
}

func (p *ProductionBugsCalculator) calculateEntry(ctx context.Context, req *pb.GetMetricRequest, entry *pb.GetMetricResponse_TimeSeriesEntry, matchTerms *set.Set) error {
	queryResp, err := p.EventServer.QueryEvents(ctx, &pb.QueryEventsRequest{
		Timerange: entry.Timerange,
		Type:      "JIRAScraperEvent",
	})
	if err != nil {
		if grpc.Code(err) == codes.NotFound {
			return nil
		}
		return err
	}

	// accumulate list of numbers of open issues at points in time.
	var openIssues []float64

	for _, event := range queryResp.Events {
		jiraEvent := &eventspb.JIRAScraperEvent{}
		err = proto.Unmarshal(event.Body, jiraEvent)
		if err != nil {
			return errf(codes.Internal,
				"Error unmarshalling JIRA event: %v", err)
		}

		// These are jira projects, not RPS projects.
		totalProductionBugs := float64(0)
		hasMatches := false
		for _, bugsPerProject := range jiraEvent.ProductionBugsPerProject {
			// "/" characters in the project name are replaced with "-" characters
			// because the restful API cannot query for terms containing "/".
			projectName := strings.Replace(bugsPerProject.Project, "/", "-", -1)
			if matchTerms.Has(projectName) {
				totalProductionBugs += float64(bugsPerProject.TotalProductionBugs)
				hasMatches = true
			}
		}
		if hasMatches {
			openIssues = append(openIssues, totalProductionBugs)
		}
	}

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

	median, err := stats.Median(openIssues)
	if err != nil {
		return errf(codes.Internal, "Error calculating median value: %v", err)
	}

	entry.Value = &structpb.Value{
		Kind: &structpb.Value_NumberValue{
			NumberValue: median,
		},
	}

	return nil
}

// Calculate the metric's time series and fill out the response.
func (p *ProductionBugsCalculator) Calculate(ctx context.Context, req *pb.GetMetricRequest, resp *pb.GetMetricResponse) error {
	matchTerms, err := p.getMatchTerms(ctx, req)
	if err != nil {
		return err
	}

	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 = "Issues"

	sem := make(chan error, len(timeSeries))
	for _, bucket := range timeSeries {
		go func(ctx context.Context, req *pb.GetMetricRequest, entry *pb.GetMetricResponse_TimeSeriesEntry) {
			sem <- p.calculateEntry(ctx, req, entry, matchTerms)
		}(ctx, req, bucket)
	}
	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
}
