#!/usr/bin/perl

use strict;
use warnings;

use FindBin qw($RealBin);

use lib "$RealBin/../lib";

use Config::General;
use File::Path;
use Getopt::Long;
use JSON;
use Log::Log4perl qw(:easy);

use Data::Validate::Domain qw(is_domain);
use Data::Validate::IP qw(is_ipv4);
use Net::IP;

use perfSONAR_PS::MeshConfig::Config::Mesh;

Log::Log4perl->easy_init($DEBUG);

my ( $status, $res );

my $INPUT = '-';
my $OUTPUT = '-';
my $LOGGER_CONF;
my $DEBUGFLAG;
my $HELP;

$status = GetOptions(
    'input=s'  => \$INPUT,
    'output=s' => \$OUTPUT,
    'logger=s'  => \$LOGGER_CONF,
    'verbose'   => \$DEBUGFLAG,
    'help'      => \$HELP
);

my $logger;
unless ( $LOGGER_CONF ) {
    use Log::Log4perl qw(:easy);

    my $output_level = $INFO;
    if ( $DEBUGFLAG ) {
        $output_level = $DEBUG;
    }

    my %logger_opts = (
        level  => $output_level,
        layout => '%d (%P) %p> %F{1}:%L %M - %m%n',
    );

    Log::Log4perl->easy_init( \%logger_opts );
}
else {
    use Log::Log4perl qw(get_logger :levels);

    Log::Log4perl->init( $LOGGER_CONF );
}

$logger = get_logger( "perfSONAR_PS" );
$logger->level( $DEBUG ) if $DEBUGFLAG;

my %configuration;

if ($INPUT eq "-") {
    my $config = "";
    while(<>) {
        $config .= $_;
    }
    %configuration = Config::General->new(-String => $config)->getall;
}
else {
    %configuration = Config::General->new($INPUT)->getall;
}

# We maintain a mapping of the array variables to their 'real' name so we know
# what to rename them to.
my %array_variables = (
                    "administrator" => "administrators",
                    "test" => "tests",
                    "measurement_archive" => "measurement_archives",
                    "address" => "addresses",
                    "member" => "members",
                    "a_member" => "a_members",
                    "b_member" => "b_members",
                    "site" => "sites",
                    "organization" => "organizations",
                    "host" => "hosts",
                    "no_agent" => "no_agents",
                );

# Parse everything except the 'test', 'group' and 'test_spec' elements.
my %json_configuration = %{ __parse_hash(\%configuration, [ "test", "group", "test_spec" ]) };

# Go through and parse all the test specs, keeping track of their id.
my %test_specs = ();
foreach my $test_spec_id (keys %{ $configuration{"test_spec"} }) {
    my $desc = $configuration{"test_spec"}->{$test_spec_id};

    $test_specs{$test_spec_id} = __parse_hash($desc);
}

# Go through and parse all the groups, keeping track of their id.
my %groups = ();
foreach my $group_id (keys %{ $configuration{"group"} }) {
    my $desc = $configuration{"group"}->{$group_id};

    $groups{$group_id} = __parse_hash($desc);
}

my @tests = ();

$configuration{"test"} = [] unless $configuration{"test"};

if (ref($configuration{"test"}) ne "ARRAY") {
    $configuration{"test"} = [ $configuration{"test"} ];
}

# Parse all the tests, and merge their group and test_spec so that the test is
# of the appropriate json format.
foreach my $test (@{ $configuration{"test"} }) {
    $test = __parse_hash($test);
    my $group = $test->{group};
    my $test_spec = $test->{test_spec};

    delete($test->{group});
    delete($test->{test_spec});

    unless ($test_specs{$test_spec}) {
       die("missing test spec: ".$test_spec);
    }

    unless ($groups{$group}) {
       die("missing group: ".$group);
    }

    my %test_spec = %{ $test_specs{$test_spec} };
    my %group = %{ $groups{$group} };

    $test->{parameters} = \%test_spec;
    $test->{members}    = \%group;

    # In the .json, no_agents are specified at the group level.
    if ($test->{no_agents}) {
        $test->{members}->{no_agents} = $test->{no_agents};
        delete($test->{no_agents});
    }

    push @tests, $test;
}

$json_configuration{tests} = \@tests;

# Look through the hosts and fill-in measurement archives for any hosts
# labelled "uses_default_toolkit_archives"
__fill_toolkit_archives(\%json_configuration);

# Parse the resulting hash to  make sure it's correct. We use strict checking
# to ensure we're not generating junk.
my $mesh = perfSONAR_PS::MeshConfig::Config::Mesh->parse(\%json_configuration, 1);

$mesh->validate_mesh();

if ($OUTPUT eq "-") {
    print JSON->new->pretty(1)->encode(\%json_configuration);
}
else {
    open(OUTPUT, ">$OUTPUT");
    print OUTPUT JSON->new->pretty(1)->encode(\%json_configuration);
    close(OUTPUT);
}

exit 0;

sub __fill_toolkit_archives {
    my ($element) = @_;

    if ($element->{hosts}) {
        foreach my $host (@{ $element->{hosts} }) {
            if ($host->{uses_default_toolkit_archives}) {
                delete($host->{uses_default_toolkit_archives});
                __add_toolkit_archives($host);
            }
        }
    }

    foreach my $key ("organizations", "sites") {
        if ($element->{$key}) {
            foreach my $elm (@{ $element->{$key} }) {
                __fill_toolkit_archives($elm);
            }
        }
    }

    return;
}

sub __add_toolkit_archives {
    my ($host) = @_;

    return unless $host->{addresses};

    my $default_addr;

    my @dns_names  = grep { is_domain($_) } @{ $host->{addresses} };
    my @ipv4_addrs = grep { is_ipv4($_) } @{ $host->{addresses} };

    if (scalar(@dns_names) > 0) {
        $default_addr = $dns_names[0];
    }

    if (not $default_addr and scalar(@ipv4_addrs) > 0) {
        $default_addr = $ipv4_addrs[0];
    }

    if (not $default_addr) {
        $default_addr = $host->{addresses}->[0];
    }

    return unless $default_addr;

    $host->{measurement_archives} = [
        {
            type      => "perfsonarbuoy/bwctl",
            read_url  => "http://$default_addr:8085/perfSONAR_PS/services/pSB",
            write_url => "$default_addr:8569"
        },
        {
            type      => "perfsonarbuoy/owamp",
            read_url => "http://$default_addr:8085/perfSONAR_PS/services/pSB",
            write_url => "$default_addr:8570"
        },
        {
            type      => "pinger",
            read_url  => "http://$default_addr:8075/perfSONAR_PS/services/pinger/ma"
        }
    ];
}

# Go through the hash, and convert any 'array' variables into an array, and
# rename their 'key'.
sub __parse_hash {
    my ($hash, $skip) = @_;

    my %skip_map = map { $_ => 1 } @$skip;

    my %new_hash = ();

    foreach my $key  (keys %$hash) {
        my $value = $hash->{$key};

        next if ($skip_map{$key});

        if ($array_variables{$key}) {
            $key = $array_variables{$key};

            $value = [ $value ] unless (ref($value) eq "ARRAY");
        }

        if (ref($value) eq "ARRAY") {
            my @new_value = ();

            foreach my $element (@$value) {
                if (ref($element) eq "HASH") {
                    push @new_value, __parse_hash($element);
                }
                else {
                    push @new_value, $element;
                }
            }

            $value = \@new_value;
        }
        elsif (ref($value) eq "HASH") {
            $value = __parse_hash($value);
        }

        $new_hash{$key} = $value;
    }

    return \%new_hash;
}
