package RPS::Sale::Match::WMG;

use strict;
use base 'RPS::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 RPS::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 ? RPS::Sale::Match::STATUS_MATCH :
            $num_products == 0 ? RPS::Sale::Match::STATUS_NOMATCH :
            $num_products  > 1 ? RPS::Sale::Match::STATUS_MULTIMATCH :
            '';

	    $result = new RPS::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 RPS::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 ? RPS::Sale::Match::STATUS_DONT_MATCH : $self->{map_type};

    return RPS::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;
