package GenericLoader;
use strict;

use base qw(Class::Data::Inheritable);

use vars qw(@ALL_VALIDATIONS);
use subs qw(unique);

use CCExtensions;
use Counter;
use File::Basename;
use File::Spec;
use GenericLoaderUtilities;
use InstanceData;
use LogHandles;
use LogHandles::OrderLoader;
use RFC::RFC822::Address;
use Utilities::Data qw(deffed filtered quoted);
use Utilities::Database;
use Validate;

use constant TEXT_COLUMNS    => qw(site_id spid ship_name ship_address1 ship_address2 ship_city
                                   ship_state ship_zip ship_country bill_name bill_address1 bill_address2
                                   bill_city bill_state bill_zip bill_country card_type credit_card_number
                                   expiration_date partial_card_number email gift_name gift_message continuous
                                   sp_order_id non_enews_order_id non_enews_customer_id telephone order_date
                                   load_file_name non_enews_store_id non_enews_source_id non_enews_line_number
                                   currency_ident currency_code readers_advantage_id);
use constant NUMERIC_COLUMNS => qw(amount_paid offer_id send_as_gift_flag email_offers magazine_order_id quantity
                                   price non_enews_email_offers process_line_number load_id);

tie my $new_load_id, 'Counter', 'Load_Id';

#
# Subroutines appear in alphabetical order.
#

sub archive {
    my $class = shift;

    my $order_dir   = File::Spec->catdir($class->ROOT_DIR, $class->ORDER_DIR);
    my $archive_dir = File::Spec->catdir($class->ROOT_DIR, $class->ARCHIVE_DIR);

    my @archived_files;

    opendir ORDER_DIR, $order_dir or
        do { $ERROR_LOG->print("Unable to open $order_dir for reading");
             return;
         };
    foreach my $file (grep !/^\./, readdir ORDER_DIR) {
        next if -d File::Spec->catfile($order_dir, $file);
        if (Encryption::Encrypt_PGP($file, "$file.pgp", $order_dir, $archive_dir)) {
            push @archived_files, $file;
        } else {
            $ERROR_LOG->print("Unable to archive $file");
        }
    }
    closedir ORDER_DIR;

    unlink map {File::Spec->catfile($order_dir, $_)} @archived_files;
}

GenericLoader->mk_classdata('decryptions');

# Decrypt any encrypted files from PGP_DIR into ORDER_DIR
sub decrypt {
    my $class = shift;

    return unless defined $class->PGP_DIR;
    my $pgp_dir   = File::Spec->catdir($class->ROOT_DIR, $class->PGP_DIR);
    my $order_dir = File::Spec->catdir($class->ROOT_DIR, $class->ORDER_DIR);
    opendir PGP_DIR, $pgp_dir or
        do { $ERROR_LOG->print("Unable to open $pgp_dir for reading");
             return;
         };

    my %decryptions = (success => 0,
                       fail    => 0);

    foreach my $file (grep { !/^\./ && /\.pgp$/ } readdir PGP_DIR) {
        next if -d File::Spec->catfile($pgp_dir, $file);
        (my $out_file = $file) =~ s/\.pgp$//;
        if (Encryption::Decrypt_PGP($file, $out_file, $pgp_dir, $order_dir)) {
            ++$decryptions{success};
            unlink File::Spec->catfile($pgp_dir, $file);
        } else {
            ++$decryptions{fail};
            next;
        }
    }

    closedir PGP_DIR;

    $class->decryptions(\%decryptions);
}

sub finalize {}

sub initialize {
    my $class = shift;

    LogHandles::setSubjectName($class->PARTNER);
    if (defined (my $bad_order_sub = $class->BAD_ORDER_REPORT)) {
        $SUMMARY_LOG->add_final_callback($bad_order_sub);
    }

}

sub insert_invalid_order {
    my $class   = shift;
    my $hashref = shift;

    my %new_row;
    @new_row{(TEXT_COLUMNS(), qw(fail_reason order_load_id process_status update_time))} =
              (quoted(@$hashref{(TEXT_COLUMNS(), 'fail_reason')}, InstanceData->CURRENT_DATETIME, 'F'), 'getdate()');
    @new_row{NUMERIC_COLUMNS()} = @$hashref{NUMERIC_COLUMNS()};
    $new_row{load_id} ||= $new_load_id;
    
    Utilities::Database::insert('archive..bad_order_load', %new_row);
}
*insert_duplicate_order = \&insert_invalid_order;

