#!/usr/bin/perl -Ilibs

use strict;
use warnings;

package API::PAPR;

use Tie::Hash::Indexed ();
use Try::Tiny;

our $Config;
BEGIN{
	use API::Config;
	$Config = $API::Config::Config;
}
use lib $Config->{Website_libs};

use ECS::Category ();
use ECS::Report;
use ECS::SQL;
use ECS::Time;
use ECS::Vehicle::Product;
use ECS::Vehicle;

my $report_row_limit = 2000;
my $insert_compliance_limit = 1000;

tie my %report_config, 'Tie::Hash::Indexed';

$report_config{'vehicleLabel'} = { 'label' => 'Vehicle' };
$report_config{'groupName'}    = { 'label' => 'PAPR Group Name' };
$report_config{'groupClass'}   = { 'label' => 'PAPR Class' };
$report_config{'numBrands'}    = { 'label' => '# Brands' };
$report_config{'categoryPath'} = { 'label' => 'Category' };
$report_config{'websiteLink'}  = { 'label' => 'Website Location' };

$ECS::Report::fields->{'paprCompliance'} = \%report_config;

return 1;

###############################################################
# The following subroutines have been converted from the      #
# PAPR PHP files in Portal SVN. These files (now subroutines) #
# were the ones that made writes to the database. The PHP     #
# files in in Portal not mentioned here only make only reads  #
# (or write to temp tables).                                  #
###############################################################

sub inapp_save{
	# When all OEM and AFR fields for a vehicle are "not applicated"
	# and "inapplicable" has been checked and saved. Icons will appear
	# as Yellow Checkmarks
	my $self = shift;
	my $input = shift;
	my $result = {
		'success'	=> 1,
		'error'		=> ''
	};
	
	my $DBH = ECS::SQL::connect();
	
	my $s_query = "INSERT INTO paprNotApplicable (paprGroupID, vehicleConfigID, note) 
				VALUES (".$input->{groupID}.", ".$input->{vehicleConfigID}.", '".$input->{note}."') 
				ON DUPLICATE KEY UPDATE note = '".$input->{note}."'";	
	
	if(!ECS::SQL::query({DBH => $DBH, sql => $s_query})){
		$result->{success} = 0;
		$result->{error} = "Unable to insert record into paprNotApplicable table. $!\n";
	}
	
	return $result;	
}
##############################################################

sub inapp_delete{
	# When all OEM and AFR fields for a vehicle are "inapplicable"
	# and "inapplicable" has been checked and saved. Icons will appear
	# as Red X
	my $self = shift;
	my $input = shift;
	my $result = {
		'success'	=> 1,
		'error'		=> ''
	};
	
	my $DBH = ECS::SQL::connect();
	
	if(!ECS::SQL::delete({
		DBH => $DBH, 
		table => 'paprNotApplicable',
		columns => [qw(paprGroupID vehicleConfigID)],
		hash => {
			paprGroupID => $input->{groupID},
			vehicleConfigID => $input->{vehicleConfigID}
		}
	})){
		$result->{success} = 0;
		$result->{error} = "Unable to delete from paprNotApplicable table. $!\n";
	}
	
	return $result;	
}
##############################################################

sub class_remove{
	# Remove the class heading and all groups associated with it
	my $self = shift;
	my $input = shift;
	my $result = {
		'success'	=> 1,
		'error'		=> ''
	};
	
	my $DBH = ECS::SQL::connect();
	
	my $s_query = "DELETE paprGroup, paprCriteria, paprNotApplicable 
					FROM paprGroup 
						LEFT JOIN paprCriteria ON (paprGroup.paprGroupID = paprCriteria.paprGroupID) 
						LEFT JOIN paprNotApplicable ON (paprGroup.paprGroupID = paprNotApplicable.paprGroupID)
					WHERE class = '".$input->{alpha}."'";
	
	if(!ECS::SQL::query({DBH=>$DBH, sql=>$s_query})){
		$result->{success} = 0;
		$result->{error} = "Unable to remove class from dabase. $!\n";
	}
	
	return $result;
}
##############################################################

sub class_rename{
	# class heading has been double clicked and new name has been selected
	my $self = shift;
	my $input = shift;
	my $result = {
		'success'	=> 1,
		'error'		=> ''
	};
	
	my $DBH = ECS::SQL::connect();

	my $s_query = "UPDATE paprGroup SET class = '".$input->{newAlpha}."' WHERE class = '".$input->{oldAlpha}."'";
	if(!ECS::SQL::query({DBH=>$DBH, sql=>$s_query})){
		$result->{success} = 0;
		$result->{error} = "Unable to rename class. Failed to update paprGroup. $!"
	}
	
	return $result;
}
##############################################################

