package metrics

import (
	"errors"
	"time"

	"go.uber.org/zap"

	identifier "code.justin.tv/amzn/TwitchProcessIdentifier"
	telemetry "code.justin.tv/amzn/TwitchTelemetry"
	cw "code.justin.tv/amzn/TwitchTelemetryCloudWatchMetricsSender"
	telemetryPoller "code.justin.tv/amzn/TwitchTelemetryPollingCollector"
	"code.justin.tv/websocket-edge/server/internal/logs"
)

const (
	flushPeriod         = 30 * time.Second
	bufferSize          = 100000
	aggregationPeriod   = time.Minute
	dimensionErrorCause = "ErrorCause"
)

var processIdentifier identifier.ProcessIdentifier

// Statter is the public interface used to measure metrics.
type Statter interface {
	Increment(metricName string, dimension ...string)
	IncrementAt(metricName string, ts time.Time, dimension ...string)
	IncrementErr(metricName string, errDimension string)
	IncrementErrAt(metricName string, errDimension string, ts time.Time)
	Timing(metricName string, duration time.Duration, dimensions ...string)
	TimingAt(metricName string, duration time.Duration, ts time.Time, dimensions ...string)
	Size(metricName string, size int, dimensions ...string)
	StartPoller(metricName string, valueFn func() float64, interval time.Duration)
	Close() error
}

type statter struct {
	logger        logs.Logger
	sampleBuilder *telemetry.SampleBuilder
	observer      telemetry.SampleObserver
}

func (s *statter) addDimensions(sample *telemetry.Sample, dimensions ...string) (*telemetry.Sample, error) {
	if len(dimensions)%2 != 0 {
		return nil, errors.New("Dimensions must be of even length")
	}

	for i := 0; i < len(dimensions); i += 2 {
		dim := dimensions[i]
		val := dimensions[i+1]
		sample.MetricID.AddDimension(dim, val)
		sample = withRollupDimension(sample, dim)
	}
	return sample, nil
}

func (s *statter) observe(metricName string, value float64, unit string, dimensions ...string) {
	sample, err := s.sampleBuilder.Build(metricName, value, unit)
	if err != nil {
		s.error(metricName, err)
		return
	}

	sample, err = s.addDimensions(sample, dimensions...)
	if err != nil {
		s.error(metricName, err)
		return
	}
	s.observer.ObserveSample(sample)
}

func (s *statter) observeAt(metricName string, value float64, unit string, ts time.Time, dimensions ...string) {
	sampleBuilder := &telemetry.SampleBuilder{
		ProcessIdentifier: processIdentifier,
		Timestamp:         ts,
	}
	sample, err := sampleBuilder.Build(metricName, value, unit)
	if err != nil {
		s.error(metricName, err)
		return
	}

	sample, err = s.addDimensions(sample, dimensions...)
	if err != nil {
		s.error(metricName, err)
		return
	}
	s.observer.ObserveSample(sample)
}

func (s *statter) error(metricName string, err error) {
	s.logger.Error("Error building sample.", zap.String("metric", metricName), zap.Error(err))
}

func (s *statter) Increment(metricName string, dimensions ...string) {
	s.observe(metricName, 1, telemetry.UnitCount, dimensions...)
}

func (s *statter) IncrementAt(metricName string, ts time.Time, dimensions ...string) {
	s.observeAt(metricName, 1, telemetry.UnitCount, ts, dimensions...)
}

func (s *statter) IncrementErr(metricName string, errDimension string) {
	s.observe(metricName, 1, telemetry.UnitCount, dimensionErrorCause, errDimension)
}

func (s *statter) IncrementErrAt(metricName string, errDimension string, ts time.Time) {
	s.observeAt(metricName, 1, telemetry.UnitCount, ts, dimensionErrorCause, errDimension)
}

func (s *statter) Timing(metricName string, duration time.Duration, dimensions ...string) {
	s.observe(metricName, duration.Seconds(), telemetry.UnitSeconds, dimensions...)
}

func (s *statter) TimingAt(metricName string, duration time.Duration, ts time.Time, dimensions ...string) {
	s.observeAt(metricName, duration.Seconds(), telemetry.UnitSeconds, ts, dimensions...)
}

func (s *statter) Size(metricName string, size int, dimensions ...string) {
	s.observe(metricName, float64(size), telemetry.UnitBytes, dimensions...)
}

// For each existing rollup, adds a new rollup with the inclusion of the passed in
// dimension. Also adds a rollup on the passed in dimension alone. Example:
// []
// withRollupDimension(A)
// [[A]]
// withRollupDimension(B)
// [[A], [A,B], [B]]
// withRollupDimension(C)
// [[A], [A,B], [B], [A,C], [A,B,C], [B,C], [C]]
func withRollupDimension(s *telemetry.Sample, dimension string) *telemetry.Sample {
	if s.RollupDimensions == nil {
		s.RollupDimensions = [][]string{}
	}
	for i := range s.RollupDimensions {
		s.RollupDimensions = append(s.RollupDimensions, append(s.RollupDimensions[i], dimension))
	}
	s.RollupDimensions = append(s.RollupDimensions, []string{dimension})
	return s
}

func (s *statter) Close() error {
	s.observer.Stop()
	return nil
}

// Creates and returns a Statter. Accepts service, stage, and the HEAD commit to initialize context.
func New(logger logs.Logger, serviceName, stage, gitCommit, awsRegion string) (*statter, error) {
	processIdentifier = identifier.ProcessIdentifier{
		Service: serviceName,
		Region:  awsRegion,
		Stage:   stage,
		Version: gitCommit,
	}
	sampleBuilder := &telemetry.SampleBuilder{
		ProcessIdentifier: processIdentifier,
	}

	sender := cw.NewUnbuffered(&processIdentifier, logger)
	observer := telemetry.NewBufferedAggregator(flushPeriod, bufferSize, aggregationPeriod, sender, logger)

	goStatsPoller := telemetryPoller.NewGoStatsPollingCollector(1*time.Minute, sampleBuilder, observer, logger)
	goStatsPoller.Start()

	statter := statter{
		logger:        logger,
		sampleBuilder: sampleBuilder,
		observer:      observer,
	}
	return &statter, nil
}
