package cPanel::APIClient;

use strict;
use warnings;

our $VERSION = '0.09_90';

=encoding utf-8

=head1 NAME

cPanel::APIClient - L<cPanel|http://cpanel.com> APIs, à la TIMTOWTDI!

=head1 SYNOPSIS

Create a L<cPanel::APIClient::Service::cpanel> object
to call cPanel APIs:

    my $cpanel = cPanel::APIClient->create(
        service => 'cpanel',
        transport => [ 'CLISync' ],
    );

    my $resp = $cpanel->call_uapi( 'Email', 'list_pops' );

    my $pops_ar = $resp->get_data();

Create a L<cPanel::APIClient::Service::whm> object
to call WHM APIs:

    my $whm = cPanel::APIClient->create(
        service => 'whm',
        transport => [ 'CLISync' ],
    );

    my $resp = $whm->call_api1( 'listaccts' );

    my $accts_ar = $resp->get_data();

=head1 DESCRIPTION

cPanel & WHM exposes a number of ways to access its APIs: different transport
mechanisms, different authentication schemes, etc. This library provides
client logic with sufficient abstractions to accommodate most supported
access mechanisms via a unified interface.

This library intends to supersede L<cPanel::PublicAPI> as the preferred way
to access cPanel & WHM’s APIs from Perl. It can also serve as a model for
similar client libraries in other languages.

=head1 FEATURES

=over

=item * Fully object-oriented.

=item * Can use blocking or non-blocking I/O. Non-blocking I/O implementation
works with almost any modern Perl event loop interface.

=item * Uses minimal dependencies: no L<Moose> &c.

=item * Extensively tested.

=item * Can run in pure Perl.

=back

=head1 CHARACTER ENCODING

cPanel & WHM’s API is character-set-agnostic. All text that you give to this
library should thus be encoded to binary, and all strings that you’ll receive
back will be binary.

This means that if you character-decode your inputs—as L<perlunitut>
recommends—then you’ll need to encode your strings back to bytes before
giving them to this module.

Use of UTF-8 encoding is B<strongly> recommended!

=head1 FUNCTIONS

=head2 $client = cPanel::APIClient->create( %OPTS )

A factory function that creates a “client” object that your code can
use to call the APIs.

%OPTS are:

=over

=item * C<service> - Required. The service that exposes the API(s) to call.
This controls the class of the returned object. Recognized values are:

=over

=item * C<cpanel> - Function will return a L<cPanel::APIClient::Service::cpanel>
instance.

=item * C<whm> - Function will return a L<cPanel::APIClient::Service::whm>
instance.

=back

=item * C<transport> - Required. An array reference that describes the
transport mechanism to use. The first member of this array names the mechanism;
remaining arguments are key-value pairs of attributes to give to the
mechanism class’s constructor.

Currently supported mechanisms are:

=over

=item * L<cPanel::APIClient::Transport::HTTPSync> (C<HTTPSync>) -
Synchronous HTTP requests.

=item * L<cPanel::APIClient::Transport::CLISync> (C<CLISync>) -
Synchronous local requests via cPanel & WHM’s command-line API tools.

=item * L<cPanel::APIClient::Transport::NetCurlPromiser> (C<NetCurlPromiser>) -
Asynchronous HTTP requests via
L<Net::Curl::Promiser>, which can use any event loop interface.
As of this writing it supports L<IO::Async>, L<AnyEvent>, and L<Mojolicious>
out-of-the-box.

=item * L<cPanel::APIClient::Transport::MojoUserAgent> (C<MojoUserAgent>) -
Asynchronous HTTP requests via L<Mojo::UserAgent> (pure Perl).

=back

Which of the above to use will depend on your needs. If your application
is local to the cPanel & WHM server you might find it easiest to use
C<CLISync>. For HTTP C<NetCurlPromiser> offers the best flexibility
and (probably) speed, whereas C<MojoUserAgent> and C<HTTPSync> can run in
pure Perl (assuming you have L<Net::SSLeay>).

There currently is no documentation for how to create a 3rd-party transport
mechanism (e.g., if you want to use a different HTTP library). Submissions
via pull request will be evaluated on a case-by-case basis.

=item * C<credentials> - Some transports require this; others don’t.
The recognized schemes are:

=over

=item * C<username> & C<api_token> - Authenticate with an API token

=item * C<username> & C<password> - Authenticate with a password

=item * C<username>, C<password>, & C<tfa_token> - Authenticate with a
password and two-factor authentication (2FA) token.

=item * C<username> only - Implicit authentication, only usable for local
transports.

=back

=back

Depending on the C<service> given, this function returns an instance of
either L<cPanel::APIClient::Service::cpanel> or
L<cPanel::APIClient::Service::whm>.

=cut

my @_REQUIRED = ( 'service', 'transport' );

sub create {

    # We don’t need the class, but we mandate arrow syntax rather
    # than static because it seems more consistent with Perl programmers’
    # expectations of what it looks like to call a function whose purpose
    # is to create an object.
    shift;

    my (%opts) = @_;

    my @missing = grep { !defined $opts{$_} } @_REQUIRED;
    die "Missing: @missing" if @missing;

    my $creds = delete $opts{'credentials'};

    my ( $svc, $transport ) = delete @opts{@_REQUIRED};

    if ( my @extra = sort keys %opts ) {
        die "Extra: @extra";
    }

    my $full_ns = "cPanel::APIClient::Service::$svc";
    _require($full_ns);

    my $authn = $creds && _parse_creds($creds);

    $transport = _parse_transport( $transport, $authn, $svc );

    return $full_ns->new( $transport, $authn );
}

sub _parse_transport {
    my ( $transport, $authn, $service_name ) = @_;

    if ( 'ARRAY' ne ref $transport ) {
        die "“transport” should be an ARRAY reference, not $transport!";
    }

    my ( $module, @args ) = (
        @$transport,
        service_name => $service_name,
    );

    $module = "cPanel::APIClient::Transport::$module";
    _require($module);

    if ( $module->NEEDS_CREDENTIALS() && !$authn ) {
        die "Transporter “$transport” requires credentials!";
    }

    return $module->new( $authn, @args );
}

sub _require {
    my ($full_ns) = @_;

    die if !eval "require $full_ns; 1";
}

sub _parse_creds {
    my ($creds_hr) = @_;

    my %creds_copy = %$creds_hr;

    my $username = delete $creds_copy{'username'};

    if ( !defined $username ) {
        die "Credentials need “username”!";
    }

    my ( @extras, $class );

    if ( exists $creds_copy{'api_token'} ) {
        @extras = ('api_token');
        $class  = 'cPanel::APIClient::Authn::Token';
    }
    elsif ( exists $creds_copy{'password'} ) {
        @extras = ('password');

        if ( exists $creds_copy{'tfa_token'} ) {
            push @extras, 'tfa_token';
            $class = 'cPanel::APIClient::Authn::Password2FA';
        }
        else {
            $class = 'cPanel::APIClient::Authn::Password';
        }
    }
    else {
        $class = 'cPanel::APIClient::Authn::Username';
    }

    for my $key (@extras) {
        my $val = delete $creds_copy{$key};
        if ( !defined $val ) {
            die "Undefined “$key” is invalid!";
        }
    }

    die "Bad “credentials”!" if %creds_copy;

    _require($class);

    return $class->new( $username, @{$creds_hr}{@extras} );
}

=head1 LICENSE

Copyright 2020 cPanel, L. L. C. All rights reserved. L<http://cpanel.net>

This is free software; you can redistribute it and/or modify it under the
same terms as Perl itself. See L<perlartistic>.

=cut

1;
