package BookPub::RDAS::URL::ProductPrice;

use Carp;
use URI::Escape;
use Data::Dumper;

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

use lib '/app/tools/bookpub/lib';
use BookPub::DB::Item::Product;
use BookPub::DB::Item::ProductServiceUrl;
use BookPub::Catalog::Book::Title;
use BookPub::Catalog::Book::WithAuthor;
use BookPub::Catalog::Product::Book::WithProductMonitoring;
use BookPub::Catalog::ProductFormat;
use BookPub::Catalog::Author;

use base 'BookPub::RDAS::URL';

sub GetURLObject {
    my $class     = shift;
    my $serviceID = shift;
    my %args      = @_;

    my %classes = (
        BookPub::Tracker::Service::APPLE()        => BookPub::RDAS::URL::ProductPrice::Apple,
        BookPub::Tracker::Service::AMAZON()       => BookPub::RDAS::URL::ProductPrice::Amazon,
        BookPub::Tracker::Service::BARNES_NOBLE() => BookPub::RDAS::URL::ProductPrice::BarnesNoble,
        BookPub::Tracker::Service::BORDERS()      => BookPub::RDAS::URL::ProductPrice::Borders,
        BookPub::Tracker::Service::SONY()         => BookPub::RDAS::URL::ProductPrice::Sony,
        BookPub::Tracker::Service::GOOGLE()       => BookPub::RDAS::URL::ProductPrice::Google,
    );

    my $type = $classes{"$serviceID"};

    die "No object defined for service $serviceID" unless ($type);

    eval "use $type";
    die "$@" if ($@);

    return $type->new(%args);
}

sub _searchURL {
    my $self = shift;
    my $product = shift || return $self->_ebookSearchURL();
    return $product->isEBook() ? $self->_ebookSearchURL() : $self->_physicalSearchURL();
}

sub _ebookSearchURL    { die "Must be overloaded" }
sub _physicalSearchURL { die "Must be overloaded" }

sub _init {
    my $self = shift;
    $self->SUPER::_init(@_);

    assert( $self->productID || $self->bookID );

    $self->_lookupURL();

    return $self;
}

sub _fetchURL {
    my $self = shift;
    my %args = @_;
    my $product;

    my $url = $self->{_url};
    return $url if ($url);

    my @productIDs = $self->_getProductIDs();

    die "No products found" unless (@productIDs);
    Log->notice("Search for URL for product @productIDs");

    Log->notice(" ++ Search for URL using ISBN(s)");

    # First try and generate the URL with the ISBN.  This is the only option for
    # Physical products.
    foreach my $productID (@productIDs) {
        $product = new BookPub::Catalog::Product::Book( productID => $productID );
        die "unable to find product id $productID " unless ( $product && $product->ProductID );

        if ( $product->isPublished() ) {
            Log->notice( " -- Searching using product id " . $product->ProductID );
            $url = $self->_generateWithISBN( product => $product );
            if ($url) {
                Log->info("Found URL: $url");
                last;
            }
        } else {
            Log->info( "  --  Ignore product id " . $product->ProductID . " base on published status" );
        }
    }

    # If we couldn't find it with the ISBN, then let's search by name.  This
    # is only for ebooks.
    unless ($url) {
        if ( $product->isEBook ) {
            $url = $self->_generateWithName( productIDs => \@productIDs );
        }
    }

    return $url;
}

sub _allowNameMatch { }

sub serviceID { croak "Must be overloaded" }

sub _getProductIDs {
    my $self = shift;

    assert( $self->productID || $self->bookID );

    return $self->productID if ( $self->productID );

    my @results;

    my $collection =
      BookPub::DB::Item::BookProduct->GetAllWithLimit( bookID => $self->bookID, ebookOnly => 1, serviceID => $self->serviceID );
    while ( my $dbItem = $collection->next() ) {
        push( @results, $dbItem->product_id() );
    }

    return @results;
}

sub _lookupURL {
    my $self = shift;

    assert( $self->productID() || $self->bookID() );
    assert( $self->serviceID() );

    $self->{_url} = undef;

    my $dbitem = BookPub::DB::Item::ProductServiceUrl->FindURL(
        bookID    => $self->bookID,
        productID => $self->productID,
        serviceID => $self->serviceID()
    );

    if ($dbitem) {
        $self->{_url}       = $dbitem->url();
        $self->{_productID} = $dbitem->product_id();
    }

    return $self->{_url};
}

