#!/usr/local/bin/perl -w
#
# Written by Oliver Welter
# for the OpenXPKI project 2013
# Copyright (c) 2013 by The OpenXPKI Project
#

use strict;
use warnings;
use English;
use POSIX qw( strftime );
use Getopt::Long;
use Pod::Usage;
use Data::Dumper;
use Encode;
use JSON;

use OpenXPKI::Client;
use OpenXPKI::Log4perl;

binmode(STDOUT, ':encoding(UTF-8)');
binmode(STDERR, ':encoding(UTF-8)');

my %params = (
    authstack => '_System',
    authuser => undef,
    authpass => undef,
    wfid => undef,
    wfaction => undef,
    param => [],
    arg => [],
    timeout => 30,
    json => undef,
    session  => undef,
    out => '',
    );

sub debug {
    my $arg = shift;
    if ($params{debug}) {
        Log::Log4perl->get_logger("")->debug( $arg );
    }
}

sub format_json {
    my $msg = shift;
    if (defined $params{'json-pretty'}) {
        return JSON->new->utf8->pretty(1)->canonical(1)->encode($msg);
    } else {
        return encode_json($msg);
    }
}

sub output {

    my $msg = shift;

    my $output;

    if (defined $params{json}) {
        $output = format_json($msg);
    } elsif ($msg->{error}) {
        if (not ref $msg->{error}) {
            print STDERR "Error: " . $msg->{error} . "\n";
        } else {
            my $err = $msg->{error}->{ERROR};
            print STDERR "Error: " . $err->{LABEL} . "\n";
            if ($err->{PARAMS} && $err->{PARAMS}->{__error__}) {
                print STDERR "    " . $err->{PARAMS}->{__error__} . "\n";
            }
        }
    } elsif (exists $msg->{result} && !ref $msg->{result}) {
        $output = $msg->{result} // "<undef>";
    } elsif ($msg->{result}) {
        # always use json pretty to show non-scalar output
        print JSON->new->utf8->pretty(1)->canonical(1)->encode($msg->{result});
    } else {
        # this means something unhandled error bubbled up
        print Dumper $msg;
        print "\n";
    }

    foreach my $entry (@{$params{fileout}}) {
        my ($key, $value) = ($entry =~ m{ \A (.*?) [=:] (.*) }xms);

        if (!defined $key) {
            die "fileout must be given as key=filename ($entry)\n";
        }
        next unless defined ($msg->{result}->{$key});
        -e $value && die "fileout output file exists ($value)\n";

        if (open (OUT, ">", $value)) {
            print OUT $msg->{result}->{$key};
            close OUT;
        } else {
            warn "Unable to open fileout file $value for $key!";
        }
    }

    return unless($output);

    if ($params{out}) {
        if (open (OUT, ">", $params{out})) {
            print OUT $output;
            close OUT;
            print "Wrote result to " .$params{out}."\n";
        } else {
            warn "Unable to open output file!";
            print $output;
            print "\n";
        }
    } else {
        print $output;
        print "\n";
    }
}


# GetOpts would work without this and leave the stuff in ARGV but we
# want to enforce a proper syntax to avoid any issues when upgrading
# or changing something
my @extra;
my ($dashidx) = grep { $ARGV[$_] eq '--' } (0 .. @ARGV-1);
if ($dashidx) {
    @extra = splice @ARGV, $dashidx;
    shift @extra;
}

GetOptions(\%params,
       qw(
            help|?
            man
            list
            socketfile=s
            instance|i=s
            realm=s
            authstack=s
            authuser=s
            authpass=s
            command|cmd|c=s
            param|set=s@
            arg=s@
            filearg=s@
            fileout=s@
            timeout|to=i
            session
            debug
            json
            json-pretty
            out=s
         )) or pod2usage(-verbose => 0);

pod2usage(-exitstatus => 0, -verbose => 2) if $params{man};

if ($params{help}) {
    if (scalar @ARGV == 0) {
        # --help without parameter
        pod2usage(-verbose => 1)
    } else {
        # --help COMMAND
        my $cmd = shift; $cmd =~ s/\W//msg;
        unshift @ARGV, 'api_help';
        $params{arg} = ["command=$cmd"];
    }
}

if ($params{list}) {
    unshift @ARGV, 'api_list';
}

# json pretty implies json
if (defined $params{'json-pretty'}) {
    $params{'json'} = 1;
}

