#!/usr/bin/perl -w

use utf8;
use strict;
use warnings;

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

use Cf qw($MAILDOM $RT_DOMAIN $TICKET_DOMAIN $WDESCR);
use Dbase::Getopt qw(:DEFAULT :kunden getopt_date getopt_person);
use Dbase::Globals qw(sendmail);
use Dbase::Help qw(Do DoFn DoSelect isotime like_list qquote);
use Dbase::Kunde;
use Dbase::Person;
use Loader qw(get_recipients_from_header process_template);
use noris::Ticket::API qw(get_pooled_connection in_out_list);

use constant HR => ( '-' x 79 ) . "\n";

sub exact_isotime($) {
    defined( my $epoch = shift ) or return undef;
    scalar isotime( $epoch, 1 | 2 );
}

my (
    @Bearbeiter,   @OhneBearbeiter,   $Mindestalter,
    $MustertextId, $MustertextInhalt, $NichtBearbeitetSeit,
    $Sollzeit,     $TerminAb,         $TerminBis,
    $TicketsSeit,
);
my $Speichern = 1;
GetOptions(
    'alarm2=s'            => \my @Alarm2,
    'alarm2bearbeiter!'   => \my $Alarm2Bearbeiter,
    'alarm2vertrieb-ap!'  => \my $Alarm2APvertrieb,
    'alarm2technik-ap!'   => \my $Alarm2APtechnik,
    'alarm2ticket!'       => \my $Alarm2Ticket,
    'alarm2vorgesetzter!' => \my $Alarm2Vorgesetzter,
    'abteilung-like=s'    => \my @AbteilungLike,
    'bearbeiter=s'        => sub {
        push @Bearbeiter,
          Dbase::Person->new_cached( id => &getopt_person )->user;
    },
    'empfaenger-aus-header' => \my $EmpfaengerAusHeader,
    'mail!'                 => \( my $Mail = 1 ),
    'max-prio=i'            => \my $MaxPrio,
    'min-prio=i'            => \my $MinPrio,
    'mindestalter=s' => sub { $Mindestalter = exact_isotime(&getopt_date) },
    'nicht-bearbeitet-seit=s' =>
      sub { $NichtBearbeitetSeit = exact_isotime(&getopt_date) },
    'nicht-speichern' => sub { $Speichern = 0 },
    'ohne-bearbeiter=s' => sub {
        die <<_ if length $_[1];
Derzeit wird nur -$_[0] '' zum Ausschluss freier Tickets unterstützt,
vgl. #10054651.
_
        my $person = &getopt_person;
        $person = Dbase::Person->new_cached( id => &getopt_person )->user
          if defined $person;
        push @OhneBearbeiter, $person;
    },
    'ohne-queue=s'   => \my @OhneQueues,
    'ohne-status=s'  => \my @OhneStatus,
    'min-sollzeit=f' => \$Sollzeit,
    'ohne-sollzeit'  => sub { $Sollzeit = '' },
    'queue=s'        => \my @Queues,
    'speichern!'     => \$Speichern,
    'status=s'       => \my @Status,
    'termin-ab=s'    => sub { $TerminAb = exact_isotime(&getopt_date) },
    'termin-vor=s'   => sub { $TerminBis = exact_isotime( &getopt_date - 1 ) },
    'ohne-termin'    => \my $OhneTermin,
    'tickets-seit=s' => sub { $TicketsSeit = exact_isotime(&getopt_date) },
    'unbedingt'      => \my $Unbedingt,
    'mustertext=s'   => sub {
        ( undef, my $option_value ) = @_;
        ( $MustertextId, $MustertextInhalt ) = DoFn(<<1) or die <<2;
	SELECT id, inhalt
	FROM   rt_eskalation_muster
	WHERE  name = ${\ qquote($option_value) }
1
In Kategorie "rt_eskalation_muster" existiert kein Mustertext namens "$option_value".
2
    },
);

die <<_ if $OhneTermin and defined $TerminAb || defined $TerminBis;
Eine Kombination von -ohne-termin mit -termin-{ab,vor} wird nicht mehr
unterstützt.
_

@OhneStatus = 'merged' unless @Status || @OhneStatus;

DoSelect { push @Bearbeiter, shift }
'SELECT user FROM person WHERE kunde = 1 AND user IS NOT NULL AND '
  . like_list( abt => '', @AbteilungLike )
  if @AbteilungLike;

