#!/usr/bin/perl 
#-----------------------------------
# Script Name: build_holds_queue.pl
# Description: builds a holds queue in the tmp_holdsqueue table
#-----------------------------------
# FIXME: add command-line options for verbosity and summary
# FIXME: expand perldoc, explain intended logic
# FIXME: refactor all subroutines into C4 for testability

use strict;
use warnings;
BEGIN {
    # find Koha's Perl modules
    # test carefully before changing this
    use FindBin;
    eval { require "$FindBin::Bin/../kohalib.pl" };
}

use C4::Context;
use C4::Search;
use C4::Items;
use C4::Branch;
use C4::Circulation;
use C4::Members;
use C4::Biblio;

use List::Util qw(shuffle);

my $bibs_with_pending_requests = GetBibsWithPendingHoldRequests();

my $dbh   = C4::Context->dbh;
$dbh->do("DELETE FROM tmp_holdsqueue");  # clear the old table for new info
$dbh->do("DELETE FROM hold_fill_targets");

my $total_bibs            = 0;
my $total_requests        = 0;
my $total_available_items = 0;
my $num_items_mapped      = 0;

my @branches_to_use = _get_branches_to_pull_from();

foreach my $biblionumber (@$bibs_with_pending_requests) {
    $total_bibs++;
    my $hold_requests   = GetPendingHoldRequestsForBib($biblionumber);
    my $available_items = GetItemsAvailableToFillHoldRequestsForBib($biblionumber, @branches_to_use);
    $total_requests        += scalar(@$hold_requests);
    $total_available_items += scalar(@$available_items);
    my $item_map = MapItemsToHoldRequests($hold_requests, $available_items, @branches_to_use);

    (defined($item_map)) or next;

    my $item_map_size = scalar(keys %$item_map);
    $num_items_mapped += $item_map_size;
    CreatePicklistFromItemMap($item_map);
    AddToHoldTargetMap($item_map);
    if (($item_map_size < scalar(@$hold_requests  )) and
        ($item_map_size < scalar(@$available_items))) {
        # DOUBLE CHECK, but this is probably OK - unfilled item-level requests
        # FIXME
        #warn "unfilled requests for $biblionumber";
        #warn Dumper($hold_requests), Dumper($available_items), Dumper($item_map);
    }
}

exit 0;

=head2 GetBibsWithPendingHoldRequests

=over 4

my $biblionumber_aref = GetBibsWithPendingHoldRequests();

=back

Return an arrayref of the biblionumbers of all bibs
that have one or more unfilled hold requests.

=cut

sub GetBibsWithPendingHoldRequests {
    my $dbh = C4::Context->dbh;

    my $bib_query = "SELECT DISTINCT biblionumber
                     FROM reserves
                     WHERE found IS NULL
                     AND priority > 0
                     AND reservedate <= CURRENT_DATE()";
    my $sth = $dbh->prepare($bib_query);

    $sth->execute();
    my $biblionumbers = $sth->fetchall_arrayref();

    return [ map { $_->[0] } @$biblionumbers ];
}

=head2 GetPendingHoldRequestsForBib

=over 4

my $requests = GetPendingHoldRequestsForBib($biblionumber);

=back

Returns an arrayref of hashrefs to pending, unfilled hold requests
on the bib identified by $biblionumber.  The following keys
are present in each hashref:

    biblionumber
    borrowernumber
    itemnumber
    priority
    branchcode
    reservedate
    reservenotes
    borrowerbranch

The arrayref is sorted in order of increasing priority.

=cut

sub GetPendingHoldRequestsForBib {
    my $biblionumber = shift;

    my $dbh = C4::Context->dbh;

    my $request_query = "SELECT biblionumber, borrowernumber, itemnumber, priority, reserves.branchcode, 
                                reservedate, reservenotes, borrowers.branchcode AS borrowerbranch
                         FROM reserves
                         JOIN borrowers USING (borrowernumber)
                         WHERE biblionumber = ?
                         AND found IS NULL
                         AND priority > 0
                         AND reservedate <= CURRENT_DATE()
                         ORDER BY priority";
    my $sth = $dbh->prepare($request_query);
    $sth->execute($biblionumber);

    my $requests = $sth->fetchall_arrayref({});
    return $requests;

}

=head2 GetItemsAvailableToFillHoldRequestsForBib

=over 4

my $available_items = GetItemsAvailableToFillHoldRequestsForBib($biblionumber);

=back

Returns an arrayref of items available to fill hold requests
for the bib identified by C<$biblionumber>.  An item is available
to fill a hold request if and only if:

    * it is not on loan
    * it is not withdrawn
    * it is not marked notforloan
    * it is not currently in transit
    * it is not lost
    * it is not sitting on the hold shelf

=cut

