package WebService::E4SE;

use Moo;
use Authen::NTLM 1.09;
use LWP::UserAgent 6.02;
use HTTP::Headers;
use HTTP::Request;
use Scalar::Util ();
use URI 1.60;
use XML::Compile::SOAP11 2.38;
use XML::Compile::SOAP11::Client;
use XML::Compile::WSDL11;
use XML::Compile::Transport::SOAPHTTP;
use XML::LibXML;

use Carp ();
use strictures 2;
use namespace::clean;
use v5.10;

our $AUTHORITY = 'cpan:CAPOEIRAB';
our $VERSION = '0.03';

has _wsdls => (
	is => 'lazy',
	isa => sub { die "Not a HashRef" unless ref($_[0]) eq 'HASH' },
	default => sub { {} },
);

has _ua => (
	is => 'lazy',
	isa => sub { die "Not an LWP::UserAgent" unless Scalar::Util::blessed($_[0]) && $_[0]->isa('LWP::UserAgent') },
	default => sub { LWP::UserAgent->new( keep_alive=>1 ) },
);

has base_url => (
	is => 'rw',
	isa => sub {die "Not an URI" unless Scalar::Util::blessed($_[0]) && $_[0]->isa('URI')},
	required => 1,
	default => sub {
		URI->new('http://epicor/e4se')
	}
);

has files => (
	is => 'lazy',
	isa => sub { die 'Should be an array reference' unless $_[0] && ref($_[0]) eq 'ARRAY' },
	default => sub {[
		'ActionCall.asmx',
		'Attachment.asmx',
		'BackOfficeAP.asmx',
		'BackOfficeAR.asmx',
		'BackOfficeCFG.asmx',
		'BackOfficeGB.asmx',
		'BackOfficeGL.asmx',
		'BackOfficeIV.asmx',
		'BackOfficeMC.asmx',
		'Billing.asmx',
		'Business.asmx',
		'Carrier.asmx',
		'CommercialTerms.asmx',
		'Company.asmx',
		'ControllingProject.asmx',
		'CostVersion.asmx',
		'CRMClientHelper.asmx',
		'Currency.asmx',
		'Customer.asmx',
		'ECSClientHelper.asmx',
		'Employee.asmx',
		'ExchangeInterface.asmx',
		'Expense.asmx',
		'FinancialsAP.asmx',
		'FinancialsAR.asmx',
		'FinancialsCFG.asmx',
		'FinancialsGB.asmx',
		'FinancialsGL.asmx',
		'FinancialsMC.asmx',
		'FinancialsSync.asmx',
		'GLAccount.asmx',
		'IntersiteOrder.asmx',
		'InventoryLocation.asmx',
		'Journal.asmx',
		'Location.asmx',
		'LotSerial.asmx',
		'Manufacturer.asmx',
		'Material.asmx',
		'MaterialPlan.asmx',
		'MiscItems.asmx',
		'MSProject.asmx',
		'MSProjectEnterpriseCustomFieldsandLookupTables.asmx',
		'Opportunity.asmx',
		'Organization.asmx',
		'PartMaster.asmx',
		'Partner.asmx',
		'PriceStructure.asmx',
		'Project.asmx',
		'Prospect.asmx',
		'PSAClientHelper.asmx',
		'PurchaseOrder.asmx',
		'Receiving.asmx',
		'Recognize.asmx',
		'Resource.asmx',
		'SalesCycleManagement.asmx',
		'SalesOrder.asmx',
		'SalesPerson.asmx',
		'Shipping.asmx',
		'Site.asmx',
		'Supplier.asmx',
		'svActionCall.asmx',
		'svAttachment.asmx',
		'svBackOfficeAP.asmx',
		'svBackOfficeAR.asmx',
		'svBackOfficeCFG.asmx',
		'svBackOfficeGB.asmx',
		'svBackOfficeGL.asmx',
		'svBackOfficeIV.asmx',
		'svBackOfficeMC.asmx',
		'svBilling.asmx',
		'svBusiness.asmx',
		'svCarrier.asmx',
		'svCommercialTerms.asmx',
		'svCompany.asmx',
		'svControllingProject.asmx',
		'svCostVersion.asmx',
		'svCRMClientHelper.asmx',
		'svCurrency.asmx',
		'svCustomer.asmx',
		'svECSClientHelper.asmx',
		'svEmployee.asmx',
		'svExchangeInterface.asmx',
		'svExpense.asmx',
		'svFinancialsAP.asmx',
		'svFinancialsAR.asmx',
		'svFinancialsCFG.asmx',
		'svFinancialsGB.asmx',
		'svFinancialsGL.asmx',
		'svFinancialsMC.asmx',
		'svFinancialsSync.asmx',
		'svGLAccount.asmx',
		'svIntersiteOrder.asmx',
		'svInventoryLocation.asmx',
		'svJournal.asmx',
		'svLocation.asmx',
		'svLotSerial.asmx',
		'svManufacturer.asmx',
		'svMaterial.asmx',
		'svMaterialPlan.asmx',
		'svMiscItems.asmx',
		'svMSProject.asmx',
		'svMSProjectEnterpriseCustomFieldsandLookupTables.asmx',
		'svOpportunity.asmx',
		'svOrganization.asmx',
		'svPartMaster.asmx',
		'svPartner.asmx',
		'svPriceStructure.asmx',
		'svProject.asmx',
		'svProspect.asmx',
		'svPSAClientHelper.asmx',
		'svPurchaseOrder.asmx',
		'svReceiving.asmx',
		'svRecognize.asmx',
		'svResource.asmx',
		'svSalesCycleManagement.asmx',
		'svSalesOrder.asmx',
		'svSalesPerson.asmx',
		'svShipping.asmx',
		'svSite.asmx',
		'svSupplier.asmx',
		'svSysArtifact.asmx',
		'svSysDirector.asmx',
		'svSysDomainInfo.asmx',
		'svSysNotify.asmx',
		'svSysSearchManager.asmx',
		'svSysSecurity.asmx',
		'svSysWorkflow.asmx',
		'svTax.asmx',
		'svTime.asmx',
		'svUOM.asmx',
		'SysArtifact.asmx',
		'SysDirector.asmx',
		'SysDomainInfo.asmx',
		'SysNotify.asmx',
		'SysSearchManager.asmx',
		'SysSecurity.asmx',
		'SysWorkflow.asmx',
		'Tax.asmx',
		'Time.asmx',
		'UOM.asmx',
	]},
);