sub group_add{
	# The [+] icon for add-row has been clicked and a name has been entered
	my $self = shift;
	my $input = shift;
	my $result = {
		'success'	=> 1,
		'error'		=> ''
	};
	
	my $DBH = ECS::SQL::connect();
	
	$result->{insertID} = ECS::SQL::insert({
		DBH => $DBH,
		table =>'paprGroup',
		columns => [qw(name class)],
		hash => {
			name =>	$input->{name},
			class => $input->{class}
		}
	});
	if(!$result->{insertID}){
		$result->{success} = 0;
		$result->{error} = "Unable to add group. Failed to insert into paprGroup table. $!\n";
	}
	
	return $result;
}
##############################################################

sub group_criteria_save{
	# After folder icon has been clicked and criteria has changed
	my $self = shift;
	my $input = shift;
	my $result = {
		'success'	=> 1,
		'error'		=> ''
	};
	
	my $DBH = ECS::SQL::connect();

	my @category_sections = ('category', 'subcategory', 'grouping', 'subgrouping', 'heading', 'subheading');

	my $s_query = "UPDATE paprGroup 
				SET keywords = '".$input->{keywords}."', 
					notkeywords = '".$input->{notkeywords}."', 
					phrase = '".$input->{phrase}."' 
				WHERE paprGroupID = ".$input->{groupID};
	
	if(!ECS::SQL::query({DBH=>$DBH, sql=>$s_query})){
		
		$result->{success} = 0;
		$result->{error} = "Unable to update group criteria. Failed to update paprGroup table. $!";
		return $result;
	}
	
	$s_query = "DELETE FROM paprCriteria WHERE paprGroupID = ".$input->{groupID};
	ECS::SQL::query({DBH=>$DBH, sql=>$s_query});

	if (defined($input->{categories})) {
		$s_query = "SELECT ".$input->{groupID}." AS paprGroupID, categoryConfigID FROM categoryConfig WHERE ";

		foreach my $cat_app (@{$input->{"categories"}}) {
			$s_query .= "(";
			foreach my $category_section (@category_sections) {
				no warnings 'numeric';
				if (int($cat_app->{$category_section}) > 0){
					$s_query .= $category_section."ID = ".int($cat_app->{$category_section});
				}else{
					$s_query .= $category_section."ID = 0";
				}
				$s_query .= " AND ";
			}
			$s_query = substr($s_query, 0, -4);
			$s_query .= ") OR ";
		}
		$s_query = substr($s_query, 0, -3);

		if(!ECS::SQL::query({DBH=>$DBH, sql=>$s_query})){
			$result->{success} = 0;
			$result->{error} = "Unable to retrieve new groupings from paprGroup table. $!";
		}
		$s_query = "INSERT INTO paprCriteria (paprGroupID, categoryConfigID) ".$s_query;
		if(!ECS::SQL::query({DBH=>$DBH, sql=>$s_query})){
			$result->{success} = 0;
			$result->{error} = "Unable to save group criteria. Failed to insert into paprGroup table. $!";
		}
	}
	
	return $result;
}
##############################################################

sub group_remove{
	# X next to a group name has been clicked
	my $self = shift;
	my $input = shift;
	my $result = {
		'success'	=> 1,
		'error'		=> ''
	};
	
	my $DBH = ECS::SQL::connect();
	
	my $s_query = "DELETE paprGroup, paprCriteria, paprNotApplicable
				FROM paprGroup
				LEFT JOIN paprCriteria ON (paprGroup.paprGroupID = paprCriteria.paprGroupID)
				LEFT JOIN paprNotApplicable ON (paprGroup.paprGroupID = paprNotApplicable.paprGroupID)
				WHERE paprGroup.paprGroupID = ".$input->{groupID};
	
	ECS::SQL::query({DBH=>$DBH, sql=>$s_query});
	
	return $result;
}
##############################################################

sub group_rename{
	# group has been double clicked and new name has been entered
	my $self = shift;
	my $input = shift;
	my $result = {
		'success'	=> 1,
		'error'		=> ''
	};
	
	my $DBH = ECS::SQL::connect();
	
	my $s_query = "UPDATE paprGroup SET name = '".$input->{name}."' WHERE paprGroupID = ".$input->{groupID};
	ECS::SQL::query({DBH=>$DBH, sql=>$s_query});
	
	return $result;
}
##############################################################

sub notavailable_save{
	# A Grey Checkmark or Red X has been clicked and "Not Yet Available" selected
	my $self = shift;
	my $input = shift;
	my $result = {
		'success'	=> 1, 
		'error'		=> ''
	};
	
	my $DBH = ECS::SQL::connect();
	
	my $s_query;
	if ($input->{notavail} eq 'true'){
		$s_query = "INSERT INTO paprNotAvailable (paprGroupID, vehicleConfigID, brand, note) 
				VALUES (".$input->{groupID}.", ".$input->{vehicleConfigID}.", '".$input->{brand}."', '".$input->{note}."') 
				ON DUPLICATE KEY UPDATE note = '".$input->{note}."'";
	}else{
		$s_query = "DELETE FROM paprNotAvailable 
					WHERE paprGroupID = ".$input->{groupID}." 
					AND vehicleConfigID = ".$input->{vehicleConfigID}." 
					AND brand = '".$input->{brand}."'";
	}
	
	ECS::SQL::query({DBH=>$DBH, sql=>$s_query});
	
	return $result;
}
##############################################################

