#!/s/std/bin/perl

use warnings;
use strict;

use Data::Dumper;
use CSL::DBs;
use CSL::Pg;
use DBI;
use Time::CTime;
use IPC::Open2;
use Getopt::Long;
use Mail::Send;


##############################################################################
#                       CONFIGURATION DEFINITIONS
##############################################################################

# Config Variables
my $QUOTA_PERCENT_THRESHOLD     = 99;     # This is when we start caring :)
my $MODIFY_TIMEOUT              = (30*24*3600); # How long till we really dont care
my $NOTIFY_TIMEOUT              = (30*24*3600); # Time between possible notifies
my $DATA_PATH                   = "/p/lab/afsquota";
my $LOG_FILE                    = "$DATA_PATH/quota_report.log";
my $STATE_FILE                  = "$DATA_PATH/quota_report.state";

# Email addresses (for admin purposes)
my $ADMIN_EMAIL             = 'timc@cs.wisc.edu';

# This command is used to check for 
my $SERVER_INFO_FILE = '/common/afs/partitions/volinfo-{SERVER}-{PARTITION}';

# Some names of programs
my $AFSDF                   = '/s/std/bin/afsdf';
my $VOS                     = '/usr/afsws/etc/vos';
my $GETALIAS                = '/s/std/bin/getalias';
my $FIND_MP                 = '/s/afstools/bin/find_mountpoints';

# This is for the afs DB
my $AFSDB_CONNECT           = $CSL::DBs::csdb;      # DB connection string
my $AFSDB_NS                = "afs";                # pg schema name
my %AFSDB_QUERY =
    ( 
        GetVolPool      => "select pool from $AFSDB_NS.volumes where name=?",
        GetPoolOwner    => "select owner from $AFSDB_NS.pools where name=?",
        GetPartOwner    => "select pool from $AFSDB_NS.partitions where server=? and partition=?",
        GetOwners       => "select p.owner from $AFSDB_NS.pools p, $AFSDB_NS.volumes v where p.name = v.pool and v.name = ?",
    );

# Global vars
my %STATS;              # keep all the gathered statistics in here
my @BadNames;           # List of volumes with 'badly formatted names'

# These are commandline option variables
my $SEND_USER_EMAIL;        # bool: should we send email to each user?
my $SEND_ADMIN_EMAIL;       # bool: should we send summary email to admin?
my $STATS_OUTPUT;           # bool: should we dump stats info to stdout?
my $DEBUG;                  # bool: should we produce debuggin output?
my $SAVE_STATE;             # bool: should we store our state information?
my $LOAD_STATE;             # bool: should we load our state information?
my $LOG_OUTPUT;             # bool: should we log our output?



##############################################################################
#                            AFS SERVER COMMANDS
##############################################################################

# This goes and runs the afsdf program to get the server list
sub getServerList
{
    # Run,parse,return
    return split /\n/, `$AFSDF -L`;
}

# Returns the partitions on a given server
sub getServerPartitions
{
    my ($server) = @_;
    
    #print "$VOS partinfo -server $server\n";
    return map { s/.*\/(\w+).*/$1/; $_ } split /\n/, `$VOS partinfo -server $server`;
}

