#!/usr/bin/perl -Ilibs

use strict;
use warnings;

use API::Controller ();
use JSON ();
use List::MoreUtils ();

package API::PricingRuleAdmin;

use Try::Tiny;

use ECS::Product;
use ECS::Vendor;
use ECS::Brand;

use ECS::Product::PricingRule;
use ECS::Product::PricingRule::Action;
use ECS::Product::PricingRule::Criteria;
use ECS::Product::PricingRule::CriteriaGroupContent;

use ECS::Product::PricingRule::Admin;
use ECS::Product::PricingRule::Admin::ChangeLog;

use ECS::Product::ProductHandler;
use ECS::Product::PricingRule::RuleRunner;

use Moose;
extends 'API::Controller';

__PACKAGE__->meta->make_immutable;

return 1;

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

sub ruleIndex {
    my $self = (shift)->new({'input' => shift || {}});
    
    my $rules = ECS::Product::PricingRule->search({
        'dbh' => $self->dbhr,
        'search' => [{}, {
            'prefetch' => [], # disables PREFETCH_ALL
        }]
    });
    
    my $actions = ECS::Product::PricingRule::Action->search({
        'dbh' => $self->dbhr,
    });
    
    $self->data->{'allPricingRuleIDs'} =
        [map { $_->pricingRuleID } @{ $rules }];
    
    $self->data->{'allPricingRuleActionIDs'} =
        [map { $_->pricingRuleActionID } @{ $actions }];
    
    $self->data->{'templates'} = [ "index.html" ];
    
    return $self->result;     
}

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

sub getActionChangeLog {
    my $self = (shift)->new({'input' => shift || {}});
    
    $self->data->{'dataType'} = "json";
    $self->data->{'data'}->{'changes'} = ECS::Product::PricingRule::Admin::ChangeLog::getChangeLog({
        'dbh' => $self->dbhr,
        'startAfterChangeID' => $self->input->{'lastChangeID'},
        'limit' => $self->input->{'limit'},
        'tableName' => 'pricingRuleAction',
        ($self->input->{'itemID'} ?
            ('itemID' => $self->input->{'itemID'})
            : ()
        ),
    });
    
    return $self->result;
}

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

sub getCriteriaChangeLog {
    my $self = (shift)->new({'input' => shift || {}});
    
    $self->data->{'dataType'} = "json";
    $self->data->{'data'}->{'changes'} = ECS::Product::PricingRule::Admin::ChangeLog::getChangeLog({
        'dbh' => $self->dbhr,
        'startAfterChangeID' => $self->input->{'lastChangeID'},
        'limit' => $self->input->{'limit'},
        'tableName' => 'pricingRuleCriteria',
        ($self->input->{'itemID'} ?
            ('itemID' => $self->input->{'itemID'})
            : ()
        ),
    });
    
    return $self->result;
}

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

sub getRuleChangeLog {
    my $self = (shift)->new({'input' => shift || {}});
    
    $self->data->{'dataType'} = "json";
    $self->data->{'data'}->{'changes'} = ECS::Product::PricingRule::Admin::ChangeLog::getChangeLog({
        'dbh' => $self->dbhr,
        'startAfterChangeID' => $self->input->{'lastChangeID'},
        'limit' => $self->input->{'limit'},
        'tableName' => 'pricingRule',
        ($self->input->{'itemID'} ?
            ('itemID' => $self->input->{'itemID'})
            : ()
        ),
    });
    
    return $self->result;
}

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

