package redis

import (
	"context"
	"fmt"
	"time"

	telemetry "code.justin.tv/amzn/TwitchTelemetry"
	goredis "github.com/go-redis/redis/v7"
)

var startTimeCtxKey = new(int)

type redisMiddleware struct {
	sampleReporter *telemetry.SampleReporter
}

// redisMiddleware implements the goredis.Hook interface.
var _ goredis.Hook = (*redisMiddleware)(nil)

func newRedisMiddleware(sampleReporter telemetry.SampleReporter, dependencyName string) *redisMiddleware {
	sampleReporter.DependencyProcessIdentifier.Service = dependencyName

	return &redisMiddleware{
		sampleReporter: &sampleReporter,
	}
}

func (r *redisMiddleware) BeforeProcess(ctx context.Context, cmd goredis.Cmder) (context.Context, error) {
	startTime := time.Now()
	ctx = contextWithStartTime(ctx, startTime)
	return ctx, nil
}

func (r *redisMiddleware) AfterProcess(ctx context.Context, cmd goredis.Cmder) error {
	sampleReporter := telemetry.SampleReporterWithContext(*r.sampleReporter, ctx)
	sampleReporter.DependencyOperationName = cmd.Name()

	code := getAvailabilityCode(cmd.Err())
	sampleReporter.ReportAvailabilitySamples(code)

	startTime := getStartTime(ctx)
	if startTime != nil {
		duration := time.Since(*startTime)
		sampleReporter.ReportDurationSample(telemetry.MetricDependencyDuration, duration)
	}

	return nil
}

func (r *redisMiddleware) BeforeProcessPipeline(ctx context.Context, cmds []goredis.Cmder) (context.Context, error) {
	startTime := time.Now()
	ctx = contextWithStartTime(ctx, startTime)
	return ctx, nil
}

func (r *redisMiddleware) AfterProcessPipeline(ctx context.Context, cmds []goredis.Cmder) error {
	sampleReporter := telemetry.SampleReporterWithContext(*r.sampleReporter, ctx)
	startTime := getStartTime(ctx)

	for _, cmd := range cmds {
		// Prefix the dependency name so that we can differentiate between standalone commands and commands
		// running in a pipeline. We want to differentiate between the two because the latency for pipelined
		// commands will be affected by the number of commands in the pipeline.
		sampleReporter.DependencyOperationName = fmt.Sprintf("pipeline:%s", cmd.Name())

		code := getAvailabilityCode(cmd.Err())
		sampleReporter.ReportAvailabilitySamples(code)

		if startTime != nil {
			duration := time.Since(*startTime)
			sampleReporter.ReportDurationSample(telemetry.MetricDependencyDuration, duration)
		}
	}
	return nil
}

func getStartTime(ctx context.Context) *time.Time {
	val := ctx.Value(startTimeCtxKey)
	if val == nil {
		return nil
	}

	return val.(*time.Time)
}

func contextWithStartTime(ctx context.Context, startTime time.Time) context.Context {
	return context.WithValue(ctx, startTimeCtxKey, &startTime)
}

func getAvailabilityCode(err error) telemetry.AvailabilityCode {
	switch err {
	case nil:
		return telemetry.AvailabilityCodeSucccess
	case goredis.Nil:
		return telemetry.AvailabilityCodeSucccess
	case context.Canceled:
		return telemetry.AvailabilityCodeSucccess
	case context.DeadlineExceeded:
		return telemetry.AvailabilityCodeServerError
	default:
		return telemetry.AvailabilityCodeServerError
	}
}
