# 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::BMO::Reports::Triage;

use 5.10.1;
use strict;
use warnings;

use Bugzilla::Component;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Product;
use Bugzilla::User;
use Bugzilla::Util qw(detaint_natural trim url_quote);
use Date::Parse;

use JSON::XS;
use List::MoreUtils qw(any);

# set an upper limit on the *unfiltered* number of bugs to process
use constant MAX_NUMBER_BUGS => 4000;

use constant DEFAULT_OWNER_PRODUCTS => (
  'Core', 'DevTools', 'External Software Affecting Firefox',
  'Firefox', 'Firefox for Android', 'Firefox Build System',
  'GeckoView', 'NSPR', 'NSS', 'Remote Protocol', 'Testing',
  'Toolkit', 'WebExtensions',
);

sub unconfirmed {
  my ($vars, $filter) = @_;
  my $dbh   = Bugzilla->dbh;
  my $input = Bugzilla->input_params;
  my $user  = Bugzilla->user;

  if ( exists $input->{'action'}
    && $input->{'action'} eq 'run'
    && $input->{'product'})
  {

    # load product and components from input

    my $product
      = Bugzilla::Product->new({name => $input->{'product'}})
      || ThrowUserError('invalid_object',
      {object => 'Product', value => $input->{'product'}});

    my @component_ids;
    if ($input->{'component'} ne '') {
      my $ra_components
        = ref($input->{'component'})
        ? $input->{'component'}
        : [$input->{'component'}];
      foreach my $component_name (@$ra_components) {
        next unless my $component = Bugzilla::Component->new(
          {name => $component_name, product => $product, cache => 1});
        push(@component_ids, $component->id);
      }
    }

    # determine which comment filters to run

    my $filter_commenter    = $input->{'filter_commenter'};
    my $filter_commenter_on = $input->{'commenter'};
    my $filter_last         = $input->{'filter_last'};
    my $filter_last_period  = $input->{'last'};

    if (!$filter_commenter || $filter_last) {
      $filter_commenter    = '1';
      $filter_commenter_on = 'reporter';
    }

    my $filter_commenter_id;
    if ($filter_commenter && $filter_commenter_on eq 'is') {
      Bugzilla::User::match_field({'commenter_is' => {'type' => 'single'}});
      my $user
        = Bugzilla::User->new({name => $input->{'commenter_is'}})
        || ThrowUserError('invalid_object',
        {object => 'User', value => $input->{'commenter_is'}});
      $filter_commenter_id = $user ? $user->id : 0;
    }

    my $filter_last_time;
    if ($filter_last) {
      if ($filter_last_period eq 'is') {
        $filter_last_period = -1;
        $filter_last_time = str2time($input->{'last_is'} . " 00:00:00") || 0;
      }
      else {
        detaint_natural($filter_last_period);
        $filter_last_period = 14 if $filter_last_period < 14;
      }
    }

    # form sql queries

    my $now      = (time);
    my $bugs_sql = "
              SELECT bug_id, short_desc, reporter, creation_ts
                FROM bugs
               WHERE product_id = ?
                     AND bug_status = 'UNCONFIRMED'";
    if (@component_ids) {
      $bugs_sql .= " AND component_id IN (" . join(',', @component_ids) . ")";
    }
    $bugs_sql .= "
            ORDER BY creation_ts
        ";

    my $comment_count_sql = "
            SELECT COUNT(*)
              FROM longdescs
             WHERE bug_id = ?
        ";

    my $comment_sql = "
              SELECT who, bug_when, type, thetext, extra_data
                FROM longdescs
               WHERE bug_id = ?
        ";
    if (!Bugzilla->user->is_insider) {
      $comment_sql .= " AND isprivate = 0 ";
    }
    $comment_sql .= "
            ORDER BY bug_when DESC
               LIMIT 1
        ";

    my $attach_sql = "
            SELECT description, isprivate
              FROM attachments
             WHERE attach_id = ?
        ";

    # work on an initial list of bugs

    my $list = $dbh->selectall_arrayref($bugs_sql, undef, $product->id);
    my @bugs;

  # this can be slow to process, resulting in 'service unavailable' errors from zeus
  # so if too many bugs are returned, throw an error

    if (scalar(@$list) > MAX_NUMBER_BUGS) {
      ThrowUserError('report_too_many_bugs');
    }

    foreach my $entry (@$list) {
      my ($bug_id, $summary, $reporter_id, $creation_ts) = @$entry;

      next unless $user->can_see_bug($bug_id);

      # get last comment information

      my ($comment_count) = $dbh->selectrow_array($comment_count_sql, undef, $bug_id);
      my ($commenter_id, $comment_ts, $type, $comment, $extra)
        = $dbh->selectrow_array($comment_sql, undef, $bug_id);
      my $commenter = 0;

      # apply selected filters

      if ($filter_commenter) {
        next if $comment_count <= 1;

        if ($filter_commenter_on eq 'reporter') {
          next if $commenter_id != $reporter_id;

        }
        elsif ($filter_commenter_on eq 'noconfirm') {
          $commenter = Bugzilla::User->new({id => $commenter_id, cache => 1});
          next if $commenter_id != $reporter_id || $commenter->in_group('canconfirm');

        }
        elsif ($filter_commenter_on eq 'is') {
          next if $commenter_id != $filter_commenter_id;
        }
      }
      else {
        $input->{'commenter'}    = '';
        $input->{'commenter_is'} = '';
      }

      if ($filter_last) {
        my $comment_time = str2time($comment_ts) or next;
        if ($filter_last_period == -1) {
          next if $comment_time >= $filter_last_time;
        }
        else {
          next if $now - $comment_time <= 60 * 60 * 24 * $filter_last_period;
        }
      }
      else {
        $input->{'last'}    = '';
        $input->{'last_is'} = '';
      }

      # get data for attachment comments

      if ($comment eq '' && $type == CMT_ATTACHMENT_CREATED) {
        my ($description, $is_private)
          = $dbh->selectrow_array($attach_sql, undef, $extra);
        next if $is_private && !Bugzilla->user->is_insider;
        $comment = "(Attachment) " . $description;
      }

      # truncate long comments

      if (length($comment) > 80) {
        $comment = substr($comment, 0, 80) . '...';
      }

      # build bug hash for template

      my $bug = {};
      $bug->{id}          = $bug_id;
      $bug->{summary}     = $summary;
      $bug->{reporter}    = Bugzilla::User->new({id => $reporter_id, cache => 1});
      $bug->{creation_ts} = $creation_ts;
      $bug->{commenter}
        = $commenter || Bugzilla::User->new({id => $commenter_id, cache => 1});
      $bug->{comment_ts}    = $comment_ts;
      $bug->{comment}       = $comment;
      $bug->{comment_count} = $comment_count;
      push @bugs, $bug;
    }

    @bugs = sort { $b->{comment_ts} cmp $a->{comment_ts} } @bugs;

    $vars->{bugs} = \@bugs;
  }
  else {
    $input->{action} = '';
  }

  if (!$input->{filter_commenter} && !$input->{filter_last}) {
    $input->{filter_commenter} = 1;
  }

  $vars->{'input'} = $input;
}

