package API::WarehouseOS;
use ECS::WarehouseOS::Batch;
use ECS::WarehouseOS::Client;
use ECS::WarehouseOS::Error;
use ECS::Site;
use ECS::DB;
use Scalar::Util qw/looks_like_number/;
use Try::Tiny;
use Carp;
use Moose;
use API::Controller;
use API::LocationAdmin;
use API::Order::Process;
use Hash::MultiValue;

{
	package DBGuard;
	our $DBH_TIMEOUT = 30; # how many seconds to wait for dbh->ping to return
	# this package exists purely to set and unset the alarm in dbh builders
	# (used so that SIGALRM can kill this thread)
	sub new {
		alarm $DBH_TIMEOUT;
		bless {}, __PACKAGE__;
	}
	sub DESTROY {
		# set alarm back to 0, the danger has passed
		alarm 0;
	}
}

has _data   => (
    is  => 'ro',
    isa => 'Hash::MultiValue',
    init_arg    => 'data',
    handles => {
        data        => 'as_hashref',
        data_get    => 'get',
        data_keys   => 'keys',
        data_set    => 'set',
    },
    required => 1,
);

has _params => (
    is  => 'ro',
    isa => 'Hash::MultiValue',
    init_arg => 'params',
    required => 1,
    handles => {
        parameters => 'as_hashref',
        params  => 'keys',
        get_param   => 'get',
    },  
);

has _primarySite => (
    is  => 'ro',
    isa => 'ECS::Site',
    lazy    => 1,
    default => sub { ECS::Site::fromName( $ECS::Site::PRIMARY_SITE ) },
);

has dbh => (
    is  => 'ro',
    isa => 'DBI::db',
    lazy => 1,
    builder => '_build_dbh',
);

has dbhr => (
    is  => 'ro',
    isa => 'DBI::db',
    lazy => 1,
    builder => '_build_dbhr',
);

has users => (
    is      => 'ro',
    isa     => 'Hash::MultiValue',
    lazy    => 1,
    builder => '_build_users',
    handles => {
        user    => 'get',
    },
);

has path => (
    is      => 'ro',
    isa     => 'Str',
    required    => 1,
);


sub _build_dbh {
	my $guard = DBGuard->new;
    shift->_primarySite->estore_dbh;
}

sub _build_dbhr {
	my $guard = DBGuard->new;
    shift->_primarySite->estore_dbhr;
}

sub _build_users {
    my $self = shift;
    my $users = $self->dbhr->selectall_arrayref( <<"SQL" );
        SELECT 
            userName,
            name 
        FROM
            scanner_user
        WHERE
            userName 
                IS NOT NULL
SQL
    $users ||= [];
    my %user_map = map( @$_, @$users );
    Hash::MultiValue->new( %user_map );
}

