#!/usr/bin/perl

=head1 NAME
 
translate - Translate a legacy MeshConfig JSON file to a pSConfig file
 
=head1 SYNOPSIS
 
translate [options] FILE
 
 General Options:
   -h | --help                    detailed help message
   -B | --bind ADDR               local address to bind to when sending http/https requests
   -o | --output FILE             file to output translated JSON    
   -q | --quiet                   suppress output to stdout and stderr
   --input-type TYPE              explicitly indicates type of input file instead of automatically detecting
   --timeout N                    number of seconds to wait to retrieve JSON
   
 MeshConfig JSON Options:
   --disable-bwctl                do not explicitly use bwctl* pscheduler tools in translated output
   --use-force-bidirectional      honor the force_bidirectional setting in MeshConfig tests

 MeshConfig Agent Tasks Options:
   --disable-bwctl                do not explicitly use bwctl* pscheduler tools in translated output 
   --global-archive-dir DIR       the directory to save archive files if --save-global-archives enabled.
   --save-global-archives         save global archives in --global-archive-dir
   --include-added-by-mesh        include tests with added_by_mesh set
   --use-archive-details          use summaries and retry policies, and do not combine measurement_archives with same URL

=head1 ARGUMENTS

=over 8

=item B<FILE>
 
A path to a local file on the filesystem or an http(s) URL to translate

=back

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

=item B<-B | --bind ADDR>
 
Local address to bind to when sending http/https requests

=item B<-o | --output FILE>
 
File to output translated JSON.

=item B<-q | --quiet>
 
Suppresses output to stdout and stderr. Useful when scripting and only concerned about exit code.

=item B<--input-type TYPE>
 
Explicitly indicates type of input file instead of automatically detecting. Valid values
are: meshconfig_json, meshconfig_agent_tasks_conf

=item B<--timeout N>
 
The integer number of seconds to wait to retrieve JSON. Default is 0, which is no timeout. 

=back

=head1 MESHCONFIG JSON OPTIONS

=over 8

=item B<--disable-bwctl >
 
If a test explicitly lists tools in throughput or trace tests to be converted, this option
prevents the translation from automatically adding tools used in BWCTL backward 
compatibility. Specifically, for throughput tests, specifying iperf3 or iperf with this 
option set will ensure no BWCTL fallback if these tools are encountered. Likewise for
trace tests, this option will prevent BWCTL fallback for traceroute and tracepath tests. If
any other tool is specified or no tool is explicitly specified, then this option will have
no effect. 

=item B<--use-force-bidirectional>
 
If force_bidirectional encountered in MeshConfig test, create an additional pSConfig task 
where second participant schedules test if its running an agent (if one end is no-agent 
then the side with the agent will setup both tests whether force_bidirectional is present 
or not). force_bidirectional tells both the source and dest to each ask 
for a test in both directions, meaning you get four tests instead of two. Default is false.
Using this option is NOT recommended for most cases since force_bidirectional largely just 
adds load on hosts, makes firewall problems more likely, and makes dashboards more confusing 
with little to no benefit. 

=back

=head1 MESHCONFIG AGENT TASKS OPTIONS

=over 8

=item B<--disable-bwctl >
 
If a test explicitly lists tools in throughput or trace tests to be converted, this option
prevents the translation from automatically adding tools used in BWCTL backward 
compatibility. Specifically, for throughput tests, specifying iperf3 or iperf with this 
option set will ensure no BWCTL fallback if these tools are encountered. Likewise for
trace tests, this option will prevent BWCTL fallback for traceroute and tracepath tests. If
any other tool is specified or no tool is explicitly specified, then this option will have
no effect. 

=item B<--global-archive-dir DIR>

The directory to save archive files if --save-global-archives  enabled. Defaults to 
/etc/perfsonar/psconfig/archives.d/. 

=item B<--include-added-by-mesh>

Include tests with added_by_mesh set

=item B<--save-global-archives>

Save global archives in --global-archive-dir. If not enabled, global archives are ignored.

=item B<--use-archive-details>

Use summaries and retry policies defined in measurement_archive blocks to be translated, 
and do not combine measurement_archives with same "database" URL. Generally you do not need
to use this and is disabled by default as it cleans out a lot of old defaults that have
accumulated over the years that don't need to be set and just cause problems. 

=back
 
=head1 DESCRIPTION
 
B<This program> will read a legacy MeshConfig JSON file or meshconfig-agent-tasks.conf file from local filesystem or an http(s) URL and convert it to a pSConfig file.
 
=cut
use strict;
use warnings;

use FindBin qw($Bin);
use lib "$Bin/../../lib";
use Getopt::Long qw(GetOptions);
use Pod::Usage;

use perfSONAR_PS::Client::PSConfig::Translators::ApiConnect;
use perfSONAR_PS::Client::PSConfig::ApiFilters;
use perfSONAR_PS::Client::PSConfig::Translators::MeshConfig::Config;
use perfSONAR_PS::Client::PSConfig::Translators::MeshConfigTasks::Config;

