/*
 * [FetchBookFacts.java]
 *
 * Summary: Get basic facts about a new book from Amazon AWS API including alternate bindings. Used to add new books.
 *
 * Copyright: (c) 2012-2017 Roedy Green, Canadian Mind Products, http://mindprod.com
 *
 * Licence: This software may be copied and used freely for any purpose but military.
 *          http://mindprod.com/contact/nonmil.html
 *
 * Requires: JDK 1.8+
 *
 * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/
 *
 * Version History:
 *  1.0 2012-03-04 initial version
 *  1.1 2012-03-07 get alternate bindings probe working.
 */
package com.mindprod.aws;

import com.mindprod.aws.jax.AWSECommerceService;
import com.mindprod.aws.jax.AWSECommerceServicePortType;
import com.mindprod.aws.jax.AwsHandlerResolver;
import com.mindprod.aws.jax.Item;
import com.mindprod.aws.jax.ItemAttributes;
import com.mindprod.aws.jax.ItemLookup;
import com.mindprod.aws.jax.ItemLookupRequest;
import com.mindprod.aws.jax.Items;
import com.mindprod.aws.jax.OperationRequest;
import com.mindprod.common18.EIO;
import com.mindprod.common18.Misc;
import com.mindprod.fastcat.FastCat;
import com.mindprod.filetransfer.FileTransfer;
import com.mindprod.htmlmacros.macro.Global;
import com.mindprod.htmlmacros.support.ConfigurationForMindprod;
import com.mindprod.http.Get;
import com.mindprod.hunkio.HunkIO;
import com.mindprod.isbn.ISBNValidate;
import com.mindprod.stores.BStore;

import javax.xml.namespace.QName;
import javax.xml.ws.Holder;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.lang.System.*;

/**
 * Get basic facts about a new book from Amazon AWS API including alternate bindings. Used to add new books.
 * <p/>
 * It does not fetch info about whether the books are in stock.
 *
 * @author Roedy Green, Canadian Mind Products
 * @version 1.1 2012-03-07 get alternate bindings probe working.
 * @see AddBindings
 * @see Bindings for binding categories
 * @since 2012-03-04
 */
