package JSON::RPC::Dispatch;
use strict;
use JSON::RPC::Constants qw(:all);
use JSON::RPC::Parser;
use JSON::RPC::Procedure;
use Router::Simple;
use Scalar::Util;
use Try::Tiny;

use Class::Accessor::Lite
    rw => [ qw(
        coder
        handlers
        parser
        prefix
        router
    ) ]
;

sub new {
    my ($class, @args) = @_;
    my $self = bless {
        handlers => {},
        @args,
    }, $class;
    if (! $self->{coder}) {
        require JSON;
        $self->{coder} = JSON->new->utf8;
    }
    if (! $self->{parser}) {
        $self->{parser} = JSON::RPC::Parser->new( coder => $self->coder )
    }
    if (! $self->{router}) {
        $self->{router} = Router::Simple->new;
    }
    return $self;
}

sub guess_handler_class {
    my ($self, $klass) = @_;

    my $prefix = $self->prefix || '';
    return "$prefix\::$klass";
}

sub construct_handler {
    my ($self, $klass) = @_;

    my $handler = $self->handlers->{ $klass };
    if (! $handler) {
        eval "require $klass";
        die if $@;
        $handler = $klass->new();
        $self->handlers->{$klass} = $handler;
    }
    return $handler;
}

sub get_handler {
    my ($self, $klass) = @_;

    if ( Scalar::Util::blessed( $klass )){
        if (JSONRPC_DEBUG > 1) {
            warn "Handler is already object : $klass";
        }
        return $klass;
    }

    if ($klass !~ s/^\+//) {
        $klass = $self->guess_handler_class( $klass );
    }

    my $handler = $self->construct_handler( $klass );
    if (JSONRPC_DEBUG > 1) {
        warn "$klass -> $handler";
    }
    return $handler;
}

sub handle_psgi {
    my ($self, $req, @args) = @_;

    if ( ! Scalar::Util::blessed($req) ) {
        # assume it's a PSGI hash
        require Plack::Request;
        $req = Plack::Request->new($req);
    }

    my $is_batch = 0;
    my @response;
    my $procedures;
    try {
        $procedures = $self->parser->construct_from_req( $req );
        if (ref $procedures eq 'ARRAY') {
            $is_batch = 1;
        } else {
            $procedures = [$procedures];
        }
        if (@$procedures <= 0 || not defined $procedures->[0]) {
            $is_batch = 0;
            push @response, {
                error => {
                    code => RPC_INVALID_REQUEST,
                    message => "Could not find any procedures"
                }
            };
        }
    } catch {
        my $e = $_;
        if (JSONRPC_DEBUG) {
            warn "error while creating jsonrpc request: $e";
        }
        if ($e =~ /Invalid parameter/) {
            push @response, {
                error => {
                    code => RPC_INVALID_PARAMS,
                    message => "Invalid parameters",
                }
            };
        } elsif ( $e =~ /parse error/ ) {
            push @response, {
                error => {
                    code => RPC_PARSE_ERROR,
                    message => "Failed to parse json",
                }
            };
        } else {
            push @response, {
                error => {
                    code => RPC_INVALID_REQUEST,
                    message => $e
                }
            }
        }
    };

    my $router = $self->router;
    foreach my $procedure (@$procedures) {
        if ( ! $procedure->{method} ) {
            my $message = "Procedure name not given";
            if (JSONRPC_DEBUG) {
                warn $message;
            }
            push @response, {
                error => {
                    code => RPC_METHOD_NOT_FOUND,
                    message => $message,
                }
            };
            next;
        }

        my $is_notification = defined $procedure->jsonrpc && $procedure->jsonrpc eq '2.0' && !$procedure->has_id;
        my $matched = $router->match( $procedure->{method} );
        if (! $matched) {
            my $message = "Procedure '$procedure->{method}' not found";
            if (JSONRPC_DEBUG) {
                warn $message;
            }
            if (!$is_notification) { # must not respond to a valid JSON-RPC notification
                push @response, {
                    error => {
                        code => RPC_METHOD_NOT_FOUND,
                        message => $message,
                    }
                };
            }
            next;
        }

        my $action = $matched->{action};
        try {
            my ($ip, $ua);
            if (JSONRPC_DEBUG > 1) {
                warn "Procedure '$procedure->{method}' maps to action $action";
                $ip = $req->address || 'N/A';
                $ua = $req->user_agent || 'N/A';
            }
            my $params = $procedure->params;
            my $handler = $self->get_handler( $matched->{handler} );

            my $code = $handler->can( $action );
            if (! $code) {
                if ( JSONRPC_DEBUG ) {
                    warn "[INFO] handler $handler does not implement method $action!.";
                }
                die "Internal Error";
            }
            my $result = $code->( $handler, $procedure->params, $procedure, @args );
            if (JSONRPC_DEBUG) {
                warn "[INFO] action=$action "
                    . "params=["
                    . (ref $params ? $self->{coder}->encode($params) : $params)
                    . "] ret="
                    . (ref $result ? $self->{coder}->encode($result) : $result)
                    . " IP=$ip UA=$ua";
            }

            # respond unless we are sure a procedure is a notification
            if (!$is_notification) {
                push @response, {
                    jsonrpc => '2.0',
                    result  => $result,
                    id      => $procedure->id,
                };
            }
        } catch {
            my $e = $_;
            if (JSONRPC_DEBUG) {
                warn "Error while executing $action: $e";
            }
            # can't respond to notifications even in case of errors
            if (!$is_notification) {
                my $error = {code => RPC_INTERNAL_ERROR} ;
                if (ref $e eq "HASH") {
                   $error->{message} = $e->{message},
                   $error->{data}    = $e->{data},
                } else {
                   $error->{message} = $e,
                }
                push @response, {
                    jsonrpc => '2.0',
                    id      => $procedure->id,
                    error   => $error,
                };
            }
        };
    }

    my $res;
    if (scalar @response) {
        $res = $req->new_response(200);
        $res->content_type( 'application/json; charset=utf8' );
        $res->body(
            $self->coder->encode( ($is_batch) ? \@response : $response[0] )
        );
        return $res->finalize;
    } else { # no content
        $res = $req->new_response(204);
    }

    return $res->finalize;
}

no Try::Tiny;

1;

__END__

=head1 NAME

JSON::RPC::Dispatch - Dispatch JSON RPC Requests To Handlers

=head1 SYNOPSIS

    use JSON::RPC::Dispatch;

    my $router = Router::Simple->new; # or use Router::Simple::Declare
    $router->connect( method_name => {
        handler => $class_name_or_instance,
        action  => $method_name_to_invoke
    );

    my $dispatch = JSON::RPC::Dispatch->new(
        router => $router
    );

    sub psgi_app {
        $dispatch->handle_psgi( $env );
    }

=head1 DESCRIPTION

See docs in L<JSON::RPC> for details

=cut
