# ---------------------------------------------------------------
# Copyright © 2014 Jason J.A. Stephenson <jason@sigio.com>
#
# This file is part of NCIPServer.
#
# NCIPServer is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# NCIPServer is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with NCIPServer.  If not, see <http://www.gnu.org/licenses/>.
# ---------------------------------------------------------------
package NCIP::ILS::Evergreen;

use Modern::Perl;
use XML::LibXML::Simple qw(XMLin);
use DateTime;
use DateTime::Format::ISO8601;
use Digest::MD5 qw/md5_hex/;
use OpenSRF::System;
use OpenSRF::Utils qw/:datetime/;
use OpenSRF::Utils::SettingsClient;
use OpenILS::Utils::Fieldmapper;
use OpenILS::Utils::CStoreEditor qw/:funcs/;
use OpenILS::Application::AppUtils;
use OpenILS::Const qw/:const/;
use MARC::Record;
use MARC::Field;
use MARC::File::XML;

# We need a bunch of NCIP::* objects.
use NCIP::Response;
use NCIP::Problem;
use NCIP::User;
use NCIP::User::OptionalFields;
use NCIP::User::AddressInformation;
use NCIP::User::Id;
use NCIP::User::BlockOrTrap;
use NCIP::User::Privilege;
use NCIP::User::PrivilegeStatus;
use NCIP::StructuredPersonalUserName;
use NCIP::StructuredAddress;
use NCIP::ElectronicAddress;

# Inherit from NCIP::ILS.
use parent qw(NCIP::ILS);

# Default values we define for things that might be missing in our
# runtime environment or configuration file that absolutely must have
# values.
#
# OILS_NCIP_CONFIG_DEFAULT is the default location to find our
# driver's configuration file.  This location can be overridden by
# setting the path in the OILS_NCIP_CONFIG environment variable.
#
# BIB_SOURCE_DEFAULT is the config.bib_source.id to use when creating
# "short" bibs.  It is used only if no entry is supplied in the
# configuration file.  The provided default is 2, the id of the
# "System Local" source that comes with a default Evergreen
# installation.
use constant {
    OILS_NCIP_CONFIG_DEFAULT => '/openils/conf/oils_ncip.xml',
    BIB_SOURCE_DEFAULT => 2
};

# A common Evergreen code shortcut to use AppUtils:
my $U = 'OpenILS::Application::AppUtils';

# The usual constructor:
sub new {
    my $class = shift;
    $class = ref $class or $class;

    # Instantiate our parent with the rest of the arguments.  It
    # creates a blessed hashref.
    my $self = $class->SUPER::new(@_);

    # Look for our configuration file, load, and parse it:
    $self->_configure();

    # Bootstrap OpenSRF and prepare some OpenILS components.
    $self->_bootstrap();

    # Initialize the rest of our internal state.
    $self->_init();

    return $self;
}

