#!/usr/bin/perl
package TMSProductSync;

use strict;
use warnings; 
use Moo;
use Time::Piece;
use Type::Utils;
use Types::Standard qw/Str StrMatch Bool Int Map Dict Tuple ArrayRef Undef FileHandle Maybe Enum Any slurpy/;
use Types::Common::Numeric qw/PositiveInt PositiveOrZeroInt/;
use Types::TypeTiny qw/ArrayLike HashLike CodeLike StringLike/;
use Regexp::Common qw/net/;
use Fcntl qw/:DEFAULT :flock/;
use IO::File;
use Carp;
use Try::Tiny;
use Template;
use File::Basename;
use Email::Valid;
use Email::Simple;
use Email::Sender;
use Email::Sender::Simple qw/sendmail/;
use Email::Sender::Transport::SMTP ();
use JSON;
use URI;
use URI::QueryParam;

use Pod::Usage;
use SQL::Abstract;

use Log::Any qw/$log/;
use Log::Any::Adapter File => '/dev/stderr';

use lib qw(libs ../libs ../website_libs ../www/libs);
use lib qw(/var/www/website_libs/);

use API::Config;
use ECS::Config ();
use ECS::Site ();
use API::Product;
use ECS::DB;

sub LOG(&);

END {
    ECS::DB->dbDisconnectAllHandles();
}

my $SiteName = declare(SiteName => as StringLike);
my $DBName = declare(DBName => as StringLike);
my $SQLStatement = declare(SQLStatement => as Str);

my $ProductID = declare(ProductID => as PositiveInt);
my $ProductIDList = declare(ProducIDList => as ArrayRef->of($ProductID));

my $BrandName = declare(BradName => as StringLike);
my $BrandNameList = declare(BrandNameList => as ArrayRef->of($BrandName))->plus_coercions(
	StringLike,	=> sub {
		[split /\s*,\s*/, $_]
	},
);

my $LimitString = declare(LimitString => as StrMatch[qr{^LIMIT\s+\d+$}])->plus_coercions(
	PositiveInt,	=> sub {
		"LIMIT $_"
	}
);

my $Uri = class_type( Uri => { class => URI:: } ); 
my $ProductUri = declare( ProductUri => as $Uri );
my $ProductAdminURL = declare_coercion ProductAdminURL => 
	to_type $ProductUri,
	from  $ProductID, sub {
		my $uri = new URI( $ECS::Config::c{product_admin} );
		$uri->query_form_hash({
			action		=> 'editProduct',
			productID	=> $_,
		});
		$uri;
	};
my $TMSProductURL = declare_coercion TMSProductURL => 
	to_type $ProductUri,
	from $ProductID, sub {
		my $uri = new URI( $ECS::Config::c{'TMS.websiteURL'} );
		$uri->path( sprintf "/p-%d" => $_ );
		$uri;
	};
my $ECSProductURL = declare_coercion ECSProductURL =>
	to_type $ProductUri,
	from $ProductID, sub {
		my $uri = new URI( $ECS::Config::c{websiteURL} );
		$uri->path( sprintf "/ES%d" => $_ );
		$uri;
	};

my $EmailValid = declare(EmailValid => as StringLike, where { Email::Valid->address( $_ ) });
my $DBh = class_type( DBI::db:: );
my $SQLAbstract = class_type(SQL::Abstract::)->plus_coercions(
					Undef, => sub {
						SQL::Abstract->new 
					});
my $Template = class_type( Template:: )->plus_coercions(
					Undef, => sub {
						Template->new(
							INTERPOLATE	=> 1,
							STRICT => 1,
							COMPILE_DIR	=> '/tmp/',
							COMPILE_EXT	=> 'ttc',
						);
					});
my $TemplateOutput = declare(TemplateOutput => as StringLike);
my $ECSSite = class_type(ECS::Site::)->plus_coercions(
					$SiteName => sub {
						ECS::Site::fromName( $_ )
					});