sub override_save{
	# An orange triangle has been clicked and "Alert Override" selected
	my $self = shift;
	my $input = shift;
	my $result = {
		'success'	=> 1, 
		'error'		=> ''
	};
	
	my $DBH = ECS::SQL::connect();
	
	my $s_query;
	if ($input->{override} eq 'true'){
		$s_query = "INSERT INTO paprAlertOverride (paprGroupID, vehicleConfigID, brand, note) 
				VALUES (".$input->{groupID}.", ".$input->{vehicleConfigID}.", '".$input->{brand}."', '".$input->{note}."') 
				ON DUPLICATE KEY UPDATE note = '".$input->{note}."'";
	}else{
		$s_query = "DELETE FROM paprAlertOverride 
					WHERE paprGroupID = ".$input->{groupID}." 
					AND vehicleConfigID = ".$input->{vehicleConfigID}." 
					AND brand = '".$input->{brand}."'";
	}
	
	ECS::SQL::query({DBH=>$DBH, sql=>$s_query});
	
	return $result;
}

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

sub vehicle_group_compliance_save {
	my $self = shift;
	my $input = shift;
	
	my $vehicleConfigIDs = $input->{'currentVehicles'};
	my $groupID = $input->{'groupID'};
	my $num_brands = $input->{'numBrands'};
	
	# compliantStatus must be either "Y" or "N"
	my $compliant_status = $input->{'compliantStatus'} if (
		($input->{'compliantStatus'} || '') eq 'Y'
		or ($input->{'compliantStatus'} || '') eq 'N'
	);
	
	my $result = {
		'success' => 1,
		'error'   => ''
	};
	
	unless (
		$vehicleConfigIDs
		and $groupID
		and $num_brands
		and $compliant_status
	) {
		$result->{'success'} = 0;
		$result->{'error'} = 'Missing a required input';
		return $result;
	}
	
	my $p = {
		'DBH' => ECS::SQL::connect(),
	};
	
	my $get_current_compliants = ECS::SQL::toDBI($p)->prepare(
		'SELECT
			vehicleConfigID,
			paprGroupID,
			numBrands
		FROM
			paprCompliance
		WHERE
			vehicleConfigID IN ('.join(',', map {'?'} @{ $vehicleConfigIDs }).')
			AND paprGroupID = ?'
	);
	
	unless (
		try {
			$get_current_compliants->execute(
				@{ $vehicleConfigIDs },
				$groupID,
			);
			1
		}
		catch { warn $_; 0 }
	) {
		$result->{'success'} = 0;
		$result->{'error'} = 'Failed to fetch currently compliant data';
		return $result;
	}
	
	my %current_compliants;
	
	while (my $comp = $get_current_compliants->fetchrow_hashref) {
		$current_compliants{$comp->{'vehicleConfigID'}}->{$comp->{'paprGroupID'}} = $comp->{'numBrands'};
	}
	
	my $remove_compliance = ECS::SQL::toDBI($p)->prepare(
		'DELETE FROM
			paprCompliance
		WHERE
			vehicleConfigID = ?
			AND paprGroupID = ?'
	);
	
	my $update_compliance = ECS::SQL::toDBI($p)->prepare(
		'UPDATE
			paprCompliance
		SET
			numBrands = ?
		WHERE
			vehicleConfigID = ?
			AND paprGroupID = ?'
	);
	
	my $insert_compliance = ECS::SQL::toDBI($p)->prepare(
		'INSERT INTO paprCompliance (
			vehicleConfigID,
			paprGroupID,
			numBrands
		) VALUES (?,?,?)'
	);
	
	for my $vehicleConfigID (@{ $vehicleConfigIDs }) {
		
		# If removing compliance
		if ($compliant_status eq 'N') {
			
			unless (
				try {
					$remove_compliance->execute(
						$vehicleConfigID,
						$groupID
					); 1
				}
				catch { warn $_; 0 }
			) {
				$result->{'success'} = 0;
				$result->{'error'} = 'Failed to remove compliancy';
				return $result;
			}
			
			next;
		}
		
		# Set compliance
		
		# If is currently set as compliant
		if (
			my $current_compliant_num_brands =
				$current_compliants{$vehicleConfigID}->{$groupID}
		) {
			
			# skip this one, because the current compliance has less brands
			next if $current_compliant_num_brands <= $num_brands;
			
			unless (
				try {
					$update_compliance->execute(
						$num_brands,
						$vehicleConfigID,
						$groupID
					); 1
				}
				catch { warn $_; 0 }
			) {
				$result->{'success'} = 0;
				$result->{'error'} = 'Failed to update compliancy';
				return $result;
			}
			
			next;
		}
		
		# No current compliancy
		
		unless (
			try {
				$insert_compliance->execute(
					$vehicleConfigID,
					$groupID,
					$num_brands,
				); 1
			}
			catch { warn $_; 0 }
		) {
			$result->{'success'} = 0;
			$result->{'error'} = 'Failed to create compliancy';
			return $result;
		}
		
	}
	
	return $result;
}

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

