#!/usr/bin/perl -w

use utf8;
use strict;
use warnings;

BEGIN {
    unshift @INC, ( $ENV{'POPHOME'} || '@POPHOME@' ) . '/lib'
      unless $ENV{'KUNDE_NO_PERLPATH'};
}

use noris::NetSaint ();
use Text::Abbrev    ();
use Time::Period    ();
use Dbase::Getopt qw(:DEFAULT :kunden);
use Dbase::Globals qw(is_holiday name_kunde);
use Dbase::Help qw(:readonly unixtime);
use noris::Ticket::API qw(get_pooled_connection in_out_list);

use constant KEYWORD => { feiertage => undef, holidays => undef, ticket => 1 };
use constant ABBREV  => { Text::Abbrev::abbrev keys %{ +KEYWORD } };
use constant UNIT    => { s => 1, m => 60, h => 3600, d => 86400 };
use constant UNIT_RE => qr/[${\join '', keys %{+UNIT}}]/i;

our $NetSaint = new noris::NetSaint;

my @Args = GetOptions(
    'holiday-scheme=s' => \my $Profile,
    'max-priority=i'   => \my $MaxPriority,
    'min-priority=i'   => \my $MinPriority,
    'ohne-queue=s'     => \my @OhneQueue,
    'queue=s'          => \my @Queue,
);

my $Calendar;

my @actions;
{
    my @filters;
    my $mode;
    for my $arg (@Args) {
        if ( $arg =~
/\s*(?:(\d+)\s*(${\UNIT_RE})?\s*)?\/\s*(?:(\d+)\s*(${\UNIT_RE})?\s*)?\z/
          )
        {
            push @actions,
              {
                filters => [@filters],
                defined $1
                ? ( Warning => $1 * ( defined $2 ? UNIT->{$2} : 1 ) )
                : (),
                defined $3
                ? ( Critical => $3 * ( defined $4 ? UNIT->{$4} : 1 ) )
                : ()
              };
            undef @filters;
            undef $mode;
        }
        elsif ( $arg =~ /^\s*(\^)?(\w+)\s*\z/
            && defined( my $keyword = ABBREV->{$2} ) )
        {
            if ( defined KEYWORD->{$keyword} ) { $mode = !defined $1 }
            else {
                push @filters,
                  $mode
                  ? defined $1
                  ? sub { !is_holiday( shift, $Profile ) }
                  : sub { is_holiday( shift, $Profile ) }
                  : defined $1
                  ? sub { !is_holiday( $^T, $Profile ) }
                  : sub { is_holiday( $^T, $Profile ) }
            }
        }
        elsif ( -1 < ( my $inPeriod = Time::Period::inPeriod( $^T, $arg ) ) ) {
            push @filters, $mode
              ? sub { Time::Period::inPeriod shift, $arg }
              : sub { $inPeriod }
        }
        else { die "Nicht identifizierbares Argument: $arg\n" }
    }
}

my $n_tickets = 0;
get_pooled_connection()->select_tickets(
    attributes => [qw(ticket_number kunde queue created)],
    query      => {
        in_out_list(
            kunde => [ map name_kunde($_), @Kunden ],
            [ map name_kunde($_), @OhneKunden ]
        ),
        owner    => undef,
        priority => [ range => $MinPriority, $MaxPriority ],
        status   => 'open',
        in_out_list( queue => \@Queue, \@OhneQueue ),
    },
  )->foreach_row(
    sub {
        my ( $ticket, $kunde, $queue, $beginn ) = @_;
        ++$n_tickets;
        $beginn = unixtime($beginn);
        my $offen = $^T - $beginn;
        for my $status (qw(Critical Warning)) {
          ACTION: for my $action (@actions) {
                next
                  unless exists $action->{$status}
                  && $offen > $action->{$status};
                $_->($beginn) or next ACTION for @{ $action->{filters} };
                $NetSaint->update(
                    $status =>
"Ticket $ticket von Kunde $kunde in Queue $queue steht seit "
                      . (
                        $offen > 172800
                        ? 'über ' . int( $offen / 86400 ) . ' Tagen'
                        : $offen > 7200
                        ? 'über ' . int( $offen / 3600 ) . ' Stunden'
                        : $offen > 120
                        ? 'über ' . int( $offen / 60 ) . ' Minuten'
                        : "$offen Sekunden"
                      )
                      . ' offen'
                );
                return;
            }
        }
    }
  );

