#!/usr/bin/perl -w
use OpenXPKI;

# Core modules
use FindBin qw( $Script );
use Getopt::Long;
use List::Util qw(none any);
use Module::Metadata;
use Mojo::Loader;
use JSON::PP;
use Term::ReadKey;

# CPAN modules
use PPI::Document;
use YAML::PP;

# Project modules
use OpenXPKI::i18n qw(set_language);
use OpenXPKI::Client::API;
use OpenXPKI::Client::API::Response;
use OpenXPKI::Client::CLI;
use OpenXPKI::DTO::Authenticator;

=head2 show_help I<command> [I<subcommand>]

Runs C<get_pod_text> on the package name constructed from the given arguments.

If a I<subcommand> is given, evaluates the parameter specification and
renders a description on the parameters.

=cut

signature_for show_help => (
    named => [
        api => 'OpenXPKI::Client::API',
        command => 'Str',
        subcommand => 'Str', { default => '' },
        short => 'Bool', { default => 0 },
    ],
);
sub show_help ($arg) {
    my $api = $arg->api;
    my $cmd = $arg->command;
    my $subcmd = $arg->subcommand // '';
    my $subcmd_var = $arg->subcommand || '[SUBCOMMAND]';

    LOGDIE("Invalid characters in command") unless $cmd =~ m{\A\w*\z};
    LOGDIE("Invalid characters in subcommand") unless $subcmd =~ m{\A\w*\z};

    my $pod;
    $pod.= "=head1 USAGE\n\n%%SCRIPT%% $cmd $subcmd_var [OPTIONS] PARAMETERS\n\n" unless $arg->short;

    #
    # COMMAND help
    #
    # TODO - select right sections and enhance formatting
    unless ($subcmd) {
        $pod.= $api->get_pod($cmd, 'DESCRIPTION') unless $arg->short;

        $pod.= "\n\n=head1 SUBCOMMANDS\n\n=over\n\n";
        my $subcmds = $api->subcommands($cmd);
        for my $subcmd (sort keys $subcmds->%*) {
            $pod.= sprintf "=item %s\n\n%s\n\n", $subcmd, $subcmds->{$subcmd};
        }
        $pod.= "=back\n\n";

        $api->show_pod(-oxi_pod => $pod);
    }


    if ($cmd eq 'api' && none { $subcmd eq $_ } ('','help','list')) {
        $subcmd = 'execute';
    }

    #
    # SUBCOMMAND help
    #
    $pod.= $api->get_pod("${cmd}::$subcmd", 'DESCRIPTION');

    # check for needs_realm and protected flags
    my $package = $api->namespace_commands($cmd)->{$subcmd};
    if ($package) {
        my $meta = $package->meta;
        my @flags;
        push @flags, 'requires privileged access (auth-key)' if $meta->can('protected') && $meta->protected;
        push @flags, 'requires realm (--realm)' if $meta->can('needs_realm') && $meta->needs_realm;
        if (@flags) {
            $pod .= ucfirst(join(', ', @flags))
        }
    }

    my $attrs = $api->get_attribute_details($cmd, $subcmd);
    if (scalar keys $attrs->%*) {
        $pod.= "\n\n=head1 PARAMETERS\n\n=over\n\n";
        for my $name (sort keys $attrs->%*) {
            $pod.= sprintf(
                "=item --%s (%s)\n\n%s\n\n",
                $name, $attrs->{$name}->{spec}, $attrs->{$name}->{desc},
            );
        };
        $pod.= "=back\n\n";
    }

    # might be useful to have something like "fullhelp"?
    #$pod.= $api->get_pod(__FILE__, 'OPTIONS');

    $api->show_pod(-oxi_pod => $pod);
}

sub handle_error ($err, $debug_details = '') {
    # a simple plain error message
    if (not ref $err) {
        ERROR(OpenXPKI::i18n::i18nTokenizer($err));
        exit 10;
    }

    # known special error
    if (blessed $err) {
        if ($err->isa('OpenXPKI::DTO::ValidationException')) {
            ERROR(OpenXPKI::i18n::i18nTokenizer($err->message));
            exit 10;
        }
        if ($err->isa('OpenXPKI::Exception')) {
            ERROR(OpenXPKI::i18n::i18nTokenizer($err->message)) unless $err->__is_logged;
            exit 10;
        }
    }

    # unknown error
    ERROR($debug_details) if $debug_details;
    ERROR(sprintf "Something went wrong (%s)", ref $err);
    exit 255;
}

sub read_password {

    my $msg = shift;
    say $msg if ($msg);
    Term::ReadKey::ReadMode('noecho');
    my $input = Term::ReadKey::ReadLine(0);
    Term::ReadKey::ReadMode('restore');
    my ($password) = $input =~ m{\A\s*(\S+)\s*\z};
    return $password;
}