sub invalidate {
    my $self = shift;

    assert( $self->productID() || $self->bookID() );
    assert( $self->serviceID() );

    $self->{_url} = undef;

    my $dbitem = BookPub::DB::Item::ProductServiceUrl->InvalidateURLs(
        bookID    => $self->bookID,
        productID => $self->productID,
        serviceID => $self->serviceID()
    );
}

sub storedURL {
    my $self = shift;

    assert( $self->productID() || $self->bookID() );
    assert( $self->serviceID() );

    my $dbitem = BookPub::DB::Item::ProductServiceUrl->FindURL(
        bookID    => $self->bookID,
        productID => $self->productID,
        serviceID => $self->serviceID()
    );

    return $dbitem ? $dbitem->url() : undef;
}

sub _generateURL {
    Log->notice("_generateURL not overloaded.  NoOp.");
}

sub _storeURL {
    my $self        = shift;
    my %args        = @_;
    my $url         = $args{url} || return;
    my $productID   = $args{productID};
    my $invalid     = $args{invalid};
    my $note        = $args{note};
    my $noOverwrite = $args{noOverwrite};

    assert($productID);

    $self->{_productID} = $productID;

    Log->notice("Save URL to the product");

    assert($productID);

    my $dbItem = BookPub::DB::Item::ProductServiceUrl->FindRecent(
        invalid   => $invalid,
        url       => $url,
        productID => $productID,
        serviceID => $self->serviceID
    );
    if ($dbItem) {
        if ( $note && !$noOverwrite ) {
            $dbItem->note($note);
        }

        $dbItem->invalid($invalid);
        $dbItem->save();

        #Log->notice( " -- URL Save Aborted, found duplicate" );
        return $dbItem->invalid ? undef : 1;
    }

    $dbItem = BookPub::DB::Item::ProductServiceUrl->Create();

    $dbItem->product_id( $self->productID );
    $dbItem->service_id( $self->serviceID );
    $dbItem->url($url);
    $dbItem->generated(1);
    $dbItem->invalid(1)  if ($invalid);
    $dbItem->note($note) if ($note);

    $dbItem->save;

    if ( defined( $dbItem->invalid ) && $dbItem->invalid == 1 ) {
        Log->info(" URL storage failed.");
        return;
    } else {
        Log->info(" URL storage success.");
        return 1;
    }
}

sub replaceURL {
    my $self = shift;
    my $url  = shift;

    assert($url);

    my $dbItem = BookPub::DB::Item::ProductServiceUrl->Lookup( product_id => $self->productID, service_id => $self->serviceID );
    $dbItem = BookPub::DB::Item::ProductServiceUrl->Create() unless ($dbItem);

    $dbItem->product_id( $self->productID );
    $dbItem->service_id( $self->serviceID );
    $dbItem->url($url);
    $dbItem->generated(1);

    $dbItem->save();
}

sub _searchISBN13 {
    my $self = shift;
    my $product = shift || die;

    return $product->ISBN13();
}

sub _searchISBN10 {
    my $self = shift;
    my $product = shift || die;

    return $product->ISBN10();
}

sub _generateWithISBN {
    my $self    = shift;
    my %args    = @_;
    my $product = $args{product} || return;
    my $note    = undef;
    my $result;

    my $search = $self->_fetchWithISBN13( product => $product )
      || $self->_fetchWithISBN10( product => $product );

    return unless ( $search && $search->URL );

    my $valid = $self->_validateFromISBN( request => $search, product => $product );

    if ( $valid && $valid == 2 ) {
        $note = "Mapped from product_input_map";
    } elsif ( !$valid ) {
        $self->{_ISBNMatchedProduct} = $productID;
        Log->notice(" -- Found URL, but failed ISBN validation");
        $result = $self->_parseURL( product => $product, url => $search->URL );
        $self->_storeURL( url => $result, productID => $product->ProductID, invalid => 1, note => "ISBN search validation failed" );
        return;
    }

    $result = $self->_parseURL( product => $product, url => $search->URL );
    return $result if ( $self->_storeURL( url => $result, productID => $product->ProductID, note => $note ) );

    return;
}

