package alerts

import (
	"fmt"
	"sort"
	"strings"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/cloudwatch"
	"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
	"github.com/aws/aws-sdk-go/service/rds"
	"github.com/aws/aws-sdk-go/service/sns"
	"github.com/aws/aws-sdk-go/service/sns/snsiface"
)

type HostAlarmSet map[string]*cloudwatch.MetricAlarm
type AssetAlarms map[string]HostAlarmSet

func buildConfigs() {
	requestedAlarms = make(map[string]*AlarmConfig)
	addAdvancedAlarm(MinimumAlarms, "CPUUtilization", cloudwatch.ComparisonOperatorGreaterThanOrEqualToThreshold, 80, "Over 80%% CPU utilization for %s", 60, 5, aws.String("Average"), nil)
	addAlarm(HighAlarms, "ReplicaLag", cloudwatch.ComparisonOperatorGreaterThanThreshold, 301, "Over 5 minutes replica lag for %s")
	addPercentAlarm(ModerateAlarms, "DatabaseConnections", cloudwatch.ComparisonOperatorGreaterThanThreshold, "memory", float64(4)/float64(150), 1, "Over 80%% connection utilization for %s")
	addPercentAlarm(ModerateAlarms, "ReadIOPS", cloudwatch.ComparisonOperatorGreaterThanThreshold, "iops", 0.8, 1, "Over 80%% read IOPS utilization for %s")
	addPercentAlarm(ModerateAlarms, "WriteIOPS", cloudwatch.ComparisonOperatorGreaterThanThreshold, "iops", 0.8, 1, "Over 80%% write IOPS utilization for %s")
	addPercentAlarm(MinimumAlarms, "FreeableMemory", cloudwatch.ComparisonOperatorLessThanThreshold, "memory", 0.2, 1, "Less than 20%% available RAM for %s")
	//5 queue depth per 1000 IOPS indicates saturation, according to http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html
	addPercentAlarm(HighAlarms, "DiskQueueDepth", cloudwatch.ComparisonOperatorGreaterThanOrEqualToThreshold, "iops", float64(4)/float64(1000), 4, "80%% Queue Depth Saturation for %s")

	addAdvancedAlarm(VerboseAlarms, "WriteLatency", cloudwatch.ComparisonOperatorGreaterThanOrEqualToThreshold, 0.2, "Over 200ms p99 WriteLatency for %s", 60, 3, nil, aws.String("p99"))
	addAdvancedAlarm(VerboseAlarms, "WriteLatency", cloudwatch.ComparisonOperatorGreaterThanOrEqualToThreshold, 0.075, "Over 75ms p90 WriteLatency for %s", 60, 3, nil, aws.String("p90"))
	addAdvancedAlarm(VerboseAlarms, "ReadLatency", cloudwatch.ComparisonOperatorGreaterThanOrEqualToThreshold, 0.2, "Over 200ms p99 ReadLatency for %s", 60, 3, nil, aws.String("p99"))
	addAdvancedAlarm(VerboseAlarms, "ReadLatency", cloudwatch.ComparisonOperatorGreaterThanOrEqualToThreshold, 0.075, "Over 75ms p90 ReadLatency for %s", 60, 3, nil, aws.String("p90"))
}

func alarmName(metricName string, extendedStatistic *string) string {
	if extendedStatistic == nil {
		return metricName
	}

	return fmt.Sprintf("%s%s", metricName, *extendedStatistic)
}

func getRelevantAlarms(cwClient cloudwatchiface.CloudWatchAPI, team string) (AssetAlarms, error) {
	alarms := make(AssetAlarms)

	err := cwClient.DescribeAlarmsPages(&cloudwatch.DescribeAlarmsInput{
		AlarmNamePrefix: aws.String(fmt.Sprintf("rds-buddy-%s", team)),
	}, func(page *cloudwatch.DescribeAlarmsOutput, lastPage bool) bool {

		for _, alarm := range page.MetricAlarms {
			for _, dimension := range alarm.Dimensions {
				if *dimension.Name == "DBInstanceIdentifier" {
					hostSet, ok := alarms[*dimension.Value]
					if !ok {
						hostSet = make(HostAlarmSet)
						alarms[*dimension.Value] = hostSet
					}
					hostSet[alarmName(*alarm.MetricName, alarm.ExtendedStatistic)] = alarm
					break
				}
			}
		}

		return true
	})

	return alarms, err
}