sub lookupuser {
    my $self = shift;
    my $request = shift;

    # Check our session and login if necessary.
    $self->login() unless ($self->checkauth());

    my $message_type = $self->parse_request_type($request);

    # Let's go ahead and create our response object. We need this even
    # if there is a problem.
    my $response = NCIP::Response->new({type => $message_type . "Response"});
    $response->header($self->make_header($request));

    # Need to parse the request object to get the barcode and other
    # data out.
    my $barcode = $self->find_barcode($request);

    # If we can't find a barcode, report a problem.
    unless ($barcode) {
        # Fill in a problem object and stuff it in the response.
        my $problem = NCIP::Problem->new();
        $problem->ProblemType('Needed Data Missing');
        $problem->ProblemDetail('Cannot find user barcode in message.');
        $problem->ProblemElement('AuthenticationInputType');
        $problem->ProblemValue('Barcode');
        $response->problem($problem);
        return $response;
    }

    # Look up our patron by barcode:
    my $user = $U->simplereq(
        'open-ils.actor',
        'open-ils.actor.user.fleshed.retrieve_by_barcode',
        $self->{session}->{authtoken},
        $barcode,
        0
    );

    # Check for a failure, or a deleted, inactive, or expired user,
    # and if so, return empty userdata.
    if (!$user || $U->event_code($user) || $U->is_true($user->deleted())
            || !grep {$_->barcode() eq $barcode && $U->is_true($_->active())} @{$user->cards()}) {

        my $problem = NCIP::Problem->new();
        $problem->ProblemType('Unknown User');
        $problem->ProblemDetail("User with barcode $barcode unknown");
        $problem->ProblemElement('AuthenticationInputData');
        $problem->ProblemValue($barcode);
        return $response;
    }

    # We got the information, so lets fill in our userdata.
    my $userdata = NCIP::User->new();

    # Make an array of the user's active barcodes.
    my $ids = [];
    foreach my $card (@{$user->cards()}) {
        if ($U->is_true($card->active())) {
            my $id = NCIP::User::Id->new({
                UserIdentifierType => 'Barcode',
                UserIdentifierValue => $card->barcode()
            });
            push(@$ids, $id);
        }
    }
    $userdata->UserId($ids);

    # Check if they requested any optional fields and return those.
    my $elements = $request->{$message_type}->{UserElementType};
    if ($elements) {
        $elements = [$elements] unless (ref $elements eq 'ARRAY');
        my $optionalfields = NCIP::User::OptionalFields->new();

        # First, we'll look for name information.
        if (grep {$_ eq 'Name Information'} @$elements) {
            my $name = NCIP::StructuredPersonalUserName->new();
            $name->Surname($user->given_name());
            $name->GivenName($user->first_given_name());
            $name->Prefix($user->prefix());
            $name->Suffix($user->suffix());
            $optionalfields->NameInformation($name);
        }

        # Next, check for user address information.
        if (grep {$_ eq 'User Address Information'} @$elements) {
            my $addresses = [];

            # See if the user has any valid, physcial addresses.
            foreach my $addr (@{$user->addresses()}) {
                next if ($U->is_true($addr->pending()));
                my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>$addr->address_type()});
                my $physical = NCIP::StructuredAddress->new();
                $physical->Line1($addr->street1());
                $physical->Line2($addr->street2());
                $physical->Locality($addr->city());
                $physical->Region($addr->state());
                $physical->PostalCode($addr->post_code());
                $physical->Country($addr->country());
                $address->PhysicalAddress($physical);
                push @$addresses, $address;
            }

            # Right now, we're only sharing email address if the user
            # has it. We don't share phone numbers.
            if ($user->email()) {
                my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>'Email Address'});
                $address->ElectronicAddress(
                    NCIP::ElectronicAddress->new({
                        Type=>'Email Address',
                        Data=>$user->email()
                    })
                );
                push @$addresses, $address;
            }

            $optionalfields->UserAddressInformation($addresses);
        }

        # Fetch the user's home_ou. We'll need for a couple of things
        # below here.
        my $aou = $self->editor->retrieve_actor_org_unit($user->home_ou());

        # Check for User Privilege.
        if (grep {$_ eq 'User Privilege'} @$elements) {
            # Get the user's group:
            my $pgt = $self->editor->retrieve_permission_grp_tree($user->profile());
            if ($pgt) {
                my $privilege = NCIP::User::Privilege->new();
                $privilege->AgencyId($aou->shortname());
                $privilege->AgencyUserPrivilegeType($pgt->name());
                $privilege->ValidToDate($user->expire_date());

                my $status = 'Active';
                if (_expired($user)) {
                    $status = 'Expired';
                } elsif ($U->is_true($user->barred())) {
                    $status = 'Barred';
                } elsif (!$U->is_true($user->active())) {
                    $status = 'Inactive';
                }
                if ($status) {
                    $privilege->UserPrivilegeStatus(
                        NCIP::User::PrivilegeStatus->new({
                            UserPrivilegeStatusType => $status
                        })
                    );
                }

                $optionalfields->UserPrivilege([$privilege]);
            }
        }

        # Check for Block Or Trap.
        if (grep {$_ eq 'Block Or Trap'} @$elements) {
            my $blocks = [];

            # First, let's check if the profile is blocked from ILL.
            if (grep {$_->id() == $user->profile()} @{$self->{block_profiles}}) {
                my $block = NCIP::User::BlockOrTrap->new();
                $block->AgencyId($aou->shortname());
                $block->BlockOrTrapType('Block Interlibrary Loan');
                push @$blocks, $block;
            }

            # Next, we loop through the user's standing penalties
            # looking for blocks on CIRC, HOLD, and RENEW.
            my ($have_circ, $have_renew, $have_hold) = (0,0,0);
            foreach my $penalty (@{$user->standing_penalties()}) {
                my @block_list = split(/\|/, $penalty->standing_penalty->block_list());
                my $ou = $self->editor->retrieve_actor_org_unit($penalty->standing_penalty->org_unit());

                # Block checkout.
                if (!$have_circ && grep {$_ eq 'CIRC'} @block_list) {
                    my $bot = NCIP::User::BlockOrTrap->new();
                    $bot->AgencyId($ou->shortname());
                    $bot->BlockOrTrapType('Block Checkout');
                    push @blocks, $bot;
                    $have_circ = 1;
                }

                # Block holds.
                if (!$have_hold && grep {$_ eq 'HOLD'} @block_list) {
                    my $bot = NCIP::User::BlockOrTrap->new();
                    $bot->AgencyId($ou->shotrname());
                    $bot->BlockOrTrapType('Block Holds');
                    push @blocks, $bot;
                    $have_hold = 1;
                }

                # Block renewals.
                if (!$have_renew && grep {$_ eq 'RENEW'} @block_list) {
                    my $bot = NCIP::User::BlockOrTrap->new();
                    $bot->AgencyId($ou->shortname());
                    $bot->BlockOrTrapType('Block Renewals');
                    push @blocks, $bot;
                    $have_renew = 1;
                }

                # Stop after we report one of each, even if more
                # blocks remain.
                last if ($have_circ && $have_renew && $have_hold);
            }

            $optionalfields->BlockOrTrap($blocks);
        }

        $userdata->UserOptionalFields($optionalfields);
    }

    $response->data($userdata);

    return $response;
}

