#!/usr/bin/perl
use strict;
my($app_svnid) = '$HeadURL$ $LastChangedRevision$';
#  Allow bare words, so &ade_err_error() calls look nicer.
no strict 'subs';
use lib substr `ade-config ade_include_prefix`,0,-1;
use ADE;
use lib substr `fad-config fad_include_prefix`,0,-1;
use FAD;
use Getopt::Long qw(:config no_ignore_case);
use Sys::Hostname;
use Data::Dumper;

#  Without predeclaring variables returned by fad-config, an error will be generated 
#  (probably because of 'use strict' above).  If ade-app-config is modified to generate a 
#  'my' itself then somehow the 'my' narrows the scope of the assignment to within the 
#  backquotes. Try running:
#
#      perl -e 'eval "my(\$x)=4"; print "x=$x\n";'
# 
#  with and without the 'my' to see what I mean. This means that we should not willy
#  nilly just run: 
#
#      eval `fad-config --format=perl`
#
#  Because if a new variable is added to the output then it will break this script. So
#  this script must (1) do 'my' for anything it wants, (2) specify explicitly anything
#  it wants from fad-config as a way of preventing fad-config from showing assignments
#  for more variables than we have 'my'ed.

my($fad_log_prefix, $fad_state_prefix, $fadscan_state_prefix, $fadscan_log_prefix);
eval `fad-config --format=perl fad_log_prefix fad_state_prefix fadscan_state_prefix fadscan_log_prefix`;

&ade_err_registerdefderrs({
    fadscan_err_misc     => { fmt => "%s" },
    fadscan_err_access   => { fmt => "%s: can't %s" },
});

my($opt_mode, $opt_statedir, $opt_logdir, $opt_mailto, $opt_nocrcs);
my(@fadscan_config_hasharray) = (
    { dsc => "opt_mode",     var => \$opt_mode,     dfl => "undefined" },
    { dsc => "opt_logdir",   var => \$opt_logdir,   dfl => $fadscan_log_prefix },
    { dsc => "opt_statedir", var => \$opt_statedir, dfl => $fadscan_state_prefix },
    { dsc => "opt_mailto",   var => \$opt_mailto,   dfl => "undefined" },
    { dsc => "opt_nocrcs",   var => \$opt_nocrcs,   dfl => 0,          },
);

my(@find_roots, $fileset);
my($all_cnt, $del_cnt, $own_cnt, $grp_cnt, $mde_cnt, $typ_cnt, $lnk_cnt);
my($crc_cnt, $mmn_cnt, $add_cnt, $sym_cnt, $schedflag_file, $hostname); 
#  The names of the days and monthes, used in date conversion
my(@day_names)  = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");

#  Log and state files should not be readable by common users
umask 077;