sub report_loader {
	my $self = shift;
	my $input = shift;
	
	my $result = {
		'success'	=> 1, 
		'error'		=> ''
	};
	
	my $DBH = ECS::SQL::connect();
	
	my $p = {
		'DBH'         => $DBH,
		'nolimit'     => 1,
		'paprGroupID' => $input->{'groupID'},
		'class'       => $input->{'classID'},
		'numBrands'   => $input->{'numBrands'},
	};
	
	# -- Report Statuses ----------------------------------------------------------------------------------
	# green check (good): Meets all Keywords, Phrase, Categories
	# yellow check (inapplicable): Not Applicable (ex: manual transmission fluid for an automatic only car)
	# grey check (not yet available): Not applicated due to inavailability
	# alert (partial application): Meets partial Keywords, Phrase, Categories
	# red cross (needs applicated): Meets no Keywords, Phrase, Categories
	# -----------------------------------------------------------------------------------------------------
	
	my @groups = API::PAPR::get_groups($p);
	
	my %all_group_results = ();
	
	# loop thru each of the selected vehicles
	for my $vehicle_config_id (@{ $input->{'vehicles'} }) {
		
		local $p->{'vehicleConfigID'} = $vehicle_config_id;
		local $p->{'vehicleProducts'} = API::PAPR::get_vehicle_products($p);
		
		# loop thru each of the PAPR groups for the current vehicle
		for my $group (@groups) {
			
			if (
				not $group->{'categories'}
				and defined $input->{'groupID'}
			) {
				$result->{'success'} = 0;
				$result->{'error'} = "No categories for this group";
				return $result;
			}
			
			next unless $group->{'categories'};
			
			local $p->{'group'} = $group;
			
			my $group_result = API::PAPR::get_group_result($p);
			
			if ($group_result->{'inapp'}) {
				$group_result->{'compliance'} = 1;
			}
			
			push(@{ $all_group_results{$group->{'paprGroupID'}} }, $group_result);
			
		}
	}

	return \%all_group_results;
}

# ------------------------------------------------------------
# Compliance-processing cron to determine if any vehicle/group
# combinations are no longer compliant or can become compliant
# ------------------------------------------------------------
sub check_compliance {
	my $self = shift;
	my $input = shift;
	
	my $result = {
		'success' => 1, 
		'error'   => '',
	};
	
	my $p = {
		'DBH'     => (
			$input->{'DBH'}
			|| ECS::SQL::connect()
		),
		'nolimit' => 1,
	};
	
	my $vehicles = API::PAPR::get_all_vehicles($p);
	my @groups = API::PAPR::get_groups($p);
	my %compliances = API::PAPR::get_compliances($p);
	
	my @newly_compliants = ();
	my @incompliants = ();
	
	# Loop thru each of the vehicles
	for my $vehicle (@{ $vehicles }) {
		my $vehicle_config_id = $vehicle->{'vehicleConfigID'};
		
		local $p->{'vehicleConfigID'} = $vehicle_config_id;
		local $p->{'vehicleProducts'} = API::PAPR::get_vehicle_products($p);
		
		# loop thru each of the PAPR groups
		for my $group (@groups) {
			my $compliance = ($compliances{$vehicle_config_id}->{$group->{'paprGroupID'}} || {});
			
			next unless $group->{'categories'};
			
			local $p->{'group'} = $group;
			
			# Default compliance check to 3 brands
			local $p->{'numBrands'} = 3;
			
			$p->{'numBrands'} = $compliance->{'numBrands'} if $compliance->{'compliant'};
			
			my $group_result = API::PAPR::get_group_result($p);
			
			# If Inapplicable
			if ($group_result->{'inapp'}) {
				if ($compliance->{'compliant'}) {
					# override compliance here, because it is possible to set
					# compliance with Inapplicable through the PAPR UI
					$group_result->{'compliance'} = 1;
				} else {
					# For auto-set-compliancy, never comply with Inapplicable
					$group_result->{'compliance'} = 0;
					$compliance->{'paprComplianceID'} = undef;
				}
			}
			
			if (
				$group_result->{'compliance'}
				and not $compliance->{'compliant'}
			) {
				push(@newly_compliants, {
					'vehicle'    => $vehicle,
					'group'      => $group,
					'compliance' => {
						'numBrands' => $p->{'numBrands'},
					}
				});
			} elsif (
				not $group_result->{'compliance'}
				and $compliance->{'paprComplianceID'}
			) {
				push(@incompliants, {
					'vehicle'    => $vehicle,
					'group'      => $group,
					'compliance' => $compliance,
				});
			}
			
		}
		
	}
	
	if (@newly_compliants) {
		
		local $p->{'compliants'} = \@newly_compliants;
		API::PAPR::insert_compliants($p);
		
		# Do not send "Now Compliant" Email
		# local $p->{'emailType'} = 'Now Compliant';
		# API::PAPR::send_compliance_emails($p);
		
	}
	
	if (@incompliants) {
		
		local $p->{'compliants'} = \@incompliants;
		API::PAPR::set_compliance_notified($p);
		
		local $p->{'emailType'} = 'No Longer Compliant';
		API::PAPR::send_compliance_emails($p);
		
	}
	
	$result->{'data'} = {
		'numNewlyCompliant' => scalar @newly_compliants,
		'numIncompliant' => scalar @incompliants,
	};
	
	return $result;
}

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