sub getRuleAssocGroupChangeLog {
    my $self = (shift)->new({'input' => shift || {}});
    
    my $changes = ECS::Product::PricingRule::Admin::ChangeLog::getChangeLogAssoc({
        'dbh' => $self->dbhr,
        'startAfterChangeID' => $self->input->{'lastChangeID'},
        'limit' => $self->input->{'limit'},
        'tableNames' => ['pricingRuleToGroup'],
        'itemID' => $self->input->{'itemID'},
    });
    
    my %rules;
    
    if (@{ $changes }) {
        
        my @ruleIDs = List::MoreUtils::uniq(
            map { $_->{'tableID'} || $_->{'valueNew'} }
                @{ $changes }
        );
        
        %rules = map { $_->pricingRuleID => $_ } @{
            ECS::Product::PricingRule->search({
                'dbh' => $self->dbh,
                'search' => [
                    {
                        'me.pricingRuleID' => \@ruleIDs,
                    },
                    {
                        'prefetch' => [], # disables PREFETCH_ALL
                    }
                ]
            })
        };
    }
    
    for my $change (@{ $changes }) {
        my $child_ruleID = (
            $change->{'tableID'}
            || $change->{'valueNew'}
        );
        
        if (
            $child_ruleID
            and my $rule = $rules{$child_ruleID}
        ) {
            
            $change->{'content'} = $rule->ruleName;
            
            if (
                defined $change->{'valueOld'}
                and defined $change->{'valueNew'}
            ) {
                $change->{'content'} .= sprintf(' (%s -> %s)', (
                    $change->{'valueOld'},
                    $change->{'valueNew'},
                ));
            }
        }
        elsif ($change->{'valueNew'}) {
            $change->{'content'} = $change->{'valueNew'};
        }
    }
    
    $self->data->{'dataType'} = "json";
    $self->data->{'data'}->{'changes'} = $changes;
    
    return $self->result;
}

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

sub getRuleAssocActionChangeLog {
    my $self = (shift)->new({'input' => shift || {}});
    
    my $changes = ECS::Product::PricingRule::Admin::ChangeLog::getChangeLogAssoc({
        'dbh' => $self->dbhr,
        'startAfterChangeID' => $self->input->{'lastChangeID'},
        'limit' => $self->input->{'limit'},
        'tableNames' => ['pricingRuleToAction'],
        'itemID' => $self->input->{'itemID'},
    });
    
    my %actions;
    
    if (@{ $changes }) {
        
        my @actionIDs = List::MoreUtils::uniq(
            map { $_->{'tableID'} }
                @{ $changes }
        );
        
        %actions = map { $_->pricingRuleActionID => $_ } @{
            ECS::Product::PricingRule::Action->search({
                'dbh' => $self->dbhr,
                'search' => [
                    {
                        'me.pricingRuleActionID' => \@actionIDs,
                    },
                    {
                        'prefetch' => ['field'],
                    },
                ],
            })
        };
    }
    
    for my $change (@{ $changes }) {
        if (
            $change->{'tableID'}
            and my $action = $actions{$change->{'tableID'}}
        ) {
            # actionField, actionValue, actionMethod
            $change->{'content'} = sprintf('%s = %s', (
                $action->fieldLabel,
                $action->valueLabel,
            ));
        }
    }
    
    $self->data->{'dataType'} = "json";
    $self->data->{'data'}->{'changes'} = $changes;
    
    return $self->result;
}

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

sub getRuleAssocCriteriaChangeLog {
    my $self = (shift)->new({'input' => shift || {}});
    
    my $changes = ECS::Product::PricingRule::Admin::ChangeLog::getRuleCriteriaChangeLog({
        'dbh' => $self->dbhr,
        'pricingRuleID' => $self->input->{'itemID'},
        'startAfterChangeID' => $self->input->{'lastChangeID'},
        'limit' => $self->input->{'limit'},
    });
    
    $self->data->{'dataType'} = "json";
    $self->data->{'data'}->{'changes'} = $changes;
    
    return $self->result;
}

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

