#!/usr/bin/perl
#
# Author: Bill McKinnon
# Date:   2010-11-12
#
#    Copyright Bill McKinnon 2011. All rights reserved.
#
# This script is under development. You are welcome to read it and to
# use it in whole or in part but please do not redistribute it. It is 
# not ready for that yet!
#
# Script to output who the current on call person is (assumes
# all timestamps are in the current timezone).
#
# called hourly from cron
#
# please send feedback suggestions and innovations to betsy.schwartz@gmail.com
#
# tech on duty follows a weekly schedule
#
# other teams use weekly rotations and this script assumes Sunday midnight
#
#Usage: get-tech-on-call.pl --timeframe <week|nextweek>   --schedule-file <schedule xml file>
#       get-tech-on-call.pl --timeframe now --type <type> --schedule-file <schedule xml file>
#
#  also takes a -name flag to output name instead of pager number
#
#  And a text file named <rotation>-override.txt containing a name will 
#  override a weekly rotation, if someone needs a substitute but does not 
#  want to change the basic rotation
#
use Getopt::Long;
use XML::Parser;
#use Data::Dumper;

use strict;

# Use a few globals to manage different bits of data...little lame but this is a small program so it should be ok.
my %options         = ();
my %techContactInfo = ();
my %scheduleInfo    = ();
my %overrideFile    = ();
my %techContacts    = ();
my %rotateWeekly    = ();

sub usage {
    my ($str) = @_;
    chomp($str);
    warn "$str\n" if ($str);
    die "Usage: $0 --timeframe <week|nextweek>   --schedule-file <schedule xml file>\n" .
        "       $0 --timeframe now --type <type> --schedule-file <schedule xml file>\n"  unless (@ARGV == 2);
}

sub parse_options {
    my %commandOptions = ();
    my ($timeframe, $type, $scheduleFile);
    GetOptions (\%commandOptions,
                "timeframe:s",
                "type:s",
                "names",
                "schedule-file:s")
      or usage();
    usage("Error: Missing --timeframe or --schedule-file options.") if (not exists($commandOptions{'timeframe'}) or not exists($commandOptions{'schedule-file'}));
    if (exists($commandOptions{'timeframe'}) and 
        $commandOptions{'timeframe'} eq 'now' and 
        not exists($commandOptions{'type'})) {
        usage("Error: --timeframe requires the --type flag be used.");
    }
    usage("Error: Invalid timeframe; must be one of now, week, or nextweek") 
        if (exists($commandOptions{'timeframe'}) and not $commandOptions{'timeframe'} =~ /^now$|^week$|^nextweek$/);
    return %commandOptions;
                   
}

sub handle_start {
    my ($expat, $element, %attr) = @_;

    if ($element eq 'schedule') {
        # Populate %scheduleInfo like:  $scheduleInfo{<type>}{<day>}{<hour>} => <list of staff members and their metadata>
        if (not exists($attr{'type'}) or 
            not exists($attr{'staff-members'}) or
            not exists($attr{'days'}) or
            not exists($attr{'hours'})) {
            warn "Missing required attribute (type, staff-members, days, or hours) in some schedules; on-call info may be inaccurate.\n";
        }

        # Populate overrides
        if ($attr{'check-for-override-file'}) {
            $overrideFile{$attr{'type'}} = "$attr{'type'}-override.txt";
        }

        # Note whether we should rotate through staff members on a weekly basis (by week number) or not.
        if (exists($attr{'rotate-by-week'})) {
            warn "Warning: rotate-by-week values should be 0 or 1 to disable or enable.\n" if ($attr{'rotate-by-week'} ne '0' and $attr{'rotate-by-week'} ne '1');
            $rotateWeekly{$attr{'type'}} = $attr{'rotate-by-week'};
        }

        my $tmpSched = {};
        my @days = ();
        my @hours = ();
        if ($attr{'days'} eq '*') {
            @days = qw(sun mon tue wed thu fri sat) 
        }
        else {
            @days = split(/\s*,\s*/, $attr{'days'});
        }
        if ($attr{'hours'} eq '*') {
            @hours = qw(0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23);
        }
        else {
            @hours = split(/\s*,\s*/, $attr{'hours'});
        }
        foreach my $day (@days) {
            $day = lc($day) if ($day =~ /[A-Z]/);
            warn "Warning: invalid day $day found (should be lowercase 3 letter day).\n" unless ($day =~ /^sun$|^mon$|^tue$|^wed$|^thu$|^fri$|^sat$/);
            foreach my $hour (@hours) {
                $hour = '0' . $hour if (length($hour) == 1);
                warn "Warning: invalid hour $hour found (should be 0-23).\n" unless ($hour =~ /^[01][0-9]$|^[2][0123]$/);
                #$scheduleInfo{$attr{'type'}}{$day}{$hour} = $attr{'staff-members'};
                push(@{ $scheduleInfo{$attr{'type'}}{$day}{$hour}}, split(/\s*,\s*/, $attr{'staff-members'}));
            }
        }
        my @staffMembers = split(/\s*,\s*/, $attr{'staff-members'});
        foreach my $staff (@staffMembers) {
            $techContacts{$staff} = 1;  # Note who we've seen so we can ensure they all have contact info later
        }
    }
    elsif ($element eq 'contact-info') {
        foreach my $contactAttr (keys %attr) {
            next if ($contactAttr eq 'name');
            $techContactInfo{$attr{'name'}}{$contactAttr} = $attr{$contactAttr};
        } 
    }
}

sub getDayAndHour {
    my $day = lc(`date +%a`);
    my $hour = `date +%H`;
    chomp($day);
    chomp($hour);
    return ($day, $hour);
}

