The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package App::SD::Replica::rt::PullEncoder;
use Any::Moose; use strict;
extends 'App::SD::ForeignReplica::PullEncoder';

use Params::Validate qw(:all);
use Memoize;
use App::SD::Util;

has sync_source => 
    ( isa => 'App::SD::Replica::rt',
      is => 'rw');

sub ticket_id {
    my $self = shift;
    my $ticket = shift;
     return $ticket->{id};
}

sub translate_ticket_state {
    my $self   = shift;
    my $ticket = shift;
    my $transactions = shift;

    # undefine empty fields, we'll delete after cleaning
    $ticket->{$_} = undef for
        grep defined $ticket->{$_} && $ticket->{$_} eq '',
        keys %$ticket;

    $ticket->{'id'} =~ s/^ticket\///g;

    $ticket->{ $self->sync_source->uuid . '-' . lc($_) } = delete $ticket->{$_}
        for qw(Queue id);

    delete $ticket->{'Owner'} if lc($ticket->{'Owner'}) eq 'nobody';
    $ticket->{'Owner'} = $self->resolve_user_id_to( email_address => $ticket->{'Owner'} )
        if $ticket->{'Owner'};

    # normalize names of watchers to variant with suffix 's'
    foreach my $field (qw(Requestor Cc AdminCc)) {
        if ( defined $ticket->{$field} && defined $ticket->{$field .'s'} ) {
            die "It's impossible! Ticket has '$field' and '${field}s'";
        } elsif ( defined $ticket->{$field} ) {
            $ticket->{$field .'s'} = delete $ticket->{$field};
        }
    }

    for my $date (grep defined $ticket->{$_}, qw(Created Resolved Told LastUpdated Due Starts Started)) {
        my $dt = App::SD::Util::string_to_datetime($ticket->{$date});
        if ($dt) {
            $ticket->{$date} =  $dt->ymd('-')." ".$dt->hms(":");
        } else {
            delete $ticket->{$date}
        }
    }

    $ticket->{$_} =~ s/ minutes$//
        for grep defined $ticket->{$_}, qw(TimeWorked TimeLeft TimeEstimated);

    $ticket->{'Status'} =~ $self->translate_status($ticket->{'Status'});

    # delete undefined and empty fields
    delete $ticket->{$_} for
        grep !defined $ticket->{$_} || $ticket->{$_} eq '',
        keys %$ticket;

    return $ticket, {%$ticket};
}

=head2 find_matching_tickets query => QUERY

Returns an RT::Client ticket collection for all tickets found matching your QUERY string.

=cut

sub find_matching_tickets {
    my $self = shift;
    my %args = validate(@_,{query => 1});
    my $query = $args{query};
    # If we've ever synced, we can limit our search to only newer things
    if ( my $before = $self->_only_pull_tickets_modified_after ) {
       $query = "($query) AND LastUpdated >= '" . $before->ymd('-') . " " . $before->hms(':') . "'";
        $self->sync_source->log( "Skipping all tickets not updated since " . $before->iso8601 );
    }
    return [map {
        Prophet::CLI->end_pager();
        # squelch chatty RT::Client::REST "Unknown key" warnings unless debugging turned on
        local $SIG{__WARN__} = sub { $self->sync_source->log_debug(@_) };
        my $hash = $self->sync_source->rt->show( type => 'ticket', id => $_ );
        $hash->{id} =~ s|^ticket/||g;
        $hash
    } $self->sync_source->rt->search( type => 'ticket', query => $query )];
}


=head2 find_matching_transactions { ticket => $id, starting_transaction => $num }

Returns a reference to an array of all transactions (as hashes) on ticket $id after transaction $num.

=cut

