package NGCP::Test::ReferenceData;
use strict;
use warnings;

use Moose;
use File::Basename;
use File::Slurp;
use File::Find::Rule;
use File::Path qw/make_path/;
use JSON qw/from_json to_json/;
use Data::Dumper;

has 'sid' => (
    isa => 'Maybe[Str]',
    is => 'rw',
    lazy => 1,
    default => sub { my $self = shift; $self->_test->generate_sid(); },

);

has 'use_persistent' => (
    isa => 'Int',
    is => 'rw',
    default => 1,
);

has 'persistent_path' => (
    isa => 'Str',
    is => 'rw',
    default => '/tmp',
);

has 'client' => (
    isa => 'Object',
    is => 'ro',
);

has 'ref_dir' => (
    is => 'rw',
    isa => 'Str',
    default => 'ReferenceData',
);

has 'depends' => (
    is => 'rw',
    isa => 'ArrayRef',
    default => sub {[]},
);

has '_dependstree' => (
    is => 'rw',
    isa => 'HashRef',
    default => sub {{}},
);

has '_refdata' => (
    is => 'rw',
    isa => 'ArrayRef',
    default => sub {[]},
);

has '_reftree' => (
    is => 'rw',
    isa => 'HashRef',
    default => sub {{}},
);

has '_refurls' => (
    is => 'rw',
    isa => 'ArrayRef',
    default => sub {[]},
);

has 'delete_persistent' => (
    isa => 'Bool',
    is => 'rw',
    default => 0,
);

has '_test' => (
    isa => 'Object',
    is => 'ro',
);

sub BUILD {
    my ($self) = @_;

    my $ref_done = 0;
    unless(defined $self->sid) {
        $self->sid("".time);
    } else {
        if($self->use_persistent) {
            my $data_path = $self->persistent_path . "/" . $self->sid . "/refdata.json";
            my $tree_path = $self->persistent_path . "/" . $self->sid . "/reftree.json";
            if(-r $data_path && -r $tree_path) {
                #$self->_test->debug("Persistent file $data_path exists, reading from there\n");
                my $json = read_file($data_path);
                my $data = from_json($json);
                $self->_refdata($data);

                #$self->_test->debug("Persistent file $tree_path exists, reading from there\n");
                $json = read_file($tree_path);
                $data = from_json($json);
                $self->_reftree($data);

                $ref_done = 1;
            }
        }
    }
    unless($ref_done) {
        $self->_read_refdata();

        # if we read it from file, write it back to cache if enabled
        if($self->use_persistent) {
            my $path = $self->persistent_path . "/" . $self->sid;
            #$self->_test->debug("Checking for persistent path '$path'\n");
            if(-d $path) {
                #$self->_test->debug("Persistent path $path already exists, skip writing data\n");
            } else {
                #$self->_test->debug("Creating persistent path '$path'\n");
                make_path $path or die "Failed to create persistent path $path: $!\n";

                my $json = to_json($self->_refdata);
                my $fpath = "$path/refdata.json";
                open my $fh, ">", $fpath or die "Failed to open persistent file $fpath for writing: $!\n";
                print $fh $json;
                close $fh;

                $json = to_json($self->_reftree);
                $fpath = "$path/reftree.json";
                open $fh, ">", $fpath or die "Failed to open persistent file $fpath for writing: $!\n";
                print $fh $json;
                close $fh;
            }
        } else {
            #$self->_test->debug("Persistent feature disabled\n");
        }
    }

    $self->_read_depends();
}

sub _read_refdata {
    my ($self) = @_;

    my ($name,$path,$suffix) = fileparse(__FILE__);
    $path .= $self->ref_dir;

    my @files = File::Find::Rule->file()
        ->name('*.json')
        ->in(($path));

	my @json = ();
    foreach my $file(@files) {
        my $json = read_file($file)
            or die "Failed to open reference file '$file': $!\n";

        my $data= from_json($json);
        if(ref $data eq "ARRAY") {
            push @json, @{ $data };
        } elsif(ref $data eq "HASH") {
            push @json, $data;
        } else {
            die "Reference json file '$file' must contain array or object\n";
        }
    }

    $self->_refdata(\@json);

    foreach my $ref(@{ $self->_refdata }) {
        $self->_resolve_deps($ref->{name}, $ref);
    }

    #$self->_test->debug Dumper $self->_reftree;


}

