#!/usr/bin/perl

use strict;
use warnings;

package API::ProductAlternateImportsAdmin;

use Archive::Zip qw(AZ_OK);
use File::Slurp ();
use File::Temp;
use List::MoreUtils qw/uniq firstidx/;
use MIME::Base64;
use MIME::Entity;
use MIME::Lite;
use SOAP::Lite ();
use base 'SOAP::Server::Parameters';
use Text::CSV;

use ECS::Config;
use ECS::Product;
use ECS::Product::Kit;
use ECS::Product::KitAlternate;
use ECS::Site;
use ECS::SQL;

our $Config;
BEGIN {
	use API::Config;
	$Config = $API::Config::Config;
}
use lib ($Config->{'Website_libs'});

my $website_config = \%ECS::Config::c;

###############################################################################

sub productLevelKitComponentAltImport {
    my $self = shift;
    my $input = shift;
    my $envelope = pop;
    my $site = ECS::Site::fromName(ECS);
    my $dbh = $site->estore_dbh;
    my $dbhr = $site->estore_dbhr;
    my $employeeID = $input->{'employeeID'};

    my $result = {
        'success' => 1,
        'error' => undef,
        'data' => {},
        'update_DB' => $Config->{'update_DB'}
    };

    my @parts = $envelope->parts();
    my $file;

    # grab attachment from request
    if (
        defined($parts[0])
        and defined($parts[0]->[0])
    ) {
        $file = $parts[0]->[0];
    } else {
        return $self->_markError($result, 'No file provided');
    }

    # open mime attachment bodyhandle and read into zip file
    my $fh = $file->bodyhandle->open('r');
    my $zip = Archive::Zip->new();
    unless($zip->read($fh) == 0) {
        return $self->_markError($result, 'Failed to open file');
    }

    # identify relevant csv file in zip file
    my $member = $zip->memberNamed('file.csv');
    my $tmp = File::Temp->new( SUFFIX => '.csv' );

    # extract csv from zip to file handle for reading
    unless($member->extractToFileHandle($tmp) == 0) {
        return $self->_markError($result, 'Failed to open file');
    }

    seek($tmp, 0, 0);

    # format into Text::CSV for processing
    my $csv = Text::CSV->new({
        'binary' => 1,
    });
    my $header_line = <$tmp>;
    $csv->parse($header_line);
    my @headers = $csv->fields();
    my %headers = map +( $_ => 1), @headers;

    return $self->_markError($result, 'File is missing header row') if !@headers || (scalar @headers == 1 && !defined $headers[0]);

    my @expectedHeaders = ('productID', 'altComponentProductID', 'sortOrder');
    for my $header (@headers) {
        return $self->_markError($result, "File contains erroneous header field '%s'", $header) if !(grep(/^$header$/, @expectedHeaders));
    }
    
    for my $expectedHeader (@expectedHeaders) {
        return $self->_markError($result, "File is missing required header field '%s'", $expectedHeader) unless defined $headers{ $expectedHeader };
    }

    return $self->_markError($result, 'Too many header fields on file') if (scalar @headers != scalar @expectedHeaders);

    my $alternates;
    my @checkedProductIDs;
    my $row = 2;
    $csv->column_names(qw( productID altComponentProductID sortOrder ));
    while (my $parse = $csv->getline_hr($tmp)) {
        my $productID = $parse->{productID};
        my $altComponentProductID = $parse->{altComponentProductID};
        my $sortOrder = $parse->{sortOrder};

        next if ($productID eq '' || $altComponentProductID eq '' || $sortOrder eq '');

        # validate productIDs
        my $product = ECS::Product->new({
            'dbh' => $dbhr,
            'productID' => $productID,
        });

        return $self->_markError($result, "Product with ES# %d not found in row %d column 'productID'", ($productID, $row)) if !$product->partnumber;

        my $alternate = ECS::Product->new({
            'dbh' => $dbhr,
            'productID' => $altComponentProductID,
        });

        return $self->_markError($result, "Product with ES# %d not found in row %d column 'altComponentProductID'", ($altComponentProductID, $row)) if !$alternate->partnumber;

        # build out alternates structure
        $alternates->{$productID} = [] if !$alternates->{$productID};
        push @{$alternates->{$productID}}, {'altComponentProductID' => $altComponentProductID, 'sortOrder' => $sortOrder};
    }

    my $employeeNameSQL = qq{
        SELECT nameFirst, nameLast
        FROM employeeInfo
        WHERE employeeID = ?
    };
    my $employeeNameSth = $dbhr->prepare( $employeeNameSQL );
    $employeeNameSth->execute($employeeID);
    my $employeeNameRecord = $employeeNameSth->fetchrow_hashref;
    return $self->_markError($result, "Invalid employee ID") if !$employeeNameRecord;
    my $employeeName = $employeeNameRecord->{'nameFirst'} . ' ' . $employeeNameRecord->{'nameLast'};

    for my $componentProductID (keys %{ $alternates }) {
        my $product = ECS::Product->new({
            'dbh' => $dbhr,
            'productID' => $componentProductID,
        });
        my $existingAlternates = ECS::Product::KitAlternate->_build_defaultKitAlternates({
            'dbhr' => $dbhr,
            'componentProductID' => $componentProductID,
        });
        my @mappedAlts = map { $_->{'altComponentProductID'} } @{$alternates->{$componentProductID}};
        my $altComponents = {$componentProductID => \@mappedAlts};

        my @orderedAltsArray = map {$_->{'altComponentProductID'} } @{$existingAlternates};
        for my $alternate (@{ $alternates->{$componentProductID} }) {
            my $sortOrder = $alternate->{'sortOrder'};
            my $inserted = 0;
            for my $existingAlt (@{$existingAlternates}) {
                if ($sortOrder <= $existingAlt->{'sortOrder'}) {
                    my $existingAltComponentProductID = $existingAlt->{'altComponentProductID'};
                    my $orderedIndex;
                    for my $index (keys @orderedAltsArray) {
                        if ($orderedAltsArray[$index] == $existingAltComponentProductID) {
                            $orderedIndex = $index;
                            last;
                        }
                    }

                    if (!defined($orderedIndex)) {
                        die 'something is very wrong here';
                    }

                    splice(@orderedAltsArray, $orderedIndex, 0, $alternate->{'altComponentProductID'});
                    $inserted = 1;
                    # loop through existing and bump back all later sort orders back to ensure proper comparisons on future alternates in the loop
                    for my $loopExistingAlt (@{$existingAlternates}) {
                        next if $loopExistingAlt->{'sortOrder'} < $existingAlt->{'sortOrder'};

                        $loopExistingAlt->{'sortOrder'}++;
                    }
                    last;
                }
            }
            push @orderedAltsArray, $alternate->{'altComponentProductID'} if !$inserted;
        }

        my $alternatesObject = {$componentProductID => \@orderedAltsArray};

        my $diff = ECS::Product::KitAlternate->_get_KitOverrideAlternatesDiff({
            'alternates' => $altComponents,
            'existingAlternates' => $existingAlternates,
        });

        if ($diff->{'inserts'}) {
            ECS::Product::KitAlternate->addKitAlternates({
                'dbh' => $dbh,
                'dbhr' => $dbhr,
                'inserts' => { 0 => \%{$diff->{'inserts'}} },
                'alternates' => $alternatesObject,
                'employeeID' => $employeeID,
            });
        }

        # update pricing for any kits that the product is on in case alts change
        my $parentProductsSQL = qq{
            SELECT productID
            FROM productComponent
            WHERE componentProductID = ?
        };
        my $parentProductsSth = $dbhr->prepare( $parentProductsSQL );
        $parentProductsSth->execute($componentProductID);

        my @parentProductIDs = ();
        while (my $entry = $parentProductsSth->fetchrow_hashref) {
            push(@parentProductIDs, $entry->{'productID'});
        }

        if (@parentProductIDs) {
            ECS::Product::Kit::callPriceUpdateApi({
                'productIDs' => \@parentProductIDs,
                'employeeID' => $employeeName,
                'iplocation' => 'product alt updates',
            });

            ECS::Product::Kit::callPriceUpdateApi({
                'productIDs' => \@parentProductIDs,
                'employeeID' => $employeeName,
                'iplocation' => 'product alt updates',
                'site'       => 'TMS',
            });
        }
    }

    return $result;
}

