package Sale::Match::WMG;

use strict;
use base 'Sale::Match';

use lib '/app/tools/common/lib';
use Common::Util qw(clean word_containment);

use lib '/app/tools/data_classes/lib';
use File::Sale;
use Client::Service;

use constant DEBUG => 0;

use constant NP      => 0;
use constant NOMATCH => 1;
use constant ISMATCH => 2;

use constant SUGGEST => 'suggest';
use constant MATCH   => 'match';

my @FORMAT_GROUP_DIGITAL_AUDIO =
  ( File::Sale::FORMAT_DOWNLOAD, File::Sale::FORMAT_STREAM, File::Sale::FORMAT_TETHERED, File::Sale::FORMAT_RINGTONE );
my @FORMAT_GROUP_DIGITAL_VIDEO = ( File::Sale::FORMAT_VIDEO, File::Sale::FORMAT_VIDEOSTREAM );

# format to media type mapping
# if there are multiple for a given format then list them in order of priority
my @DEFAULT_MEDIA_TYPES = qw(EMD CD);
my %ALT_MEDIA_TYPES     = (
    File::Sale::FORMAT_DUALDOWNLOAD()     => [qw(EMD)],
    File::Sale::FORMAT_VIDEOSTREAM()      => [qw(EVD)],
    File::Sale::FORMAT_VIDEO()            => [ 'EVD', 'EHCD', 'Dual Disc' ],
    File::Sale::FORMAT_RINGBACK()         => [qw(eRingback)],
    File::Sale::FORMAT_MASTERTONE()       => [qw(eMastertone eVoiceRinger)],
    File::Sale::FORMAT_ANIMATEDRINGTONE() => [qw(eAnimatedRingtone)],
    File::Sale::FORMAT_VOICERINGER()      => [qw(eVoiceRinger eMastertone)],
    File::Sale::FORMAT_VIDEORINGER()      => [qw(eVideoRinger)],
    File::Sale::FORMAT_MIDI()             => [qw(eMIDI)],
    File::Sale::FORMAT_GRAPHIC()          => [qw(eScreensaver eGraphic eWallpaper)],
    File::Sale::FORMAT_SMSTONE()          => [qw(eSMSTone)],
    File::Sale::FORMAT_WALLPAPER()        => [qw(eScreensaver eWallpaper eGraphic)],
);

my @MAP_REQFIELDS_ALBUM = qw(product_type format upc artist album service_id);
my @MAP_REQFIELDS_TRACK = qw(product_type format upc artist album service_id isrc track);
my @MAP_REQFIELDS_PHYS  = qw(product_type format upc artist album service_id);

sub new {
    my ( $self, $parent ) = @_;
    DEBUG && print STDERR "hello from subclass\n";
    return undef unless ( ref($parent) eq 'HASH' );
    $parent->{album_tt} = {};
    $parent->{track_tt} = {};

    return bless $parent;
}