sub get_groups {
	my $p = shift;
	my $DBH = $p->{'DBH'};
	
	my $sql_where = '';
	
	if ($p->{'paprGroupID'}) {
		$sql_where = 'WHERE paprGroup.paprGroupID = '.$p->{'paprGroupID'};
	} elsif ($p->{'class'}) {
		$sql_where = "WHERE paprGroup.class = '".$p->{'class'}."'";
	}
	
	local $p->{'sql'} =
		"SELECT
			paprGroup.paprGroupID,
			paprGroup.name,
			paprGroup.class,
			paprGroup.keywords,
			paprGroup.notkeywords,
			paprGroup.phrase,
			GROUP_CONCAT(CONCAT_WS('_', 
				categoryID,
				subcategoryID,
				groupingID,
				subgroupingID,
				headingID,
				subheadingID
			)) AS categories
			FROM
				paprGroup
				LEFT JOIN paprCriteria ON (paprGroup.paprGroupID = paprCriteria.paprGroupID)
				LEFT JOIN categoryConfig ON (paprCriteria.categoryConfigID = categoryConfig.categoryConfigID)
		".$sql_where."
		GROUP BY
			paprGroup.paprGroupID
		ORDER BY
			paprGroup.class,
			paprGroup.name";
	
	my @groups = ();
	my %groups_by_id = ();
	
	for my $group ( @{ ECS::SQL::query($p) || [] } ) {
		
		$group->{'notavails'} = {};
		$group->{'overrides'} = {};
		$group->{'inapps'} = {};
		
		# build category configs array stripping trailing zeros
		$group->{'categoryConfigs'} = [
			map {
				join('_', grep { $_ > 0 } split(/_/, $_))
			} split(',', $group->{'categories'})
		];
		
		$group->{'matchKeywords'} = [split(', ', lc($group->{'keywords'} || ''))];
		$group->{'matchNotKeywords'} = [split(', ', lc($group->{'notkeywords'} || ''))];
		
		push(@groups, $group);
		$groups_by_id{$group->{'paprGroupID'}} = $group;
	}
	
	# Get notApplicable, notAvailable, and alertOverride info
	
	local $p->{'sql'} = 'SELECT * FROM paprNotApplicable WHERE paprGroupID IN ('.join(',', keys %groups_by_id).')';
	my $inapps = (ECS::SQL::query($p) || []);
	
	local $p->{'sql'} = 'SELECT * FROM paprNotAvailable WHERE paprGroupID IN ('.join(',', keys %groups_by_id).')';
	my $notavails = (ECS::SQL::query($p) || []);
	
	local $p->{'sql'} = 'SELECT * FROM paprAlertOverride WHERE paprGroupID IN ('.join(',', keys %groups_by_id).')';
	my $overrides = (ECS::SQL::query($p) || []);
	
	for my $inapp (@{ $inapps }) {
		$groups_by_id{$inapp->{'paprGroupID'}}->{'inapps'}
			->{$inapp->{'vehicleConfigID'}} = $inapp->{'note'};
	}
	
	for my $notavail (@{ $notavails }) {
		$groups_by_id{$notavail->{'paprGroupID'}}->{'notavails'}
			->{$notavail->{'vehicleConfigID'}}->{$notavail->{'brand'}} = $notavail->{'note'};
	}
	
	for my $override (@{ $overrides }) {
		$groups_by_id{$override->{'paprGroupID'}}->{'overrides'}
			->{$override->{'vehicleConfigID'}}->{$override->{'brand'}} = $override->{'note'};
	}
	
	return @groups;
}

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

sub get_all_vehicles {
	my $p = shift;
	
	local $p->{'sql'} =
		'SELECT
			vehicleConfig.vehicleConfigID,
			vehicleConfig.vehicleLabel,
			vehicleConfig.vehicleURL,
			vehicleConfig.makeID,
			make.makeName
		FROM
			make
			JOIN vehicleConfig USING (makeID)
		WHERE
			make.showNav = 1
			AND vehicleConfig.detailVehicle = 1';
	
	# if ($ECS::Config::c{'sandbox'}) {
		# warn 'sandbox limiting all_vehicles to only 1 Volkswagen';
		# $p->{'sql'} .= ' AND vehicleConfig.makeID = 3 LIMIT 1';
	# }
	
	return (ECS::SQL::query($p) || []);
}

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

