package RTTicketSearch;

=head1 NAME

RTTicketSearch -- functions for searching RT tickets.

=head1 SYNOPSIS

 use RTTicketSearch;

 my $ts = RTTicketSearch->new($server, $username)->login($password);

 my @tickets = $ts->get_tickets_sorted("Queue = 'foo' AND Status = 'Open'", ['+Owner', '+LastUpdated']);

 print $tickets[0]->{Subject};

=head1 USAGE NOTES

You will most likely be using the C<get_tickets> or C<get_tickets_sorted> functions with a query string to get a list of hashes.
Each hash contains the fields of a single ticket.
Some of the fields are C<id>, C<Status>, C<Owner>, C<Creator>, C<Subject>, C<LastUpdated>. (Case matters).

=head2 QUERY STRING FORMAT

An RT query is pretty much the WHERE clause of an SQL query.
You will generally want to restrict the queue you're looking at, with an expression like C<Queue = 'foo'>.
Dates and times can also be specified in a relative format such as "6 days ago".
Note that this may lead to counter-intuitive queries: for example, C<Created E<lt> '6 days ago'> actually means "more than 6 days old", since it is comparing the timestamp in the C<Created> field against the timestamp specified by the string "6 days ago".

=cut

use warnings;
use strict;
use lib '/p/condor/software/RT-Client-REST-0.41/lib';
use RT::Client::REST;
use Error qw(:try);
use Carp;

=head1 METHODS

=over

=item new ($rt_server, $rt_user)

Constructor. C<$rt_server> is the URL of the server, and C<$rt_user> is the username to log in with.

=cut

sub new {
    my $class = shift;
    my ($rt_server, $rt_user) = @_;
    $class = ref($class) || $class;
    my $self = {rt_server => $rt_server, rt_user => $rt_user};
    bless $self, $class;
    return $self;
}


=item login($rt_password)

Log in to the RT server (field C<rt_server>) using the username in C<rt_user> and the password in C<$rt_password>.

The C<rt> field will contain an C<RT::Client::REST> object that the rest of the methods will use to communicate with RT.

=cut

sub login {
    my $self = shift;
    my ($rt_password) = @_;
    $self->{rt} = RT::Client::REST->new(server  => $self->{rt_server},
                                        timeout => 60);

    try {
        $self->{rt}->login(username => $self->{rt_user}, password => $rt_password);
    }
    catch Exception::Class::Base with {
        croak "Problem logging in: ", shift->message;
    };

    return $self;
}

=item safe_search(%params)

Wrapper around C<RT::Client::REST::search> with friendlier error messages.
Example of use:

  @ids = $foo->safe_search(type=>'ticket', query=>"Status = 'new'");

See L<RT::Client::REST> for more information

=cut

sub safe_search {
    my $self = shift;
    my %params = @_;
    my $rt = $self->{rt};

    my @ids;
    try {
        @ids = $rt->search(%params);
    }
    catch RT::Client::REST::UnauthorizedActionException with {
        croak 'Not authorized to search';
    }
    catch RT::Client::REST::Exception with {
        croak "Error occurred while searching: $@";
    };
    return @ids;
}



=item safe_show(%params)

Wrapper around C<RT::Client::REST::show> with friendlier error messages.
Example of use:

  @tickets = $foo->safe_show(type=>'ticket', id=>"ticket/319");

Also adds a URL field to each ticket with the URL to view the ticket in the web interface.

See L<RT::Client::REST> for more information

=cut

sub safe_show {
    my $self = shift;
    my %params = @_;
    my $rt = $self->{rt};

    my @tickets;
    try {
        @tickets = $rt->show(%params);
    }
    catch RT::Client::REST::UnauthorizedActionException with {
        croak 'Not authorized to view tickets';
    }
    catch RT::Client::REST::Exception with {
        croak "Error occurred while viewing tickets: $@";
    };
    for (@tickets) {
        (my $idnum = $_->{id}) =~ s{ticket/}{};
        $_->{URL} = "$self->{rt_server}/Ticket/Display.html?id=$idnum";
    }
    return @tickets;
}