sub insert_valid_order {
    my $class   = shift;
    my $hashref = shift;

    my %new_row;
    @new_row{(TEXT_COLUMNS(), qw(order_load_id process_status update_time))} =
        (quoted(@$hashref{TEXT_COLUMNS()}, InstanceData->CURRENT_DATETIME, 'N'), 'getdate()');
    @new_row{NUMERIC_COLUMNS()} = @$hashref{NUMERIC_COLUMNS()};
    $new_row{load_id} ||= $new_load_id;
    Utilities::Database::insert('archive..order_load', %new_row);
}

sub load_orders {
    my $class      = shift;
    my $order_fh   = shift; # file handle to read from
    my $order_file = shift; # name of the file for recording only
    my $date       = shift;

    my $delimiter = qr/@{[$class->DELIMITER]}/;

    my @OrderHash;

    while (my $order_line = <$order_fh>) {
        next if $order_line =~ m/^#/;
        next unless $order_line =~ /$delimiter/;
        $order_line =~ s/\r?\n$//; # just chomping isn't enough!
        my %hash = (load_date           => $date,
                    load_file_name      => $order_file,
                    process_line_number => $.);
        $class->pre_load(\%hash);
        my @fields = map {s/^\s+//; s/\s+$//; $_} split /$delimiter/, $order_line, -1;
        if (scalar @fields > $class->FIELDS) {
            $APDEV_LOG->print("@{[$class->PARTNER]}:_Too_Many_Fields",
                              "Too many fields in order line from $order_file, line $..");
            next;
        } elsif (scalar @fields < $class->FIELDS) {
            $ERROR_LOG->print("Too few fields in order line ($order_file, line $.), trying anyway.");
        }
        @hash{deffed $class->FIELDS} = @fields;
        $class->post_load(\%hash);
        @hash{keys %hash} = filtered values %hash; # get rid of unwanted characters
        # if a numeric column is all spaces, undef it so that it becomes NULL
        # we don't want to do this for text columns so that this works like
        # the old order loaders.  this sucks.
        foreach (NUMERIC_COLUMNS) {
            next unless defined $hash{$_};
            next if $hash{$_} =~ /\S/;
            undef $hash{$_};
        }
        push @OrderHash, \%hash;
    }
    @OrderHash;
}

{
    (my $log_date = InstanceData->START_DATE) =~ tr/0-9//cd;

    sub log_validation_error {
        my $class   = shift;
        my $hashref = shift;

        my $record = sprintf "%s||0|%s|%s\r\n", deffed @$hashref{qw(non_enews_order_id offer_id order_date)};
        my $file = sprintf "%s/%s/%sResults%s.txt", $class->ROOT_DIR, $class->STATUS_DIR, $class->PARTNER, $log_date;
        open STATUS, ">>$file" or do { $ERROR_LOG->print("Unable to open $file");
                                       return;
                                   };
        print STATUS $record;
        close STATUS;
    }
}

GenericLoader->mk_classdata('order_counts');

# Process the files in ORDER_DIR
sub process {
    my $class = shift;

    my $order_dir   = File::Spec->catdir($class->ROOT_DIR, $class->ORDER_DIR);
    opendir ORDER_DIR, $order_dir or
        do { $ERROR_LOG->print("Unable to open $order_dir for reading");
             return;
         };

    (my $order_date = InstanceData->START_DATE) =~ tr/0-9//cd;

    my @file_counts;

    foreach (grep !/^\./, readdir ORDER_DIR) {
        my $file = File::Spec->catfile($order_dir, $_);
        next if -d $file;
        open ORDERFILE, $file or
            do { $ERROR_LOG->print("Unable to open $file for reading");
                 next;
             };
        my @orders = $class->load_orders(\*ORDERFILE, $_, $order_date);
        close ORDERFILE;
        $class->record_load_statistics(@orders);
        my %total = map { $_ => 0 } qw(valid invalid unloadable duplicate);
        foreach my $order (@orders) {
            my $validity = $class->validate_order($order);
            my $method = "insert_${validity}_order";
            if ($class->$method($order)) {
                ++$total{$validity};
            } else {
                ++$total{unloadable};
            }
        }
        my %file_detail = (file => $_, counts => \%total);
        push @file_counts, \%file_detail;
    }
    closedir ORDER_DIR;
    $class->order_counts(\@file_counts);
}

sub post_load {};

sub pre_load {};

sub record_load_statistics {
    my $class = shift;

    my $site_id = eval join '||' => ((grep /^\d+$/, map {$_->{site_id}} @_), 0);

    my %row = (load_file_name => quoted($_[0]->{load_file_name}),
               load_date      => 'getdate()',
               site_id        => $site_id,
               record_count   => scalar(@_));
    Utilities::Database::insert('archive..OL_record_count', %row);
}

sub refresh_sp_order_id {
    my $class   = shift;
    my $hashref = shift;

    $hashref->{sp_order_id} = GenericLoaderUtilities::get_sp_order_id($hashref);
}

sub report {
    my $class = shift;

    $SUMMARY_LOG->print("Affiliate: ", $class->PARTNER);
    if (defined(my $decrypts = $class->decryptions)) {
        $SUMMARY_LOG->printf("Successful Decryptions: %5d", $decrypts->{success});
        $SUMMARY_LOG->printf("Failed Decryptions:     %5d", $decrypts->{fail});
        $SUMMARY_LOG->printf("Total Decryptions:      %5d", $decrypts->{success} + $decrypts->{fail});
    }
    $SUMMARY_LOG->print("");

    if (defined($class->order_counts) && (my @order_counts = @{$class->order_counts})) {
        my %grand_total;
        foreach my $file_count (@order_counts) {
            $grand_total{$_} += $file_count->{counts}->{$_} foreach keys %{$file_count->{counts}};
        }
        $SUMMARY_LOG->printf("Order Files Processed: %d", scalar @order_counts);
        $SUMMARY_LOG->print("");
        $SUMMARY_LOG->printf("Valid orders:           %5d", $grand_total{valid});
        $SUMMARY_LOG->printf("Invalid orders:         %5d", $grand_total{invalid});
        $SUMMARY_LOG->printf("Duplicate orders:       %5d", $grand_total{duplicate});
        $SUMMARY_LOG->printf("Unloadable orders:      %5d", $grand_total{unloadable});
        $SUMMARY_LOG->print("");
        $SUMMARY_LOG->printf("TOTAL ORDERS PROCESSED: %5d", eval join '+' => values %grand_total);
        $SUMMARY_LOG->print("");
        $SUMMARY_LOG->print("DETAILS:");
        $SUMMARY_LOG->print('=' x 50);
        foreach my $file_count (@order_counts) {
            $SUMMARY_LOG->print("");
            $SUMMARY_LOG->print("ORDER FILE: ", $file_count->{file});
            $SUMMARY_LOG->print('-' x 50);
            $SUMMARY_LOG->printf("Valid orders:      %5d", $file_count->{counts}->{valid});
            $SUMMARY_LOG->printf("Invalid orders:    %5d", $file_count->{counts}->{invalid});
            $SUMMARY_LOG->printf("Duplicate orders:  %5d", $file_count->{counts}->{duplicate});
            $SUMMARY_LOG->printf("Unloadable orders: %5d", $file_count->{counts}->{unloadable});
            $SUMMARY_LOG->print("");
            $SUMMARY_LOG->printf("TOTAL PROCESSED IN FILE: %5d", eval join '+' => values %{$file_count->{counts}});
        }
    } else {
        $SUMMARY_LOG->print("No files loaded");
    }
    if (defined $class->bad_orders) {
        $SUMMARY_LOG->print("");
        $SUMMARY_LOG->print("INVALID ORDER DETAILS:");
        $SUMMARY_LOG->print('=' x 50);
        $SUMMARY_LOG->print($_) foreach @{$class->bad_orders};
    }

}

sub report_validation_error {
    my $class   = shift;
    my $hashref = shift;

    $ERROR_LOG->printf("File: %s, Line: %d, invalid due to %s",
                       @$hashref{qw(load_file_name process_line_number fail_reason)});

}

GenericLoader->mk_classdata('bad_orders');

sub store_bad_order {}

sub store_bad_order_detail {
    my $class   = shift;
    my $hashref = shift;

    my $bad_order = join ' | ' => deffed @$hashref{qw(non_enews_line_number non_enews_store_id non_enews_source_id
                                                      non_enews_order_id fail_reason order_date load_date ship_name
                                                      ship_address1 ship_address2 ship_city ship_state ship_zip
                                                      ship_country bill_name bill_address1 bill_address2 bill_city
                                                      bill_state bill_zip bill_country amount_paid offer_id
                                                      card_type email gift_name send_as_gift_flag gift_message
                                                      quantity non_enews_customer_id price telephone load_file_name)};
    if (defined $class->bad_orders) {
        push @{$class->bad_orders}, $bad_order;
    } else {
        $class->bad_orders([$bad_order]);
    }
}

sub store_bad_order_summary {
    my $class   = shift;
    my $hashref = shift;

    my $bad_order = join ' | ' => @$hashref{qw(load_file_name order_date non_enews_order_id email fail_reason)};
    if (defined $class->bad_orders) {
        push @{$class->bad_orders}, $bad_order;
    } else {
        $class->bad_orders([$bad_order]);
    }
}

push @ALL_VALIDATIONS, 'bill_address';
sub validate_bill_address {
    my $class   = shift;
    my $hashref = shift;

    my @errors;

    my @required_fields = qw(bill_name bill_address1 bill_city bill_country);
    unless (GenericLoaderUtilities::price_code($hashref->{bill_country}) eq 'F') {
        push @required_fields, qw(bill_state bill_zip);
    }
    foreach my $field (@required_fields) {
        push @errors, GenericLoaderUtilities::validate_required($hashref, $field);
    }
    push @errors, GenericLoaderUtilities::validate_billable($hashref->{bill_country});
    push @errors, map {"bill_$_"} GenericLoaderUtilities::validate_state($hashref->{bill_country},
                                                                         $hashref->{bill_state});
    @errors;
}

push @ALL_VALIDATIONS, 'bill_zip';
sub validate_bill_zip {
    my $class   = shift;
    my $hashref = shift;

    # if validate_zip returns 'zip', map it to 'bill_zip'
    map {"bill_$_"} GenericLoaderUtilities::validate_zip($hashref->{bill_zip}, $hashref->{bill_country});
}

push @ALL_VALIDATIONS, 'credit_card';
sub validate_credit_card {
    my $class   = shift;
    my $hashref = shift;

    my @errors;

    my $cc_number = (CCExtensions::Decrypt($hashref->{credit_card_number}))[1];
    if (GenericLoaderUtilities::luhn_sum($cc_number) % 10) {
        push @errors, 'credit_card_number';
    }
    if ($hashref->{card_type} ne GenericLoaderUtilities::card_type($cc_number)) {
        push @errors, 'card_type';
    }
    @errors;
}

push @ALL_VALIDATIONS, 'email';
sub validate_email {
    my $class   = shift;
    my $hashref = shift;

    my @errors;

    if (length $hashref->{email} > 50) {
        $ERROR_LOG->print('Email address too long!');
        push @errors, 'email';
    } elsif (not RFC::RFC822::Address::valid($hashref->{email})) {
        $ERROR_LOG->print("$hashref->{email} is not a valid email address.");
        push @errors, 'email';
    }
    @errors;
}

push @ALL_VALIDATIONS, 'expiration_date';
sub validate_expiration_date {
    my $class   = shift;
    my $hashref = shift;

    my @errors;
    my $delay_days = GenericLoaderUtilities::charge_delay_days($hashref->{site_id});

    unless (GenericLoaderUtilities::valid_expiration_date($hashref->{expiration_date},
                                                          $hashref->{order_date},
                                                          $delay_days)) {
        push @errors, 'expiration_date';
    }
    @errors;
}

push @ALL_VALIDATIONS, 'offer';
sub validate_offer {
    my $class   = shift;
    my $hashref = shift;

    map {"offer_id: $_"} GenericLoaderUtilities::valid_offer(@$hashref{qw(offer_id continuous order_date
                                                                          price ship_country)});
}

sub validate_offer_except_price {
    my $class   = shift;
    my $hashref = shift;

    map {"offer_id: $_"}
    GenericLoaderUtilities::valid_offer_disregarding_price(@$hashref{qw(offer_id continuous order_date
                                                                        price ship_country)});
}

sub validate_order {
    my $class   = shift;
    my $hashref = shift;

    my @errors;
    my $invalid = 'invalid';

    foreach my $routine ($class->VALIDATIONS) {
        my $function = "validate_$routine";
        push @errors, $class->$function($hashref);
    }
    if (Validate::DUPLICATE($hashref)) {
        unshift @errors, 'duplicate';
        $invalid = 'duplicate';
    }
    if (@errors) {
        $hashref->{fail_reason} = join ' ' => unique @errors;
        $class->report_validation_error($hashref);
        $class->log_validation_error($hashref);
        $class->store_bad_order($hashref);
        $invalid;
    } else {
        'valid';
    }
}

push @ALL_VALIDATIONS, 'order_date';
sub validate_order_date {
    my $class   = shift;
    my $hashref = shift;

    my @errors;

    unless (GenericLoaderUtilities::p_datetime($hashref->{order_date})) {
        push @errors, 'order_date';
    }
    @errors;
}

push @ALL_VALIDATIONS, 'ship_address';
sub validate_ship_address {
    my $class   = shift;
    my $hashref = shift;

    my @errors;

    foreach my $field (qw(ship_name ship_address1 ship_city ship_state ship_zip ship_country)) {
        push @errors, GenericLoaderUtilities::validate_required($hashref, $field);
    }
    push @errors, GenericLoaderUtilities::validate_shippable($hashref->{ship_country});
    push @errors, map {"ship_$_"} GenericLoaderUtilities::validate_state($hashref->{ship_country},
                                                                          $hashref->{ship_state});
    @errors;
}

push @ALL_VALIDATIONS, 'ship_zip';
sub validate_ship_zip {
    my $class   = shift;
    my $hashref = shift;

    # if validate_zip returns 'zip', map it to 'ship_zip'
    map {"ship_$_"} GenericLoaderUtilities::validate_zip($hashref->{ship_zip}, $hashref->{ship_country});
}

push @ALL_VALIDATIONS, 'site_id';
sub validate_site_id {
    my $class   = shift;
    my $hashref = shift;

    my @errors;

    unless (GenericLoaderUtilities::valid_site($hashref->{site_id})) {
        push @errors, 'site_id';
    }
    @errors;
}

# sp order id is usually the catenation of site_id, non_enews_order_id, order_date, and non_enews_line_number
# order_date will now be stored like the string "Apr 15 1987 10:23PM" so we need to change that to 19870415.
# We've changed the way that sp_order_id is generated, so we need to strip out all non digits from both the
# generated sp_order_id and the one from the order before comparing them.
push @ALL_VALIDATIONS, 'sp_order_id';
sub validate_sp_order_id {
    my $class   = shift;
    my $hashref = shift;

    my @errors;

    foreach my $field (qw(non_enews_order_id non_enews_line_number)) {
        push @errors, GenericLoaderUtilities::validate_required($hashref, $field);
    }

    my $order_date         = GenericLoaderUtilities::datetime_to_yyyymmdd($hashref->{order_date});
    my $sp_order_id_check  =
        "$hashref->{site_id}$hashref->{non_enews_order_id}$order_date$hashref->{non_enews_line_number}";
    $sp_order_id_check =~ tr/0-9A-Za-z//cd;
    if ($sp_order_id_check ne $hashref->{sp_order_id}) {
        push @errors, 'sp_order_id';
    }
    @errors;
}

sub unique {
    my %things;
    map {$things{$_} = 1} @_;
    keys %things;
}

1;
