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

package RemoteAgentPacked;

my $xs = 1;
eval { require JSON::XS; };
if ( $@ ) {
    require JSON;
    $xs = 0;
};
#use Data::Dumper;
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;
    if ( $xs ) {
        $self->{in_json}  = JSON::XS->new();
        $self->{out_json} = JSON::XS->new();
    }
    else {
        $self->{in_json}  = JSON->new();
        $self->{in_json}->allow_nonref(1);
        $self->{out_json} = JSON->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 ) = @_;
    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 = shift;

    $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;
        };

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

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

        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 });
            next;
        }

        my $result;
        eval { $result = $self->do_prov_call( $o, $action ); };
        if ( $@ ) {
            $self->send( $@ );
            warn "do_prov_call error: $@\n";
        };

        $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 $pkg",
        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() );