sub SearchCatalog {
    my $self = shift;
    my (%in) = @_;

    my $search_type    = lc $in{type};
    my $search_keyword = $in{keyword} || $in{keywords};
    my $product_type   = uc $in{product_type};

    my $bool_terms  = ( exists $in{bool_terms} )  ? uc $in{bool_terms}  : 'AND';
    my $bool_fields = ( exists $in{bool_fields} ) ? uc $in{bool_fields} : 'OR';
    my $limit = ( exists $in{limit} && $in{limit} ) ? $in{limit} : 100;
    my $data = ( exists $in{data} ) ? $in{data} : undef;

    # for backwards compatibility (temporary)
    if ( exists $in{all} && defined $in{all} ) {
        $search_type    = 'all';
        $search_keyword = $in{all};
    }

    #my @search_fields = qw(artist album track isrc upc);
    my @search_fields = qw(track album artist isrc upc grid);
    my %search_types_allowed = map { $_ => 1 } ( 'smart', 'all', @search_fields );

    unless ( defined $search_type && exists $search_types_allowed{$search_type} ) {
        $self->{errstr} = "You must specify a valid type parameter: " . join( ', ', keys %search_types_allowed );
        return undef;
    }

    unless ( defined $search_keyword && length( clean($search_keyword) ) >= 3 ) {
        $self->{errstr} = "You must specify a keyword value with at least three letters and/or numbers";
        return undef;
    }

    unless ( defined $product_type && $product_type =~ /^[A-Z0-9]$/ ) {
        $self->{errstr} = "You must specify a valid product_type value";
        return undef;
    }

    if ( defined $limit && ( $limit !~ /^\d+$/ || $limit < 1 ) ) {
        $self->{errstr} = "The value for 'limit' must be an integer greater than zero";
        return undef;
    }

    unless ( $bool_terms && $bool_terms =~ /^AND|OR$/ ) {
        $self->{errstr} = "You must specify a valid bool_terms value";
        return undef;
    }

    unless ( $bool_fields && $bool_fields =~ /^AND|OR$/ ) {
        $self->{errstr} = "You must specify a valid bool_fields value";
        return undef;
    }

    my $search_keyword_clean = clean($search_keyword);
    my $smart_key_found;
    if ( $search_type eq 'smart' ) {

        unless ( defined $data && ref($data) eq 'HASH' ) {
            $self->{errstr} = "You must specify a data hashref for a smart search";
            return undef;
        }

        foreach my $field (@search_fields) {
            next unless ( exists $data->{$field} && defined $data->{$field} );
            next if length( $data->{$field} ) < 3;

            # try to see if our search keyword is a substring of any of the product data fields

            if ( $data->{$field} =~ /$search_keyword/i ) {
                $smart_key_found = $field;
                last;
            }

            # next try to match the "clean" version
            my $data_field_clean = clean( $data->{$field} );
            if ( length($search_keyword_clean) >= 3 && length($data_field_clean) >= 3 && $data_field_clean =~ /$search_keyword_clean/ ) {
                $smart_key_found = $field;
                last;
            }
        }

        # if we couldn't limit the search to a specific field, revert to searching by type
        $search_type =
            $smart_key_found     ? $smart_key_found
          : $product_type eq 'T' ? 'track'
          : $product_type eq 'A' ? 'album'
          :                        'all';
    }

    DEBUG && print STDERR "PRODUCT_SEARCH: search_type = $search_type\n";

    my $media_types = GetMediaTypes( $data->{format} );
    my $types = join( ',', @$media_types );
    $types =~ s/[^,]+/?/g;

    my $sql             = "SELECT product_id FROM product_catalog_extract WHERE product_type=? AND vendor_id IN($types) AND ";
    my $where           = '';
    my @args            = ( $product_type, @$media_types );
    my @search_products = ();

    #select track_name from track where match(track_name) against ('Napster Presents - A Conversation With Nas') limit 30;

    if ( $search_type eq 'all' ) {
        my @prodFields = qw(track_clean album_clean artist_clean isrc upc company_id);
        $where = '(';
        $where .= join( '=? or ', @prodFields ) . '=?';
        map { push( @args, $search_keyword_clean ) } qw(track_clean album_clean artist_clean);
        map { push( @args, $search_keyword ) } qw(isrc upc grid);
        $where .= ') limit 100';

        my $product_list = $self->_do_list_query( $sql . $where, @args );
        push( @search_products, @{$product_list} ) if $product_list;
    } elsif ( $search_type =~ /^(track|album|artist)$/ ) {
        $where = $search_type . '_clean=?';
        push( @args, $search_keyword_clean );

        my $product_list = $self->_do_list_query( $sql . $where, @args );
        push( @search_products, @{$product_list} ) if $product_list;

        if ( scalar @search_products < 10 )    # try full-text search
        {
            $where        = "match(${search_type}_name) against (?) limit 100";
            @args         = ( $product_type, @$media_types, $search_keyword );
            $product_list = $self->_do_list_query( $sql . $where, @args );
            push( @search_products, @{$product_list} ) if $product_list;
        }
    } elsif ( $search_type eq 'grid' ) {
        $where = "(company_id=? OR company_id LIKE '$search_keyword%')";
        push( @args, $search_keyword );

        my $product_list = $self->_do_list_query( $sql . $where, @args );
        push( @search_products, @{$product_list} ) if $product_list;
    } else    # must be isrc or upc
    {
        $where = "($search_type=? OR $search_type LIKE '$search_keyword%')";
        push( @args, $search_keyword );

        my $product_list = $self->_do_list_query( $sql . $where, @args );
        push( @search_products, @{$product_list} ) if $product_list;
    }

    my $detail = {};
    foreach my $product_id (@search_products) {
        $detail->{$product_id} = {
            level   => 60,
            type    => $search_type . '_search',
            pattern => 'undef'
        };
    }

    return new Sale::Match::Result(
        products        => [],
        num_products    => 0,
        import_status   => undef,
        map_id          => undef,
        rec_products    => [],
        search_products => \@search_products,
        detail          => $detail
    );
}

sub _do_list_query {
    my $self = shift;

    DEBUG && print STDERR 'list query: ' . join( ',', @_ ) . "\n";
    my $sth = $self->rsdb->DBH->prepare(shift);
    $sth->execute(@_);

    if ( $sth->rows ) {
        my @list = map { @{$_} } @{ $sth->fetchall_arrayref( [0] ) };
        return \@list;
    }
}

