The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package App::TimeTracker::Command::RT;
use strict;
use warnings;
use 5.010;

# ABSTRACT: App::TimeTracker RT plugin
use App::TimeTracker::Utils qw(error_message warning_message);

use Moose::Role;
use RT::Client::REST;
use RT::Client::REST::Ticket;
use Try::Tiny;
use Unicode::Normalize;

has 'rt' => (
    is            => 'rw',
    isa           => 'TT::RT',
    coerce        => 1,
    documentation => 'RT: Ticket number',
    predicate     => 'has_rt'
);
has 'rt_client' => (
    is         => 'ro',
    isa        => 'Maybe[RT::Client::REST]',
    lazy_build => 1,
    traits     => ['NoGetopt'],
    predicate  => 'has_rt_client'
);
has 'rt_ticket' => (
    is         => 'ro',
    isa        => 'Maybe[RT::Client::REST::Ticket]',
    lazy_build => 1,
    traits     => ['NoGetopt'],
);

sub _build_rt_ticket {
    my ($self) = @_;

    if ( my $ticket = $self->init_rt_ticket( $self->_current_task ) ) {
        return $ticket;
    }
}

sub _build_rt_client {
    my $self   = shift;
    my $config = $self->config->{rt};

    unless ($config) {
        error_message("Please configure RT in your TimeTracker config");
        return;
    }

    return try {
        my $client = RT::Client::REST->new(
            server  => $config->{server},
            timeout => $config->{timeout},
        );
        $client->login(
            username => $config->{username},
            password => $config->{password} );
        return $client;
    }
    catch {
        error_message("Could not log in to RT: $_");
        return;
    };
}

before [ 'cmd_start', 'cmd_continue', 'cmd_append' ] => sub {
    my $self = shift;
    return unless $self->has_rt;

    my $ticketname = 'RT' . $self->rt;
    $self->insert_tag($ticketname);

    my $ticket;
    if ( $self->rt_client ) {
        $ticket = $self->rt_ticket;
        if ( defined $ticket ) {
            if ( defined $self->description ) {
                $self->description(
                    sprintf(
                        '%s (%s)', $self->description, $ticket->subject
                    ) );
            }
            else {
                $self->description( $ticket->subject );
            }
        }
    }

    if ( $self->meta->does_role('App::TimeTracker::Command::Git') ) {
        my $branch = $ticketname;
        if ($ticket) {
            my $subject = $self->safe_ticket_subject( $ticket->subject );
            $branch .= '_' . $subject;
        }
        $self->branch($branch) unless $self->branch;
    }
};

after [ 'cmd_start', 'cmd_continue', 'cmd_append' ] => sub {
    my $self = shift;
    return unless $self->has_rt && $self->rt_client;

    my $ticket = $self->rt_ticket;

    return unless $ticket;
    try {
        my $do_store = 0;
        if ( $self->config->{rt}{set_owner_to} ) {
            if (    $ticket->owner() ne 'Nobody'
                and $ticket->owner() ne $self->config->{rt}{set_owner_to} )
            {
                warning_message(
                    'Will not steal tickets, please do that via RT Web-UI');
                return;
            }
            $ticket->owner( $self->config->{rt}{set_owner_to} );
            $do_store = 1;
        }

        my $status = $self->config->{rt}{set_status}{start};
        if ( $status and $status ne $ticket->status ) {
            $ticket->status($status);
            $do_store = 1;
        }
        $ticket->store() if $do_store;
    }
    catch {
        error_message( 'Could not set RT owner/status: %s', $_ );
    };
};

