package System::Introspector::State;
use Moo;
use File::Tree::Snapshot;
use System::Introspector::Gatherer;
use Object::Remote::Future qw( await_all );

use JSON::Diffable qw( encode_json );

has config => (is => 'ro', required => 1);

has root => (is => 'ro', required => 1);

sub user { $_[0]->config->user }

sub sudo_user { $_[0]->config->sudo_user }

sub _log { shift; printf "[%s] %s\n", scalar(localtime), join '', @_ }

sub gather {
    my ($self, @groups) = @_;
    $self->_log('Start');
    for my $group (@groups) {
        my @waiting;
        for my $host ($self->config->hosts) {
            $self->_log("Beginning to fetch group '$group' on '$host'");
            push @waiting, [$host, $self->fetch($host, $group)];
        }
        $self->_log("Now waiting for results");
        my @results = map {
            my ($host, @futures) = @$_;
            my $done = [$host, await_all @futures];
            $self->_log("Received all from group '$group' on '$host'");
            $done;
        } @waiting;
        $self->_log("All gathered for group '$group'");
        for my $result (@results) {
            my ($host, @data) = @$result;
            $self->_store($host, $group, +{ map %$_, @data });
        }
    }
    $self->_log('Done');
    return 1;
}

sub introspectors {
    my ($self, $group) = @_;
    return $self->config->config_for_group($group)->{introspect};
}

sub fetch {
    my ($self, $host, $group) = @_;
    my $spec = $self->introspectors($group);
    my (@sudo, @nosudo);
    push(@{ $spec->{$_}{sudo} ? \@sudo : \@nosudo}, [$_, $spec->{$_}])
        for sort keys %$spec;
    my @futures;
    if (@nosudo) {
        $self->_log("Without sudo: ", join ", ", map $_->[0], @nosudo);
        my $proxy = $self->_create_gatherer(
            host => $host,
            introspectors => [@nosudo],
        );
        push @futures, $proxy->start::gather_all;
    }
    if (@sudo) {
        $self->_log("With sudo: ", join ", ", map $_->[0], @nosudo);
        my $proxy = $self->_create_gatherer(
            sudo => 1,
            host => $host,
            introspectors => [@sudo],
        );
        push @futures, $proxy->start::gather_all;
    }
    return @futures;
}

sub storage {
    my ($self, @path) = @_;
    my $storage = File::Tree::Snapshot->new(
        allow_empty  => 0,
        storage_path => join('/', $self->root, @path),
    );
    $storage->create
        unless $storage->exists;
    return $storage;
}

sub _store {
    my ($self, $host, $group, $gathered) = @_;
    my $storage = $self->storage($host, $group);
    my $ok = eval {
        my @files;
        for my $class (sort keys %$gathered) {
            my $file = sprintf '%s.json', join '/',
                map lc, map {
                    s{([a-z0-9])([A-Z])}{${1}_${2}}g;
                    $_;
                } split m{::}, $class;
            my $fh = $storage->open('>:utf8', $file, mkpath => 1);
            my $full_path = $storage->file($file);
            $self->_log("Writing $full_path");
            print $fh encode_json($gathered->{$class});
            push @files, $full_path;
        }
        $self->_cleanup($storage, [@files]);
        $self->_log("Committing");
        $storage->commit;
    };
    unless ($ok) {
        $self->_log("Rolling back snapshot because of: ", $@ || 'unknown error');
        $storage->rollback;
        die $@;
    }
    return 1;
}

sub _cleanup {
    my ($self, $storage, $known_files) = @_;
    my %known = map { ($_ => 1) } @$known_files;
    my @files = $storage->find_files('json');
    for my $file (@files) {
        next if $known{$file};
        $self->_log("Removing $file");
        unlink($file)
            or die "Unable to remove '$file': $!\n";
    }
    return 1;
}

sub _create_gatherer {
    my ($self, %arg) = @_;
    return System::Introspector::Gatherer->new_from_spec(
        user          => $self->user,
        host          => $arg{host},
        sudo_user     => $arg{sudo} && $self->sudo_user,
        introspectors => $arg{introspectors},
    );
}

1;

=head1 NAME

System::Introspector::State - Gather system state

=head1 SYNOPSIS

    my $state = System::Introspector::State->new(
        host    => 'foo.example.com',
        storage => $storage_obj,
        config  => {
            introspect => [qw( ProbeName )],
        },
    );

    my $data = $state->fetch;
    $state->fetch_and_store;

=head1 DESCRIPTION

Gathers system introspection data based on configuration and stores
it with a L<File::Tree::Snapshot> object.

=head1 ATTRIBUTES

=head2 config

A hash reference containing a C<introspect> key with an array reference
value containing a list of probe names without the
C<System::Introspector::Probe::> prefix. This attribute is required.

=head2 host

An optional hostname. If no hostname is supplied, the local configuration
data will be fetched.

=head2 storage

A L<File::Tree::Snapshot> object.

=head1 METHODS

=head2 fetch

    my $data = $state->fetch;

Fetches all probe data.

=head2 fetch_and_store

    $state->fetch_and_store;

Fetches all probe data and stores it in the L</storage>.

=cut