my $EmailSimple = class_type( Email::Simple:: ); 
my $EmailArgs = declare EmailArgs => as Dict->of(
	to		=> $EmailValid,
	from	=> $EmailValid,
	subject	=> StringLike,
	body	=> StringLike,
);
my $HtmlEmail = declare_coercion HtmlEmail =>
	to_type $EmailSimple,
	from $EmailArgs, sub {
		Email::Simple->create( header => [
			To		=> $_->{to},
			From	=> $_->{from},
			Subject	=> $_->{subject},
			'Content-Type' => 'text/html',
		],
		body => $_->{body})
	};
my $EmailSenderTransport = class_type( Email::Sender::Transport::SMTP:: );
my $HostName = declare HostName => as StrMatch[qr{^$RE{net}{domain}{-nospace}{-rfc1101}$}];
my $EmailTransport = declare_coercion EmailTransport =>
	to_type $EmailSenderTransport,
	from $HostName, sub {
		Email::Sender::Transport::SMTP->new({
			host => $_,
		});
	};

my $Importer = declare(Importer => as CodeLike)->plus_coercions(
	$SiteName,	=> sub { 
		API::Product->can( "create$_" )
	}
);
my $ImporterArgs = declare(ImporterArgs => as Map[Str,Any]);
my $ProductImportArgs = declare_coercion ProductImportArgs =>
	to_type $ImporterArgs =>
	from Undef, sub {+{
		copyImages => 1,
		vendorOverrideColumns => {
			ruleOverride => 1,
			markup_method => 'markup_fixed',
		},
	}};
my $KitImportArgs = declare_coercion KitImportArgs =>
	to_type $ImporterArgs =>
	from Undef, sub {+{
		copyImages => 1,
		vendorOverrideColumns => {
			ruleOverride => 1,
		}
	}};

my $ImportSuccess = declare(ImportSuccess => as Dict->of( 
	success => Enum[1], 
	error => Undef, 
	data => Map[Str,Any], 
	slurpy Any 
));
my $ImportError = declare(ImportError => as Dict->of(
	success => Enum[0], 
	error => StringLike, 
	slurpy Any 
));
my $ImportInput = declare(ImportInput => 
		as Tuple->of($Importer,$ImporterArgs,$ProductID));
my $ImportResult = union(ImportResult => [$ImportSuccess,$ImportError]);
my $ImportResults = declare(ImportResults => as Dict[
	fail	=> ArrayRef[$ImportError],
	success	=> ArrayRef[$ImportSuccess],
]);
my $ImportReport = declare_coercion ImportReport =>
	to_type $TemplateOutput,
	from $ImportResults, sub {
		my $tt = $Template->coerce();
		my $data = {
			%$_,
			ECSProductURL	=> $ECSProductURL,
			TMSProductURL	=> $TMSProductURL,
			ProductAdminURL	=> $ProductAdminURL,
		}; 
		$tt->process( 
			_import_template_report(), 
			$data,
			\(my $output)
		) or die "$Template::ERROR";
		return $output;
	};
my $ImportProduct = declare_coercion ImportProduct =>
	to_type $ImportResult,
	from $ImportInput, sub {
		my ($importer, $args, $product_id) = @$_;
		my $result = try {
			# avoid the logs generated by Search::Elasticsearch::Logger 
			# (activated by the presence of Log::Any in this module)
			# limit the scope of the Null adapter to this try block
			Log::Any::Adapter->set({ lexically => \my $lex}, "Null" );
			my $res = $importer->(
				__PACKAGE__, 
				{ 
					%$args, 
					productID => $product_id,
				}
			);
			$res->{data}{productID} ||= $product_id;
			$res;
		} catch {
			+{ error => $_, success => 0, data => { productID => $product_id } };
		};
		$result;
	};




=pod

=head1 NAME

TMSProductSync - Import Genuine Products from ECS to Turner