sub fadscan
{
    my($errstack_ref) = @_;
    my($rc, $confirm, $cmd, $pars, $lc_dayname);
    my($oldfad_stem, $log_file, $in_file);
    my(@find_filters, %headers, $combined_scan_args);
    my($nowsec, $nowmin, $nowhour, $nowmday, $nowmon, $nowyear, $nowwday, $nowyday, $nowisdst, $nowwday, $nowwday);
    my($in_handle, $procopts_hashref);

    ##########################################################################
    #
    #  Process options
    #
    ##########################################################################

    $procopts_hashref = { 
        "i|mode-init"     => sub { $opt_mode = "init"; }, 
        "c|mode-check"    => sub { $opt_mode = "check"; }, 
        "r|mode-refresh"  => sub { $opt_mode = "refresh"; }, 
        "s|mode-schedule" => sub { $opt_mode = "schedule"; }, 
        "log-dir=s"       => \$opt_logdir, 
        "state-dir=s"     => \$opt_statedir, 
        "m|mail=s"        => \$opt_mailto, 
        "no-crcs"         => sub { $opt_nocrcs = 1; }
    };

    if (($rc=&ade_spc_procopts($errstack_ref, \&fadscan_listpaths, \&fadscan_usage, \&fadscan_version, \@fadscan_config_hasharray, $procopts_hashref)) != $ade_err_ok) {
        return($rc); 
    } 

    ##########################################################################
    #
    #  Process arguments
    #
    ##########################################################################

    if  ($opt_mode eq "schedule") {
        (!defined($ARGV[0]) || defined($ARGV[1])) && &ade_msg_usage($errstack_ref, \&fadscan_usage, 1);
        $fileset = $ARGV[0];
    } else {
        (!defined($ARGV[1]) || defined($ARGV[2])) && &ade_msg_usage($errstack_ref, \&fadscan_usage, 1);
        $fileset = $ARGV[0];
        $in_file = $ARGV[1];
    }

    ##########################################################################
    #
    #  Sanity checks and derivations
    #
    ##########################################################################

    if (! -d $opt_logdir) {
        &ade_err_error($errstack_ref, fadscan_err_access, $opt_logdir, "stat");
        return($ade_err_fail);
    }
    if (! -d $opt_statedir) {
        &ade_err_error($errstack_ref, fadscan_err_access, $opt_statedir, "stat");
        return($ade_err_fail);
    }

    #  Set some global variables that we'll want to access everywhere.
    $hostname = &Sys::Hostname::hostname;
    $hostname || &ade_err_error($errstack_ref, fadscan_err_misc, "can't determine hostname");
    ($nowsec, $nowmin, $nowhour, $nowmday, $nowmon, $nowyear, $nowwday, $nowyday, $nowisdst) = localtime;
    $oldfad_stem = "$opt_statedir/$fileset-basesnap";
    ($lc_dayname = $day_names[$nowwday]) =~ tr/A-Z/a-z/;
    $log_file = "$opt_logdir/$fileset-$lc_dayname.gz";
    $schedflag_file = "$opt_statedir/$fileset-scheduled";
    &ade_err_debug($errstack_ref, 10, "fadscan: log_file=$log_file, in_file=$in_file, schedflag_file=$schedflag_file, opt_mode=$opt_mode");
    

    ##########################################################################
    #
    #  Guts
    #
    ##########################################################################

    #  And here we make do all the work.
    &ade_tmp_registerfile($errstack_ref, $log_file);
    if (($rc=&ade_spc_rollingstatusmanager($errstack_ref,
                                           $opt_mode, 
                                           $schedflag_file, 
                                           \&fadscan_scan_cb, 
                                           \&fadscan_compare_cb, 
					   undef,
                                           $oldfad_stem, 
                                           ".gz",
                                           $log_file, 
                                           { opt_nocrcs => $opt_nocrcs, in_file => $in_file },
	                                   $ade_tmp_dir,
					   1)) != $ade_err_ok) {
        return($rc);
    }
    #  No need to delete it we get this far.
    &ade_tmp_deregisterfile($errstack_ref, $log_file);

    #  Mail the log file if necessary.
    if (($opt_mode eq "check" || $opt_mode eq "refresh") && $opt_mailto ne "undefined") {
        &ade_err_debug($errstack_ref, 4, "main: opening compressed log file ...");
        if (($rc=&ade_fcm_openreadcompressed($errstack_ref, $log_file, \*LOG_HANDLE)) != $ade_err_ok) {
            &ade_err_error($errstack_ref, fadscan_err_access, $log_file, "open");
            return($rc);
        }
        %headers = ();
        &ade_err_debug($errstack_ref, 4, "main: emailing log file ...");
        &ade_mua_addmailheader($errstack_ref, \%headers, "Subject: Fadscan ($hostname $fileset $day_names[$nowwday])");
        if (($rc=&ade_mua_sendmail($errstack_ref, \%headers, \*LOG_HANDLE, [ $opt_mailto ])) != $ade_err_ok) {
            return($rc);
        }
        close LOG_HANDLE;
    }

    #  Ensure sensible return code
    return($ade_err_ok);
}

sub fadscan_version
{
    my($errstack_ref, $version_ref) = @_;

    return(&ade_smf_extractversionfromsvnstring($errstack_ref, $app_svnid, $version_ref));
}

sub fadscan_listpaths
{
    my($errstack_ref, $pathlist_ref) = @_;
    my($rc);

    %{$pathlist_ref} = (
        "Log-Directory"   => $opt_logdir,
        "State-Directory" => $opt_statedir,
    );
    return($ade_err_ok);
}

