# 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::MyDashboard::Queries;

use 5.10.1;
use strict;
use warnings;

use Bugzilla;
use Bugzilla::Bug;
use Bugzilla::CGI;
use Bugzilla::Constants;
use Bugzilla::Search;
use Bugzilla::Flag;
use Bugzilla::Status qw(is_open_state);
use Bugzilla::Util qw(format_time datetime_from time_ago);

use Bugzilla::Extension::MyDashboard::Util qw(open_states quoted_open_states);

use DateTime;

use base qw(Exporter);
our @EXPORT = qw(
  QUERY_ORDER
  SELECT_COLUMNS
  QUERY_DEFS
  query_bugs
  query_flags
);

# Default sort order
use constant QUERY_ORDER => ("changeddate desc", "bug_id");

# List of columns that we will be selecting. In the future this should be configurable
# Share with buglist.cgi?
use constant SELECT_COLUMNS => qw(
  bug_id
  bug_type
  bug_status
  priority
  short_desc
  changeddate
);

sub QUERY_DEFS {
  my $user = Bugzilla->user;

  my @query_defs = (
    {
      name    => 'assignedbugs',
      heading => 'Assigned to You',
      description =>
        'The bug has been assigned to you, and it is not resolved or closed.',
      params => {
        'bug_status'        => ['__open__'],
        'emailassigned_to1' => 1,
        'emailtype1'        => 'exact',
        'email1'            => $user->login
      }
    },
    {
      name    => 'newbugs',
      heading => 'New Reported by You',
      description =>
        'You reported the bug; it\'s unconfirmed or new. No one has assigned themselves to fix it yet.',
      params => {
        'bug_status'     => ['UNCONFIRMED', 'NEW'],
        'emailreporter1' => 1,
        'emailtype1'     => 'exact',
        'email1'         => $user->login
      }
    },
    {
      name    => 'inprogressbugs',
      heading => "In Progress Reported by You",
      description =>
        'A developer accepted your bug and is working on it. (It has someone in the "Assigned to" field.)',
      params => {
        'bug_status' => [
          map { $_->name }
          grep($_->name ne 'UNCONFIRMED' && $_->name ne 'NEW', open_states())
        ],
        'emailreporter1' => 1,
        'emailtype1'     => 'exact',
        'email1'         => $user->login
      }
    },
    {
      name        => 'openccbugs',
      heading     => "You Are CC'd On",
      description => 'You are in the CC list of the bug, so you are watching it.',
      params      => {
        'bug_status' => ['__open__'],
        'emailcc1'   => 1,
        'emailtype1' => 'exact',
        'email1'     => $user->login
      }
    },
    {
      name        => 'mentorbugs',
      heading     => "You Are a Mentor",
      description => 'You are one of the mentors for the bug.',
      params      => {
        'bug_status'       => ['__open__'],
        'emailbug_mentor1' => 1,
        'emailtype1'       => 'exact',
        'email1'           => $user->login
      }
    },
    {
      name        => 'lastvisitedbugs',
      heading     => 'Updated Since Last Visit',
      description => 'Bugs updated since last visited',
      mark_read   => 'Mark Visited',
      params => {o1 => 'lessthan', v1 => '%last_changed%', f1 => 'last_visit_ts',},
    },
    {
      name        => 'interestingbugs',
      heading     => 'Interesting Bugs',
      description => 'Bugs that you may find interesting',
      mark_read   => 'Remove Interest',
      params      => {
        j_top => 'OR',
        f1    => 'bug_interest_ts',
        o1    => 'isnotempty',

        f2 => 'last_visit_ts',
        o2 => 'lessthan',
        v2 => '%last_changed%',
      }
    },
    {
      name        => 'nevervisitbugs',
      heading     => 'Involved with and Never Visited',
      description => "Bugs you've never visited, but are involved with",
      mark_read   => 'Mark Visited',
      params      => {
        query_format => "advanced",
        bug_status   => ['__open__'],
        ,
        o1 => "isempty",
        f1 => "last_visit_ts",
        j2 => "OR",
        f2 => "OP",
        f3 => "assigned_to",
        o3 => "equals",
        v3 => $user->login,
        o4 => "equals",
        f4 => "reporter",
        v4 => $user->login,
        v5 => $user->login,
        f5 => "qa_contact",
        o5 => "equals",
        o6 => "equals",
        f6 => "cc",
        v6 => $user->login,
        f7 => "bug_mentor",
        o7 => "equals",
        v7 => $user->login,
        f9 => "CP",
      },
    },
  );

  if (Bugzilla->params->{'useqacontact'}) {
    push(
      @query_defs,
      {
        name    => 'qacontactbugs',
        heading => 'You Are QA Contact',
        description =>
          'You are the qa contact on this bug, and it is not resolved or closed.',
        params => {
          'bug_status'       => ['__open__'],
          'emailqa_contact1' => 1,
          'emailtype1'       => 'exact',
          'email1'           => $user->login
        }
      }
    );
  }

  if ($user->showmybugslink) {
    my $query = Bugzilla->params->{mybugstemplate};
    my $login = $user->login;
    $query =~ s/%userid%/$login/;
    $query =~ s/^buglist.cgi\?//;
    push(@query_defs,
      {name => 'mybugs', heading => "My Bugs", saved => 1, params => $query,});
  }

  foreach my $q (@{$user->queries}) {
    next if !$q->in_mydashboard;
    push(@query_defs, {name => $q->name, saved => 1, params => $q->url});
  }

  return @query_defs;
}