sub getRuleAssocVendorBrandChangeLog {
    my $self = (shift)->new({'input' => shift || {}});
    
    my $changes = ECS::Product::PricingRule::Admin::ChangeLog::getChangeLogAssoc({
        'dbh' => $self->dbhr,
        'startAfterChangeID' => $self->input->{'lastChangeID'},
        'limit' => $self->input->{'limit'},
        'tableNames' => [qw(
            vendor
            brand
        )],
        'itemID' => $self->input->{'itemID'},
    });
    
    my %vendors;
    my %brands;
    
    for my $change (@{ $changes }) {
        
        next unless $change->{'tableID'};
        
        if ($change->{'tableName'} eq 'vendor') {
            $vendors{$change->{'tableID'}} = 1;
        }
        elsif ($change->{'tableName'} eq 'brand') {
            $brands{$change->{'tableID'}} = 1;
        }
    }
    
    if (%vendors) {
        %vendors = map { $_->{'vendorID'} => $_ } @{
            ECS::Vendor::getAllVendorsName({
                'DBH' => $self->dbhr,
            })
        };
    }
    
    if (%brands) {
        %brands = map { $_->{'brandID'} => $_ } @{
            ECS::Brand::getAllBrandsName({
                'DBH' => $self->dbhr,
            })
        };
    }
    
    for my $change (@{ $changes }) {
        if ($change->{'tableName'} eq 'vendor') {
            $change->{'content'} = sprintf('Vendor: %s', (
                ($vendors{$change->{'tableID'}} ?
                    $vendors{$change->{'tableID'}}->{'mm_VendorID'}
                    : 'All'
                ),
            ));
        }
        elsif ($change->{'tableName'} eq 'brand') {
            $change->{'content'} = sprintf('Brand: %s', (
                ($brands{$change->{'tableID'}} ?
                    $brands{$change->{'tableID'}}->{'name'}
                    : 'All'
                ),
            ));
        }
    }
    
    $self->data->{'dataType'} = "json";
    $self->data->{'data'}->{'changes'} = $changes;
    
    return $self->result;
}

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

sub getRuleSummary {
    my $self = (shift)->new({'input' => shift || {}});
    
    $self->data->{'pricingRuleID'} = $self->input->{'pricingRuleID'};
    $self->data->{'dataType'} = "plain";
    $self->data->{'templates'} = [ "ruleSummary.html" ];
    
    return $self->result; 
}

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

sub updateRuleOrder {
    my $self = (shift)->new({'input' => shift || {}});
    my $p = $self->input;
    $p->{'dbh'} = $self->dbh;

    ECS::Product::PricingRule::Admin::updateRuleOrder($p);

    $self->data->{'dataType'} = "json";
    $self->data->{'data'}->{'success'} = 1;
    return $self->result;
}

sub newRule {
    my $self = (shift)->new({'input' => shift || {}});
    
    $self->data->{'pricingRuleID'} = 0;
    $self->data->{'templates'} = [ "editRule.html" ];
    
    return $self->result; 
}

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

sub createRule {
    my $self = (shift)->new({'input' => shift || {}});
    
    my $p =  $self->input;
    $p->{'dbh'} = $self->dbh;
    
    my $result = ECS::Product::PricingRule::Admin::createRule($p);
    
    unless ($result->{'success'}) {
        
        $self->data->{'redirect'} = (
            './?action=newRule'
            .'&errorMsg=Failed to Add New Rule.'
        );
        
        warn $result->{'result_message'};
        return $self->result;
    }
    
    $self->data->{'redirect'} = (
        '?action=editRule&pricingRuleID='.$result->{'pricingRuleID'}
        .'&successMsg=Successfully added new Rule.'
    );
    return $self->result;
}

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

sub editRule {
    my $self = (shift)->new({'input' => shift || {}});

    $self->data->{'pricingRuleID'} = $self->input->{'pricingRuleID'};
    $self->data->{'templates'} = [ "editRule.html" ];

    return $self->result; 
}

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