my %query = (
    created => [ range => $TicketsSeit, $Mindestalter ],
    due => $OhneTermin ? undef: [ range => $TerminAb, $TerminBis ],
    in_out_list(
        kunde => (
            [ map Dbase::Kunde->new_cached( id => $_ )->name, @Kunden ],
            [ map Dbase::Kunde->new_cached( id => $_ )->name, @OhneKunden ]
        )
    ),

    # sobald #10054651 umgesetzt ist:
    # in_out_list( owner => \@Bearbeiter, \@OhneBearbeiter ),
    # vorerst:
    in_out_list( owner => \@Bearbeiter, [] ),

    priority => [ range => $MinPrio, $MaxPrio ],
    in_out_list( queue  => \@Queues, \@OhneQueues ),
    in_out_list( status => \@Status, \@OhneStatus ),
);

if ( defined $NichtBearbeitetSeit ) {
    $ENV{TICKET_BACKENDS} = 'RT';
    $query{last_action} = [ '<', $NichtBearbeitetSeit ];
}

my @attributes = 'ticket_number';
push @attributes, 'est_effort' if defined $Sollzeit;
push @attributes, 'owner' if @OhneBearbeiter;

get_pooled_connection()
  ->select_tickets( attributes => \@attributes, query => \%query )
  ->foreach_row(
    sub {
        my $tn = shift;
        my $est_effort;
        $est_effort = shift if defined $Sollzeit;

        # Workaround, bis #10054651 umgesetzt ist:
        return if @OhneBearbeiter && !defined shift;

        # Bereits eskalierte Fälle können wir erst hier abfangen, solange
        # der OTRS-TicketServer kein not_in für ticket_number unterstützt:
        return if defined $MustertextId && !$Unbedingt && DoFn(<<_);
	SELECT 1
	FROM   rt_eskalation
	WHERE  rtticket = $tn AND template = $MustertextId
_

        # -min-sollzeit und -ohne-sollzeit müssen wir ebenfalls hier
        # behandeln, weil der OTRS-TicketServer das als Stringfeld
        # betrachtet und also keine range-Abfragen unterstützt.
        $est_effort = undef
          if defined $est_effort
          && $est_effort eq '';  # OTRS liefert bei nicht gesetzter Sollzeit "".
        return
          if defined $Sollzeit
          and $Sollzeit eq '' ? defined $est_effort : defined $est_effort
          && $est_effort >= $Sollzeit;

        require Dbase::Ticket;
        my $ticket = Dbase::Ticket->new( ticket_number => $tn );
        my $ticket_email = "$tn\@$RT_DOMAIN";

        my @empfaenger;
        {
            my %empfaenger;

            @empfaenger{ map /\@/ ? $_ : "$_\@$MAILDOM", @Alarm2 } = ();

          Switch: for (
                [ $Alarm2Bearbeiter   => qw/owner/ ],
                [ $Alarm2APtechnik    => qw/kunde ap_technik/ ],
                [ $Alarm2APvertrieb   => qw/kunde ap_vertrieb/ ],
                [ $Alarm2Vorgesetzter => qw/owner vorgesetzter/ ],
              )
            {
                my ( $switch, @indirektion ) = @$_;
                next unless $switch;
                my $person = $ticket;
                defined( $person = $person->$_ )
                  or next Switch
                  for @indirektion;
                if ( defined( my $email = $person->email ) ) {
                    $empfaenger{$email} = undef;
                }
                elsif ( defined( my $user = $person->user ) ) {
                    $empfaenger{"$user\@$MAILDOM"} = undef;
                }
            }

            # Solange es das RT noch gibt,
            # belassen wir's erstmal bei $RT_DOMAIN,
            # weil das auch fürs OTRS funktioniert,
            # aber umgekehrt $TICKET_DOMAIN nicht fürs RT:
            $empfaenger{$ticket_email} = undef if $Alarm2Ticket;

            @empfaenger = sort keys %empfaenger;
        }

        my %data = (
            empfaenger    => \@empfaenger,
            rt_domain     => $RT_DOMAIN,
            ticket        => $ticket,
            ticket_domain => $TICKET_DOMAIN,
            wdescr        => $WDESCR,
        );

        if ( defined $MustertextId ) {
            process_template( \%data, \$MustertextInhalt, \my $mail );

            if ($EmpfaengerAusHeader) {
                my %known;
                @empfaenger = grep !$known{ +lc }++, @empfaenger,
                  get_recipients_from_header($mail);
            }

            if ( $mail !~ /^\s*IGNORE\s*\z/ ) {
                if ( $Mail && @empfaenger ) {
                    sendmail( $mail, $ticket_email, @empfaenger );
                }
                else { print HR, $mail }
                Do( <<1. ( $Unbedingt ? <<'2': '' ) ) if $Speichern;
	INSERT INTO rt_eskalation SET rtticket=$tn, template=$MustertextId
1
	ON DUPLICATE KEY UPDATE timestamp = NULL
2
            }
        }
        else { process_template( \%data ) }
    }
  );