public class FetchBookFacts
    {
    // declarations

    /**
     * true if debugging, and want extra output
     */
    private static final boolean DEBUGGING = false;

    /**
     * how long to give B&N to respond in millis.
     */
    private static final int CONNECT_TIMEOUT = ( int ) TimeUnit.SECONDS.toMillis( 70 );

    /**
     * fetch confidential amazon.com-assigned awsAccessKeyId from environment set awsaccesskeyid=xxxxx
     */
    private static final String AWS_ACCESS_KEY_ID = System.getenv( "awsaccesskeyid" );

    /**
     * fetch confidential amazon.com-assigned awsSecretAccessKey assigned by Amazon set awssecretaccesskey=xxxxx
     */
    private static final String AWS_SECRET_ACCESS_KEY = System.getenv( "awssecretaccesskey" );

    /**
     * Nampspace for amazon.com in the USA.  Would have to be modified for other countries.
     */
    private static final String NAME_SPACE_URI = "http://webservices.amazon.com/AWSECommerceService/2013-08-01";

    /**
     * name of the SOAP service
     */
    private static final String QNAME = "AWSECommerceService";

    private static final String USAGE = "\nUsage: FetchBookFacts.jar isbn";

    /**
     * location of JAX schmo, locally cached. for amazon.com in the USA.  Would have to be modified for other countries.
     * Original came from http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsd
     */
    private static final String WSDL_LOCATION = "file:///E:/com/mindprod/aws/jax/AWSECommerceService.wsdl";

    /**
     * pattern marking an nook ean
     */
    private static final Pattern EBOOK_PATTERN = Pattern.compile( "\"NOOKEAN\"\\s+:\\s+\"(97[0-9]{11})\"," );

    /**
     * fetch public amazon.com-assigned associateTag from environment set awsassociatetag=xxxxx
     */
    private static String associateTag;

    /**
     * audio EAN
     */
    private static String audio;

    /**
     * author's name
     */
    private static String author;

    /**
     * isbn of ebook from nook
     */
    private static String eBook;

    /**
     * hardcover EAN
     */
    private static String hardcover;

    /**
     * primary EAN
     */
    private static String isbn13;

    /**
     * primaryAsin of kindle
     */
    private static String kindle;

    /**
     * EAN of paperback
     */
    private static String paperback;

    /**
     * primary primaryAsin
     */
    private static String primaryAsin;

    /**
     * yyyy-mm-dd published
     */
    private static String published;

    /**
     * publisher
     */
    private static String publisher;

    /**
     * title of book
     */
    private static String title;

    /**
     * Which store do we want to probe for new products.
     */
    private static BStore primaryBStore;

    /**
     * where Book cover images files are kept
     */
    private static File bookCoverDir;

    /**
     * where Book files are kept
     */
    private static File bookDir;
    // /declarations
    // methods

    /**
     * Convert primaryAsin back to EAN isbn-13
     *
     * @param asin amazon primaryAsin number of book. If EAN already returns EAN.
     *
     * @return isbn13
     * @throws java.net.MalformedURLException if generated URL is malformed.
     */
    private static String asinToEAN( final String asin ) throws MalformedURLException
        {
        if ( asin == null || asin.length() == 13 )
            {
            return asin; // actually EAN already
            }
        // define magic configuring strings
        final AWSECommerceServicePortType port = getPort();
        // new ItemLookup
        final ItemLookup itemLookup = new ItemLookup();
        // new ItemLookupRequest which is part of the ItemLookup
        final ItemLookupRequest itemLookupRequest = new ItemLookupRequest();
        // odd our lookup request to the the Lookup.
        final List<ItemLookupRequest> itemLookupRequests = itemLookup.getRequest();
        itemLookupRequests.add( itemLookupRequest );
        // Set up the values of the ItemLookupRequest
        itemLookupRequest.setIdType( "ASIN" );
        itemLookupRequest.getItemId().add( asin );    // there is no setItemID method
        // specify info to include in response
        final List<String> responseGroup = itemLookupRequest.getResponseGroup();
        responseGroup.add( "ItemAttributes" );
        // set up Holder for the response tree
        final Holder<OperationRequest> operationrequest = new Holder<>();
        final Holder<List<Items>> items = new Holder<>();
        final String marketplaceDomain = "";  // not yet supported
        final String xmlEscaping = "Single";
        final String validate = "";
        // Probe Amazon Server, WARNING! Amazon changes the parms to this method frequently.
        // Inserts awsAccessKeyID and associateTag
        // note order validate, xmlEscaping, different from itemSearch
        port.itemLookup( marketplaceDomain, AWS_ACCESS_KEY_ID, associateTag, validate, xmlEscaping,
                itemLookupRequest, itemLookupRequests, operationrequest, items );
        // analyse results
        final List<Items> result = items.value;
        final int size = result.get( 0 ).getItem().size();
        for ( int i = 0; i < size; i++ )
            {
            final Item item = result.get( 0 ).getItem().get( i );
            final ItemAttributes attributes = item.getItemAttributes();
            if ( attributes != null )
                {
                return attributes.getEAN();
                }
            }
        err.println( "cannot find EAN corresponding to " + asin );
        return null;
        }// /method

    /**
     * get isbn13 of Nook ebook equivalent
     *
     * @param isbn13
     *
     * @return ebook-isbn13 or null if none
     */
    private static final String fetchNookEAN( String isbn13 )
        {
        final Get get = new Get();
        get.setInstanceFollowRedirects( true );  // chase moved urls.
        get.setConnectTimeout( CONNECT_TIMEOUT );
        // B&N will give us a 301 moved permanently with an url of the form:
        // http://search.barnesandnoble.com/title/author/e/9780321480910
        try
            {
            final String response = get.send( new URL( "http://search.barnesandnoble" +
                                                       ".com/booksearch/isbninquiry" +
                                                       ".asp?isbn=" + isbn13 ), Get.UTF8 );
            if ( response == null || response.length() == 0 )
                {
                err.println( get.getResponseMessage() + " for " + isbn13 );
                err.println( get.getInterruptResponseMessage() );
                return null;
                }
            final Matcher m = EBOOK_PATTERN.matcher( response );
            if ( m.find() )
                {
                return m.group( 1 );
                }
            else
                {
                return null;
                }
            }
        catch ( MalformedURLException e )
            {
            err.println( "could not build URL" );
            return null;
            }
        }// /method

    /**
     * generate the xxxx.html file containing the Book macro with info we gleaned from Amazon.
     *
     * @param target file where write generated Book macro
     *
     * @throws IOException if trouble writing file
     */
    private static void generate( final File target ) throws IOException
        {
        // does not handle birth/death
        final FastCat sb = new FastCat( 36 );
        sb.append( "<!-- macro BookHead {" );
        sb.append( title );
        sb.append( "} published=", published, " -->\n" );
        sb.append( "<!-- macro Book isbn=", isbn13, "\n" );
        if ( paperback != null && paperback.length() != 0 )
            {
            sb.append( "paperback=", paperback, "\n" );
            }
        if ( hardcover != null && hardcover.length() != 0 )
            {
            sb.append( "hardcover=", hardcover, "\n" );
            }
        if ( kindle != null && kindle.length() != 0 )
            {
            sb.append( "kindle=", kindle, "\n" );
            }
        if ( eBook != null && eBook.length() != 0 )
            {
            sb.append( "ebook=", eBook, "\n" );
            }
        if ( audio != null && audio.length() != 0 )
            {
            sb.append( "audio=", audio, "\n" );
            }
        sb.append( "title={", title, "}\n" );
        sb.append( "author={", author, "}\n" );    // born will go here later on same line.
        sb.append( "publisher={", publisher, "}", " published=", published, "\n" );
        sb.append( "notes={} -->\n" );
        sb.append( "<!-- macro Foot -->\n" );
        final String generated = sb.toString();
        HunkIO.writeEntireFile( target, generated, HunkIO.UTF8 );
        out.println();
        out.println( EIO.getCanOrAbsPath( target ) + "    written." );
        }// /method

    /**
     * standard boilerplate to connect to amazon.com AWS server with SOAP
     *
     * @return port to use to send requests.
     * @throws java.net.MalformedURLException if some URLs were mis-specified in the following code.
     */
    private static AWSECommerceServicePortType getPort() throws MalformedURLException
        {
        // Set the service:
        AWSECommerceService service = new AWSECommerceService( new URL( WSDL_LOCATION ), new QName( NAME_SPACE_URI,
                QNAME ) );
        // AwsHandlerResolver does the timestamp, signing and Base64 encoding
        service.setHandlerResolver( new AwsHandlerResolver( AWS_SECRET_ACCESS_KEY ) );
        // Set the service port:
        return primaryBStore.getPort( service );
        }// /method

    /**
     * Probe the amazon AWS api for isbn13, results in class vars
     *
     * @throws java.net.MalformedURLException if generated URL is malformed.
     */
    private static void queryISBN() throws MalformedURLException
        {
        // for amazon.com in USA
        // define magic configuring strings
        final AWSECommerceServicePortType port = getPort();
        // new ItemLookup
        final ItemLookup itemLookup = new ItemLookup();
        // new ItemLookupRequest which is part of the ItemLookup
        final ItemLookupRequest itemLookupRequest = new ItemLookupRequest();
        // odd our lookup request to the the Lookup.
        final List<ItemLookupRequest> itemLookupRequests = itemLookup.getRequest();
        itemLookupRequests.add( itemLookupRequest );
        // Set up the values of the ItemLookupRequest
        itemLookupRequest.setSearchIndex( "Books" );
        itemLookupRequest.setIdType( "EAN" );
        itemLookupRequest.getItemId().add( isbn13 );    // there is no setItemID method
        // specify info to include in response
        final List<String> responseGroup = itemLookupRequest.getResponseGroup();
        responseGroup.add( "Images" );
        responseGroup.add( "ItemAttributes" );
        // set up Holder for the response tree
        final Holder<OperationRequest> operationrequest = new Holder<>();
        final Holder<List<Items>> items = new Holder<>();
        final String marketplaceDomain = "";  // not yet supported
        final String xmlEscaping = "Single";
        final String validate = "";
        // Probe Amazon Server, WARNING! Amazon changes the parms to this method frequently.
        // Inserts awsAccessKeyID and associateTag
        // note order validate, xmlEscaping, different from itemSearch
        port.itemLookup( marketplaceDomain, AWS_ACCESS_KEY_ID, associateTag, validate, xmlEscaping,
                itemLookupRequest, itemLookupRequests, operationrequest, items );
        // analyse results
        final List<Items> result = items.value;
        final int size = result.get( 0 ).getItem().size();
        for ( int i = 0; i < size; i++ )
            {
            final Item item = result.get( 0 ).getItem().get( i );
            primaryAsin = item.getASIN();
            final ItemAttributes attributes = item.getItemAttributes();
            title = attributes.getTitle();
            List<String> authors = attributes.getAuthor();
            final FastCat sb = new FastCat( authors.size() * 2 );
            for ( String anAuthor : authors )
                {
                sb.append( anAuthor );
                sb.append( ", " );
                }
            sb.drop(); // last , if any
            author = sb.toString();
            publisher = attributes.getPublisher();
            if ( publisher.equals( "O'Reilly Media" ) )
                {
                publisher = "O&rsquo;Reilly";
                }
            published = attributes.getReleaseDate();
            published = attributes.getPublicationDate();
            final String binding = attributes.getBinding();
            if ( binding != null )
                {
                if ( binding.equals( "Paperback" ) )
                    {
                    paperback = isbn13;
                    }
                else if ( binding.equals( "Hardcover" ) )
                    {
                    hardcover = isbn13;
                    }
                }
            out.println( "isbn:" + isbn13
                         + " primaryAsin: " + primaryAsin
                         + " title:" + title
                         + " author:" + author
                         + " publisher:" + publisher
                         + " published:" + published
                         + " binding: " + binding );
            if ( item.getLargeImage() == null )
                {
                err.println( "no image for " + isbn13 );
                }
            else
                {
                final URL u = new URL( item.getLargeImage().getURL() );
                File target = new File( bookCoverDir, isbn13 + ".jpg" );
                new FileTransfer().download( u, target, false );
                out.println();
                out.println( EIO.getCanOrAbsPath( target ) + "   downloaded" );
                break;
                }
            } //end loop to process results
        }// /method

    /**
     * probe the amazon AWS api to find alternate bindings.
     *
     * @throws java.net.MalformedURLException if generated URL is malformed
     */
    private static void queryOtherBindings() throws MalformedURLException
        {
        final AWSECommerceServicePortType port = getPort();
        // new ItemLookup
        final ItemLookup itemLookup = new ItemLookup();
        // new ItemLookupRequest which is part of the ItemLookup
        final ItemLookupRequest itemLookupRequest = new ItemLookupRequest();
        // odd our lookup request to the the Lookup.
        final List<ItemLookupRequest> itemLookupRequests = itemLookup.getRequest();
        itemLookupRequests.add( itemLookupRequest );
        // Set up the values of the ItemLookupRequest
        // lookup of variants only works with ASIN
        itemLookupRequest.setIdType( "ASIN" );
        itemLookupRequest.getItemId().add( primaryAsin );    // there is no setItemID method
        // specify info to include in response
        final List<String> responseGroup = itemLookupRequest.getResponseGroup();
        responseGroup.add( "AlternateVersions" );
        // set up Holder for the response tree
        final Holder<OperationRequest> operationRequest = new Holder<>();
        final Holder<List<Items>> items = new Holder<>();
        final String marketplaceDomain = "";  // not yet supported
        final String xmlEscaping = "Single";
        final String validate = "";
        // Probe Amazon Server, WARNING! Amazon changes the parms to this method frequently.
        // Inserts awsAccessKeyID and associateTag
        // note order validate, xmlEscaping, different from itemSearch
        port.itemLookup( marketplaceDomain, AWS_ACCESS_KEY_ID, associateTag, validate, xmlEscaping,
                itemLookupRequest, itemLookupRequests, operationRequest, items );
        // analyse results
        final List<Items> result = items.value;
        final int size = result.get( 0 ).getItem().size();
        for ( int i = 0; i < size; i++ )
            {
            final Item item = result.get( 0 ).getItem().get( i );
            Item.AlternateVersions alternateVersions = item.getAlternateVersions();
            if ( alternateVersions != null )
                {
                for ( Item.AlternateVersions.AlternateVersion alternateVersion : alternateVersions
                        .getAlternateVersion() )
                    {
                    final String asin = alternateVersion.getASIN();
                    final String bindingName = alternateVersion.getBinding();
                    final Binding b = Binding.exactMatch( bindingName );
                    recordBindingASIN( b, asin );
                    } // end loop to process alternateVersions  pass 1
                for ( Item.AlternateVersions.AlternateVersion alternateVersion : alternateVersions
                        .getAlternateVersion() )
                    {
                    final String asin = alternateVersion.getASIN();
                    final String bindingName = alternateVersion.getBinding();
                    final Binding b = Binding.approximateMatch( bindingName );
                    if ( b == null )
                        {
                        err.println( "Unrecognised binding " + bindingName + " on asin:" + asin );
                        }
                    else
                        {
                        recordBindingASIN( b, asin );
                        }
                    } // end loop to process alternateVersions pass 2
                } // end alternateVersions not null
            } //end loop to process results
        paperback = asinToEAN( paperback );
        hardcover = asinToEAN( hardcover );
        audio = asinToEAN( audio );
        out.println( "Alternate bindings"
                     + " primaryAsin:" + primaryAsin
                     + " paperback:" + paperback
                     + " hardcover:" + hardcover
                     + " audio:" + audio
                     + " kindle:" + kindle
                     + " eBook:" + eBook );
        }// /method

    /**
     * record the asin in the corresponding binding variable, if we don't have a value already.
     */
    private static void recordBindingASIN( final Binding b, final String asin )
        {
        if ( b == null )
            {
            return;
            }
        switch ( b )
            {
            case PAPERBACK:
                if ( paperback == null )
                    {
                    paperback = asin;
                    }
                break;
            case HARDCOVER:
                if ( hardcover == null )
                    {
                    hardcover = asin;
                    }
                break;
            case AUDIO:
                if ( audio == null )
                    {
                    audio = asin;
                    }
                break;
            case KINDLE:
                if ( kindle == null )
                    {
                    kindle = asin;
                    }
                break;
            case EBOOK:
            case OTHER:
            default:
                // ignore
            }
        }// /method

    /**
     * takes an isbn13 on the command line.  Creates a Book macro for it and downloads book cover.
     *
     * @param args isbn13/EAN to probe.
     *
     * @throws java.io.IOException if trouble reading/writing files
     */
    public static void main( String[] args ) throws IOException
        {
        // BStore enum inits needs access to the global configuration
        Global.installConfiguration( new ConfigurationForMindprod() );
        final File webrootDir = new File( Global.configuration.getLocalWebrootWithSlashes() );
        bookDir = new File( webrootDir, "book" );
        bookCoverDir = new File( webrootDir, "image/bookcover" );
        primaryBStore = BStore.AMAZONCOM;
        associateTag = primaryBStore.getAccount();
        if ( DEBUGGING )
            {
            out.println( "primaryBStore = " + primaryBStore.name() );
            out.println( "associateTag = " + associateTag );
            out.println( "AWS_ACCESS_KEY_ID = " + AWS_ACCESS_KEY_ID );
            out.println( "AWS_SECRET_ACCESS_KEY = " + AWS_SECRET_ACCESS_KEY );
            }
        if ( args.length != 1 )
            {
            throw new IllegalArgumentException( USAGE );
            }
        isbn13 = ISBNValidate.tidyISBN10or13RemovingDashes( args[ 0 ] );
        final File target = new File( bookDir, isbn13 + ".html" );
        if ( target.exists() )
            {
            err.println( "book already exists at " + EIO.getCanOrAbsPath( target ) );
            }
        else
            {
            queryISBN();
            queryOtherBindings();
            eBook = fetchNookEAN( isbn13 );
            generate( target );
            }
        Misc.trackLastThread();
        System.exit( 0 );
        }// /method
    // /methods
    }