#!/usr/bin/perl
#
# Copyright (c) 2013, Michael Hanselmann
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# - Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
# - Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
# - Neither the name of Michael Hanselmann nor the names of any contributor may
#   be used to endorse or promote products derived from this software without
#   specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

use strict;
use warnings;

use Try::Tiny;
use POSIX qw(ctime);
use Getopt::Std;
use File::Basename;
use Foundation;

use constant OK => 0;
use constant WARNING => 1;
use constant CRITICAL => 2;
use constant UNKNOWN => 3;
use constant BACKUPD_STRINGS =>
  '/System/Library/CoreServices/backupd.bundle/' .
  'Contents/Resources/English.lproj/Localizable.strings';

my $result_plist_path = '/var/db/.TimeMachine.Results.plist';

my $VERSION = '0.1.1';

$Getopt::Std::STANDARD_HELP_VERSION = 1;

sub HELP_MESSAGE {
  my ($fh) = @_;
  my $name = basename($0);

  print $fh
    "Usage: $name -w <warn time> -c <critical time>\n",
    "\n",
    " -w  Seconds since last backup to issue warning\n",
    " -c  Seconds since last backup for critical status\n",
    "\n";
}

sub VERSION_MESSAGE {
  my ($fh) = @_;
  my $name = basename($0);

  print $fh "$name version $VERSION\n";
}

# Use UTF-8 output
binmode(STDOUT, ':utf8');
binmode(STDERR, ':utf8');

sub load_plist {
  my ($path) = @_;

  my $dict = NSDictionary->dictionaryWithContentsOfFile_($path);

  unless ($$dict) {
    die "Couldn't load results from '$path'\n";
  }

  return Foundation::perlRefFromObjectRef($dict);
}

sub parse_date {
  my ($date) = @_;

  my $obj = NSDate->dateWithString_($date);

  unless ($$obj) {
    die "Couldn't parse date string '$date'\n";
  }

  return $obj->timeIntervalSince1970;
}

sub lookup_error {
  my ($code) = @_;

  return try {
    my $strings = load_plist(BACKUPD_STRINGS);
    my $header = $strings->{"RESULTCODE_${code}_HEADER"};
    my $msg = $strings->{"RESULTCODE_${code}_MESSAGE"};

    if (defined $header) {
      chomp $header;
      $header =~ s/\s+/ /g;
    }

    if (defined $msg) {
      chomp $msg;
      $msg =~ s/\s+/ /g;
    }

    if (defined $header and defined $msg) {
      "$header: $msg";
    } elsif (defined $msg) {
      $msg;
    } else {
      die;
    }
  } catch {
    "Unknown error code $code";
  };
}

sub _exit {
  my ($code, $prefix, $msg) = @_;
  chomp $msg;
  print "$prefix: $msg\n";
  exit $code;
}

sub exit_unknown {
  _exit(UNKNOWN, 'UNKNOWN', @_);
}

sub exit_critical {
  _exit(CRITICAL, 'CRITICAL', @_);
}

sub exit_warning {
  _exit(WARNING, 'WARNING', @_);
}

sub exit_ok {
  _exit(OK, 'OK', @_);
}

sub main {
  my $warning_age;
  my $critical_age;
  my %opts;

  getopts('w:c:', \%opts) or exit UNKNOWN;

  if (defined $opts{w}) {
    $warning_age = $opts{w};
  } else {
    exit_unknown("Missing warning age (-w)");
  }

  if (defined $opts{c}) {
    $critical_age = $opts{c};
  } else {
    exit_unknown("Missing critical age (-w)");
  }

  if ($warning_age =~ m/^\d+/) {
    $warning_age = int($warning_age);
  } else {
    exit_unknown("Warning age is not a number: $warning_age");
  }

  if ($critical_age =~ m/^\d+/) {
    $critical_age = int($critical_age);
  } else {
    exit_unknown("Critical age is not a number: $critical_age");
  }

  if ($critical_age < $warning_age) {
    exit_unknown("Critical age must be equal or larger than warning age");
  }

  # Try loading data
  my $data = try {
    load_plist($result_plist_path);
  } catch {
    exit_critical($_);
  };

  my $result_str = $data->{RESULT};
  my $completed_str = $data->{BACKUP_COMPLETED_DATE};

  unless (defined $result_str) {
    exit_critical('Unable to find result code');
  }

  unless ($result_str =~ m/^\d+$/) {
    exit_critical("Result code not a number: $result_str");
  }

  my $result_code = int($result_str);

  if ($result_code != 0) {
    exit_critical(lookup_error($result_code));
  }

  unless (defined $completed_str) {
    exit_critical('Unable to find completion date');
  }

  my $completed = try {
    parse_date($completed_str);
  } catch {
    exit_critical($_);
  };

  my $now = time;
  chomp (my $fmtcompleted = ctime($completed));

  if ($completed < ($now - $critical_age)) {
    exit_critical("Backup is older than $critical_age seconds ($fmtcompleted)");
  } elsif ($completed < ($now - $warning_age)) {
    exit_warning("Backup is older than $warning_age seconds ($fmtcompleted)");
  } else {
    exit_ok("Backup finished at " . ctime($completed));
  }
}

main;

# vim: set sw=2 sts=2 et foldmethod=marker :