sub updateRule {
    my $self = (shift)->new({'input' => shift || {}});
    
    my $json = JSON->new;
    my %decodedInput;
    
    my $decoded = try {
        for my $k (qw(
            vendorBrands
            groupRuleLinks
            actionLinks
            criteria
        )) {
            $decodedInput{$k} = $json->decode($self->input->{$k});
        }
        return 1;
    } catch {
        warn 'failed to decode updateRule input: '.$_;
        return 0;
    };
    
    unless ($decoded) {
        
        $self->data->{'redirect'} = (
            '?action=editRule&pricingRuleID='.$self->input->{'pricingRuleID'}
            .'&errorMsg=Failed to Update Rule: failed to decode input'
        );
        
        return $self->result;
    }
    
    # Many aspects of a Pricing Rule can change here
    #  Users could be applying rules to products right now
    #  By writing in a transaction, rules will never be applied in an intermediate
    #  state that could result in run-time errors
    $self->dbh->begin_work;
    
    my $pricingRule = ECS::Product::PricingRule->new({
        'dbh' => $self->dbh,
        'pricingRuleID' => $self->input->{'pricingRuleID'},
    });
    
    # The row needs to be loaded so that original row data can be compared with
    $pricingRule->ruleName;
    
    ECS::Product::PricingRule::Admin::updateRule({
        'dbh' => $self->dbh,
        'pricingRule' => $pricingRule,
        'input' => $self->input,
    });
    
    ECS::Product::PricingRule::Admin::updateRuleVendorBrands({
        'dbh' => $self->dbh,
        'pricingRule' => $pricingRule,
        'vendorBrands' => $decodedInput{'vendorBrands'},
        'employeeID' => $self->input->{'employeeID'},
    });
    
    ECS::Product::PricingRule::Admin::updateRuleGroupLinks({
        'dbh' => $self->dbh,
        'pricingRule' => $pricingRule,
        'groupRuleLinks' => $decodedInput{'groupRuleLinks'},
        'employeeID' => $self->input->{'employeeID'},
    });
    
    ECS::Product::PricingRule::Admin::updateRuleActionLinks({
        'dbh' => $self->dbh,
        'pricingRule' => $pricingRule,
        'actionLinks' => $decodedInput{'actionLinks'},
        'employeeID' => $self->input->{'employeeID'},
    });
    
    ECS::Product::PricingRule::Admin::updateRuleCriteria({
        'dbh' => $self->dbh,
        'pricingRule' => $pricingRule,
        'criteria' => $decodedInput{'criteria'},
        'employeeID' => $self->input->{'employeeID'},
    });
    
    ECS::Product::PricingRule::Admin::updateRuleTS({
        'dbh' => $self->dbh,
        'pricingRuleID' => $pricingRule->pricingRuleID,
    });
    
    $self->dbh->commit;

    $self->data->{'redirect'} = (
        '?action=editRule&pricingRuleID='.$self->input->{'pricingRuleID'}
        .'&successMsg=Rule Saved.'
    );

    return $self->result;
}

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

sub deleteRule {
    my $self = (shift)->new({'input' => shift || {}});
    my $p = $self->input;
    $p->{'dbh'} = $self->dbh;

    unless (ECS::Product::PricingRule::Admin::deleteRule($p)) {
        $self->data->{'redirect'} = (
            '?action=editRule&pricingRuleID='.$self->input->{'pricingRuleID'}
            .'&errorMsg=Failed+to+Delete+Rule.'
        );
        return $self->result;
    }

    $self->data->{'redirect'} = './?successMsg=Rule+Deleted';
    return $self->result;
}

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

sub newAction {
    my $self = (shift)->new({'input' => shift || {}});
    
    $self->data->{'pricingRuleActionID'} = 0;
    $self->data->{'templates'} = [ "editAction.html" ];
    
    return $self->result; 
}

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

sub createAction {
    my $self = (shift)->new({'input' => shift || {}});
    my $p = $self->input;
    $p->{'dbh'} = $self->dbh;
    
    my $result = ECS::Product::PricingRule::Admin::createRuleAction($p);
    
    unless ($result->{'success'}) {
        
        $self->data->{'redirect'} = (
            './?action=newAction'
            .'&errorMsg=Failed to Add New Action: '.$result->{'result_message'}
        );
        
        return $self->result;
    }
    
    $self->data->{'redirect'} = (
        './?action=editAction&pricingRuleActionID='
            .$result->{'pricingRuleActionID'}
        .'&successMsg=Successfully added new Action.'
    );
    return $self->result;
}

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