sub _fetchWithISBN10 {
    my $self    = shift;
    my %args    = @_;
    my $product = $args{product};

    assert($product);

    my $isbn = $self->_searchISBN10($product) || return;

    my $url = $self->_buildURL( $self->_searchURL($product), $isbn );

    my $search = $self->_searchRequestObject( url => $url );
    $search->run();

    return $search->URL ? $search : undef;
}

sub _fetchWithISBN13 {
    my $self    = shift;
    my %args    = @_;
    my $product = $args{product};

    assert($product);

    my $isbn = $self->_searchISBN13($product) || return;

    my $url = $self->_buildURL( $self->_searchURL($product), $isbn );

    my $search = $self->_searchRequestObject( url => $url );
    $search->run();

    return $search->URL ? $search : undef;
}

sub _parseURL {
    my $self = shift;
    my %args = @_;

    return $args{url};
}

sub _searchName {
    my $self = shift;
    my $book = shift || die;

    my $result = $book->Title();
    $result .= ' ' . $book->AuthorName();

    return $result;
}

sub _generateWithName {
    my $self       = shift;
    my %args       = @_;
    my $productIDs = $args{productIDs};
    my $result;
    my $note = "Name match found";

    assert( $self->bookID );
    assert($productIDs);

    $productIDs = [$productIDs] unless ( ref($productIDs) );

    my $product = $self->_getDefaultProductMatch( possibleIDs => $productIDs );
    assert($product);

    $self->{_productID} = $product->ProductID;

    my $book = new BookPub::Catalog::Book::WithAuthor( bookID => $self->bookID );
    return unless ( $book->Title );

    return unless ( $self->_searchName($book) );

    Log->notice(" ++ Search for URL using TITLE");

    my $url = $self->_buildURL( $self->_searchURL($product), $self->_searchName($book) );

    $search = $self->_searchRequestObject( url => $url, bookID => $self->bookID );
    $search->run();

    return unless ( $search->URL() );

    $result = $self->_parseURL( url => $search->URL );

    $product =
        $self->_allowNameMatch()
      ? $self->_getProductMatch( possibleIDs => $productIDs, parser => $search, book => $book, url => $search->URL )
      : $self->_getDefaultProductMatch( possibleIDs => $productIDs );

    my $invalid = $self->_allowNameMatch() ? undef : 1;

    if ( !$product ) {
        $product = $self->_getDefaultProductMatch( possibleIDs => $productIDs );
        $self->{_productID} = $product->ProductID;
        $note = "No product match found";
        $self->_storeURL( url => $result, productID => $product->ProductID, invalid => 1, note => "Name search failed", noOverwrite => 1 );
        return;
        $invalid = 1;
    } elsif ( $result && !$self->_validateFromName( request => $search, product => $product ) ) {
        Log->notice(" -- Found URL, but failed Name validation");
        $self->_storeURL( url => $result, productID => $product->ProductID, invalid => 1, note => "Name search validation failed" );
        return;
    }

    $self->_storeURL( url => $result, productID => $product->ProductID, invalid => $invalid, note => $note );
    return $result;
}