my %args;
# command with json data
if (scalar @ARGV == 2 && $params{json}) {

    # If it does not start with a { it is a filename
    my $json_data = pop @ARGV;

    if ($json_data !~ m/^\s*{/) {
        -e $json_data || die "JSON input file not found\n";
        $json_data = do {
            local $INPUT_RECORD_SEPARATOR;
            open my $HANDLE, '<', $json_data;
            <$HANDLE>;
        };
    }

    eval { %args = %{ decode_json($json_data) }; };
    if ($EVAL_ERROR) {
        print STDERR "Unable to decode given JSON ($EVAL_ERROR)\n";
        exit 0;
    }
}

if (scalar @ARGV != 1) {
    print STDERR "Usage: openxpkicli [OPTIONS] COMMAND\n";
    exit 0;
}

my $command = shift;

# expect foo=bar or foo:bar in @param, split at assignment and
# build hash
my @parameters = split(/,/, join(',', @{$params{param}}));
my %parameters;
my $params_is_array = 1;
foreach my $entry (@parameters) {
    my ($key, $value) = ($entry =~ m{ \A (.*?) [=:] (.*) }xms);
    if (! defined $key) {
        $key = $value = $entry;
    } else {
         $params_is_array = 0;
    }
    $parameters{$key} = $value;
}

my @args = split(/,/, join(',', @{$params{arg}}));
push @args, @extra;

foreach my $entry (@args) {
    my ($key, $value) = ($entry =~ m{ \A (.*?) [=:] (.*) }xms);

    if (!defined $key) {
        $key = $entry;
        $value = 1;
    }

    $args{$key} = $value;
}

foreach my $entry (@{$params{filearg}}) {
    my ($key, $value) = ($entry =~ m{ \A (.*?) [=:] (.*) }xms);

    if (!defined $key) {
        die "filearg must be given as key=filename ($entry)\n";
    }

    -e $value || die "filearg input file not found ($value)\n";
    $args{$key} = do {
        local $INPUT_RECORD_SEPARATOR;
        open my $HANDLE, '<', $value;
        <$HANDLE>;
    };

}

if($params{instance}){
    $params{socketfile} = sprintf '/var/openxpki/%s/openxpki.socket', $params{instance};
} elsif(!$params{socketfile}){
    $params{socketfile} = '/var/openxpki/openxpki.socket';
    debug("using default socket %s \n", $params{socketfile});
}

OpenXPKI::Log4perl::init_or_fallback('' , $params{debug} ? 'DEBUG' : 'WARN');

debug(sprintf("Socketfile: %s", $params{socketfile}));

my $reply;
my $client = OpenXPKI::Client->new({
    SOCKETFILE => $params{socketfile},
    TIMEOUT  => $params{timeout},
    });


if (! defined $client) {
    output({'error' => "Could not instantiate OpenXPKI client. Stopped"});
    exit 1;
}

if (! $client->init_session()) {
    output({'error' => "Could not initiate OpenXPKI server session. Stopped"});
    exit 1;
}
if ($params{session}) {

    # session mode and session id in env, try to reinit session
    if ($command eq 'login') {
        # noop
    } elsif ($ENV{OPENXPKI_SESSION_ID}) {
        $reply = $client->init_session( { SESSION_ID => $ENV{OPENXPKI_SESSION_ID} } );
        if ($reply->{SERVICE_MSG} ne 'SERVICE_READY') {
            output({'error' => "Login with session id failed"});
            exit 1;
        }
    # no session in env and login not requested
    } else {
        output({'error' => "Please set the session id via environment OPENXPKI_SESSION_ID"});
        exit 1;
    }

}

my $session_id = $client->get_session_id();
debug("Session id: $session_id");

$reply = $client->send_receive_service_msg('PING');

SERVICE_MESSAGE:
while (1) {
    debug(Dumper $reply);
    my $status = $reply->{SERVICE_MSG};

    if ($status eq 'GET_PKI_REALM') {
        if (! $params{realm}) {
            if (defined $params{json}) {
                print format_json({ result => { pki_realm => keys %{$reply->{PARAMS}->{PKI_REALMS}} } });
            } else {
                print "Available PKI realms:\n";
                foreach my $entry (keys %{$reply->{PARAMS}->{PKI_REALMS}}) {
                    print "  $entry\n";
                }
            }
            die "No --realm specified. Stopped";
        }
        $reply = $client->send_receive_service_msg('GET_PKI_REALM',{
            PKI_REALM => $params{realm},
            AUTHENTICATION_STACK => $params{authstack},
        });
      next SERVICE_MESSAGE;
    }

    if ($reply->{SERVICE_MSG} eq 'GET_AUTHENTICATION_STACK') {
        if (!$params{authstack}) {
            if (defined $params{json}) {
                print format_json({ result => { auth_stack => keys %{$reply->{PARAMS}->{AUTHENTICATION_STACKS}} } });
            } else {
                print "Available authentication stacks:\n";
                foreach my $entry (keys %{$reply->{PARAMS}->{AUTHENTICATION_STACKS}}) {
                    print "  $entry\n";
                }
            }
            die "No --authstack specified. Stopped";
        }
        $reply = $client->send_receive_service_msg('GET_AUTHENTICATION_STACK', {
            AUTHENTICATION_STACK => $params{authstack},
        });
        next SERVICE_MESSAGE;
    }

    if ($reply->{SERVICE_MSG} eq 'GET_PASSWD_LOGIN') {
        if (! $params{authuser}) {
            die "No --authuser specified. Stopped";
        }
        $reply = $client->send_receive_service_msg('GET_PASSWD_LOGIN', {
            LOGIN => $params{authuser},
            PASSWD => $params{authpass},
        });
        next SERVICE_MESSAGE;
    }

    if ($reply->{SERVICE_MSG} eq 'SERVICE_READY') {

        # if we are in session mode login sequence print session id
        if ($command eq 'login') {
            printf "OPENXPKI_SESSION_ID=%s; export OPENXPKI_SESSION_ID;\n", $client->get_session_id();
            exit 0;
        }
        last SERVICE_MESSAGE;
    }

    output({ error => $reply  });

    die "Unhandled service message. Stopped";
}

# logged in, now run requested command


if (%parameters) {

    my $p;
    if ($params_is_array) {
        my @t = keys(%parameters);
        $p = \@t;
    } else {
        $p = \%parameters;
    }

    $args{params} = $p;

}


if ($command ne 'logout') {
    $reply = $client->send_receive_service_msg('COMMAND', {
        COMMAND => $command,
        PARAMS => \%args,
        API => 2
    });

    # terminate the login session if we are not in "session reuse" mode
    if (!$params{session}) {
        $client->logout();
    }

    if ($reply->{SERVICE_MSG} eq 'COMMAND') {
        output({ result => $reply->{PARAMS} });
        exit 0;
    } else {
        output({ error => $reply  });
        exit 1;
    }

} else {
    $client->logout();
    debug('Client session terminated');
}


__END__

=head1 NAME

openxpkicli - command line tool for running API requests

=head1 USAGE

openxpkicli [options] command

  Options (all arguments are optional):
    --help                brief help message
    --help COMMAND        show help for given API command
    --man                 full documentation
    --list                list all available API commands
    --socketfile FILE     OpenXPKI daemon socket file
    --instance  NAME      Shortcut to set socket on multi-instance configs
    --realm REALM         OpenXPKI realm
    --authstack STACK     authentication stack to use
    --authuser USER       authentication user
    --authpass PASS       password for authentication user
    --arg KEY=VALUE       pass VALUE to method as parameter KEY
    --filearg KEY=FILE    as --arg but read value from file name
    --param VALUE         stack VALUE onto methods PARAMS array
    --param KEY=VALUE     set VALUE in methods PARAMS hash using KEY
    --debug               enable debug mode
    --timeout             socket timeout in seconds (default 30s)
    --json                activate json mode
                          If active you can pass either a JSON string or
                          a filename to read a json string from as second
                          argument AFTER the command name. The parameters
                          are merged with the other arguments.
    --json-pretty         human readble json
    --session             session mode, see below for details

  Output control - use for testing and debugging only and might change anytime!

    --out FILE            Write output to FILE instead of STDOUT. Works only in
                          conjunction with --json or if the result is a single
                          scalar value.
    --fileout KEY=FILE    Write the value of the result item KEY to FILE.
                          No file is written if the KEY is not part of the
                          result, dies if the file exists.

=head1 INVOCATION

Invoke the named command through the OpenXPKI API using the given connection socket.

Anything passed using C<arg> is added to the methods argument list, you can also
add arguments after the special argument C<--> at the end of the line. If the method
has a PARAMS argument which accepts an array or hash, you can use param to fill this
argument. You must not mix value only and key=value param calls on one command.

=head1 EXAMPLES

openxpkicli --realm "Server CA" --arg message=testmail \
    --param notify_to=pki@mycompany.local send_notification

openxpkicli --realm "Server CA" get_token_info -- alias=vault-2

=head1 Operational Modes

=head2 Batch

This is the default mode, you must pass all parameters as arguments as
stated above. The client will try to login with the credentials given
(or as anonymous) and run the provided command.

=head2 Session

Activated by adding I<--session> to the command line. This mode allows you
to reuse an existing session id for subsequent calls to a command. The
session id must be passed in the environment variable OPENXPKI_SESSION_ID.

To initialize a session, call openxpkicli with the expected auth* parameters
and pass I<login> as command. This will perform the login and print the
session id as output. Put the received id into the environment and run your
commands. If you are done, pass command I<logout>.

If you are using bash, the output of the init command can be directly
passed to an I<eval> to set the environment.

B<Note>: session mode is currently not fully compatible with --json (session
init and error handling do not print proper json).

B<Example>

    openxpkicli --authstack=Testing --authuser=raop --authpass=openxpki \
        --session login

    echo $OPENXPKI_SESSION_ID

    openxpkicli --session get_session_info

    openxpkicli --session logout

Pass I<--session> as parameter and pass the ID of an existings session in

=head2 JSON Mode

    openxpkicli --json search_workflow_instances_count '{"proc_state":["exception","retry_exceeded"]}'

=head2 Interactive

Not supported yet :)