=head1 SYNOPSIS

	$ perl scripts/TMSProductSync.pm [email EMAIL] [brands BRANDLIST] [limit NUM] 

=head1 OPTIONS

=head2 email EMAIL

B<< Default: none >>

E-mail address to send TMS Product Sync emails to

=cut

has email => (
	is		 => 'ro',
	isa		 => Maybe[$EmailValid],
	default	 => undef,
);

=pod

=head 2 from EMAIL

B<< Default: tmsproductsync@ecstuning.com >>

E-mail address to send TMS Product Sync emails from

=cut

has from_email => (
	is		 => 'ro',
	isa		 => $EmailValid,
	default	 => 'tmsproductsync@ecstuning.com',
	init_arg => 'from',
);

=pod include_kits BOOL

B<< Default: 1 >>

Whether or not to include kits in the products to import

=cut

has include_kits => (
	is		 => 'ro',
	isa		 => Bool,
	default	 => 1,
);

has sandbox => (
	is		 => 'ro',
	isa		 => Bool,
	lazy	 => 1,
	builder	 => '_build_sandbox',
);

has sandbox_email => (
	is		 => 'ro',
	isa		 => $EmailValid,
	lazy	 => 1,
	builder	 => '_build_sandbox_email',
);

has _lock => (
	is		 => 'rw',
	isa		 => FileHandle,
	clearer	 => '_clear_lock',
	init_arg => undef,
);

has site => (
	is		=> 'ro',
	isa		=> $ECSSite,
	default	=> 'TMS',
	coerce	=> 1,
);

has _site_prefix => (
	is		 => 'ro',
	isa		 => $DBName,
	lazy	 => 1,
	builder	 => '_build_site_prefix',
	init_arg => undef,
);

has dbh => (
	is		 => 'ro',
	isa		 => $DBh,
	lazy	 => 1,
	builder	 => '_build_dbh',
	init_arg => undef,
);

has dbhr => (
	is		 => 'ro',
	isa		 => $DBh,
	lazy	 => 1,
	builder	 => '_build_dbhr',
	init_arg => undef,
);

=pod

=head2 brands BRANDLIST

B<< Default: >>

=over

=item * B<< Genuine BMW >>

=item * B<< Genuine MINI >>

=item * B<< Genuine European BMW >>

=item * B<< Genuine European Mini >>

=item * B<< Genuine BMW MINI >>

=item * B<< Genuine BMW Motorsport >>

=item * B<< Genuine BMW M Performance >>

=back

Comma separated list of brands to sync, should be quoted if containing spaces

	brands 'Genuine BMW,Genuine MINI' 

=cut

has brands => (
	is		=> 'ro',
	isa		=> $BrandNameList,
	lazy	=> 1,
	builder	=> '_build_brands',
	coerce	=> 1,
);

=pod

=head2 limit NUM

B<< Default: none >>

Limit the number of products to import

=cut

has limit => (
	is		=> 'ro',
	isa		=> Maybe[$LimitString],
	defult	=> undef,
	coerce	=> 1,
);

has product_sql => (
	is		=> 'ro',
	isa		=> $SQLStatement,
	lazy	=> 1,
	builder	=> '_build_product_sql',
);

has _products => (
	is	=> 'ro',
	isa	=> $ProductIDList,
	lazy => 1,
	coerce => 1,
	builder => '_build_products',
);

has _kits => (
	is	=> 'ro',
	isa	=> $ProductIDList,
	lazy => 1,
	coerce => 1,
	builder => '_build_kits',
);


unless (caller() && caller() ne 'PAR') {
	if (@ARGV == 1) {
		pod2usage( -exitval => 0, -verbose => 1 );
	}
	my %args = eval {
		use warnings FATAL => qw/misc/;
		my %args = @ARGV;
	};
	my $script = __PACKAGE__->new( \%args );
	my $result;
	if ($script->lock()) {
		$result = $script->main() && 1;
		$script->unlock();
	} else {
		LOG { "$0 already running" };
		$result = 1;
	}
	exit 0+!$result;
}


