# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.

package Bugzilla::Extension::RequestNagger;

use 5.10.1;
use strict;
use warnings;

use base qw(Bugzilla::Extension);

use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Extension::RequestNagger::Settings;
use Bugzilla::Flag;
use Bugzilla::Install::Filesystem;
use Bugzilla::User::Setting;
use Bugzilla::Util qw(datetime_from detaint_natural time_ago);
use DateTime;

our $VERSION = '1';

BEGIN {
  *Bugzilla::Flag::age             = \&_flag_age;
  *Bugzilla::Flag::deferred        = \&_flag_deferred;
  *Bugzilla::Product::nag_interval = \&_product_nag_interval;
}

sub _flag_age {
  return time_ago(datetime_from($_[0]->modification_date));
}

sub _flag_deferred {
  my ($self) = @_;
  if (!exists $self->{deferred}) {
    my $dbh = Bugzilla->dbh;
    my ($defer_until)
      = $dbh->selectrow_array("SELECT defer_until FROM nag_defer WHERE flag_id=?",
      undef, $self->id);
    $self->{deferred} = $defer_until ? datetime_from($defer_until) : undef;
  }
  return $self->{deferred};
}

sub _product_nag_interval { $_[0]->{nag_interval} }

sub object_columns {
  my ($self,  $args)    = @_;
  my ($class, $columns) = @$args{qw(class columns)};
  if ($class->isa('Bugzilla::Product')) {
    push @$columns, 'nag_interval';
  }
}

sub object_update_columns {
  my ($self,   $args)    = @_;
  my ($object, $columns) = @$args{qw(object columns)};
  if ($object->isa('Bugzilla::Product')) {
    push @$columns, 'nag_interval';
  }
}

sub object_before_create {
  my ($self,  $args)   = @_;
  my ($class, $params) = @$args{qw(class params)};
  return unless $class->isa('Bugzilla::Product');
  my $input = Bugzilla->input_params;
  if (exists $input->{nag_interval}) {
    my $interval = _check_nag_interval($input->{nag_interval});
    $params->{nag_interval} = $interval;
  }
}

sub object_end_of_set_all {
  my ($self,   $args)   = @_;
  my ($object, $params) = @$args{qw(object params)};
  return unless $object->isa('Bugzilla::Product');
  my $input = Bugzilla->input_params;
  if (exists $input->{nag_interval}) {
    my $interval = _check_nag_interval($input->{nag_interval});
    $object->set('nag_interval', $interval);
  }
}

sub _check_nag_interval {
  my ($value) = @_;
  detaint_natural($value)
    || ThrowUserError('invalid_parameter',
    {name => 'request reminding interval', err => 'must be numeric'});
  return $value < 0 ? 0 : $value * 24;
}

sub page_before_template {
  my ($self, $args) = @_;
  my ($vars, $page) = @$args{qw(vars page_id)};
  return unless $page eq 'request_defer.html';

  my $user  = Bugzilla->login(LOGIN_REQUIRED);
  my $input = Bugzilla->input_params;

  # load flag
  my $flag_id
    = scalar($input->{flag}) || ThrowUserError('request_nagging_flag_invalid');
  detaint_natural($flag_id) || ThrowUserError('request_nagging_flag_invalid');
  my $flag = Bugzilla::Flag->new({id => $flag_id, cache => 1})
    || ThrowUserError('request_nagging_flag_invalid');

  # you can only defer flags directed at you
  $user->can_see_bug($flag->bug->id)
    || ThrowUserError("bug_access_denied", {bug_id => $flag->bug->id});
  $flag->status eq '?' || ThrowUserError('request_nagging_flag_set');
  $flag->requestee || ThrowUserError('request_nagging_flag_wind');
  $flag->requestee->id == $user->id
    || ThrowUserError('request_nagging_flag_not_owned');

  my $date = DateTime->now()->truncate(to => 'day');
  my $defer_until;
  if ( $input->{'defer-until'}
    && $input->{'defer-until'} =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/)
  {
    $defer_until = DateTime->new(year => $1, month => $2, day => $3);
    if ($defer_until > $date->clone->add(days => 7)) {
      $defer_until = undef;
    }
  }

  if ($input->{save} && $defer_until) {
    $self->_defer_until($flag_id, $defer_until);
    $vars->{saved}       = "1";
    $vars->{defer_until} = $defer_until;
  }
  else {
    my @dates;
    foreach my $i (1 .. 7) {
      $date->add(days => 1);
      unshift @dates, {days => $i, date => $date->clone};
    }
    $vars->{defer_until} = \@dates;
  }

  $vars->{flag} = $flag;
}

sub _defer_until {
  my ($self, $flag_id, $defer_until) = @_;
  my $dbh = Bugzilla->dbh;

  $dbh->bz_start_transaction();

  my ($defer_id)
    = $dbh->selectrow_array("SELECT id FROM nag_defer WHERE flag_id=?",
    undef, $flag_id);
  if ($defer_id) {
    $dbh->do("UPDATE nag_defer SET defer_until=? WHERE id=?",
      undef, $defer_until->ymd, $flag_id);
  }
  else {
    $dbh->do("INSERT INTO nag_defer(flag_id, defer_until) VALUES (?, ?)",
      undef, $flag_id, $defer_until->ymd);
  }

  $dbh->bz_commit_transaction();
}

