#!/usr/bin/perl

=head1 NAME
 
remote - Add/delete/list remote URLs and associated parameters in agent configuration file(s)
 
=head1 SYNOPSIS
 
remote add|delete|list [options] URL
 
 Options:
   -h | --help                  Help message
   --agent AGENT                Name of agent to use (e.g. pscheduler, maddash). Default is to look at all installed agents.
   -f|--file FILE               The file to edit. Must specify --agent if using this option. Default is installed agent defaults.
   --transform JSON|@FILE       JSON transform object or a path to a file starting with @ that alters downloaded json
   --configure-archives         Indicates archives defined in remote URL should be used
   --bind-address ADDRESS       Local address to bind to when downloading JSON
   --quiet                      Suppress output to stdout and stderr
   --ssl-ca-file FILE           A certificate authority (CA) file used to verify server SSL certificate when using https.

=head1 ARGUMENTS

=over 8

=item B<add|delete|list>
 
The action to perform

add: Adds remote definition to agent config file. If agent with same URL already exists then overwrites it.

delete: Deletes any entry with the matching options from the agent configuration file

list: Lists all the remote definitions currently in the agent configuration file. If no URL given lists all definition. If url and/or other parameters given, only matches those with same parameters.

=item B<URL>
 
A URL to the pSConfig JSON file. Not required for list. Can be of type http, https or file. If no prefix then assumed a local file.  

=back

=head1 OPTIONS
 
=over 8
 
=item B<-h | --help>
 
Prints a help message and exits

=item B<--agent AGENT>
 
Name of agent to use (e.g. pscheduler, maddash). Default is to look at all installed agents.

=item B<-f | --file FILE>
 
The file to edit. Must specify --agent if using this option. Default is installed agent defaults.

=item B<--transform JSON|@FILE>
 
JSON transform object or a path to a file starting with @ that alters downloaded json

=item B<--configure-archives>
 
Indicates archives defined in remote URL should be used

=item B<--bind-address>
 
Local address to bind to when downloading JSON

=item B<--quiet>
 
Suppress output to stdout and stderr

=item B<--ssl-ca-file FILE>
 
A typical certificate authority (CA) file found on BSD. Used to verify server SSL certificate when using https.

=back
 
=head1 DESCRIPTION
 
B<This program> will add/delete/list remote URLs and associated parameters in agent configuration file.
 
=cut
use strict;
use warnings;

use FindBin qw($Bin);
use lib "$Bin/../../lib";

use Getopt::Long qw(GetOptions);
use JSON qw( from_json to_json );
use Module::Load;
use Pod::Usage;

use perfSONAR_PS::PSConfig::CLI::Constants;
use perfSONAR_PS::Client::PSConfig::JQTransform;
use perfSONAR_PS::PSConfig::Remote;
use perfSONAR_PS::PSConfig::TransformConnect;

#parse options
my $help;
my $agent_opt;
my $file_opt;
my $transform_opt;
my $config_archives;
my $bind_address;
my $quiet_opt;
my $ssl_file;
GetOptions(
    'help|h' => \$help,
    'f|file=s' => \$file_opt,
    'transform=s' => \$transform_opt,
    'agent=s' => \$agent_opt,
    'configure-archives' => \$config_archives,
    'bind-address=s' => \$bind_address,
    'quiet' => \$quiet_opt,
    'ssl-ca-file=s' => \$ssl_file,
) or pod2usage({ -verbose => 0, -exitval => 2 });

#parse arguments
if($help){
    pod2usage({ -exitval => 0, -verbose => 2, -noperldoc => 1});
}elsif(@ARGV == 0){
    my $exitval = 2;
    pod2usage({
        -message => "Must specify an ACTION(add, delete or list) and URL/file path to JSON file.",
        -exitval => $exitval,
        -verbose => 0   
    });
    exit $exitval;
}elsif(@ARGV > 2){
    my $exitval = 2;
    pod2usage({
        -message => "Too many parameters given. Only specify an ACTION(add, delete or list) and URL/file path to JSON file.",
        -exitval => $exitval,
        -verbose => 0
    });
    exit $exitval;
}
#get action
my $action = shift @ARGV;
chomp $action;
$action = lc($action);