sub FindBestMatch {
    my $self     = shift;
    my (%in)     = @_;
    my $skip_rec = ( exists $in{skip_rec} && $in{skip_rec} ) ? 1 : 0;
    my $map_only = $in{map_only};

    # check to see if we have a previous map entry for this input data
    # if we do, then we can short-circuit the normal search
    if ( my $result = $self->_search_product_input_map( $in{data} ) ) {
        return $result if defined $result;
    }
    return undef if $map_only;

    my $matches = {};

    if ( $in{data}{product_type} eq 'T' ) {
        $matches = $self->_get_track_matches( $in{data} );
    } elsif ( $in{data}{product_type} eq 'A' ) {
        $matches = $self->_get_album_matches( $in{data} );
    }

    my $result;
    if ( $skip_rec == 1 )    # matches (if any)
    {
        # remove any suggestions
        DEBUG && print STDERR "remove suggestions\n";
        while ( my ( $id, $match ) = each %$matches ) {
            delete $matches->{$id} if ( $match->{status} eq SUGGEST );
        }

        my $num_products = scalar keys %$matches;
        DEBUG && print STDERR "num prod: $num_products\n";
        my @product_list = ();

        if ( $num_products == 1 ) {
            @product_list = keys %$matches;
        } elsif ( $num_products > 1 ) {
            DEBUG && print STDERR "sort by weight\n";
            my $max_weight = 0;
            foreach my $id ( $self->_sort_by_weight($matches) ) {
                if ( $matches->{$id}{weight} < $max_weight ) {
                    delete $matches->{$id};
                    next;
                }
                $max_weight = $matches->{$id}{weight};
            }

            $num_products = scalar keys %$matches;
            if ( $num_products == 1 ) {
                @product_list = keys %$matches;
            } else {
                DEBUG && print STDERR "filter by media type\n";
                $self->_filter_by_media_type( $matches, $in{data}{format} );
                $num_products = scalar keys %$matches;

                if ( $num_products == 1 ) {
                    @product_list = keys %$matches;
                } else {
                    DEBUG && print STDERR "filter by artist name\n";
                    $self->_filter_by_artist_name( $in{data}{artist}, $matches ) if $in{data}{artist};
                    $num_products = scalar keys %$matches;

                    if ( $num_products == 1 ) {
                        @product_list = keys %$matches;
                    } else {
                        DEBUG && print STDERR "filter by release date\n";
                        $self->_filter_by_release_date($matches);
                        $num_products = scalar keys %$matches;

                        @product_list = keys %$matches;
                    }
                }
            }
        }

        my $import_status =
            $num_products == 1 ? Sale::Match::STATUS_MATCH
          : $num_products == 0 ? Sale::Match::STATUS_NOMATCH
          : $num_products > 1  ? Sale::Match::STATUS_MULTIMATCH
          :                      '';

        $result = new Sale::Match::Result(
            products        => \@product_list,
            num_products    => $num_products,
            import_status   => $import_status,
            map_id          => undef,
            rec_products    => [],
            search_products => undef,
            detail          => $matches,
        );
    } else    # suggestions
    {
        DEBUG && print STDERR 'matches: ' . join( ',', keys %$matches ) . "\n";
        my $recommended =
          keys %$matches
          ? $self->_sort_suggestions( $in{data}, $matches )
          : $self->_best_recommended_search( $in{data} );

        $result = new Sale::Match::Result(
            products        => [],
            num_products    => 0,
            import_status   => undef,
            map_id          => undef,
            rec_products    => $recommended || [],
            search_products => undef,
            detail          => {},
        );
    }

    return $result if defined $result;

    $self->{errstr} = "Could not find/return results!";
    return undef;
}

sub GetMediaTypes {
    my $format = shift;
    return $ALT_MEDIA_TYPES{$format} ? \@{ $ALT_MEDIA_TYPES{$format} } : \@DEFAULT_MEDIA_TYPES;
}

sub _check_format_type {
    my $self   = shift;
    my $format = uc(shift);

    map { return 'DA' if ( $format eq $_ ) } @FORMAT_GROUP_DIGITAL_AUDIO;
    map { return 'DV' if ( $format eq $_ ) } @FORMAT_GROUP_DIGITAL_VIDEO;

    return $format;
}

sub _best_recommended_search {
    my $self = shift;
    my $data = shift;

    #select track_name from track where match(track_name) against ('Napster Presents - A Conversation With Nas') limit 30;
    my ( $match, $value, $limit );
    if ( $data->{product_type} eq 'T' && defined $data->{track} ) {
        $limit = 100;
        $match = 'track_name';
        $value = $data->{track};
        if ( defined( $data->{artist} ) ) {
            $match = 'artist_name,track_name';
            $value = $data->{artist} . " " . $data->{track};
        }
    } elsif ( $data->{product_type} eq 'A' && defined $data->{album} ) {
        $limit = 30;
        $match = 'album_name';
        $value = $data->{album};
    }

    #elsif (defined $data->{artist})
    #{
    #$match = 'artist_name';
    #$value = $data->{artist};
    #}
    else { return undef; }

    my $media_types = GetMediaTypes( $data->{format} );
    my $types = join( ',', @$media_types );
    $types =~ s/[^,]+/?/g;

    my $sql =
"SELECT product_id FROM product_catalog_extract WHERE vendor_id in($types) and product_type=? and match($match) against(?) limit $limit";
    DEBUG && print STDERR join( ',', $sql, @$media_types, $data->{product_type}, $value, "\n" );
    my $sth = $self->dbh()->prepare($sql) || print STDERR "rec search error: " . $self->dbh->errstr;
    $sth->execute( @$media_types, $data->{product_type}, $value ) or print STDERR "rec search error: " . $sth->errstr;

    my @productList = map { $_->[0] } @{ $sth->fetchall_arrayref( [0] ) };

    return \@productList || undef;
}