=item safe_search_tickets_by_query ($query)

Convenience wrapper around C<safe_search> to return tickets matching the query C<$query>.
Like C<safe_search>, returns a list of ids.
B<Note:> an id is actually a string like C<'ticket/319'> instead of just a number.

=cut


sub safe_search_tickets_by_query {
    my $self = shift;
    my ($query) = @_;
    return $self->safe_search(type=>'ticket', query=>$query);
}


=item safe_show_tickets_by_id ($id)

Convenience wrapper around C<safe_show> to return tickets matching the id C<$id>.
Like C<safe_show>, returns a list of ticket objects (list of hashes), although this is usually a singleton list since only one ticket should match any given id.
B<Note:> an id is actually a string like C<'ticket/319'> instead of just a number, but this method adds the 'ticket/' if you forget.

=cut


sub safe_show_tickets_by_id {
    my $self = shift;
    my ($id) = @_;
    if ($id =~ m{ticket/}) {
        $id = "ticket/$id";
    }
    return $self->safe_show(type=>'ticket', id=>$id);
}


=item verify_queues (@queues)

Dies with an error if a query on all tickets in any of the RT queues named by C<@queues> returns nothing.
This I<usually> happens because the RT user does not have both the C<SeeQueue> and C<ShowTicket> permissions for that queue, but it can also happen if the queue is brand new and has nothing in it.
Unfortunately I haven't found a way to distinguish between those two cases.

=cut

sub verify_queues {
    my $self = shift;
    my @queues = @_;

    foreach my $q (@queues) {
        my $query = "(Queue = '$q')";

        my @ids = $self->safe_search(type=>'ticket', query=>$query);
        if (!@ids) {
            carp "Unable to find any tickets in queue $q: verify that $self->{rt_user} has SeeQueue and ShowTicket permissions for that queue.\n";
        }
    }
}


=item get_tickets ($query)

Returns a list of ticket objects matching the query given by C<$query>.

=cut


sub get_tickets {
    my $self = shift;
    my ($query) = @_;

    my @ids = $self->safe_search_tickets_by_query($query);
    my @tickets;
    foreach my $id (@ids) {
        next if ! $id;
        push @tickets, $self->safe_show_tickets_by_id($id);
    }

    return @tickets;
}


=item get_tickets_sorted ($query, $sortingref)

Like C<get_tickets> but also sorts the results according to C<$sortingref>.
C<$sortingref> is an arrayref containing the list of fields to sort by prefixed
by C<+> or C<-> for ascending or descending.
For example, C<['+Status', '+Owner']> sorts by the Status field first, and then by the Owner field if the Status fields are the same.

=cut

sub get_tickets_sorted {
    my $self = shift;
    my ($query, $sortingref) = @_;

    return sort { &cmp_tickets($sortingref, $a, $b) } $self->get_tickets($query);
}

=back

=head1 FUNCTIONS

=over

=item cmp_tickets ($sortingref, $ticketrefa, $ticketrefb)

A comparison function used with the sort builtin for sorting tickets.
See C<get_tickets_sorted> above for a description of C<$sortingref>.
C<$ticketrefa> and C<$ticketrefb> are references to tickets as returned by C<safe_show>.

=cut

sub cmp_tickets {
    my ($sortingref, $ticketrefa, $ticketrefb) = @_;

    my $cmp = 0;
    foreach my $s (@$sortingref) {
        my ($order, $field) = $s =~ /^([+-]?)(\w+)/;
        next if ! $field;
        $order ||= '+';

        if ($order eq '+') {
            $cmp ||= ($ticketrefa->{$field} || '') cmp ($ticketrefb->{$field} || '');
        }
        elsif ($order eq '-') {
            $cmp ||= ($ticketrefb->{$field} || '') cmp ($ticketrefa->{$field} || '');
        }
    }

    return $cmp;
}

=back


=head1 AUTHOR

Matyas Selmeci <matyas@cs.wisc.edu>

=cut

1;

