#!/usr/bin/perl
#
# bin/worker/sns-notifier
#
# Listens on SNS queues and takes some action when a message comes in.
#
# Authors:
#      Mark Smith <mark@dreamwidth.org>
#
# Copyright (c) 2019 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself.  For a copy of the license, please reference
# 'perldoc perlartistic' or 'perldoc perlgpl'.
#

use strict;
use v5.10;

BEGIN {
    require "$ENV{LJHOME}/cgi-bin/ljlib.pl";
}

use Log::Log4perl;
my $log = Log::Log4perl->get_logger(__PACKAGE__);

use List::Util qw/ sum /;
use LWP::UserAgent;
use Paws;
use POSIX qw/ strftime /;

use LJ::JSON;

my $HOOK_URL = $LJ::BUILD_NOTIFY_HOOK_URL or die;

my @ALL_PHASES = qw/
    SUBMITTED QUEUED PROVISIONING DOWNLOAD_SOURCE INSTALL
    PRE_BUILD POST_BUILD UPLOAD_ARTIFACTS FINALIZING COMPLETED
    /;

my %PHASES = (
    SUBMITTED => rgb( 255, 255, 128 ),
    QUEUED    => rgb( 128, 128, 255 ),
    BUILD     => rgb( 128, 128, 255 ),
    COMPLETED => rgb( 128, 255, 128 ),
);

## Establish SQS connection
my $paws = Paws->new(
    config => {
        region => $LJ::SQS{region},
    },
) or $log->logcroak('Failed to initialize Paws object.');
my $sqs = $paws->service('SQS')
    or $log->logcroak('Failed to initialize Paws::SQS object.');
my $queue_url = $sqs->GetQueueUrl( QueueName => 'dw-codebuild-notifications' )->QueueUrl;

## Start listening for notifications, one by one
while (1) {
    my $res = eval {
        $sqs->ReceiveMessage(
            QueueUrl            => $queue_url,
            MaxNumberOfMessages => 1,
            WaitTimeSeconds     => 20
        );
    };
    if ( $@ && $@->isa('Paws::Exception') ) {
        die $@->Message;
    }

    my $messages = $res->Messages;
    $log->warn( 'Got ', scalar(@$messages), ' messages.' );
    next unless @$messages;

    foreach my $message (@$messages) {
        my $msg = LJ::JSON::from_json( $message->Body );
        my $d   = $msg->{detail};
        my $ai  = $d->{'additional-information'};

        my $project   = $d->{'project-name'} // '(unknown)';
        my $current   = $d->{'current-phase'};
        my $completed = $d->{'completed-phase'};
        my $next =
              $ai->{phases}
            ? $ai->{phases}[-1]{'phase-type'}
            : undef;

        my ( $phase, $message );
        if ( $current eq 'SUBMITTED' ) {

            # Job has been submitted (very first phase)
            $phase   = $current;
            $message = 'Build request submitted, waiting for a VM...';
        }
        elsif ( $current eq 'COMPLETED' ) {

            # Job is fully complete
            $phase   = $current;
            $message = 'Upload completed, build was successful!';
        }
        elsif ( $completed eq 'BUILD' ) {

            # Job has finished building, moving on to uploading
            $phase   = $completed;
            $message = 'Build stage complete, uploading artifact.';
        }
        elsif ( $completed eq 'QUEUED' ) {

            # Job has been assigned resources and is beginning to execute
            #
            # This might be nice if we ever see lag between SUBMITTED and QUEUED,
            # but in all our experience so far, it's instant. Let's not send it
            # for now.
            #
            # $phase   = $completed;
            # $message = 'Resources allocated, provisioning build environment.';
        }
        next unless $phase;

        my $color   = $PHASES{$phase};
        my $seconds = $d->{'completed-phase-duration-seconds'};
        my $elapsed =
            $ai->{phases}
            ? sum( map { $_->{'duration-in-seconds'} + 0 } @{ $ai->{phases} } )
            : undef;

        my $embed = make_embed(
            $project, $message, $color,
            commit   => github_url( $ai->{'source-version'} ),
            duration => ( $seconds ? timestr($seconds) : '--' ),
            elapsed  => ( $elapsed ? timestr($elapsed) : '--' ),
        );
        send_webhook($embed) or die;
    }

    my $idx = 0;
    $sqs->DeleteMessageBatch(
        QueueUrl => $queue_url,
        Entries  => [ map { { Id => $idx++, ReceiptHandle => $_->ReceiptHandle } } @$messages ]
    );
    if ( $@ && $@->isa('Paws::Exception') ) {
        die $@->Message;
    }
}

sub make_embed {
    my ( $project, $msg, $color, @fields ) = @_;

    my @embed_fields;
    while ( my @field = splice @fields, 0, 2 ) {
        push @embed_fields,
            { name => $field[0], value => $field[1], inline => LJ::JSON->to_boolean(1) };
    }

    my $obj = {
        'embeds' => [
            {
                'title'       => sprintf( '[%s] build update', $project ),
                'description' => $msg,
                'fields'      => \@embed_fields,
                'color'       => $color,
                'footer'      => {
                    'text' => 'AWS CodeBuild Notifier'
                },
                'timestamp' => strftime( '%Y-%m-%dT%H:%M:%S.000Z', gmtime ),
            }
        ]
    };

    return $obj;
}

sub send_webhook {
    my $obj = $_[0];

    my $ua = LWP::UserAgent->new;
    $ua->agent('Dreamwidth AWS CodeBuild Notifier <support@dreamwidth.org>');

    my $res = $ua->post(
        $HOOK_URL,
        Content        => LJ::JSON::to_json($obj),
        'Content-Type' => 'application/json',
    );
    return 1 if $res->is_success;

    die $res->decoded_content;
}

sub rgb {
    return ( $_[0] << 16 ) + ( $_[1] << 8 ) + $_[2];
}

sub timestr {
    my $secs = $_[0];

    if ( $secs > 3600 ) {
        return sprintf( '%dh%s', int( $secs / 3600 ), timestr( $secs % 3600 ) );
    }
    elsif ( $secs > 60 ) {
        return sprintf( '%dm%s', int( $secs / 60 ), timestr( $secs % 60 ) );
    }
    else {
        return sprintf( '%ds', $secs );
    }
}

sub github_url {
    my $hash = substr( $_[0], 0, 8 );

    return qq|[$hash](https://github.com/dreamwidth/dreamwidth/commit/$hash)|;
}
