package RPS::Import::MapFaceBase;

use strict;
use Data::Dumper;

use lib '/app/tools/common/lib';
use Common::Assert;

use lib '/app/tools/raptor/lib/';
use Raptor::DB::Item::Sale;

use lib '/app/tools/sale_import/lib/';
use Import::Importer;
use RPS::Import::Service;

use lib '/app/tools/rps/lib';
use RPS::DB::Item::MapFaceTempMapping;
use RPS::DB::Item::MapFaceTempMappingCriteria;
use RPS::DB::Item::SaleImportError;
use RPS::DB::Item::SaleImportErrorRawValue;
use RPS::DB::Item::SaleImportFieldMapping;
use RPS::DB::Item::SaleImportFileHeader;

use constant MAPFACE_CRITERIA_METHOD => {
    serviceID   => '_mapFaceServiceID',
    productType => '_mapFaceProductType',
    formatType  => '_mapFaceFormatType',
    mediaType   => '_mapFaceMediaType',
    countryCode => '_mapFaceCountryCode',
};

use constant MAPFACE_ERROR_CRITERIA => {
    RPS::DB::Item::SaleImportError::kErrorService     => 'serviceID',
    RPS::DB::Item::SaleImportError::kErrorCountry     => 'countryCode',
    RPS::DB::Item::SaleImportError::kErrorProductType => 'productType',
    RPS::DB::Item::SaleImportError::kErrorFormatType  => 'formatType',
    RPS::DB::Item::SaleImportError::kErrorMediaType   => 'mediaType',
};

# We create the sale_import_error entries before we have a sale id.
# So, we need to store their ids and circle back at the end and fill in the sale id.
my @saleImportErrorIDs;

# Same deal with some of the raw values on the sales lines.
my @saleImportErrorRawValueIDs;

sub _getOffset {
    my $self  = shift;
    my $field = shift;

    my $validAccessors = $self->_getValidAccessorHash();

    assert( $field, "Field required" );

    my $map = $self->_fieldMap();
    my $unverifiedMap = $self->_unverifiedFieldMap() || {};

    if ( $validAccessors->{$field} ) {
        return $self->_convertIndex( $map->{$field} );

    } elsif ( exists( $unverifiedMap->{$field} ) ) {
        return $self->_convertIndex( $unverifiedMap->{$field} );

    } else {
        return undef;
    }
}

sub handleError {
    my ( $self, $description, $errType ) = @_;

    $errType ||= 0;
    if ( exists MAPFACE_ERROR_CRITERIA->{$errType} ) {
        $self->_createCache();
        my $res = $self->mapError($errType);
        return $res if $res;
    }

    $self->logError( $description, $errType );
    return;
}

sub mapError {
    my ( $self, $errType ) = @_;

    my $hSaleData = $self->_getSaleCriteriaMappings($errType);
    my $hExistedMappings = $self->_cache->{prepared_service_mappings};

    foreach my $hExistedMapping ( values %$hExistedMappings ) {
        next unless $self->_areFieldsMatched($hSaleData, $hExistedMapping->{mapface_mapping_id});

        if ( 'serviceID' eq MAPFACE_ERROR_CRITERIA->{$errType} && $hExistedMapping->{mapped_service_id} ) {
            # We also need to make sure that this service is in the client's service table.
            # We'll just try to add it, and it's smart enough to skip it if it's already there.
            my $clientID = Common::RSApp::GetClientID();
            Client::Service::AddService(
                client_id  => $clientID,
                service_id => $hExistedMapping->{mapped_service_id}
            );
            return $hExistedMapping->{mapped_service_id};
        } elsif ( 'countryCode' eq MAPFACE_ERROR_CRITERIA->{$errType} && $hExistedMapping->{mapped_country_code} ) {
            return $hExistedMapping->{mapped_country_code};
        } elsif ( 'productType' eq MAPFACE_ERROR_CRITERIA->{$errType} && $hExistedMapping->{mapped_product_type} ) {
            return $hExistedMapping->{mapped_product_type};
        } elsif ( 'formatType' eq MAPFACE_ERROR_CRITERIA->{$errType} && $hExistedMapping->{mapped_format_type} ) {
            return $hExistedMapping->{mapped_format_type};
        } elsif ( 'mediaType' eq MAPFACE_ERROR_CRITERIA->{$errType} && $hExistedMapping->{mapped_media_type} ) {
            return $hExistedMapping->{mapped_media_type};
        }
    }

    return;
}