sub find_matching_transactions {
    my $self = shift;
    my %args = validate( @_, { ticket => 1, starting_transaction => 1 } );
    my @txns;

    my $rt_handle = $self->sync_source->rt;

    my $ticket_id = $self->ticket_id( $args{ticket} );

    my $latest = $self->sync_source->app_handle->handle->last_changeset_from_source(
        $self->sync_source->uuid_for_remote_id($ticket_id) ) || 0;

    for my $txn ( sort $rt_handle->get_transaction_ids( parent_id => $ticket_id ) ) {

        # Skip things calling code told us to skip
        next if $txn < $args{'starting_transaction'};

        # skip things we had on our last pull
        next if $txn <= $latest;

        # Skip things we've pushed
        next if $self->sync_source->foreign_transaction_originated_locally( $txn, $ticket_id );
        my $txn_hash = $rt_handle->get_transaction( parent_id => $ticket_id, id => $txn, type => 'ticket' );
        if ( my $attachments = delete $txn_hash->{'Attachments'} ) {
            for my $attach ( split( /\n/, $attachments ) ) {
                next unless ( $attach =~ /^(\d+):/ );
                my $id = $1;
                my $a = $rt_handle->get_attachment( parent_id => $ticket_id, id => $id );

                push( @{ $txn_hash->{_attachments} }, $a ) if ( $a->{Filename} );

            }

        }
        push @txns,
            {
            timestamp => App::SD::Util::string_to_datetime( $txn_hash->{Created} ),
            serial    => $txn_hash->{id},
            object    => $txn_hash
            };
    }
    return \@txns;
}

sub transcode_one_txn {
    my ($self, $txn_wrapper, $ticket_initial_state, $ticket) = (@_);
   
    my $txn = $txn_wrapper->{object};

    my $sub = $self->can( '_recode_txn_' . $txn->{'Type'} );
    unless ( $sub ) {
        die "Transaction type $txn->{Type} (for transaction $txn->{id}) not implemented yet";
    }
    my $changeset = Prophet::ChangeSet->new(
        {   original_source_uuid => $self->sync_source->uuid_for_remote_id( $ticket->{ $self->sync_source->uuid . '-id' } ),
            original_sequence_no => $txn->{'id'},
            created =>  $txn->{'Created'},
            creator => $self->resolve_user_id_to( email_address => $txn->{'Creator'} ),
        }
    );

    if ( $txn->{'Ticket'} ne $ticket->{$self->sync_source->uuid . '-id'}
        && $txn->{'Type'} !~ /^(?:Comment|Correspond)$/
    ) {
        warn "Skipping a data change from a merged ticket " . $txn->{'Ticket'} .' vs '. $ticket->{$self->sync_source->uuid . '-id'} . "\n";
        return;
    }

    delete $txn->{'OldValue'} if ( $txn->{'OldValue'} eq '');
    delete $txn->{'NewValue'} if ( $txn->{'NewValue'} eq '');

    $sub->( $self, ticket => $ticket, txn          => $txn, changeset    => $changeset);
    $self->translate_prop_names($changeset);

    if (my $attachments = delete $txn->{'_attachments'}) {
       for my $attach (@$attachments) { 
            $self->_recode_attachment_create( ticket => $ticket, txn => $txn, changeset =>$changeset, attachment => $attach); 
       }
    }

    return $changeset;
}