sub _get_album_matches {
    my ( $self, $data ) = @_;

    my $upc = length $data->{upc} > 12 ? $data->{upc} :    # grid
      length $data->{upc} >= 10
      ? substr( $data->{upc}, 0, 10 )
      : undef;
    my $upc_opt =
        length $data->{client_product_id} > 10  ? $data->{client_product_id}
      : length $data->{service_product_id} > 10 ? $data->{service_product_id}
      :                                           undef;
    $upc_opt = substr( $upc_opt, 0, 10 ) if ( $upc_opt && length $upc_opt < 13 );

    my $album_clean = $data->{album} ? clean( $data->{album} ) : undef;
    return undef unless ( $upc or $album_clean );

    my $media_types = GetMediaTypes( $data->{format} );
    my $types = join( ',', @$media_types );
    $types =~ s/[^,]+/?/g;

    my $sql = <<EofSQL;
SELECT product_id, vendor_id as media_type, artist_name, release_date, upc, company_id as upc_opt, album_clean
FROM product_catalog_extract
WHERE vendor_id in($types)
  AND product_type=?
EofSQL
    my @params = ( @$media_types, $data->{product_type} );

    my %raw_results = ();
    if ( length $upc == 10 ) {
        my $sql_upc = $sql;
        $sql_upc .= ' AND upc like ?';
        push( @params, "$upc%" );
        DEBUG && print STDERR "sql: $sql_upc\nparams: " . join( ',', @params ) . "\n";
        $self->_get_by_id( $sql_upc, \@params, \%raw_results );
        pop(@params);
    }

    if ($album_clean) {
        $sql .= ' AND album_clean=?';
        push( @params, $album_clean );
        DEBUG && print STDERR "sql: $sql\nparams: " . join( ',', @params ) . "\n";
        $self->_get_by_id( $sql, \@params, \%raw_results );
    }

    my %matches = ();
    my $tt      = $self->_get_album_tt();

    # initialize all flags to NP
    my $upc_flag   = NP;
    my $album_flag = NP;

    while ( my ( $id, $row ) = each %raw_results ) {
        if ( defined $upc ) {
            $upc_flag = ( $row->{upc} =~ /^$upc/ or $row->{upc_opt} eq $upc ) ? ISMATCH : NOMATCH;
        }

        if ( $upc_flag != ISMATCH and defined $upc_opt ) {

            # only override to ISMATCH otherwise leave as it was (NOMATCH or NP)
            $upc_flag = ISMATCH if ( $row->{upc} =~ /^$upc_opt/ or $row->{upc_opt} eq $upc_opt );
        }

        if ( defined $album_clean ) {
            $album_flag = $row->{album_clean} eq $album_clean ? ISMATCH : NOMATCH;
        }

        next unless exists $tt->{$upc_flag}{$album_flag};

        my $match_status = $tt->{$upc_flag}{$album_flag}{status};

        # override status to SUGGEST if upc or album are NP but artist is and no match
        $match_status = SUGGEST
          if ( $match_status == MATCH
            && $data->{artist}
            && ( $upc_flag == NP || $album_flag == NP )
            && !word_containment( $data->{artist}, $row->{artist_name} ) );

        $matches{ $row->{product_id} } = {
            data   => $row,
            status => $match_status,
            weight => $tt->{$upc_flag}{$album_flag}{weight},
        };
    }

    return \%matches;
}

