package middleware

import (
	"context"
	"fmt"
	"github.com/twitchtv/twirp"
	"time"

	identifier "code.justin.tv/amzn/TwitchProcessIdentifier"
	telemetry "code.justin.tv/amzn/TwitchTelemetry"
	"code.justin.tv/video/metrics-middleware/v2/operation"
)

// OperationMonitor monitors operation spans, converts them into samples, and reports them.
type OperationMonitor struct {
	// SampleReporter is used to report samples for the operations.
	SampleReporter telemetry.SampleReporter

	// When AutoFlush is true, the observer is flushed after a KindServer operation completes.
	// This prevents the observer from efficiently batching metrics for multiple operations together
	// and is intended for AWS Lambda where all metrics need to be processed before the response is sent.
	AutoFlush bool

	// MonitorConfig is used for additional middleware customization
	MonitorConfig MonitorConfig
}

// MonitorConfig is used to tune metrics middleware behavior
type MonitorConfig struct {
	// When HTTPErrorCodes is true, the closest HTTP code is additionally returned on failures
	// (in addition to the default Success, ClientError, ServerError metrics)
	HTTPErrorCodes bool

	// When TwirpErrorCodes is true, the Twirp error code is additionally returned on failures
	// (in addition to the default Success, ClientError, ServerError metrics)
	TwirpErrorCodes bool
}

// MonitorOp monitors operation spans and processes their reports when they complete.
func (monitor *OperationMonitor) MonitorOp(parent context.Context, name operation.Name) (ctx context.Context, monitorPoints *operation.MonitorPoints) {
	ctx = parent
	monitorPoints = &operation.MonitorPoints{}

	switch name.Kind {
	// Internal spans are converted into timer metrics.
	case operation.KindSpan:
		monitorPoints.End = func(report *operation.Report) { monitor.sendTimerMetric(parent, name.Method, report) }

	// For server spans, the start time and operation name are added to the context.
	// Upon completion, duration and availability metrics are emitted.
	case operation.KindServer:
		timestamp := time.Now()
		ctx = telemetry.ContextWithTimestamp(parent, timestamp)
		ctx = telemetry.ContextWithOperationName(ctx, name.Method)
		monitorPoints.End = func(report *operation.Report) {
			monitor.sendRequestMetrics(name.Method, report, timestamp)
			if monitor.AutoFlush {
				monitor.SampleReporter.Flush()
			}
		}

	// Client spans are converted into dependency metrics.
	case operation.KindClient:
		monitorPoints.End = func(report *operation.Report) { monitor.sendDependencyMetric(parent, name, report) }
	}
	return
}

// sendRequestMetrics converts a report into request metrics and sends them to the observer.
func (monitor *OperationMonitor) sendRequestMetrics(operationName string, report *operation.Report, timestamp time.Time) {
	reporter := monitor.SampleReporter
	reporter.OperationName = operationName
	reporter.Timestamp = timestamp

	reporter.ReportDurationSample(telemetry.MetricDuration, report.EndTime.Sub(report.StartTime))
	reporter.ReportAvailabilitySamples(availabilityFromStatus(report.Status))
	if monitor.MonitorConfig.HTTPErrorCodes {
		metricName := constructHTTPMetric(report.Status)
		if metricName != "" {
			reporter.Report(metricName, 1.0, telemetry.UnitCount)
		}
	}
	if monitor.MonitorConfig.TwirpErrorCodes {
		metricName := constructTwirpMetric(report.Status)
		if metricName != "" {
			reporter.Report(metricName, 1.0, telemetry.UnitCount)
		}
	}
}

// sendTimerMetric converts a report into a timer metric and sends it to the observer.
func (monitor *OperationMonitor) sendTimerMetric(ctx context.Context, metricName string, report *operation.Report) {
	reporter := telemetry.SampleReporterWithContext(monitor.SampleReporter, ctx)

	reporter.ReportDurationSample(metricName, report.EndTime.Sub(report.StartTime))
}

// sendDependencyMetric converts a report into dependency metrics and sends them to the observer.
func (monitor *OperationMonitor) sendDependencyMetric(ctx context.Context, dependencyName operation.Name, report *operation.Report) {
	reporter := telemetry.SampleReporterWithContext(monitor.SampleReporter, ctx)
	reporter.DependencyProcessIdentifier = identifier.ProcessIdentifier{Service: dependencyName.Group}
	reporter.DependencyOperationName = dependencyName.Method

	reporter.ReportDurationSample(telemetry.MetricDependencyDuration, report.EndTime.Sub(report.StartTime))
	reporter.ReportAvailabilitySamples(availabilityFromStatus(report.Status))
}

// constructHTTPMetric returns the custom string that includes the HTTP status code.
func constructHTTPMetric(status operation.Status) string {
	if status.Code == 0 {
		return ""
	}
	twirpError := twirp.ErrorCode(status.TwirpErrorCode())
	httpCode := twirp.ServerHTTPStatusFromErrorCode(twirpError)
	return fmt.Sprintf("%v_%v", telemetry.MetricHTTPError, httpCode)
}

// constructTwirpMetric returns the custom string that includes the Twirp status code.
func constructTwirpMetric(status operation.Status) string {
	if status.Code == 0 {
		return ""
	}
	twirpError := twirp.ErrorCode(status.TwirpErrorCode())
	return fmt.Sprintf("%v_%v", telemetry.MetricTwirpError, twirpError)
}

// grpcCodeToAvailability maps grpc codes to availability.
var grpcCodeToAvailability = map[int32]telemetry.AvailabilityCode{
	0:  telemetry.AvailabilityCodeSucccess,    // ok
	1:  telemetry.AvailabilityCodeClientError, // canceled
	2:  telemetry.AvailabilityCodeServerError, // unknown
	3:  telemetry.AvailabilityCodeClientError, // invalid argument
	4:  telemetry.AvailabilityCodeServerError, // deadline exceeded
	5:  telemetry.AvailabilityCodeClientError, // not found
	6:  telemetry.AvailabilityCodeClientError, // already exists
	7:  telemetry.AvailabilityCodeClientError, // permission denied
	8:  telemetry.AvailabilityCodeClientError, // resource exhausted
	9:  telemetry.AvailabilityCodeClientError, // failed precondition
	10: telemetry.AvailabilityCodeServerError, // aborted
	11: telemetry.AvailabilityCodeClientError, // out of range
	12: telemetry.AvailabilityCodeClientError, // unimplemented
	13: telemetry.AvailabilityCodeServerError, // internal
	14: telemetry.AvailabilityCodeServerError, // unavailable
	15: telemetry.AvailabilityCodeServerError, // data loss
	16: telemetry.AvailabilityCodeClientError, // unauthenticated
}

// availabilityFromStatus returns the availability for a status.
func availabilityFromStatus(status operation.Status) telemetry.AvailabilityCode {
	if availability, ok := grpcCodeToAvailability[status.Code]; ok {
		return availability
	}
	return telemetry.AvailabilityCodeServerError
}