# Get current staff member on rotation based on week number
sub getCurrentRotationStaffMember {
    my ($staffMembersAref, $day, $hour, $nextWeek) = @_;

    my $weekNumber;
    if ($nextWeek) {    
        $weekNumber = `date +%V -d "now + 1 week"`;
    }
    else {
        $weekNumber = `date +%V`;
    }
    chomp($weekNumber);
    my $staff;
    $staff = $staffMembersAref->[ $weekNumber % @$staffMembersAref ];  # mod week number by # of staff members for offset
    return $staff;
}

# Output schedule in csv form for easy display in Excel. We assume that the 'tech' schedule type
# has the normal hours info, while anything else with rotate-by-week set is an on-call schedule.
# Only these two types of schedules will be shown.
sub outputSchedule {
    my @hours = qw(00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23);
    my @days = qw(sun mon tue wed thu fri sat);
    print qq("Tech schedule:"\n);
    print qq("",), join(',', map { qq("$_") } @days), "\n";
    foreach my $hour (@hours) {
        print qq("$hour:00");
        foreach my $day (@days) {
            if (exists($scheduleInfo{'tech'}{$day}{$hour})) {
                print qq(,"), join(',', @{ $scheduleInfo{'tech'}{$day}{$hour}}), qq(");
            }
            else {
                print qq(,"");
            }
        }
        print "\n";
    }

    print "\n";
    print qq("Weekly on-call schedule:"\n);
    print qq("On call type","staff member","overridden?"\n);
    my @other = sort keys(%scheduleInfo);
    foreach my $type (@other) {
        next if ($type eq 'tech');
        next if (not $rotateWeekly{$type});

        my $override = 0;
        my @staff = ();
        my $onCall;
        if ($overrideFile{$type} and not -z $overrideFile{$type} and open(FH, "< $overrideFile{$type}")) {
            # Try to load the override file...it's ok if it doesn't exist though.
            my @lines = <FH>;
            warn "Warning: override file $overrideFile{$type} contains more than 1 line; ignoring all but first.\n" if (@lines > 1);
            my $overrideStr = $lines[0];
            chomp($overrideStr);
            @staff = split(/\s*,\s*/, $overrideStr);
            $override = 1;
            close(FH);
        }
        else {
            # Any day and hour combo should work for this week since we assume these on-call things have day='*' and hour='*'
            # TODO: Actually verify the above.
            my ($day, $hour) = getDayAndHour();
            @staff = @{ $scheduleInfo{$type}{$day}{$hour} };
            if ($options{'timeframe'} eq 'nextweek') {
                my $onCall = getCurrentRotationStaffMember(\@staff, $day, $hour, 1);
                @staff = ();
                push(@staff, $onCall);
            }
            else {
                my $onCall = getCurrentRotationStaffMember(\@staff, $day, $hour);
                @staff = ();
                push(@staff, $onCall);
            }
        }
        print qq("$type","), join(',', @staff), qq(",), ($override ? '"yes"' : '"no"'), "\n";
    }
}

sub showCurrentStaff {
    my ($day, $hour) = getDayAndHour();
    my @staff = ();
    if ($overrideFile{$options{'type'}} and not -z $overrideFile{$options{'type'}} and open(FH, "< $overrideFile{$options{'type'}}")) {
        # Try to load the override file...it's ok if it doesn't exist though.
        my @lines = <FH>;
        warn "Warning: override file $overrideFile{$options{'type'}} contains more than 1 line; ignoring all but first.\n" if (@lines > 1);
        my $overrideStr = $lines[0];
        chomp($overrideStr);
        @staff = split(/\s*,\s*/, $overrideStr);
        close(FH);
    }
    else {
        # Otherwise get staff from data struct populated by reading xml schedule (the normal way)
        @staff = @{ $scheduleInfo{$options{'type'}}{$day}{$hour} };
    }

    if (@staff) {
        if ($rotateWeekly{$options{'type'}}) {
            # Get the staff member that's on call per rotation (done by week no.)
            warn "Weekly rotation enabled but only one staff members provided.\n" if (@staff == 1 and not exists($overrideFile{$options{'type'}}));
            my $onCall = getCurrentRotationStaffMember(\@staff, $day, $hour);
            @staff = ();
            push(@staff, $onCall); 
        }
        # Actually output staff member(s) with all metadata
        if ($options{'names'}) {
            # Just output names
            print join(',', @staff), "\n";
        }
        else {
            # Just output pager info
            print join(',', map { "$techContactInfo{$_}{'page-number'}" } @staff), "\n";
        }
    }
}




#
# Mainline code
#
%options = parse_options();

# Parse XML schedule to populate various data structures
my $p2 = new XML::Parser(Handlers => {Start => \&handle_start});
$p2->parsefile($options{'schedule-file'});

# Verify all staff referenced in schedule exist in contact info
my $missingContactInfo = 0;
foreach my $contact (keys %techContacts) {
    if (not exists($techContactInfo{$contact})) {
        warn "Error: Contact $contact doesn't have any contact info defined.\n";
        $missingContactInfo = 1;
    }
}
die "Missing contact info; exiting.\n" if ($missingContactInfo);

if ($options{'timeframe'} eq 'now' and not exists($scheduleInfo{$options{'type'}})) {
    die "Error: Type $options{'type'} not found in the given schedule file.\n";
} 

if ($options{'timeframe'} eq 'now') {
    # Output current staff and their metadata for right now given schedule
    showCurrentStaff();
}
else {
    outputSchedule();
}
