package Net::OATH::Server::Lite::Endpoint::User;
use strict;
use warnings;
use overload
    q(&{})   => sub { shift->psgi_app },
    fallback => 1;

use Try::Tiny qw/try catch/;
use Plack::Request;
use Params::Validate;
use JSON::XS qw/decode_json encode_json/;
use Convert::Base32 qw/encode_base32/;

use Net::OATH::Server::Lite::Error;

sub new {
    my $class = shift;
    my %args = Params::Validate::validate(@_, {
        data_handler => 1,
    });
    my $self = bless {
        data_handler   => $args{data_handler},
    }, $class;
    return $self;
}

sub data_handler {
    my ($self, $handler) = @_;
    $self->{data_handler} = $handler if $handler;
    $self->{data_handler};
}

sub psgi_app {
    my $self = shift;
    return $self->{psgi_app}
        ||= $self->compile_psgi_app;
}

sub compile_psgi_app {
    my $self = shift;

    my $app = sub {
        my $env = shift;
        my $req = Plack::Request->new($env);
        my $res; try {
            $res = $self->handle_request($req);
        } catch {
            # Internal Server Error
            warn $_;
            $res = $req->new_response(500);
        };
        return $res->finalize;
    };

    return $app;
}

sub handle_request {
    my ($self, $request) = @_;

    my $res = try {
        my $code = 200;

        my $data_handler = $self->{data_handler}->new(request => $request);
        Net::OATH::Server::Lite::Error->throw(
            code => 500,
            error => q{server_error},
        ) unless ($data_handler && $data_handler->isa(q{Net::OATH::Server::Lite::DataHandler}));

        # HTTP method MUST be POST
        Net::OATH::Server::Lite::Error->throw() unless ($request->method eq q{POST});

        # content MUST be JSON
        my $content = {};
        eval {
            $content = decode_json($request->content) if $request->content;
        };
        Net::OATH::Server::Lite::Error->throw() if $@;

        unless (defined $content->{method} &&
                ($content->{method} eq q{create} ||
                 $content->{method} eq q{read} ||
                 $content->{method} eq q{update} ||
                 $content->{method} eq q{delete})) {
            Net::OATH::Server::Lite::Error->throw(
                description => q{method not found},
            );
        }

        my $user;
        if ($content->{method} eq q{create}) {
            if ($content->{id}) {
                Net::OATH::Server::Lite::Error->throw();
            } else {
                $user = Net::OATH::Server::Lite::Model::User->new(
                   id => $data_handler->create_id(),
                   secret => $data_handler->create_secret(),
                );

                $user->type($content->{type}) if $content->{type};
                $user->algorithm($content->{algorithm}) if $content->{algorithm};
                $user->digits($content->{digits}) if $content->{digits};
                $user->counter($content->{counter}) if $content->{counter};
                $user->period($content->{period}) if $content->{period};
                Net::OATH::Server::Lite::Error->throw() unless $user->is_valid;

                unless ($data_handler->insert_user($user)) {
                    Net::OATH::Server::Lite::Error->throw(
                        code => 500,
                        error => q{server_error},
                    );
                }

                $code = 201;
            }
        }

        if ($content->{method} eq q{read}) {
            if ($content->{id}) {
                $user = $data_handler->select_user($content->{id}) or
                        Net::OATH::Server::Lite::Error->throw(
                            code => 404,
                            description => q{invalid id},
                        );
            } else {
                Net::OATH::Server::Lite::Error->throw(
                    description => q{missing id},
                );
            }
        }

        if ($content->{method} eq q{update}) {
            if ($content->{id}) {
                $user = $data_handler->select_user($content->{id}) or
                        Net::OATH::Server::Lite::Error->throw(
                            code => 404,
                            description => q{invalid id},
                        );

                $user->type($content->{type}) if $content->{type};
                $user->algorithm($content->{algorithm}) if $content->{algorithm};
                $user->digits($content->{digits}) if $content->{digits};
                $user->counter($content->{counter}) if $content->{counter};
                $user->period($content->{period}) if $content->{period};
                Net::OATH::Server::Lite::Error->throw() unless $user->is_valid;

                unless ($data_handler->update_user($user)) {
                    Net::OATH::Server::Lite::Error->throw(
                        code => 500,
                        error => q{server_error},
                    );
                }
            } else {
                Net::OATH::Server::Lite::Error->throw(
                    description => q{missing id},
                );
            }
        }

        if ($content->{method} eq q{delete}) {
            if ($content->{id}) {
                $user = $data_handler->select_user($content->{id}) or
                        Net::OATH::Server::Lite::Error->throw(
                            code => 404,
                            description => q{invalid id},
                        );

                unless ($data_handler->delete_user($user->id)) {
                    Net::OATH::Server::Lite::Error->throw(
                        code => 500,
                        error => q{server_error},
                    );
                }
            } else {
                Net::OATH::Server::Lite::Error->throw(
                    description => q{missing id},
                );
            }
        }

        my $params = ($content->{method} eq q{delete}) ? {} :
                     _create_response_from_user($user);

        return $request->new_response($code,
            [ "Content-Type"  => "application/json;charset=UTF-8",
              "Cache-Control" => "no-store",
              "Pragma"        => "no-cache" ],
            [ encode_json($params) ]);
    } catch {
        if ($_->isa("Net::OATH::Server::Lite::Error")) {
            my $error_params = {
                error => $_->error,
            };
            $error_params->{error_description} = $_->description if $_->description;

            return $request->new_response($_->code,
                [ "Content-Type"  => "application/json;charset=UTF-8",
                  "Cache-Control" => "no-store",
                  "Pragma"        => "no-cache" ],
                [ encode_json($error_params) ]);
        } else {
            die $_;
        }
    };
}

sub _create_response_from_user {
    my ($user) = @_;
    return unless ($user && $user->isa(q{Net::OATH::Server::Lite::Model::User}));

    return {
        id        => $user->id,
        secret    => encode_base32($user->secret),
        type      => $user->type,
        algorithm => $user->algorithm,
        digits    => $user->digits,
        counter   => $user->counter,
        period    => $user->period,
    };
}

1;