package WWW::{{invokerPackage}}::ApiClient;

use strict;
use warnings;
use utf8;

use MIME::Base64;
use LWP::UserAgent;
use HTTP::Headers;
use HTTP::Response;
use HTTP::Request::Common qw(DELETE POST GET HEAD PUT);
use HTTP::Status;
use URI::Query;
use JSON;
use URI::Escape;
use Scalar::Util;
use Log::Any qw($log);
use Carp;
use Module::Runtime qw(use_module);

use WWW::{{invokerPackage}}::Configuration;

sub new
{
  my $class = shift;
  my (%args) = (
    'ua' => LWP::UserAgent->new,
    'base_url' => '{{basePath}}',
    @_
  );

  return bless \%args, $class;
}

# Set the user agent of the API client
#
# @param string $user_agent The user agent of the API client
#
sub set_user_agent {
  my ($self, $user_agent) = @_;
  $self->{http_user_agent}= $user_agent;
}

# Set timeout
#
# @param integer $seconds Number of seconds before timing out [set to 0 for no timeout]
# 
sub set_timeout {
  my ($self, $seconds) = @_;
  if (!looks_like_number($seconds)) {
    croak('Timeout variable must be numeric.');
  }
  $self->{http_timeout} = $seconds;
}

# make the HTTP request
# @param string $resourcePath path to method endpoint
# @param string $method method to call
# @param array $queryParams parameters to be place in query URL
# @param array $postData parameters to be placed in POST body
# @param array $headerParams parameters to be place in request header
# @return mixed
sub call_api {
  my $self = shift;
  my ($resource_path, $method, $query_params, $post_params, $header_params, $body_data, $auth_settings) = @_;

  # update parameters based on authentication settings
  $self->update_params_for_auth($header_params, $query_params, $auth_settings); 


  my $_url = $self->{base_url} . $resource_path;

  # build query 
  if (%$query_params) {
    $_url = ($_url . '?' . eval { URI::Query->new($query_params)->stringify });
  }


  # body data
  $body_data = to_json($body_data->to_hash) if defined $body_data && $body_data->can('to_hash'); # model to json string
  my $_body_data = %$post_params ? $post_params : $body_data;

  # Make the HTTP request
  my $_request;
  if ($method eq 'POST') {
      # multipart
      $header_params->{'Content-Type'} = lc $header_params->{'Content-Type'} eq 'multipart/form' ? 
          'form-data' : $header_params->{'Content-Type'};
      
      $_request = POST($_url, %$header_params, Content => $_body_data);

  }
  elsif ($method eq 'PUT') {
      # multipart
      $header_params->{'Content-Type'}  = lc $header_params->{'Content-Type'} eq 'multipart/form' ? 
          'form-data' : $header_params->{'Content-Type'};

      $_request = PUT($_url, %$header_params, Content => $_body_data);

  }
  elsif ($method eq 'GET') {
      my $headers = HTTP::Headers->new(%$header_params);
      $_request = GET($_url, %$header_params);
  }
  elsif ($method eq 'HEAD') {
      my $headers = HTTP::Headers->new(%$header_params);
      $_request = HEAD($_url,%$header_params); 
  }
  elsif ($method eq 'DELETE') { #TODO support form data
      my $headers = HTTP::Headers->new(%$header_params);
      $_request = DELETE($_url, %$headers);
  }
  elsif ($method eq 'PATCH') { #TODO
  }
  else {
  }
 
  $self->{ua}->timeout($self->{http_timeout} || $WWW::{{invokerPackage}}::Configuration::http_timeout); 
  $self->{ua}->agent($self->{http_user_agent} || $WWW::{{invokerPackage}}::Configuration::http_user_agent);
  
  my $_response = $self->{ua}->request($_request);

  unless ($_response->is_success) {
    croak("API Exception(".$_response->code."): ".$_response->message);
  }
     
  return $_response->content;

}

#  Take value and turn it into a string suitable for inclusion in
#  the path, by url-encoding.
#  @param string $value a string which will be part of the path
#  @return string the serialized object
sub to_path_value {
    my ($self, $value) = @_;
    return uri_escape($self->to_string($value));
}


# Take value and turn it into a string suitable for inclusion in
# the query, by imploding comma-separated if it's an object.
# If it's a string, pass through unchanged. It will be url-encoded
# later.
# @param object $object an object to be serialized to a string
# @return string the serialized object
sub to_query_value {
      my ($self, $object) = @_;
      if (is_array($object)) {
          return implode(',', $object);
      } else {
          return $self->to_string($object);
      }
}


# Take value and turn it into a string suitable for inclusion in
# the header. If it's a string, pass through unchanged
# If it's a datetime object, format it in ISO8601
# @param string $value a string which will be part of the header
# @return string the header string
sub to_header_value {
    my ($self, $value) = @_;
    return $self->to_string($value);
}