# Implementation functions that might be useful to a subclass.

# Get a CStoreEditor:
sub editor {
    my $self = shift;

    # If we have an editor, check the validity of the auth session, then
    # invalidate the editor if the session is not valid.
    if ($self->{editor}) {
        undef($self->{editor}) unless ($self->checkauth());
    }

    # If we don't have an editor, make a new one.
    unless (defined($self->{editor})) {
        $self->login() unless ($self->checkauth());
        $self->{editor} = new_editor(authtoken=>$self->{session}->{authtoken});
    }

    return $self->{editor};
}

# Login via OpenSRF to Evergreen.
sub login {
    my $self = shift;

    # Get the authentication seed.
    my $seed = $U->simplereq(
        'open-ils.auth',
        'open-ils.auth.authenticate.init',
        $self->{config}->{credentials}->{username}
    );

    # Actually login.
    if ($seed) {
        my $response = $U->simplereq(
            'open-ils.auth',
            'open-ils.auth.authenticate.complete',
            {
                username => $self->{config}->{credentials}->{username},
                password => md5_hex(
                    $seed . md5_hex($self->{config}->{credentials}->{password})
                ),
                type => 'staff',
                workstation => $self->{config}->{credentials}->{workstation}
            }
        );
        if ($response) {
            $self->{session}->{authtoken} = $response->{payload}->{authtoken};
            $self->{session}->{authtime} = $response->{payload}->{authtime};
        }
    }
}

# Return 1 if we have a 'valid' authtoken, 0 if not.
sub checkauth {
    my $self = shift;

    # We implement our own version of this function, rather than rely
    # on CStoreEditor, because we may want to check this at times that
    # we don't have a CStoreEditor.

    # We use AppUtils to do the heavy lifting.
    if (defined($self->{session})) {
        if ($U->check_user_session($self->{session}->{authtoken})) {
            return 1;
        } else {
            return 0;
        }
    }

    # If we reach here, we don't have a session, so we are definitely
    # not logged in.
    return 0;
}