has force_wsdl_reload => (
	is => 'rw',
	coerce => sub { return 0 unless $_[0]; return 1 if ref($_[0]); (lc($_[0]) eq 'false') ? 0 : 1},
	required => 1,
	default => 0
);

has password => (
	is => 'rw',
	required => 1,
	default => '',
);

has realm => (
	is => 'rw',
	required => 1,
	default => '',
);

has site => (
	is => 'rw',
	required => 1,
	default => 'epicor:80',
);

has username => (
	is => 'rw',
	required => 1,
	default => '',
);

sub _get_port {
	my ( $self, $file ) = @_;
	return "WSSoap" unless defined($file) and length($file);
	$file =~ s/\.\w+$//; #strip extension
	return $file."WSSoap";
}

sub _valid_file {
	my ( $self, $file ) = @_;
	return 0 unless defined $file and length $file;
	return 1 if (grep {$_ eq $file} @{$self->files});
	return 0;
}

sub _wsdl {
	my ( $self, $file ) = @_;
	my $wsdl = $self->_wsdls();
	#if our wsdl is already setup, let's just return
	return 1 if ( exists($wsdl->{$file}) && defined($wsdl->{$file}) );

	#wsdl doesn't exist.  let's setup the user agent for our transport and move along
	$self->_ua->credentials( $self->site, $self->realm, $self->username, $self->password );

	my $res = $self->_ua->get($self->base_url . '/'. $file . '?WSDL' );
	Carp::croak('Unable to setup WSDL: '.$res->status_lin()) unless $res->is_success;

	$wsdl->{$file} = XML::Compile::WSDL11->new( $res->decoded_content );
	Carp::croak( "Unable to create new XML::Compile::WSDL11 object" ) unless $wsdl->{$file};

	my $trans = XML::Compile::Transport::SOAPHTTP->new(
		user_agent=> $self->_ua,
		address => $self->base_url.'/'. $file,
	);
	Carp::carp( "Unable to create new XML::Compile::Transport::SOAPHTTP object" ) unless $trans;

	$wsdl->{$file}->compileCalls(
		port => $self->_get_port($file),
		transport => $trans,
	);
	$self->_wsdls($wsdl);
	return 1;
}