__END__

=encoding utf8

=head1 NAME

rt-eskalation - alarmiere, wenn RT-Tickets zu lange einen bestimmten Status haben

=head1 BESCHREIBUNG

Das Script erzeugt für alle L<ausgewählten Tickets|/Optionen zum Auswählen der
Tickets, für die Alarme erzeugt werden sollen> E-Mails anhand von
L<Mustertexten|/-mustertext Name> an die L<gewünschten Empfänger|/Optionen zum
Auswählen der Alarmierungsziele> bzw. tut dies I<nicht>, wenn für ein bestimmtes
Ticket der gewünschte L<Mustertext|/-mustertext Name> bereits verwendet wurde.

=head1 OPTIONEN

=head2 Optionen zum Auswählen der Tickets, für die Alarme erzeugt werden sollen

=over 4

=item -abteilung-like SQL-Wildcard

Selektion nach Abteilung/Team des Ticket-Bearbeiters, also z. B.

 	-abteilung-like 'Technik%'
oder
	-abteilung-like 'Technik, Team Entwicklung'

Bei mehrfacher Verwendung der Option werden alle Tickets selektiert, bei denen
die Abteilung des Bearbeiters zu mindestens einer der angegebenen SQL-Wildcards
passt.

=item -bearbeiter Person

nur Tickets des angegebenen Bearbeiters beachten.
Tickets ohne Bearbeiter lassen sich auswählen, indem man eine leere
Zeichenkette als Argument übergibt.
Kann mehrfach verwendet werden, um Tickets mehrerer Bearbeiter zu beachten.

=item -ohne-bearbeiter Person

Tickets des angegebenen Bearbeiters nicht beachten.
Tickets ohne Bearbeiter lassen sich ausschließen, indem man eine leere
Zeichenkette als Argument übergibt.
Kann mehrfach verwendet werden, um Tickets mehrerer Bearbeiter auszuschließen.

=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 -kunden-prio a

=item -kunden-prio b

=item -kunden-prio c

nur Tickets von Kunden der angegebenen Priorität beachten.
Kann mehrfach verwendet werden, um die Tickets von Kunden unterschiedlicher
Prioritäten zu beachten.

=item -ohne-kunden-prio a

=item -ohne-kunden-prio b

=item -ohne-kunden-prio c

nur Tickets von Kunden beachten, die I<nicht> zur angegebenen Priorität gehören.
Kann mehrfach verwendet werden, um mehrere Kundenprioritäten auszuschließen.

=item -min-prio Priorität

nur Tickets, die mindestens diese Priorität haben

=item -max-prio Priorität

nur Tickets, die maximal diese Priorität haben

=item -tickets-seit Datum

Nur Tickets beachten, die seit dem angegebenen Datum entstanden sind.

=item -mindestalter Datum

Nur Tickets beachten, die bis zum angegebenen Datum entstanden sind.
Das Datum wird mit L<Time::ParseDate> analyisiert und kann (also) auch relativ
angegeben werden.

=item -nicht-bearbeitet-seit Datum

Nur Tickets beachten, in denen sich seit dem angegebenen Datum nichts getan hat.
Das Datum wird mit L<Time::ParseDate> analyisiert und kann (also) auch relativ
angegeben werden.

B<Funktioniert nur fürs RT!>
Bei Verwendung dieser Option werden keine OTRS-Tickets gefunden.

=item -termin-ab Datum

Nur Tickets beachten, bei denen ein Termin gesetzt ist, der nicht vor dem
angegebenen Datum liegt.
(Kann nicht mit L</-ohne-termin> kombiniert werden.)

=item -termin-vor Datum

Nur Tickets beachten, bei denen ein Termin gesetzt ist, der vor dem angegeben
Datum liegt.
(Kann nicht mit L</-ohne-termin> kombiniert werden.)

=item -ohne-termin