sub createActionAndLink {
    my $self = (shift)->new({'input' => shift || {}});
    my $p = $self->input;
    $p->{'dbh'} = $self->dbh;

    my $result = ECS::Product::PricingRule::Admin::createRuleAction($p);

    unless ($result->{'success'}) {
        $self->data->{'dataType'} = "json";
        $self->data->{'data'}->{'error'} = (
            'Failed to Add New Action: '
            .$result->{'result_message'}
        );
        return $self->result;
    }
    
    $self->data->{'dataType'} = "json";
    $self->data->{'data'}->{'success'} = 1;
    
    my $ruleAction = ECS::Product::PricingRule::Action->new({
        'dbh' => $self->dbh,
        'pricingRuleActionID' => $result->{'pricingRuleActionID'},
    });
    
    for my $k (qw(
        pricingRuleActionID
        actionField
        fieldLabel
        actionValue
        valueLabel
        actionMethod
    )) {
        $self->data->{'data'}->{$k} = $ruleAction->$k;
    }
    
    return $self->result;
}

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

sub editAction {
    my $self = (shift)->new({'input' => shift || {}});

    $self->data->{'pricingRuleActionID'} = $self->input->{'pricingRuleActionID'};
    $self->data->{'templates'} = [ "editAction.html" ];

    return $self->result; 
}

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

sub updateAction {
    my $self = (shift)->new({'input' => shift || {}});
    my $p = $self->input;
    $p->{'dbh'} = $self->dbh;
    
    my $result = ECS::Product::PricingRule::Admin::updateRuleAction($p);
    
    unless ($result->{'success'}) {
        
        $self->data->{'redirect'} = (
            '?action=editAction&pricingRuleActionID='
                .$self->input->{'pricingRuleActionID'}
            .'&errorMsg=Failed+to+Update+Action.'
        );
        
        warn $result->{'result_message'};
        return $self->result; 
    }

    $self->data->{'redirect'} = (
        '?action=editAction&pricingRuleActionID='
            .$self->input->{'pricingRuleActionID'}
        .'&successMsg=Successfully updated action.'
    );
    return $self->result;
}

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

sub deleteAction {
    my $self = (shift)->new({'input' => shift || {}});
    my $p = $self->input;
    $p->{'dbh'} = $self->dbh;
    
    unless (ECS::Product::PricingRule::Admin::deleteRuleAction($p)) {
        $self->data->{'redirect'} = (
            '?action=editAction&pricingRuleActionID='
                .$self->input->{'pricingRuleActionID'}
            .'&errorMsg=Failed+to+Delete+Action.'
        );
        return $self->result;
    }
    
    $self->data->{'redirect'} = './?successMsg=Action+Deleted';
    return $self->result;
}

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

sub newCriteria {
    my $self = (shift)->new({'input' => shift || {}});

    $self->data->{'pricingRuleCriteriaID'} = 0;
    $self->data->{'templates'} = [ "editCriteria.html" ];

    return $self->result; 
}

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

sub createCriteria {
    my $self = (shift)->new({'input' => shift || {}});
    my $p = $self->input;
    $p->{'dbh'} = $self->dbh;
    
    my $result = ECS::Product::PricingRule::Admin::createRuleCriteria($p);
    
    unless ($result->{'success'}) {
        
        $self->data->{'redirect'} = (
            './?action=newCriteria'
            .'&errorMsg=Failed to Add New Criteria.'
        );
        
        return $self->result;
    }
    
    $self->data->{'redirect'} = (
        './?action=editCriteria&pricingRuleCriteriaID='
            .$result->{'pricingRuleCriteriaID'}
        .'&successMsg=Successfully added new Criteria.'
    );
    return $self->result;
}

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

