package GraphQL::Subscription;

use 5.014;
use strict;
use warnings;
use Types::Standard -all;
use Types::TypeTiny -all;
use GraphQL::Type::Library -all;
use GraphQL::MaybeTypeCheck;
use GraphQL::Language::Parser qw(parse);
use GraphQL::Error;
use GraphQL::Debug qw(_debug);
require GraphQL::Execution;
use Exporter 'import';

=head1 NAME

GraphQL::Subscription - Implement GraphQL subscriptions

=cut

our @EXPORT_OK = qw(
  subscribe
);
our $VERSION = '0.02';

use constant DEBUG => $ENV{GRAPHQL_DEBUG}; # "DEBUG and" gets optimised out if false

=head1 SYNOPSIS

  use GraphQL::Subscription qw(subscribe);
  my $result = subscribe($schema, $doc, $root_value);

=head1 DESCRIPTION

Implements a GraphQL "subscription" - a feed of events, commonly of
others' mutations.

=head1 METHODS

=head2 subscribe

  my $result = subscribe(
    $schema,
    $doc, # can also be AST
    $root_value,
    $context_value,
    $variable_values,
    $operation_name,
    $field_resolver,
    $promise_code,
    $subscribe_resolver,
  );

Takes the same args as L<GraphQL::Execution/execute>, except
that the C<$promise_code> is mandatory, and there is the optional
C<$subscribe_resolver> which, supplied or not, will be used instead
of the C<$field_resolver> to resolve the initial subscription. The
C<$field_resolver> will still be used for resolving each result.

Returns a promise of either:

=over

=item *

an error result if the subscription fails (generated by GraphQL)

=item *

a L<GraphQL::AsyncIterator> instance generated by a resolver, probably
hooked up to a L<GraphQL::PubSub> instance

=back

The event source returns values by using
L<GraphQL::AsyncIterator/publish>. To communicate errors, resolvers
can throw errors in the normal way, or at the top level, use
L<GraphQL::AsyncIterator/error>.

The values will be resolved in the normal GraphQL fashion, including that
if there is no specific-to-field resolver, it must be a value acceptable
to the supplied or default field-resolver, typically a hash-ref with a
key of the field's name.

=cut

fun subscribe(
  (InstanceOf['GraphQL::Schema']) $schema,
  Str | ArrayRef[HashRef] $doc,
  Any $root_value,
  Any $context_value,
  Maybe[HashRef] $variable_values,
  Maybe[Str] $operation_name,
  Maybe[CodeLike] $field_resolver,
  PromiseCode $promise_code,
  Maybe[CodeLike] $subscribe_resolver = undef,
) :ReturnType(Promise) {
  die 'Must supply $promise_code' if !$promise_code;
  my $result = eval {
    my $ast;
    my $context = eval {
      $ast = ref($doc) ? $doc : parse($doc);
      GraphQL::Execution::_build_context(
        $schema,
        $ast,
        $root_value,
        $context_value,
        $variable_values,
        $operation_name,
        $subscribe_resolver,
        $promise_code,
      );
    };
    DEBUG and _debug('subscribe', $context, $@);
    die $@ if $@;
    # from GraphQL::Execution::_execute_operation
    my $operation = $context->{operation};
    my $op_type = $operation->{operationType} || 'subscription';
    my $type = $context->{schema}->$op_type;
    my ($fields) = $type->_collect_fields(
      $context,
      $operation->{selections},
      [[], {}],
      {},
    );
    DEBUG and _debug('subscribe(fields)', $fields, $root_value);
    # from GraphQL::Execution::_execute_fields
    my ($field_names, $nodes_defs) = @$fields;
    die "Subscription needs to have only one field; got (@$field_names)\n"
      if @$field_names != 1;
    my $result_name = $field_names->[0];
    my $nodes = $nodes_defs->{$result_name};
    my $field_node = $nodes->[0];
    my $field_name = $field_node->{name};
    my $field_def = GraphQL::Execution::_get_field_def($context->{schema}, $type, $field_name);
    DEBUG and _debug('subscribe(resolve)', $type->to_string, $nodes, $root_value, $field_def);
    die "The subscription field '$field_name' is not defined\n"
      if !$field_def;
    my $resolve = $field_def->{subscribe} || $context->{field_resolver};
    my $path = [ $result_name ];
    my $info = GraphQL::Execution::_build_resolve_info(
      $context,
      $type,
      $field_def,
      $path,
      $nodes,
    );
    my $result = GraphQL::Execution::_resolve_field_value_or_error(
      $context,
      $field_def,
      $nodes,
      $resolve,
      $root_value,
      $info,
    );
    DEBUG and _debug('subscribe(result)', $result, $@);
    die "The subscription field '$field_name' returned non-AsyncIterator '$result'\n"
      if !is_AsyncIterator($result);
    $result->map_then(
      sub {
        GraphQL::Execution::execute(
          $schema,
          $ast,
          $_[0],
          $context_value,
          $variable_values,
          $operation_name,
          $field_resolver,
          $promise_code,
        )
      },
      sub {
        my ($error) = @_;
        die $error if !GraphQL::Error->is($error);
        GraphQL::Execution::_build_response(GraphQL::Execution::_wrap_error($error));
      },
    );
  };
  $result = GraphQL::Execution::_build_response(GraphQL::Execution::_wrap_error($@)) if $@;
  $promise_code->{resolve}->($result);
}

1;