sub _get_track_matches {
    my ( $self, $data ) = @_;
    my $track_clean = $data->{track} ? clean( $data->{track} ) : undef;
    return undef unless ( $data->{isrc} or $track_clean );

    my $media_types = GetMediaTypes( $data->{format} );
    my $types = join( ',', @$media_types );
    $types =~ s/[^,]+/?/g;

    my $sql = <<EofSQL;
SELECT product_id, vendor_id as media_type, artist_name, release_date,
       company_id as upc_opt, upc, isrc, album_name, album_clean, track_clean, track_alt_clean, track_custom_2
FROM product_catalog_extract
WHERE vendor_id in($types)
  AND product_type=?
EofSQL
    my @params = ( @$media_types, $data->{product_type} );

    my %raw_results = ();
    if ( $data->{isrc} ) {
        my $sql_isrc = $sql;
        $sql_isrc .= ' AND isrc=?';
        push( @params, $data->{isrc} );
        DEBUG && print STDERR "sql: $sql_isrc\nparams: " . join( ',', @params ) . "\n";
        $self->_get_by_id( $sql_isrc, \@params, \%raw_results );
        pop(@params);
    }

    if ($track_clean) {
        $sql .= ' AND track_clean=?';
        push( @params, $track_clean );
        DEBUG && print STDERR "sql: $sql\nparams: " . join( ',', @params ) . "\n";
        $self->_get_by_id( $sql, \@params, \%raw_results );

        $sql =~ s/track_clean=/track_alt_clean=/;
        DEBUG && print STDERR "sql: $sql\nparams: " . join( ',', @params ) . "\n";
        $self->_get_by_id( $sql, \@params, \%raw_results );
    }

    my $upc = length $data->{upc} > 12 ? $data->{upc} :    # potential grid
      length $data->{upc} >= 10
      ? substr( $data->{upc}, 0, 10 )
      : undef;
    my $upc_opt =
        length $data->{client_product_id} > 10  ? $data->{client_product_id}
      : length $data->{service_product_id} > 10 ? $data->{service_product_id}
      :                                           undef;
    $upc_opt = substr( $upc_opt, 0, 10 ) if ( $upc_opt && length $upc_opt < 13 );

    my $album_clean = $data->{album} ? clean( $data->{album} ) : undef;

    my %matches = ();
    my $tt      = $self->_get_track_tt();

    # initialize all flags to NP
    my $upc_flag   = NP;
    my $album_flag = NP;
    my $isrc_flag  = NP;
    my $track_flag = NP;

    while ( my ( $id, $row ) = each %raw_results ) {
        if ( defined $upc ) {
            $upc_flag = ( $row->{upc} =~ /^$upc/ or $row->{upc_opt} eq $upc ) ? ISMATCH : NOMATCH;
        }

        if ( $upc_flag != ISMATCH and defined $upc_opt ) {

            # only override to ISMATCH otherwise leave as it was (NOMATCH or NP)
            $upc_flag = ISMATCH if ( $row->{upc} =~ /^$upc_opt/ or $row->{upc_opt} eq $upc_opt );
        }

        if ( defined $album_clean ) {
            $album_flag = $row->{album_clean} eq $album_clean ? ISMATCH : NOMATCH;

            if ( $album_flag == NOMATCH and $upc_flag == ISMATCH ) {
                $album_flag = word_containment( $data->{album}, $row->{album_name} ) ? ISMATCH : NOMATCH;
            }
        }

        if ( $data->{isrc} ) {
            $isrc_flag = $row->{isrc} eq $data->{isrc} ? ISMATCH : NOMATCH;
        }

        if ( defined $track_clean ) {
            $track_flag =
              ( $row->{track_clean} eq $track_clean || $row->{track_alt_clean} eq $track_clean ) ? ISMATCH : NOMATCH;

            if ( $track_flag == NOMATCH and $isrc_flag == ISMATCH ) {
                my ( $track_cleaned, $prod_cleaned );
                ( $track_cleaned = $track_clean ) =~ s/_//g;
                ( $prod_cleaned  = $row->{track_clean} ) =~ s/_//g;

                if ( length $track_cleaned > 2 and length $prod_cleaned > 2 ) {
                    if (   $track_cleaned eq $prod_cleaned
                        || index( $track_cleaned, $prod_cleaned ) > -1
                        || index( $prod_cleaned,  $track_cleaned ) > -1 ) {
                        $track_flag = ISMATCH;
                        DEBUG && print STDERR "cleaned track name match\n";
                    }
                }
            }
        }

        next unless exists $tt->{$upc_flag}{$album_flag}{$isrc_flag}{$track_flag};

        my $match_status = $tt->{$upc_flag}{$album_flag}{$isrc_flag}{$track_flag}{status};

        # override status to SUGGEST if upc, album, isrc are NP but artist is and no match
        $match_status = SUGGEST
          if ( $match_status == MATCH
            && $data->{artist}
            && $upc_flag == NP
            && $album_flag == NP
            && $isrc_flag == NP
            && !word_containment( $data->{artist}, $row->{artist_name} ) );

        DEBUG && print STDERR "$id $upc_flag $album_flag $isrc_flag $track_flag ($match_status) ";
        $matches{ $row->{product_id} } = {
            data   => $row,
            status => $match_status,
            weight => $tt->{$upc_flag}{$album_flag}{$isrc_flag}{$track_flag}{weight},
        };
        DEBUG && print STDERR "v: $matches{$row->{product_id}}{status} $matches{$row->{product_id}}{weight}\n";
    }

    return \%matches;
}

sub _get_by_id {
    my ( $self, $sql, $params, $result ) = @_;

    my $sth = $self->rsdb()->DoCmdParams( $sql, @$params );
    $sth->execute(@$params);
    return unless $sth->rows();

    while ( my $row = $sth->fetchrow_hashref() ) {
        DEBUG && print STDERR $row->{product_id} . ',';
        $result->{ $row->{product_id} } = $row;
    }
    DEBUG && print STDERR "\n";

    return $sth->rows();
}

sub _get_album_tt {
    my $self = shift;
    return $self->{album_tt} if ( keys %{ $self->{album_tt} } );

    my $tt = {};
    $tt->{ NP() }{ ISMATCH() }      = { status => MATCH,   weight => 1 };
    $tt->{ NOMATCH() }{ ISMATCH() } = { status => SUGGEST, weight => 0 };
    $tt->{ ISMATCH() }{ NP() }      = { status => MATCH,   weight => 1 };
    $tt->{ ISMATCH() }{ NOMATCH() } = { status => SUGGEST, weight => 0 };
    $tt->{ ISMATCH() }{ ISMATCH() } = { status => MATCH,   weight => 1 };

    $self->{album_tt} = $tt;
    return $self->{album_tt};
}