# -------------------------------------------------------------
# Get all currently compliant info
# -------------------------------------------------------------
sub get_compliances {
	my $p = shift;
	my $DBH = shift;
	
	local $p->{'sql'} =
		'SELECT
			paprComplianceID,
			vehicleConfigID,
			paprGroupID,
			numBrands,
			IF(notifiedDate, 0, 1) AS compliant
		FROM
			paprCompliance';
	
	my %compliances = ();
	
	for my $compliance (@{ ECS::SQL::query($p) || [] }) {
		$compliances{$compliance->{'vehicleConfigID'}}
			->{$compliance->{'paprGroupID'}} = $compliance;
	}
	
	return %compliances;
}

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

sub get_group_result {
	my $p = shift;
	my $vehicle_config_id = $p->{'vehicleConfigID'};
	my $group = $p->{'group'};
	
	my %brands_result = (
		'OEM' => 1,
	);
	
	for (my $i=1; $i<$p->{'numBrands'}; $i++) {
		$brands_result{'AFR'.$i} = 1;
	}
	
	for my $brand (keys %brands_result) {
		
		$brands_result{$brand} = {
			'notavail' => ($group->{'notavails'}->{$vehicle_config_id}->{$brand} || ''),
			'override' => ($group->{'overrides'}->{$vehicle_config_id}->{$brand} || ''),
			'cats'     => {}
		};
		
		for my $category_config (@{ $group->{'categoryConfigs'} }) {
			$brands_result{$brand}->{'cats'}->{$category_config} = 0;
		}
	}
	
	local $p->{'brandsResult'} = \%brands_result;
	
	for my $category (@{ $group->{'categoryConfigs'} }) {
		local $p->{'category'} = $category;
		API::PAPR::process_vehicle_products_category($p);
	}
	
	my $num_brands_compliant = 0;
	my $compliant = 0;
	
	for my $brand (keys %brands_result) {
		my $brand_result = $brands_result{$brand};
		
		my $brand_compliant = 1;
		
		for my $cat (keys %{ $brand_result->{'cats'} }) {
			if(!(
				$brand_result->{'cats'}->{$cat} == 4
				|| $brand_result->{'notavail'}
				|| $brand_result->{'override'}
			)) {
				$brand_compliant = 0;							
			}
		}
		
		$num_brands_compliant += $brand_compliant;
	}
	
	if ($num_brands_compliant >= $p->{'numBrands'}) {
		# Group/vehicle combination is compliant
		$compliant = 1;
	}
	
	return {
		'brands'     => \%brands_result,
		'inapp'      => ($group->{'inapps'}->{$vehicle_config_id} || ''),
		'compliance' => $compliant,
	};
}

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

# ------------------------------------------------------------------------------
# get product 'score' based on "keywords", "not keywords", and "phrase" validity
# ------------------------------------------------------------------------------
sub get_product_score {
	my $p = shift;
	my $group = $p->{'group'};
	my $product = $p->{'product'};
	
	my $keywords = $group->{'matchKeywords'};
	my $not_keywords = $group->{'matchNotKeywords'};
	
	my $desc = ($product->{'description'} || '');
	my $lc_desc = lc($desc); # use lowercase description for "keywords" and "not keywords" checks
	
	my $missing_keyword = 0;
	for my $keyword (@{ $keywords }) {
		$keyword =~ s/^\s+//;
		$keyword =~ s/\s+$//;
		if (index($lc_desc, $keyword) == -1) {
			$missing_keyword = 1;
			last;
		}
	}
	
	my $keywords_valid = 0;
	$keywords_valid = 1 if not $missing_keyword;
	
	if ($keywords_valid) {
		for my $not_keyword (@{ $not_keywords }) {
			$not_keyword =~ s/^\s+//;
			$not_keyword =~ s/\s+$//;
			if (index($lc_desc, $not_keyword) != -1){
				$keywords_valid = 0;
				last;
			}
		}
	}
	
	my $phrase_valid = 0;
	
	if ($group->{'phrase'}) {
		
		my $match_phrase = (' - '.$group->{'phrase'});
		
		$phrase_valid = 1 if (
			index($desc, $match_phrase)
			== (length($desc) - length($match_phrase))
		);
		
	} else {
		# no phrase for this group: valid
		$phrase_valid = 1;
	}
	
	#4 = perfect
	#3 = keywords or not_keywords invalid
	#2 = phrase not valid
	#1 = keywords and phrase not valid
	
	my $score = 4;
	
	$score -= 1 unless $keywords_valid;
	$score -= 2 unless $phrase_valid;
	
	return $score;
}

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