# Take value and turn it into a string suitable for inclusion in
# the http body (form parameter). If it's a string, pass through unchanged
# If it's a datetime object, format it in ISO8601
# @param string $value the value of the form parameter
# @return string the form string
sub to_form_value {
    my ($self, $value) = @_;
    return $self->to_string($value);
}

# Take value and turn it into a string suitable for inclusion in
# the parameter. If it's a string, pass through unchanged
# If it's a datetime object, format it in ISO8601
# @param string $value the value of the parameter
# @return string the header string
sub to_string {
  my ($self, $value) = @_;
  if (ref($value) eq "DateTime") { # datetime in ISO8601 format
    return $value->datetime();
  }
  else {
    return $value;
  }
}

# Deserialize a JSON string into an object
#  
# @param string $class class name is passed as a string
# @param string $data data of the body
# @return object an instance of $class
sub deserialize
{
  my ($self, $class, $data) = @_;
  $log->debugf("deserializing %s for %s", $data, $class);
  my $_result;

  if (not defined $data) {
    return undef;
  } elsif ( lc(substr($class, 0, 4)) eq 'map[') { #hash
    $_result = \(json_decode $data);
  } elsif ( lc(substr($class, 0, 6)) eq 'array[' ) { # array of data
    return $data if $data eq '[]'; # return if empty array

    my $_sub_class = substr($class, 6, -1);
    my @_json_data = json_decode $data;
    my @_values = ();
    foreach my $_value (@_json_data) {
      push @_values, $self->deserialize($_sub_class, $_value);
    }
    $_result = \@_values;
  } elsif ($class eq 'DateTime') {
    $_result = DateTime->from_epoch(epoch => str2time($data));
  } elsif (grep /^$class$/, ('string', 'int', 'float', 'bool', 'object')) { #TODO revise the primitive type
    $_result= $data;
  } else { # model
    my $_instance = use_module("WWW::{{invokerPackage}}::Object::$class")->new;
    $_result = $_instance->from_hash(decode_json $data);
  }

  return $_result;

}

# return 'Accept' based on an array of accept provided
# @param [Array] header_accept_array Array fo 'Accept'
# @return String Accept (e.g. application/json)
sub select_header_accept
{
  my ($self, @header) = @_;

  if (@header == 0 || (@header == 1 && $header[0] eq '')) {
    return undef;
  } elsif (grep(/^application\/json$/i, @header)) {
    return 'application/json';
  } else {
    return join(',', @header);
  }

}

# return the content type based on an array of content-type provided
# @param [Array] content_type_array Array fo content-type
# @return String Content-Type (e.g. application/json)
sub select_header_content_type
{
  my ($self, @header) = @_;

  if (@header == 0 || (@header == 1 && $header[0] eq '')) {
    return 'application/json'; # default to application/json
  } elsif (grep(/^application\/json$/i, @header)) {
    return 'application/json';
  } else {
    return join(',', @header);
  }

}

# Get API key (with prefix if set)
# @param string key name
# @return string API key with the prefix
sub get_api_key_with_prefix
{
    my ($self, $api_key) = @_;
    if ($WWW::{{invokerPackage}}::Configuration::api_key_prefix->{$api_key}) {
      return $WWW::{{invokerPackage}}::Configuration::api_key_prefix->{$api_key}." ".$WWW::{{invokerPackage}}::Configuration::api_key->{$api_key};
    } else {
      return $WWW::{{invokerPackage}}::Configuration::api_key->{$api_key};
    }
}

# update hearder and query param based on authentication setting
#  
# @param array $headerParams header parameters (by ref)
# @param array $queryParams query parameters (by ref)
# @param array $authSettings array of authentication scheme (e.g ['api_key'])
sub update_params_for_auth {
  my ($self, $header_params, $query_params, $auth_settings) = @_;

  return if (!defined($auth_settings) || scalar(@$auth_settings) == 0);

  # one endpoint can have more than 1 auth settings
  foreach my $auth (@$auth_settings) {
    # determine which one to use
    if (!defined($auth)) {
    }
    {{#authMethods}}elsif ($auth eq '{{name}}') {
      {{#isApiKey}}{{#isKeyInHeader}}$header_params->{'{{keyParamName}}'} = $self->get_api_key_with_prefix('{{keyParamName}}');{{/isKeyInHeader}}{{#isKeyInQuery}}$query_params->{'{{keyParamName}}'} = $self->get_api_key_with_prefix('{{keyParamName}}');{{/isKeyInQuery}}{{/isApiKey}}{{#isBasic}}$header_params->{'Authorization'} = 'Basic '.encode_base64($WWW::{{invokerPackage}}::Configuration::username.":".$WWW::{{invokerPackage}}::Configuration::password);{{/isBasic}}
        {{#isOAuth}}# TODO support oauth{{/isOAuth}}
    }
    {{/authMethods}}
    else {
      # TODO show warning about security definition not found
    }
  }
}


1;