sub _get_track_tt {
    my $self = shift;
    return $self->{track_tt} if ( keys %{ $self->{track_tt} } );

    my $tt = {};

    #     UPC         Album       ISRC        Track
    $tt->{ NP() }{ NP() }{ NP() }{ ISMATCH() }                = { status => MATCH,   weight => 1 };
    $tt->{ NP() }{ NP() }{ NOMATCH() }{ ISMATCH() }           = { status => SUGGEST, weight => 0 };
    $tt->{ NP() }{ NP() }{ ISMATCH() }{ NP() }                = { status => MATCH,   weight => 1 };
    $tt->{ NP() }{ NP() }{ ISMATCH() }{ NOMATCH() }           = { status => SUGGEST, weight => 0 };
    $tt->{ NP() }{ NP() }{ ISMATCH() }{ ISMATCH() }           = { status => MATCH,   weight => 1 };
    $tt->{ NP() }{ NOMATCH() }{ NP() }{ ISMATCH() }           = { status => SUGGEST, weight => 0 };
    $tt->{ NP() }{ NOMATCH() }{ NOMATCH() }{ ISMATCH() }      = { status => SUGGEST, weight => 0 };
    $tt->{ NP() }{ NOMATCH() }{ ISMATCH() }{ NP() }           = { status => SUGGEST, weight => 0 };
    $tt->{ NP() }{ NOMATCH() }{ ISMATCH() }{ NOMATCH() }      = { status => SUGGEST, weight => 0 };
    $tt->{ NP() }{ NOMATCH() }{ ISMATCH() }{ ISMATCH() }      = { status => SUGGEST, weight => 0 };
    $tt->{ NP() }{ ISMATCH() }{ NP() }{ NOMATCH() }           = { status => SUGGEST, weight => 0 };
    $tt->{ NP() }{ ISMATCH() }{ NP() }{ ISMATCH() }           = { status => MATCH,   weight => 1 };
    $tt->{ NP() }{ ISMATCH() }{ NOMATCH() }{ ISMATCH() }      = { status => SUGGEST, weight => 0 };
    $tt->{ NP() }{ ISMATCH() }{ ISMATCH() }{ NP() }           = { status => MATCH,   weight => 1 };
    $tt->{ NP() }{ ISMATCH() }{ ISMATCH() }{ NOMATCH() }      = { status => SUGGEST, weight => 0 };
    $tt->{ NP() }{ ISMATCH() }{ ISMATCH() }{ ISMATCH() }      = { status => MATCH,   weight => 1 };
    $tt->{ NOMATCH() }{ NP() }{ NP() }{ ISMATCH() }           = { status => SUGGEST, weight => 0 };
    $tt->{ NOMATCH() }{ NP() }{ NOMATCH() }{ ISMATCH() }      = { status => SUGGEST, weight => 0 };
    $tt->{ NOMATCH() }{ NP() }{ ISMATCH() }{ NOMATCH() }      = { status => SUGGEST, weight => 0 };
    $tt->{ NOMATCH() }{ NP() }{ ISMATCH() }{ ISMATCH() }      = { status => SUGGEST, weight => 0 };
    $tt->{ NOMATCH() }{ NOMATCH() }{ NP() }{ ISMATCH() }      = { status => SUGGEST, weight => 0 };
    $tt->{ NOMATCH() }{ NOMATCH() }{ NOMATCH() }{ ISMATCH() } = { status => SUGGEST, weight => 0 };
    $tt->{ NOMATCH() }{ NOMATCH() }{ ISMATCH() }{ NP() }      = { status => SUGGEST, weight => 0 };
    $tt->{ NOMATCH() }{ NOMATCH() }{ ISMATCH() }{ NOMATCH() } = { status => SUGGEST, weight => 0 };
    $tt->{ NOMATCH() }{ NOMATCH() }{ ISMATCH() }{ ISMATCH() } = { status => SUGGEST, weight => 0 };
    $tt->{ NOMATCH() }{ ISMATCH() }{ NP() }{ NOMATCH() }      = { status => SUGGEST, weight => 0 };
    $tt->{ NOMATCH() }{ ISMATCH() }{ NP() }{ ISMATCH() }      = { status => MATCH,   weight => 1 };
    $tt->{ NOMATCH() }{ ISMATCH() }{ NOMATCH() }{ ISMATCH() } = { status => SUGGEST, weight => 0 };
    $tt->{ NOMATCH() }{ ISMATCH() }{ ISMATCH() }{ NP() }      = { status => MATCH,   weight => 1 };
    $tt->{ NOMATCH() }{ ISMATCH() }{ ISMATCH() }{ NOMATCH() } = { status => SUGGEST, weight => 0 };
    $tt->{ NOMATCH() }{ ISMATCH() }{ ISMATCH() }{ ISMATCH() } = { status => MATCH,   weight => 1 };
    $tt->{ ISMATCH() }{ NP() }{ NP() }{ ISMATCH() }           = { status => MATCH,   weight => 2 };
    $tt->{ ISMATCH() }{ NP() }{ NOMATCH() }{ ISMATCH() }      = { status => SUGGEST, weight => 0 };
    $tt->{ ISMATCH() }{ NP() }{ ISMATCH() }{ NP() }           = { status => MATCH,   weight => 2 };
    $tt->{ ISMATCH() }{ NP() }{ ISMATCH() }{ NOMATCH() }      = { status => SUGGEST, weight => 0 };
    $tt->{ ISMATCH() }{ NP() }{ ISMATCH() }{ ISMATCH() }      = { status => MATCH,   weight => 2 };
    $tt->{ ISMATCH() }{ NOMATCH() }{ NP() }{ ISMATCH() }      = { status => MATCH,   weight => 2 };
    $tt->{ ISMATCH() }{ NOMATCH() }{ NOMATCH() }{ ISMATCH() } = { status => MATCH,   weight => 1 };
    $tt->{ ISMATCH() }{ NOMATCH() }{ ISMATCH() }{ NP() }      = { status => MATCH,   weight => 2 };
    $tt->{ ISMATCH() }{ NOMATCH() }{ ISMATCH() }{ NOMATCH() } = { status => SUGGEST, weight => 0 };
    $tt->{ ISMATCH() }{ NOMATCH() }{ ISMATCH() }{ ISMATCH() } = { status => MATCH,   weight => 2 };
    $tt->{ ISMATCH() }{ ISMATCH() }{ NP() }{ ISMATCH() }      = { status => MATCH,   weight => 3 };
    $tt->{ ISMATCH() }{ ISMATCH() }{ NOMATCH() }{ ISMATCH() } = { status => MATCH,   weight => 2 };
    $tt->{ ISMATCH() }{ ISMATCH() }{ ISMATCH() }{ NP() }      = { status => MATCH,   weight => 3 };
    $tt->{ ISMATCH() }{ ISMATCH() }{ ISMATCH() }{ NOMATCH() } = { status => SUGGEST, weight => 0 };
    $tt->{ ISMATCH() }{ ISMATCH() }{ ISMATCH() }{ ISMATCH() } = { status => MATCH,   weight => 3 };

    $self->{track_tt} = $tt;
    return $self->{track_tt};
}

