package metrics

import (
	"context"
	"strings"

	log "github.com/Sirupsen/logrus"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"

	"code.justin.tv/common/config"

	"code.justin.tv/dta/rockpaperscissors/internal/api/contextlogger"
	pb "code.justin.tv/dta/rockpaperscissors/proto"
)

var (
	errf = grpc.Errorf
)

func init() {
	config.Register(map[string]string{
		// Comma separated list of metric IDs to disable.
		"disabled-metrics": "",
	})
}

type dispatcherEntry struct {
	Info       *pb.MetricInfo
	Calculator Calculator
}

// Dispatcher dispatches metric calculation requests to the correct calculator.
type Dispatcher map[string]dispatcherEntry

// DispatcherInterface is promised interface for Dispatcher, used for testing.
type DispatcherInterface interface {
	List() []*pb.MetricInfo
	Get(string) *pb.MetricInfo
	Calculate(context.Context, *pb.GetMetricRequest) (*pb.GetMetricResponse, error)
}

// NewDispatcher is a dispatcher for sending metrics calculation requests to the right calculator.
func NewDispatcher(projectMetadataServer pb.ProjectMetadataServiceServer, eventServer pb.EventServiceServer) (*Dispatcher, error) {
	// Make "set" of disabled metrics from the disabled-metrics flag.
	disabledMetrics := make(map[string]struct{})
	for _, metric := range strings.Split(config.Resolve("disabled-metrics"), ",") {
		disabledMetrics[metric] = struct{}{}
	}

	d := make(Dispatcher)
	for name, entry := range *Registry() {
		if _, ok := disabledMetrics[name]; ok {
			log.Infof("Disabling metric: %s", name)
			continue
		}

		calculator, err := entry.Factory(projectMetadataServer, eventServer)
		if err != nil {
			return nil, err
		}
		d[name] = dispatcherEntry{
			Info:       entry.Info,
			Calculator: calculator,
		}
	}
	return &d, nil
}

// List the enabled metrics calculators.
func (d *Dispatcher) List() []*pb.MetricInfo {
	v := make([]*pb.MetricInfo, 0, len(*d))
	for _, entry := range *d {
		v = append(v, entry.Info)
	}
	return v
}

// Get information about a metrics calculator.
func (d *Dispatcher) Get(name string) *pb.MetricInfo {
	return (*d)[name].Info
}

// Calculate a metric by dispatching the request to the relevant metrics calculator.
func (d *Dispatcher) Calculate(ctx context.Context, req *pb.GetMetricRequest) (*pb.GetMetricResponse, error) {
	entry := (*d)[req.MetricId]
	if entry.Info == nil {
		return nil, errf(codes.NotFound, "Metric %q not found", req.MetricId)
	}

	_ = contextlogger.SetContextLogField(ctx, "metric", req.MetricId)

	selectorFieldsSet := 0
	if len(req.ProjectId) > 0 {
		selectorFieldsSet++
		if !entry.Info.ValidForProject {
			return nil, errf(codes.Unimplemented,
				"%s doesn't support selecting by project_id", entry.Info.MetricId)
		}
		_ = contextlogger.SetContextLogField(ctx, "project", req.ProjectId)
	}
	if len(req.Developer) > 0 {
		selectorFieldsSet++
		if !entry.Info.ValidForDeveloper {
			return nil, errf(codes.Unimplemented,
				"%s doesn't support selecting by developer", entry.Info.MetricId)
		}
	}
	if len(req.Team) > 0 {
		selectorFieldsSet++
		if !entry.Info.ValidForTeam {
			return nil, errf(codes.Unimplemented,
				"%s doesn't support selecting by team", entry.Info.MetricId)
		}
	}
	if len(req.Org) > 0 {
		selectorFieldsSet++
		if !entry.Info.ValidForOrg {
			return nil, errf(codes.Unimplemented,
				"%s doesn't support selecting by org", entry.Info.MetricId)
		}
	}

	if selectorFieldsSet != 1 {
		validFields := make([]string, 0, 4)
		if entry.Info.ValidForProject {
			validFields = append(validFields, "project_id")
		}
		if entry.Info.ValidForDeveloper {
			validFields = append(validFields, "developer")
		}
		if entry.Info.ValidForTeam {
			validFields = append(validFields, "team")
		}
		if entry.Info.ValidForOrg {
			validFields = append(validFields, "org")
		}
		return nil, errf(codes.InvalidArgument,
			"Only one of the following fields are required: %s",
			strings.Join(validFields, ", "))
	}

	resp := &pb.GetMetricResponse{
		MetricInfo: entry.Info,
	}

	err := entry.Calculator.Calculate(ctx, req, resp)
	return resp, err
}
