#!/usr/bin/perl
# Skip to bottom for the (very short) main program

package RemoteAgentPacked;

use JSON::XS;
use IO::File;
use IO::Handle;
use Getopt::Long 'GetOptionsFromArray';
Getopt::Long::Configure('pass_through');

use lib 'lib';
use Provision::Unix;

sub new {
    my ( $class, %args ) = @_;
    my $self = bless { %args }, $class;
    $self->{in_json}  = JSON::XS->new();
    $self->{out_json} = JSON::XS->new();
    $self->{ins}      = undef;
    $self->{outs}     = undef;
    $self->{buffer}   = [];
    defined $self->{timeout} or $self->{timeout} = 0;
    $self->{pretty} and $self->{out_json}->pretty;
    return $self;
}

sub new_from_cl {
    my ( $class, %args ) = @_;
    my $rv = GetOptionsFromArray(
        $args{ARGV},
        'pretty'         => \my $pretty,
        'timeout=i'      => \my $timeout,
    ) or die "Didn't understand command line parameters";
    my $self = $class->new( pretty => $pretty, timeout => $timeout );
    $self->{ins}  = IO::Handle->new_from_fd( fileno(STDIN),  'r' );
    $self->{outs} = IO::Handle->new_from_fd( fileno(STDOUT), 'w' );
    $self->{outs}->autoflush(1);
    return $self;
}

sub send {
    my ( $self, $obj ) = @_;
    my $msg = $self->{out_json}->encode($obj);
    local $SIG{PIPE} = sub {
        die {
            status  => 'error',
            type    => 'protocol',
            message => 'Remote unexpectedly closed pipe'
        };
    };
    $self->{outs}->print("$msg\n");
}

sub receive {
    my ($self) = @_;

    return shift @{ $self->{buffer} } if scalar @{ $self->{buffer} };

    my $ins     = $self->{ins};
    my $outs    = $self->{outs};
    my $timeout = $self->{timeout};
    my $in_json = $self->{in_json};

    return if ! defined $ins;

    my $run = 1;
    while ( $run > 0 ) {
        my $i;
        if ($timeout) {
            eval {
                local $SIG{ALRM} = sub { die "alarm\n" };
                alarm $timeout;
                $i = $ins->getline;
                $timeout and alarm(0);
            };
        }
        else {
            eval { $i = $ins->getline; };
        }
        if ($@) {
            die {
                status  => 'error',
                type    => 'timeout',
                message => 'Timed out',
            }
            if $@ eq "alarm\n";

            die {
                status  => 'error',
                type    => 'protocol',
                message => 'Unknown communication error'
            };
        }
        if ( ! defined $i ) {
            delete $self->{ins};
            return;
        }
        my @reqs;
        eval { @reqs = $in_json->incr_parse($i); };
        if ($@) {
            $in_json->incr_reset;
            $self->send(
                {   status  => 'error',
                    type    => 'syntax',
                    message => 'Malformed message: parse error'
                }
            );
        }
        elsif ( scalar @reqs ) {
            push @{ $self->{buffer} }, @reqs;
            $in_json->incr_reset;
            return shift @{ $self->{buffer} };
        }
    }
    die {
        status  => 'error',
        type    => 'protocol',
        message => 'Remote terminated'
    }
    if $run < 0;
}

sub run {
    my ($self) = @_;

    $self->{prov} = Provision::Unix->new( debug => 0 );
    $self->{running} = 1;

    while ( $self->{running} ) {

        my $o;
        eval { $o = $self->receive; };
        if ( ! defined $o ) { # Session terminated w/o saying goodbye
            $self->send(
                {   status  => 'error',
                    type    => 'system',
                    message => $@,
                }
            );
            last;
        };

        my $id;
        if ( ref $o ne 'HASH' ) {
            $self->send( 
                {   status  => 'error',
                    type    => 'syntax',
                    message => 'Malformed message: parse error'
                }
            );
            next;
        };

        $id = $o->{id};
        my $action = $o->{action};
        if ( ! length($action) ) {
            $self->send(
                {   status  => 'error',
                    type    => 'dispatch',
                    message => 'Malformed message: no action',
                    id      => $id
                }
            );
        };

        if ( $action eq 'close' ) {
            $self->send( { status => 'ok', message => 'Bye', id => $id } );
            last;
        }
        elsif ( $action eq 'echo' ) {
            $self->send(
                {   status  => 'ok',
                    message => 'Echo',
                    id      => $id,
                    data    => $params,
                }
            );
            next;
        }

        my $result;
        eval { $result = $self->do_prov_call( $o, $action ); };
        if ( $@ ) {
            $self->send( $@ );
        };

        $self->send(
            {   status => 'ok',
                id     => $id,
                audit  => $self->{prov}->audit,
                result => $result,
            }
        );
    }
}

sub do_prov_call {
    my ( $self, $req, $action ) = @_;
    $action = 'get_status' if $action eq 'probe';
    my $pkg    = $req->{provisiontype};
    my $suffix = '_' . lc($pkg);
    $pkg = 'Provision::Unix::' . $pkg;

    eval "require $pkg;";
    die {
        status  => 'error',
        type    => 'dispatch',
        message => 'Error loading provisioning module',
        debug   => $@,
        id      => $req->{id}
    } if $@;

    my $params = $req->{params} || {};
    my $instance = $pkg->new( prov => $self->{prov} );

    my $method;
    if ( $pkg->can( $action . $suffix ) ) {
        $method = $action . $suffix;
    }
    elsif ( $pkg->can($action) ) {
        $method = $action;
    }
    else {
        die {
            status  => 'error',
            type    => 'dispatch',
            message => "Unknown action '$action$suffix'",
            id      => $req->{id}
        };
    }
    $self->send(
        {   status  => 'debug',
            message => "Calling '$pkg'::'$method'",
            id      => $req->{id},
            data    => $params
        }
    );

    my $rv;
    eval { $rv = $instance->$method( defined $params ? %$params : () ); };
    die {
        status    => 'error',
        type      => 'operation',
        id        => $req->{id},
        message   => "Unable to $action",
        audit     => $self->{prov}->audit,
        exception => $@
    }
    if ! $rv;
    return $rv;
}

package main;
exit( RemoteAgentPacked->new_from_cl( ARGV => \@ARGV )->run() );