sub _sort_by_weight {
    my ( $self, $matches ) = @_;

    return sort { $matches->{$b}{weight} <=> $matches->{$a}{weight} } keys %$matches;
}

sub _filter_by_media_type {
    my ( $self, $matches, $format ) = @_;
    my %media_seen = ();

    while ( my ( $id, $attr ) = each %$matches ) {
        push( @{ $media_seen{ lc( $attr->{data}{media_type} ) } }, $id );
    }

    my $selected_media;
    my $media_types = GetMediaTypes($format);
    foreach my $media (@$media_types) {
        my $media_lc = lc($media);
        if ( exists $media_seen{$media_lc} ) {
            if ( $selected_media && $selected_media ne $media_lc ) {
                map { delete $matches->{$_} } @{ $media_seen{$media_lc} };
                next;
            }
            $selected_media = $media_lc;
        }
    }
}

sub _filter_by_artist_name {
    my ( $self, $sale_artist, $matches ) = @_;

    my @nomatch_list = ();
    while ( my ( $id, $attr ) = each %$matches ) {
        DEBUG && print STDERR "compare '$sale_artist' and '$attr->{data}{artist_name}'\n";
        push( @nomatch_list, $id ) unless ( word_containment( $sale_artist, $attr->{data}{artist_name} ) );
    }

    map { delete $matches->{$_} } @nomatch_list;
}

sub _filter_by_release_date {
    my ( $self, $matches ) = @_;

    my $min_date = 99991231;
    while ( my ( $id, $attr ) = each %$matches ) {
        my $date = $attr->{data}{release_date};
        $date =~ s/-//g;    # now it's just a number
        $min_date = $date if ( $date < $min_date );
    }
    DEBUG && print STDERR "min release date is $min_date\n";

    while ( my ( $id, $attr ) = each %$matches ) {
        my $date = $attr->{data}{release_date};
        $date =~ s/-//g;
        delete $matches->{$id} unless ( $date == $min_date );
    }
}

#my @recommended = keys %$matches ? $self->_sort_by_weight($matches) :
#$self->_sort_suggestions($matches, $in{data}{format}) :
sub _sort_suggestions {
    my ( $self, $sale_data, $matches ) = @_;
    my %media_seen = ();

    foreach my $id ( $self->_sort_by_weight($matches) ) {
        push( @{ $media_seen{ lc( $matches->{$id}{data}{media_type} ) } }, $id );
    }

    my @sorted_list = ();
    my $media_types = GetMediaTypes( $sale_data->{format} );
    foreach my $media (@$media_types) {
        my $media_lc = lc($media);
        next unless exists $media_seen{$media_lc};

        my @artist_nomatch = ();
        foreach my $id ( @{ $media_seen{$media_lc} } ) {
            unless ( word_containment( $sale_data->{artist}, $matches->{$id}{data}{artist_name} ) ) {
                push( @artist_nomatch, $id );
                next;
            }
            push( @sorted_list, $id );
        }
        push( @sorted_list, @artist_nomatch ) if @artist_nomatch;
    }

    return \@sorted_list;
}

sub MakeMatch {
    my $self = shift;
    my %in   = @_;
    return undef unless ( ref( $in{data} ) eq 'HASH' );
    unless ( $in{product_id} ) {
        $self->{errstr} = 'You must specify a product_id param';
        return undef;
    }

    my $data       = $in{data};
    my $product_id = $in{product_id};

    my @req_fields  = ();
    my $allow_reuse = 1;

    # If our input data does NOT meet a certain threshold,
    # do NOT allow our map entry to be re-used automatically in the future
    # (use it ONLY for this one-time manual product match)
    if ( uc( $data->{product_type} ) eq 'T' ) {
        @req_fields = @MAP_REQFIELDS_TRACK;
        if ( $data->{service_id} == Client::Service::DSP_VERIZON && $data->{format} eq File::Sale::FORMAT_WALLPAPER ) {
            $allow_reuse = 0 unless ( length $data->{upc} > 12 );
        } else {
            $allow_reuse = 0 unless ( $data->{track} && $data->{artist} );
        }
    } elsif ( uc( $data->{product_type} ) eq 'A' ) {
        @req_fields = @MAP_REQFIELDS_ALBUM;
        $allow_reuse = 0 unless ( $data->{album} && $data->{artist} );
    } elsif ( $data->{product_type} =~ /^\d+$/ )    # physical
    {
        @req_fields = @MAP_REQFIELDS_ALBUM;

        # don't allow reuse on physical matches... at least not for the present
        $allow_reuse = 0;
    } else {
        $self->{errstr} = "Unknown product type: '$data->{product_type}'";
        return undef;
    }

    map {
        unless ( exists $data->{$_} ) {
            $self->{errstr} = "missing '$_' in match hash";
            return undef;
        }
    } @req_fields;

    my $match_md5 = $self->_match_md5( data => $data ) || return undef;

    my $sql = qq{
                     INSERT INTO product_input_map 
                     (product_id, product_type, format, match_md5_new, upc, isrc, 
                      artist_name, album_name, track_name, track_num, 
                      service_id, service_product_id,
                      allow_reuse, date_created)
                     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now())
                    };

    my $sth = $self->dbh()->prepare($sql);

    unless ($sth) {
        $self->{errstr} = $self->dbh()->errstr || "Can't prepare SQL";
        return undef;
    }

    my $format = $self->_check_format_type( $data->{format} );
    unless (
        $sth->execute(
            $product_id,    $data->{product_type}, $format,             $match_md5,
            $data->{upc},   $data->{isrc},         $data->{artist},     $data->{album},
            $data->{track}, $data->{track_num},    $data->{service_id}, $data->{service_product_id},
            $allow_reuse
        )
      ) {
        $self->{errstr} = $sth->errstr;
        return undef;
    }

    my $map_id = $self->rsdb->LastInsertID();

    unless ($map_id) {
        $self->{errstr} = "Could not get the last inserted map_id";
        return undef;
    }

    return $map_id;
}