#make sure we have URL
if($action eq 'add' || $action eq 'delete'){
    if(@ARGV != 1){
        my $exitval = 2;
        pod2usage({
            -message => "URL required for $action action",
            -exitval => $exitval,
            -verbose => 0
        });
        exit $exitval;
    }
}elsif($action ne 'list'){
    my $exitval = 2;
    pod2usage({
        -message => "Invalid action given: $action. Must be one of add, delete or list.",
        -exitval => $exitval,
        -verbose => 0
    });
    exit $exitval;
}

#get url
my $url;
if(@ARGV > 0){
    $url = shift @ARGV;
    chomp $url;
}

#check file_opt
if($file_opt && !$agent_opt){
    _error("Must specify --agent when specifying --file");
    exit 2;
}

my $success_msg = "";
my $agent_found = 0;
foreach my $cli_agent(@{perfSONAR_PS::PSConfig::CLI::Constants::CLI_AGENTS()}){
    #get agent name
    my $cli_agent_name = '=== ' . $cli_agent->{name} . " Agent ===";
    if($agent_opt && lc($agent_opt) ne lc($cli_agent->{name})){
        next;
    }
    #see if file exists
    my $config_file = $file_opt ? $file_opt : $cli_agent->{config_file};
    unless(-e $config_file){
        next;
    }
    #should be good to do some work now
    $agent_found = 1;
    
    #load config file
    my ($agent_conf_client, $agent_conf) = _load_config($config_file, $cli_agent->{client_class}, $cli_agent_name);
    next unless($agent_conf_client);
    
    #build remote
    my $remote = new perfSONAR_PS::PSConfig::Remote();
    $remote->url($url) if($url);
    my $url_only_checksum = $remote->checksum();
    $remote->configure_archives(1) if($config_archives);
    $remote->bind_address($bind_address) if($bind_address);
    $remote->ssl_ca_file($ssl_file) if($ssl_file);
    if($transform_opt){
        if($transform_opt =~ /^\@/){
            my $jqt = _load_transform(substr($transform_opt, 1), $cli_agent_name);
            next unless($jqt);
            $remote->transform($jqt);
        }else{
            my $jqt = new perfSONAR_PS::Client::PSConfig::JQTransform();
            $jqt->script($transform_opt);
            $remote->transform($jqt);
        }
    }
    if($remote->validation_error()){
        _error($remote->validation_error());
        exit 2;
    }
    my $remote_checksum = $remote->checksum();
    my $is_url_only = ($url && $remote_checksum eq $url_only_checksum);

    #perform action
    $success_msg .= "\n" if($success_msg);
    $success_msg .= "${cli_agent_name}\n";
    if($action eq 'add'){
        my $found = 0;
        for(my $i = 0; $i < @{$agent_conf->remotes()}; $i++){
            #if command just provided URL, match anything with that URL
            #if provided more, match entire checksum. 
            if($agent_conf->remote($i)->url() eq $remote->url()){
                $success_msg .= "Replaced existing remote configuration for $url\n";
                $agent_conf->remote($i, $remote);
                $found = 1;
                last;
            }
        }
        $success_msg .= "Added remote configuration $url\n" unless($found);
        $agent_conf->add_remote($remote) unless($found);
        _save_file($agent_conf_client, $agent_conf, $cli_agent_name);
    }elsif($action eq 'delete'){
        my $found = 0;
        for(my $i = 0; $i < @{$agent_conf->remotes()}; $i++){
            if(($is_url_only && $agent_conf->remote($i)->url() eq $remote->url()) ||
                    (!$is_url_only && $agent_conf->remote($i)->checksum() eq $remote_checksum)){
                $success_msg .= "Removed remote configuration for $url\n";
                $agent_conf->remove_list_item('remotes', $i);
                $found = 1;
                last;
            }
        }
        $success_msg .= "No remote configuration for $url found\n" unless($found);
        _save_file($agent_conf_client, $agent_conf, $cli_agent_name);
    }elsif($action eq 'list'){
        my $output = "[";
        my $printed = 0;
        foreach my $conf_remote(@{$agent_conf->remotes()}){
            if($is_url_only && $conf_remote->url() ne $url){
                next;
            }elsif(!$is_url_only && $url && $conf_remote->checksum() ne $remote_checksum){
                next;
            }
            if($printed){
                $output .= ',';
            }else{
                $printed = 1;
            }
            $output .= $conf_remote->json(); 
        }
        $output .= "]";
        #format nicely
        $success_msg .= to_json(from_json($output), {pretty=> 1});
    }
}