if ( $NetSaint->status eq 'Unknown' ) {
    $NetSaint->message(
        "$n_tickets offene" . ( $n_tickets == 1 ? 's Ticket' : ' Tickets' ) );
    $NetSaint->status('OK');
}

__END__

=head1 NAME

check_rt-tickets -- Monitoring-Plugin für zu lange unbearbeitete Tickets

=head1 GEBRAUCH

    check_rt-tickets <Option>* { <Zeit-Filter>* <Aktions-Schwelle> }+

=head1 BEGRIFFSDEFINITION

=over 4

=item offenes Ticket

Gemeint ist ein Ticket mit Status C<open> und ohne Besitzer.

=back

=head1 BEISPIELE

  check_rt-tickets --kunde consors \
                   --queue support --queue technik \
                   'wd {Mon-Fri} hr {8-17}' ^fe 15m/30m \
                   1d/

Erzeugt an allen Mon- bis Freitagen, die nicht Feiertage sind, zwischen 8 und
(kurz vor) 18 Uhr für alle nach fünfzehn Minuten noch offenen, dem Kunden
C<consors> zugeordneten C<support>- und C<technik>-Tickets eine Warnung bzw.
nach dreißig Minuten ggf. einen kritischen Alarm und die übrige Zeit eine
Warnung, wenn entsprechende Tickets nach einem Tag noch offen sind.

  check_rt-tickets --queue incident --queue support --max-prio 3 \
    'wd {Mon-Fri} hr {9} min {30-59}, wd {Mon-Fri} hr {10-16}' ^ho 30m/42m \
    ticket 'wd {Mon-Fri} hr {9-16}' ^ho 30m/42m

Dieser Job gilt für alle C<incident>- und C<support>-Tickets mit einer Priorität
zwischen 0 und 3 und tut folgendes:

=over 4

=item *

Zwischen 8:00 und 16:59 Uhr und sofern nicht Wochenende oder Feiertag ist,
wird für entsprechende Tickets nach 30 Minuten eine Warnung und nach 42 Minuten
ein kritischer Alarm erzeugt.

=item *

Ansonsten werden auch außerhalb dieser Zeiten bei entsprechend lang offenen
Tickets entsprechende Alarme erzeugt, sofern das fragliche Ticket innerhalb der
oben bereits erläuterten Zeiten entstanden ist.
(Das sorgt also dafür, dass ein noch vor 17 Uhr reingekommenes Ticket nicht
versehentlich bis zum nächsten Tag liegen bleibt.)

=back

=head1 OPTIONEN

=over 4

=item --holiday-scheme <Profil-Name>

Alternatives Feiertags-Schema verwenden.
Details siehe L<Date::Calendar::Profiles|Date::Calendar::Profiles>.

=item -ap-technik Person

=item -ap-vertrieb Person

=item -kunde Kunde

=item -kunde-und-unterkunden Kunde

=item -ohne-ap-technik Person

=item -ohne-ap-vertrieb Person

=item -ohne-kunde Kunde

=item -ohne-kunde-und-unterkunden Kunde

Per Default werden Tickets aller Kunden untersucht.
Dies lässt sich vermittels dieser Optionen modifizieren.
Details dazu siehe L<Dbase::Getopt/:kunden>.

=item --min-priority <Priorität>

Suche auf Tickets ab einer bestimmten Mindest-Priorität einschränken.

=item --max-priority <Priorität>

Suche auf Tickets bis zu einer bestimmten Maximal-Priorität einschränken.

=item --queue <Queue>

Suche auf (eine) bestimmte Queue(s) einschränken.
(Um mehrere Queues zu berücksichtigen, Option bitte mehrfach verwenden!)