sub createCriteriaAndLink {
    my $self = (shift)->new({'input' => shift || {}});
    my $p = $self->input;
    $p->{'dbh'} =  $self->dbh;
    
    my $result = ECS::Product::PricingRule::Admin::createRuleCriteria($p);
    
    unless ($result->{'success'}) {
        $self->data->{'dataType'} = "json";
        $self->data->{'data'}->{'error'} = (
            'Failed to Add New Criteria: '
            .$result->{'result_message'}
        );
        return $self->result;
    }
    
    $self->data->{'dataType'} = "json";
    $self->data->{'data'}->{'success'} = 1;
    
    my $ruleCriteria = ECS::Product::PricingRule::Criteria->new({
        'dbh' => $self->dbh,
        'pricingRuleCriteriaID' => $result->{'pricingRuleCriteriaID'},
    });
    
    my %newCriteria;
    
    for my $k (qw(
        pricingRuleCriteriaID
        criteriaField
        fieldLabel
        operator
        criteriaValue
        valueLabel
        criteriaMethod
    )) {
        $newCriteria{$k} = $ruleCriteria->$k;
    }
    
    $self->data->{'data'}->{'newCriteria'} = \%newCriteria;
    
    return $self->result;
}

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

sub editCriteria {
    my $self = (shift)->new({'input' => shift || {}});

    $self->data->{'pricingRuleCriteriaID'} = $self->input->{'pricingRuleCriteriaID'};
    $self->data->{'templates'} = [ "editCriteria.html" ];

    return $self->result; 
}

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

sub updateCriteria {
    my $self = (shift)->new({'input' => shift || {}});
    my $p = $self->input;
    $p->{'dbh'} = $self->dbh;

    my $result = ECS::Product::PricingRule::Admin::updateCriteria($p);

    unless ($result->{'success'}) {
        
        $self->data->{'redirect'} = (
            '?action=editCriteria&pricingRuleCriteriaID='
                .$self->input->{'pricingRuleCriteriaID'}
            .'&errorMsg=Failed+to+Update+Criteria.'
        );
        
        return $self->result; 
    }

    $self->data->{'redirect'} = (
        '?action=editCriteria&pricingRuleCriteriaID='
            .$self->input->{'pricingRuleCriteriaID'}
        .'&successMsg=Successfully updated criterion.'
    );
    return $self->result;
}

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

sub deleteCriteria {
    my $self = (shift)->new({'input' => shift || {}});
    my $p = $self->input;
    $p->{'dbh'} = $self->dbh;
    
    my $result = ECS::Product::PricingRule::Admin::deleteRuleCriteria($p);
    
    unless ($result->{'success'}) {
        $self->data->{'redirect'} = (
            '?action=editCriteria&pricingRuleCriteriaID='
                .$self->input->{'pricingRuleCriteriaID'}
            .'&errorMsg=Failed+to+Delete+Criteria: '
                .$result->{'result_message'}
        );
        return $self->result;
    }
    
    $self->data->{'redirect'} = './?successMsg=Criteria+Deleted';
    return $self->result;
}

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

sub newField {
    my $self = (shift)->new({'input' => shift || {}});
    
    $self->data->{'pricingRuleFieldID'} = 0;
    $self->data->{'templates'} = [ "editField.html" ];
    
    return $self->result; 
}

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

sub createField {
    my $self = (shift)->new({'input' => shift || {}});
    my $p = $self->input;
    $p->{'dbh'} = $self->dbh;
    
    my $result = ECS::Product::PricingRule::Admin::createRuleField($p);
    
    unless ($result->{'success'}) {
        
        $self->data->{'redirect'} = (
            './?action=newField'
            .'&errorMsg=Failed+to+Add+field.'
        );
        
        warn $result->{'result_message'};
        return $self->result; 
    }
    
    $self->data->{'redirect'} = (
        './?action=editField&pricingRuleFieldID='
            .$result->{'pricingRuleFieldID'}
        .'&successMsg=Successfully+added+new+field.'
    );
    
    return $self->result; 
}

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

sub editField {
    my $self = (shift)->new({'input' => shift || {}});
    
    $self->data->{'pricingRuleFieldID'} = $self->input->{'pricingRuleFieldID'};
    $self->data->{'templates'} = [ "editField.html" ];

    return $self->result; 
}

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