sub call {
	my ( $self, $file, $function, %parameters ) = @_;
	unless ( $self->_valid_file($file) ) {
		Carp::carp( "$file is not a valid web service found in E4SE." );
		return 0;
	}
	my $wsdl = $self->wsdl();
	if ( $self->force_wsdl_reload() ) {
		delete($wsdl->{$file}) if exists($wsdl->{$file});
		$self->force_wsdl_reload(0);
	}
	return 0 unless $self->_wsdl($file);

	return $wsdl->{$file}->call($function,%parameters);
}

sub get_object {
	my ( $self, $file ) = @_;
	unless ( $self->_valid_file($file) ) {
		Carp::carp( "$file is not a valid web service found in E4SE." );
		return 0;
	}
	my $wsdl = $self->wsdl;
	if ( $self->force_wsdl_reload() ) {
		delete($wsdl->{$file}) if exists($wsdl->{$file});
		$self->force_wsdl_reload(0);
	}
	return 0 unless $self->_wsdl($file);
	return $wsdl->{$file};
}

sub operations {
	my ( $self, $file ) = @_;
	unless ( $self->_valid_file($file) ) {
		Carp::carp( "$file is not a valid web service found in E4SE." );
		return [];
	}
	if ( $self->force_wsdl_reload() ) {
		delete($self->wsdl->{$file}) if exists($self->wsdl->{$file});
		$self->force_wsdl_reload(0);
	}
	unless ( $self->_wsdl($file) ) {
		return [];
	}
	my @ops = $self->wsdl->{$file}->operations(port=>$self->_get_port($file));
	my @ret = ();
	for my $op ( @ops ) {
		push @ret, $op->name;
	}
	return \@ret;
}

1; # End of WebService::E4SE

=encoding utf8

=head1 NAME

WebService::E4SE - Communicate with the various Epicor E4SE web services.

=head1 SYNOPSIS

	use WebService::E4SE;

	# create a new object
	my $ws = WebService::E4SE->new(
		username => 'AD\username',                  # NTLM authentication
		password => 'A password',                   # NTLM authentication
		realm => '',                                # LWP::UserAgent and Authen::NTLM
		site => 'epicor:80',                        # LWP::UserAgent and Authen::NTLM
		base_url => URL->new('http://epicor/e4se'), # LWP::UserAgent and Authen::NTLM
		timeout => 30,                              # LWP::UserAgent
	);

	# get an array ref of web service APIs to communicate with
	my $res = $ws->files();
	say Dumper $res;

	# returns a list of method names for the file you wanted to know about.
	my @operations = $ws->operations('Resource.asmx');
	say Dumper @operations;

	# call a method and pass some named parameters to it
	my ($res,$trace) = $ws->call('Resource.asmx','GetResourceForUserID', userID=>'someuser');

	# give me the XML::Compile::WSDL11 object
	my $wsdl = $ws->get_object('Resource.asmx'); #returns the usable XML::Compile::WSDL11 object

=head1 DESCRIPTION

L<WebService::E4SE> allows us to connect to
L<Epicor's E4SE|http://www.epicor.com/products/e4se.aspx> SOAP-based APIs
service to access our data or put in our timesheet.

Each action on the software calls a SOAP-based web service API method. Each API
call is authenticated via NTLM.