sub _getSaleCriteriaMappings {
    my ($self, $errorType) = @_;

    my $hResult = {};

    my $criteriaName   = MAPFACE_ERROR_CRITERIA->{$errorType};
    my $criteriaMethod = MAPFACE_CRITERIA_METHOD->{$criteriaName};
    foreach my $criteriaField ( $self->$criteriaMethod() ) {
        next unless $criteriaField;
        if ( ref $criteriaField eq 'HASH' ) {
            my ($field, $value) = %$criteriaField;
            $hResult->{$field} = $value;
        } else {
            $hResult->{$criteriaField} = $self->_getByFieldName($criteriaField);
        }
    }

    return $hResult;
}

sub logError {
    my ( $self, $description, $type ) = @_;

    my $saleImportError = RPS::DB::Item::SaleImportError->Create(
        file_id     => $self->{_file}->FileID,
        description => $description,
        error_type  => $type,
    );
    $saleImportError->save();

    push( @saleImportErrorIDs, $saleImportError->sale_import_error_id );

    # Setting this flag so that we don't try to find a match for this sale later.
    $self->{_hasError} = 1;

    # So now we need to store all of the values used for mapping for this sale, if we haven't done so already.
    # We'll also need to set a flag so that we don't do it more than once for a sale.
    if ( !$self->{_rawValuesStored} ) {
        foreach my $field ( $self->_mappingFields() ) {
            my $value = defined $self->_getByFieldName($field) ? $self->_getByFieldName($field) : '';
            my $errorRawValue = RPS::DB::Item::SaleImportErrorRawValue->Create(
                file_id => $self->{_file}->FileID,
                field   => $field,
                value   => $value,
            );
            $errorRawValue->save();
            push( @saleImportErrorRawValueIDs, $errorRawValue->sale_import_error_raw_value_id );
        }
        $self->{_rawValuesStored} = 1;
    }
}

# The importers must overload these with the field accessors used to derive each one.
sub _mapFaceCountryCode {
    return undef;
}

sub _mapFaceServiceID {
    return undef;
}

sub _mapFaceProductType {
    return undef;
}

sub _mapFaceFormatType {
    return undef;
}

sub _mapFaceMediaType {
    return undef;
}

# By default, the mappings made for a specific importer version will not be shared.
# Importers can override this if they want to look for mapping across multiple versions.
sub _mapFaceCompatibleVersions {
    my $self = shift;

    my $serviceID = $self->{_file}->ServiceID;
    my $hServiceVersions = RPS::Import::Service::GetImporters($serviceID);
    return keys %$hServiceVersions;
}

# Now we need a list of the distinct accessors used across all of the mapping fields
sub _mappingFields {
    my $self = shift;

    my %accessors;
    foreach my $method ( values %{+MAPFACE_CRITERIA_METHOD} ) {
        foreach my $fieldName ( $self->$method() ) {
            ($fieldName) = %$fieldName if ref $fieldName eq 'HASH';
            $accessors{$fieldName}++;
        }
    }

    return keys %accessors;
}

sub _fillInSaleID {
    my ( $self, $saleID ) = @_;

    foreach my $saleImportErrorID (@saleImportErrorIDs) {
        my $saleImportError = RPS::DB::Item::SaleImportError->Lookup( sale_import_error_id => $saleImportErrorID );
        if ($saleImportError) {
            $saleImportError->sale_id($saleID);
            $saleImportError->save();
        }
    }
    undef @saleImportErrorIDs;

    foreach my $saleImportErrorRawValueID (@saleImportErrorRawValueIDs) {
        my $saleImportErrorRawValue =
          RPS::DB::Item::SaleImportErrorRawValue->Lookup( sale_import_error_raw_value_id => $saleImportErrorRawValueID );
        if ($saleImportErrorRawValue) {
            $saleImportErrorRawValue->sale_id($saleID);
            $saleImportErrorRawValue->save();
        }
    }
    undef @saleImportErrorRawValueIDs;
    undef $self->{_rawValuesStored};
    undef $self->{_hasError};
}