{ # Recoding RT transactions

sub _recode_attachment_create {
    my $self   = shift;
    my %args   = validate( @_, { ticket => 1, txn => 1, changeset => 1, attachment => 1 } );
    my $change = Prophet::Change->new(
        {   record_type => 'attachment',
            record_uuid => $self->sync_source->uuid_for_url( $self->sync_source->remote_url . "/attachment/" . $args{'attachment'}->{'id'} ),
            change_type => 'add_file'
        }
    );
    $change->add_prop_change( name => 'content_type', old  => undef, new  => $args{'attachment'}->{'ContentType'});
    $change->add_prop_change( name => 'created', old  => undef, new  => $args{'txn'}->{'Created'} );
    $change->add_prop_change( name => 'creator', old  => undef, new  => $self->resolve_user_id_to( email_address => $args{'attachment'}->{'Creator'}));
    $change->add_prop_change( name => 'content', old  => undef, new  => $args{'attachment'}->{'Content'});
    $change->add_prop_change( name => 'name', old  => undef, new  => $args{'attachment'}->{'Filename'});
    $change->add_prop_change( name => 'ticket', old  => undef, new  => $self->sync_source->uuid_for_remote_id( $args{'ticket'}->{ $self->sync_source->uuid . '-id'} ));
    $args{'changeset'}->add_change( { change => $change } );
}

sub _recode_txn_Keyword {} # RT 2 - unused
sub _recode_txn_CommentEmailRecord { return; }

sub _recode_txn_EmailRecord     { return; }
sub _recode_txn_AddReminder     { return; }
sub _recode_txn_ResolveReminder { return; }
sub _recode_txn_DeleteLink      { }

sub _recode_txn_Status {
    my $self = shift;
    my %args = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );

    $args{txn}->{'Type'} = 'Set';
    return $self->_recode_txn_Set(%args);
}

sub _recode_txn_Told {
    my $self = shift;
    my %args = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );
    $args{txn}->{'Type'} = 'Set';
    return $self->_recode_txn_Set(%args);
}

sub _recode_txn_Set {
    my $self = shift;
    my %args = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );

    my $change = Prophet::Change->new(
        {   record_type => 'ticket',
            record_uuid => $self->sync_source->uuid_for_remote_id( $args{'ticket'}->{$self->sync_source->uuid . '-id'} ),
            change_type => 'update_file'
        }
    );

    my ($field, $old, $new) = @{ $args{txn} }{qw(Field OldValue NewValue)};

    if ( $field eq 'Queue' ) {
        my $current_queue = $args{'ticket'}->{$self->sync_source->uuid .'-queue'};
        my $user          = $args{txn}->{Creator};
        if ( $args{txn}->{Description} =~ /Queue changed from (.*) to $current_queue by $user/ ) {
            $old = $1;
            $new = $current_queue;
        }
    } elsif ( $field eq 'Status' ) {
        $new = $self->translate_status($new);
        $old = $self->translate_status($old); 

    } elsif ( $field eq 'Owner' ) {
        $new = $self->resolve_user_id_to( email_address => $new );
        $old = $self->resolve_user_id_to( email_address => $old );
    }

    $args{'changeset'}->add_change( { change => $change } );
    
    # XXX: This line is kind of magic
    # TODO: check if it's sill needed
    $args{'ticket'}->{ $field } = $old;

    $change->add_prop_change( name => $field, old => $old, new => $new );
}

*_recode_txn_Steal = \&_recode_txn_Set;
*_recode_txn_Take  = \&_recode_txn_Set;
*_recode_txn_Give  = \&_recode_txn_Set;

sub _recode_txn_Create {
    my $self = shift;
    my %args = validate( @_, {  txn => 1, ticket => 1, changeset => 1 } );

    my $change = Prophet::Change->new( {
        record_type => 'ticket',
        record_uuid => $self->sync_source->uuid_for_remote_id(
            $args{'ticket'}->{$self->sync_source->uuid . '-id'}
        ),
        change_type => 'add_file'
    } );

    $args{'changeset'}->add_change( { change => $change } );
    for my $name ( keys %{ $args{'ticket'} } ) {
        $change->add_prop_change(
            name => $name,
            old  => undef,
            new  => $args{'ticket'}->{$name},
        );
    }

    $self->_recode_content_update(%args);    # add the create content txn as a seperate change in this changeset
}