sub GetItemsAvailableToFillHoldRequestsForBib {
    my $biblionumber = shift;
    my @branches_to_use = @_;

    my $dbh = C4::Context->dbh;
    my $items_query = "SELECT itemnumber, homebranch, holdingbranch, itemtypes.itemtype AS itype
                       FROM items ";

    if (C4::Context->preference('item-level_itypes')) {
        $items_query .=   "LEFT JOIN itemtypes ON (itemtypes.itemtype = items.itype) ";
    } else {
        $items_query .=   "JOIN biblioitems USING (biblioitemnumber)
                           LEFT JOIN itemtypes USING (itemtype) ";
    }
    $items_query .=   "WHERE items.notforloan = 0
                       AND holdingbranch IS NOT NULL
                       AND itemlost = 0
                       AND wthdrawn = 0
                       AND items.onloan IS NULL
                       AND (itemtypes.notforloan IS NULL OR itemtypes.notforloan = 0)
                       AND itemnumber NOT IN (
                           SELECT itemnumber
                           FROM reserves
                           WHERE biblionumber = ?
                           AND itemnumber IS NOT NULL
                           AND (found IS NOT NULL OR priority = 0)
                        )
                       AND items.biblionumber = ?";
    my @params = ($biblionumber, $biblionumber);
    if ($#branches_to_use > -1) {
        $items_query .= " AND holdingbranch IN (" . join (",", map { "?" } @branches_to_use) . ")";
        push @params, @branches_to_use;
    }
    my $sth = $dbh->prepare($items_query);
    $sth->execute(@params);

    my $items = $sth->fetchall_arrayref({});
    $items = [ grep { my @transfers = GetTransfers($_->{itemnumber}); $#transfers == -1; } @$items ]; 
    map { my $rule = GetBranchItemRule($_->{homebranch}, $_->{itype}); $_->{holdallowed} = $rule->{holdallowed}; $rule->{holdallowed} != 0 } @$items;
    return [ grep { $_->{holdallowed} != 0 } @$items ];
}

=head2 MapItemsToHoldRequests

=over 4

MapItemsToHoldRequests($hold_requests, $available_items);

=back

=cut

sub MapItemsToHoldRequests {
    my $hold_requests = shift;
    my $available_items = shift;
    my @branches_to_use = @_;

    # handle trival cases
    return unless scalar(@$hold_requests) > 0;
    return unless scalar(@$available_items) > 0;

    # identify item-level requests
    my %specific_items_requested = map { $_->{itemnumber} => 1 } 
                                   grep { defined($_->{itemnumber}) }
                                   @$hold_requests;

    # group available items by itemnumber
    my %items_by_itemnumber = map { $_->{itemnumber} => $_ } @$available_items;

    # items already allocated
    my %allocated_items = ();

    # map of items to hold requests
    my %item_map = ();
 
    # figure out which item-level requests can be filled    
    my $num_items_remaining = scalar(@$available_items);
    foreach my $request (@$hold_requests) {
        last if $num_items_remaining == 0;

        # is this an item-level request?
        if (defined($request->{itemnumber})) {
            # fill it if possible; if not skip it
            if (exists $items_by_itemnumber{$request->{itemnumber}} and
                not exists $allocated_items{$request->{itemnumber}}) {
                $item_map{$request->{itemnumber}} = { 
                    borrowernumber => $request->{borrowernumber},
                    biblionumber => $request->{biblionumber},
                    holdingbranch =>  $items_by_itemnumber{$request->{itemnumber}}->{holdingbranch},
                    pickup_branch => $request->{branchcode},
                    item_level => 1,
                    reservedate => $request->{reservedate},
                    reservenotes => $request->{reservenotes},
                };
                $allocated_items{$request->{itemnumber}}++;
                $num_items_remaining--;
            }
        } else {
            # it's title-level request that will take up one item
            $num_items_remaining--;
        }
    }

    # group available items by branch
    my %items_by_branch = ();
    foreach my $item (@$available_items) {
        push @{ $items_by_branch{ $item->{holdingbranch} } }, $item unless exists $allocated_items{ $item->{itemnumber} };
    }

    # now handle the title-level requests
    $num_items_remaining = scalar(@$available_items) - scalar(keys %allocated_items); 
    foreach my $request (@$hold_requests) {
        last if $num_items_remaining <= 0;
        next if defined($request->{itemnumber}); # already handled these

        # look for local match first
        my $pickup_branch = $request->{branchcode};
        if (exists $items_by_branch{$pickup_branch} and 
            not ($items_by_branch{$pickup_branch}->[0]->{holdallowed} == 1 and 
                 $request->{borrowerbranch} ne $items_by_branch{$pickup_branch}->[0]->{homebranch}) 
           ) {
            my $item = pop @{ $items_by_branch{$pickup_branch} };
            delete $items_by_branch{$pickup_branch} if scalar(@{ $items_by_branch{$pickup_branch} }) == 0;
            $item_map{$item->{itemnumber}} = { 
                                                borrowernumber => $request->{borrowernumber},
                                                biblionumber => $request->{biblionumber},
                                                holdingbranch => $pickup_branch,
                                                pickup_branch => $pickup_branch,
                                                item_level => 0,
                                                reservedate => $request->{reservedate},
                                                reservenotes => $request->{reservenotes},
                                             };
            $num_items_remaining--;
        } else {
            my @pull_branches = ();
            if ($#branches_to_use > -1) {
                @pull_branches = @branches_to_use;
            } else {
                @pull_branches = sort keys %items_by_branch;
            }
            foreach my $branch (@pull_branches) {
                next unless exists $items_by_branch{$branch} and
                            not ($items_by_branch{$branch}->[0]->{holdallowed} == 1 and 
                                $request->{borrowerbranch} ne $items_by_branch{$branch}->[0]->{homebranch});
                my $item = pop @{ $items_by_branch{$branch} };
                delete $items_by_branch{$branch} if scalar(@{ $items_by_branch{$branch} }) == 0;
                $item_map{$item->{itemnumber}} = { 
                                                    borrowernumber => $request->{borrowernumber},
                                                    biblionumber => $request->{biblionumber},
                                                    holdingbranch => $branch,
                                                    pickup_branch => $pickup_branch,
                                                    item_level => 0,
                                                    reservedate => $request->{reservedate},
                                                    reservenotes => $request->{reservenotes},
                                                 };
                $num_items_remaining--; 
                last;
            }
        }
    }
    return \%item_map;
}

=head2 CreatePickListFromItemMap 

=cut

sub CreatePicklistFromItemMap {
    my $item_map = shift;

    my $dbh = C4::Context->dbh;

    my $sth_load=$dbh->prepare("
        INSERT INTO tmp_holdsqueue (biblionumber,itemnumber,barcode,surname,firstname,phone,borrowernumber,
                                    cardnumber,reservedate,title, itemcallnumber,
                                    holdingbranch,pickbranch,notes, item_level_request)
        VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
    ");

    foreach my $itemnumber  (sort keys %$item_map) {
        my $mapped_item = $item_map->{$itemnumber};
        my $biblionumber = $mapped_item->{biblionumber}; 
        my $borrowernumber = $mapped_item->{borrowernumber}; 
        my $pickbranch = $mapped_item->{pickup_branch};
        my $holdingbranch = $mapped_item->{holdingbranch};
        my $reservedate = $mapped_item->{reservedate};
        my $reservenotes = $mapped_item->{reservenotes};
        my $item_level = $mapped_item->{item_level};

        my $item = GetItem($itemnumber);
        my $barcode = $item->{barcode};
        my $itemcallnumber = $item->{itemcallnumber};

        my $borrower = GetMember('borrowernumber'=>$borrowernumber);
        my $cardnumber = $borrower->{'cardnumber'};
        my $surname = $borrower->{'surname'};
        my $firstname = $borrower->{'firstname'};
        my $phone = $borrower->{'phone'};
   
        my $bib = GetBiblioData($biblionumber);
        my $title = $bib->{title}; 

        $sth_load->execute($biblionumber, $itemnumber, $barcode, $surname, $firstname, $phone, $borrowernumber,
                           $cardnumber, $reservedate, $title, $itemcallnumber,
                           $holdingbranch, $pickbranch, $reservenotes, $item_level);
    }
}

=head2 AddToHoldTargetMap

=cut

sub AddToHoldTargetMap {
    my $item_map = shift;

    my $dbh = C4::Context->dbh;

    my $insert_sql = q(
        INSERT INTO hold_fill_targets (borrowernumber, biblionumber, itemnumber, source_branchcode, item_level_request)
                               VALUES (?, ?, ?, ?, ?)
    );
    my $sth_insert = $dbh->prepare($insert_sql);

    foreach my $itemnumber (keys %$item_map) {
        my $mapped_item = $item_map->{$itemnumber};
        $sth_insert->execute($mapped_item->{borrowernumber}, $mapped_item->{biblionumber}, $itemnumber,
                             $mapped_item->{holdingbranch}, $mapped_item->{item_level});
    }
}

=head2 _get_branches_to_pull_from

Query system preferences to get ordered list of
branches to use to fill hold requests.

=cut

sub _get_branches_to_pull_from {
    my @branches_to_use = ();
  
    my $static_branch_list = C4::Context->preference("StaticHoldsQueueWeight");
    if ($static_branch_list) {
        @branches_to_use = map { s/^\s+//; s/\s+$//; $_; } split /,/, $static_branch_list;
    }

    @branches_to_use = shuffle(@branches_to_use) if  C4::Context->preference("RandomizeHoldsQueueWeight");

    return @branches_to_use;
}