sub get_genuine_brands {
	my $p = shift;
	
	local $p->{'sql'} = 'SELECT brandID FROM brand WHERE genuine = 1';
	
	my $genuine_brands_result = ECS::SQL::query($p);
	
	return {
		map {
			$_->{'brandID'} => 1,
		} @{ $genuine_brands_result }
	};
}

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

sub get_vehicle_products {
	# -------------------------------------------------------------
	# ECS::Vehicle to get the all vehicleConfigIDs that fall within 
	# applicability of our detail vehicles, and fetch product cache 
	#--------------------------------------------------------------
	my $p = shift;

	# Get the ECS::Vehicle object to pass to ECS::Vehicle::Product::getAllProducts
	local $p->{'vehicle'} = ECS::Vehicle->new({
        'dbh' => ECS::SQL::toDBI($p),
        'vehicleConfigID' => $p->{'vehicleConfigID'},
    });

	# -------------------------------------------------------
	# Get the vehicle_products memcached 
	# + requires input->vehicleConfigID and input->vehicleSQL
	# -------------------------------------------------------
	local $p->{'vehicleProducts'} = ECS::Vehicle::Product::getAllProducts($p);
	return {} unless %{ $p->{'vehicleProducts'} };
	
	$p->{'input'}->{'products'} = [values %{ ECS::Vehicle::Product::findProducts($p->{'vehicleProducts'}) }];
	return {} unless @{ $p->{'input'}->{'products'} };
	
	ECS::Vehicle::Product::getProductInfo($p);
	return {} unless %{ $p->{'input'}->{'productHash'} };
	
	local $p->{'productInfos'} = $p->{'input'}->{'productHash'};
	
	$p->{'genuineBrands'} ||= API::PAPR::get_genuine_brands($p);
	
	API::PAPR::build_vehicle_products($p);
	
	return $p->{'vehicleProducts'};
}

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

sub build_vehicle_products {
	my $p = shift;
	my $items = $p->{'vehicleProducts'};
	
	for my $ID (keys %{ $items }) {
        if(exists $items->{$ID}->{'dtModified'}) {
			my $product_info = $p->{'productInfos'}->{$ID};
			
			unless ($product_info->{'enabled'}) {
				delete($items->{$ID});
				next;
			}
			
			$items->{$ID}->{'OEM'} = ($p->{'genuineBrands'}->{$product_info->{'brandID'} || 0}) ? 1 : 0;
			
			# Copy product info into product
			for my $k (keys %{ $product_info }) {
				$items->{$ID}->{$k} = $product_info->{$k};
			}
			
        }
        else {
			local $p->{'vehicleProducts'} = $items->{$ID};
            API::PAPR::build_vehicle_products($p);
        }
    }
	
	return;
}

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

sub process_vehicle_products_category {
	my $p = shift;
	
	my $vehicle_products = $p->{'vehicleProducts'};
	my $brands_result = $p->{'brandsResult'};
	my $num_brands = $p->{'numBrands'};
	my $category = $p->{'category'};
	
	my $category_products = ECS::Vehicle::Product::findProducts(
		ECS::Vehicle::Product::findCategory($vehicle_products, split(/_/, $category))
	);
	
	for my $productID (keys %{ $category_products }) {
		my $product = $category_products->{$productID};
		my $brand;
		
		local $p->{'product'} = $product;
		my $score = API::PAPR::get_product_score($p);
		
		if (
			$product->{'OEM'}
			and defined($brands_result->{'OEM'}->{'cats'}->{$category})
			and ($score > $brands_result->{'OEM'}->{'cats'}->{$category})
		) {
			$brand = 'OEM';
		} elsif (
			defined($brands_result->{'AFR1'}->{'cats'}->{$category})
			and $score > $brands_result->{'AFR1'}->{'cats'}->{$category}
		) {
			$brand = 'AFR1';
		} else {
			for (my $i=1; $i<$num_brands; $i++) {
				unless (
					defined($brands_result->{'AFR'.$i}->{'cats'}->{$category})
					and $score < $brands_result->{'AFR'.$i}->{'cats'}->{$category}
				) {
					last if (($i+1) > ($num_brands-1)); # this is excess info for the current num_brands setting. skip it
					$brand = 'AFR'.($i+1);
				}
			}
		}
		
		next unless $brand;
		
		$brands_result->{$brand}->{'cats'}->{$category} = $score;
		
	}
	
	return 1;
}

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