func checkAlarms(cwClient cloudwatchiface.CloudWatchAPI, instance *rds.DBInstance, instancePriority AlarmPriority, alarms HostAlarmSet, foundTopicArn string, team string) error {
	//Delete any alarms that shouldn't exist, if any other alarms don't match the config,
	//send the params to upsertAlarms
	alarmsToDelete := []*cloudwatch.MetricAlarm{}
	doUpsert := false

	//Check for alarms that need to be deleted or updated
	for alarmName, alarm := range alarms {
		alarmCfg, ok := requestedAlarms[alarmName]
		if !ok || alarmCfg.Priority() > instancePriority {
			alarmsToDelete = append(alarmsToDelete, alarm)
			continue
		}

		matches, err := alarmCfg.Matches(alarm, instance)
		if err != nil {
			return err
		}

		if !matches {
			doUpsert = true
		}
	}

	if !doUpsert {
		//Check for alarms that need to be created
		for alarmName, alarmCfg := range requestedAlarms {
			_, ok := alarms[alarmName]
			if !ok && alarmCfg.Priority() <= instancePriority {
				doUpsert = true
				break
			}
		}
	}

	if len(alarmsToDelete) > 0 {
		var deleteSet []string
		for _, alarmToDelete := range alarmsToDelete {
			deleteSet = append(deleteSet, *alarmToDelete.AlarmName)
		}

		err := deleteAlarms(cwClient, deleteSet)
		if err != nil {
			return err
		}
	}

	if doUpsert {
		return upsertAlarms(cwClient, instance, instancePriority, foundTopicArn, team)
	}

	return nil
}

func deleteAlarms(cwClient cloudwatchiface.CloudWatchAPI, deleteSet []string) error {
	//This is obnoxious, but strings coming out of a map have to be sorted before passing
	//to the API so the mocking library can actually compare them consistently
	sort.Strings(deleteSet)

	var alarmNames []*string
	for _, deleteName := range deleteSet {
		alarmNames = append(alarmNames, aws.String(deleteName))
	}

	_, err := cwClient.DeleteAlarms(&cloudwatch.DeleteAlarmsInput{
		AlarmNames: alarmNames,
	})
	return err
}

func upsertAlarms(cwClient cloudwatchiface.CloudWatchAPI, instance *rds.DBInstance, instancePriority AlarmPriority, alertTopicArn string, team string) error {
	for alarmName, alarmCfg := range requestedAlarms {
		if instancePriority < alarmCfg.Priority() {
			continue
		}

		threshold, err := alarmCfg.Threshold(instance)
		if err != nil {
			return err
		}

		_, err = cwClient.PutMetricAlarm(&cloudwatch.PutMetricAlarmInput{
			ActionsEnabled: aws.Bool(true),
			AlarmActions: []*string{
				aws.String(alertTopicArn),
			},
			AlarmDescription:   aws.String(alarmCfg.Description(instance)),
			AlarmName:          aws.String(fmt.Sprintf("rds-buddy-%s-%s-%s", team, alarmName, *instance.DBInstanceIdentifier)),
			ComparisonOperator: aws.String(alarmCfg.ComparisonOperator()),
			Dimensions: []*cloudwatch.Dimension{
				&cloudwatch.Dimension{
					Name:  aws.String("DBInstanceIdentifier"),
					Value: instance.DBInstanceIdentifier,
				},
			},
			EvaluationPeriods: aws.Int64(alarmCfg.EvaluationPeriods()),
			ExtendedStatistic: alarmCfg.ExtendedStatistic(),
			MetricName:        aws.String(alarmCfg.MetricName()),
			Namespace:         aws.String("AWS/RDS"),
			OKActions: []*string{
				aws.String(alertTopicArn),
			},
			Period:    aws.Int64(alarmCfg.Period()),
			Statistic: alarmCfg.Statistic(),
			Threshold: aws.Float64(threshold),
		})
		if err != nil {
			return err
		}
	}

	return nil
}

func ConfigureAlarms(snsClient snsiface.SNSAPI, cwClient cloudwatchiface.CloudWatchAPI, team string, instancePriorities map[string]AlarmPriority, instances map[string]*rds.DBInstance) error {
	buildConfigs()

	//Find the SNS topic named rds-buddy-<team>-cloudwatch-alarms
	//SNS doesn't have a prefix or tag or name search soooo
	var foundTopicArn string

	err := snsClient.ListTopicsPages(&sns.ListTopicsInput{},
		func(topics *sns.ListTopicsOutput, lastPage bool) bool {
			for _, topic := range topics.Topics {
				arn := *(*topic).TopicArn
				if strings.HasSuffix(arn, fmt.Sprintf(":rds-buddy-%s-cloudwatch-alarms", team)) {
					foundTopicArn = arn
					return false
				}
			}

			return true
		})
	if err != nil {
		return err
	}
	if foundTopicArn == "" {
		return fmt.Errorf("Could not find any ARN containing `:rds-buddy-%s-cloudwatch-alarms` - have the alerting artifacts been provisioned?", team)
	}

	alarms, err := getRelevantAlarms(cwClient, team)
	if err != nil {
		return err
	}

	for host, hostAlarms := range alarms {
		instance, ok := instances[host]
		if !ok {
			//Delete this alarm
			var deleteSet []string
			for _, alarm := range hostAlarms {
				deleteSet = append(deleteSet, *alarm.AlarmName)
			}

			err = deleteAlarms(cwClient, deleteSet)
			if err != nil {
				return err
			}
			continue
		}

		err = checkAlarms(cwClient, instance, instancePriorities[host], hostAlarms, foundTopicArn, team)
		if err != nil {
			return err
		}
	}

	for instanceName, instance := range instances {
		_, ok := alarms[instanceName]
		if !ok {
			err = upsertAlarms(cwClient, instance, instancePriorities[instanceName], foundTopicArn, team)
			if err != nil {
				return err
			}
		}
	}

	return nil
}
