package Business::OnlineThirdPartyPayment::PayPal;

use strict;
use base 'Business::OnlineThirdPartyPayment';

use strict;
use LWP;
use JSON;
use URI;
use Cache::FileCache;
use Crypt::CBC;

our $VERSION = '0.01';
our $ENDPOINT_SANDBOX = 'api.sandbox.paypal.com';
our $ENDPOINT_LIVE    = 'api.paypal.com';

our $DEBUG = 0;

sub set_defaults {
  my $self = shift;
  my %args = @_;
  $self->build_subs(qw(username password error_object host access_token));
  if ( $args{debug} ) {
    $DEBUG = $args{debug};
  }
}

sub authenticate {
  my $self = shift;
  my $host = shift;

  die "PayPal client ID (username) must be configured\n"
    unless $self->username;
  die "PayPay client secret (password) must be configured\n"
    unless $self->password;

  $self->{cache} = Cache::FileCache->new(
    { cache_root => File::Spec->tmpdir,
      namespace  => 'BOTP-PayPal' }
  );
  $self->{cipher} = Crypt::CBC->new( -key     => $self->password,
                                     -cipher  => 'Blowfish' );

  if ( my $token = $self->cache->get($self->username) ) {
    $self->access_token( $self->cipher->decrypt($token) );
  } else {
    my $ua = LWP::UserAgent->new;
    my $auth_request = HTTP::Request->new(POST => "$host/v1/oauth2/token");
    $auth_request->header('Accept' => 'application/json');
    # documentation says application/json; it lies.
    $auth_request->header('Content-Type'=>
                              'application/x-www-form-urlencoded');
    $auth_request->authorization_basic( $self->username, $self->password );
    $auth_request->content('grant_type=client_credentials');
    warn "Sending authentication request.\n" if $DEBUG;
    my $auth_response = $ua->request($auth_request);
    unless ( $auth_response->is_success ) {
      die "Authentication failed: ".$auth_response->status_line."\n".
        $auth_response->content;
    }
    warn "Authentication response:\n".$auth_response->content."\n\n"
      if $DEBUG > 2;
    my $hash = decode_json($auth_response->content);
    my $token = $hash->{access_token};
    $self->access_token($token);
    $self->cache->set($self->username, $self->cipher->encrypt( $token ),
      $hash->{expires_in} - 5);
  }
  return $self->access_token;
}

sub cache { $_[0]->{cache} }

sub cipher { $_[0]->{cipher} }

sub rest {
  my ($self, $path, $content) = @_;
  my $host = $self->host;

  if ( $self->test_transaction ) {
    $host ||= $ENDPOINT_SANDBOX;
  } else {
    $host ||= $ENDPOINT_LIVE;
  }
  $host = 'https://'.$host;

  my $token = $self->access_token || $self->authenticate($host);
  my $ua = LWP::UserAgent->new;

  my $json_request = encode_json($content);
  warn "REQUEST:\n$json_request\n\n" if $DEBUG >= 2;

  my $url = $host . $path;
  warn "Sending to $url\n" if $DEBUG;

  my $request = HTTP::Request->new(POST => $url);
  $request->header('Accept'         => 'application/json');
  $request->header('Authorization'  => "Bearer $token");
  $request->header('Content-Type'   => 'application/json');
  $request->content($json_request);

  my $response = $ua->request($request);
  if ( !$response ) {
    die "API request failed: ".$response->status_line."\n".
        $response->content;
  }
  warn "RESPONSE:" . $response->status_line."\n".$response->content."\n\n"
    if $DEBUG >= 2;

  if ( $response->is_success ) {
    $self->is_success(1);
    return decode_json($response->content);
  } else {
    $self->is_success(0);
    if ( $response->content ) {
      my $error = decode_json($response->content);
      $self->error_object($error);
      my $error_message = sprintf("%s: %s",
                    $error->{'name'}, $error->{'message'});
      if ( $error->{'details'} ) {
        foreach (@{ $error->{'details'} }) {
          $error_message .= sprintf("\n%s:\t%s", $_->{'field'}, $_->{'issue'});
        }
      }
      $self->error_message($error_message);
      return $error;
    } else {
      $self->error_object({});
      $self->error_message($response->status_line);
      return {};
    }
  }
}

sub create {
  my $self = shift;
  my %content = @_;
  my $return_url = URI->new($self->return_url)
    or die "return_url required";
  my $cancel_url = URI->new($self->cancel_url)
    or die "cancel_url required";
  
  my $request = 
    {
      intent  => 'sale',
      payer   => {
        payment_method  => 'paypal',
      },
      transactions => [
        {
          amount => {
            total => $content{'amount'},
            currency => ($content{'currency'} || 'USD'),
          },
          description => $content{'description'},
        },
      ],
      redirect_urls => {
        return_url => $return_url->as_string,
        cancel_url => $cancel_url->as_string,
      },
    };

  my $response = $self->rest('/v1/payments/payment', $request);

  if ( $self->is_success ) {
    $self->token($response->{'id'});

    my %links = map { $_->{rel} => $_->{href} } @{ $response->{'links'} };
    $self->redirect($links{'approval_url'});
    # other links are "self", which is where we just posted,
    # and "execute_url", which we can determine from the payment id
  }
}

sub execute {
  my $self = shift;
  my %params = @_;
  #my $payer_id = $params{'payer_id'} # documentation is wrong here
  my $payer_id = $params{'PayerID'}
    or die "cannot complete payment: missing PayerID"; #payer_id";
  
  my $request = { 'payer_id' => $payer_id };
  $self->order_number($self->token);
  my $execute_path = '/v1/payments/payment/' . $self->token. '/execute';
  $self->rest($execute_path, $request);
}

1;
__END__

=head1 NAME

Business::OnlineThirdPartyPayment::PayPal

=head1 DESCRIPTION

=head1 AUTHOR

Mark Wells <mark@freeside.biz>

Based in part on Net::PayPal, by Sherzod B. Ruzmetov <sherzodr@cpan.org>.

=head1 SEE ALSO

perl(1). L<Business::OnlineThirdPartyPayment>.

=cut