sub updateField {
    my $self = (shift)->new({'input' => shift || {}});
    my $p = $self->input;
    $p->{'dbh'} = $self->dbh;
    
    my $result = ECS::Product::PricingRule::Admin::updateRuleField($p);
    
    unless ($result->{'success'}) {
        
        $self->data->{'redirect'} = (
            '?action=editField&pricingRuleFieldID='
                .$self->input->{'pricingRuleFieldID'}
            .'&errorMsg=Failed+to+Update+Field.'
        );
        
        warn $result->{'result_message'};
        return $self->result; 
    }

    $self->data->{'redirect'} = (
        '?action=editField&pricingRuleFieldID='
            .$self->input->{'pricingRuleFieldID'}
        .'&successMsg=Successfully+updated+Field.'
    );
    return $self->result;
}

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

sub deleteField {
    my $self = (shift)->new({'input' => shift || {}});
    my $p = $self->input;
    $p->{'dbh'} = $self->dbh;
    
    unless (ECS::Product::PricingRule::Admin::deleteRuleField($p)) {
        $self->data->{'redirect'} = (
            '?action=editField&pricingRuleFieldID='
                .$self->input->{'pricingRuleFieldID'}
            .'&errorMsg=Failed+to+Delete+Field.'
        );
        return $self->result;
    }
    
    $self->data->{'redirect'} = './?successMsg=Field+Deleted';
    return $self->result;
}

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

sub runRules {
    my $self = (shift)->new({'input' => shift || {}});
    
    my %search;

    if (defined $self->input->{'vendorID'}) {
        $search{'productVendor.vendorID'} = $self->input->{'vendorID'};
    }
    
    if (defined $self->input->{'brandID'}) {
        $search{'me.brandID'} = $self->input->{'brandID'};
    }
    
    die('Vendor or Brand is required to Run Rules')
        unless %search;
    
    if ($self->input->{'kit'} eq 'YES') {
        $search{'me.type'} = 'Kit';
    } else {
        $search{'me.type'} = { '!=', 'Kit' };
    }
    
    # The following criteria were found in
    #    ECS::Product::PricingRule::Run::runMultipleProducts
    $search{'me.stockstatus'} = [ '-or' => {'=', 'YES'}, {'=', undef} ];
    $search{'me.nla'} = [ '-or' => {'=', 'NO'}, {'=', undef} ];
    $search{'me.isService'} = [ '-or' => {'=', 'NO'}, {'=', undef} ];
    
    my @productsNonCompliant;
    
    my %results = (
        'success' => 1,
        'numProductsNonCompliant' => 0,
        'rulesApplied' => {},
        'rulesNonCompliant' => {},
        'productsNonCompliant' => \@productsNonCompliant,
    );
    
    my $productHandler = ECS::Product::ProductHandler->new({
        'dbh' => $self->dbh,
        'search' => [
            \%search,
            {
                'rows' => $self->input->{'limit'},
                'page' => $self->input->{'pageNum'},
            }
        ],
    });
    
    unless (defined $self->input->{'numTotalProducts'}) {
        $results{'numTotalProducts'} = $productHandler->getTotalProductRowsCount();
    }
    
    $results{'numProductsChecked'} = scalar(@{ $productHandler->productRows });
    
    unless ($results{'numProductsChecked'}) {
        $self->data->{'dataType'} = "json";
        $self->data->{'data'} = \%results;
        return $self->result;
    }
    
    my $ruleRunner = ECS::Product::PricingRule::RuleRunner->new({
        'dbh' => $self->dbh,
        'productHandler' => $productHandler,
    });
    
    $ruleRunner->applyRules();
    
    for my $productRow (@{ $productHandler->productRows }) {
        my $productRowsMap = $productHandler->getRowsMap($productRow);
        my $productID = $productRow->productID;
        
        for my $productVendorRow (@{ $productRowsMap->{'productVendor'} }) {
            my $vendorID = $productVendorRow->vendorID;
            my $ruleAffectedFields =
                $ruleRunner->affectedFields->{$productID}->{$vendorID};
            
            next unless $ruleAffectedFields;
            
            my %ruleAffected;
            for my $field (keys %{ $ruleAffectedFields }) {
                $ruleAffected{$ruleAffectedFields->{$field}} = 1;
            }
            
            for my $ruleID (keys %ruleAffected) {
                $results{'rulesApplied'}->{$ruleID} ++;
            }
            
            my $fieldChanges =
                $ruleRunner->fieldChanges->{$productID}->{$vendorID};
            
            my %ruleChanges;
            my @productChanges;
            for my $field (keys %{ $fieldChanges }) {
                
                $ruleChanges{$ruleAffectedFields->{$field}} = 1;
                
                my $valueOld = $fieldChanges->{$field}->{'old'};
                my $valueNew = $fieldChanges->{$field}->{'new'};
                
                # Need to store changes as strings to avoid massive memory consumption
                push(@productChanges, sprintf('%s from %s to %s', (
                    $ruleRunner->ruleFieldsByName->{$field}->label,
                    (defined $valueOld ? $valueOld : '<i>NULL</i>'),
                    (defined $valueNew ? $valueNew : '<i>NULL</i>'),
                )));
            }
            
            next unless %ruleChanges;
            
            $results{'numProductsNonCompliant'} ++;
            
            for my $ruleID (keys %ruleChanges) {
                $results{'rulesNonCompliant'}->{$ruleID} ++;
            }
            
            push(@productsNonCompliant, {
                'productID' => $productID,
                'vendorID' => $vendorID,
                'partnumber' => $productRow->partnumber,
                'description' => $productRow->description,
                'changes' => \@productChanges,
            });
        }
    }
    
    $self->data->{'dataType'} = "json";
    $self->data->{'data'} = \%results;
    
    return $self->result;
}

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

