package Ocsinventory::Agent::Backend;

use strict;
no strict 'refs';
use warnings;

#use ExtUtils::Installed;

sub new {
    my (undef, $params) = @_;

    my $self = {};

    $self->{accountconfig} = $params->{context}->{accountconfig};
    $self->{accountinfo} = $params->{context}->{accountinfo};
    $self->{config} = $params->{context}->{config};
    $self->{inventory} = $params->{inventory};

    $self->{common} = $params->{context}->{common};

    my $logger = $self->{logger} = $params->{context}->{logger};
    $self->{prologresp} = $params->{prologresp};

    $self->{modules} = {};

    $self->{backendSharedFuncs} = {
    
        can_run => sub {
            my $binary = shift;
    
            my $calling_namespace = caller(0);
            chomp(my $binpath=`which $binary 2>/dev/null`);
            return unless -x $binpath;
            $self->{logger}->debug(" - $binary found");
            1
        },
        can_load => sub {
            my $module = shift;
    
            my $calling_namespace = caller(0);
            eval "package $calling_namespace; use $module;";
          # print STDERR "$module not loaded in $calling_namespace! $!: $@\n" if $@;
            return if $@;
             $self->{logger}->debug(" - $module loaded");
          # print STDERR "$module loaded in $calling_namespace!\n";
            1;
        },
        can_read => sub {
            my $file = shift;
            return unless -r $file;
            $self->{logger}->debug(" - $file can be read");
            1;
        },
        runcmd => sub {
            my $cmd = shift;
            return unless $cmd;
    
            # $self->{logger}->debug(" - run $cmd");
    
            return `$cmd`;
        }
    };
    bless $self;
}