There are more than 100 web service files you could work with (.asmx
extensions) each having their own set of methods. On your installation of E4SE,
you can get a listing of method calls available by visiting one of those files
directly (C<http://your_epicor_server/e4se/Resource.asmx> for example).

The module will grab the WSDL from the file you're trying to deal with.  It will make
use of that WSDL with L<XML::Compile::WSDL11>.  You can force a reload of the WSDL at any
time.  So, we build the L<XML::Compile::WSDL11> object and hold onto it for any further
calls to that file.  These are generated by the calls you make, so hopefully we don't
kill you with too many objects.  You can work directly with the new L<XML::Compile::WSDL11>
object if you like, or use the abstracted out methods listed below.

For transportation, we're using L<XML::Compile::Transport::SOAPHTTP> using
L<LWP::UserAgent> with L<Authen::NTLM>.

=head1 ATTRIBUTES

L<WebService::E4SE> makes the following attributes available:

=head2 base_url

	my $url = $ws->base_url;
	$url = $ws->base_url(URI->new('http://epicor/e4se'));

This should be the base L<URL|URI> for your E4SE installation.

=head2 files

	my $files = $ws->files;
	$files = $ws->files(['file1.asmx', 'file2.asmx']);
	say join ', ', @$files;

This is reference to an array of file names that this web service has
knowledge of for an E4SE installation. If your installation has some services
that we're missing, you can inject them here. This will clobber, not
merge/append.

=head2 force_wsdl_reload

	my $force = $ws->force_wsdl_reload;
	$force = $ws->force_wsdl_reload(1);

This attribute is defaulted to false (0).  If set to true, the next call to a
method that would require a L<XML::Compile::WSDL11> object will go out to the
server and re-grab the WSDL and re-setup that WSDL object no matter if we have
already generated it or not. The attribute will be reset to false (0) directly
after the next WSDL object setup.

=head2 password

	my $pass = $ws->password;
	$pass = $ws->password('foobarbaz');

This will be your domain password.  No attempt to hide this is made.

=head2 realm

	my $realm = $ws->realm;
	$realm = $ws->realm('MyADRealm');

Default is an empty string.  This is for the L<Authen::NTLM> module and can generally be left blank.

=head2 site

	my $site = $ws->site;
	$site = $ws->site('epicor:80');

This is for the L<Authen::NTLM> module.  Set this accordingly.

=head2 username

	my $user = $ws->username;
	$user = $ws->username('AD\myusername');

Usually, you need to prefix this with the domain your E4SE installation is using.

=head1 METHODS

L<WebService::E4SE> makes the following methods available:

=head2 call

	use Try::Tiny;
	try {
		my ( $res, $trace) = $ws->call('Resource.asmx', 'GetResourceForUserID', %parameters );
		say Dumper $res;
	}
	catch {
		warn "An error happened: $_";
		exit(1);
	}

This method will call an API method for the file you want.  It will die on
errors outside of L<XML::Compile::WSDL11>'s knowledge, otherwise
it's just a little wrapper around L<XML::Compile::WSDL11>->call();

Another way to do this would be

	$ws->get_object('Reource.asmx')->call( 'GetResourceForUserID', %params );

=head2 get_object

	my $wsdl = $ws->get_object('Resource.asmx');

This method will return an L<XML::Compile::WSDL11> object for the file name
you supply.  This handles going to the file's WSDL URL, grabbing that URL
with L<LWP::UserAgent> and L<Authen::NTLM>, and using that WSDL response to
setup a new L<XML::Compile::WSDL11> object.

Note that if you have previously setup a L<XML::Compile::WSDL> object for that
file name, it will just return that object rather than going to the server and
requesting a new WSDL.

=head2 operations

	my $available_operations = $ws->operations( $file );

This method will return a list of  L<XML::Compile::SOAP::Operation> objects
that are available for the given file.

=head1 AUTHOR

Chase Whitener << <cwhitener at gmail.com> >>

=head1 BUGS

Please report any bugs or feature requests on GitHub L<https://github.com/genio/webservice-e4se/issues>.
We appreciate any and all criticism, bug reports, enhancements, or fixes.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

	perldoc WebService::E4SE


You can also look for information at:

=over 4

=item * GitHub

L<https://github.com/genio/webservice-e4se>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/WebService-E4SE>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/WebService-E4SE>

=item * Search CPAN

L<http://search.cpan.org/dist/WebService-E4SE/>

=back

=head1 ACKNOWLEDGEMENTS

=head1 LICENSE AND COPYRIGHT

Copyright 2015
This program is free software, you can redistribute it and/or modify it under
the terms of the Artistic License version 2.0.

=cut
