# 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::BzAPI;

use 5.10.1;
use strict;
use warnings;

use base qw(Bugzilla::Extension);

use Bugzilla::Extension::BzAPI::Constants;
use Bugzilla::Extension::BzAPI::Util qw(fix_credentials filter_wants_nocache);

use Bugzilla::Error;
use Bugzilla::Util qw(datetime_from);
use Bugzilla::Constants;
use Bugzilla::Install::Filesystem;
use Bugzilla::WebService::Constants;
use Module::Runtime qw(require_module);

use File::Basename;

our $VERSION = '0.1';

################
# Installation #
################

sub install_filesystem {
  my ($self, $args) = @_;
  my $files = $args->{'files'};

  my $extensionsdir = bz_locations()->{'extensionsdir'};
  my $scriptname    = $extensionsdir . "/" . __PACKAGE__->NAME . "/bin/rest.cgi";

  $files->{$scriptname} = {perms => Bugzilla::Install::Filesystem::WS_EXECUTE};
}

##################
# Template Hooks #
##################

sub template_before_process {
  my ($self, $args) = @_;
  my $vars = $args->{'vars'};
  my $file = $args->{'file'};

  if ($file =~ /config\.json\.tmpl$/) {
    $vars->{'initial_status'} = Bugzilla::Status->can_change_to;
    $vars->{'status_objects'} = [Bugzilla::Status->get_all];
  }
}

##############
# Code Hooks #
##############

sub bug_start_of_update {
  my ($self, $args) = @_;
  my $old_bug = $args->{old_bug};
  my $params  = Bugzilla->input_params;

  return if !Bugzilla->request_cache->{bzapi};

  # Check for a mid-air collision. Currently this only works when updating
  # an individual bug and if last_changed_time is provided. Otherwise it
  # allows the changes.
  my $delta_ts = $params->{last_change_time} || '';

  if ($delta_ts && exists $params->{ids} && @{$params->{ids}} == 1) {
    _midair_check($delta_ts, $old_bug->delta_ts);
  }
}

sub object_end_of_set_all {
  my ($self, $args) = @_;
  my $object = $args->{object};
  my $params = Bugzilla->input_params;

  return if !Bugzilla->request_cache->{bzapi};
  return if !$object->isa('Bugzilla::Attachment');

  # Check for a mid-air collision. Currently this only works when updating
  # an individual attachment and if last_changed_time is provided. Otherwise it
  # allows the changes.
  my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {};
  my $delta_ts = $stash->{last_change_time};

  _midair_check($delta_ts, $object->modification_time) if $delta_ts;
}

sub _midair_check {
  my ($delta_ts, $old_delta_ts) = @_;
  my $delta_ts_z = datetime_from($delta_ts)
    || ThrowCodeError('invalid_timestamp', {timestamp => $delta_ts});
  my $old_delta_tz_z = datetime_from($old_delta_ts);
  if ($old_delta_tz_z ne $delta_ts_z) {
    ThrowUserError('bzapi_midair_collision');
  }
}

sub webservice_error_codes {
  my ($self, $args) = @_;
  my $error_map = $args->{error_map};
  $error_map->{'bzapi_midair_collision'} = 400;
}

sub webservice_fix_credentials {
  my ($self, $args) = @_;
  my $rpc    = $args->{rpc};
  my $params = $args->{params};
  return if !Bugzilla->request_cache->{bzapi};
  fix_credentials($params);
}

sub webservice_rest_request {
  my ($self, $args) = @_;
  my $rpc    = $args->{rpc};
  my $params = $args->{params};
  my $cache  = Bugzilla->request_cache;

  return if !$cache->{bzapi};

  # Stash certain values for later use
  $cache->{bzapi_rpc} = $rpc;

  # Internal websevice method being used
  $cache->{bzapi_rpc_method} = $rpc->path_info . "." . $rpc->bz_method_name;

  # Load the appropriate request handler based on path and type
  if (my $handler = _find_handler($rpc, 'request')) {
    &$handler($params);
  }
}

