package main

import (
	"context"
	"fmt"
	"sync"
	"time"

	"code.justin.tv/foundation/admin-panel-lambdas/internal/logparse"
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/cloudwatch"
	"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
)

type handler struct {
	Environment string

	initSync   sync.Once
	cloudwatch cloudwatchiface.CloudWatchAPI
	namespace  string
}

func (h *handler) init() {
	h.initSync.Do(func() {
		h.cloudwatch = cloudwatch.New(session.Must(session.NewSession(&aws.Config{
			Region: aws.String("us-west-2"),
		})))

		h.namespace = fmt.Sprintf("admin-panel-%s-latency", h.Environment)
	})
}

func (h *handler) Handle(ctx context.Context, event events.CloudwatchLogsEvent) error {
	h.init()

	data, err := event.AWSLogs.Parse()

	if err != nil {
		return err
	}

	for _, logEvent := range data.LogEvents {
		if err := h.handle(ctx, logEvent); err != nil {
			return err
		}
	}

	return nil
}

func (h *handler) handle(ctx context.Context, logEvent events.CloudwatchLogsLogEvent) error {
	line := logparse.LogLine{Message: []byte(logEvent.Message)}

	if call, ok := line.BackendServiceCallSuccess(); ok {
		return h.handleBackendServiceCallSuccess(ctx, call)
	}
	if call, ok := line.BackendServiceCallError(); ok {
		return h.handleBackendServiceCallError(ctx, call)
	}
	if call, ok := line.APEndpointCall(); ok {
		return h.handleAPEndpointCall(ctx, call)
	}
	return nil
}

func (h *handler) handleAPEndpointCall(ctx context.Context, call *logparse.AdminPanelRequest) error {
	data := make([]*cloudwatch.MetricDatum, 0)
	apDimensionSet := [][]*cloudwatch.Dimension{
		{
			{Name: aws.String("Environment"), Value: aws.String(h.Environment)},
			{Name: aws.String("Host"), Value: aws.String(call.URLHost)},
			{Name: aws.String("RailsController"), Value: aws.String(call.RailsController)},
		},
		{
			{Name: aws.String("Environment"), Value: aws.String(h.Environment)},
			{Name: aws.String("Host"), Value: aws.String(call.URLHost)},
			{Name: aws.String("Path"), Value: aws.String(call.URLPath)},
		},
	}

	for _, dimensions := range apDimensionSet {
		data = append(data, h.baseDatum(call.BackendServiceCall).
			SetDimensions(dimensions).
			SetMetricName(fmt.Sprintf("HTTPCode_Backend_%dXX", call.ResponseStatus/100)).
			SetUnit("Count").
			SetValue(1))

		data = append(data, h.baseDatum(call.BackendServiceCall).
			SetDimensions(dimensions).
			SetMetricName("Latency").
			SetUnit("Seconds").
			SetValue(time.Duration(call.Latency).Seconds()))
	}

	_, err := h.cloudwatch.PutMetricDataWithContext(ctx, &cloudwatch.PutMetricDataInput{
		MetricData: data,
		Namespace:  aws.String(fmt.Sprintf("%s/ap-endpoints", h.namespace)),
	})
	return err
}

func (h *handler) handleBackendServiceCallSuccess(ctx context.Context, call *logparse.BackendServiceCallSuccess) error {
	data := make([]*cloudwatch.MetricDatum, 0)
	for _, dimensions := range h.dimensionSets(call.BackendServiceCall) {
		data = append(data, h.baseDatum(call.BackendServiceCall).
			SetDimensions(dimensions).
			SetMetricName(fmt.Sprintf("HTTPCode_Backend_%dXX", call.ResponseStatus/100)).
			SetUnit("Count").
			SetValue(1))

		data = append(data, h.baseDatum(call.BackendServiceCall).
			SetDimensions(dimensions).
			SetMetricName("Latency").
			SetUnit("Seconds").
			SetValue(time.Duration(call.Latency).Seconds()))
	}

	_, err := h.cloudwatch.PutMetricDataWithContext(ctx, &cloudwatch.PutMetricDataInput{
		MetricData: data,
		Namespace:  aws.String(fmt.Sprintf("%s/backend-success", h.namespace)),
	})
	return err
}

func (h *handler) handleBackendServiceCallError(ctx context.Context, call *logparse.BackendServiceCallError) error {
	base := func() *cloudwatch.MetricDatum {
		return h.baseDatum(call.BackendServiceCall).
			SetUnit("Count").
			SetValue(1)
	}

	data := make([]*cloudwatch.MetricDatum, 0)
	for _, dimensions := range h.dimensionSets(call.BackendServiceCall) {
		data = append(data, base().
			SetDimensions(dimensions).
			SetMetricName("BackendConnectionErrors"))
		data = append(data, base().
			SetDimensions(dimensions).
			SetMetricName(call.ErrorMessage))
	}

	_, err := h.cloudwatch.PutMetricDataWithContext(ctx, &cloudwatch.PutMetricDataInput{
		MetricData: data,
		Namespace:  aws.String(fmt.Sprintf("%s/backend-error", h.namespace)),
	})
	return err
}

func (h *handler) baseDatum(call logparse.BackendServiceCall) *cloudwatch.MetricDatum {
	d := &cloudwatch.MetricDatum{}
	return d.
		SetStorageResolution(60).
		SetTimestamp(time.Time(call.AccessDate))
}

// all dimension sets we want to send each metric with
func (h *handler) dimensionSets(call logparse.BackendServiceCall) [][]*cloudwatch.Dimension {
	return [][]*cloudwatch.Dimension{
		{
			{Name: aws.String("Environment"), Value: aws.String(h.Environment)},
			{Name: aws.String("Host"), Value: aws.String(call.URLHost)},
		},
		{
			{Name: aws.String("Environment"), Value: aws.String(h.Environment)},
			{Name: aws.String("Host"), Value: aws.String(call.URLHost)},
			{Name: aws.String("Path"), Value: aws.String(call.URLPath)},
		},
	}
}
