package Net::Payjp;

use strict;
use warnings;

use LWP::UserAgent;
use LWP::Protocol::https;
use HTTP::Request::Common;
use JSON;
use List::Util qw/min/;
use POSIX qw/floor/;

use Net::Payjp::Account;
use Net::Payjp::Charge;
use Net::Payjp::Customer;
use Net::Payjp::Plan;
use Net::Payjp::Subscription;
use Net::Payjp::Token;
use Net::Payjp::Transfer;
use Net::Payjp::Event;
use Net::Payjp::Tenant;
use Net::Payjp::TenantTransfer;
use Net::Payjp::Statement;
use Net::Payjp::Balance;
use Net::Payjp::Term;
use Net::Payjp::Object;
use Net::Payjp::ThreeDSecureRequest;

# ABSTRACT: API client for pay.jp

=head1 SYNOPSIS

 # Create charge
 my $payjp = Net::Payjp->new(api_key => $API_KEY);
 my $res = $payjp->charge->create(
   card => 'token_id_by_Checkout_or_payjp.js',
   amount => 3500,
   currency => 'jpy',
 );
 if(my $e = $res->error){
   print "Error;
   print $e->{message}."\n";
 }
 # Id of charge.
 print $res->id;

 # Retrieve a charge
 $payjp->id($res->id); # Set id of charge
 $res = $payjp->charge->retrieve; # or $payjp->charge->retrieve($res->id);

=head1 DESCRIPTION

This module is a wrapper around the Pay.jp HTTP API.Methods are generally named after the object name and the acquisition method.

This method returns json objects for responses from the API.

=head1 new Method

This creates a new Payjp api object. The following parameters are accepted:

=over

=item api_key

This is required. You get this from your Payjp Account settings.

=back

=cut

our $VERSION = '0.4.0';
our $API_BASE = 'https://api.pay.jp';
our $INITIAL_DELAY_SEC = 2;
our $MAX_DELAY_SEC = 32;

sub new{
  my $self = shift;
  bless{__PACKAGE__->_init(@_)},$self;
}

sub _init{
  my $self = shift;
  my %p = @_;
  return(
    api_key  => $p{api_key},
    id       => $p{id},
    api_base => $API_BASE,
    max_retry     => $p{max_retry} || 0,
    initial_delay => $p{initial_delay} || $INITIAL_DELAY_SEC,
    max_delay     => $p{max_delay} || $MAX_DELAY_SEC,
  );
}

sub api_key{
  my $self = shift;
  $self->{api_key} = shift if @_;
  return $self->{api_key};
}

sub api_base{
  my $self = shift;
  $self->{api_base} = shift if @_;
  return $self->{api_base};
}

sub id{
  my $self = shift;
  $self->{id} = shift if @_;
  return $self->{id};
}

=head1 Charge Methods

=head2 create

Create a new charge

L<https://pay.jp/docs/api/#支払いを作成>

 $payjp->charge->create(
   card => 'tok_76e202b409f3da51a0706605ac81',
   amount => 3500,
   currency => 'jpy',
   description => 'yakiimo',
 );

=head2 retrieve

Retrieve a charge

L<https://pay.jp/docs/api/#支払いを情報を取得>

 $payjp->charge->retrieve('ch_fa990a4c10672a93053a774730b0a');

=head2 save

Update a charge

L<https://pay.jp/docs/api/#支払いを情報を取得>

 $payjp->id('ch_fa990a4c10672a93053a774730b0a');
 $payjp->charge->save(description => 'update description.');

=head2 refund

Refund a charge

L<https://pay.jp/docs/api/#返金する>

 $payjp->id('ch_fa990a4c10672a93053a774730b0a');
 $payjp->charge->refund(amount => 1000, refund_reason => 'test.');

=head2 capture

Capture a charge

L<https://pay.jp/docs/api/#支払い処理を確定する>

 $payjp->id('ch_fa990a4c10672a93053a774730b0a');
 $payjp->charge->capture(amount => 2000);

=head2 all

Returns the charge list

L<https://pay.jp/docs/api/#支払いリストを取得>

 $payjp->charge->all("limit" => 2, "offset" => 1);

=head1 Customer Methods

=head2 create

Create a cumtomer

L<https://pay.jp/docs/api/#顧客を作成>

 $payjp->customer->create(
   "description" => "test",
 );