after 'cmd_stop' => sub {
    my $self = shift;
    return unless $self->rt_client;

    my $task = $self->_previous_task;
    return unless $task;
    my $task_rounded_minutes = $task->rounded_minutes;
    return unless $task_rounded_minutes > 0;

    my $ticket = $self->init_rt_ticket($task);
    if ( not $ticket ) {
        say
            "Last task did not contain a RT ticket id, not updating TimeWorked or Status.";
        return;
    }

    my $do_store = 0;
    if ( $self->config->{rt}{update_time_worked} and $task_rounded_minutes ) {

        my $worked = $ticket->time_worked || 0;
        $worked =~ s/\D//g
            ;  # RT stores in minutes, API give back a string like "x minutes"

        $ticket->time_worked( $worked + $task_rounded_minutes );
        $do_store = 1;
    }

    if ( $self->config->{rt}{update_time_left} and $ticket->time_left ) {
        my $time_left = $ticket->time_left;
        $time_left =~ s/\D//g
            ;  # RT stores in minutes, API give back a string like "x minutes"

        $ticket->time_left( $time_left - $task_rounded_minutes );
        $do_store = 1;
    }

    my $status = $self->config->{rt}{set_status}{stop};
    # Do not change the configured stop status if it has been changed since starting the ticket
    if ( defined $status
        and $ticket->status() eq $self->config->{rt}{set_status}{start} )
    {
        $ticket->status($status);
        $do_store = 1;
    }
    return unless $do_store;

    try {
        $ticket->store;
    }
    catch {
        error_message( 'Could not update ticket: %s', $_ );
    };
};

sub init_rt_ticket {
    my ( $self, $task ) = @_;
    my $id;
    if ($task) {
        $id = $task->rt_id;
    }
    elsif ( $self->rt ) {
        $id = $self->rt;
    }
    return unless $id;

    my $rt_ticket = RT::Client::REST::Ticket->new(
        rt => $self->rt_client,
        id => $id,
    );
    $rt_ticket->retrieve;
    return $rt_ticket;
}

sub App::TimeTracker::Data::Task::rt_id {
    my $self = shift;
    foreach my $tag ( @{ $self->tags } ) {
        next unless $tag =~ /^RT(\d+)/;
        return $1;
    }
}

sub safe_ticket_subject {
    my ( $self, $subject ) = @_;

    $subject = NFKD($subject);
    $subject =~ s/\p{NonspacingMark}//g;
    $subject =~ s/\W/_/g;
    $subject =~ s/_+/_/g;
    $subject =~ s/^_//;
    $subject =~ s/_$//;
    return $subject;
}

no Moose::Role;
1;

__END__

=pod

=head1 NAME

App::TimeTracker::Command::RT - App::TimeTracker RT plugin

=head1 VERSION

version 2.019

=head1 DESCRIPTION

This plugin takes a lot of hassle out of working with Best Practical's
RequestTracker available for free from
L<http://bestpractical.com/rt/>.

It can set the description and tags of the current task based on data
entered into RT, set the owner of the ticket and update the
time-worked as well as time-left in RT. If you also use the C<Git> plugin, this plugin will
generate very nice branch names based on RT information.

=head1 CONFIGURATION

=head2 plugins

Add C<RT> to the list of plugins. 

=head2 rt

add a hash named C<rt>, containing the following keys:

=head3 server [REQUIRED]

The server name RT is running on.

=head3 username [REQUIRED]

Username to connect with. As the password of this user might be distributed on a lot of computer, grant as little rights as needed.

=head3 password [REQUIRED]

Password to connect with.

=head3 timeout

Time in seconds to wait for an connection to be established. Default: 300 seconds (via RT::Client::REST)

=head3 set_owner_to

If set, set the owner of the current ticket to the specified value during C<start> and/or C<stop>.

=head3 update_time_worked

If set, updates the time worked on this task also in RT.

=head3 update_time_left

If set, updates the time left property on this task also in RT using the time worked tracker value.

=head1 NEW COMMANDS

none

=head1 CHANGES TO OTHER COMMANDS

=head2 start, continue

=head3 --rt

    ~/perl/Your-Project$ tracker start --rt 1234

If C<--rt> is set to a valid ticket number:

=over

=item * set or append the ticket subject in the task description ("Rev up FluxCompensator!!")

=item * add the ticket number to the tasks tags ("RT1234")

=item * if C<Git> is also used, determine a save branch name from the ticket number and subject, and change into this branch ("RT1234_rev_up_fluxcompensator")

=item * set the owner of the ticket in RT (if C<set_owner_to> is set in config)

=item * updates the status of the ticket in RT (if C<set_status/start> is set in config)

=back

=head2 stop

If <update_time_worked> is set in config, adds the time worked on this task to the ticket.
If <update_time_left> is set in config, reduces the time left on this task to the ticket.
If <set_status/stop> is set in config, updates the status of the ticket

=head1 AUTHOR

Thomas Klausner <domm@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2011 by Thomas Klausner.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut