package BookPub::RDAS::Request::ProductPrice::Apple;

use Data::Dumper;

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

use lib '/app/tools/rdas/lib';
use RDAS::Fetcher::Mechanize::Authenticated;

use lib '/app/tools/bookpub/lib';
use BookPub::ServiceCredential;
use BookPub::Tracker::Service;
use BookPub::Catalog::Product::Book;
use BookPub::RDAS::Parser::ProductPrice::Apple;
use BookPub::RDAS::URL::ProductPrice::Apple;
use BookPub::RDAS::Response::ProductPrice::Apple;

use BookPub::RDAS::Parser::ProductPrice::Apple::Home;
use BookPub::RDAS::Parser::ProductPrice::Apple::Search;
use BookPub::RDAS::Parser::ProductPrice::Apple::Product;
use BookPub::RDAS::Parser::ProductPrice::Apple::Availability;
use BookPub::RDAS::Parser::ProductPrice::Apple::ProductPrice;
use BookPub::RDAS::Parser::ProductPrice::Apple::ProductMetadata;

use base 'BookPub::RDAS::Request::ProductPrice';

use constant kMaxRetries => 3;

###
#   Apple is special because it is an authenticated service and it takes 6
#   page requests to fetch the information.
###

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

    $self->{_bookID}    = $args{bookID};
    $self->{_productID} = $args{productID};

    return $self;
}

sub run {
    my $self = shift;

    if ( $self->fetch(@_) ) {
        $self->respond();
    }
}

sub error { shift->{_error} }

sub fetch {
    my $self = shift;

    my @productIDs = $self->_getProductIDs();
    foreach my $id (@productIDs) {
        $self->{_productID} = $id;
        return 1 if ( $self->_simpleFetch() || $self->_fullFetch() );
    }

    return;
}

# Get the product IDs we need to search.  First we search URLs to find a match.
# Otherwise we will grab all digital products for the book.
sub _getProductIDs {
    my $self = shift;
    my $url  = $self->_getMatchedProducts();

    if ($url) {
        return ( $url->productID() );
    } elsif ( @ids == 0 ) {
        return $self->_getBookProducts();
    } else {
        die "Multiple URL matches found for book id " . $self->bookID();
    }
}

sub _getMatchedProducts {
    my $self      = shift;
    my $serviceID = BookPub::RDAS::Response::ProductPrice::Apple->serviceID();
    my $url       = new BookPub::RDAS::URL::ProductPrice::Apple( bookID => $self->bookID, serviceID => $serviceID );
    return $url->url ? $url : undef;
}

sub _getBookProducts {
    my $self = shift;
    my @results;
    my $serviceID = BookPub::RDAS::Response::ProductPrice::Apple->serviceID();

    assert( $self->bookID );

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

    return @results;
}

# Just pull info from the product page, no connect login
sub _simpleFetch {
    my $self      = shift;
    my $serviceID = BookPub::RDAS::Response::ProductPrice::Apple->serviceID();
    my $currentMetadata =
      BookPub::DB::Item::ProductMonitorItemBookMetadata->GetCurrent( product_id => $self->productID, service_id => $serviceID );

    # Ensure we have done one full pull of the metadata
    return unless ($currentMetadata);

    # Check to see if we have a stored product page.  If we don't bail and do a full pull
    my $productBase = BookPub::RDAS::URL::ProductPrice::Apple->ProductPageURL();
    my $url = new BookPub::RDAS::URL::ProductPrice::Apple( productID => $self->productID, serviceID => $serviceID );
    return unless ( $url && $url->url && $url->url =~ /$productBase/ );

    $self->{_parser}->{metadata} = $self->_productDataPage( $url->url ) || return;

    # If we see an expected release date then it is a preorder and we have to do a full pull
    return $self->parser->{metadata}->ExpectedReleaseDate() ? undef : 1;
}

# Full connect login
sub _fullFetch {

    # Disable warnings coming from HTTP::Cookie.
    $^W = 0;

    my $self    = shift;
    my $success = 0;
    $self->{_error} = undef;

    Log->info( "  - Start _fullFetch for product id " . $self->productID );

    for ( my $retry = 1 ; $retry <= kMaxRetries && !$success && !$self->error() ; $retry++ ) {
        $self->{_parser} = {};

        unless ( $self->{_parser}->{home} = $self->loginRequired ? $self->_loginHomePage : $self->_fetchHomePage ) {
            Log->info( "Failed to fetch homepage.  Retry ( $retry of " . kMaxRetries . ")" );
            die "Authentication failed" if ( $retry > 1 );
            next;
        }

        $self->{_parser}->{search} = $self->_searchPage;

        # If we found a match, but the status is wrong then just bail on this product.
        if ( $self->parser->{search} && !ref( $self->parser->{search} ) ) {
            return;
        }

        # If we have no match then this was a failed request.
        elsif ( !defined( $self->parser->{search} ) ) {
            Log->info( "Failed to fetch search.  Retry ( $retry of " . kMaxRetries . ")" );
            next;
        }

        unless ( $self->{_parser}->{product} = $self->_productPage ) {
            Log->info( "Failed to fetch product page.  Retry ( $retry of " . kMaxRetries . ")" );
            next;
        }

        unless ( $self->{_parser}->{availability} = $self->_productAvailability ) {
            Log->info( "Failed to fetch product availability.  Retry ( $retry of " . kMaxRetries . ")" );
            last;
        }

        if ( $self->parser->{product}->available ) {
            unless ( $self->{_parser}->{metadata} = $self->_productDataPage ) {
                Log->info( "Failed to fetch product page data.  Retry ( $retry of " . kMaxRetries . ")" );
                next;
            }
            my $price = $self->parser->{metadata}->ProductPrice();

            # Only pull the price from connect if we couldn't get it from the metadata page
            if ( !$price ) {
                unless ( $self->{_parser}->{price} = $self->_productPricePage ) {
                    Log->info( "Failed to fetch product page data.  Retry ( $retry of " . kMaxRetries . ")" );
                    next;
                }
            }
        }

        $success = 1;
    }

    unless ($success) {
        $self->{_error} = "Product not available on service" unless ( $self->error );
        Log->info( "Failed to fetch data for product id: " . $self->productID );
        Log->info( "ERROR: " . $self->error() ) if ( $self->error );
        $self->fail( failure => $self->error );
    }

    return $success;
}

sub _getFetchObject {
    my $self = shift;
    my $url  = shift || die;
    my $ua   = $self->userAgent;

    return RDAS::Fetcher::Mechanize::Authenticated->new(
        url       => $url,
        userAgent => $ua,
    );
}

sub _getURLObject {
    my $self = shift;

    $self->{_urlObj} = new BookPub::RDAS::URL::ProductPrice::Apple( productID => $self->productID(), @_ )
      unless ( $self->{_urlObj} );

    return $self->{_urlObj};
}

sub _getResponseObject {
    my $self = shift;
    return new BookPub::RDAS::Response::ProductPrice::Apple( parser => $self->parser, productID => $self->productID, @_ );
}

sub _credentials {
    my $self = shift;

    unless ( $self->{_credentials} ) {
        my $serviceID = BookPub::RDAS::Response::ProductPrice::Apple->serviceID();
        my $cred = new BookPub::ServiceCredential( serviceID => $serviceID );
        if ( $cred && $cred->Username && $cred->Password ) {
            $self->{_credentials} = [ $cred->Username, $cred->Password ];
        }
    }

    if ( $self->{_credentials} ) {
        return @{ $self->{_credentials} };
    } else {
        die "No login credentials";
    }
}

sub loginRequired { shift->{_loginRequired} }

sub _isLoginPage {
    my $self = shift;
    my $parser = shift || die;

    if ( $parser->Login ) {
        $self->{_loginRequired} = 1;
        Log->info("Not authenticated!");
        return 1;
    }

    $self->{_loginRequired} = undef;

    return;
}

sub _loginHomePage {
    my $self = shift;
    my ( $uname, $passwd ) = $self->_credentials();
    my $credentials = { theAccountName => $uname, theAccountPW => $passwd };

    Log->info(" + Login to homepage");

    assert( $uname,  "Username required" );
    assert( $passwd, "Password required" );

    return $self->_fetchHomePage($credentials);
}

sub _fetchHomePage {
    my $self        = shift;
    my $credentials = shift;

    Log->info(" + Fetch homepage");
    my $fetcher = $self->_fetchPage( BookPub::RDAS::URL::ProductPrice::Apple->SiteLoginURL(), form => $credentials ) || return;

    my $parser = new BookPub::RDAS::Parser::ProductPrice::Apple::Home(
        content => $fetcher->content(),
        links   => $fetcher->links()
    );

    return $self->_isLoginPage($parser) ? undef : $parser;
}

sub _searchPage {
    my $self     = shift;
    my $parsers  = $self->parser;
    my $homepage = $parsers->{home};
    my $product  = new BookPub::Catalog::Product::Book( productID => $self->productID );

    Log->info(" + Fetch search page");
    my $link = $homepage->link( text => 'Manage Your Books' );

    $url = BookPub::RDAS::URL::ProductPrice::Apple->SiteURL . $link->url;

    my $fetcher = $self->_fetchPage($url) || return;

    my $form = $fetcher->form(2);
    my @inputs = $form->inputs() if ($form);
    my $isbnInput;
    my $submitButton;

    foreach my $input (@inputs) {

        # For some reason the input field for ISBN has the "Language :" value.  Blame the form parser
        if ( $input->type() eq 'text' && $input->{value_name} && $input->{value_name} =~ /Language/ ) {
            $isbnInput = $input;
        } elsif ( $input->type() eq 'submit' && $input->{value_name} && $input->{value_name} =~ /Search Multiple Apple ID/ ) {
            $submitButton = $input;
            Log->debug( Dumper $input );
        }
    }

    unless ($isbnInput) {
        Log->error("Could not find ISBN input.");
        return;
    }

    unless ($submitButton) {
        Log->error("Could not find Submit Button.");
        return;
    }

    Log->notice( "Search ISBN: " . $product->ISBN13() );
    $isbnInput->value( $product->ISBN13 );
    unless ( $fetcher->submit( form => { $isbnInput->name => $isbnInput->value }, submit => $submitButton->name(), form_number => 2 ) ) {
        $self->{_error} = "Fetch failed.  HTTP Code: " . $fetcher->Code();
        return;
    }

    my $parser = new BookPub::RDAS::Parser::ProductPrice::Apple::Search( content => $fetcher->content() );

    if ( !defined( $parser->ResultCount() ) ) {
        $self->{_error} = "No match found for ISBN: " . $product->ISBN13();
        return;
    } elsif ( $parser->ResultCount != 1 ) {
        $self->{_error} = "Multiple matchs found for ISBN: " . $product->ISBN13();
        return;
    }

    return $self->_isLoginPage($parser) ? undef : $parser;
}

sub _productPage {
    my $self    = shift;
    my $parsers = $self->parser;
    my $search  = $parsers->{search};
    my $link    = $search->URL;

    Log->info(" + Fetch product page");

    unless ($link) {
        $self->{_error} = "No product page URL found.";
        return;
    }

    $url = BookPub::RDAS::URL::ProductPrice::Apple->SiteURL . $link;

    my $fetcher = $self->_fetchPage($url) || return;

    my $parser = new BookPub::RDAS::Parser::ProductPrice::Apple::Product(
        content => $fetcher->content(),
        links   => $fetcher->links()
    );

    return undef if ( $self->_isLoginPage($parser) );
    return $parser;
}

sub _productAvailability {
    my $self    = shift;
    my $parsers = $self->parser;
    my $url     = $parsers->{product}->StatusURL;

    $url = BookPub::RDAS::URL::ProductPrice::Apple->SiteURL() . "$url" if ($url);

    Log->info(" + Fetch product availability");

    unless ($url) {
        $self->{_error} = "No product availability URL found.";
        return;
    }
    my $fetcher = $self->_fetchPage($url) || return;

    my $parser = new BookPub::RDAS::Parser::ProductPrice::Apple::Availability( content => $fetcher->content() );

    return $parser->Available ? $parser : undef;
}

sub _productDataPage {
    my $self    = shift;
    my $parsers = $self->parser;
    my $product = $parsers->{product};
    my $url     = shift || BookPub::RDAS::URL::ProductPrice::Apple->ProductPageURL() . $product->AppleID;

    Log->info(" + Fetch public product page");

    my $fetcher = $self->_fetchPage($url) || return;

    my $parser = new BookPub::RDAS::Parser::ProductPrice::Apple::ProductMetadata( content => $fetcher->content() );

    return $self->_isLoginPage($parser) ? undef : $parser;
}

sub _productPricePage {
    my $self    = shift;
    my $parsers = $self->parser;
    my $product = $parsers->{product};
    my $link    = $product->link( text => 'Edit Rights and Pricing' );

    Log->info(" + Fetch product price page");

    unless ($link) {
        $self->{_error} = "No product page URL found.";
        return;
    }

    $url = BookPub::RDAS::URL::ProductPrice::Apple->SiteURL . $link->url;

    my $fetcher = $self->_fetchPage($url) || return;

    my $parser = new BookPub::RDAS::Parser::ProductPrice::Apple::ProductPrice( content => $fetcher->content() );

    return $self->_isLoginPage($parser) ? undef : $parser;
}

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

    my $fetcher = $self->_getFetchObject($url);

    if ( $fetcher->fetch(%args) ) {
        return $fetcher;
    } else {
        $self->{_error} = "Fetch failed.  HTTP Code: " . $fetcher->errorCode();
        return;
    }
}

1;