sub _getProductMatch {
    my $self = shift;
    my %args = @_;

    my $parser     = $args{parser};
    my $book       = $args{book};
    my $productIDs = $args{possibleIDs};
    my $url        = $args{url};

    my $highPrice;
    my $resultProduct;

    assert($parser);
    assert($book);
    assert($productIDs);

    my $subtypeMismatch = [];

    $productIDs = [$productIDs] unless ( ref($productIDs) );

    # We only do product matching for ebooks because physical uses ISBNs to match
    return unless ( $self->_ebookFormatPriority );

    Log->info(" - attempting to find a product match using priority");

    foreach my $onixcode ( $self->_ebookFormatPriority ) {
        Log->debug(" - Looking for product match on onix code '$onixcode'");
        foreach my $productID (@$productIDs) {
            my $product = new BookPub::Catalog::Product::Book::WithPricing( productID => $productID );
            next unless ( $product && $product->ProductID );

            # Check the publisher status.  If it's not publised skip it
            unless ( $product->isPublished() ) {
                Log->info( "  -- ignoring unpublished product id " . $product->ProductID );
                next;
            }

            my $format = new BookPub::Catalog::ProductFormat( bookFormatID => $product->BookFormatID );

            if ( $format->isSubFormat($onixcode) ) {
                my $catalogPrice = $self->_getCatalogPrice($product);
                my $servicePriceTypeID = $self->_getServicePriceTypeID( $product, $url );

                if ( defined($servicePriceTypeID) && $catalogPrice->PriceTypeID != $servicePriceTypeID ) {
                    Log->info( "  -- subtype match, but price type incorrect: " . $catalogPrice->PriceTypeID . " != $servicePriceTypeID" );
                    push( @$subtypeMismatch, { url => $url, productID => $productID } );
                } else {

                    # We want to pick the higher priced product.  This is because we were getting multiple products returned
                    # for a price type and it was generally because they had a free product (sample) and a paid product.
                    Log->info( "  + product match: $productID price: " . $catalogPrice->Price );

                    if ( !defined($highPrice) || $catalogPrice->Price > $highPrice ) {
                        Log->info("  -- Higher price found.  Storing product") if ( defined($highPrice) );
                        $highPrice     = $catalogPrice->Price;
                        $resultProduct = $product;
                    }
                }
            } else {
                my $subformat = $format->{BookFormatSubtype}{Code} || '';
                Log->info( "   Sub format " . $subformat . " didn't match $onixcode" );
            }
        }
    }

    unless ($resultProduct) {
        Log->info("  --- No product match found --- ");
        $self->_handleMismatchErrors($subtypeMismatch);
    }

    return $resultProduct;
}

sub _handleMismatchErrors { }

sub _getCatalogPrice {
    my $self = shift;
    my $product = shift || return;

    my $price = $product->getPriceByCurrency('USD');

    return $price;
}

sub _getServicePriceTypeID { }

sub _ebookFormatPriority { }

sub _getDefaultProductMatch {
    my $self = shift;
    my %args = @_;

    my $productIDs = $args{possibleIDs};

    assert($productIDs);

    $productIDs = [$productIDs] unless ( ref($productIDs) );

    foreach my $id (@$productIDs) {
        my $product = new BookPub::Catalog::Product::Book( productID => $id );
        return $product if ( $product && $product->ProductID && $product->isPublished );
    }

    return;
}

sub _validateFromISBN {
    my $self    = shift;
    my %args    = @_;
    my $request = $args{request};
    my $product = $args{product};

    assert($request);
    assert($product);

    Log->notice(" ++ Start ISBN validation");

    return unless ( $self->_validateValidURL( url => $request->URL, product => $product ) );

    my $mapped = $self->_validateTitleMap( request => $request, product => $product );

    return unless ( $mapped || $self->_validateTitleMatch( request => $request, product => $product, contains => 1 ) );

    return $mapped ? 2 : 1;
}

sub _validateFromName {
    my $self    = shift;
    my %args    = @_;
    my $request = $args{request};
    my $product = $args{product};

    assert($request);
    assert($product);

    Log->notice(" ++ Start Name validation");

    return unless ( $self->_validateValidURL( url => $request->URL, product => $product ) );

    return unless ( $self->_validateTitleMatch( request => $request, product => $product, contains => 1 ) );
    return unless ( $self->_validateAuthorMatch( request => $request, product => $product ) );

    return 1;
}

sub _validateValidURL {
    my $self    = shift;
    my %args    = @_;
    my $url     = $args{url};
    my $product = $args{product};

    assert($url);
    assert($product);

    return 1;    # Don't do url validation for now.
}

# Check to see if we have an entry in the product input map table for this title
sub _validateTitleMap {
    my $self    = shift;
    my %args    = @_;
    my $request = $args{request};
    my $product = $args{product};

    Log->info("   Validate fetched title matches against product input map records");

    my $hasSubtitle = $request->can('Subtitle');

    my $fetchedTitle = $request->Title;
    my $fetchedSubtitle = $request->Subtitle if ($hasSubtitle);

    my $fetched = new BookPub::Catalog::Book::Title( title => $fetchedTitle, subtitle => $fetchedSubtitle );

    assert($request);
    assert($product);

    # First just look for the title in the map.
    my $dbitem = BookPub::DB::Item::ProductInputMap->Lookup(
        isbn13      => $product->ISBN13,
        product_id  => $product->ProductID,
        title       => $fetched->Title(),
        allow_reuse => 1
    ) if ( $fetched->Title() );

    # Then try the combined title and subtitle
    $dbitem = BookPub::DB::Item::ProductInputMap->Lookup(
        isbn13      => $product->ISBN13,
        product_id  => $product->ProductID,
        title       => $fetched->Combined(),
        allow_reuse => 1
    ) if ( $fetched->Combined() && !$dbitem );

    unless ($dbitem) {
        Log->info("   -- No title match in the product map table");
        return;
    }

    Log->info("   -- FOUND product input map!");
    return 1;
}

