/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.sword2;

import java.io.IOException;
import java.util.Date;

import org.apache.logging.log4j.Logger;
import org.dspace.content.Collection;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.CollectionService;
import org.dspace.core.Context;
import org.dspace.core.LogHelper;
import org.swordapp.server.AuthCredentials;
import org.swordapp.server.CollectionDepositManager;
import org.swordapp.server.Deposit;
import org.swordapp.server.DepositReceipt;
import org.swordapp.server.SwordAuthException;
import org.swordapp.server.SwordConfiguration;
import org.swordapp.server.SwordError;
import org.swordapp.server.SwordServerException;

public class CollectionDepositManagerDSpace extends DSpaceSwordAPI
    implements CollectionDepositManager {
    /**
     * logger
     */
    private static Logger log = org.apache.logging.log4j.LogManager
        .getLogger(CollectionDepositManagerDSpace.class);

    protected CollectionService collectionService = ContentServiceFactory
        .getInstance().getCollectionService();

    private VerboseDescription verboseDescription = new VerboseDescription();

    public DepositReceipt createNew(String collectionUri, Deposit deposit,
                                    AuthCredentials authCredentials, SwordConfiguration swordConfig)
        throws SwordError, SwordServerException, SwordAuthException {
        // start the timer
        Date start = new Date();

        // store up the verbose description, which we can then give back at the end if necessary
        this.verboseDescription.append("Initialising verbose deposit");

        SwordContext sc = null;
        SwordConfigurationDSpace config = (SwordConfigurationDSpace) swordConfig;

        try {
            // first authenticate the request
            // note: this will build our various DSpace contexts for us
            sc = this.doAuth(authCredentials);
            Context context = sc.getContext();

            if (log.isDebugEnabled()) {
                log.debug(LogHelper.getHeader(context, "sword_create_new", ""));
            }

            // get the deposit target
            Collection collection = this
                .getDepositTarget(context, collectionUri, config);
            if (collection == null) {
                throw new SwordError(404);
            }

            // Ensure that this method is allowed
            WorkflowManager wfm = WorkflowManagerFactory.getInstance();
            wfm.createResource(context, collection);

            // find out if the supplied SWORDContext can submit to the given
            // dspace object
            SwordAuthenticator auth = new SwordAuthenticator();
            if (!auth.canSubmit(sc, collection, this.verboseDescription)) {
                // throw an exception if the deposit can't be made
                String oboEmail = "none";
                if (sc.getOnBehalfOf() != null) {
                    oboEmail = sc.getOnBehalfOf().getEmail();
                }
                log.info(LogHelper
                             .getHeader(context, "deposit_failed_authorisation",
                                        "user=" +
                                            sc.getAuthenticated().getEmail() +
                                            ",on_behalf_of=" + oboEmail));
                throw new SwordAuthException(
                    "Cannot submit to the given collection with this context");
            }

            // make a note of the authentication in the verbose string
            this.verboseDescription.append("Authenticated user: " +
                                               sc.getAuthenticated().getEmail());
            if (sc.getOnBehalfOf() != null) {
                this.verboseDescription.append("Depositing on behalf of: " +
                                                   sc.getOnBehalfOf().getEmail());
            }

            DepositResult result = null;
            try {
                if (deposit.isBinaryOnly()) {
                    result = this.createNewFromBinary(sc, collection, deposit,
                                                      authCredentials, config);
                } else if (deposit.isEntryOnly()) {
                    result = this.createNewFromEntry(sc, collection, deposit,
                                                     authCredentials, config);
                } else if (deposit.isMultipart()) {
                    result = this
                        .createNewFromMultipart(sc, collection, deposit,
                                                authCredentials, config);
                }
            } catch (DSpaceSwordException | SwordError e) {
                if (config.isKeepPackageOnFailedIngest()) {
                    try {
                        if (deposit.isBinaryOnly()) {
                            this.storePackageAsFile(deposit, authCredentials,
                                                    config);
                        } else if (deposit.isEntryOnly()) {
                            this.storeEntryAsFile(deposit, authCredentials,
                                                  config);
                        } else if (deposit.isMultipart()) {
                            this.storePackageAsFile(deposit, authCredentials,
                                                    config);
                            this.storeEntryAsFile(deposit, authCredentials,
                                                  config);
                        }
                    } catch (IOException e2) {
                        log.warn("Unable to store SWORD package as file: " + e);
                    }
                }
                throw e;
            }

            // now we've produced a deposit, we need to decide on its workflow state
            wfm.resolveState(context, deposit, result, this.verboseDescription);

            ReceiptGenerator genny = new ReceiptGenerator();
            DepositReceipt receipt = genny
                .createReceipt(context, result, config);

            Date finish = new Date();
            long delta = finish.getTime() - start.getTime();

            this.verboseDescription
                .append("Total time for deposit processing: " + delta +
                            " ms");
            this.addVerboseDescription(receipt, this.verboseDescription);

            // if something hasn't killed it already (allowed), then complete the transaction
            sc.commit();

            return receipt;
        } catch (DSpaceSwordException e) {
            log.error("caught exception:", e);
            throw new SwordServerException(
                "There was a problem depositing the item", e);
        } finally {
            // this is a read operation only, so there's never any need to commit the context
            if (sc != null) {
                sc.abort();
            }
        }
    }

    protected DepositResult createNewFromBinary(SwordContext swordContext,
                                                Collection collection, Deposit deposit,
                                                AuthCredentials authCredentials,
                                                SwordConfigurationDSpace swordConfig)
        throws DSpaceSwordException, SwordError, SwordAuthException,
        SwordServerException {
        // get the things out of the service that we need
        Context context = swordContext.getContext();
        // is the content acceptable?  If not, this will throw an error
        this.isAcceptable(swordConfig, context, deposit, collection);

        // Obtain the relevant ingester from the factory
        SwordContentIngester si = SwordIngesterFactory
            .getContentInstance(context, deposit, collection);
        this.verboseDescription
            .append("Loaded ingester: " + si.getClass().getName());

        // do the deposit
        DepositResult result = si
            .ingest(context, deposit, collection, this.verboseDescription);
        this.verboseDescription.append("Archive ingest completed successfully");

        // store the originals (this code deals with the possibility that that's not required)
        this.storeOriginals(swordConfig, context, this.verboseDescription,
                            deposit, result);

        return result;
    }

    protected DepositResult createNewFromEntry(SwordContext swordContext,
                                               Collection collection, Deposit deposit,
                                               AuthCredentials authCredentials,
                                               SwordConfigurationDSpace swordConfig)
        throws DSpaceSwordException, SwordError, SwordAuthException,
        SwordServerException {
        // get the things out of the service that we need
        Context context = swordContext.getContext();
        // Obtain the relevant ingester from the factory
        SwordEntryIngester si = SwordIngesterFactory
            .getEntryInstance(context, deposit, collection);
        this.verboseDescription
            .append("Loaded ingester: " + si.getClass().getName());

        // do the deposit
        DepositResult result = si
            .ingest(context, deposit, collection, this.verboseDescription);
        this.verboseDescription.append("Archive ingest completed successfully");

        // store the originals (this code deals with the possibility that that's not required)
        this.storeOriginals(swordConfig, context, this.verboseDescription,
                            deposit, result);

        return result;
    }

    protected DepositResult createNewFromMultipart(SwordContext swordContext,
                                                   Collection collection, Deposit deposit,
                                                   AuthCredentials authCredentials,
                                                   SwordConfigurationDSpace swordConfig)
        throws DSpaceSwordException, SwordError, SwordAuthException,
        SwordServerException {
        // get the things out of the service that we need
        Context context = swordContext.getContext();

        // is the content acceptable?  If not, this will throw an error
        this.isAcceptable(swordConfig, context, deposit, collection);

        // Obtain the relevant content ingester from the factory
        SwordContentIngester sci = SwordIngesterFactory
            .getContentInstance(context, deposit, collection);
        this.verboseDescription
            .append("Loaded content ingester: " + sci.getClass().getName());

        // obtain the relevant entry intester from the factory
        SwordEntryIngester sei = SwordIngesterFactory
            .getEntryInstance(context, deposit, collection);
        this.verboseDescription
            .append("Loaded entry ingester: " + sei.getClass().getName());

        DepositResult result;
        if (swordConfig.isEntryFirst()) {
            // do the entry deposit
            result = sei.ingest(context, deposit, collection,
                                this.verboseDescription);

            // do the content deposit
            result = sci.ingest(context, deposit, collection,
                                this.verboseDescription, result);
            this.verboseDescription
                .append("Archive ingest completed successfully");
        } else {
            // do the content deposit
            result = sci.ingest(context, deposit, collection,
                                this.verboseDescription);

            // do the entry deposit
            result = sei.ingest(context, deposit, collection,
                                this.verboseDescription, result, false);
            this.verboseDescription
                .append("Archive ingest completed successfully");
        }

        // store the originals (this code deals with the possibility that that's not required)
        this.storeOriginals(swordConfig, context, this.verboseDescription,
                            deposit, result);

        return result;
    }

    protected Collection getDepositTarget(Context context, String depositUrl,
                                          SwordConfigurationDSpace config)
        throws DSpaceSwordException, SwordError {
        SwordUrlManager urlManager = config.getUrlManager(context, config);

        // get the target collection
        Collection collection = urlManager.getCollection(context, depositUrl);
        if (collection == null) {
            throw new SwordError(404);
        }

        this.verboseDescription
            .append("Performing deposit using deposit URL: " + depositUrl);

        this.verboseDescription
            .append("Location resolves to collection with handle: " +
                        collection.getHandle() +
                        " and name: " + collectionService.getName(collection));

        return collection;
    }
}