sub webservice_rest_response {
  my ($self, $args) = @_;
  my $rpc      = $args->{rpc};
  my $result   = $args->{result};
  my $response = $args->{response};
  my $cache    = Bugzilla->request_cache;

  # Stash certain values for later use
  $cache->{bzapi_rpc} ||= $rpc;

  return if !Bugzilla->request_cache->{bzapi} || ref $$result ne 'HASH';

  if (exists $$result->{error}) {
    $$result->{documentation} = BZAPI_DOC;
    return;
  }

  # Load the appropriate response handler based on path and type
  if (my $handler = _find_handler($rpc, 'response')) {
    &$handler($result, $response);
  }

  # Add a Location header if a newly created resource
  # such as a bug or comment.
  if ( $rpc->bz_success_code
    && $rpc->bz_success_code == STATUS_CREATED
    && $$result->{ref})
  {
    $response->header("Location", $$result->{ref});
  }
}

sub webservice_rest_resources {
  my ($self, $args) = @_;
  my $rpc       = $args->{rpc};
  my $resources = $args->{resources};

  return if !Bugzilla->request_cache->{bzapi};

  _add_resources($rpc, $resources);
}

sub webservice_status_code_map {
  my ($self, $args) = @_;
  my $status_code_map = $args->{status_code_map};
  $status_code_map->{51} = STATUS_BAD_REQUEST;
}

#####################
# Utility Functions #
#####################

sub _find_handler {
  my ($rpc, $type) = @_;

  my $path_info      = $rpc->cgi->path_info;
  my $request_method = $rpc->request->method;

  my $module = $rpc->bz_class_name || '';
  $module =~ s/^Bugzilla::WebService:://;

  my $cache = _preload_handlers();

  return undef if !exists $cache->{$module};

  # Make a copy of the handler array so
  # as to not alter the actual cached data.
  my @handlers = @{$cache->{$module}};

  while (my $regex = shift @handlers) {
    my $data = shift @handlers;
    next if ref $data ne 'HASH';
    if ( $path_info =~ $regex
      && exists $data->{$request_method}
      && exists $data->{$request_method}->{$type})
    {
      return $data->{$request_method}->{$type};
    }
  }

  return undef;
}

sub _add_resources {
  my ($rpc, $native_resources) = @_;

  my $cache = _preload_handlers();

  foreach my $module (keys %$cache) {
    my $native_module = "Bugzilla::WebService::$module";
    next if !$native_resources->{$native_module};

    # Make a copy of the handler array so
    # as to not alter the actual cached data.
    my @handlers = @{$cache->{$module}};

    my @ext_resources = ();
    while (my $regex = shift @handlers) {
      my $data = shift @handlers;
      next if ref $data ne 'HASH';
      my $new_data = {};
      foreach my $request_method (keys %$data) {
        next if !exists $data->{$request_method}->{resource};
        $new_data->{$request_method} = $data->{$request_method}->{resource};
      }
      push(@ext_resources, $regex, $new_data);
    }

    # Places the new resources at the beginning of the list
    # so we can capture specific paths before the native resources
    unshift(@{$native_resources->{$native_module}}, @ext_resources);
  }
}

sub _resource_modules {
  my $extdir = bz_locations()->{extensionsdir};
  return
    map { basename($_, '.pm') }
    glob("$extdir/" . __PACKAGE__->NAME . "/lib/Resources/*.pm");
}

# preload all handlers into cache
# since we don't want to parse all
# this multiple times
sub _preload_handlers {
  my $cache = Bugzilla->request_cache;

  if (!exists $cache->{rest_handlers}) {
    my $all_handlers = {};
    foreach my $module (_resource_modules()) {
      my $resource_class = "Bugzilla::Extension::BzAPI::Resources::$module";
      eval { require_module($resource_class) };
      next if ($@ || !$resource_class->can('rest_handlers'));
      my $handlers = $resource_class->rest_handlers;
      next if (ref $handlers ne 'ARRAY' || scalar @$handlers % 2 != 0);
      $all_handlers->{$module} = $handlers;
    }
    $cache->{rest_handlers} = $all_handlers;
  }




  return $cache->{rest_handlers};
}

sub app_startup {
  my ($self, $args) = @_;
  my $app = $args->{app};
  my $r   = $app->routes;

  Bugzilla::App::CGI->load_one('bzapi_cgi', 'extensions/BzAPI/bin/rest.cgi');
  $r->any('/extensions/BzAPI/bin/rest.cgi/*PATH_INFO')->to('CGI#bzapi_cgi');
  $r->any('/latest/*PATH_INFO')->to('CGI#bzapi_cgi');
  $r->any('/bzapi/*PATH_INFO')->to('CGI#bzapi_cgi');
}

__PACKAGE__->NAME;