# This parses the volinfo file in /common and returns a hash of results
sub parseVolinfoFile
{
    my ($server, $partition) = @_;

    my $fname = $SERVER_INFO_FILE;
    $fname =~ s/\{SERVER\}/$server/;
    $fname =~ s/\{PARTITION\}/$partition/;

    # Read the file in as one BIG LINE
    open my $fh, "<$fname" or return;
    my @lines = map { chomp $_; $_; } grep { ! /^Inode / } <$fh>;
    close $fh;

    # split into one string per patition
    my $whole = join " ", @lines;
    $whole =~ s/Volume header for volume/\nVolume header for volume/g;
    my @parts = split /\n/, $whole;
   
    # Parse the data
    my %h = map  {
                    my $name = $_;
                    my $total = $_;
                    my $used = $_;
                    my $last_modify = $_;
                    
                    $name =~ s/^Volume header for volume \d+ \(([\w\-\.]*)\).*$/$1/;
                    $total =~ s/^.*maxquota = (\d+).*$/$1/;
                    $used =~ s/^.*diskused = (\d+).*$/$1/;
                    $last_modify =~ s/^.*updateDate = (\d+).*$/$1/;
                    
                    my $percent = 0;
                    $percent = 100.0*$used/$total if $total && $total > 0;

                   
                    $name => {
                        name => $name,
                        total => $total,
                        used => $used,
                        server => $server,
                        partition => $partition,
                        percent => $percent,
                        modified => $last_modify,
                    }
                } @parts;

    # eliminate trash
    my %t = %h;
    %h = map { $_ => $t{$_} } grep { /\w/ } keys %h;

    # Create some statistics on how many volues are at a certain percent of quota
    my $pquota;
    $pquota = $STATS{PERCENT_QUOTA};
    foreach my $volname (keys %h)
    {
        my $vol = $h{$volname};
        
        my $per = $vol->{percent};
        next if !(defined $per);
        $per = sprintf "%.0f", $per; # truncate!
        if(!$pquota->{$per})
        {
            $pquota->{$per} = 1;
        }
        else
        {
            $pquota->{$per} ++;
        }
    }
    $STATS{PERCENT_QUOTA} = $pquota;

    # Detect bad names
    foreach my $name (sort keys %h)
    {
        # Search for anything with either a ".." or a "^afs" or anything odd
        if(
                $name =~ /\.\./
            ||  $name =~ /^afs/
            ||  $name =~ /^\./
            ||  $name =~ /\.$/
          )
        {
            push @BadNames, $name;
        }
    }
    
    # Eliminate the partitions that have less than $QUOTA_PERCENT_THRESHOLD
    # And the ones that were modified more than $MODIFY_TIMEOUT seconds ago
    # Also try and ignore the backup and replicated volumes
    # and return
    my $time_cutoff = time - $MODIFY_TIMEOUT;
    return map {$_ => $h{$_} } grep { 
                        $h{$_}->{percent} && ($h{$_}->{percent} >= $QUOTA_PERCENT_THRESHOLD) 
                        && $h{$_}->{modified} >= $time_cutoff 
                        && !($h{$_}->{name} =~ /\.backup$/)
                        && !($h{$_}->{name} =~ /\.readonly$/)                                
                        } keys %h;
}


# This is a rewrite of the checkQuotas.
# This grabs a servers volume information, parses it, and returns volumes
# that have greater than or equal to the max_usage percent of usage
sub checkQuotas
{
    my ($server) = @_;

    # Run thru all of the partitions, parsing each volinfo file
    return map  { parseVolinfoFile($server, $_); } getServerPartitions($server);
}





##############################################################################
#                            AFS DATABASE COMMANDS
##############################################################################
sub afsdbConnect
{
    my $db = DBI->connect($AFSDB_CONNECT) or die "Couldn't connect to the afs database\n";

}

sub afsdbGetVolumeOwner
{
    my ($db, $volinfo) = @_;

    $volinfo->{owner} = "lab";

    # CHECK the partition owner!
    # This will only really matter if it is owned by LAB or something
    my $partition = $volinfo->{partition};
    $partition =~ s/vicep//;
    my $q = $db->prepare($AFSDB_QUERY{GetPartOwner}); 
    $q->execute($volinfo->{server}, $partition);
    my $res = $q->fetchrow_hashref();
    $q->finish();
    if($res)
    {
        $volinfo->{pool} = $res->{pool};
        if($volinfo->{pool} =~ /^lab\-/i)
        {
            $volinfo->{owner} = "lab";
            return $volinfo;
        }
    }
    
    # First: if it is a user volume, this is simple
    if($volinfo->{name} =~ /^u\.(\S+)/)
    {
        $volinfo->{owner} = $1;
        return $volinfo;
    }

    # See if its a class partition for a TA
    if($volinfo->{name} =~ /^c\.\S+\-(\S+)$/)
    {
        $volinfo->{owner} = $1;
        return $volinfo;
    }


    # Check the pool, and see if its owned by anyone
    $q = $db->prepare($AFSDB_QUERY{GetVolPool});
    $q->execute($volinfo->{name});
    $res = $q->fetchrow_hashref();
    $q->finish();
    if($res)
    {
        $volinfo->{pool} = $res->{pool};
         # Determine owner of pool
        $q = $db->prepare($AFSDB_QUERY{GetPoolOwner});
        $q->execute($volinfo->{pool});
        $res = $q->fetchrow_hashref();
        $q->finish();
        if($res)
        {
            $volinfo->{owner} = $volinfo->{pool};
            $volinfo->{owner} = $res->{owner} if $res->{owner};
            return $volinfo;
        }
    }
    
    return $volinfo;
}