sub _processHeader {
    my $self = shift;
    return unless $self->_isHeader;

    my $fileID = $self->{_file}->FileID;

    foreach my $field ( $self->_mappingFields ) {
        next unless defined $self->_getByFieldName($field);
        my $header = $self->_getByFieldName($field);

        # First, let's make sure we don't already have this exact header already.
        my $alreadyStored;
        my $collection = RPS::DB::Item::SaleImportFileHeader->GetAllByFieldAndFileID($field, $fileID);
        while ( $collection->hasNext() ) {
            my $oFileHeader = $collection->next();
            if ( $oFileHeader->header eq $header ) {
                $alreadyStored = 1;
                last;
            }
        }

        unless ($alreadyStored) {
            RPS::DB::Item::SaleImportFileHeader->Create(
                file_id => $fileID,
                field   => $field,
                header  => $header,
            )->save();
        }
    }

    # Let's also store a list of which fields map to which values.
    # We really only need this for highlighting on the mapping page.
    while ( my($field, $method) = each %{+MAPFACE_CRITERIA_METHOD} ) {
        foreach my $fieldName ( $self->$method() ) {
            ($fieldName) = %$fieldName if ref $fieldName eq 'HASH';
            RPS::DB::Item::SaleImportFieldMapping->Create(
                file_id => $fileID,
                field   => $field,
                mapping => $fieldName,
            )->save();
        }
    }

    return 1;
}

# We're going to deal with the qualifying errors in groups, so let's go ahead
# and figure those groups out now.
sub _postProcess {
    my ($self, %args) = @_;
    Log->info("Starting _postProcess");

    foreach my $serviceID ( keys %{ $self->{services} } ) {
        Client::Service::AddService(
            client_id  => $args{client_id},
            service_id => $serviceID
        );
    }

    # So we need to create separate groups based on the distinct combination of values
    # for each field in the sale_import_error_raw_value table.

    my $fileID = $self->{_file}->FileID;

    # Let's make sure that we don't have an unqualified errors before even bothering with this.
    my $unqualifiedErrorCount = RPS::DB::Item::SaleImportError->GetNonQualifiedCountByFileID($fileID);
    if ($unqualifiedErrorCount) {
        Log->warn("MapFace: there are unqualified errors.");
        return;
    }

    # This will holds some strings that we'll use to tell the mapping groups apart
    my %flatCombos;

    # As we go through and create the groups, I'm going to keep track of which one
    # each sale belongs to with this.
    my %saleGroup;

    # Going to start the mapping groups at 1 instead of 0, but we'll wait to increment it until we need to.
    my $groupNum = 0;

    # Let's grab all of the sale_import_error records
    my $importErrors = RPS::DB::Item::SaleImportError->GetAllQualifyingByFileID($fileID);
    while ( $importErrors->hasNext() ) {
        my $importError = $importErrors->next();
        my $saleID      = $importError->sale_id;

        # We only need to do this once per sale, not once per error.
        if ( !$saleGroup{$saleID} ) {
            my ($flatCombo, %hashCombo);
            my $oMappings = RPS::DB::Item::SaleImportErrorRawValue->GetAllBySaleID($saleID);

            while ( $oMappings->hasNext() ) {
                my $oMapping = $oMappings->next();

                my $fieldName = $oMapping->field;
                my $value     = $oMapping->value;
                $hashCombo{$fieldName} = $value;
                $flatCombo .= $fieldName . $value;
            }

            # We need a little more info to figure out which sales should be grouped together.
            # We're only going to store these values in the $flatCombo, since their only purpose
            # is to help us distinguish between like sales.
            my $fieldMappings = RPS::DB::Item::SaleImportFieldMapping->GetAllByFileID($fileID);
            my %seenFieldMapping;
            my $sale = Raptor::DB::Item::Sale->Lookup( sale_id => $saleID );
            while ( $fieldMappings->hasNext() ) {
                my $fieldMapping = $fieldMappings->next();
                my $fieldName    = $fieldMapping->field;

                if ( !$seenFieldMapping{$fieldName} ) {
                    $flatCombo .= $fieldName;

                    if ( $fieldName eq 'productType' ) {
                        $flatCombo .= $sale->product_type;
                    } elsif ( $fieldName eq 'mediaType' ) {
                        $flatCombo .= $sale->media_type;
                    } elsif ( $fieldName eq 'formatType' ) {
                        $flatCombo .= $sale->format_type;
                    } elsif ( $fieldName eq 'serviceID' ) {
                        $flatCombo .= $sale->service_id;
                    } elsif ( $fieldName eq 'countryCode' ) {
                        $flatCombo .= $sale->country_code;
                    }

                    $seenFieldMapping{$fieldName} = 1;
                }
            }

            # So if $flatCombo is new, then we have a new group.
            if ( !$flatCombos{$flatCombo} ) {
                $groupNum++;

                my $tempMapping = RPS::DB::Item::MapFaceTempMapping->Create(
                    file_id   => $fileID,
                    group_num => $groupNum,
                    status    => 0,
                );

                $tempMapping->save();

                $flatCombos{$flatCombo} = $tempMapping->mapface_temp_mapping_id;

                # Store the fields for this mapping group
                foreach my $fieldName ( keys %hashCombo ) {
                    my $value           = $hashCombo{$fieldName};
                    my $mappingCriteria = RPS::DB::Item::MapFaceTempMappingCriteria->Create(
                        mapface_temp_mapping_id => $tempMapping->mapface_temp_mapping_id,
                        field                   => $fieldName,
                        value                   => $value,
                    );

                    $mappingCriteria->save();
                }
            }

            $saleGroup{$saleID} = $flatCombos{$flatCombo};
        }
    }

    # Ok, now we just need to fill in the mapface temp mapping id for all
    # of the sale_import_error records for this file.
    foreach my $saleID ( keys %saleGroup ) {
        my $mappingID    = $saleGroup{$saleID};
        my $importErrors = RPS::DB::Item::SaleImportError->GetAllQualifyingBySaleID($saleID);
        while ( $importErrors->hasNext() ) {
            my $importError = $importErrors->next();
            $importError->mapface_temp_mapping_id($mappingID);
            $importError->save();
        }
    }

}

