import * as cdk from "@aws-cdk/core";
import * as cw from "@aws-cdk/aws-cloudwatch";
import { Duration } from "@aws-cdk/core";
import * as cw_actions from "@aws-cdk/aws-cloudwatch-actions";
import { AlarmSNSTopics } from "../metrics/alarm-sns-topics";
import { EnvName } from "../../env-names";


export interface MemcachedAlarmProps {
    readonly envName: EnvName;
    readonly clusterName: string;
    readonly numNodes: number;
    readonly cloudwatchAlarmSNSTopics: AlarmSNSTopics;
    readonly twitchSeverity: number;
}

interface AlarmArgs {
    readonly description: string;

    readonly actionsEnabled: boolean;

    /**
     * nameSuffix should be a dash separated phrase describing what's going on.
     * e.g. too-many-connections
     * It'll be appended to the node's information to create the alarm name.
     */
    readonly nameSuffix: string;

    readonly metricName: string;

    readonly statistic: string;

    readonly comparsionOperator: cw.ComparisonOperator;

    readonly threshold: number;

    readonly period: Duration;

    readonly evaluationPeriods: number;
}

/**
 * Generate a stack consisting of alarms for core Memcached health metrics. These are
 * CPU Utilization, Swap Usage, Evictions and Current Connections. A set of alarms will
 * be created for each individual node.
 *
 * Fore more details, see:
 * https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/CacheMetrics.WhichShouldIMonitor.html
 */
export class Alarms extends cdk.Construct {
    constructor(scope: cdk.Construct, id: string, props: MemcachedAlarmProps) {
        super(scope, id);

        const alarmArgs: AlarmArgs[] = [
            this.getCPUUtilizationAlarmArgs(),
            this.getSwapUsageAlarmArgs(),
            this.getEvictionsAlarmArgs(),
            this.getCurrentConnectionsAlarmArgs(),
        ];

        // Create a set of alarms for each node
        for (let nodeIndex = 1; nodeIndex <= props.numNodes; nodeIndex++) {
            for (let alarmArg of alarmArgs) {
                this.addElasticacheAlarm(alarmArg, props, nodeIndex)
            }
        }
    }

    private getCPUUtilizationAlarmArgs(): AlarmArgs {
        return {
            actionsEnabled: true,
            description: "Alarms if the CPU usage for the Memcached node is too high.",
            nameSuffix: "cpu-too-high",
            metricName: "CPUUtilization",
            statistic: "Average",
            comparsionOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
            threshold: 80,
            period: Duration.minutes(1),
            evaluationPeriods: 5,
        };
    }

    private getSwapUsageAlarmArgs(): AlarmArgs {
        return {
            actionsEnabled: true,
            description: "Alarms if the swap usage of a node is >= 50mb.",
            nameSuffix: "swap-usage-too-high",
            metricName: "SwapUsage",
            statistic: "Average",
            comparsionOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
            threshold: 50000000,
            period: Duration.minutes(5),
            evaluationPeriods: 1,
        };
    }

    private getEvictionsAlarmArgs(): AlarmArgs {
        return {
            actionsEnabled: true,
            description: "Alarms if the Memcached node is evicting items due to memory pressure.",
            nameSuffix: "evictions-too-high",
            metricName: "Evictions",
            statistic: "Sum",
            comparsionOperator: cw.ComparisonOperator.GREATER_THAN_THRESHOLD,
            threshold: 0,
            period: Duration.minutes(1),
            evaluationPeriods: 5,
        };
    }

    private getCurrentConnectionsAlarmArgs(): AlarmArgs {
        return {
            actionsEnabled: true,
            description: "Alarms if the Memcached node has too many connections.",
            nameSuffix: "current-connections-too-high",
            metricName: "CurrConnections",
            statistic: "Average",
            comparsionOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
            threshold: 4000,
            period: Duration.minutes(1),
            evaluationPeriods: 5,
        };
    }

    private addElasticacheAlarm(
        alarmArgs: AlarmArgs,
        nodeProps: MemcachedAlarmProps,
        nodeIndex: number,
    ) {
        const nodeId = nodeIndex.toString().padStart(4, "0");
        const metric = new cw.Metric({
            metricName: alarmArgs.metricName,
            namespace: "AWS/ElastiCache",
            period: alarmArgs.period,
            statistic: alarmArgs.statistic,
            dimensions: {
                CacheClusterId: nodeProps.clusterName,
                CacheNodeId: nodeId,
            }
        });

        // Use an alarm name that's descriptive enough to be meaningful if read from a slack alert.
        // Example: staging-memcached-001-002-too-many-connections
        const alarmName = `${nodeProps.envName}-memcached-${nodeId}-${alarmArgs.nameSuffix}`;
        const constructID = `${alarmArgs.metricName}-${nodeProps.clusterName}-${nodeId}`;

        const alarm = new cw.Alarm(this, constructID, {
            alarmName,
            actionsEnabled: alarmArgs.actionsEnabled,
            alarmDescription: `${alarmArgs.description} (env: ${nodeProps.envName}, node: ${nodeId})`,
            comparisonOperator: alarmArgs.comparsionOperator,
            evaluationPeriods: alarmArgs.evaluationPeriods,
            metric: metric,
            threshold: alarmArgs.threshold,
            treatMissingData: cw.TreatMissingData.MISSING
        });

        const topic = nodeProps.cloudwatchAlarmSNSTopics.getTopicForSeverity(
            nodeProps.twitchSeverity
        );

        if (topic) {
            alarm.addAlarmAction(new cw_actions.SnsAction(topic));
            alarm.addOkAction(new cw_actions.SnsAction(topic));
        }
    }

}