###############################################################################

sub kitLevelKitComponentAltImport {
    my $self = shift;
    my $input = shift;
    my $envelope = pop;
    my $site = ECS::Site::fromName(ECS);
    my $dbh = $site->estore_dbh;
    my $dbhr = $site->estore_dbhr;
    my $employeeID = $input->{'employeeID'};

    my $result = {
        'success' => 1,
        'error' => undef,
        'data' => {},
        'update_DB' => $Config->{'update_DB'}
    };

    my @parts = $envelope->parts();
    my $file;

    # grab attachment from request
    if (
        defined($parts[0])
        and defined($parts[0]->[0])
    ) {
        $file = $parts[0]->[0];
    } else {
        return $self->_markError($result, 'No file provided');
    }

    # open mime attachment bodyhandle and read into zip file
    my $fh = $file->bodyhandle->open('r');
    my $zip = Archive::Zip->new();
    unless($zip->read($fh) == 0) {
        return $self->_markError($result, 'Failed to open file');
    }

    # identify relevant csv file in zip file
    my $member = $zip->memberNamed('file.csv');
    my $tmp = File::Temp->new( SUFFIX => '.csv' );

    # extract csv from zip to file handle for reading
    unless($member->extractToFileHandle($tmp) == 0) {
        return $self->_markError($result, 'Failed to open file');
    }

    seek($tmp, 0, 0);

    # format into Text::CSV for processing
    my $csv = Text::CSV->new({
        'binary' => 1,
    });
    my $header_line = <$tmp>;
    $csv->parse($header_line);
    my @headers = $csv->fields();
    my %headers = map +( $_ => 1), @headers;

    return $self->_markError($result, 'File is missing header row') if !@headers || (scalar @headers == 1 && !defined $headers[0]);

    my @expectedHeaders = ('parentProductID', 'componentProductID', 'altComponentProductID', 'sortOrder');
    for my $header (@headers) {
        return $self->_markError($result, "File contains erroneous header field '%s'", $header) if !(grep(/^$header$/, @expectedHeaders));
    }
    
    for my $expectedHeader (@expectedHeaders) {
        return $self->_markError($result, "File is missing required header field '%s'", $expectedHeader) if !(grep(/^$expectedHeader$/, @headers));
    }

    return $self->_markError($result, 'Too many header fields on file') if (scalar @headers != scalar @expectedHeaders);

    my $alternates;
    my @checkedProductIDs;
    my $row = 2;
    $csv->column_names(qw( parentProductID componentProductID altComponentProductID sortOrder ));
    while (my $parse = $csv->getline_hr($tmp)) {
        my $productID = $parse->{parentProductID};
        my $componentProductID = $parse->{componentProductID};
        my $altComponentProductID = $parse->{altComponentProductID};
        my $sortOrder = $parse->{sortOrder};

        next if ($productID eq '' || $componentProductID eq '' || $altComponentProductID eq '' || $sortOrder eq '');

        # validate productIDs
        my $product = ECS::Product->new({
            'dbh' => $dbhr,
            'productID' => $productID,
        });

        return $self->_markError($result, "Product with ES# %d not found in row %d column 'productID'", ($productID, $row)) if !$product->partnumber;

        my $component = ECS::Product->new({
            'dbh' => $dbhr,
            'productID' => $componentProductID,
        });

        return $self->_markError($result, "Product with ES# %d not found in row %d column 'componentProductID'", ($componentProductID, $row)) if !$product->partnumber;

        my $alternate = ECS::Product->new({
            'dbh' => $dbhr,
            'productID' => $altComponentProductID,
        });

        return $self->_markError($result, "Product with ES# %d not found in row %d column 'altComponentProductID'", ($altComponentProductID, $row)) if !$alternate->partnumber;

        # build out alternates structure
        $alternates->{$productID}->{$componentProductID} = [] if !$alternates->{$productID}->{$componentProductID};
        push @{$alternates->{$productID}->{$componentProductID}}, {'altComponentProductID' => $altComponentProductID, 'sortOrder' => $sortOrder};
    }

    my $employeeNameSQL = qq{
        SELECT nameFirst, nameLast
        FROM employeeInfo
        WHERE employeeID = ?
    };
    my $employeeNameSth = $dbhr->prepare( $employeeNameSQL );
    $employeeNameSth->execute($employeeID);
    my $employeeNameRecord = $employeeNameSth->fetchrow_hashref;
    return $self->_markError($result, "Invalid employee ID") if !$employeeNameRecord;
    my $employeeName = $employeeNameRecord->{'nameFirst'} . ' ' . $employeeNameRecord->{'nameLast'};

    for my $productID (keys %{ $alternates }) {
        my $existingAlternates = ECS::Product::KitAlternate->_build_kitLevelAlternates({
            'dbhr' => $dbhr,
            'kitOverrideProductID' => $productID,
            'loadIntoProduct' => 0,
        });
        for my $componentProductID (keys %{$alternates->{$productID}}) {
            my @mappedAlts = map { $_->{'altComponentProductID'} } @{$alternates->{$productID}->{$componentProductID}};
            my $altComponents = {$componentProductID => \@mappedAlts};

            my @kitExistingAlternates;
            for my $existingAlt (@{$existingAlternates}) {
                next if $existingAlt->{'componentProductID'} != $componentProductID;

                push @kitExistingAlternates, $existingAlt;
            }


            my @orderedAltsArray = map {$_->{'altComponentProductID'} } @kitExistingAlternates;
            for my $alternate (@{ $alternates->{$productID}->{$componentProductID} }) {
                my $sortOrder = $alternate->{'sortOrder'};
                my $inserted = 0;
                for my $existingAlt (@kitExistingAlternates) {
                    if ($sortOrder <= $existingAlt->{'sortOrder'}) {
                        my $existingAltComponentProductID = $existingAlt->{'altComponentProductID'};
                        my $orderedIndex;
                        for my $index (keys @orderedAltsArray) {
                            if ($orderedAltsArray[$index] == $existingAltComponentProductID) {
                                $orderedIndex = $index;
                                last;
                            }
                        }

                        if (!defined($orderedIndex)) {
                            die 'something is very wrong here';
                        }

                        splice(@orderedAltsArray, $orderedIndex, 0, $alternate->{'altComponentProductID'});
                        $inserted = 1;
                        # loop through existing and bump back all later sort orders back to ensure proper comparisons on future alternates in the loop
                        for my $loopExistingAlt (@kitExistingAlternates) {
                            next if $loopExistingAlt->{'sortOrder'} < $existingAlt->{'sortOrder'};

                            $loopExistingAlt->{'sortOrder'}++;
                        }
                        last;
                    }
                }
                push @orderedAltsArray, $alternate->{'altComponentProductID'} if !$inserted;
            }

            my $alternatesObject = {$componentProductID => \@orderedAltsArray};

            my $diff = ECS::Product::KitAlternate->_get_KitOverrideAlternatesDiff({
                'alternates' => $altComponents,
                'existingAlternates' => $existingAlternates,
            });

            if ($diff->{'inserts'}) {
                ECS::Product::KitAlternate->addKitAlternates({
                    'dbh' => $dbh,
                    'dbhr' => $dbhr,
                    'inserts' => { $productID => \%{$diff->{'inserts'}} },
                    'alternates' => $alternatesObject,
                    'employeeID' => $employeeID,
                });
            }
        }

        ECS::Product::Kit::callPriceUpdateApi({
            'productIDs' => [$productID],
            'employeeID' => $employeeName,
            'iplocation' => 'kit alt updates',
        });

        ECS::Product::Kit::callPriceUpdateApi({
            'productIDs' => [$productID],
            'employeeID' => $employeeName,
            'iplocation' => 'kit alt updates',
            'site'       => 'TMS',
        });
    }

    return $result;

}

###############################################################################

# helper function for marking a result as errored, makes error logging cleaner in functions
sub _markError {
    my $self = shift;
    my $result = shift;
    my $errorMessage = shift;
    my @parameters = @_;

    $result->{'success'} = 0;
    $result->{'error'} = sprintf $errorMessage, @parameters;

    return $result;
}

###############################################################################

1;