set_language('en_US');
use Log::Log4perl qw(:easy :no_extra_logdie_message);

my $verbose = 0;
my %opt = ('verbose' => \$verbose);

# pass_through: anything unknown, ambiguous or invalid will be passed through to @ARGV
Getopt::Long::Configure('pass_through','bundling');

GetOptions( \%opt, ('verbose|v+','json','json-pretty','auth-key|k=s','auth-config|c=s','pass|p:s','no-auth','socket=s','out|o=s','help','version|V'));

if ($opt{version}) {
    if (!Mojo::Loader::load_class('OpenXPKI::Enterprise::VERSION')) {
        say "OpenXPKI Enterprise Edition v$OpenXPKI::Enterprise::VERSION::VERSION (core v$OpenXPKI::VERSION::VERSION)";
    } else {
        say "OpenXPKI Community Edition v$OpenXPKI::VERSION::VERSION";
    }
    exit 0;
}

Getopt::Long::Configure('no_pass_through');

my $l4p_level;
my $l4p_layout = '%m%n';
if ($verbose > 2) {
    $l4p_level = $TRACE;
    $l4p_layout = '%l %F:%L %m%n';
} elsif ($verbose == 2) {
    $l4p_level = $DEBUG;
    $l4p_layout = '%m%n';
} elsif ($verbose == 1) {
    $l4p_level = $INFO;
} else {
    $l4p_level = $ERROR;
}
Log::Log4perl->easy_init({ level => $l4p_level, layout => $l4p_layout });

my $api = OpenXPKI::Client::API->new(
    enable_acls => 0,
    script_name => $Script,
);

# Help
$api->show_pod(
    -msg => "Missing command. Use --help for more details.\n",
    -oxi_pod => $api->get_pod(__FILE__),
    -sections => 'COMMANDS',
) unless (@ARGV or $opt{help});

my $want_help = $opt{help};
my $command = shift // '';
my $subcommand = shift  // '';

if ($command eq 'help') {
    $want_help = 1;
    $command = $subcommand;
    $subcommand = shift // '';
}

# help is also avail via flag for subcommands
if ($want_help) {

    if (!$command) {
        $api->show_pod(-oxi_pod => join('', $api->get_pod_nodes(__FILE__)));

    # we let the help request for a named command pass and catch this later
    } elsif ($command ne 'api' || any { $subcommand eq $_ } ('list','help','execute')) {
        show_help(
            api => $api,
            command =>  $command,
            $subcommand ? (subcommand => $subcommand) : (),
        );
    }
}

# List subcommands for command
if (not $subcommand) {
    say "Missing subcommand.\n";
    show_help(
        api => $api,
        command => $command,
        short => 0,
    );
}


# we have command and subcommand so lets handle parameters and dispatch

my @extra_args;
my %params;
# Some commands can consume extra positional args, we strip them now
# afterwards the first argument of ARGV should be something parsed
# by GetOptions which leaves the extra parameters after "--" on ARGV
while (@ARGV && substr($ARGV[0],0,1) ne '-') {
    push @extra_args, shift;
}

# special handling of the API command
# we want to use the server command directly as subcommand
if ($command eq 'api' && none { $subcommand eq $_ } ('list','help','execute')) {
    $params{command} = $subcommand;
    # named command wit help flag passed from above
    if ($want_help) {
        $subcommand = 'help';
    } else {
        $subcommand = 'execute';
    }
}

# Create GetOption spec from the parameters recorded in the command
try {
    if (my @getopts = $api->getopt_params($command, $subcommand)) {
        TRACE('GetOptions parameters: ' . Dumper \@getopts);
        GetOptions( \%params, @getopts ) || exit 1;
    }
}
catch ($error) {
    handle_error($error);
}

# reserved internal command parameters
$params{positional_args} = \@extra_args if @extra_args;
$params{payload} = \@ARGV if @ARGV;

# TODO - Review if there is a better place for this
my %auth_args;

# For non-global commands the API injects 'realm' into the list of required parameters.
# So if realm is present and set we put it into auth_args
# NB: Realm argument for commands is pki_realm
if ($params{realm}) {
    DEBUG('Set command realm ' . $params{realm});
    $auth_args{'pki_realm'} = $params{realm};
}

# Explicit auth key given
if (my $keyfile = $opt{'auth-key'}) {
    if (!-r $keyfile) {
        LOGDIE('Unable to find/read keyfile at ' . $keyfile);
    }
    $auth_args{account_key} = $keyfile;

# Explicit auth config
} elsif ($opt{'auth-config'}) {

} elsif ($keyfile = $ENV{OPENXPKI_CLIENT_KEY_FILE}) {
    if (!-r $keyfile) {
        LOGDIE('Unable to find/read keyfile from ENV at ' . $keyfile);
    }
    $auth_args{account_key} = $keyfile;

} else {
    my $keyfile = glob("~/.oxi/client.key");
    # first try to autodetect the admin mode key file...
    if ((not $opt{'no-auth'}) && -e $keyfile && -r $keyfile) {
        $auth_args{account_key} = $keyfile;

    # ...and second the user config file.
    } else {

    }
}

