#!/usr/bin/perl
use strict;
use warnings;
my($app_svnid) = '$HeadURL$ $LastChangedRevision$'; ## no critic (RequireInterpolationOfMetachars)
use lib substr(`ade-config ade_share_prefix`,0,-1) . '/include';  ## no critic (ProhibitBacktickOperators)
use ADE;
use lib substr(`fad-config fad_share_prefix`,0,-1) . '/include';  ## no critic (ProhibitBacktickOperators)
use FAD;
use Getopt::Long qw(:config no_ignore_case);
use Sys::Hostname;
use Data::Dumper;
use Fatal qw( close unlink umask );  #  obviate checking close()'s return code
use experimental 'smartmatch';

my($fadscan_state_prefix, $fadscan_log_prefix);
eval `fad-config --format=perl fadscan_state_prefix fadscan_log_prefix`; ## no critic (ProhibitStringyEval, ProhibitBacktickOperators, RequireCheckingReturnValueOfEval)

my(%fadscan_defined_errors) = (
    fadscan_err_misc     => { fmt => '%s' },
    fadscan_err_access   => { fmt => '%s: can\'t %s' },
);

#  Options
my($opt_mode, $opt_statedir, $opt_logdir, $opt_nocrcs);

my($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)  = ( qw(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, $lc_dayname);
    my($oldfad_stem, $log_file, $in_file);
    my(%headers);
    my($nowsec, $nowmin, $nowhour, $nowmday, $nowmon, $nowyear, $nowyday, $nowisdst, $nowwday);
    my($in_handle);

    #  Set ADE options
    ADE::ade_err_registerdefderrs(\%fadscan_defined_errors);

    #  Defaults for options
    $opt_mode     = undef;
    $opt_logdir   = $fadscan_log_prefix;
    $opt_statedir = $fadscan_state_prefix;
    $opt_nocrcs   = 0;

    #  Register options
    if (($rc=ADE::ade_opt_register($errstack_ref, 'icrs', 'mode-init,mode-check,mode-refresh,mode-schedule,log-dir:s,state-dir:s,no-crcs', 'main::fadscan_opt_handler_%s')) != $ADE::ADE_ERR_OK) {
        return($rc);
    }

    #  Register handler functions
    if (($rc=ADE::ade_msg_register($errstack_ref, \&fadscan_usage, \&fadscan_version, \&fadscan_listpaths)) != $ADE::ADE_ERR_OK) {
        return($rc);
    }

    #  Process options
    ADE::ade_err_debug($errstack_ref, 10, 'fadscan: processing options ...');
    if (($rc=ADE::ade_opt_process($errstack_ref)) != $ADE::ADE_ERR_OK) {
        return($rc);
    }

    #  Process arguments
    if  (defined $opt_mode and $opt_mode eq 'schedule') {
        ADE::ade_msg_usage($errstack_ref) if (not defined $ARGV[0] or defined$ARGV[1]);
        $fileset = $ARGV[0];
    } else {
        ADE::ade_msg_usage($errstack_ref) if (not defined $ARGV[1] or defined $ARGV[2]);
        $fileset = $ARGV[0];
        $in_file = $ARGV[1];
    }

    #  Sanity checks and derivations
    if (! -d $opt_logdir) {
        ADE::ade_err_error($errstack_ref, 'fadscan_err_access', $opt_logdir, 'stat');
        return($ADE::ADE_ERR_FAIL);
    }
    if (! -d $opt_statedir) {
        ADE::ade_err_error($errstack_ref, 'fadscan_err_access', $opt_statedir, 'stat');
        return($ADE::ADE_ERR_FAIL);
    }

    #  Set some global variables that we'll want to access everywhere.
    $hostname = Sys::Hostname::hostname();
    $hostname or ADE::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::ade_err_debug($errstack_ref, 10, sprintf 'fadscan: log_file=%s, in_file=%s, schedflag_file=%s, opt_mode=%s', $log_file, defined $in_file ? $in_file : 'undef', $schedflag_file, $opt_mode);

    #  Guts
    #  Don't allow scheduling of non-existent filesets. A non-existing fileset
    #  is one which has not yet a base snapshot file. (This is FAD#148.)
    if ($opt_mode eq 'schedule' and not -f "$oldfad_stem.gz") {
        ADE::ade_err_error($errstack_ref, 'fadscan_err_misc', "$fileset: no such fileset");
        return($ADE::ADE_ERR_FAIL);
    }

    #  And here we make do all the work.
    ADE::ade_tmp_registerfile($errstack_ref, $log_file);
    if (($rc=ADE::ade_spc_rollingstatusmanager($errstack_ref,
                                               $opt_mode,
                                               $schedflag_file,
                                               \&fadscan_scan_cb,
                                               \&fadscan_compare_cb,
                                               $oldfad_stem,
                                               '.gz',
                                               $log_file,
                                               { no_crcs => $opt_nocrcs, in_file => $in_file },
	                                       $ADE::ADE_TMP_DIR)) != $ADE::ADE_ERR_OK) {
        return($rc);
    }
    ADE::ade_err_debug($errstack_ref, 4, 'returned from ADE::ade_spc_rollingstatusmanager()');
    #  No need to delete it we get this far.
    ADE::ade_tmp_deregisterfile($errstack_ref, $log_file);

    #  Ensure sensible return code
    return($ADE::ADE_ERR_OK);
}