sub main {
	my $self = shift;
	my $count = $self->product_count;
	return 1 unless $count;
	LOG { sprintf "Syncing %d products to %s" => $count, $self->site->siteName };
	my (@fail, @success);
	my $SaveResult = Type::Utils::compile_match_on_type(
		$ImportSuccess => sub {
			push @success, $_;
		},
		$ImportError => sub {
			push @fail, $_;
		},
		=> sub { },
	);
	my $LogResult = Type::Utils::compile_match_on_type(
		$ImportSuccess => sub {
			LOG { sprintf "Imported ES#%d as T#%d", @{$_->{data}}{qw/productID tmsProductID/} };
		},
		$ImportError => sub {
			LOG { sprintf "Failed to import ES#%d: %s", $_->{data}{productID}, $_->{error} };
		},
		=> sub { die "Import Failed. $_ is not a valid Import Result." }
	);
	die;
	my $result;
	my $importer = $Importer->coerce( $self->site );
	my $importer_args = $ProductImportArgs->();
	foreach my $productID (@{ $self->products }) {
		my $result = $ImportProduct->coerce([$importer, $importer_args, $productID]);
		$LogResult->($result);
		$SaveResult->($result);
	}
	$importer_args = $KitImportArgs->();
	foreach my $productID (@{ $self->kits }) {
		my $result = $ImportProduct->coerce([$importer, $importer_args, $productID]);
		$LogResult->($result);
		$SaveResult->($result);
	}

	LOG { 
		sprintf "Synced %d products to %s (%d failed to import)" => 
			scalar(@success), 
			$self->site->siteName,
			scalar(@fail)
	}; 
	return 1 unless $self->email;
	my $import_report = $ImportReport->({
		fail	=> [@fail],
		success	=> [@success],
	});
	$self->email_report( $import_report );
}

sub email_report {
	my $self = shift;
	LOG { "Emailing TMS Product Sync Report" };
	my ($report) = @_;
	my $email = $self->sandbox ? $self->sandbox_email : $self->email;
	my $message = $HtmlEmail->({
		to		=> $email,
		from	=> $self->from_email,
		subject	=> 'TMS Product Sync Report',
		body	=> $report,
	});
	my $transport = $EmailTransport->( $ECS::Config::c{email}{smtp_server} );
	return try {
		Email::Sender::Simple->send( $message, {
			transport	=> $transport
		});
		LOG { "Emailed TMS Product Sync Report" };
		1;
	} catch {
		LOG { "Failed to email TMS Product Sync Report: $_" };
		die "Failed to email TMS Product Sync Report: $_";
	};
}

sub _import_template_report {
	\q{<html><title>TMS Product Sync Report</title><body>
		[% IF success.size > 0 %]
		<table border="1">
			<caption>Products Imported</caption>
			<thead><td>ES#</td><td>T#</td></thead>
			<tbody>
			[% FOREACH result IN success %]
				<tr>
					<td>
						<a href="[% ECSProductURL.coerce( result.data.productID ) %]">
							ES#[%- result.data.productID -%]
						</a>
					</td>
					<td>
						<a href="[% TMSProductURL.coerce( result.data.tmsProductID ) %]">
							T#[% result.data.tmsProductID %]</td>
						</a>
					</td>
				</tr>
				<tr>
					<td colspan="2"><a href="[% ProductAdminURL.coerce( result.data.productID ) %]">
						Product Admin: ES#[%- result.data.productID -%]
					</a></td>
				</tr>
			[% END %]
			</tbody>
		</table border="1">
		[% END %]
		[% IF fail.size > 0 %]
		<table border="1">
			<caption>Failed to Import</caption>
			<thead><td>ES#</td><td>Reason</td></thead>
			<tbody>
			[% FOREACH result IN fail %]
				<tr>
					<td>
						<a href="[% ECSProductURL.coerce( result.data.productID ) %]">
							ES#[%- result.data.productID -%]
						</a>
					</td>
					<td>
						[% result.error FILTER html %]
					</td>
				</tr>
				<tr>
					<td colspan="2"><a href="[% ProductAdminURL.coerce( result.data.productID ) %]">
						Product Admin: ES#[%- result.data.productID -%]
					</a></td>
				</tr>
			[% END %]
			</tbody>
		</table>
		[% END %]
			
	</body><html>} 
}