*_recode_txn_Link  = \&_recode_txn_AddLink;
sub _recode_txn_AddLink {
    # XXX, TODO: syncing links doesn't work
    return;

    my $self      = shift;
    my %args      = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );
    my $new_state = $args{'ticket'}->{ $args{'txn'}->{'Field'} };
    $args{'ticket'}->{ $args{'txn'}->{'Field'} } = $self->warp_list_to_old_value(
        $args{'ticket'}->{ $args{'txn'}->{'Field'} },
        $args{'txn'}->{'NewValue'},
        $args{'txn'}->{'OldValue'}
    );

    my $change = Prophet::Change->new( {
        record_type => 'ticket',
        record_uuid => $self->sync_source->uuid_for_remote_id(
            $args{'ticket'}->{$self->sync_source->uuid . '-id'}
        ),
        change_type => 'update_file',
    } );
    $change->add_prop_change(
        name => $args{'txn'}->{'Field'},
        old  => $args{'ticket'}->{ $args{'txn'}->{'Field'} },
        new  => $new_state
    );
    $args{'changeset'}->add_change( { change => $change } );
}

sub _recode_content_update {
    my $self = shift;
    my %args = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );
    my $url = $self->sync_source->remote_url . "/transaction/" . $args{'txn'}->{'id'};
    my $change = Prophet::Change->new( {
        record_type => 'comment',
        record_uuid => $self->sync_source->uuid_for_url( $url ),
        change_type => 'add_file',
    } );

    $change->add_prop_change( name => 'created', new  => $args{'txn'}->{'Created'});
    $change->add_prop_change( name => 'type',    new  => $args{'txn'}->{'Type'});
    $change->add_prop_change( name => 'creator', new  => $self->resolve_user_id_to(
        email_address => $args{'txn'}->{'Creator'}
    ) );
    $change->add_prop_change( name => 'content', new  => $args{'txn'}->{'Content'});
    $change->add_prop_change( name => 'ticket',  new  => $self->sync_source->uuid_for_remote_id(
        $args{'ticket'}->{ $self->sync_source->uuid . '-id'}
    ) );
    $args{'changeset'}->add_change( { change => $change } );
}

*_recode_txn_Comment    = \&_recode_content_update;
*_recode_txn_Correspond = \&_recode_content_update;

sub _recode_txn_AddWatcher {
    my $self = shift;
    my %args = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );

    my $type = $args{'txn'}->{'Field'};

    my $new_state = $args{'ticket'}->{ $type .'s' };
    $args{'ticket'}->{ $type .'s' } = $self->warp_list_to_old_value(
        $new_state,
        $self->resolve_user_id_to( email_address => $args{'txn'}->{'NewValue'} ),
        $self->resolve_user_id_to( email_address => $args{'txn'}->{'OldValue'} )
    );

    my $change = Prophet::Change->new({
        record_type   => 'ticket',
        record_uuid   => $self->sync_source->uuid_for_remote_id(
            $args{'ticket'}->{$self->sync_source->uuid . '-id'}
        ),
        change_type => 'update_file'
    } );
    $change->add_prop_change(
        name => $args{'txn'}->{'Field'},
        old  => $args{'ticket'}->{ $args{'txn'}->{'Field'} .'s' },
        new  => $new_state
    );
    $args{'changeset'}->add_change( { change => $change } );
}

*_recode_txn_DelWatcher = \&_recode_txn_AddWatcher;

sub _recode_txn_CustomField {
    my $self = shift;
    my %args = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );

    my $new = $args{'txn'}->{'NewValue'};
    my $old = $args{'txn'}->{'OldValue'};
    my $name;
    if ( $args{'txn'}->{'Description'} =~ /^(.*) $new added by/ ) {
        $name = $1;
    }
    elsif ( $args{'txn'}->{'Description'} =~ /^(.*) changed to $new by/ ) {
        $name = $1;

    } elsif ( $args{'txn'}->{'Description'} =~ /^(.*) $old deleted by/ ) {
        $name = $1;
    } else {
        die "Unknown transaction description " . $args{'txn'}->{'Description'};
    }

    $args{'txn'}->{'Field'} = "CF-" . $name;

    my $new_state = $args{'ticket'}->{ $args{'txn'}->{'Field'} };
    $args{'ticket'}->{ $args{'txn'}->{'Field'} } = $self->warp_list_to_old_value(
        $args{'ticket'}->{ $args{'txn'}->{'Field'} },
        $args{'txn'}->{'NewValue'},
        $args{'txn'}->{'OldValue'}
    );

    my $change = Prophet::Change->new(
        {   record_type   => 'ticket',
            record_uuid   => $self->sync_source->uuid_for_remote_id( $args{'ticket'}->{$self->sync_source->uuid . '-id'} ),
            change_type => 'update_file'
        }
    );

    $args{'changeset'}->add_change( { change => $change } );
    $change->add_prop_change(
        name => $args{'txn'}->{'Field'},
        old  => $args{'ticket'}->{ $args{'txn'}->{'Field'} },
        new  => $new_state
    );
}

}


