package Mojo::WebSocketProxy::Backend::JSONRPC;

use strict;
use warnings;

use parent qw(Mojo::WebSocketProxy::Backend);

use feature qw(state);

no indirect;

use curry;

use MojoX::JSON::RPC::Client;

our $VERSION = '0.14';    ## VERSION

__PACKAGE__->register_type('jsonrpc');

sub url { return shift->{url} }

my $request_number = 0;

=head2 call_rpc

Description: Makes a remote call to a  process  returning the result to the client in JSON format.
Before, After and error actions can be specified using call backs.
It takes the following arguments

=over 4

=item - $c  : L<Mojolicious::Controller>

=item - $req_storage A hashref of attributes stored with the request.  This routine uses some of the
following named arguments.

=over 4

=item - url, if not specified url set on C<< $self >> object is used. Must be supplied by either method.

=item - method, The name of the method at the remote end (this is appened to C<< $request_storage->{url} >> )

=item - msg_type, a name for this method if not supplied C<method> is used.

=item - call_params, a hashref of arguments on top of C<req_storage> to send to remote method. This will be suplemented with C<< $req_storage->{args} >>
added as an C<args> key and be merged with C<< $req_storage->{stash_params} >> with stash_params overwriting any matching
keys in C<call_params>.

=item - rpc_response_callback,  If supplied this will be run with C<< Mojolicious::Controller >> instance the rpc_response and C<< $req_storage >>.
B<Note:> if C<< rpc_response_callback >> is supplied the success and error callbacks are not used.

=item - before_get_rpc_response,  array ref of subroutines to run before the remote response, is passed C<< $c >> and C<< req_storage >>

=item - after_get_rpc_response, arrayref of subroutines to run after the remote response,  is passed C<< $c >> and C<< req_storage >>
called only when there is an actual response from the remote call .  IE if there is communication  error with the call it will
not be called versus an error message being returned from the call when it will.

=item - before_call, arrayref of subroutines called before the request to the remote service is made.

=item -  error,  a subroutine reference that will be called with C<< Mojolicious::Controller >> the rpc_response and C<< $req_storage >>
if a C<< $response->{error} >>  error was returned from the remote call, and C<< $req_storage->{rpc_response_cb} >> was not passed.

=item - success, a subroutines reference that will be called if there was no error returned from the remote call and  C<< $req_storage->{rpc_response_cb} >> was not passed.

=item - rpc_failure_cb, a sub routine reference to call if the remote call fails at a http level. Called with C<< Mojolicious::Controller >> the rpc_response and C<< $req_storage >>

=back

=back

Returns undef.

=cut

sub call_rpc {
    my ($self, $c, $req_storage) = @_;
    state $client = MojoX::JSON::RPC::Client->new;

    my $url = $req_storage->{url} // $self->url;
    die 'No url found' unless $url;

    $url .= $req_storage->{method};

    my $method   = $req_storage->{method};
    my $msg_type = $req_storage->{msg_type} ||= $req_storage->{method};

    $req_storage->{call_params} ||= {};

    my $rpc_response_cb = $self->get_rpc_response_cb($c, $req_storage);

    my $before_get_rpc_response_hook = delete($req_storage->{before_get_rpc_response}) || [];
    my $after_got_rpc_response_hook  = delete($req_storage->{after_got_rpc_response})  || [];
    my $before_call_hook             = delete($req_storage->{before_call})             || [];
    my $rpc_failure_cb               = delete($req_storage->{rpc_failure_cb});
    # If this flag true, then proxy will not send the rpc response to the client back.
    # It is very useful when websocket app itself (not websocket client) want to get information from rpc.

    my $block_response = delete($req_storage->{block_response});

    my $callobj = {
        # enough for short-term uniqueness
        id     => join('_', $$, $request_number++, time, (0 + [])),
        method => $method,
        params => $self->make_call_params($c, $req_storage),
    };

    $_->($c, $req_storage) for @$before_call_hook;

    $client->call(
        $url, $callobj,
        $client->$curry::weak(
            sub {
                my $client = shift;
                my $res    = pop;

                $_->($c, $req_storage) for @$before_get_rpc_response_hook;

                # unconditionally stop any further processing if client is already disconnected
                return unless $c->tx;

                my $api_response;
                if (!$res) {
                    my $tx = $client->tx;
                    $req_storage->{req_url} = $tx->req->url;
                    my $err = $tx->error;
                    $rpc_failure_cb->(
                        $c, $res,
                        $req_storage,
                        {
                            code    => $err->{code},
                            message => $err->{message},
                            type    => 'WrongResponse',
                        }) if $rpc_failure_cb;
                    return if $block_response;
                    $api_response = $c->wsp_error($msg_type, 'WrongResponse', 'Sorry, an error occurred while processing your request.');
                    $c->send({json => $api_response}, $req_storage);
                    return;
                }

                $_->($c, $req_storage, $res) for @$after_got_rpc_response_hook;

                if ($res->is_error) {
                    $rpc_failure_cb->(
                        $c, $res,
                        $req_storage,
                        {
                            code    => $res->error_code,
                            message => $res->error_message,
                            type    => 'CallError',
                        }) if $rpc_failure_cb;
                    return if $block_response;
                    $api_response = $c->wsp_error($msg_type, 'CallError', 'Sorry, an error occurred while processing your request.');
                    $c->send({json => $api_response}, $req_storage);
                    return;
                }

                $api_response = $rpc_response_cb->($res->result);
                return if $block_response || !$api_response;
                $c->send({json => $api_response}, $req_storage);

                return;
            }));
    return;
}

1;

__END__

=head1 NAME

Mojo::WebSocketProxy::Backend

=head1 DESCRIPTION

A subclass of L<Mojo::WebSocketProxy::Backend> which dispatched RPC requests
over JSON-RPC over HTTP/HTTPS.

=head1 METHODS

=head2 url

    $url = $backend->url

Returns the configured default dispatch URL.

=head2 call_rpc

Implements the L<Mojo::WebSocketProxy::Backend/call_rpc> interface.

=head1 SEE ALSO

L<Mojolicious::Plugin::WebSocketProxy>,
L<Mojo::WebSocketProxy>
L<Mojo::WebSocketProxy::Dispatcher>,
L<Mojo::WebSocketProxy::Config>
L<Mojo::WebSocketProxy::Parser>

=cut