sub object_end_of_update {
  my ($self, $args) = @_;
  if ($args->{object}->isa("Bugzilla::Flag") && exists $args->{changes}) {

# any change to the flag (setting, clearing, or retargetting) will clear the deferals
    my $flag = $args->{object};
    Bugzilla->dbh->do("DELETE FROM nag_defer WHERE flag_id=?", undef, $flag->id);
  }
}

sub user_preferences {
  my ($self, $args) = @_;
  my $tab = $args->{'current_tab'};
  return unless $tab eq 'request_nagging';

  my $save = $args->{'save_changes'};
  my $vars = $args->{'vars'};
  my $user = Bugzilla->user;
  my $dbh  = Bugzilla->dbh;

  my %watching = map { $_ => 1 } @{
    $dbh->selectcol_arrayref(
      "SELECT profiles.login_name
                FROM nag_watch
                    INNER JOIN profiles ON nag_watch.nagged_id = profiles.userid
                WHERE nag_watch.watcher_id = ?
                ORDER BY profiles.login_name", undef, $user->id
    )
  };

  my $nag_settings = Bugzilla::Extension::RequestNagger::Settings->new($user->id);

  if ($save) {
    my $input = Bugzilla->input_params;
    Bugzilla::User::match_field({'add_watching' => {'type' => 'multi'}});

    $dbh->bz_start_transaction();

    # user preference
    if (my $value = $input->{request_nagging}) {
      my $settings = $user->settings;
      my $setting  = new Bugzilla::User::Setting('request_nagging');
      if ($value eq 'default') {
        $settings->{request_nagging}->reset_to_default;
      }
      else {
        $setting->validate_value($value);
        $settings->{request_nagging}->set($value);
      }
    }

    # watching
    if ($input->{remove_watched_users}) {
      my $del_watching
        = ref($input->{del_watching})
        ? $input->{del_watching}
        : [$input->{del_watching}];
      foreach my $login (@$del_watching) {
        my $u = Bugzilla::User->new({name => $login, cache => 1}) || next;
        next unless exists $watching{$u->login};
        $dbh->do("DELETE FROM nag_watch WHERE watcher_id=? AND nagged_id=?",
          undef, $user->id, $u->id);
        delete $watching{$u->login};
      }
    }
    if ($input->{add_watching}) {
      my $add_watching
        = ref($input->{add_watching})
        ? $input->{add_watching}
        : [$input->{add_watching}];
      foreach my $login (@$add_watching) {
        my $u = Bugzilla::User->new({name => $login, cache => 1}) || next;
        next if exists $watching{$u->login};
        $dbh->do("INSERT INTO nag_watch(watcher_id, nagged_id) VALUES(?, ?)",
          undef, $user->id, $u->id);
        $watching{$u->login} = 1;
      }
    }

    # watching settings
    foreach my $field (Bugzilla::Extension::RequestNagger::Settings::FIELDS()) {
      $nag_settings->set($field, $input->{$field});
    }

    $dbh->bz_commit_transaction();
  }

  $vars->{watching} = [sort keys %watching];
  $vars->{settings} = $nag_settings;

  my $handled = $args->{'handled'};
  $$handled = 1;
}

#
# installation
#

sub db_schema_abstract_schema {
  my ($self, $args) = @_;
  $args->{'schema'}->{'nag_watch'} = {
    FIELDS => [
      id        => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,},
      nagged_id => {
        TYPE       => 'INT3',
        NOTNULL    => 1,
        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',}
      },
      watcher_id => {
        TYPE       => 'INT3',
        NOTNULL    => 1,
        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',}
      },
    ],
    INDEXES => [
      nag_watch_idx => {FIELDS => ['nagged_id', 'watcher_id'], TYPE => 'UNIQUE',},
    ],
  };
  $args->{'schema'}->{'nag_defer'} = {
    FIELDS => [
      id      => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,},
      flag_id => {
        TYPE       => 'INT3',
        NOTNULL    => 1,
        REFERENCES => {TABLE => 'flags', COLUMN => 'id', DELETE => 'CASCADE',}
      },
      defer_until => {TYPE => 'DATETIME', NOTNULL => 1,},
    ],
    INDEXES => [nag_defer_idx => {FIELDS => ['flag_id'], TYPE => 'UNIQUE',},],
  };
  $args->{'schema'}->{'nag_settings'} = {
    FIELDS => [
      id      => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,},
      user_id => {
        TYPE       => 'INT3',
        NOTNULL    => 1,
        REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',}
      },
      setting_name  => {TYPE => 'VARCHAR(16)', NOTNULL => 1,},
      setting_value => {TYPE => 'VARCHAR(16)', NOTNULL => 1,},
    ],
    INDEXES => [
      nag_setting_idx => {FIELDS => ['user_id', 'setting_name'], TYPE => 'UNIQUE',},
    ],
  };
}

sub install_update_db {
  my $dbh = Bugzilla->dbh;
  $dbh->bz_add_column('products', 'nag_interval',
    {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 7 * 24});
}

sub install_filesystem {
  my ($self, $args) = @_;
  my $files          = $args->{'files'};
  my $extensions_dir = bz_locations()->{'extensionsdir'};
  my $script_name
    = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/send-request-nags.pl";
  $files->{$script_name} = {perms => Bugzilla::Install::Filesystem::WS_EXECUTE};
}

sub install_before_final_checks {
  my ($self, $args) = @_;
  add_setting({
    name     => 'request_nagging',
    options  => ['on', 'off'],
    default  => 'on',
    category => 'Reviews and Needinfo'
  });
}

__PACKAGE__->NAME;