sub fadscan_opt_handler_i ## no critic (RequireArgUnpacking)
{
    return(fadscan_opt_handler_mode_init(@_));
}

sub fadscan_opt_handler_c ## no critic (RequireArgUnpacking)
{
    return(fadscan_opt_handler_mode_check(@_));
}

sub fadscan_opt_handler_r ## no critic (RequireArgUnpacking)
{
    return(fadscan_opt_handler_mode_refresh(@_));
}

sub fadscan_opt_handler_s ## no critic (RequireArgUnpacking)
{
    return(fadscan_opt_handler_mode_schedule(@_));
}

sub fadscan_opt_handler_mode_init
{
    my($errstack_ref) = @_;

    $opt_mode = 'init';

    return($ADE::ADE_ERR_OK);
}


sub fadscan_opt_handler_mode_check
{
    my($errstack_ref) = @_;

    $opt_mode = 'check';

    return($ADE::ADE_ERR_OK);
}

sub fadscan_opt_handler_mode_refresh
{
    my($errstack_ref) = @_;

    $opt_mode = 'refresh';

    return($ADE::ADE_ERR_OK);
}

sub fadscan_opt_handler_mode_schedule
{
    my($errstack_ref) = @_;

    $opt_mode = 'schedule';

    return($ADE::ADE_ERR_OK);
}

sub fadscan_opt_handler_log_dir
{
    my($errstack_ref, $logdir) = @_;

    $opt_logdir = $logdir;

    return($ADE::ADE_ERR_OK);
}

sub fadscan_opt_handler_state_dir
{
    my($errstack_ref, $statedir) = @_;

    $opt_statedir = $statedir;

    return($ADE::ADE_ERR_OK);
}

sub fadscan_opt_handler_no_crcs
{
    my($errstack_ref) = @_;

    $opt_nocrcs = 1;

    return($ADE::ADE_ERR_OK);
}

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

    return(ADE::ade_smf_extractversionfromsvnstring($errstack_ref, $app_svnid, $version_text_ref));
}

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

    ${$pathlist_text_ref} = "Log-Directory: $opt_logdir\n" .
                            "State-Directory: $opt_statedir";

    return($ADE::ADE_ERR_OK);
}