sub _validateTitleMatch {
    my $self    = shift;
    my %args    = @_;
    my $request = $args{request};
    my $product = $args{product};

    # Check for a subset match regardless of word order
    my $containSearch = $args{contains};

    assert($request);
    assert($product);

    my $hasSubtitle = $request->can('Subtitle');

    my $book = new BookPub::Catalog::Book( bookID => $product->BookID );

    my $fetchedTitle = $request->Title;
    my $fetchedSubtitle = $request->Subtitle if ($hasSubtitle);

    my $catalogTitle = $book->Title;
    my $catalogSubtitle = $book->Subtitle if ($hasSubtitle);

    Log->info("   Validate fetched title matches catalog title");

    if ( !defined($fetchedTitle) ) {
        Log->info("   -- Undetermined title from request object");
        return;
    }

    if ( !defined($catalogTitle) ) {
        Log->info("   -- Undetermined title from catalog");
        return;
    }

    my $fetched = new BookPub::Catalog::Book::Title( title => $fetchedTitle, subtitle => $fetchedSubtitle );
    my $catalog = new BookPub::Catalog::Book::Title( title => $catalogTitle, subtitle => $catalogSubtitle );
    my $parsed  = new BookPub::Catalog::Book::Title( parse => $fetchedTitle );

    #die $parsed->Title . " eq " . $catalog->Title;
    unless ( $fetched->fuzzyMatch( title => $catalog->Title, subtitle => $catalog->Subtitle )
        || $parsed->fuzzyMatch( title => $catalog->Title, subtitle => $catalog->Subtitle )
        || $parsed->titleFuzzyMatch( title => $catalog->Title )
        || ( $containSearch && $fetched->contains( title => $catalog->Title, subtitle => $catalog->Subtitle ) ) ) {
        Log->info( "   -- Title match failed. Fetched: \"" . $fetched->Combined . "\" Catalog: \"" . $catalog->Combined . "\"" );
        return;
    }

    Log->info("   -- Title match success!");
    return 1;
}

sub _validateAuthorMatch {
    my $self    = shift;
    my %args    = @_;
    my $request = $args{request};
    my $product = $args{product};

    assert($request);
    assert($product);

    # Only validate author if we can parse an author name
    return 1 unless ( $request->can('Author') );

    my $book = new BookPub::Catalog::Book::WithAuthor( bookID => $product->BookID );

    Log->info("   Validate fetched author matches catalog author");

    my $fetched = new BookPub::Catalog::Author( $request->Author );
    my $catalog = new BookPub::Catalog::Author( $book->AuthorName );

    if ( !defined( $fetched->Author ) ) {
        Log->info("   -- Undetermined author from request object");
        return;
    }

    if ( !defined( $catalog->Author ) ) {
        Log->info("   -- Undetermined author from catalog");
        return;
    }

    unless ( $fetched->fuzzyMatch( $catalog->Author ) ) {
        Log->info( "   -- Author match failed. Fetched: \"" . $fetched->Author . "\" Catalog: \"" . $catalog->Author . "\"" );
        return;
    }

    Log->info( "Fetched Author: " . $fetched );
    Log->info( "Catalog Author: " . $fetched );

    return 1;
}

sub _buildURL {
    my $self   = shift;
    my $url    = shift;
    my $search = shift;

    assert($url);
    assert($search);

    $url .= '%s' unless ( $url =~ /%s/ );

    return sprintf( $url, uri_escape($search) );
}

sub _clearCache {
    my $self = shift;
    BookPub::DB::Item::ProductServiceUrl->InvalidateURLs(
        bookID    => $self->bookID,
        productID => $self->productID,
        serviceID => $self->serviceID()
    );
}

1;
