#!/usr/bin/perl

use strict;
use warnings;

use FindBin qw($RealBin);

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

use Config::General;
use Getopt::Long;
use Log::Log4perl qw(:easy);
use URI;

use perfSONAR_PS::Client::LS::PSQueryObjects::PSServiceQueryObject;
use perfSONAR_PS::MeshConfig::Utils qw( build_json );

use SimpleLookupService::Client::SimpleLS;
use SimpleLookupService::Client::Query;


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

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

my ( $status, $res );

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


#setup logging
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;

#read input configuration
my %conf;

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

#determine LS URL
unless ($conf{ls_instance}){
    $logger->error("Unable to determine ls_instance");
    exit( -1 ); 
}
if(ref($conf{ls_instance}) ne 'ARRAY'){
    $conf{ls_instance} = [ $conf{ls_instance} ];
}

#init LS client(s)
my @ls_clients = ();
foreach my $ls_i(@{$conf{ls_instance}}){
    my $ls_connection = SimpleLookupService::Client::SimpleLS->new();
    my $uri = URI->new($ls_i); 
    my $ls_port =$uri->port();
    if(!$ls_port &&  $uri->scheme() eq 'https'){
        $ls_port = 443;
    }elsif(!$ls_port){
        $ls_port = 80;
    }
    $ls_connection->init( host=> $uri->host(), port=> $ls_port );
    my $ls_client = SimpleLookupService::Client::Query->new();
    $ls_client->init( { server => $ls_connection } );
    push @ls_clients, $ls_client;
}

#get query object and make a list
if(ref($conf{query}) ne 'ARRAY'){
    $conf{query} = [ $conf{query} ];
}

#iterate through LS query definitions
my $org_map = {};
my $site_map = {};
my $host_map = {};
my $default_org = {'site' => []};
foreach my $lsq_def(@{$conf{query}}){
    #validate query definitions
    if(!$lsq_def->{service_type}){
        $logger->error("Invalide query object in config file: Missing service_type field");
        exit( -1 ); 
    }elsif(ref($lsq_def->{service_type}) ne 'ARRAY'){
        $lsq_def->{service_type} = [ $lsq_def->{service_type} ];
    }
    
    if(!$lsq_def->{filter}){
        $lsq_def->{filter} = [];
    }elsif(ref($lsq_def->{filter}) ne 'ARRAY'){
        $lsq_def->{filter} = [ $lsq_def->{filter} ];
    }
    
    #determine organization
    my $current_org = $default_org;
    delete $current_org->{tag} if($current_org->{tag});
    my @measurement_archives = ();
    my %tags = ();
    if($lsq_def->{output_settings}){
        # set the tags
        for my $tag_type( ('organization', 'site', 'host', 'address') ){
            $tags{$tag_type} = [];
            my $tag_label = "${tag_type}_tag";
            if($lsq_def->{output_settings}->{$tag_label}){
                if(ref($lsq_def->{output_settings}->{$tag_label}) ne 'ARRAY'){
                    $lsq_def->{output_settings}->{$tag_label} = [ $lsq_def->{output_settings}->{$tag_label} ];
                }
                foreach my $tag(@{$lsq_def->{output_settings}->{$tag_label}}){
                    push @{$tags{$tag_type}}, $tag;
                }
            }
        }
        
        # set the organization name
        if($lsq_def->{output_settings}->{organization_name}){
            if($org_map->{$lsq_def->{output_settings}->{organization_name}}){
                $current_org = $org_map->{$lsq_def->{output_settings}->{organization_name}};
                _merge_tags($current_org->{tag}, $tags{'organization'});
            }else{
                $current_org = {
                    'description' => $lsq_def->{output_settings}->{organization_name},
                    'site' => [],
                    'tag' => _copy_config($tags{'organization'})
                };
                $org_map->{$lsq_def->{output_settings}->{organization_name}} = $current_org;
            }
        }
        
        # set the measurement archives
        if($lsq_def->{output_settings}->{measurement_archive}){
            if(ref($lsq_def->{output_settings}->{measurement_archive}) ne 'ARRAY'){
                $lsq_def->{output_settings}->{measurement_archive} = [ $lsq_def->{output_settings}->{measurement_archive} ];
            }
            foreach my $archive(@{$lsq_def->{output_settings}->{measurement_archive}}){
                if(!$archive->{type}){
                    $logger->error("Invalid archive definition in config file: Missing type field");
                    exit( -1 ); 
                }elsif(ref($archive->{type}) ne 'ARRAY'){
                    $archive->{type} = [ $archive->{type} ];
                }
                
                foreach my $archive_type(@{$archive->{type}}){
                    push @measurement_archives, {
                      'type' =>  $archive_type,
                      'read_url' => $archive->{read_url},
                      'write_url' => $archive->{write_url},
                    };
                }
            }
            
        }
    
    }
    
    #build query
    my $query = new perfSONAR_PS::Client::LS::PSQueryObjects::PSServiceQueryObject;
    ##add filters
    foreach my $lsq_filter(@{$lsq_def->{filter}}){
        if(!$lsq_filter->{filter_key}){
            $logger->error("Invalid query filter: Missing filter_key field");
            exit( -1 ); 
        }
        if(!$lsq_filter->{filter_value}){
            $logger->error("Invalid query filter: Missing filter_value field");
            exit( -1 ); 
        }
        $query->addField({'key' => $lsq_filter->{filter_key}, 'value' => $lsq_filter->{filter_value} });
    }
    ##iterate through service type
    foreach my $lsq_service_type(@{$lsq_def->{service_type}}){
        $query->setServiceType($lsq_service_type);
        foreach my $ls_client(@ls_clients){
            my ($resCode, $res) = $ls_client->query($query);
            unless($resCode == 0){
                $logger->warn("Error performing query. Moving to next query. Error was: " . $res->{message});
                next;
            }
            foreach my $ls_result(@{$res}){
                #determine the site
                my $is_new_site = 0;
                my $current_site = {'host' => [], 'tag' => _copy_config($tags{'site'})};
                if($ls_result->getSiteName()){
                    if($site_map->{$ls_result->getSiteName()->[0]}){
                        $current_site = $site_map->{$ls_result->getSiteName()->[0]};
                        _merge_tags($current_site->{tag}, $tags{'site'});
                    }else{
                       $current_site = {'description' => $ls_result->getSiteName()->[0], 'host' => [], 'tag' => _copy_config($tags{'site'})};
                       $site_map->{$ls_result->getSiteName()->[0]} = $current_site;
                       $is_new_site = 1;
                    }
                }else{
                    #create separate site for each unlabeled host
                    $is_new_site = 1;
                }
            
                #determine the host
                my $is_new_host = 0;
                my $current_host = {'address' => [], 'measurement_archive' => _copy_config(\@measurement_archives), 'tag' => _copy_config($tags{'host'})};
                if($ls_result->getServiceHost()){
                    if($host_map->{$ls_result->getServiceHost()->[0]}){
                        $current_host = $host_map->{$ls_result->getServiceHost()->[0]};
                        _merge_tags($current_host->{tag}, $tags{'host'});
                        _merge_archives($current_host->{measurement_archive}, \@measurement_archives);
                    }else{
                       $host_map->{$ls_result->getServiceHost()->[0]} = $current_host;
                       push @{$current_site->{host}}, $current_host;
                       $is_new_host = 1;
                    }
                }else{
                    $logger->warn("Found service at " . $ls_result->getServiceLocators()->[0] . "without a host record. Skipping to avoid duplicate hosts.");
                    next;
                }
            
                #add site here so we don't add empty sites
                push @{$current_org->{site}}, $current_site if($is_new_site && $is_new_host); 
            
                ##iterate through all addresses
                my %address_map = map { $_->{address} => $_ } @{$current_host->{address}};
                foreach my $service_url(@{$ls_result->getServiceLocators()}){
                    #determine address
                    $service_url =~ s|^.+://|http://|; #hack to get URI to recognize URL
                    my $address = URI->new($service_url)->host();
                
                    #determine if address already exists
                    if($address_map{$address}){
                         _merge_tags($address_map{$address}->{tag}, $tags{'address'});
                    }else{
                        #create new address
                        my $addr_obj = {
                            address => $address,
                            tag => _copy_config($tags{'address'})
                        };
                        push @{$current_host->{address}}, $addr_obj;
                        $address_map{$address} = $addr_obj; #avoid duplicates in same record
                    }
                }
            }
        }
    }
}