INFO('Enable privileged mode using keyfile '. $auth_args{account_key})
    if ($auth_args{account_key});

# if we have a keyfile we check if the password flag was given
if ($auth_args{account_key}) {
    my $pass;
    if ($opt{'pass'}) {
        $pass = $opt{'pass'};
        if (substr($pass,0,4) eq 'env:') {
            $pass = $ENV{substr($pass,4)} || die "Given ENV to read password is empty";
        }
    } elsif (defined $opt{'pass'}) {
        $pass = read_password("Please enter your key password: ");
    } elsif ($ENV{OPENXPKI_CLIENT_KEY_PASSPHRASE}) {
        $pass = $ENV{OPENXPKI_CLIENT_KEY_PASSPHRASE};
    }
    $auth_args{account_key} = Crypt::PK::ECC->new($auth_args{account_key}, $pass);
}

INFO('Enable privileged mode using keyfile '. $auth_args{account_key})
    if ($auth_args{account_key});

DEBUG('Using non-standard socket: '.$opt{socket}) if($opt{socket});

my $client = OpenXPKI::Client::CLI->new(
    authenticator => OpenXPKI::DTO::Authenticator->new(%auth_args),
    ($opt{socket} ? (socketfile => $opt{socket}) : ()),
);
$api->client($client);

# Dispatch request
TRACE "Command parameters: " . Dumper  \%params;
my $res;
try {
    my $payload = $api->dispatch(
        rel_namespace => $command,
        command => $subcommand,
        params => \%params,
    );
    $res = OpenXPKI::Client::API::Response->new(
        payload => $payload,
    );
}
catch ($err) {
    $res = OpenXPKI::Client::API::Response->new(
        payload => $err,
        state => 400,
    );
}
TRACE(Dumper $res);

my $json = JSON::PP->new();

if ($res->state == 200) {
    my $out = $res->payload;
    if (blessed $out) {
        $out = $out->params();
    }

    # redirect output if needed
    my $outfile = $opt{out} || '/dev/stdout';
    open(my $OUT, '>', $outfile) || die "unable to write to outfile at $outfile\n";

    # output plain strings as is and ignore format flags
    if (ref $out eq '')  {
        $out = OpenXPKI::i18n::i18nTokenizer($out);
        chomp $out;
        say $OUT $out;
        exit 0;
    }

    # run i18 translater on structure
    $out = OpenXPKI::i18n::i18n_walk($out);
    if ($opt{'json-pretty'}) {
        print $OUT $json->pretty()->encode($out);
    } elsif ($opt{'json'}) {
        print $OUT $json->encode($out);
    } else {
        my $ypp = YAML::PP->new( schema => [qw/ + Perl tags=!perl / ]);
        print $OUT $ypp->dump_string($out);
    }
    exit 0;

} else {
    handle_error($res->payload, sprintf("State: %d", $res->state));
}


=head1 NAME

%%SCRIPT%% - Manage OpenXPKI instances and artefacts

=head1 SYNOPSIS

Manage OpenXPKI instances and artefacts:

    %%SCRIPT%% COMMAND [SUBCOMMAND] [OPTIONS] PARAMETERS

Show detailed help on a command or subcommand:

    %%SCRIPT%% help COMMAND [SUBCOMMAND]
    %%SCRIPT%% COMMAND [SUBCOMMAND] --help

=head1 COMMANDS

%%COMMANDS%%

=head1 OPTIONS

=over

=item --help

Show help on a command or subcommand.

=item --verbose|-v

Raise loglevel to INFO, DEBUG (-vv) or TRACE (-vvv).

=item --json

Print output data as compact JSON structure.

=item --json-pretty

Print output data as formated JSON structure.

=item --out|o

Redirect the output to the given file.

=item --auth-config|-c

Path to a config file to read the authentication details from.

=item --auth-key|-k

Path to the login key to run commands in privileged mode

Default location: ~/.oxi/client.key

=item --pass|-p

Passphrase to decrypt your authentication key in case it is protected.

If used without argument, you will be prompted to enter your password.
If the provided string starts with C<env:> it is assumed to point to
an environment variable holding the password, any other value is
considered to be the literal password.

If no option is given, the ENV key I<OPENXPKI_CLIENT_KEY_PASSPHRASE> is
checked and used as password.

=item --no-auth

Do not authenticate even if a login key/config was found.

=item --socket

Location of the socket to connect to the server.

Default /run/openxpkid/openxpkid.sock

=back

=cut

1;