sub _search_product_input_map {
    my $self = shift;
    my $data = shift;

    my @req_fields = ();
    if ( uc( $data->{product_type} ) eq 'T' ) {
        @req_fields = @MAP_REQFIELDS_TRACK;
    } elsif ( uc( $data->{product_type} ) eq 'A' ) {
        @req_fields = @MAP_REQFIELDS_ALBUM;
    } elsif ( $data->{product_type} =~ /^\d+$/ )    # physical
    {
        @req_fields = @MAP_REQFIELDS_PHYS;
    } else {
        $self->errstr("Unknown product type: '$data->{product_type}'");
        return undef;
    }

    map { return undef unless ( exists $data->{$_} ); } @req_fields;

    ## try new maps
    my $match_md5 = $self->_match_md5( data => $data ) || return undef;
    my ( $map_id, $product_id, $dont_match ) = $self->_lookup_product_map( md5 => $match_md5, product_type => $data->{product_type} );

    unless ( $map_id && $product_id ) {
        ## no match found using new-style maps so check old ones
        $match_md5 = $self->_match_md5( data => $data, old_map => 1 ) || return undef;
        ( $map_id, $product_id, $dont_match ) =
          $self->_lookup_product_map( md5 => $match_md5, product_type => $data->{product_type}, old_map => 1 );
    }
    return undef unless ( $map_id && defined $product_id );

    my $import_status = $dont_match ? Sale::Match::STATUS_DONT_MATCH : $self->{map_type};

    return Sale::Match::Result->new(
        products        => [$product_id],
        num_products    => 1,
        import_status   => $import_status,
        map_id          => $map_id,
        rec_products    => [],
        search_products => [],
        detail          => {$product_id} => {
            level   => 100,
            type    => 'map',
            pattern => undef
        }
    );
}

sub _match_md5 {
    my $self      = shift;
    my %in        = @_;
    my $data      = $in{data};
    my $old_style = $in{old_map} || 0;

    my %args = (
        product_type       => uc $data->{product_type},
        format             => uc $self->_check_format_type( $data->{format} ),
        isrc               => uc $data->{isrc},
        upc                => uc $data->{upc},
        artist             => lc $data->{artist},
        album              => lc $data->{album},
        track              => lc $data->{track},
        track_num          => lc $data->{track_num},
        service_id         => lc $data->{service_id},
        service_product_id => lc $data->{service_product_id},
    );

    my $match_md5_string = undef;
    my @match_fields     = ();

    if ( $args{product_type} eq 'A' ) {
        @match_fields = qw(product_type format upc artist album);
        push( @match_fields, qw(service_id service_product_id) ) unless $old_style;
    } elsif ( $args{product_type} eq 'T' ) {
        @match_fields = qw(product_type format isrc upc artist album track);
        push( @match_fields, qw(track_num service_id service_product_id) ) unless $old_style;
    } elsif ( $args{product_type} =~ m/^\d$/ )    # physical
    {
        @match_fields = qw(product_type format upc artist album);
    }

    my $string = '';
    map { $string .= "$_=$args{$_};" } @match_fields;
    $string =~ s/;$//;

    return Digest::MD5::md5_base64($string);
}

sub _lookup_product_map {
    my $self      = shift;
    my %in        = @_;
    my $md5_field = 'match_md5_new';
    $md5_field = 'match_md5' if $in{old_map};
    my $sql =
      qq{SELECT map_id, product_id, dont_match FROM product_input_map WHERE $md5_field = ? AND product_type = ? AND allow_reuse = 1};

    my $sth = $self->dbh()->prepare($sql);
    unless ($sth) {
        $self->{errstr} = $self->dbh()->errstr || "Can't prepare SQL: $sql";
        return undef;
    }

    unless ( $sth->execute( $in{md5}, $in{product_type} ) ) {
        $self->{errstr} = $sth->errstr;
        return undef;
    }

    return ( $sth->fetchrow_array() );
}

1;