Tickets ohne gesetzten Termin auswählen.
(Kann nicht mit L</-termin-ab Datum> und/oder L</-termin-vor Datum> kombiniert
werden.)

=item -queue Queue

nur Tickets in der angegebenen Queue beachten.
Kann mehrfach verwendet werden, um Tickets aus unterschiedlichen Queues zu
beachten.

=item -ohne-queue Queue

Ticket in der angegebenen Queue ausschließen.
Kann mehrfach verwendet werden, um Tickets aus unterschiedlichen Queues
auszuschließen.

=item -min-sollzeit Stunden

nur Tickets, bei denen mindestens die angegeben Sollzeit eingetragen ist

=item -ohne-sollzeit

nur Tickets ohne Sollzeiteintragung

=item -status open

=item -status stalled

=item -status resolved

=item -ohne-status open

=item -ohne-status stalled

=item -ohne-status resolved

Ticket-Auswahl nach Status(gruppen) einschränken.
Diese Optionen können jeweils mehrfach verwendet werden.
Wird keine der beiden Optionen verwendet, so werden gilt als Default:
	-ohne-status merged

=back

=head2 Optionen zum Auswählen der Alarmierungsziele

=over 4

=item -empfaenger-aus-header

Ermittle (ggf. zusätzliche) Empfänger aus dem C<To:>-, C<CC:>- und
C<Bcc:>-Header der E-Mail.

=item -alarm2 E-Mail-Adresse

sende einen Alarm an die angegebene E-Mail-Adresse.
Die Option kann mehrfach verwendet werden, um Alarme an mehrere E-Mail-Adressen
zu versenden.

=item -alarm2bearbeiter

Sende einen Alarm an die E-Mail-Adresse des Bearbeiters des Tickets.

=item -alarm2ap-vertrieb

Sende einen Alarm an die E-Mail-Adresse des vertrieblichen Ansprechpartners des
Kunden, zu dem das Ticket gehört.

=item -alarm2ap-technik

Sende einen Alarm an die E-Mail-Adresse des technischen Ansprechpartners des
Kunden, zu dem das Ticket gehört.

=item -alarm2ticket

Sende einen Alarm an das Ticket selbst.

=item -alarm2vorgesetzter

Sende einen Alarm an den direkten Vorgesetzten des Bearbeiters des Tickets.

=head2 sonstige Optionen

=item -mustertext Name

Name des für die versendeten E-Mails zu verwendenden Mustertexts aus der
Kategorie "rt-eskalation".
Wird keiner angegeben, erfolgt eine Debug-Ausgabe der Daten pro Ticket.

Der Mail-Versand wird unterdrückt, wenn der Mustertext (ausschließlich) "IGNORE"
zurückgibt; es wird dann auch nichts in der Datenbank vermerkt.

=item -nomail

Damit keine E-Mails versendet, sondern nur auf der Standardausgabe angezeigt
weden.

=item -nicht-speichern

Damit nicht in der Datenbank vermerkt wird, dass für das Ticket mit diesem
Mustertext bereits alarmiert wurde.
Ein erneuter Aufruf mit denselben Parametern würde dann also im Regelfall
erneut dieselben Alarme erzeugen.

=item -unbedingt

Damit nicht in der Datenbank geschaut wird, ob für das Ticket mit diesem
Mustertext bereits alarmiert wurde, also ggf. trotzdem (nochmal) alarmiert
wird.
In diesem Fall wird der alte durch den neuen Alarmierungseintrag überschrieben,
sofern nicht gleichzeitig L</-nicht-speichern> verwendet wird.

=item -help

=item -?

um (nur) diese Dokumentation anzeigen zu lassen

=back

=head1 BUGS

Folgende ehemaligen Features fielen, nachdem sie laut Startlog bzw. den in der
Datenbank gespeicherten Templates aktuell nicht benötigt wurden, erstmal der
Umstellung auf die Ticket-API zum Opfer:

=over 4

=item *

Option C<-debug-sql>;
bitte bei Bedarf stattdessen den L<noris::Ticket::API/Debug-Modus> nutzen!

=item *

Optionen C<-kunden-prio> und C<-ohne-kunden-prio>

=item *

Option C<-confitem> und C<-ohne-confitems>

=item *

Ticket-Attribut C<confitem>

=item *

Ticket-Attribut C<termin>

=item *

Kombination der Optionen L</-termin-ab> und L</-termin-vor> einerseits mit
L</-ohne-termin> andererseits

=back