sub fadscan_usage
{
    my($errstack_ref, $usage_text_ref, $passno) = @_;

    if ($passno == 1) {
        ${$usage_text_ref} = '{ -[icr] <scanid> <listfile> | -s <scanid> }';
    } elsif ($passno == 2) {
        ${$usage_text_ref} = "         -i        | --mode-init             operate in 'initialise' mode\n" .
                             "         -c        | --mode-check            operate in 'check' mode\n" .
                             "         -r        | --mode-refresh          operate in 'refresh' mode\n" .
                             "         -s        | --mode-schedule         operate in 'schedule' mode\n" .
                             "                     --log-dir=<logdir>      use alternate log directory\n" .
                             "                     --state-dir=<statedir>  use alternate state info directory\n" .
                             "                     --no-crcs               do not calculate CRCs\n";
    }

    return($ADE::ADE_ERR_OK);
}

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

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

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

    #  Open the two input files
    ADE::ade_err_debug($errstack_ref, 10, 'fadscan_compare_cb: opening the two fad files for comparison ...');
    if (($rc=ADE::ade_fcm_openreadcompressed($errstack_ref, $oldfad_file, \$oldfad_handle)) != $ADE::ADE_ERR_OK) {
        ADE::ade_err_error($errstack_ref, 'fadscan_err_access', $oldfad_file, 'open');
        return($rc);
    }
    if (($rc=ADE::ade_fcm_openreadcompressed($errstack_ref, $newfad_file, \$newfad_handle)) != $ADE::ADE_ERR_OK) {
        ADE::ade_err_error($errstack_ref, 'fadscan_err_access', $newfad_file, 'open');
        return($rc);
    }

    #  Load fad files.
    ADE::ade_err_debug($errstack_ref, 5, 'fadscan_compare_cb: loading first FAD file ...');
    return($rc) if (($rc=FAD::fad_load($errstack_ref, $oldfad_handle, \%old_store, \%old_fad_headers, undef)) != $ADE::ADE_ERR_OK);
    ADE::ade_err_debug($errstack_ref, 5, 'fadscan_compare_cb: loading second FAD file ...');
    return($rc) if (($rc=FAD::fad_load($errstack_ref, $newfad_handle, \%new_store, \%new_fad_headers, undef)) != $ADE::ADE_ERR_OK);


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

    #  Write report header.
    ADE::ade_err_debug($errstack_ref, 10, 'fadscan_compare_cb: writing a report header ...');
    print $comparison_report_handle "Host:                                      $hostname\n";
    print $comparison_report_handle "Fileset:                                   $fileset\n";
    print $comparison_report_handle "Start-Time:                                $fmtdatestart\n";
    print $comparison_report_handle 'Last-Refresh-Time:                         ' .
            localtime($old_fad_headers{'Unix-Time'}) . "\n";
    if (($rc=fadscan_version($errstack_ref, \$version)) != $ADE::ADE_ERR_OK) {
        return($rc);
    }
    print $comparison_report_handle "Version:                                   $version\n";
    print $comparison_report_handle "\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::ade_err_debug($errstack_ref, 10, 'fadscan_compare_cb: calling fadstreams_to_diffstream() to diff the opened fad files ...');
    if (($rc=FAD::fad_diff($errstack_ref, \%old_store, \%new_store, \&fadscan_processdiff_cb, { out_handle => $comparison_report_handle, 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::ADE_ERR_OK) {
        close $newfad_handle;
        close $oldfad_handle;
        ADE::ade_err_error($errstack_ref, 'fadscan_err_misc', 'failed to work out differences');
    }

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

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

    return($ADE::ADE_ERR_OK);
}

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

    #  Unpack the two parameters from the one the callback interface allowed to pass.
    $nocrcs  = ${$param}{'no_crcs'};
    $in_file = ${$param}{'in_file'};

    ADE::ade_err_debug($errstack_ref, 10, "fadscan: opening $in_file ...");
    if ($in_file eq '-') {
        $in_handle = \*STDIN;
    } elsif (!open $in_handle, '<', $in_file) {
        ADE::ade_err_error($errstack_ref, 'fadscan_err_access', $in_file, 'open');
        return($rc);
    }

    ADE::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::fad_collect_info($errstack_ref, $_, $nocrcs, \$collected_info_ref)) != $ADE::ADE_ERR_OK and (ADE::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::ade_err_resetstack($errstack_ref, stack=>$errstack_ref);
            ADE::ade_err_warning($errstack_ref, 'ade_err_access', $_, 'access (did it disappear?)');
            next;
        } elsif ($rc != $ADE::ADE_ERR_OK) {
            close $in_handle if ($in_file ne '-');
            return($rc);
        }
        if (($rc=FAD::fad_insert_info($errstack_ref, \%store1, $collected_info_ref)) != $ADE::ADE_ERR_OK) {
            close $in_handle if ($in_file ne '-');
            return($rc);
        }
    }

    close $in_handle if ($in_file ne '-');

    #  Dump store to already-opened file.
    ADE::ade_err_debug($errstack_ref, 10, 'fadscan_scan_cb: dumping store to stdout (which is redirected) ...');
    if (($rc=FAD::fad_dump($errstack_ref, $out_handle, \%store1)) != $ADE::ADE_ERR_OK) {
        return($rc);
    }

    return($ADE::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 ...
    foreach my $i (0..$#{@{$change_ref}{'changes'}}) {

        #  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'};
        if ($changetype ~~ [ $FAD::CT_ADDED, $FAD::CT_DELETED ]) {
            ADE::ade_err_debug($errstack_ref, 4, "fadscan_processdiff_cb:: changetype=$changetype, file=$file");
        } else {
            ADE::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 $FAD::CT_OWNER) {
            ${$own_cnt_ref}++;
            $txt_own = 'own';
        } elsif ($changetype eq $FAD::CT_GROUP) {
            ${$grp_cnt_ref}++;
            $txt_grp = 'grp';
        } elsif ($changetype eq $FAD::CT_MODE) {
            ${$mde_cnt_ref}++;
            $txt_mde = 'mde';
        } elsif ($changetype eq $FAD::CT_LINKS) {
            ${$lnk_cnt_ref}++;
            $txt_lnk = 'lnk';
        } elsif ($changetype eq $FAD::CT_DELETED) {
            ${$del_cnt_ref}++;
            $txt_del = 'del';
        } elsif ($changetype eq $FAD::CT_TYPE) {
            ${$typ_cnt_ref}++;
            $txt_typ = 'typ';
        } elsif ($changetype eq $FAD::CT_ADDED) {
            ${$add_cnt_ref}++;
            $txt_add = 'add';
        } elsif ($changetype eq $FAD::CT_CONT_CRC) {
            ${$crc_cnt_ref}++;
            $txt_crc = 'crc';
        } elsif ($changetype eq $FAD::CT_CONT_SYMLINK) {
            ${$sym_cnt_ref}++;
            $txt_sym = 'sym';
        } elsif ($changetype eq $FAD::CT_CONT_MAJMIN) {
            ${$mmn_cnt_ref}++;
            $txt_mmn = 'mmn';
        } else {
            ADE::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::ADE_ERR_OK);
}

ADE::ade_gep_main(\&fadscan);
