The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package App::SD::Model::Ticket;
use Any::Moose;
extends 'App::SD::Record';

use Term::ANSIColor;
use App::SD::Util;

use constant collection_class => 'App::SD::Collection::Ticket';
has type => ( default => 'ticket');

our $ACTIVE_STATUSES;

__PACKAGE__->register_reference( comments => 'App::SD::Collection::Comment', by => 'ticket');
__PACKAGE__->register_reference( attachments => 'App::SD::Collection::Attachment', by => 'ticket');

sub BUILD {
    my $self = shift;
    $ACTIVE_STATUSES ||= $self->app_handle->setting(label => 'active_statuses')->get();
}

sub default_prop_component { 
    my $self = shift; 
    return $self->app_handle->setting(label => 'default_component')->get()->[0];
}

sub default_prop_milestone { 
    my $self = shift; 
    return $self->app_handle->setting(label => 'default_milestone')->get()->[0];
}

=head2 default_prop_status

Returns a string of the default value of the C<status> prop.

=cut

sub default_prop_status { 
    my $self = shift; 
    return $self->app_handle->setting(label => 'default_status')->get()->[0];
}

sub has_active_status {
    my $self = shift;
    return 1 if grep { $_ eq $self->prop('status') } @{$ACTIVE_STATUSES};
}

=head2 default_prop_reporter

Returns a string of the default value of the C<reporter> prop.
(Currently, this is the config variable C<email_address> or
the environmental variable C<EMAIL>.)

=cut

sub default_prop_reporter {
    my $self = shift;
    my $reporter = $self->app_handle->current_user_email;
    if ( $reporter ) {
        return $reporter;
    }
    else {
        die "Cannot determine an email to attribute your changes to."
           ." You can\nfix this by setting the config variable"
           ." 'user.email-address'.\n";
    }
}

=head2 canonicalize_prop_status

resolved is called closed.

=cut

my %canonicalize_status = (
    resolved => 'closed',
);

sub canonicalize_prop_status {
    my $self = shift;
    my %args = @_;

    my $props = $args{props};

    if (defined $canonicalize_status{ $props->{status} }) {
        $props->{status} = $canonicalize_status{ $props->{status} };
    }

    return 1;
}


sub canonicalize_prop_due {
    my $self = shift;
    my %args = @_;
    my $props = $args{props};
    # skip blank
    return 1 unless $props->{due};
    #skip well formed
    return 1 if $props->{due} =~ /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/; 
    require DateTime::Format::Natural;
    my $parser = DateTime::Format::Natural->new;
    my $dt = $parser->parse_datetime($props->{due});
    if ($parser->success) {
     # operate on $dt/@dt, for example:
        $props->{due} = sprintf( "%04d-%02d-%02d %02d:%02d:%02d", $dt->year, $dt->month, $dt->day, $dt->hour, $dt->min, $dt->sec);
    }
    return 1;
}


=head2 _default_summary_format

The default ticket summary format (used for displaying tickets in a
list, generally).

=cut

sub _default_summary_format { '%s,$luid | %s,summary | %s,status' }

=head2 validate_prop_status { props = $hashref, errors = $hashref }

Determines whether the status prop value given in C<$args{props}{status}>
is valid.

Returns true if the status is valid. If the status is invalid, sets
C<$args{errors}{status}> to an error message and returns false.

=cut

sub validate_prop_status {
    my ( $self, %args ) = @_;
    return $self->validate_prop_from_recommended_values( 'status', \%args );
}

sub validate_prop_component {
    my ( $self, %args ) = @_;
    return $self->validate_prop_from_recommended_values( 'component', \%args );
}

sub validate_prop_milestone {
    my ( $self, %args ) = @_;
    return $self->validate_prop_from_recommended_values( 'milestone', \%args );
}

sub _recommended_values_for_prop_milestone {
   return @{ shift->app_handle->setting( label => 'milestones' )->get() };
}

sub _recommended_values_for_prop_status {
   return @{ shift->app_handle->setting( label => 'statuses' )->get() };
}

sub _recommended_values_for_prop_component {
   return @{ shift->app_handle->setting( label => 'components' )->get() };
}

=head2 props_to_show { 'verbose' => 1, update => 0 }

A list of which properties to display for the C<show> command (in order
from first to last).

If called with 'verbose' as a true value, will return all the
declared props of a ticket rather than the predefined list of ones
to show. (Should not be called this way during a ticket create as
new tickets have no declared properties.)

If called with 'update' as a true value, props in the prop ordering
setting will still be returned in the list even if the record
doesn't have that property. (Because we often want to not show
blank properties, but still have the option of adding them in
an update.)

=cut

sub props_to_show {
    my $self = shift;
    my $args = shift || {};
    my @props_from_config = split(',',
        ($self->app_handle->config->get( key => 'ticket.common-props') || ''));
    my $props_list = @props_from_config ? \@props_from_config :
        $self->app_handle->setting(label => 'common_ticket_props')->get();

    return @{$props_list} unless $args->{'verbose'};

    return _create_prop_ordering( hash_to_order => $self->get_props,
                                  order => $props_list,
                                  update => $args->{update});
}

=head2 _create_prop_ordering hash_to_order => $hashref, order => $arrayref [, update => 1 ]

Given references to a hash and an array, return an array of the keys of the
hash in the order specified by the array, with any extra keys at the end of the
ordering.

If called with update as a true value, will add keys in the ordering to the
returned order even if they're not in the hash.

=cut

sub _create_prop_ordering {
    my %args = @_;
    my %props = %{$args{hash_to_order}};
    my @order = @{$args{order}};
    my @new_props_list;

    # if props in the ordering are in the hash, add them to
    # the new ordering
    for my $prop (@order) {
        if ( $props{$prop} || $prop eq 'id' || $args{update} ) {
            push @new_props_list, $prop;
            delete $props{$prop};
        }
    }
    # add hash keys not in the ordering to the end of the new ordering
    push @new_props_list, keys %props;

    return @new_props_list;
}

=head2 immutable_props

A pattern of props not to show in an editor (when creating or updating a
ticket, for example). Could also be used to determine which props shouldn't be
user-modifiable.

=cut

sub immutable_props { qw(id creator created original_replica) }

=head2 is_overdue [$date]

Takes an ISO date (or uses the C<date> prop value if no date is given).

Returns false if the date is not a valid ISO date or its due date is
in the future. Returns true if the due date has passed.

=cut

sub is_overdue {
    my $self = shift;
    my $date = shift || $self->prop('due');

    my $then = App::SD::Util::string_to_datetime($date);

    if (!$then) {
        warn "Unknown date format '$date'";
        return 0;
    }

    if ($then < DateTime->now()){ 
        return 1;
    }

    return 0;
}


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