sub _build_brands {
	return [
		'Genuine BMW',
		'Genuine MINI',
		'Genuine European BMW',
		'Genuine European Mini',
		'Genuine BMW MINI',
		'Genuine BMW Motorsport',
		'Genuine BMW M Performance'
	]
}

sub _product_sql_builder {
	my $self = shift;
	my $sql = $SQLAbstract->coerce();
	my @brands = @{ $self->brands || [] };
	my $tms = $self->_site_prefix;
	my $dbh = $self->dbhr;
	my @types = @_;
	
	my ($stmt, @bind) = $sql->select(
		"product p
			JOIN productVendor pv USING (productID) 
			JOIN brand b USING (brandID) 
			LEFT JOIN $tms.product tp USING (productID) 
			LEFT JOIN $tms.product tpp ON (tpp.partnumber = p.partnumber) ",
		[\"DISTINCT p.productID"],
		{ -and => [
			'tp.tmsProductID' => undef,
			'tpp.tmsProductID' => undef,
			supersession_productID => undef,
			type => { -in => [@types] },
			stockstatus => { -in => [map \($dbh->quote($_)), '', 'YES'] },
			'b.name' => { -in => $self->brands }
		]},
		[qw/type productID/]
	);
	$stmt .= " ".$self->limit if $self->limit;
	return $stmt;
}

sub _build_product_sql {
	shift->_product_sql_builder('Automotive');
}

sub _build_kit_sql {
	shift->_product_sql_builder('Kit');
}

sub _products_builder {
	my $self = shift;
	my ($product_type) = @_;
	my $dbh = $self->dbhr;
	my $sth = $dbh->prepare( $self->product_sql );
	$sth->execute($product_type, @{ $self->brands });
	my $products = [map @$_, map @$_, $sth->fetchall_arrayref([0])];
}

sub _build_products {
	shift->_products_builder('Automotive');
}

sub _build_kits {
	shift->_products_builder('Kit');
}

sub products {
	wantarray 
		? @{shift->_products || [] }
		: shift->_products;
}

sub kits {
	wantarray 
		? @{ shift->_kits || [] }
		: shift->_kits;
}

sub product_count {
	return $#{ shift->_products || [] } + 1;
}

sub _build_site_prefix {
	shift->site->prefix;
}

sub _build_dbh { 
	shift->site->estore_dbh;
}

sub _build_dbhr { 
	shift->site->estore_dbhr;
}

sub _build_sandbox_email {
	$ECS::Config::c{'sandbox_email'}[0]
}

sub _build_sandbox {
	$ECS::Config::c{sandbox} ? 1 : 0
}

sub LOG(&) {
    my $sub = shift;
    $log->info( $sub->() );
}

sub lock {
    my $self = shift;
    IO::File->new( '/tmp/'.basename($0).'.lock', 'w');
    my $fh = try {
        IO::File->new( '/tmp/'.basename($0).'.lock', 'w');
    } catch {
        croak "Can't open lockfile ".basename($0).".lock for write $_";
    } or croak "Can't open lockfile ".basename($0).".lock for write $!";

    return flock( $fh, LOCK_EX | LOCK_NB ) ? scalar($self->_lock( $fh ), $fh) : 0;
}

sub unlock {
    my $self = shift;
    return unless $self->_lock;
    $self->_clear_lock;
    try {
        unlink '/tmp/'.basename($0).'.lock';
    };
}

sub DESTROY {
	eval { unlink '/tmp/'.basename($0).'.lock' };
}



1;
