#!/usr/bin/perl -w

##############################################################
# Dieses Tool bitte _nicht_ mehr supporten!                  #
# Wer damit ein Problem hat, soll bitte "tickets" verwenden. #
##############################################################

use utf8;
use strict;
use warnings;
use utf8;

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

use FindBin;
warn <<_ if -t STDERR;
=====================================================================
$FindBin::Script wird demnaechst sterben.
Bitte auf $FindBin::Bin/tickets umstellen!
Details siehe
https://intra.office.noris.de/wiki/index.php/Kunde/Tool_%22tickets%22
=====================================================================
_

use Dbase::Getopt qw(
  :DEFAULT
  :kunden
  getopt_abt_like
  getopt_date
  getopt_person
);
use Dbase::Globals qw(if_defined kpersinfo name_kunde time4ticket);
use Dbase::Help qw(:readonly DoSelect DoTrans in_list isotime unixtime);
use List::Util qw(min);
use noris::CSV;
use noris::Ticket::API qw(get_pooled_connection in_out_list link_list);
use Umlaut qw(textmodus);

use constant OHNE_MIT => map "Arbeitszeit $_ Faktoren (s)", qw(ohne mit);

sub empty_unless_defined {
    my $value = shift;
    defined $value && $value;
}

sub flatten_list {
    my $value = shift;
    defined $value && join ' ', sort { lc $a cmp lc $b } @$value;
}

sub seconds_since_incident_start {
    my ( $value, $ticketdaten ) = @_;
    $value && $ticketdaten->{incident_start}
      and unixtime($value) - unixtime( $ticketdaten->{incident_start} );
}

use constant TICKET_ATTRIBUTES => (
    [
        est_effort => 'geplante Arbeitszeit',
        sub { my $value = shift; defined $value && $value * 3600 },
    ],
    [ owner     => 'Bearbeiter', ],
    [ priority  => 'Priorität', ],
    [ kunde     => 'Kunde', ],
    [ status    => 'Status', ],
    [ queue     => 'Queue', ],
    [ type      => 'Area', \&empty_unless_defined ],
    [ title     => 'Subject', ],
    [ info      => 'Info', ],
    [ created   => 'Entstehungszeitpunkt' ],
    [ confitems => 'Conf.Items', \&flatten_list ],
    [ leitungen => 'Leitungen', \&flatten_list ],
    [
        incident_response => 'Dauer bis Beginn der Störungsanalyse (s)',
        \&seconds_since_incident_start
    ],
    [
        incident_resolved => 'Wiederherstellungszeit (s)',
        \&seconds_since_incident_start
    ],
    [ incident_type => 'Störungsart', \&empty_unless_defined ],

    # Der Spaltenname "Störungsflags" ist historisch bedingt:
    # Im RT waren mal beliebig viele Flags zu Störungen vorgesehen.
    # Faktisch haben wir aber immer nur "sla-relevant" genutzt.
    # => Sollte man gelegentlich vielleicht mal umbenennen,
    # aber eher erst, wenn das RT abgeschafft ist.
    [
        sla_relevant => 'Störungsflags',
        sub { shift() ? 'sla-relevant' : '-' }
    ],

    # Wird selbst nicht ausgegeben, sondern nur zum Errechnen der "Dauer bis
    # Beginn der Störungsanalyse" und der "Wiederherstellungszeit" benötigt:
    ['incident_start'],
);

# Was wir von der API haben wollen:
use constant ATTRIBUTES => map $_->[0], TICKET_ATTRIBUTES;