=head2 retrieve

Retrieve a customer

L<https://pay.jp/docs/api/#顧客情報を取得>

 $payjp->customer->retrieve('cus_121673955bd7aa144de5a8f6c262');

=head2 save

Update a customer

L<https://pay.jp/docs/api/#顧客情報を更新>

 $payjp->id('cus_121673955bd7aa144de5a8f6c262');
 $payjp->customer->save(email => 'test@test.jp');

=head2 delete

Delete a customer

L<https://pay.jp/docs/api/#顧客を削除>

 $payjp->id('cus_121673955bd7aa144de5a8f6c262');
 $payjp->customer->delete;

=head2 all

Returns the customer list

L<https://pay.jp/docs/api/#顧客リストを取得>

$res = $payjp->customer->all(limit => 2, offset => 1);

=cut

sub charge{
  my $self = shift;
  return Net::Payjp::Charge->new(%$self);
}

=head1 Cutomer card Methods

Returns a customer's card object

 my $card = $payjp->customer->card('cus_4df4b5ed720933f4fb9e28857517');

=head2 create

Create a customer's card

L<https://pay.jp/docs/api/#顧客のカードを作成>

 $card->create(
   card => 'tok_76e202b409f3da51a0706605ac81'
 );

=head2 retrieve

Retrieve a customer's card

L<https://pay.jp/docs/api/#顧客のカード情報を取得>

 $card->retrieve('car_f7d9fa98594dc7c2e42bfcd641ff');

=head2 save

Update a customer's card

L<https://pay.jp/docs/api/#顧客のカードを更新>

$card->id('car_f7d9fa98594dc7c2e42bfcd641ff');
$card->save(exp_year => "2026", exp_month => "05", name => 'test');

=head2 delete

Delete a customer's card

L<https://pay.jp/docs/api/#顧客のカードを削除>

 $card->id('car_f7d9fa98594dc7c2e42bfcd641ff');
 $card->delete;

=head2 all

Returns the customer's card list

L<https://pay.jp/docs/api/#顧客のカードリストを取得>

 $card->all(limit => 2, offset => 0);

=head1 Customer subscription Methods

Returns a customer's subscription object

 my $subscription = $payjp->customer->subscription('sub_567a1e44562932ec1a7682d746e0');

=head2 retrieve

Retrieve a customer's subscription

L<https://pay.jp/docs/api/#顧客の定期課金情報を取得>

 $subscription->retrieve('sub_567a1e44562932ec1a7682d746e0');

=head2 all

Returns the customer's subscription list

L<https://pay.jp/docs/api/#顧客の定期課金リストを取得>

$subscription->all(limit => 1, offset => 0);

=cut

sub customer{
  my $self = shift;
  return Net::Payjp::Customer->new(%$self);
}

=head1 Plan Methods

=head2 create

Create a plan

L<https://pay.jp/docs/api/#プランを作成>

 $payjp->plan->create(
   amount => 500,
   currency => "jpy",
   interval => "month",
   trial_days => 30,
   name => 'test_plan'
 );

=head2 retrieve

Retrieve a plan

L<https://pay.jp/docs/api/#プラン情報を取得>

 $payjp->plan->retrieve('pln_45dd3268a18b2837d52861716260');

=head2 save

Update a plan

L<https://pay.jp/docs/api/#プランを更新>

 $payjp->id('pln_45dd3268a18b2837d52861716260');
 $payjp->plan->save(name => 'NewPlan');

=head2 delete

Delete a plan

L<https://pay.jp/docs/api/#プランを削除>

 $payjp->id('pln_45dd3268a18b2837d52861716260');
 $payjp->plan->delete;

=head2 all

Returns the plan list

L<https://pay.jp/docs/api/#プランリストを取得>

 $payjp->plan->all("limit" => 5, "offset" => 0);

=cut

sub plan{
  my $self = shift;
  return Net::Payjp::Plan->new(%$self);
}

=head1 Subscription Methods

=head2 create

Create a subscription

L<https://pay.jp/docs/api/#定期課金を作成>

 $payjp->subscription->create(
   customer => 'cus_4df4b5ed720933f4fb9e28857517',
   plan => 'pln_9589006d14aad86aafeceac06b60'
 );

=head2 retrieve

Retrieve a subscription