sub pickComplete {
    my $self = shift;
    (my $orderNumber = ($self->data_get( 'OrderNumber' )||"")) =~
        s{^(O[PS]?|E#?)}{}g;

    my $orderNumbers;
    if(($self->data_get('OrderNumber')||"") =~ m{^BATCH\d+$}) {
        (my $productID = $self->data_get("ItemNumber")) =~ s/^ES//;
        my $qty = $self->data_get("Quantity");
        my $siteName = $self->data_get("LineNote");
        # site is now part of LineNote with product description
        # make sure we have a site (LineNote = "(STE) Product Description"
        unless ($siteName =~ m{^[A-Z]{3}$}) {
            $siteName =~ s{^.*?([A-Z]{3}).*$}{$1}s;
        }
        $siteName ||= "ECS";
        my $site = ECS::Site->fromName( $siteName );


        my @orderNumbers = $self->_getOrderNumbersOSOOB({
            'productID' => $productID,
            'maxQty'    => $qty,
            'site'      => $site,
        });

        # process each of the orders we found
        for $orderNumber (@orderNumbers) {
            $self->data_set( "OrderNumber", "OP$orderNumber" );
            my $result = $self->pickComplete;
            if($result->errorCode) {
                ECS::DB->dbDisconnectAllHandles();
                return $result;
            }
        }
        $orderNumbers = join ',', map("E#$_", @orderNumbers);

        ECS::DB->dbDisconnectAllHandles();
        return new ECS::WarehouseOS::Error(
                errorCode   => 0,
                message     => "PickComplete: $orderNumbers: Updated Orders from Host",
        );
    }

    if ($self->data_get('OrderNumber') =~ m{^BATCH\d+$}) {
        ECS::DB->dbDisconnectAllHandles();
        return new ECS::WarehouseOS::Error(
                errorCode   => 0,
                message     => "PickComplete: $orderNumbers: OSOOB Order Picked",
        );
    }
    unless ($orderNumber && looks_like_number( $orderNumber )) {
        warn  "PickComplete: $orderNumber: Invalid OrderNumber from Host";
        throw ECS::WarehouseOS::Error(
            errorCode   => 1023,
            message     => "PickComplete: $orderNumber: Invalid OrderNumber from Host",
        );
    }
    
    my $order = try {
        load ECS::Order({
            DBH     => $self->dbh,
            orderID   => $orderNumber,
        });
    } catch {
        warn  "PickComplete: $orderNumber: Cannot load order ($_)";
        throw ECS::WarehouseOS::Error(
            errorCode       => 1022,
            message         => "PickComplete: Cannot load Order E#$orderNumber ($_)",
        );
    };

    if ($order->status !~ m{^(Batched|Picking)}) {
        warn  "PickComplete: $orderNumber: Order E#$orderNumber not in 'Batched'"; 
        throw ECS::WarehouseOS::Error(
            errorCode   => 1021,
            message     => (sprintf 'PickComplete: Order E#%d not in "Batched" (%s)' => $order->orderID, $order->status),   
        );
    }


    if ($order->status eq q{Batched} && $order->{picker} ne $ECS::WarehouseOS::Batch::batchUser) {
        warn  "PickComplete: $orderNumber:  Order not currently being processed by warehouseOS";
        throw ECS::WarehouseOS::Error(
            errorCode   => 1020,
            message     => "PickComplete: Order E#$orderNumber not currently being processed by warehouseOS",
        );
    }

    my $inProcess = $order->status eq 'Batched' ? 0 : 1;
    
    unless ($inProcess) {
        my ($picker, $device) = $self->data_get('PickCompletes') ?
            @{ ( $self->data_get('PickCompletes') )->[0] }{ qw/UserId Device/ } :
            map( $self->data_get( $_ ), qw/UserId Device/ );
        # lookup mapping from warehouseOS userid to scanner_user name
        $order->{picker} = $picker; 
        $order->status( 'Picking' );
        if (my $user = $self->user( $picker )) {
            $order->{picker} = $user;
            addNote $order(sprintf "WarehouseOS assigned %s (%s) as Picker on %s" => $picker, $order->{picker}, $device); 
        } else {
            addNote $order(sprintf "WarehouseOS assigned %s as Picker on %s" => $picker, $device); 
        }
        try {
            save $order;
        } catch {
            warn  "PickComplete: $orderNumber: Unable to update order ($_)";
            throw ECS::WarehouseOS::Error(
                errorCode   => 1019,
                message     => "PickComplete: Unable to update order E#$orderNumber ($_)",
            );
        };
    }

    # not actually an error but warehouseOS trigger API requires errorcode 0 on success
    ECS::DB->dbDisconnectAllHandles();
    return new ECS::WarehouseOS::Error(
            errorCode   => 0,
            message     => (sprintf "%s: Updated Order (E#%d) from Host" => 'PickComplete' => $order->orderID),
    );
    
    
}

sub verifyComplete {
    my $self = shift;
    (my $orderNumber = $self->data_get( 'OrderNumber' )) =~
        s{^(O[PS]?|E#?)}{}g;

    my $orderNumbers = "";
    if($self->data_get('OrderNumber') =~ m{^BATCH\d+$}) {
        my $items = $self->data_get("PickCompletes");
        for my $item (@{ $items }) {
            (my $productID = $item->{'ItemNumber'}) =~ s/^ES//;
            my $qty = $item->{'Quantity'};
            my $siteName = $item->{'LineNote'};
            # site is now part of LineNote with product description
            # make sure we have a site (LineNote = "(STE) Product Description"
            unless ($siteName =~ m{^[A-Z]{3}$}) {
               $siteName =~ s{^.*?([A-Z]{3}).*$}{$1}s;
            }
            $siteName ||= "ECS";
            my $site = ECS::Site->fromName( $siteName );
            my @orderNumbers = $self->_getOrderNumbersOSOOB({
                'productID' => $productID,
                'maxQty'    => $qty,
                'site'      => $site,
            });

            # process each of the orders we found
            for $orderNumber (@orderNumbers) {
                $self->data_set( "OrderNumber", "OP$orderNumber" );
                my $result = $self->verifyComplete;
                if($result->errorCode) {
                    ECS::DB->dbDisconnectAllHandles();
                    return $result;
                }
            }
            $orderNumbers = join ",", map("E#$_", @orderNumbers);
        }
        ECS::DB->dbDisconnectAllHandles();
        return new ECS::WarehouseOS::Error(
                errorCode   => 0,
                message     => "VerifyComplete: $orderNumbers: Updated Orders from Host",
        );
    }

    unless ($orderNumber && looks_like_number( $orderNumber)) {
        warn  "VerifyComplete: $orderNumber: Invalid OrderNumber from Host";
        throw ECS::WarehouseOS::Error(
            errorCode   => 1023,
            message     => "VerifyComplete: $orderNumber: Invalid OrderNumber from Host",
        );
    }
    my $orderID = $orderNumber;

    my $lpn = $self->data_get('RecordedLPN');
    unless ($lpn && $lpn =~ s{^L0*(\d+)$}{$1}) {
        warn  "VerifyComplete: $orderNumber ($lpn): Invalid LPN From Host";
        throw ECS::WarehouseOS::Error(
            errorCode   => 1026,
            message     => "VerifyComplete: $orderNumber: Invalid LPN ($lpn) From Host",
        );
    }
    
    my $order = try {
        load ECS::Order({
            DBH     => $self->dbh,
            orderID   => $orderID,
        });
    } catch {
        warn  "VerifyComplete: $orderNumber ($lpn) Cannot load Order ($_)";
        throw ECS::WarehouseOS::Error(
            errorCode       => 1022,
            message         => "Cannot load Order $orderID ($_)",
        );
    };

    if ($order->status !~ m{^Picking.*}) {
        warn  "VerifyComplete: $orderNumber ($lpn) Order not in Picking (".$order->status.")";
        throw ECS::WarehouseOS::Error(
            errorCode       => 1025,
            message         => "Order E#".$order->orderID." not in Picking (".$order->status.")",
        );
    }

    my $locAdmin = API::LocationAdmin->new({ 
        dbh => $self->dbh,
    });
    my $success = try {
        $locAdmin->assignLPN({
            lpnID   => $lpn,
            orderID => $orderID,
        });
    } catch {
       warn  "VerifyComplete: $orderNumber ($lpn) Can't assign Order to LPN ($_)";
       throw ECS::WarehouseOS::Error(
            errorCode       => 1027,
            message         => "Can't assign Order $orderID to LPN ($_)",
        );
    };
    # check for positive integer, not just truth 
    # ("0E0" is true and is returned when dbi->do succeeded but affected no rows)
    unless ($success && $success+0) { 
       warn  "VerifyComplete: $orderNumber ($lpn) Can't assign Order to LPN (no records updated)";
       throw ECS::WarehouseOS::Error(
            errorCode       => 1028,
            message         => "Can't assign Order $orderID to LPN",
        );
    }
    (my $new_status = $order->status) =~ s/Picking(?: Overstock)?/Picked/;
    my $api = API::Controller->new({ dbh => $self->dbh });
    $success = try {
        API::Order::markPickedOrders( $api, {
            user        => $ECS::WarehouseOS::Batch::batchUser,
            ipaddress   => $ECS::WarehouseOS::Batch::batchIPAddress,
            orders      => [{ orderID => $orderID, status => $new_status }],
        });
    } catch {
       warn  "VerifyComplete: $orderNumber ($lpn): Can't move order to Picked ($_)";
       throw ECS::WarehouseOS::Error(
            errorCode       => 1029,
            message         => "Can't move Order $orderID to Picked ($_)",
        );
    };

    $api = API::Controller->new({ dbh => $self->dbh });
    $success = try {
        API::Order::Process::getPackOrderDetails( $api, {
            dbh         => $self->dbh,
            LPN         => $lpn,
            warehouseStation => 'station1',
            user        => $ECS::WarehouseOS::Batch::batchUser,
            ipaddress   => $ECS::WarehouseOS::Batch::batchIPAddress,
            noUpdatePacker => 1,
            # status_map     => { map( ($_ => 'Batched'), 'Picking', 'Picking Overstock', 'Picking OSOOB') },
        });
    };

    if (!$success) {
       warn  "VerifyComplete: $orderNumber ($lpn): Can't move order to Picked";
       throw ECS::WarehouseOS::Error(
            errorCode       => 1030,
            message         => "Can't move order $orderID to Picked",
        );
    }
    if (!$success->{success}) {
       warn  "VerifyComplete: $orderNumber ($lpn): Can't move order to Picked ($success->{error}";
       throw ECS::WarehouseOS::Error(
            errorCode       => 1031,
            message         => "Can't move order $orderID to Picked ($success->{error}",
        );
    }
    ECS::DB->dbDisconnectAllHandles();
    return new ECS::WarehouseOS::Error(
            errorCode   => 0,
            message     => "VerifyComplete: E#$orderID Verified Order Picked",
    );

}

# order complete and pick complete may both or either be called from warehouseOS
# same functionality for both
sub orderComplete {
    shift->pickComplete( @_ );
}

sub _getOrderNumbersOSOOB {
    my ($self, $p) = @_;

    my $productID = $p->{'productID'} or croak "Missing productID";
    my $maxQty    = $p->{'maxQty'}    or croak "Missing maxQty";
    my $site      = $p->{'site'}      or croak "Missing site";

    if($productID !~ m/^\d+$/) {
        warn "Invalid productID: $productID";
        throw ECS::WarehouseOS::Error(
            'errorCode' => 1040,
            'message'   => "Invalid productID",
        );
    }

    my $sql = qq{
        SELECT DISTINCT
            orders.orderID,
            cart.qty * COALESCE( cartComponent.required, 1 ) AS qty
        FROM orders
        JOIN orderShipping USING (orderID)
        LEFT JOIN shippingService USING (shippingServiceCode)
        LEFT JOIN shippingServiceGroup ON shippingServiceGroup.shippingServiceGroupID = COALESCE( orderShipping.originalShippingServiceGroupID, orderShipping.shippingServiceGroupID )
        JOIN cart
            USING (orderID)
        LEFT JOIN cartComponent 
            USING (cartitemID)
        WHERE
            orders.status = ?
            AND orders.siteID = ?
            AND (cart.productID = ? OR cartComponent.productID = ?)
        ORDER BY
			IF( orders.priority = 'Priority' OR
				shippingService.expedited OR
					shippingServiceGroup.priorityShipping, 1, 2 ),
			LEAST( 
				IF( orders.estimated_shipdate <> '$ECS::Order::BACK_ORDER_DATE', COALESCE(orders.estimated_shipdate, '$ECS::Order::ORDER_NO_DATE'), '$ECS::Order::ORDER_NO_DATE'),
				IF( orders.revised_ETS <> '$ECS::Order::BACK_ORDER_DATE', COALESCE(orders.revised_ETS, '$ECS::Order::ORDER_NO_DATE'), '$ECS::Order::ORDER_NO_DATE')
			),
            orderID
    };
    my $sth = $self->dbhr->prepare( $sql );
    $sth->execute(
       'Picking OSOOB',
        $site->siteID,
        ($productID)x2,
    );

    my @orderIDs = ();
    while(my ($orderID, $qty) = $sth->fetchrow_array) {
        next if $qty > $maxQty;

        $maxQty -= $qty;
        last if $maxQty < 0;

        push @orderIDs, $orderID;
    }

    return @orderIDs;
}

__PACKAGE__->meta->make_immutable;
no Moose;
1;