sub resolve_user_id_to {
    my $self = shift;
    my $attr = shift;
    my $id   = shift;
    return undef unless $id;

    local $@;
    my $user = eval {
       Prophet::CLI->end_pager();
       # squelch chatty RT::Client::REST "Unknown key" warnings
       local $SIG{__WARN__} = sub { $self->sync_source->log_debug(@_) };
       RT::Client::REST::User->new( rt => $self->sync_source->rt, id => $id )->retrieve;
    };
    if ( my $err = $@ ) {
        warn $err;
        return $attr eq 'name' ? 'Unknown user' : 'unknown@localhost';
    }
    my $name = $user->name;
    if ( lc $name eq 'nobody' ) {
        return $attr eq 'name' ? 'nobody' : undef;
    }
    elsif ( lc $name eq 'RT_System' ) {
        return $attr eq 'name' ? 'system' : undef;
    } else {
        return $user->$attr();
    }
}

memoize 'resolve_user_id_to';

our %PROP_MAP = (
    subject         => 'summary',
    status          => 'status',
    owner           => 'owner',
    initialpriority => '_delete',
    finalpriority   => '_delete',
    told            => '_delete',
    requestor       => 'reporter',
    requestors      => 'reporter',
    cc              => 'cc',
    ccs             => 'cc',
    admincc         => 'admin_cc',
    adminccs        => 'admin_cc',
    refersto        => 'refers_to',
    referredtoby    => 'referred_to_by',
    dependson       => 'depends_on',
    dependedonby    => 'depended_on_by',
    hasmember       => 'members',
    memberof        => 'member_of',
    priority        => 'priority_integer',
    resolved        => 'completed',
    due             => 'due',
    creator         => 'creator',
    timeworked      => 'time_worked',
    timeleft        => 'time_left',
    timeestimated   => 'time_estimated',
    lastupdated     => '_delete',
    created         => 'created',
    queue           => 'queue',
    starts          => '_delete',
    started         => '_delete',
);

sub translate_status {
    my $self = shift;
    my $status = shift;

    $status =~ s/^resolved$/closed/;
    

    return $status;
}

sub translate_prop_names {
    my $self      = shift;
    my $changeset = shift;

    for my $change ( $changeset->changes ) {
        next unless $change->record_type eq 'ticket';

        my @new_props;
        for my $prop ( $change->prop_changes ) {
            next if ( ( $PROP_MAP{ lc( $prop->name ) } || '' ) eq '_delete' );
            $prop->name( $PROP_MAP{ lc( $prop->name ) } ) if $PROP_MAP{ lc( $prop->name ) };
            # Normalize away undef -> "" and vice-versa
            for (qw/new_value old_value/) {
                $prop->$_("") if !defined ($prop->$_());
                }
            next if ( $prop->old_value eq $prop->new_value);

            if ( $prop->name =~ /^cf-(.*)$/ ) {
                $prop->name( 'custom-' . $1 );
            }

            push @new_props, $prop;

        }
        $change->prop_changes( \@new_props );

    }
    return $changeset;
}

__PACKAGE__->meta->make_immutable;
no Any::Moose;
1;