L<https://pay.jp/docs/api/#定期課金情報を取得>

 $payjp->subscription->retrieve('sub_567a1e44562932ec1a7682d746e0');

=head2 save

Update a subscription

L<https://pay.jp/docs/api/#定期課金を更新>

 $payjp->id('sub_567a1e44562932ec1a7682d746e0');
 $payjp->subscription->save(trial_end => 1473911903);

=head2 pause

Pause a subscription

L<https://pay.jp/docs/api/#定期課金を停止>

 $payjp->id('sub_567a1e44562932ec1a7682d746e0');
 $payjp->subscription->pause;

=head2 resume

Resume a subscription

L<https://pay.jp/docs/api/#定期課金を再開>

 $payjp->id('sub_567a1e44562932ec1a7682d746e0');
 $payjp->subscription->resume;

=head2 cancel

Cancel a subscription

L<https://pay.jp/docs/api/#定期課金をキャンセル>

 $payjp->id('sub_567a1e44562932ec1a7682d746e0');
 $payjp->subscription->cancel;

=head2 delete

Delete a subscription

L<https://pay.jp/docs/api/#定期課金を削除>

 $payjp->id('sub_567a1e44562932ec1a7682d746e0');
 $payjp->subscription->delete;

=head2 all

Returns the subscription list

L<https://pay.jp/docs/api/#定期課金のリストを取得>

 $payjp->subscription->all(limit => 3, offset => 0);

=cut

sub subscription{
  my $self = shift;
  return Net::Payjp::Subscription->new(%$self);
}

=head1 Token Methods

=head2 retrieve

Retrieve a token

L<https://pay.jp/docs/api/#トークン情報を取得>

$payjp->token->retrieve('tok_eff34b780cbebd61e87f09ecc9c6');

=head2 tds_finish

Finish 3D-Secure flow of token

L<https://pay.jp/docs/api/#%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AB%E5%AF%BE%E3%81%99%E3%82%8B3d%E3%82%BB%E3%82%AD%E3%83%A5%E3%82%A2%E3%83%95%E3%83%AD%E3%83%BC%E3%82%92%E5%AE%8C%E4%BA%86%E3%81%99%E3%82%8B>

 $payjp->id('tok_xxxxxx');
 $payjp->token->tds_finish();

=cut

sub token{
  my $self = shift;
  return Net::Payjp::Token->new(%$self);
}

=head1 Transfer Methods

=head2 retrieve

Retrieve a transfer

L<https://pay.jp/docs/api/#入金情報を取得>

 $payjp->transfer->retrieve('tr_8f0c0fe2c9f8a47f9d18f03959ba1');

=head2 all

Returns the transfer list

L<https://pay.jp/docs/api/#入金リストを取得>

 $payjp->transfer->all("limit" => 3, offset => 0);

=head2 charges

Returns the charge list

L<https://pay.jp/docs/api/#入金の内訳を取得>

 $payjp->transfer->charges(
   limit => 3,
   offset => 0
 );

=cut

sub transfer{
  my $self = shift;
  return Net::Payjp::Transfer->new(%$self);
}

=head1 ThreeDSecureRequest Methods

=head2 create

Create a three_d_secure_request

L<https://pay.jp/docs/api/#3d%E3%82%BB%E3%82%AD%E3%83%A5%E3%82%A2%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90>

 $payjp->three_d_secure_request->create(
   "resource_id" => "car_xxxx",
 );

=head2 retrieve

Retrieve a three_d_secure_request

L<https://pay.jp/docs/api/#3d%E3%82%BB%E3%82%AD%E3%83%A5%E3%82%A2%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E6%83%85%E5%A0%B1%E3%82%92%E5%8F%96%E5%BE%97>

 $payjp->three_d_secure_request->retrieve('tdsr_xxxx');

=head2 all

Returns the three_d_secure_request list

L<https://pay.jp/docs/api/#3d%E3%82%BB%E3%82%AD%E3%83%A5%E3%82%A2%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E5%8F%96%E5%BE%97>

$res = $payjp->three_d_secure_request->all(limit => 2, offset => 1);

=cut

sub three_d_secure_request{
  my $self = shift;
  return Net::Payjp::ThreeDSecureRequest->new(%$self);
}

=head1 Event Methods

=head2 retrieve

Retrieve a event

L<https://pay.jp/docs/api/#イベント情報を取得>

 $res = $payjp->event->retrieve('evnt_2f7436fe0017098bc8d22221d1e');