sub query_bugs {
  my $qdef         = shift;
  my $dbh          = Bugzilla->dbh;
  my $user         = Bugzilla->user;
  my $datetime_now = DateTime->now(time_zone => $user->timezone);

  ## HACK to remove POST
  delete $ENV{REQUEST_METHOD};

  my $params = new Bugzilla::CGI($qdef->{params});

  my $search = new Bugzilla::Search(
    fields => [SELECT_COLUMNS],
    params => scalar $params->Vars,
    order  => [QUERY_ORDER]
  );
  my $data = $search->data;

  my @bugs;
  foreach my $row (@$data) {
    my $bug = {};
    foreach my $column (SELECT_COLUMNS) {
      $bug->{$column} = shift @$row;
      if ($column eq 'changeddate') {
        my $datetime = datetime_from($bug->{$column});
        $datetime->set_time_zone($user->timezone);
        $bug->{$column} = $datetime->strftime('%Y-%m-%d %T %Z');
        $bug->{'changeddate_fancy'} = time_ago($datetime, $datetime_now);

        # Provide a version for use by Bug.history and also for looking up last comment.
        # We have to set to server's timezone and also subtract one second.
        $datetime->set_time_zone(Bugzilla->local_timezone);
        $datetime->subtract(seconds => 1);
        $bug->{changeddate_api} = $datetime->strftime('%Y-%m-%d %T');
      }
    }
    push(@bugs, $bug);
  }

  return (\@bugs, $params->canonicalize_query());
}

sub query_flags {
  my ($type) = @_;
  my $user   = Bugzilla->user;
  my $dbh    = Bugzilla->dbh;
  my $datetime_now = DateTime->now(time_zone => $user->timezone);

  ($type ne 'requestee' || $type ne 'requester')
    || ThrowCodeError('param_required', {param => 'type'});

  my $match_params = {status => '?'};

  if ($type eq 'requestee') {
    $match_params->{'requestee_id'} = $user->id;
  }
  else {
    $match_params->{'setter_id'} = $user->id;
    # Exclude flags without a requestee, such as `in-testsuite?`
    $match_params->{'requestee_id'} = NOT_NULL;
  }

  my $matched = Bugzilla::Flag->match($match_params);

  return [] if !@$matched;

  my @unfiltered_flags;
  my %all_bugs;    # Use hash to filter out duplicates
  foreach my $flag (@$matched) {
    next
      if ($flag->attach_id && $flag->attachment->isprivate && !$user->is_insider);

    my $data = {
      id        => $flag->id,
      type      => $flag->type->name,
      status    => $flag->status,
      attach_id => $flag->attach_id,
      is_patch  => $flag->attach_id ? $flag->attachment->ispatch : 0,
      bug_id    => $flag->bug_id,
      requester => $flag->setter->login,
      requestee => $flag->requestee ? $flag->requestee->login : '',
      updated   => $flag->modification_date,
    };
    push(@unfiltered_flags, $data);

    # Record bug id for later retrieval of status/summary
    $all_bugs{$flag->{'bug_id'}}++;
  }

  # Filter the bug list based on permission to see the bug
  my %visible_bugs = map { $_ => 1 } @{$user->visible_bugs([keys %all_bugs])};

  return [] if !scalar keys %visible_bugs;

  # Get all bug statuses and summaries in one query instead of loading
  # many separate bug objects
  my $bug_rows = $dbh->selectall_arrayref(
    "SELECT bug_id, bug_status, short_desc
                                               FROM bugs
                                              WHERE "
      . $dbh->sql_in('bug_id', [keys %visible_bugs]), {Slice => {}}
  );
  foreach my $row (@$bug_rows) {
    $visible_bugs{$row->{'bug_id'}}
      = {bug_status => $row->{'bug_status'}, short_desc => $row->{'short_desc'}};
  }

  # Now drop out any flags for bugs the user cannot see
  # or if the user did not want to see closed bugs
  my @filtered_flags;
  foreach my $flag (@unfiltered_flags) {

    # Skip this flag if the bug is not visible to the user
    next if !$visible_bugs{$flag->{'bug_id'}};

    # Include bug status and summary with each flag
    $flag->{'bug_status'}  = $visible_bugs{$flag->{'bug_id'}}->{'bug_status'};
    $flag->{'bug_summary'} = $visible_bugs{$flag->{'bug_id'}}->{'short_desc'};

    # Format the updated date specific to the user's timezone
    # and add the fancy human readable version
    my $datetime = datetime_from($flag->{'updated'});
    $datetime->set_time_zone($user->timezone);
    $flag->{'updated'}       = $datetime->strftime('%Y-%m-%d %T %Z');
    $flag->{'updated_epoch'} = $datetime->epoch;
    $flag->{'updated_fancy'} = time_ago($datetime, $datetime_now);

    push(@filtered_flags, $flag);
  }

  return [] if !@filtered_flags;

  # Sort by most recently updated
  return [sort { $b->{'updated_epoch'} <=> $a->{'updated_epoch'} }
      @filtered_flags];
}

1;