sub _read_depends {
    my ($self) = @_;
    foreach my $d(@{ $self->depends }) {
        my $resource_found = 0;
        foreach my $k(keys %{ $self->_reftree }) {
            my $ref = $self->_reftree->{$k};
            #$self->_test->debug("Checking dependency '$$d{resource}' against reference '$$ref{name}' of type '$$ref{type}'\n");
            if($ref->{type} eq $d->{resource}) {
                #$self->_test->debug("Found a reference data entry of resource $$d{resource}\n");
                if(defined $d->{hints}) {
                    my $hint_found = 0;
                    foreach my $hint(@{ $d->{hints} }) {
               
                        #$self->_test->debug("Checking hint field $$hint{field} with expected value $$hint{value} against ".$ref->{data}->{$hint->{field}}."\n");
                        if(defined $hint->{name} && $hint->{name} eq $ref->{name}) {
                            $hint_found = 1;
                            $self->_dependstree->{$d->{name}}->{data} = $ref->{data};
                            $self->_dependstree->{$d->{name}}->{meta}->{type} = $ref->{type};
                            last;
                        } elsif(defined $hint->{field} && exists $ref->{data}->{$hint->{field}} &&
                           "".$hint->{value} eq "".$ref->{data}->{$hint->{field}}) {

                            #$self->_test->debug("found reference data using hints:\n");
                            #$self->_test->debug Dumper $ref->{data};
                            $hint_found = 1;
                            $self->_dependstree->{$d->{name}}->{data} = $ref->{data};
                            $self->_dependstree->{$d->{name}}->{meta}->{type} = $ref->{type};
                            last;
                        }
                    }
                    if($hint_found) {
                        $resource_found = 1;
                        last;
                    }
                } else {
                    #$self->_test->debug("found reference data:\n");
                    #$self->_test->debug Dumper $ref->{data};
                    $resource_found = 1;
                    $self->_dependstree->{$d->{name}}->{data} = $ref->{data};
                    $self->_dependstree->{$d->{name}}->{meta}->{type} = $ref->{type};
                    last;
                }
            }
        }
        unless($resource_found) {
            die "No reference data for '$$d{resource}' found with given hints\n";
        }
    }
}

sub _replace_vars {
    my ($self, $var, $refname) = @_;

    if(ref $$var eq "HASH") {
        foreach my $k(keys %{ $$var }) {
            $self->_replace_vars(\$$var->{$k}, $refname);
        }
    } elsif(ref $$var eq "ARRAY") {
        foreach my $k(@{ $$var }) {
            $self->_replace_vars(\$k, $refname);
        }
    } elsif(ref $$var ne "") {
        return;
    } else {
        my @vars = $$var =~ /(\$\{.+?\})/g;
        $self->_test->debug("Found vars in $refname:\n");
        $self->_test->debug(Dumper \@vars);
        foreach my $tvar(@vars) {
            my $val;
            my $revar = $tvar;
            $revar =~ s/([\$\{\}])/\\$1/g;
            $self->_test->debug("Replaced varname '$tvar' by revar '$revar'\n");
            if($tvar eq '${sid}') {
                $val = $self->sid;
                $self->_test->debug("Replace $tvar by sid $val\n");
            } else {
                my $len = length($tvar) - 3;
                my $varname = substr($tvar, 2, $len);
                $self->_test->debug("Extracted varname '$varname'\n");
                unless(exists $self->_reftree->{$varname}) {
                    die "Internal error, unresolved dependency '$varname'\n";
                }
                $val = $self->_reftree->{$varname}->{data}->{id};
            }
            $self->_test->debug("about to replace, before='$$var'\n");
            $$var =~ s/$revar/$val/g;
            $self->_test->debug("done replace, after='$$var'\n");
        }
    }
}