sub owners {
  my ($vars, $filter) = @_;
  my $dbh   = Bugzilla->dbh;
  my $input = Bugzilla->input_params;
  my $user  = Bugzilla->user;

  Bugzilla::User::match_field({'owner' => {'type' => 'multi'}});

  my @products;
  if (!$input->{product} && $input->{owner}) {
    @products = @{$user->get_selectable_products};
  }
  else {
    my @product_names
      = $input->{product} ? ($input->{product}) : DEFAULT_OWNER_PRODUCTS;
    foreach my $name (@product_names) {
      next unless my $product = Bugzilla::Product->new({name => $name, cache => 1});
      push(@products, $product);
    }
  }

  my @component_ids;
  if (@products == 1 && $input->{'component'}) {
    my $ra_components
      = ref($input->{'component'})
      ? $input->{'component'}
      : [$input->{'component'}];
    foreach my $component_name (@$ra_components) {
      next unless my $component = Bugzilla::Component->new(
        {name => $component_name, product => $products[0], cache => 1});
      push(@component_ids, $component->id);
    }
  }

  my @owner_names = split(/[,;]+/, $input->{owner}) if $input->{owner};
  my @owner_ids;
  foreach my $name (@owner_names) {
    $name = trim($name);
    next unless $name;
    push(@owner_ids, login_to_id($name, THROW_ERROR));
  }

  my $sql
    = "SELECT products.name, components.name, components.id, components.triage_owner_id
               FROM components JOIN products ON components.product_id = products.id
               WHERE products.id IN ("
    . join(',', map { $_->id } @products) . ")";
  if (@component_ids) {
    $sql .= " AND components.id IN (" . join(',', @component_ids) . ")";
  }
  if (@owner_ids) {
    $sql .= " AND components.triage_owner_id IN (" . join(',', @owner_ids) . ")";
  }
  $sql .= " ORDER BY products.name, components.name";

  my $rows = $dbh->selectall_arrayref($sql);

  my $bug_count_sth = $dbh->prepare("
        SELECT bugs.bug_type, COUNT(bugs.bug_id)
        FROM   bugs INNER JOIN components AS map_component ON bugs.component_id = map_component.id
               INNER JOIN bug_status AS map_bug_status ON bugs.bug_status = map_bug_status.value
               INNER JOIN priority AS map_priority ON bugs.priority = map_priority.value
        WHERE  bugs.resolution IN ('')
                AND bugs.priority IN ('--')
                AND bugs.creation_ts >= '2016-06-01'
                AND (NOT( EXISTS (
                    SELECT 1
                    FROM   bugs bugs_1
                           LEFT JOIN attachments AS attachments_1 ON bugs_1.bug_id = attachments_1.bug_id
                           LEFT JOIN flags AS flags_1 ON bugs_1.bug_id = flags_1.bug_id AND (flags_1.attach_id = attachments_1.attach_id OR flags_1.attach_id IS NULL)
                           LEFT JOIN flagtypes AS flagtypes_1 ON flags_1.type_id = flagtypes_1.id
                    WHERE  bugs_1.bug_id = bugs.bug_id AND CONCAT(flagtypes_1.name, flags_1.status) = 'needinfo?')))
                AND bugs.component_id = ?
       GROUP BY bugs.bug_type");

  my @results;
  foreach my $row (@$rows) {
    my ($product_name, $component_name, $component_id, $triage_owner_id) = @$row;
    my $triage_owner
      = $triage_owner_id
      ? Bugzilla::User->new({id => $triage_owner_id, cache => 1})
      : "";
    my $data = {
      product    => $product_name,
      component  => $component_name,
      owner      => $triage_owner,
      bug_counts => {defect => 0, enhancement => 0, task => 0},
    };
    $data->{buglist_url}
      = 'priority=--&resolution=---&f1=creation_ts&o1=greaterthaneq&v1=2016-06-01'
      . '&f2=flagtypes.name&o2=notequals&v2=needinfo%3F';
    if ($triage_owner) {
      $data->{buglist_url}
        .= '&f3=triage_owner&o3=equals&v3=' . url_quote($triage_owner->login);
    }
    $bug_count_sth->execute($component_id);

    my $total = 0;
    while (my ($type, $count) = $bug_count_sth->fetchrow_array()) {
      $data->{bug_counts}->{$type} = $count;
      $total += $count;
    }
    $data->{bug_counts}->{total} = $total;

    push @results, $data;
  }
  $vars->{results} = \@results;

  my $json_data = {products => []};
  foreach my $product (@{$user->get_selectable_products}) {
    my $prod_data = {name => $product->name, components => [],};
    foreach my $component (@{$product->components}) {
      my $selected = 0;
      if ( $input->{product}
        && $input->{product} eq $product->name
        && $input->{component})
      {
        $selected = 1
          if (
          ref $input->{component} && any { $_ eq $component->name }
          @{$input->{component}}
          );
        $selected = 1
          if (!ref $input->{component} && $input->{component} eq $component->name);
      }
      my $comp_data = {name => $component->name, selected => $selected};
      push(@{$prod_data->{components}}, $comp_data);
    }
    push(@{$json_data->{products}}, $prod_data);
  }

  $vars->{product}   = $input->{product};
  $vars->{owner}     = $input->{owner};
  $vars->{json_data} = encode_json($json_data);
}

1;
