#!/usr/bin/perl -Ilibs -I../www/libs

package API::GP::Daemon;

use strict;
use warnings;
use Moo;

use Data::Serializer::Raw;
use File::Temp;
use JSON;
use Carp;
use URI;
use Try::Tiny;
use Time::Seconds ();
use Amazon::SQS::Simple;

use ECS::Config;
use ECS::DB;
use ECS::dbConfig;
use ECS::Site;
use ECS::GP;
use ECS::Purchasing::PO;
use ECS::Purchasing::PR;
use ECS::Purchasing::WR;
use ECS::Purchasing::VendorCredit;

use API::GP::Action;

use Moo;
use JSON;
use Types::Standard -types;

my $PROCESS_WORK = 1; # to process or not to process work in the queue
my $MAX_ATTEMPTS = 3; # retries before skipping the item
my $RETRY_DELAY  = 30; # seconds between retrying
my $MESSAGE_VISIBILITY = 5 * Time::Seconds::ONE_MINUTE; # length of time a queue message is valid, and can be deleted

=pod

=head1 NAME

API::GP::Daemon - GP Daemon Handler

=head1 SYNOPSIS

	my $daemon = API::GP::Daemon->new( max_attempts => 1, process_work => 0 );

	my $daemon = API::GP::Daemon->new( queue => $queue, dispatch_class => 'Test::Dispatch' );
	
	$daemon->run; # process queue items until shutdown or restart

	$daemon->run_once; # process one queue item, or return if non available after timeout

=pod

=head1 ATTRIBUTES

=head2 max_attempts B<Int>

How many times to retry an action before failing it, and adding it to the error queue

Default: 3

=cut

has max_attempts => (
	is => 'ro',
	isa => Int,
	default => $MAX_ATTEMPTS,
);

=pod

=head2 retry_delay B<Int>

How many seconds to pause between attempts to process an action on failure

Default: 30

=cut

has retry_delay => (
	is => 'ro',
	isa => Int,
	default => $RETRY_DELAY,
);

=pod

=head2 message_visibility Int

How many seconds to keep the queue message active, and able to be deleted.   Set
to the maximum processing time for queue items (up to 12 hours)

Default: 120 B<2 minutes>

=cut

has message_visibility => (
	is => 'ro',
	isa => Int,
	default => $MESSAGE_VISIBILITY,
);

=pod

=head2 dispatch_class ClassName

Class used to dispatch queue messages to work.   C<dispatch> is called on this class
as a class method, with the message body

Default: C<API::GP::Action>

=cut

has dispatch_class => (
	is => 'ro',
	isa => ClassName,
	default => 'API::GP::Action',
);

has _dispatch_handler => (
	is => 'ro',
	isa => CodeRef,
	lazy => 1,
	builder => '_build_dispatch',
);

has queue => (
	is => 'rw',
	isa => Object,
	lazy => 1,
	builder => '_build_queue',
);

=pod

=head2 shutdown Bool

Set to true to stop processing of the queue before the next item is received

=cut

has shutdown => (
	is => 'rw',
	isa => Bool,
	default => 0,
	init_arg => undef,
);

=pod

=head2 restart Bool

Set to true to stop processing of the queue before the next item is received,
so that the process can be restarted

=cut

has restart => (
	is => 'rw',
	isa => Bool,
	default => 0,
	init_arg => undef,
	trigger => sub {
		shift->shutdown(1);
	}
);


has json => (
	is => 'ro',
	isa => InstanceOf[JSON::],
	init_arg => undef,
	default => sub { JSON->new->allow_blessed->convert_blessed->utf8->allow_nonref }
);

###############################################################################

sub _build_dispatch {
	my $self = shift;
	my $dispatch_class = $self->dispatch_class;
	my $sub = $dispatch_class->can( 'dispatch' );
	$sub;
}

###############################################################################

sub _build_queue {
	my $self = shift;
    my ($aws_access_key, $aws_secret_key, $queue_arn) =
        @{$ECS::Config::c{gpUpdates}}{qw/
            accessKey
            secretKey
            sqsQueueArn
        /};
    my $sqs = Amazon::SQS::Simple->new( $aws_access_key, $aws_secret_key );
    my $queue = $sqs->GetQueue( $queue_arn );
}

sub run_once {
	my $self = shift;
	return if $self->shutdown();
	$self->_do_work();
}

sub run {
	my $self = shift;
	return if $self->shutdown();
	printf "[%s] Starting up.\n", scalar localtime; 
	$self->_do_work() until $self->shutdown();
	printf "[%s] Shutting down.\n", scalar localtime; 
}

sub _do_work {
	my $self = shift;
	my $queue = $self->queue;
	my $json = $self->json;
	return unless my $count = $queue->GetAttributes()->{ApproximateNumberOfMessages};
	my $visibility = $self->message_visibility;
	
	my @messages = $queue->ReceiveMessage(
		MaxNumberOfMessages => 1,
		VisibilityTimeout => $visibility + $RETRY_DELAY,
	);
	my $success = 0;
	MESSAGE: for my $message (@messages) {
		my $item = try { 
			$json->decode( $message->MessageBody() ); 
		} catch {
			+{}
		};
		if (ref $item eq 'HASH' && $item->{action}) {
			my $retries = 0;
			my $result = 0;
			DISPATCH: while (!$result) {
				$queue->ChangeMessageVisibility( $message->ReceiptHandle, $visibility + $RETRY_DELAY );
				$result = $self->_dispatch( $item );
				if ($result) {
					printf "[%s] - - - %s OK\n",
						scalar localtime,
						$item->{action};
            		print "# " . JSON->new->allow_nonref->encode($item) . "\n";
				} else {
					if ( $retries++ < $self->max_attempts ) { # retry
						warn sprintf "[%s] - - - %s failed, retrying in %d seconds\n",
							scalar localtime,
							$item->{'action'},
							$RETRY_DELAY;
						if (!$self->shutdown) {
							# keep item available for this process (plus the retry delay)
							sleep $RETRY_DELAY;
							# $queue->ChangeMessageVisibility( $message->ReceiptHandle, $visibility + $RETRY_DELAY );
						} else {
							# make current message available for next time we run
							try { $queue->ChangeMessageVisibility( $message->ReceiptHandle, 0 ); };
							last MESSAGE
						}
					} else { # give up
						# send it to the error queue
						warn sprintf "[%s] - - - %s failed, giving up on it\n",
							scalar localtime,
							$item->{'action'};
						$self->enqueueError( $item );
						last DISPATCH;
					}
				}
			}
		} else {
			warn sprintf "Invalid Queue Message %s", $self->json->encode( $item );
		}
	} continue {
		try {
			$queue->DeleteMessage( $message->ReceiptHandle );
		} catch {
			warn "Can't delete message, retrying\n";
		};
	}
}

###############################################################################

sub _dispatch {
	my $self = shift;
	my $dispatch = $self->_dispatch_handler;
	my ($item) = @_;
	$item->$dispatch;
}

###############################################################################

sub enqueueError {
	my $self = shift;
    my ($item) = @_;

    my $errors = API::GP::Action::getErrors();
    push @{ $errors }, $item;
    API::GP::Action::saveErrors( $errors );

    return;
}

###############################################################################

no Types::Standard;
no JSON;
no File::Temp;
no Carp;
no Moo;
1;
