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

export interface CommonServiceProps {
    readonly environmentName: EnvName;
    readonly cloudwatchAlarmSNSTopics: AlarmSNSTopics;
    readonly serviceName: string,
    readonly region: string,
}

/**
 * Creates a default set of alarms for a specified api. This will include the following:
 *  - An alarm that fires if success events drop below 99% in 5 minutes
 *  - An alarm that fires when the p90 latency exceeds the specified value
 *
 * @param operationName - The api name
 * @param scope - The Construct that owns this alarm
 * @param serviceProps - The common properties of the service and environment
 * @param maxP90Latency - The p95 latency alarm trigger threshold
 * @param twitchSeverity - The twitch severity these alarms should be considered at
 */
export function createDefaultAlarmsForApi(
    scope: Construct,
    operationName: string,
    serviceProps: CommonServiceProps,
    maxP90Latency: Duration,
    twitchSeverity: number
) {
    const snsTopic = serviceProps.cloudwatchAlarmSNSTopics.getTopicForSeverity(twitchSeverity);

    // Creates an alarm that triggers when the success ratio of the api is below 99%
    createSuccessPercentageAlarm(scope, operationName, serviceProps, snsTopic);

    // Creates an alarm if the p90 for latency is above the specified latency duration
    createP90Alarm(scope, operationName, maxP90Latency, serviceProps, snsTopic);
}

/**
 * Creates an alarm that triggers when the percentage of api success events
 * is below 99% of all events.
 *
 * We are purposely excluding client errors from the success rate calculation, as
 * client errors are not actionable by an on call engineer.
 *
 * @param scope - The Construct that owns this alarm
 * @param operationName - The api name
 * @param serviceProps - The common properties of the service and environment
 * @param snsTopic - The sns topic for the alarm
 */
export function createSuccessPercentageAlarm(
    scope: Construct,
    operationName: string,
    serviceProps: CommonServiceProps,
    snsTopic: sns.ITopic | null)
{
    const commonProps = {
        namespace: serviceProps.serviceName,
        statistic: "Sum",
        unit: Unit.COUNT,
        dimensions: {
            Operation: operationName,
            Region: serviceProps.region,
            Service: serviceProps.serviceName,
            Stage: serviceProps.environmentName,
            Substage: "primary",
        }
    };

    const successMetric = new cw.Metric({
        metricName: "Success",
        label: "Success",
        ...commonProps
    });

    const serverErrorMetric = new cw.Metric({
        metricName: "ServerError",
        label: "Server Errors",
        ...commonProps
    });

    const exp = new MathExpression({
        expression: "(numSuccess / (numSuccess + numServerErrors)) * 100",
        usingMetrics: {
            numSuccess: successMetric,
            numServerErrors: serverErrorMetric,
        }
    }).with({
        label: "Success Rate (Percentage)",
        period: Duration.minutes(1),
    });

    const alarmIdentifier = "success_rate_below_sla";
    const alarmName = createAlarmName(serviceProps.environmentName, operationName, alarmIdentifier)
    const constructId = createConstructId(operationName, alarmIdentifier);
    const alarm = exp.createAlarm(scope, constructId, {
        alarmName,
        alarmDescription: `${operationName} success rate is less than 99% over the last minute`,
        comparisonOperator: ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD,
        evaluationPeriods: 3,
        threshold: 99,
        treatMissingData: TreatMissingData.NOT_BREACHING,
        actionsEnabled: true,
    });

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

/**
 * Creates an alarm if the p90 for latency is above the specified latency duration.
 *
 * @param scope - The Construct that owns this alarm
 * @param operationName - The api name
 * @param serviceProps - The common properties of the service and environment
 * @param maxP90Latency - The max latency the api can exhibit before triggering the alarm
 * @param snsTopic - The sns topic for the alarm
 */
export function createP90Alarm(
    scope: Construct,
    operationName: string,
    maxP90Latency: Duration,
    serviceProps: CommonServiceProps,
    snsTopic: sns.ITopic | null)
{
    const percentileStat = "p90";
    const durationMetric = new cw.Metric({
        metricName: "Duration",
        namespace: serviceProps.serviceName,
        statistic: percentileStat,
        dimensions: {
            Operation: operationName,
            Region: serviceProps.region,
            Service: serviceProps.serviceName,
            Stage: serviceProps.environmentName,
            Substage: "primary",
        }
    });

    // Convert duration metric from seconds to milliseconds
    const exp = new MathExpression({
        expression: "duration * 1000",
        usingMetrics: {
            duration: durationMetric
        }
    }).with({
        label: `${percentileStat} Latency (ms)`,
        period: Duration.minutes(1),
    });

    const latencyThreshold = maxP90Latency.toMilliseconds();
    const alarmIdentifier = "latency_greater_than_max";
    const alarmDescription = `${operationName} ${percentileStat} latency is greater than ${latencyThreshold}ms over the last 5 minutes`;
    const alarmName = createAlarmName(serviceProps.environmentName, operationName, alarmIdentifier);
    const constructId = createConstructId(operationName, alarmIdentifier);
    const alarm = exp.createAlarm(scope, constructId, {
        alarmName,
        alarmDescription: alarmDescription,
        comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
        evaluationPeriods: 5,
        threshold: latencyThreshold,
        treatMissingData: TreatMissingData.NOT_BREACHING,
        actionsEnabled: true,
    });

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

function createAlarmName(environmentName: string, operationName: string, alarmIdentifier: string) : string {
    return `${environmentName}-${operationName}-${alarmIdentifier}`;
}

function createConstructId(operationName: string, alarmIdentifier: string) : string {
    return `${operationName}-alarm-${alarmIdentifier}`;
}