# Uses ECS::Report to build and dispatch the data from the cron
sub send_compliance_emails {
	my $p = shift;
	my $compliants = $p->{'compliants'};
	
	my %make_reports = ();
	
	for my $comp (@{ $compliants }) {
		
		my $vehicle = $comp->{'vehicle'};
		my $group = $comp->{'group'};
		my $compliance = $comp->{'compliance'};
		
		my $make_report = $make_reports{$vehicle->{'makeID'}} ||= {
			'makeName'   => $vehicle->{'makeName'},
			'recipients' => [
				$ECS::Config::c{'email'}->{'brandManagers'}->{$vehicle->{'makeID'}}
				|| 'cron13paprcomp@ecstuning.com'
			],
			'rows'       => [],
		};
		
		for my $category (@{ $group->{'categoryConfigs'} }) {
			
			local $p->{'category'} = $category;
			my @category_names = API::PAPR::get_category_names($p);
			
			push(@{ $make_report->{'rows'} }, {
				'vehicleLabel' => $vehicle->{'vehicleLabel'},
				'groupName'    => $group->{'name'},
				'groupClass'   => uc($group->{'class'}),
				'numBrands'    => $compliance->{'numBrands'},
				'categoryPath' => join(' &gt; ', @category_names),
				'websiteLink'  => (
					'<a href="http://www.ecstuning.com/'
					.$vehicle->{'vehicleURL'}.'/'
					.join('/', map {
						API::PAPR::format_category_link($_)
					} @category_names)
					.'/">Location</a>'
				),
			});
		}
	}
	
	my $report_fields = [keys %{ $ECS::Report::fields->{'paprCompliance'} }];
	
	for my $makeID (keys %make_reports) {
		my $make_report = $make_reports{$makeID};
		
		my $report_title = sprintf('PAPR %s Vehicle/Product Combinations %s', (
			$make_report->{'makeName'},
			$p->{'emailType'},
		));
		
		while (
			my @report_rows_chunk = splice(@{ $make_report->{'rows'} }, 0, $report_row_limit)
		) {
			
			my $report_body = ECS::Report::buildReport({
				'type'   => 'paprCompliance',
				'title'  => $report_title,
				'fields' => $report_fields,
				'rows'   => \@report_rows_chunk,
			});
			
			ECS::Report::sendHTMLReport({
				'recipients' => $make_report->{'recipients'},
				'subject'    => $make_report->{'makeName'}.' PAPR Compliance Report',
				'content'    => (
					'<html>'
						.'<head><title>'.$report_title.'</title></head>'
						.'<body>'.$report_body.'</body>'
					.'</html>'
				),
			});
			
		}
	
	}
	
	return 1;
}

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

sub get_category_names {
	my $p = shift;
	
	$p->{'categoryNames'} ||= API::PAPR::get_all_category_names($p);
	
	my $idx = 0;
	my @names = ();
	
	for my $catID (split(/_/, $p->{'category'})) {
		
		push(@names, (
			$p->{'categoryNames'}->[$idx]->{$catID}
			|| 'UNKNOWN'
		));
		
		$idx++;
	}
	
	return @names;
}

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

sub get_all_category_names {
	my $p = shift;
	
	my @categories = ();
	
	for my $level (@ECS::Category::LEVELS) {
		
		local $p->{'sql'} =
			'SELECT '.$level.'ID AS ID, '.$level.'Name AS name
			FROM '.$level;
		
		push(@categories, {
			map {
				$_->{'ID'} => $_->{'name'}
			} @{ ECS::SQL::query($p) || [] }
		});
	}
	
	return \@categories;
}

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

sub format_category_link {
	# Replace spaces with underscores and forward-slashes with dashes
	my $category_name = shift;
	$category_name =~ s/\s/_/g;
	$category_name =~ s/\//-/g;
	return $category_name;
}

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

sub insert_compliants {
	my $p = shift;
	
	my @compliants = @{ $p->{'compliants'} };
	
	while (
		my @compliants_chunk = splice(@compliants, 0, $insert_compliance_limit)
	) {
		my @values = ();
		
		my $insert_compliants = ECS::SQL::toDBI($p)->prepare(
			'INSERT INTO paprCompliance (vehicleConfigID, paprGroupID, numBrands) VALUES '
			.join(',', map {
				
				push(@values, (
					$_->{'vehicle'}->{'vehicleConfigID'},
					$_->{'group'}->{'paprGroupID'},
					$_->{'compliance'}->{'numBrands'},
				));
				
				'(?, ?, ?)'
				
			} @compliants_chunk)
			# If a record for this vehicleConfigID, paprGroupID, numBrands already exists
			# clear out the notifiedDate making this record compliant
			.' ON DUPLICATE KEY UPDATE notifiedDate = NULL'
		);
		
		try { $insert_compliants->execute(@values); 1 }
		catch { warn $_; 0 }
		
	}
	
	return 1;
}

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

sub set_compliance_notified {
	my $p = shift;
	
	local $p->{'sql'} =
		'UPDATE
			paprCompliance
		SET
			notifiedDate = NOW()
		WHERE
			paprComplianceID IN ('.join(',', map {
				$_->{'compliance'}->{'paprComplianceID'}
			} @{ $p->{'compliants'} }).')';
	
	unless (ECS::SQL::query($p)) {
		warn 'Error updating notifiedDate in paprCompliance.';
		return 0;
	}
	
	return 1;
}
