package HTTP::API::Client;

use strict;
use warnings;
use HTTP::Tiny;
use JSON::PP qw(encode_json decode_json);
use Scalar::Util qw(blessed);

use HTTP::API::Client::Response;
use HTTP::API::Client::Error;

our $VERSION = '0.01';

sub new {
    my ($class, %args) = @_;

    my $base_url = delete $args{base_url};
    die "base_url is required\n" if !defined($base_url) || $base_url eq '';
    $base_url =~ s{/+\z}{};

    my $headers = delete($args{headers}) || {};
    die "headers must be a hash reference\n" if ref($headers) ne 'HASH';

    my $timeout = exists $args{timeout} ? delete($args{timeout}) : 10;
    die "timeout must be a positive number\n" if !defined($timeout) || $timeout !~ /\A(?:\d+(?:\.\d*)?|\.\d+)\z/ || $timeout <= 0;

    my $transport = delete $args{transport};
    die "transport must be a code reference\n" if defined($transport) && ref($transport) ne 'CODE';

    die "unknown constructor option: $_\n" for sort keys %args;

    my $self = bless {
        base_url  => $base_url,
        headers   => { %$headers },
        timeout   => $timeout,
        transport => $transport,
    }, $class;

    $self->{http} = HTTP::Tiny->new(timeout => $timeout) if !$transport;
    return $self;
}

sub base_url { $_[0]->{base_url} }
sub timeout  { $_[0]->{timeout} }

sub get    { my ($self, $path, %opts) = @_; return $self->request('GET',    $path, %opts) }
sub post   { my ($self, $path, %opts) = @_; return $self->request('POST',   $path, %opts) }
sub put    { my ($self, $path, %opts) = @_; return $self->request('PUT',    $path, %opts) }
sub patch  { my ($self, $path, %opts) = @_; return $self->request('PATCH',  $path, %opts) }
sub delete { my ($self, $path, %opts) = @_; return $self->request('DELETE', $path, %opts) }

sub request {
    my ($self, $method, $path, %opts) = @_;
    $method = uc($method // '');
    die "method is required\n" if $method eq '';
    die "path is required\n" if !defined $path;

    my $url = $path =~ m{\Ahttps?://} ? $path : $self->_join_url($path);

    my %headers = (%{ $self->{headers} }, %{ delete($opts{headers}) || {} });
    my $content;

    if (exists $opts{json}) {
        my $value = delete $opts{json};
        $content = eval { encode_json($value) };
        if ($@) {
            die HTTP::API::Client::Error->new(
                category => 'encode',
                method   => $method,
                url      => $url,
                message  => "failed to encode JSON request: $@",
            );
        }
        $headers{'content-type'} ||= 'application/json';
        $headers{'accept'}       ||= 'application/json';
    }
    elsif (exists $opts{content}) {
        $content = delete $opts{content};
    }

    die "unknown request option: $_\n" for sort keys %opts;

    my $raw;
    eval {
        if ($self->{transport}) {
            $raw = $self->{transport}->($method, $url, {
                headers => \%headers,
                (defined($content) ? (content => $content) : ()),
            });
        }
        else {
            $raw = $self->{http}->request($method, $url, {
                headers => \%headers,
                (defined($content) ? (content => $content) : ()),
            });
        }
        1;
    } or do {
        my $cause = $@;
        die $cause if blessed($cause) && $cause->isa('HTTP::API::Client::Error');
        die HTTP::API::Client::Error->new(
            category  => 'transport',
            method    => $method,
            url       => $url,
            retryable => 1,
            message   => "HTTP transport failed: $cause",
        );
    };

    if (ref($raw) ne 'HASH' || !exists $raw->{status}) {
        die HTTP::API::Client::Error->new(
            category => 'transport', method => $method, url => $url,
            message => 'HTTP transport returned an invalid response',
        );
    }

    my $response = HTTP::API::Client::Response->new(
        status  => 0 + $raw->{status},
        reason  => $raw->{reason},
        headers => $raw->{headers} || {},
        content => defined($raw->{content}) ? $raw->{content} : '',
        method  => $method,
        url     => $url,
    );

    if (!$response->is_success) {
        die HTTP::API::Client::Error->new(
            category   => 'http',
            status     => $response->status,
            method     => $method,
            url        => $url,
            retryable  => _retryable_status($response->status),
            retry_after => $response->header('retry-after'),
            request_id => _request_id($response),
            response   => $response,
            message    => sprintf('HTTP %d%s', $response->status, defined($response->reason) && length($response->reason) ? ' ' . $response->reason : ''),
        );
    }

    return $response;
}

sub _join_url {
    my ($self, $path) = @_;
    $path =~ s{\A/+}{};
    return $self->{base_url} . '/' . $path;
}

sub _retryable_status {
    my ($status) = @_;
    return 1 if $status == 408 || $status == 425 || $status == 429;
    return 1 if $status >= 500 && $status <= 599;
    return 0;
}

sub _request_id {
    my ($response) = @_;
    for my $name (qw(x-request-id request-id x-correlation-id)) {
        my $value = $response->header($name);
        return $value if defined $value && length $value;
    }
    return undef;
}

1;

__END__

=head1 NAME

HTTP::API::Client - Small foundation for JSON HTTP API clients

=head1 SYNOPSIS

  use HTTP::API::Client;

  my $api = HTTP::API::Client->new(
      base_url => 'https://api.example.com',
      headers  => { Authorization => "Bearer $token" },
      timeout  => 10,
  );

  my $response = $api->get('/users');
  my $users = $response->json;

  my $created = $api->post('/users', json => { name => 'Alice' });

=head1 DESCRIPTION

HTTP::API::Client is a deliberately small base layer for building HTTP API
clients. Version 0.01 provides base URL handling, default headers, JSON request
encoding, JSON response decoding, timeout configuration, and structured errors.

It uses L<HTTP::Tiny> and L<JSON::PP>, both available with modern Perl.

=head1 METHODS

=head2 new

  my $api = HTTP::API::Client->new(
      base_url => 'https://api.example.com',
      headers  => { ... },
      timeout  => 10,
  );

C<base_url> is required. C<headers> and C<timeout> are optional.

=head2 get, post, put, patch, delete

Convenience methods around C<request>.

=head2 request

  my $response = $api->request('POST', '/items', json => { ... });

Pass C<json> to encode a Perl value as JSON, or C<content> to send raw content.
Per-request C<headers> override default headers.

Non-2xx responses throw L<HTTP::API::Client::Error>.

=head1 ERROR HANDLING

Errors expose stable fields such as C<category>, C<status>, C<method>, C<url>,
C<retryable>, C<retry_after>, and C<request_id>. Exact error message wording is
not intended as a machine-readable API.

=head1 LICENSE

This library is free software; you may redistribute it and/or modify it under
the same terms as Perl itself.

=cut