sub initModList {
    my $self = shift;

    my $logger = $self->{logger};
    my $config = $self->{config};

    my @dirToScan;
    my @installed_mods;
    my @installed_files;

    # This is a workaround for PAR::Packer. Since it resets @INC
    # I can't find the backend modules to load dynamically. So
    # I prepare a list and include it.
    eval "use Ocsinventory::Agent::Backend::ModuleToLoad;";
    if (!$@) {
        $logger->debug("use Ocsinventory::Agent::Backend::ModuleToLoad to get the modules ".
        "to load. This should not append unless you use the standalone agent built with ".
        "PAR::Packer (pp)");
        push @installed_mods, @Ocsinventory::Agent::Backend::ModuleToLoad::list;
    }

    if ($config->{devlib}) {
        # devlib enable, I only search for backend module in ./lib
        push (@dirToScan, './lib');
    } else {
        #  my ($inst) = ExtUtils::Installed->new();
        #  eval {@installed_files =
        #  $inst->files('Ocsinventory')};

        # ExtUtils::Installed is nice but it needs properly installed package with
        # .packlist
        # This is a workaround for 'invalide' installations...
        foreach (@INC) {
            next if ! -d || (-l && -d readlink) || /^(\.|lib)$/;
            push @dirToScan, $_;
        }
    }
    if (@dirToScan) {
        eval {require File::Find};
        if ($@) {
            $logger->debug("Failed to load File::Find");
        } else {
            # here I need to use $d to avoid a bug with AIX 5.2's perl 5.8.0. It
            # changes the @INC content if i use $_ directly
            # thanks to @rgs on irc.perl.org
          File::Find::find(
              {
                  wanted => sub {
                      push @installed_files, $File::Find::name if $File::Find::name =~ /Ocsinventory\/Agent\/Backend\/.*\.pm$/;
                  },
                  follow => 1,
                  follow_skip => 2
              }
           , @dirToScan);
        }
    }

    foreach my $file (@installed_files) {
        my $t = $file;
        next unless $t =~ s!.*?(Ocsinventory/Agent/Backend/)(.*?)\.pm$!$1$2!;
        my $m = join ('::', split /\//, $t);
        push @installed_mods, $m;
    }

    if (!@installed_mods) {
        $logger->info("ZERO backend module found! Is Ocsinventory-Agent ".
        "correctly installed? Use the --devlib flag if you want to run the agent ".
        "directly from the source directory.")
    }

    foreach my $m (@installed_mods) {
        my @runAfter;
        my @runMeIfTheseChecksFailed;
        my $enable = 1;

        if (exists ($self->{modules}->{$m}->{name})) {
            $logger->debug($m." already loaded.");
            next;
        }

        eval "use $m;";
        if ($@) {
            $logger->debug ("Failed to load $m: $@");
            $enable = 0;
        }

        my $package = $m."::";
        # Load in the module the backendSharedFuncs
        # foreach my $func (keys %{$self->{backendSharedFuncs}}) {
        #     $package->{$func} = $self->{backendSharedFuncs}->{$func};
        # }

        $self->{modules}->{$m}->{name} = $m;
        $self->{modules}->{$m}->{done} = 0;
        $self->{modules}->{$m}->{inUse} = 0;
        $self->{modules}->{$m}->{enable} = $enable;
        $self->{modules}->{$m}->{checkFunc} = $package->{"check"};
        $self->{modules}->{$m}->{runAfter} = $package->{'runAfter'};
        $self->{modules}->{$m}->{runMeIfTheseChecksFailed} = $package->{'runMeIfTheseChecksFailed'};
      # $self->{modules}->{$m}->{replace} = \@replace;
        $self->{modules}->{$m}->{runFunc} = $package->{'run'};
        $self->{modules}->{$m}->{mem} = {};
        # Load the Storable object is existing or return undef
        $self->{modules}->{$m}->{storage} = $self->retrieveStorage($m);

    }

    # the sort is just for the presentation
    foreach my $m (sort keys %{$self->{modules}}) {
        next unless $self->{modules}->{$m}->{checkFunc};
        # find modules to disable and their submodules
        if($self->{modules}->{$m}->{enable} &&
            !$self->runWithTimeout(
            $m,
            $self->{modules}->{$m}->{checkFunc},
            {
                accountconfig => $self->{accountconfig},
                accountinfo => $self->{accountinfo},
                config => $self->{config},
                inventory => $self->{inventory},
                logger => $self->{logger},
                params => $self->{params}, # Compatibiliy with agent 0.0.10 <=
                prologresp => $self->{prologresp},
                mem => $self->{modules}->{$m}->{mem},
                storage => $self->{modules}->{$m}->{storage},
                common => $self->{common},
            })) {
                    $logger->debug ($m." ignored");
                    foreach (keys %{$self->{modules}}) {
                        $self->{modules}->{$_}->{enable} = 0 if /^$m($|::)/;
                    }
            }

            # add submodule in the runAfter array
            my $t;
            foreach (split /::/,$m) {
                $t .= "::" if $t;
                $t .= $_;
                if (exists $self->{modules}->{$t} && $m ne $t) {
                    push @{$self->{modules}->{$m}->{runAfter}}, \%{$self->{modules}->{$t}}
                }
            }
    }

    # Remove the runMeIfTheseChecksFailed if needed
    foreach my $m (sort keys %{$self->{modules}}) {
        next unless    $self->{modules}->{$m}->{enable};
        next unless    $self->{modules}->{$m}->{runMeIfTheseChecksFailed};
        foreach my $condmod (@{${$self->{modules}->{$m}->{runMeIfTheseChecksFailed}}}) {
            if ($self->{modules}->{$condmod}->{enable}) {
                foreach (keys %{$self->{modules}}) {
                    next unless /^$m($|::)/ && $self->{modules}->{$_}->{enable};
                    $self->{modules}->{$_}->{enable} = 0;
                    $logger->debug ("$_ disabled because of a 'runMeIfTheseChecksFailed' in '$m'\n");
                }
            }
        }
    }
}

sub runMod {
    my ($self, $params) = @_;

    my $logger = $self->{logger};

    my $m = $params->{modname};
    my $common = $params->{common};

    return if (!$self->{modules}->{$m}->{enable});
    return if ($self->{modules}->{$m}->{done});

    $self->{modules}->{$m}->{inUse} = 1; # lock the module
    # first I run its "runAfter"

    foreach (@{$self->{modules}->{$m}->{runAfter}}) {
        if (!$_->{name}) {
            # The name is defined during module initialisation so if I
            # can't read it, I can suppose it had not been initialised.
            $logger->fault ("Module `$m' need to be runAfter a module not found.".
            "Please fix its runAfter entry or add the module.");
        }

        if ($_->{inUse}) {
        # In use 'lock' is taken during the mod execution. If a module
        # need a module also in use, we have provable an issue :).
            $logger->fault ("Circular dependency hell with $m and $_->{name}");
        }
        $self->runMod({
            common => $common,
            modname => $_->{name},
        });
    }

    $logger->debug ("Running $m");

    if ($self->{modules}->{$m}->{runFunc}) {
        $self->runWithTimeout(
             $m,
             $self->{modules}->{$m}->{runFunc},
             {
                 accountconfig => $self->{accountconfig},
                 accountinfo => $self->{accountinfo},
                 config => $self->{config},
                 common => $common,
                 logger => $logger,
                 params => $self->{params}, # For compat with agent 0.0.10 <=
                 prologresp => $self->{prologresp},
                 mem => $self->{modules}->{$m}->{mem},
                 storage => $self->{modules}->{$m}->{storage},
                 common => $self->{common},
             }
         );
    } else {
        $logger->debug("$m has no run() function -> ignored");
    }
    $self->{modules}->{$m}->{done} = 1;
    $self->{modules}->{$m}->{inUse} = 0; # unlock the module
    $self->saveStorage($m, $self->{modules}->{$m}->{storage});
}

sub feedInventory {
    my ($self, $params) = @_;

    my $common = $self->{common};


    my $inventory;
    if ($params->{inventory}) {
        $inventory = $params->{inventory};
    }

    if (!keys %{$self->{modules}}) {
        $self->initModList();
    }

    my $begin = time();
    foreach my $m (sort keys %{$self->{modules}}) {
        die ">$m Houston!!!" unless $m;
        $self->runMod ({
        common => $common,
        modname => $m,
        });
    }

    # Execution time
    # $common->setHardware({ETIME => time() - $begin});

    $inventory->{xmlroot}->{CONTENT} = $common->{xmltags};

}

sub retrieveStorage {
    my ($self, $m) = @_;

    my $logger = $self->{logger};

    my $storagefile = $self->{config}->{vardir}."/$m.storage";

    if (!exists &retrieve) {
        eval "use Storable;";
        if ($@) {
            $logger->debug("Storable.pm is not available, can't load Backend module data");
            return;
        }
    }

    return (-f $storagefile)?retrieve($storagefile):{};

}

sub saveStorage {
    my ($self, $m, $data) = @_;

    my $logger = $self->{logger};

    # Perl 5.6 doesn't provide Storable.pm
    if (!exists &store) {
        eval "use Storable;";
        if ($@) {
            $logger->debug("Storable.pm is not available, can't save Backend module data");
            return;
        }
    }

    my $storagefile = $self->{config}->{vardir}."/$m.storage";
    if ($data && keys (%$data)>0) {
        store ($data, $storagefile) or die;
    } elsif (-f $storagefile) {
        unlink $storagefile;
    }

}

sub runWithTimeout {
    my ($self, $m, $func, $params) = @_;

    my $logger = $self->{logger};

    my $ret;
       
    eval {
        local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n require
        my $timeout = $params->{config}{backendCollectTimeout};
        alarm $timeout;
        $ret = &{$func}($params);
    };
    alarm 0;

    if ($@) {
        if ($@ ne "alarm\n") {
            $logger->debug("runWithTimeout(): unexpected error: $@");
        } else {
            $logger->debug("$m killed by a timeout.");
            return;
        }
    } else {
        return $ret;
    }
}

1;