#constants
my %valid_input_types = (
    "meshconfig_json" => 1,
    "meshconfig_agent_tasks_conf" => 1
);
my $valid_input_types_str = join ', ', keys %valid_input_types;


#parse options
my $bind;
my $output_file;
my $quiet;
my $help;
my $timeout = 0;
my $input_type;
my $use_force_bidirectional;
my $disable_bwctl;
my $global_archive_dir;
my $include_added_by_mesh;
my $save_global_archives;
my $use_archive_details;
GetOptions(
    'help|h' => \$help,
    'bind|B=s' => \$bind,
    'output|o=s' => \$output_file,
    'quiet|q' => \$quiet,
    'timeout=i' => \$timeout,
    'input-type=s' => \$input_type,
    'use-force-bidirectional' => \$use_force_bidirectional,
    'disable-bwctl' => \$disable_bwctl,
    'global-archive-dir=s' => \$global_archive_dir,
    'include-added-by-mesh' => \$include_added_by_mesh,
    'save-global-archives' => \$save_global_archives,
    'use-archive-details' => \$use_archive_details,
) 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 a URL or file path.",
        -exitval => $exitval,
        -verbose => 0   
    }) unless($quiet);
    exit $exitval;
}elsif(@ARGV > 1){
    my $exitval = 2;
    pod2usage({
        -message => "Only one file or URL can be specified on the command-line",
        -exitval => $exitval,
        -verbose => 0
    }) unless($quiet);
    exit $exitval;
}

#get location of config to translate
my $config_url = shift @ARGV;
chomp $config_url;

#check input types
if($input_type && !$valid_input_types{$input_type}){
    print STDERR "Invalid --input-type given. Valid values are: $valid_input_types_str\n";
    exit 2;
}

#build client 
my $config_filters = new perfSONAR_PS::Client::PSConfig::ApiFilters(timeout => $timeout);
my $config_client = new perfSONAR_PS::Client::PSConfig::Translators::ApiConnect(url => $config_url, filters => $config_filters);
if($bind){
    $config_client->bind_address($bind);
}

#build translators
## meshconfig JSON
unless($input_type && $input_type ne 'meshconfig_json'){
    my $meshconfig_json_translator = new perfSONAR_PS::Client::PSConfig::Translators::MeshConfig::Config();
    #enable force bidirectional if we need to
    if($use_force_bidirectional){
        $meshconfig_json_translator->use_force_bidirectional(1);
    }
    if($disable_bwctl){
        $meshconfig_json_translator->disable_bwctl(1);
    }
    push @{$config_client->translator_configs}, $meshconfig_json_translator;
}
 
## meshconfig-agent-tasks.conf
unless($input_type && $input_type ne 'meshconfig_agent_tasks_conf'){
    my $meshconfig_tasks_translator = new perfSONAR_PS::Client::PSConfig::Translators::MeshConfigTasks::Config();
    if($disable_bwctl){
        $meshconfig_tasks_translator->disable_bwctl(1);
    }
    if($global_archive_dir){
        $meshconfig_tasks_translator->global_archive_dir($global_archive_dir);
    }
    if($include_added_by_mesh){
        $meshconfig_tasks_translator->include_added_by_mesh($include_added_by_mesh);
    }
    if($save_global_archives){
        $meshconfig_tasks_translator->save_global_archives($save_global_archives);
    }
    if($use_archive_details){
        $meshconfig_tasks_translator->use_archive_details($use_archive_details);
    }
    push @{$config_client->translator_configs()}, $meshconfig_tasks_translator;
}

#make sure we have valid translators
unless(@{$config_client->translator_configs()}){
    print STDERR "No valid translators found for input file.\n";
    exit 2;
}

#read configuration and try to translate
my $psconfig = $config_client->get_config();
if($config_client->error()){
    unless($quiet){
        print STDERR "Error retrieving configuration. Encountered the following error:\n\n";
        print STDERR "   " . $config_client->error() . "\n";
        foreach my $translator_config(@{$config_client->translator_configs()}){
            if($translator_config->error()){
                print STDERR "\n";
                print STDERR $translator_config->name() . " translator encountered the following errors:\n\n";
                print STDERR $translator_config->error() . "\n";
            }
        }
    }
    exit 2;
} 

#exit
if($output_file){
    &save_file($psconfig, $output_file, {"pretty" => 1, "canonical" => 1});
    print "Translation successful. Result saved in file $output_file.\n" unless($quiet);
}else{
    print $psconfig->json({"pretty" => 1, "canonical" => 1}) unless($quiet);
}

exit 0;

sub save_file() {
    my ($psconfig, $filename, $formatting_params) = @_;
    $formatting_params = {} unless $formatting_params;
    chomp $filename;
    $filename =~ s/^file:\/\///g;
    unless($filename) {
        print STDERR "No save_filename set\n";
        exit 2;
    }
    eval{
        open(my $fh, ">:encoding(UTF-8)", $filename) or die("Can't open $filename: $!");
        print $fh $psconfig->json($formatting_params);
        close $fh;
    };
    if($@){
        print STDERR "Error saving $filename: $@\n";
        exit 2;
    }
}