#Finish
if($agent_found){
    _success($success_msg);
    exit 0;
}elsif($agent_opt){
    _error("Unable to find installed agent with name $agent_opt");
    exit 1;
}else{
    _error("Unable to find any agents installed on this host");
    exit 1;
}

#Subroutines
sub _success{
    my $msg = shift;
    print "$msg" if($msg && !$quiet_opt);
}

sub _error{
    my ($msg, $cli_agent_name) = @_;
    print STDERR "$cli_agent_name\n" if($cli_agent_name && !$quiet_opt);
    print STDERR "$msg\n" unless($quiet_opt);
}

sub _load_config {
    my ($config_file, $config_class, $cli_agent_name) = @_;
    
    ##
    # Load client class
    eval{
        load  $config_class;
    };
    if($@){
        _error("\n[$cli_agent_name agent] It does not appear the agent is full installed. You may want to reinstall.");
        return;
    }
    
    ##
    #load config file
    my $agent_conf_client = $config_class->new(
        url => $config_file,
        save_filename => $config_file
    );
    if($agent_conf_client->error()){
        _error("Error opening $config_file: " . $agent_conf_client->error(), $cli_agent_name);
        return;
    } 
    my $agent_conf = $agent_conf_client->get_config();
    if($agent_conf_client->error()){
        _error("Error parsing $config_file: " . $agent_conf_client->error(), $cli_agent_name);
        return;
    }
    my @agent_conf_errors = $agent_conf->validate();
    if(@agent_conf_errors){
        my $err = "$config_file is not valid. The following errors were encountered: \n";
        foreach my $error(@agent_conf_errors){
            $err .= "    JSON Path: " . $error->path . "\n";
            $err .= "    Error: " . $error->message . "\n";
        }
        _error($err, $cli_agent_name);
        return;
    }
    
    return ($agent_conf_client, $agent_conf);
}

sub _load_transform {
    my ($config_file, $cli_agent_name) = @_;
    
    my $transform_client = new perfSONAR_PS::PSConfig::TransformConnect(url => $config_file);
    my $transform = $transform_client->get_config();
    if($transform_client->error()){
        _error("Error reading default transform file: " . $transform_client->error(), $cli_agent_name);
        return;
    } 
    #validate
    my @errors = $transform->validate();
    if(@errors){
        my $err = "$config_file is not a valid transform file. The following errors were encountered: \n";
        foreach my $error(@errors){
            my $path = $error->path;
            $path =~ s/^\/transform//; #makes prettier error message
            $err .= "    JSON Path: $path\n";
            $err .= "    Error: " . $error->message . "\n";
        }
        _error($err, $cli_agent_name);
        return;
    }
    
    return $transform;
}

sub _save_file{
    my ($agent_conf_client, $agent_conf, $cli_agent_name) = @_;
    $agent_conf_client->save_config($agent_conf, {'pretty' => 1});
    if($agent_conf_client->error()){
        _error("Error saving configuration: " . $agent_conf_client->error(), $cli_agent_name);
        return;
    }
}