# private subroutines not meant to be used directly by subclasses.
# Most have to do with setup and/or state checking of implementation
# components.

# Find, load, and parse our configuration file:
sub _configure {
    my $self = shift;

    # Find the configuration file via variables:
    my $file = OILS_NCIP_CONFIG_DEFAULT;
    $file = $ENV{OILS_NCIP_CONFIG} if ($ENV{OILS_NCIP_CONFIG});

    $self->{config} = XMLin($file, NormaliseSpace => 2,
                            ForceArray => ['block_profile', 'stat_cat_entry']);
}

# Bootstrap OpenSRF::System, load the IDL, and initialize the
# CStoreEditor module.
sub _bootstrap {
    my $self = shift;

    my $bootstrap_config = $self->{config}->{bootstrap};
    OpenSRF::System->bootstrap_client(config_file => $bootstrap_config);

    my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
    Fieldmapper->import(IDL => $idl);

    OpenILS::Utils::CStoreEditor->init;
}

# Login and then initialize some object data based on the
# configuration.
sub _init {
    my $self = shift;

    # Login to Evergreen.
    $self->login();

    # Create an editor.
    my $e = $self->editor();

    # Retrieve the work_ou as an object.
    my $work_ou = $e->search_actor_org_unit(
        {shortname => $self->{config}->{credentials}->{work_ou}}
    );
    $self->{work_ou} = $work_ou->[0] if ($work_ou && @$work_ou);

    # Load the barred groups as pgt objects into a blocked_profiles
    # list.
    $self->{blocked_profiles} = [];
    foreach (@{$self->{config}->{patrons}->{block_profile}}) {
        if (ref $_) {
            my $pgt = $e->retrieve_permission_grp_tree($_->{grp});
            push(@{$blocked_profiles}, $pgt) if ($pgt);
        } else {
            my $result = $e->search_permission_grp_tree({name => $_});
            if ($result && @$result) {
                map {push(@{$self->{blocked_profiles}}, $_)} @$result;
            }
        }
    }

    # Load the bib source if we're not using precats.
    unless ($self->{config}->{items}->{use_precats}) {
        # Retrieve the default
        my $cbs = $e->retrieve_config_bib_source(BIB_SOURCE_DEFAULT);
        my $data = $self->{config}->{items}->{bib_source};
        if ($data) {
            $data = $data->[0] if (ref($data) eq 'ARRAY');
            if (ref $data) {
                my $result = $e->retrieve_config_bib_source($data->{cbs});
                $cbs = $result if ($result);
            } else {
                my $result = $e->search_config_bib_source({source => $data});
                if ($result && @$result) {
                    $cbs = $result->[0]; # Use the first one.
                }
            }
        }
        $self->{bib_source} = $cbs;
    }

    # Load the required asset.stat_cat_entries:
    $self->{stat_cat_entries} = [];
    foreach (@{$self->{config}->{items}->{stat_cat_entry}}) {
        # Must have the stat_cat attr and the name, so we must have a
        # reference.
        next unless(ref $_);
        # We want to limit the search to the work org and its
        # ancestors.
        my $ancestors = $U->get_org_ancestors($self->{work_ou}->id());
        my $result = $e->search_asset_stat_cat_entry(
            {
                stat_cat => $_->{stat_cat},
                value => $_->{content},
                owner => $ancestors
            }
        );
        if ($result && @$result) {
            map {push(@{$self->{stat_cat_entries}}, $_)} @$result;
        }
    }
}

# Standalone, "helper" functions.  These do not take an object or
# class reference.

# Check if a user is past their expiration date.
sub _expired {
    my $user = shift;
    my $expired = 0;

    # Users might not expire.  If so, they have no expire_date.
    if ($user->expire_date()) {
        my $expires = DateTime::Format::ISO8601->parse_datetime(
            cleanse_ISO8601($user->expire_date())
        )->epoch();
        my $now = DateTime->now()->epoch();
        $expired = $now > $expires;
    }

    return $expired;
}

1;