# Bastel eine Referenz auf einen Hash, der für jedes Attribut eine Referenz
# auf eine Liste der zugehörigen Filter enthält. Sprich aus jedem
# [ attribut => 'Name', \&filter1, \&filter2, ... ] wird ein Hash-Eintrag
#   attribut => [ \&filter1, \&filter2, ... ]
use constant FILTER4ATTR =>
  { map +( $_->[0] => [ @{$_}[ 2 .. $#$_ ] ] ), TICKET_ATTRIBUTES };

use constant FOLLOW_BY => qw/main sub split_source split_target/;

die <<_ unless @ARGV;
Nein, ich werde jetzt _nicht_ die Arbeitszeit _aller_ Tickets ermitteln.
_

sub getopt_not_supported {
    die <<_;
Die Option -$_[0] wird nicht mehr unterstützt, nachdem sie länger nicht
verwendet wurde und ihre Umstellung auf die Ticket-API unverhältnismäßig
aufwändig gewesen wäre, vgl. Ticket 465235.
_
}

my (
    @Bearbeiter, $CustomerVisibility, @InfoLike,    $Locked,
    @OhneTypen,  $TicketsBis,         $TicketsSeit, @TitleLike,
    @Typen,      $Verbundene,         $ZeitSeit,    $ZeitVor,
);
my $CheckKunde = my $Summenzeile = 1;

sub set_lock($) {
    my ($lock_wanted) = @_;
    die "Was nun: nur freie oder nur gesperrte Tickets?\n"
      if defined $Locked && $Locked ne $lock_wanted;
    $Locked = $lock_wanted;
}

GetOptions(
    'ticket=i'       => \my @TicketIDs,
    'ohne-ticket=i'  => \my @OhneTicketIDs,
    'tickets-seit=s' => sub { $TicketsSeit = isotime( &getopt_date, 1 | 2 ) },
    'tickets-vor=s' => sub { $TicketsBis = isotime( &getopt_date - 1, 1 | 2 ) },
    'zeit-seit=s'                         => sub { $ZeitSeit   = &getopt_date },
    'zeit-vor=s'                          => sub { $ZeitVor    = &getopt_date },
    'zeit-fuer-ausgewaehlte-kunden'       => sub { $CheckKunde = 1 },
    'zeit-fuer-alle-kunden|nocheck-kunde' => sub { $CheckKunde = '' },
    'conf-item=s'      => \my @ConfItems,
    'ohne-conf-item=s' => \my @OhneConfItems,
    'leitung=s'        => \my @Leitungen,
    'ohne-leitung=s'   => \my @OhneLeitungen,
    'antwort=s%'       => \&getopt_not_supported,
    'bearbeiter=s' =>
      sub { push @Bearbeiter, map kpersinfo($_), &getopt_person },
    'ohne-bearbeiter=s' => sub {
        die <<_;
Die Option -$_[0] wird nicht mehr unterstützt, vgl. Ticket #10074640.
_
    },
    'nur-freie-tickets'     => sub { set_lock('unlock') },
    'nur-gesperrte-tickets' => sub { set_lock('lock') },
    'abteilung-like=s' =>
      sub { push @Bearbeiter, map kpersinfo($_), &getopt_abt_like },
    'max-priority=i' => \my $MaxPriority,
    'min-priority=i' => \my $MinPriority,
    'status=s'       => \my @Status,
    'ohne-status=s'  => \my @OhneStatus,
    'queue=s'        => \my @Queues,
    'ohne-queue=s'   => \my @OhneQueues,
    'area=s'         => \@Typen,
    'ohne-area=s'    => \@OhneTypen,
    'typ=s'          => \@Typen,
    'ohne-typ=s'     => \@OhneTypen,
    'info-like=s'    => \@InfoLike,
    'subject-like=s' => \@TitleLike,
    'title-like=s'   => \@TitleLike,
    'nur-sichtbare'  => sub {
        $CustomerVisibility = 'visible';
        warn <<_;
Die Re-Implementation der Option -$_[0] steht nach Umstellung
auf die Ticket-API noch aus, vgl. Ticket 10084205.
_
    },
    'min-sollzeit=f'     => \&getopt_not_supported,
    'ohne-sollzeit'      => \&getopt_not_supported,
    'addiere-verbundene' => sub { $Verbundene = '' },
    'zeige-verbundene'   => sub { $Verbundene = 1 },
    'verbindungsart=s'   => \my @Verbindungsarten,
    'aufrunden=i'        => \my $Aufrunden,
    'summenzeile'        => sub { $Summenzeile = 1 },
    'keine-summenzeile'  => sub { $Summenzeile = '' },
    'csv-option=s'       => \my %CSV_Options,
    'debug+'             => \my $Debug,
);

$_ eq '' and $_ = undef for @Typen, @OhneTypen;
if ($Aufrunden) {
    require POSIX and POSIX->import('ceil');
    $Aufrunden *= 60;
}

push @OhneStatus, 'merged' unless grep $_ eq 'merged', @Status, @OhneStatus;

@Verbindungsarten = FOLLOW_BY if defined $Verbundene && !@Verbindungsarten;

sub effective_tickets {
    return unless @_;
    my %effective_id;
    @effective_id{@_} = ();
    get_pooled_connection()->select_tickets(
        attributes => [qw/ticket_number merge_root/],
        query      => {
            status        => 'merged',
            ticket_number => [ in => @_ ],
        },
      )->foreach_row(
        sub {
            my ( $ticket_number, $merge_root ) = @_;
            delete $effective_id{$ticket_number};
            $effective_id{$merge_root} = undef;
        }
      );
    keys %effective_id;
}

sub zeitwerte($) {
    my ($ticket) = @_;
    map sprintf( '%.f', $_ ), @{$ticket}{ (OHNE_MIT) };
}

textmodus( \*STDOUT );
my $csv = noris::CSV->new( \%CSV_Options );
print $csv->as_decoded_string(
    defined $Verbundene ? 'Ticketgruppe' : (),
    Ticket => OHNE_MIT,
    grep defined, map $_->[1], TICKET_ATTRIBUTES
);

sub arbeitszeit4ticket($) {
    my ($ticket_number) = @_;
    time4ticket( $ticket_number, $ZeitSeit, $ZeitVor,
        $CheckKunde ? ( \@Kunden, \@OhneKunden ) : () );
}

DoTrans {

    # Nicht unnötig Tickets ohne einschlägige Zeitbuchung auswählen.
    # Wenn eh schon nur nach speziellen Ticket-IDs gefragt wird,
    # sparen wir uns diese Optimierung aber.
    # Und wenn -*-verbundene gefragt sind, können wir sie nicht machen, weil es
    # sein könnte, dass zwar auf ein einschlägiges Ticket selbst im fraglichen
    # Zeitraum keine Arbeitszeit gebucht wurde, aber auf ein damit verbundenes.
    if ( !@TicketIDs && !defined $Verbundene
        and defined $ZeitSeit || defined $ZeitVor )
    {
        my @where = 'stunden.ticket IS NOT NULL';
        push @where, "stunden.beginn >= $ZeitSeit" if defined $ZeitSeit;
        push @where, "stunden.beginn + stunden.dauer < $ZeitVor"
          if defined $ZeitVor;
        push @where, in_list( 'stunden.kunde', '', @Kunden ),
          in_list( 'stunden.kunde', NOT => @OhneKunden )
          if $CheckKunde;

        DoSelect { push @TicketIDs, shift }
        'SELECT DISTINCT ticket FROM stunden WHERE ' . join ' AND ', @where;
    }

    # TODO: Testen, ob -ohne-*, insb. -ohne-kunden, funktioniert.
    # TODO: Doku überarbeiten
    my %query = (
        link_list( confitems => [ \@ConfItems ], [ \@OhneConfItems ] ),
        created => [ range => $TicketsSeit, $TicketsBis ],
        if_defined( customer_visibility => $CustomerVisibility ),
        in_out_list(
            kunde => [ map name_kunde($_), @Kunden ],
            [ map name_kunde($_), @OhneKunden ]
        ),
        link_list( leitungen => \@Leitungen, \@OhneLeitungen ),
        if_defined( locked => $Locked ),
        in_out_list( owner => \@Bearbeiter, [] ),
        priority => [ range => $MinPriority, $MaxPriority ],
        in_out_list( queue  => \@Queues, \@OhneQueues ),
        in_out_list( status => \@Status, \@OhneStatus ),
        in_out_list(
            ticket_number => [ effective_tickets(@TicketIDs) ],
            [ effective_tickets(@OhneTicketIDs) ]
        ),
        in_out_list( type => \@Typen, \@OhneTypen ),
    );
    $query{info}  = [ like => @InfoLike ]  if @InfoLike;
    $query{title} = [ like => @TitleLike ] if @TitleLike;

    my $ticket_api = get_pooled_connection();
    my $resultset  = $Verbundene
      ? $ticket_api->select_tickets( query => \%query )->follow_tickets(
        attributes => [ ticket_number => ATTRIBUTES, 'origin' ],
        follow_by  => \@Verbindungsarten,
      )
      : $ticket_api->select_tickets(
        attributes => [ ticket_number => ATTRIBUTES ],
        query      => \%query,
      );

    # Einlesen der primären Ticket-Datensätze, noch unsortiert:
    my %ticket;
    $resultset->foreach_row(
        sub {
            my %ticketdaten;
            my ($ticket_number) =
              ( @ticketdaten{ ticket_number => ATTRIBUTES, 'origin' } ) = @_;
            die "Doppeltes Ticket #$ticket_number!?\n"
              if exists $ticket{$ticket_number};

            # Nachbearbeitung der Daten:
            for (ATTRIBUTES) {
                my $value = $ticketdaten{$_};

                # vorsorglich generell: Zeilenumbrüche entfernen:
                if ( defined $value ) {
                    s#$/# #g for ref $value ? @$value : $value;
                }

                $value = $_->( $value, \%ticketdaten )
                  for @{ FILTER4ATTR->{$_} };
                $ticketdaten{$_} = $value;
            }

            @ticketdaten{ (OHNE_MIT) } = arbeitszeit4ticket($ticket_number);

            $ticket{$ticket_number} = \%ticketdaten;
        }
    );

    # -addiere-verbundene:
    $resultset->follow_tickets(
        attributes => [qw/ticket_number origin/],
        follow_by  => \@Verbindungsarten,
      )->foreach_row(
        sub {
            my ( $ticket_number, $origin ) = @_;

           # Dieses ResultSet enthält auch die ursprünglichen Tickets nochmal;
           # deren Arbeitszeit ist aber schon berücksichtigt, nur der origin
           # noch nicht gesetzt.
            if ( exists $ticket{$ticket_number} ) {
                $ticket{$ticket_number}{origin} = $origin;
            }
            else {
                my %time4ticket;
                @time4ticket{ (OHNE_MIT) } = arbeitszeit4ticket($ticket_number);
                $_ = $_ / @$origin for values %time4ticket;

                print "Zeitverteilung: $ticket_number => "
                  . join( '/', sort @$origin ) . ' ('
                  . @$origin . ")\n"
                  if $Debug;

                for my $base_ticket_number (@$origin) {
                    die <<_ unless exists $ticket{$base_ticket_number};
Ticket #$base_ticket_number im Basis-Ergebnis nicht gefunden!
_
                    $ticket{$base_ticket_number}{$_} += $time4ticket{$_}
                      for OHNE_MIT;
                }
            }
        }
      ) if defined $Verbundene && !$Verbundene;

    if ($Aufrunden) {
        for my $ticket ( values %ticket ) {
            $_ = $Aufrunden * ceil( $_ / $Aufrunden )
              for @{$ticket}{ (OHNE_MIT) };
        }
    }

    # Traditionell gibt dieses Tool für Tickets, die zu mehreren anderen
    # verlinkt sind, nur die jeweils kleinste Nummer aus:
    $_->{origin} = defined $Verbundene ? min( @{ $_->{origin} } ) : 0
      for values %ticket;

    my %time4tickets = map +( $_ => 0 ), OHNE_MIT;
    for my $ticket_number (
        sort { $ticket{$a}{origin} <=> $ticket{$b}{origin} || $a <=> $b }
        keys %ticket
      )
    {

        my $ticket = $ticket{$ticket_number};

        $time4tickets{$_} += $ticket->{$_} for OHNE_MIT;

        print $csv->as_decoded_string(
            defined $Verbundene ? $ticket->{origin} : (),
            $ticket_number, zeitwerte($ticket), @{$ticket}{ (ATTRIBUTES) } )
          if !defined $ZeitSeit && !defined $ZeitVor
          || grep $ticket->{$_} != 0, OHNE_MIT;
    }

    print $csv->as_decoded_string( ('') x ( defined $Verbundene ? 2 : 1 ),
        zeitwerte( \%time4tickets ) )
      if $Summenzeile;
};

__END__

=head1 NAME

arbeitszeit4tickets - werte auf Tickets gebuchte Arbeitszeit aus

=head1 BESCHREIBUNG

Gibt eine CSV-Liste aller Tickets aus, auf die (ggf. im gewünschten Zeitraum)
Arbeitszeit gebucht wurde.

=head1 OPTIONEN

=over 4

=item -ticket ID

Selektion nach Ticket-ID(s); bitte die Option mehrfach verwenden, um mehrere
Tickets auszuwählen!
Funktioniert auch für Tickets, die mit anderen Tickets zusammengefasst wurden.

=item -ohne-ticket ID

Selektion nach Ticket-ID(s); bitte die Option mehrfach verwenden, um mehrere
Tickets auszuschließen!
Funktioniert auch für Tickets, die mit anderen Tickets zusammengefasst wurden.

=item -tickets-seit Datum

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

=item -tickets-vor Datum

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

=item -zeit-seit Datum

nur Zeiterfassungseinträge ab dem angegebenen Datum beachten.
Bei Verwendung dieser Option werden nur Tickets selektiert, auf die im
fraglichen Zeitraum mindestens ein Zeiterfassungseintrag gebucht wurde.
Das Datum wird mit L<Time::ParseDate> analyisiert und kann (also) auch relativ
angegeben werden.

=item -zeit-vor Datum

nur Zeiterfassungseinträge vor dem angegebenen Datum beachten.
Bei Verwendung dieser Option werden nur Tickets selektiert, auf die im
fraglichen Zeitraum mindestens ein Zeiterfassungseintrag gebucht wurde.
Das Datum wird mit L<Time::ParseDate> analyisiert und kann (also) auch relativ
angegeben werden.

=item -zeit-fuer-ausgewaehlte-kunden

nur Zeiterfassungseinträge beachten, die auf einen der mit L</-kunde> und/oder
L</-ohne-kunde> ausgewählten Kunden gebucht wurden (Default).

=item -zeit-fuer-alle-kunden

Zeiterfassungseinträge unabhängig vom Kunden, auf den sie gebucht wurden,
beachten

=item -bearbeiter Person

Selektion nach Ticket-Bearbeiter(n); bitte die Option mehrfach verwenden, um
Tickets unterschiedlicher Bearbeiter auszuwählen!

=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 -nur-freie-tickets

=item -nur-gesperrte-tickets

um nur freie bzw. nur gesperrte Tickets auszuwählen
(aus RT-Sicht sind das nur Tickets ohne bzw. mit Bearbeiter)

Die beiden Optionen schließen sich gegenseitig aus.

=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 ausgewertet.
Dies lässt sich vermittels dieser Optionen modifizieren.
Details dazu siehe L<Dbase::Getopt/:kunden>.

=item -queue Queue

Selektion nach Queue(s); bitte die Option mehrfach verwenden,
um mehrere Queues zu selektieren!

=item -ohne-queue Queue

Ausschluss von Queue(s); bitte die Option mehrfach verwenden,
um mehrere Queues auszuschließen!

=item -area Typ

Selektion nach Bereich (RT) / Ticket-Typ (OTRS); bitte die Option
mehrfach verwenden, um mehrere Bereiche/Typen auszuwählen!
Um Tickets ohne Bereich/Typ auszuwählen, bitte eine leere
Zeichenkette als Argument übergeben!

=item -ohne-area Area

Ausschluss von Bereichen/Ticket-Typen; bitte die Option mehrfach
verwenden, um mehrere Bereiche/Typen auszuschließen!  Um Tickets
ohne Area auszuschließen, bitte eine leere Zeichenkette als
Argument übergeben!

=item -info-like Wildcard

Selektion nach Ticket-Infotext.
Bei mehrfacher Verwendung der Option werden alle Tickets selektiert, deren
Infotext zu mindestens einer der angegebenen Wildcards passt.

=item -title-like Wildcard

Selektion nach Ticket-Titel.
Bei mehrfacher Verwendung der Option werden alle Tickets selektiert, deren
Subject zu mindestens einer der angegebenen Wildcards passt.

=item -status open

=item -status stalled

=item -status resolved

=item -status dead

nur Tickets beachten, die aktuell einen entsprechenden Status haben.
Kann mehrfach verwendet werden, um Tickets mit unterschiedlichen Status zu
beachten.

=item -ohne-status open

=item -ohne-status stalled

=item -ohne-status resolved

=item -ohne-status dead

Tickets, die aktuell einen entsprechenden Status haben, ausschließen.
Kann mehrfach verwendet werden, um Tickets mit unterschiedlichen Status
auszuschließen.

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

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

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

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

=item -nur-sichtbare

Nur Tickets auswählen, die für den Kunden (auf service.noris.net) freigegeben
sind.

=item -conf-item Conf.Item

Selektion nach den Tickets zugeordneten Configuration Items, also nur Tickets
auswählen, denen mindestens eines der angegebenen Configuration Items zugeordnet
ist

=item -ohne-conf-item Conf.Item

Ausschluss von Tickets, denen mindestens eines der angegebenen Configuration
Items zugeordnet ist

=item -leitung Leitungsname

Selektion nach den Tickets zugeordneten Leitungen, also nur Tickets auswählen,
denen mindestens eine der angegebenen Leitnugen zugeordnet ist

=item -ohne-leitung Leitungsname

Ausschluss von Tickets, denen mindestens eine der angegebenen Leitungen
zugeordnet ist

=item -zeige-verbundene

mit den ausgewählten Tickets verbundene Tickets ebenfalls anzeigen.
Es wird außerdem eine zusätzliche Spalte C<Ticketgruppe> angezeigt,
die für alle miteinander verbundenen Tickets dieselbe Kennnummer enthält.

Schließt L</-addiere-verbundene> aus.

=item -addiere-verbundene

auf mit ausgewählten Tickets verbundene Tickets gebuchte Arbeitszeit zu den
ausgewählten Tickets addieren.
Arbeitszeit von Tickets, die mit mehreren ausgewählten Tickets verbunden sind,
wird dabei gleichmäßig auf diese Tickets verteilt; die Summe wird anschließend
auf ganze Sekunden gerundet.

Schließt L</-zeige-verbundene> aus.

=item -verbindungsart Richtung

Diese Option ist (nur) in Zusammenhang mit L</-addiere-verbundene> oder
L</-zeige-verbundene> sinnvoll, um nur bestimmte Ticket-Verlinkungsarten zu
verfolgen, vgl. L<noris::Ticket::API/follow_by>.

Beispiel (um nicht "rückwärts" zu suchen):

    -verbindungsart sub -verbindungsart split_target

Default ist: C<main>, C<sub>, C<split_source> und C<split_target>

=item -aufrunden Minuten

Zeiteintragungen pro Ticket(gruppe) in den angegebenen Minutenschritten
aufrunden

=item -keine-summenzeile

um die abschließende Summenzeile der Arbeitszeiten zu unterbinden

=item -csv-option Name=Wert

Optionen für L<Text::CSV_XS>, s. Dokumentation dieses Moduls

=item -debug

Zu Debugging-Zwecken zusätzliche Informationen 
(über die Verteilung von Arbeitszeit bei L</-addiere-verbundene>) ausgeben.

=item -help

=item -?

um (nur) diese Dokumentation anzeigen zu lassen

=back