#output file
my @org_list = values %{$org_map};
push @org_list, $default_org if(@{$default_org->{'site'}} > 0);
my %output_config = Config::General->new({ organization => \@org_list })->getall;
my ($json_status, $result) = build_json(configuration => \%output_config, 
                                    skip_validation => 0, 
                                    resolve_includes => 0);
if($json_status < 0){
    print $result;
    exit(-1);
}

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

# Merge tag list when you encounter the same org/site/host/address multiple times
sub _merge_tags {
    my ($tags, $new_tags) = @_;
    
    my %tag_map = map { $_ => 1 } @{$tags};
    foreach my $new_tag(@{$new_tags}){
        push @{$tags}, $new_tag unless($tag_map{$new_tag});
    }
}

# Merge tag archives when you encounter the same org/site/host/address mutiple times
# Throws warning when there is conflict and uses original value
sub _merge_archives {
    my ($archives, $new_archives) = @_;
    
    my %archive_map = map { $_->{type} => $_ } @{$archives};
    foreach my $new_archive(@{$new_archives}){
        if($archive_map{$new_archive->{type}}){
            my $archive = $archive_map{$new_archive->{type}};
            if($archive->{read_url} ne $new_archive->{read_url}){
                $logger->warn("Conflicting measurement_archives in config with type " . 
                    $new_archive->{type} . ". read_url values conflict: " . 
                    $archive->{read_url} . " and " . $new_archive->{read_url} . 
                    ". Using " . $archive->{read_url});
            }
            if($archive->{write_url} ne $new_archive->{write_url}){
                $logger->warn("Conflicting measurement_archives in config with type " . 
                    $new_archive->{type} . ". write_url values conflict: " . 
                    $archive->{write_url} . " and " . $new_archive->{write_url} . 
                    ". Using " . $archive->{write_url});
            }
        }else{
            push @{$archives}, $new_archive;
        }
    }
}

# Simple array copy function for when we don't want to pass around a reference
sub _copy_config {
    my $config = shift @_;
    my @new_config = @{$config};
    return \@new_config;
}