sub fadscan_usage
{   
    my($errstack_ref, $passno) = @_;
    
    if ($passno == 1) {
        print "{ -[icr] <scanid> <listfile> | -s <scanid> }\n";
    } elsif ($passno == 2) {
        print "         -i        | --mode-init             operate in 'initialise' mode\n";
        print "         -c        | --mode-check            operate in 'check' mode\n";
        print "         -r        | --mode-refresh          operate in 'refresh' mode\n";
        print "         -s        | --mode-schedule         operate in 'schedule' mode\n";
        print "                     --log-dir=<logdir>      use alternate log directory\n";
        print "                     --state-dir=<statedir>  use alternate state info directory\n";
        print "         -m        | --mail=<addr>           mail logs to <addr>\n";
        print "                     --no-crcs               do not calculate CRCs\n";
    } else {
        &ade_err_internal($errstack_ref, "fadscan_usage: $passno: bad pass number");
    }
    
    return($ade_err_ok);
}

sub fadscan_compare_cb
{
    my ($errstack_ref, $oldfad_file, $newfad_file, $rollno) = @_;
    my ($rc, $fmtdatestart, $fmtdateend);
    my (%old_fad_headers, %headers, %new_fad_headers);
    my ($diff_rc, $version);
    my (%old_store, %new_store);

    &ade_err_debug($errstack_ref, 10, "fadscan_compare_cb: oldfad_file=$oldfad_file, newfad_file=$newfad_file, rollno=$rollno");

    #  Get the time of the report.
    $fmtdatestart = localtime;

    #  Open the two input files 
    &ade_err_debug($errstack_ref, 10, "fadscan_compare_cb: opening the two fad files for comparison ...");
    if (($rc=&ade_fcm_openreadcompressed($errstack_ref, $oldfad_file, \*OLDFAD_HANDLE)) != $ade_err_ok) {
        &ade_err_error($errstack_ref, fadscan_err_access, $oldfad_file, "open");
        return($rc);
    }
    if (($rc=&ade_fcm_openreadcompressed($errstack_ref, $newfad_file, \*NEWFAD_HANDLE)) != $ade_err_ok) {
        &ade_err_error($errstack_ref, fadscan_err_access, $newfad_file, "open");
        return($rc);
    }
    
    #  Load fad files.
    &ade_err_debug($errstack_ref, 5, "fadscan_compare_cb: loading first FAD file ...");
    return($rc) if (($rc=&fad_load($errstack_ref, \*OLDFAD_HANDLE, \%old_store, \%old_fad_headers, undef)) != $ade_err_ok);
    &ade_err_debug($errstack_ref, 5, "fadscan_compare_cb: loading second FAD file ...");
    return($rc) if (($rc=&fad_load($errstack_ref, \*NEWFAD_HANDLE, \%new_store, \%new_fad_headers, undef)) != $ade_err_ok);

    
    &ade_err_debug($errstack_ref, 10, "fadscan_compare_cb: old_store: " . Dumper(\%old_store));
    &ade_err_debug($errstack_ref, 10, "fadscan_compare_cb: new_store: " . Dumper(\%new_store));

    #  Write report header.
    &ade_err_debug($errstack_ref, 10, "fadscan_compare_cb: writing a report header ...");
    print "Host:                                      $hostname\n";
    print "Fileset:                                   $fileset\n";
    print "Start-Time:                                $fmtdatestart\n";
    print "Last-Refresh-Time:                         " . 
            localtime($old_fad_headers{'Unix-Time'}) . "\n";
    if (($rc=&fadscan_version($errstack_ref, \$version)) != $ade_err_ok) {
        return($rc);
    }
    print "Version:                                   $version\n";
    print "\n";

    #  Initialise some counters that are used to generate a report summary
    $all_cnt = 0;
    $del_cnt = 0; 
    $own_cnt = 0; 
    $grp_cnt = 0; 
    $mde_cnt = 0;  
    $typ_cnt = 0;  
    $lnk_cnt = 0;
    $crc_cnt = 0; 
    $mmn_cnt = 0; 
    $add_cnt = 0;   
    $sym_cnt = 0;

    #  Actually work out the differences processing them via a callback.
    &ade_err_debug($errstack_ref, 10, "fadscan_compare_cb: calling fadstreams_to_diffstream() to diff the opened fad files ...");
    if (($rc=&fad_diff($errstack_ref, \%old_store, \%new_store, \&fadscan_processdiff_cb, { out_handle => \*STDOUT, del_cnt_ref => \$del_cnt, own_cnt_ref => \$own_cnt, grp_cnt_ref => \$grp_cnt, mde_cnt_ref => \$mde_cnt, typ_cnt_ref => \$typ_cnt, lnk_cnt_ref => \$lnk_cnt, crc_cnt_ref => \$crc_cnt, mmn_cnt_ref => \$mmn_cnt, add_cnt_ref => \$add_cnt, sym_cnt_ref => \$sym_cnt, all_cnt_ref => \$all_cnt })) != $ade_err_ok) {
        close NEWFAD_HANDLE;
        close OLDFAD_HANDLE;
        &ade_err_error($errstack_ref, fadscan_err_misc, "failed to work out differences");
    }

    #  We've finished reading the fad files now.
    &ade_err_debug($errstack_ref, 10, "fadscan_compare_cb: comparison dome, closing fad files ...");
    close NEWFAD_HANDLE;
    close OLDFAD_HANDLE;
    print "\n"; 

    #  Write report footer.
    &ade_err_debug($errstack_ref, 10, "fadscan_compare_cb: writing a report footer ...");
    print "Total-Files-Changed:                       $all_cnt\n";
    print "Added:                                     $add_cnt\n";
    print "Deleted:                                   $del_cnt\n";
    print "Owner-Changed:                             $own_cnt\n";
    print "Group-Changed:                             $grp_cnt\n";
    print "Mode-Changed:                              $mde_cnt\n";
    print "Links-Changed:                             $lnk_cnt\n";
    print "Checksum-Changed:                          $crc_cnt\n"; 
    print "Type-Changed:                              $typ_cnt\n"; 
    print "Symlink-Dest-Changed:                      $sym_cnt\n"; 
    print "Major-Minor-Changed:                       $mmn_cnt\n"; 
    $fmtdateend = localtime;
    print "End-Time:                                  $fmtdateend\n";
    #  We've finished writing to the report

    return($ade_err_ok);
}