sub fixNonCompliantProducts {
    my $self = (shift)->new({'input' => shift || {}});
    
    my $productIDs;
    
    try {
        $productIDs = JSON::decode_json($self->input->{'productIDs'});
    } catch {
        warn 'failed to decode fixNonCompliantProducts productIDs: '.$_;
    };
    
    die('missing productIDs to fix')
        unless $productIDs;
    
    my %search;

    if (defined $self->input->{'vendorID'}) {
        $search{'productVendor.vendorID'} = $self->input->{'vendorID'};
    }
    
    if (defined $self->input->{'brandID'}) {
        $search{'me.brandID'} = $self->input->{'brandID'};
    }
    
    die('Vendor or Brand is required to Run Rules')
        unless %search;
    
    # Limit to the specified productIDs
    $search{'me.productID'} = $productIDs;
    
    my $productHandler = ECS::Product::ProductHandler->new({
        'dbh' => $self->dbh,
        'search' => [
            \%search,
        ],
    });
    
    my $ruleRunner = ECS::Product::PricingRule::RuleRunner->new({
        'dbh' => $self->dbh,
        'productHandler' => $productHandler,
    });
    
    $ruleRunner->applyRules();
    
    my @productRowsMaps;
    my %productRuleResults;
    
    for my $productRow (@{ $productHandler->productRows }) {
        my $productRowsMap = $productHandler->getRowsMap($productRow);
        
        push(@productRowsMaps, $productRowsMap);
        
        for my $productVendorRow (@{ $productRowsMap->{'productVendor'} }) {
            $productRuleResults{$productVendorRow} = $ruleRunner->getProductResults(
                $productVendorRow->productID,
                $productVendorRow->vendorID,
            );
        }
    }
    
    $productHandler->saveRowsMaps({
        'rowsMaps' => \@productRowsMaps,
        'ruleResults' => \%productRuleResults,
        'employeeID' => $self->input->{'employeeID'},
    });
    
    $self->data->{'dataType'} = "json";
    $self->data->{'data'}->{'success'} = 1;
    
    return $self->result;
}

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