#!/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 Clone qw(clone);

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

use perfSONAR_PS::MeshConfig::Utils qw(load_mesh);

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

my ( $status, $res );

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

$status = GetOptions(
    'input=s'  => \$INPUT,
    'output=s' => \$OUTPUT,
    'logger=s'  => \$LOGGER_CONF,
    'skip-validation' => \$SKIP_VALIDATION,
    'resolve-includes' => \$RESOLVE_INCLUDES,
    '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 = (
                    "include"       => { },
                    "administrator" => { new_key => "administrators" },
                    "test" => { new_key => "tests" },
                    "measurement_archive" => { new_key => "measurement_archives" },
                    "address" => { new_key => "addresses" },
                    "member" => { new_key => "members" },
                    "a_member" => { new_key => "a_members" },
                    "b_member" => { new_key => "b_members" },
                    "site" => { new_key => "sites" },
                    "organization" => { new_key => "organizations" },
                    "host" => { new_key => "hosts" },
                    "no_agent" => { except => [ "hosts" ], new_key => "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, [], "test_spec");
}

# 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, [], "group");
}

if ($configuration{"test"}) {
    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, [], "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} };

	# Fill in any parameters that override parameters from the test spec
        if ($test->{parameters}) {
            for my $param (keys %{ $test->{parameters} }) {
                $test_spec{$param} = $test->{parameters}->{$param};
            }
        }

        $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);

# Validate the mesh by outputing a temporary file, and loading the mesh as
# normal.
if ($RESOLVE_INCLUDES or not $SKIP_VALIDATION) {
    my ($fh, $tmp_json) = tempfile();

    print { $fh } JSON->new->pretty(1)->encode($json_configuration);

    close $fh;

    my ($status, $res) = load_mesh({ configuration_url => "file://".$tmp_json });

    unlink($tmp_json);

    my $meshes = $res;

    unless ($SKIP_VALIDATION) {
        if ($status == 0) {
            eval {
                # Parse the resulting hash to  make sure it's correct. We use strict checking
                foreach my $mesh (@$meshes) {
                    # Parse the resulting hash to  make sure it's correct. We use strict checking
                    $mesh->validate_mesh();
                }
            };
            if ($@) {
                $status = -1;
                $res    = $@;
            }
        }

        unless ($status == 0) {
            print "Resulting mesh is invalid: $res\n";
            exit(-1);
        }
    }

    if ($RESOLVE_INCLUDES) {
        unless ($status == 0) {
            print "Problem resolving includes: $res\n";
            exit(-1);
        }

        my $json_configuration;

        if (length($meshes) == 1) {
            my @unparsed_meshes = ();
            foreach my $mesh (@$meshes) {
                push @unparsed_meshes, $mesh->unparse();
            }

            $json_configuration = \@unparsed_meshes;
        }
        else {
            $json_configuration = $res->unparse();
        }
    }
}

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, $in_key) = @_;

    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}) {
            my $skip;
            if ($array_variables{$key}->{except}) {
                foreach my $except (@{ $array_variables{$key}->{except} }) {
                    $skip = 1 if ($except eq $in_key);
                }
            }

            unless ($skip) {
                if ($array_variables{$key}->{new_key}) {
                    $key = $array_variables{$key}->{new_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, [], $key);
                }
                else {
                    push @new_value, $element;
                }
            }

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

        $new_hash{$key} = $value;
    }

    return \%new_hash;
}