##############################################################################
#                              USER RESOLUTION
##############################################################################

# Tries to resolve what the heck this username is for a volume
sub resolveUsername
{
    my ($user) = @_;

    # Check the alias, if it isn't valid, map it to lab
    if(!checkAlias($user))
    {
        return "lab";
    }
    
    # otherwise do some mapping, and then return... TODO
    return $user;
}



# This checks to see if an email alias even exists
sub checkAlias
{
    my ($user) = @_;

    my $res = `$GETALIAS $user 2&> /dev/null`;

    return if($res =~ /not found/);

    return 1;
}








##############################################################################
#                               REPORT GENERATION
##############################################################################

sub generateEmail
{
    my ($user, %volumes) = @_;

    # Determine if we sent an email recently or not

    # Create the email message
    my $msg = "To: $user\n";
    $msg .= "Subject: AFS Volume Quota Notification\n";
    $msg .= "From: lab\@cs.wisc.edu\n";
    $msg .= "\n";
    $msg .= "Dear $user,\n";
#    $msg .= "   This automated message was generated to inform you that one ";
#    $msg .= "or more of your volumes on the Computer Science's AFS system is ";
#    $msg .= "nearing or over quota.\n";
#    $msg .= "Informing you about the following volumes that you own:\n\n";
    $msg .= "This automated message was generated to inform you that the following one or more volumes on the Computer Science's AFS system is nearing or over quota (our warning threshold is $QUOTA_PERCENT_THRESHOLD percent):\n";
    $msg .= "\n";
#    $msg .= "                            Last\n";
#    $msg .= "   Volume Name              Update       Quota         Used    Percent\n";
#    $msg .= "   ------------------------ -------- ------------ ------------ ----------\n";
    my $user_msg;
    my $proj_msg;
    map
    {   
        my $vol = $volumes{$_};
        
        # get some values
#        my $mod = strftime "%D", $vol->{modified};
        my $mod = ctime($vol->{modified});
        chomp $mod;
        my $quota = $vol->{total} / 1024.0;
        my $used = $vol->{used} / 1024.0;
        
        # try this, for readability
        $msg .= "   Volume '$vol->{name}'\n";
        $msg .= sprintf "      Current Quota: %.2f MB\n", $quota;
        $msg .= sprintf "      Current Usage: %.2f MB (%.2f percent)\n", $used, $vol->{percent};
        $msg .= "      Last Update: $mod\n";
#        $msg .= sprintf "   %-24s %-8s %9.2f MB %9.2f MB %10.2f\n", $vol->{name}, $mod, $quota, $used, $vol->{percent};

        # Check for certain volume types
        $user_msg = 1 if($_ =~ /^u\./);
        $proj_msg = 1 if($_ =~ /^p\./);
    } sort keys %volumes;
    $msg .= "\n";

    # User volume message
    if($user_msg)
    {
        $msg .= "   At least one of these volumes is a USER volume (home directory).  ";
        $msg .= "Please try and reduce your user volume usage because a full user volume can lead to undesired behavior (eg. Cannot login, programs may not work, etc).\n";
        $msg .= "Common reasons your volume may have filled up are:\n";
        $msg .= "   - Web browser cache (try cleaning out your cache directory)\n";
        $msg .= "   - Class projects have gotten out of hand\n";
        $msg .= "   - Excessive personal files (try deleting unnessasary files)\n";
        $msg .= "   - A program coredumped, leaving a large 'core' file somewhere\n";
        $msg .= "\n";
    }

    # Project volume message
    if($proj_msg)
    {
        $msg .= "   At least one of these volumes is a PROJECT volume.  ";
        $msg .= "You may want to either request more project space, or try and reduce your volume usage.  You can request more project volume space with the following url: https://www-auth.cs.wisc.edu/forms/volume_change.php3\n";
        $msg .= "\n";
    }

    # Print out the 'I ROBOT' message
    $msg .= "   This message was produced automatically by a CSL robot that monitors AFS volume usage.  The volumes listed above were automatically assumed to be owned by you.  These alert messages are generated when volumes reach at least $QUOTA_PERCENT_THRESHOLD percent of their quota.  If you have any questions, or feel that the information in this message is wrong, please respond to lab\@cs.wisc.edu\n";
    $msg .= "\n";
    $msg .= "Thank You,\n";
    $msg .= "   Your friendly AFS robot,\n";
    $msg .= "   lab\@cs.wisc.edu\n";
    
    #print $msg . "\n\n\n\n";
    return $msg;
}