=item --ohne-queue <Queue>

Bestimmte Queue(s) von der Suche ausschließen.
(Um mehrere Queues auszuschliessen, Option bitte mehrfach verwenden!)

=item --without-queue <Queue>

Alias für L<--ohne-queue|/--ohne-queue>.

=item --limit <n>

Die Datenbankabfrage wird auf maximal I<n> Tickets beschränkt (Default: 42).

=head1 ZEIT-FILTER

Zeit-Filter dienen dazu, die Suche auf bestimmte Wochentage und/oder Tageszeiten
einzuschränken.
Normalerweise beziehen sie sich auf den Zeitpunkt des Script-Starts.
Durch Übergabe des (ggf. eindeutig abkürzbaren und beliebig groß/klein
schreibbaren) Schlüsselworts "Ticket" (als eigenes Argument) kann angegeben
werden, dass sich alle folgenden Filter auf den Zeitpunkt beziehen sollen, zu
dem das jeweilige Ticket geöffnet wurde; dies gilt dann für alle Filter bis
zum Rücksetzen durch die nächste Aktion.

Alle Filter beziehen sich dabei (nur) auf die jeweils nächste angegebene
L<Aktions-Schwelle|/AKTIONS-SCHWELLE>, d. h. sie werden durch diese
zurückgesetzt.
Werden mehrere Typen von Zeit-Filtern für eine
L<Aktions-Schwelle|AKTIONS-SCHWELLE> angegeben, so müssen Tickets ggf. alle
angegebenen Filter-Kriterien erfüllen; die Filter werden also
I<UND>-verknüpft.

Es gibt zwei Typen von Zeit-Filtern:

=over 4

=item Feiertags-Filter

Durch das Schlüsselwort I<Feiertage> bzw. I<holidays> wird signalisiert, dass
nur Tickets, die an Feiertagen - dabei gelten auch Sams- und Sonntage als
Feiertage - geöffnet wurden, beachtet werden sollen.
(Diese Schlüsselwörter können bis auf minimal zwei Anfangsbuchstaben
abgekürzt werden.)
Ein vorangestelltes I<^> invertiert die Bedeutung, d. h. Feiertags-Tickets
werden dann von der Suche ausgeschlossen.
Auf den Punkt gebracht:

    fe		# nur Feiertags-Tickets
    ho          # "   "        -"
    ^Fe		# keine Feiertags-Tickets
    ^ho         # "     "        -"

Die Groß-/Kleinschreibung ist im Übrigen unbeachtlich.

=item allgemeine Zeit-Filter

Schränken die Suche auf solche Tickets ein, die an bestimmten Wochentagen,
zu bestimmten Uhrzeiten, in bestimmten Monaten etc. geöffnet wurden.
Diese Filter werden von L<Time::Period|Time::Period> interpretiert; Details zur
Syntax finden sich also in der Dokumentation zu diesem Modul.

=back

=head1 AKTIONS-SCHWELLE

Hiermit wird definiert, wie lange ein Ticket maximal unbearbeitet bleiben kann,
bevor eine Warnung bzw. ein kritischer Zustand ausgelöst wird.
Zum Beispiel:

    10m/42m	# Warnung, wenn ein Ticket mindestens 10 Minuten
                # unbearbeitet ist, kritischer Zustand ab 42 Minuten
    42/         # Warnung ab 42 Sekunden (aber nie kritisch)
    /1d		# kritisch, sobald Ticket mindestens einen Tag offen ist

Die Zeit kann dabei in B<S>ekunden, B<M>inuten, B<h> = Stunden oder B<d> = Tagen
angegeben werden.
Ein Zahlenwert ohne Einheit wird als Angabe in Sekunden betrachtet.

Ein Ticket gilt als I<unbearbeitet>, wenn es (gleichzeitig) I<open> ist,
keinen Bearbeiter hat und im Status-Feld kein Text eingegeben worden ist.

=head1 AUTOR

 Martin H. Sluka <fany@noris.net>
 für die noris network AG

=cut