sub _resolve_deps {
    my ($self, $refname, $ref) = @_;

    #$self->_test->debug("---- entering _resolve_deps with refname='$refname' and ref=\n");
    #$self->_test->debug Dumper $ref;

    if(defined $ref) {
        if(exists $ref->{depends}) {
            foreach my $dep(@{ $ref->{depends} }) { 
                #$self->_test->debug("found dependency '$dep' on ref '$refname'\n");
                my $d = $self->_resolve_deps($dep);
                unless($d) {
                    die "Unresolved dependency '$dep' on '$refname'\n";
                }
                $self->_resolve_deps($dep, $d);
            }
            $self->_replace_vars(\$ref->{data}, $refname);

            unless($ref->{data}->{id}) {
                #$self->_test->debug("The '$refname' has no id yet, create via API, uri is 'api/$$ref{type}\n");
                #$self->_test->debug Dumper $ref->{data};
                my $url = 'api/'.$ref->{type};
                my $res = $self->client->_post('api/'.$ref->{type}, $ref->{data});
                unless($res->is_success) {
                    die "Failed to create $refname: ".$res->status_line."\n";
                }
                my $id = $res->header('Location');
                $id =~ s/^(.+\/)([^\/]+)$/$2/;
                $ref->{data}->{id} = $id; # TODO: refetch if no content?
                $url .= "/$id";
                $self->_reftree->{$refname} = $ref;
                push @{ $self->_refurls }, $url;
            }
            return $ref;
        }
    } else {
        if(exists $self->_reftree->{$refname}) {
            #$self->_test->debug("Found ref '$refname' in tree, just return it\n");
            return $self->_reftree->{$refname};
        } else {
            foreach my $ref(@{ $self->_refdata }) {
                if($ref->{name} eq $refname) {
                    #$self->_test->debug("Found ref '$refname' in JSON, return for creation\n");
                    return $ref;
                }
            }
        }
    }

}

sub data {
    my ($self, $name) = @_;
    my $ref = $self->_dependstree->{$name};
    if($ref) {
        if($ref->{meta}->{full}) {
            return $ref->{data};
        } else {
            $self->_test->debug("$name not fully fetched yet\n");
            my $res = $self->client->_get(
                'api/'.$ref->{meta}->{type}.'/'.$ref->{data}->{id}
            );
            unless($res->is_success) {
                die "Failed to fully fetch " .
                    "$ref->{meta}->{type} #$ref->{data}->{id}: " .
                    $res->status_line."\n";
            }
            my $data = from_json($res->decoded_content);
            $self->_dependstree->{$name}->{data} = $data;
            $self->_test->debug("$name now fully fetched yet\n");
            $self->_dependstree->{$name}->{meta}->{full} = 1;
            return $data;
        }
    } else {
        die "Failed to find given dependency name '$name' in dependency tree, check name against 'depends' in constructor\n";
    }
}

sub DESTROY {
    my ($self) = @_;

	if($self->delete_persistent) {
		while((my $url = pop @{ $self->_refurls })) {
            #$self->_test->debug("Deleting $url\n");
			my $res = $self->client->_delete($url);
            unless($res->is_success) {
                my $data = from_json($res->decoded_content);
                if($res->code == 404) {
			        my $res = $self->client->_patch($url, [{
                        op => 'replace',
                        path => '/status',
                        value => 'terminated'
                    }]);
                    unless($res->is_success) {
                        $self->_test->info("Failed to both auto-delete or auto-terminate '$url': $$data{message}\n");
                    } else {
                        #$self->_test->debug("The $url successfully terminated\n");
                    }
                }
            } else {
                #$self->_test->debug("The $url successfully deleted\n");
            }
		}
	}
}

1;