##############################################################################
#                           STATE STUFF
##############################################################################

my %VolState;

sub readVolState
{
    # Okay, read in the log info
    # We will also flush all entries that have timed out
    %VolState = ();
    open my $fh, "<$STATE_FILE" or return;
    while(my $line = <$fh>)
    {
        chomp $line;
        my ($vol, $t) = split /\t/, $line;
        if($t + time > $NOTIFY_TIMEOUT)
        {
            $VolState{$vol} = $t;
        }
    }
    close $fh;
}

sub markVolume
{
    my ($volume) = @_;
    $VolState{$volume} = time;
    Log("Marking volume '$volume' as notified\n");
}

sub saveVolState
{
    # Write out the volume log
    open my $fh, ">$STATE_FILE" or die "Couldn't write to volume log '$STATE_FILE': $!\n";
    map { print $fh "$_\t$VolState{$_}\n" } sort keys %VolState;
    close $fh;
}


sub stateBlocked
{
    my ($volume) = @_;
    return exists $VolState{$volume};
}



##############################################################################
#                            EMAIL SENDING STUFF
##############################################################################

sub sendEmail
{
    my (%opts) = @_;

    # create mail object
    my $msg = new Mail::Send;
    
    # setup all the header stuff
    $msg->to($opts{to}) if $opts{to};
    $msg->cc($opts{cc}) if $opts{cc};
    $msg->subject($opts{subject}) if $opts{subject};
    
    # Do the message body
    my $mh = $msg->open();
    print $mh $opts{message} if $opts{message};
    
    # Send the message
    $mh->close();
}

##############################################################################
#                               LOG OUTPUT STUFF
##############################################################################

sub Log
{
    return if !$LOG_OUTPUT;
    open my $fh, ">>$LOG_FILE" or die "Couldn't open log output file '$LOG_FILE': $!\n";
    my $t = ctime(time);
    chomp $t;
    print $fh "$t: " . (join " ", @_);
    close $fh;
}

##############################################################################
#                                MAIN PROGRAM
##############################################################################

# Get the command line options

my $optres = GetOptions (
                            "user"      => \$SEND_USER_EMAIL,
                            "admin"     => \$SEND_ADMIN_EMAIL,
                            "stats"     => \$STATS_OUTPUT,
                            "debug"     => \$DEBUG,
                            "load"      => \$LOAD_STATE,
                            "save"      => \$SAVE_STATE,
                            "log"       => \$LOG_OUTPUT,
                        );
if(!$optres)
{
    die "oops, we need a help section here :)\n";
}

# Load volume state info
if($LOAD_STATE)
{
    print "Loading volume state info (from $STATE_FILE)\n" if $DEBUG;
    readVolState();
}

# Log our startup
Log("Running quota checks\n");

# Connect to the db
print "Connecting to afsdf database...\n" if $DEBUG;
my $afsdb = afsdbConnect();