sub _cache { return shift->{_cache} }

sub _createCache {
    my $self = shift;

    # don't create cache if it already exists
    return 0 if $self->{cache_flag};

    # a string with all compatible versions for mapFace (inner data)
    my $compatibleVersions = join ',', $self->_mapFaceCompatibleVersions;

    # list of prepared mappings for the service (RSCOMMON.mapface_mapping)
    my $hPreparedMappings = $self->_getMappingsList($compatibleVersions);

    # list of common mapface mapping criteria (RSCOMMON.mapface_mapping_criteria)
    my $hCommonMappingCriteria = $self->_getCommonMappingCriteriaList($hPreparedMappings);

    $self->{_cache} = {
        importer_compatible_versions => $compatibleVersions,
        prepared_service_mappings    => $hPreparedMappings,
        common_mapping_criteria      => $hCommonMappingCriteria,
    };

    # set cache flag if all cache items was created
    return $self->{cache_flag} = 1;
}

sub _getMappingsList {
    my ($self, $compatibleVersions) = @_;

    my $aMappings = Common::DB::Item::MapFaceMapping->GetByServiceAndVersion(
        $self->{_file}->ServiceID, $compatibleVersions, 'ARRAY'
    );
    my %mappings = map { $_->{mapface_mapping_id} => $_ } @$aMappings;

    return \%mappings;
}

sub _getCommonMappingCriteriaList {
    my ($self, $hPreparedMappings) = @_;

    my %data;
    return \%data unless keys %$hPreparedMappings;

    my $mapFaceMappingIDs = join ',', keys %$hPreparedMappings;
    my $aData = Common::DB::Item::MapFaceMappingCriteria->GetByMappings( \$mapFaceMappingIDs );

    $data{ $_->{mapface_mapping_id} }{ $_->{field} } = $_->{value} for @$aData;

    return \%data;
}

sub _areFieldsMatched {
    my ($self, $hSaleData, $mappingID) = @_;

    my $matched = 0;
    my $hMappingData = $self->_cache->{common_mapping_criteria}->{$mappingID};

    while ( my($field, $value) = each %$hSaleData) {
        $value = qr/^\Q$value\E$/i if ref $value ne 'Regexp';
        $matched++ if $hMappingData->{$field} =~ $value;
    }

    return keys %$hSaleData == $matched ? 1 : 0;
}


1;