=head2 all

Returns the event list

L<https://pay.jp/docs/api/#イベントリストを取得>

$payjp->event->all(limit => 10, offset => 0);

=cut

sub event{
  my $self = shift;
  return Net::Payjp::Event->new(%$self);
}

=head1 Account Methods

=head2 retrieve

Retrieve a account

L<https://pay.jp/docs/api/#アカウント情報を取得>

 $payjp->account->retrieve;

=cut

sub account{
  my $self = shift;
  return Net::Payjp::Account->new(%$self);
}

sub tenant{
  my $self = shift;
  return Net::Payjp::Tenant->new(%$self);
}

sub tenant_transfer{
  my $self = shift;
  return Net::Payjp::TenantTransfer->new(%$self);
}

sub statement{
  my $self = shift;
  return Net::Payjp::Statement->new(%$self);
}

sub balance{
  my $self = shift;
  return Net::Payjp::Balance->new(%$self);
}

sub term{
  my $self = shift;
  return Net::Payjp::Term->new(%$self);
}

sub _request{
  my $self = shift;
  my %p = @_;

  my $url = $p{url};
  my $method = $p{method} || 'GET';
  my $retry = $p{retry} || 0;

  my $req;
  my $with_param;
  if(ref $p{param} eq 'HASH' and keys %{$p{param}} > 0) {
    $with_param = 1;
  }
  if($with_param and ($method eq 'GET' or $method eq 'DELETE')){
    my @param;
    foreach my $k(keys %{$p{param}}){
      push(@param, "$k=".$p{param}->{$k});
    }
    $url .= '?'.join("&", @param);
  }
  if($method eq 'POST' and $with_param){
    $req = POST($url, $self->_api_param(param => $p{param}));
  } else {
    $req = new HTTP::Request $method => $url;
  }

  $req->authorization_basic($self->api_key, '');
  my $ua = LWP::UserAgent->new();
  $ua->timeout(30);
  my $client = {
    'bindings_version' => $VERSION,
    'lang' => 'perl',
    'lang_version' => $],
    'publisher' => 'payjp',
    'uname' => $^O
  };
  $ua->default_header(
    'User-Agent' => 'Payjp/v1 PerlBindings/'.$VERSION,
    'X-Payjp-Client-User-Agent' => JSON->new->encode($client),
  );

  my $res = $ua->request($req);
  my $code = $res->code;
  if($code == 200){
    my $obj = $self->_to_object(JSON->new->decode($res->content));
    $self->id($obj->id) if $obj->id;
    return $obj;
  } elsif($code == 429 and $retry < $self->{max_retry}){
    sleep($self->_get_delay_sec(
      retry => $retry,
      init_sec => $self->{initial_delay},
      max_sec => $self->{max_delay}
    ));
    return $self->_request(method => $method, url =>$url, param => $p{param}, retry => $retry + 1);
  } elsif($code =~ /^4/){
    return $self->_to_object(JSON->new->decode($res->content));
  }
  return $self->_to_object(
    {
      error => {
        message => $res->message,
        status_code => $code,
      }
    }
  );
}

sub _get_delay_sec {
  my $self = shift;
  my %p = @_;
  my $retry = $p{retry}; # number
  my $init_sec = $p{init_sec}; # number
  my $max_sec = $p{max_sec}; # number

  return min($init_sec * 2 ** $retry, $max_sec) / 2 * (1 + rand(1));
}

sub _to_object{
  my $self = shift;
  my $hash = shift;

  return Net::Payjp::Object->new(%$hash);
}

sub _api_param{
  my $self = shift;
  my %p = @_;
  my $param = $p{param};

  my $req_param;
  foreach my $k(keys(%{$param})){
    if(ref($param->{$k}) eq 'HASH'){
      foreach(keys(%{$param->{$k}})){
        $req_param->{$k.'['.$_.']'} = $param->{$k}->{$_};
      }
    }
    else{
      $req_param->{$k} = $param->{$k};
    }
  }
  return $req_param;
}

sub _instance_url{
  my $self = shift;
  return $self->_class_url.'/'.($self->id or '');
}

sub _class_url{
  my $self = shift;
  my ($class) = lc(ref($self)) =~ /([^:]*$)/;
  return $self->api_base.'/v1/'.$class.'s';
}

1;