# grab the volume info 
print "Getting list of AFS servers...\n" if $DEBUG;
my %volumes = map { checkQuotas($_) } getServerList();

# Incorporate the user info into the hash
print "Resolving ownership of volumes...\n" if $DEBUG;
%volumes = map  { 
                    my $x = $volumes{$_};
                    $x = afsdbGetVolumeOwner($afsdb, $x);
                    $x->{owner} = resolveUsername($x->{owner});
                    $x->{name} => $x;
                } keys %volumes;

# Dump by users
print "Resorting volumes by owner...\n" if $DEBUG;
my %users = map { $volumes{$_}->{owner} => 1 } keys %volumes;




# Go thru each user, and send a message for their volumes
print "Generating email messages for each user...\n" if $DEBUG;
my %emails;
my $SENT_EMAIL;
map {
    my $user = $_;
    my %h = map { $_ => $volumes{$_} } 
              grep { ($volumes{$_}->{owner} eq $user) and (!stateBlocked($_))} 
              sort keys %volumes;

    # If any volumes exists that are not blocked by state,
    # then generate the user email
    if(%h)
    {
        # Generate email
        my $msg = generateEmail($user, %h) if %h;
        
        # Stuff that in a hash
        $emails{$user} = $msg;

        # Mark all of the volumes in the state table
        map { markVolume($_); $SENT_EMAIL = 1; } keys %h;
    }

} sort keys %users;




# Generate the admin email if we need to
if($SEND_ADMIN_EMAIL && $SENT_EMAIL)
{
    print "Generating Admin summary email...\n" if $DEBUG;
    my $msg = "This automagically generated message is an administrative summary of the [intended] actions of the quota_report script:\n\n";

    # Show a dump of our table 
    $msg .= "Here is the overall table of the users/volumes found that are nearing quota:\n";
    $msg .= sprintf "%-16s %8s %-24s %-24s\n", "User", "Percent", "Volume Name", "Last Update";
    $msg .= "---------------- -------- ------------------------ ------------------------\n";
    map 
    {
        my $vol = $volumes{$_};
        my $mod = ctime($vol->{modified});
        chomp $mod;
        $msg .= sprintf "%-16s %8.2f %-24s %-24s\n", $vol->{owner}, $vol->{percent}, $_, $mod;
    } sort { $volumes{$a}->{owner} cmp $volumes{$b}->{owner} } keys %volumes;

    # Dump out all the 
    $msg .= "\n\n\n\n";
    $msg .= "Here are all of the messages we are (or would be) sending:\n\n\n\n";
    $msg .= join "\n\n\n\n", ( map {$emails{$_}} sort keys %emails );

    # Send message to admin acct
    #print $msg;
    sendEmail(  to          => $ADMIN_EMAIL,
#                cc          => 'ajkunen@cs.wisc.edu',
                subject     => "Volume quota report",
                message     => $msg
              );
}

# Save state
if($SAVE_STATE)
{
    print "Saving volume state info...\n" if $DEBUG;
    saveVolState();
}

# Cleanup and exit
print "Disconnecting from database...\n" if $DEBUG;
$afsdb->disconnect();

# EXIT
print "Exiting... bye now!\n" if $DEBUG;

exit 0;

# DEBUG: dump some stats
my %per = %{$STATS{PERCENT_QUOTA}};
my $total_volumes = 0;
my $over_100 = 0;
foreach my $percent (sort {$a <=> $b} keys %per)
{
#    print "$percent $per{$percent}\n";
    $total_volumes += $per{$percent};
    $over_100 ++ if($percent > 100);
}

# Okay, now get percentages
my $acc = 0;
foreach my $percent (0..100)
{
    my $dist = 0;
    $dist = $per{$percent} if $per{$percent};
    $dist = $dist / $total_volumes * 100.0;
    $acc += $dist;
    print "$percent $dist $acc\n";
}
$over_100 = $over_100 / $total_volumes * 100.0;
$acc += $over_100;
print "101 $over_100 $acc\n";


    