sub fadscan_scan_cb
{
    my ($errstack_ref, $param) = @_;
    my ($rc, $findroots_ref, $findfilters_ref);
    my (%store1, $collected_info_ref, $opt_nocrcs, $in_file, $in_handle, $instack_flag);

    #  Unpack the two parameters from the one the callback interface allowed to pass.
    $opt_nocrcs  = ${$param}{opt_nocrcs};
    $in_file = ${$param}{in_file};
    
    &ade_err_debug($errstack_ref, 10, "fadscan: opening $in_file ...");
    if ($in_file eq "-") {
        $in_handle = \*STDIN;
    } elsif (!open(IN_HANDLE, $in_file)) {
        &ade_err_error($errstack_ref, fadscan_err_access, $in_file, "open");
        return($rc);
    } else {
        $in_handle = \*IN_HANDLE;
    }

    &ade_err_debug($errstack_ref, 10, "fadscan_scan_cb: reading file list ...");
    while (<$in_handle>) {
        chomp;
        #  It's possible the file disappears before we get to it; this should not
        #  be an error, but only a warning.
        if (($rc=&fad_collect_info($errstack_ref, $_, $opt_nocrcs, \$collected_info_ref)) != $ade_err_ok && (&ade_err_instack($errstack_ref, fad_err_access, \$instack_flag),defined($instack_flag))) {
            #  Really here we want to say "without adding anything to the stack, display the
            #  stack showing all entries as warnings" but this requires using functions inside
            #  ADE.pm which I have made private. Hmm ... Should I make them public again? Maybe
            #  I have to but we'll work around the limitation for the moment. I do this by
            #  resetting the stack and then raising a *new* warning.
            &ade_err_resetstack($errstack_ref, stack=>$errstack_ref);
            &ade_err_warning($errstack_ref, ade_err_access, $_, "access (did it disappear?)");
            next;
        } elsif ($rc != $ade_err_ok) {
            close IN_HANDLE if ($in_file ne "-");
            return($rc);
        }
        if (($rc=&fad_insert_info($errstack_ref, \%store1, $collected_info_ref)) != $ade_err_ok) {
            close IN_HANDLE if ($in_file ne "-");
            return($rc);
        }
    }

    close IN_HANDLE if ($in_file ne "-");


    #  Dump store to stdout, which is redirected by ade_spc_rollingstatusmanager().
    &ade_err_debug($errstack_ref, 10, "fadscan_scan_cb: dumping store to stdout (which is redirected) ...");
    if (($rc=&fad_dump($errstack_ref, \*STDOUT, \%store1)) != $ade_err_ok) {
        return($rc);
    }

    return($ade_err_ok);
}

sub fadscan_processdiff_cb
{
    my($errstack_ref, $param, $change_ref) = @_;
    my($oldstate, $newstate, $changetype, $file);
    my($i);
    my($out_handle, $del_cnt_ref, $own_cnt_ref, $grp_cnt_ref, $mde_cnt_ref, $typ_cnt_ref, $lnk_cnt_ref, $crc_cnt_ref, $mmn_cnt_ref, $add_cnt_ref, $sym_cnt_ref, $all_cnt_ref);
    my($txt_typ, $txt_own, $txt_grp, $txt_mde, $txt_add, $txt_del, $txt_mmn, $txt_crc, $txt_lnk, $txt_sym);

    #  Unpack the many parameters from the one the callback interface allowed to pass.
    $out_handle  = ${$param}{out_handle};
    $del_cnt_ref = ${$param}{del_cnt_ref};
    $own_cnt_ref = ${$param}{own_cnt_ref};
    $grp_cnt_ref = ${$param}{grp_cnt_ref};
    $mde_cnt_ref = ${$param}{mde_cnt_ref};
    $typ_cnt_ref = ${$param}{typ_cnt_ref};
    $lnk_cnt_ref = ${$param}{lnk_cnt_ref};
    $crc_cnt_ref = ${$param}{crc_cnt_ref};
    $mmn_cnt_ref = ${$param}{mmn_cnt_ref};
    $add_cnt_ref = ${$param}{add_cnt_ref};
    $sym_cnt_ref = ${$param}{sym_cnt_ref};
    $all_cnt_ref = ${$param}{all_cnt_ref};

    #  Each output line will consist of all of these fields but with possibly
    #  a couple of them not blank.
    $txt_typ = "   ";
    $txt_own = "   ";
    $txt_grp = "   ";
    $txt_mde = "   ";
    $txt_add = "   ";
    $txt_del = "   ";
    $txt_mmn = "   ";
    $txt_crc = "   ";
    $txt_lnk = "   ";
    $txt_sym = "   ";

    #  For each sort of difference detected for the one file for which this callback was called ...
    for ($i=0; $i < $#{@{$change_ref}{changes}}+1; $i++) {
  
        #  Create some aliases
        $changetype = ${$change_ref}{changes}[$i]{type};
        $oldstate   = ${$change_ref}{changes}[$i]{old};
        $newstate   = ${$change_ref}{changes}[$i]{new};
        $file       = ${$change_ref}{file};
        &ade_err_debug($errstack_ref, 4, "fadscan_processdiff_cb:: changetype=$changetype, oldstate=$oldstate, newstate=$newstate, file=$file");

	#  Now turn on the appropriate bits of text
        if ($changetype eq $CT_OWNER) {
            ${$own_cnt_ref}++;
            $txt_own = "own";
        } elsif ($changetype eq $CT_GROUP) {
            ${$grp_cnt_ref}++;
            $txt_grp = "grp";
        } elsif ($changetype eq $CT_MODE) {
            ${$mde_cnt_ref}++;
            $txt_mde = "mde";
        } elsif ($changetype eq $CT_LINKS) {
            ${$lnk_cnt_ref}++;
            $txt_lnk = "lnk";
        } elsif ($changetype eq $CT_DELETED) {
            ${$del_cnt_ref}++;
            $txt_del = "del";
        } elsif ($changetype eq $CT_TYPE) {
            ${$typ_cnt_ref}++;
            $txt_typ = "typ";
        } elsif ($changetype eq $CT_ADDED) {
            ${$add_cnt_ref}++;
            $txt_add = "add";
        } elsif ($changetype eq $CT_CONT_CRC) {
            ${$crc_cnt_ref}++;
            $txt_crc = "crc";
        } elsif ($changetype eq $CT_CONT_SYMLINK) {
            ${$sym_cnt_ref}++;
            $txt_sym = "sym";
        } elsif ($changetype eq $CT_CONT_MAJMIN) {
            ${$mmn_cnt_ref}++;
            $txt_mmn = "mmn";
        } else {
            &ade_err_internal($errstack_ref, "fadscan_processdiff_cb: unexpected CT: $changetype");
        }
    }
    ${$all_cnt_ref}++;

    #  and write the record with the file name tagged on the end.
    print $out_handle "$txt_add $txt_del $txt_typ $txt_own $txt_grp $txt_mde $txt_lnk $txt_crc $txt_sym $txt_mmn    $file\n";

    return($ade_err_ok);
}

&ade_gep_main(\&fadscan);
