/**
 * 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.purl.sword.server;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.StringTokenizer;
import java.util.concurrent.atomic.AtomicInteger;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.Logger;
import org.purl.sword.atom.Summary;
import org.purl.sword.atom.Title;
import org.purl.sword.base.ChecksumUtils;
import org.purl.sword.base.Deposit;
import org.purl.sword.base.DepositResponse;
import org.purl.sword.base.ErrorCodes;
import org.purl.sword.base.HttpHeaders;
import org.purl.sword.base.SWORDAuthenticationException;
import org.purl.sword.base.SWORDErrorDocument;
import org.purl.sword.base.SWORDErrorException;
import org.purl.sword.base.SWORDException;

/**
 * DepositServlet
 *
 * @author Stuart Lewis
 */
public class DepositServlet extends HttpServlet {

    /**
     * Sword repository
     */
    protected transient SWORDServer myRepository;

    /**
     * Authentication type
     */
    private String authN;

    /**
     * Maximum file upload size in kB
     **/
    private int maxUploadSize;

    /**
     * Temp directory
     */
    private String tempDirectory;

    /**
     * Counter
     */
    private static final AtomicInteger counter = new AtomicInteger(0);

    /**
     * Logger
     */
    private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(DepositServlet.class);

    /**
     * Initialise the servlet.
     *
     * @throws ServletException if there is trouble with the upload directory.
     */
    @Override
    public void init() throws ServletException {

        // Instantiate the correct SWORD Server class
        String className = getServletContext().getInitParameter("sword-server-class");
        if (className == null) {
            log.fatal("Unable to read value of 'sword-server-class' from Servlet context");
            throw new ServletException("Unable to read value of 'sword-server-class' from Servlet context");
        }

        try {
            myRepository = (SWORDServer) Class.forName(className)
                                              .newInstance();
            log.info("Using " + className + " as the SWORDServer");
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            log.fatal("Unable to instantiate class from 'sword-server-class': "
                          + className);
            throw new ServletException(
                "Unable to instantiate class from 'sword-server-class': "
                    + className, e);
        }

        authN = getServletContext().getInitParameter("authentication-method");
        if ((authN == null) || (authN.equals(""))) {
            authN = "None";
        }
        log.info("Authentication type set to: " + authN);

        String maxUploadSizeStr = getServletContext().getInitParameter("maxUploadSize");
        if ((maxUploadSizeStr == null) ||
            (maxUploadSizeStr.equals("")) ||
            (maxUploadSizeStr.equals("-1"))) {
            maxUploadSize = -1;
            log.warn("No maxUploadSize set, so setting max file upload size to unlimited.");
        } else {
            try {
                maxUploadSize = Integer.parseInt(maxUploadSizeStr);
                log.info("Setting max file upload size to " + maxUploadSize);
            } catch (NumberFormatException nfe) {
                maxUploadSize = -1;
                log.warn("maxUploadSize not a number, so setting max file upload size to unlimited.");
            }
        }

        tempDirectory = getServletContext().getInitParameter(
            "upload-temp-directory");
        if ((tempDirectory == null) || (tempDirectory.equals(""))) {
            tempDirectory = System.getProperty("java.io.tmpdir");
        }
        if (!tempDirectory.endsWith(System.getProperty("file.separator"))) {
            tempDirectory += System.getProperty("file.separator");
        }
        File tempDir = new File(tempDirectory);
        log.info("Upload temporary directory set to: " + tempDir);
        if (!tempDir.exists() && !tempDir.mkdirs()) {
            throw new ServletException(
                "Upload directory did not exist and I can't create it. "
                    + tempDir);
        }
        if (!tempDir.isDirectory()) {
            log.fatal("Upload temporary directory is not a directory: "
                          + tempDir);
            throw new ServletException(
                "Upload temporary directory is not a directory: " + tempDir);
        }
        if (!tempDir.canWrite()) {
            log.fatal("Upload temporary directory cannot be written to: "
                          + tempDir);
            throw new ServletException(
                "Upload temporary directory cannot be written to: "
                    + tempDir);
        }
    }

    /**
     * Process the Get request. This will return an unimplemented response.
     *
     * @param request  the request.
     * @param response the response.
     * @throws jakarta.servlet.ServletException passed through.
     * @throws java.io.IOException            passed through.
     */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        // Send a '501 Not Implemented'
        response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
    }

    /**
     * Process a post request.
     *
     * @param request  the request.
     * @param response the response.
     * @throws jakarta.servlet.ServletException passed through.
     * @throws java.io.IOException            passed through.
     */
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        // Create the Deposit request
        Deposit d = new Deposit();
        Date date = new Date();
        log.debug("Starting deposit processing at " + date.toString() + " by "
                      + request.getRemoteAddr());

        // Are there any authentication details?
        String usernamePassword = getUsernamePassword(request);
        if ((usernamePassword != null) && (!usernamePassword.equals(""))) {
            int p = usernamePassword.indexOf(':');
            if (p != -1) {
                d.setUsername(usernamePassword.substring(0, p));
                d.setPassword(usernamePassword.substring(p + 1));
            }
        } else if (authenticateWithBasic()) {
            String s = "Basic realm=\"SWORD\"";
            response.setHeader("WWW-Authenticate", s);
            response.setStatus(401);
            return;
        }

        // Set up some variables
        String filename = null;

        // Do the processing
        try {
            // Write the file to the temp directory
            filename = tempDirectory + "SWORD-"
                + request.getRemoteAddr() + "-" + counter.addAndGet(1);
            log.debug("Package temporarily stored as: " + filename);
            InputStream inputstream = request.getInputStream();
            OutputStream outputstream = new FileOutputStream(new File(filename));
            try {
                byte[] buf = new byte[1024];
                int len;
                while ((len = inputstream.read(buf)) > 0) {
                    outputstream.write(buf, 0, len);
                }
            } finally {
                inputstream.close();
                outputstream.close();
            }

            // Check the size is OK
            File file = new File(filename);
            long fLength = file.length() / 1024;
            if ((maxUploadSize != -1) && (fLength > maxUploadSize)) {
                this.makeErrorDocument(ErrorCodes.MAX_UPLOAD_SIZE_EXCEEDED,
                                       HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE,
                                       "The uploaded file exceeded the maximum file size this server will accept (the" +
                                           " file is " +
                                           fLength + "kB but the server will only accept files as large as " +
                                           maxUploadSize + "kB)",
                                       request,
                                       response);
                return;
            }

            // Check the MD5 hash
            String receivedMD5 = ChecksumUtils.generateMD5(filename);
            log.debug("Received filechecksum: " + receivedMD5);
            d.setMd5(receivedMD5);
            String md5 = request.getHeader("Content-MD5");
            log.debug("Received file checksum header: " + md5);
            if ((md5 != null) && (!md5.equals(receivedMD5))) {
                // Return an error document
                this.makeErrorDocument(ErrorCodes.ERROR_CHECKSUM_MISMATCH,
                                       HttpServletResponse.SC_PRECONDITION_FAILED,
                                       "The received MD5 checksum for the deposited file did not match the checksum " +
                                           "sent by the deposit client",
                                       request,
                                       response);
                log.debug("Bad MD5 for file. Aborting with appropriate error message");
                return;
            } else {
                // Set the file to be deposited
                d.setFile(file);

                // Set the X-On-Behalf-Of header
                String onBehalfOf = request.getHeader(HttpHeaders.X_ON_BEHALF_OF);
                if ((onBehalfOf != null) && (onBehalfOf.equals("reject"))) {
                    // user name is "reject", so throw a not know error to allow the client to be tested
                    throw new SWORDErrorException(ErrorCodes.TARGET_OWNER_UKNOWN, "unknown user \"reject\"");
                } else {
                    d.setOnBehalfOf(onBehalfOf);
                }

                // Set the X-Packaging header
                d.setPackaging(request.getHeader(HttpHeaders.X_PACKAGING));

                // Set the X-No-Op header
                String noop = request.getHeader(HttpHeaders.X_NO_OP);
                log.error("X_NO_OP value is " + noop);
                if ((noop != null) && (noop.equals("true"))) {
                    d.setNoOp(true);
                } else if ((noop != null) && (noop.equals("false"))) {
                    d.setNoOp(false);
                } else if (noop == null) {
                    d.setNoOp(false);
                } else {
                    throw new SWORDErrorException(ErrorCodes.ERROR_BAD_REQUEST, "Bad no-op");
                }

                // Set the X-Verbose header
                String verbose = request.getHeader(HttpHeaders.X_VERBOSE);
                if ((verbose != null) && (verbose.equals("true"))) {
                    d.setVerbose(true);
                } else if ((verbose != null) && (verbose.equals("false"))) {
                    d.setVerbose(false);
                } else if (verbose == null) {
                    d.setVerbose(false);
                } else {
                    throw new SWORDErrorException(ErrorCodes.ERROR_BAD_REQUEST, "Bad verbose");
                }

                // Set the slug
                String slug = request.getHeader(HttpHeaders.SLUG);
                if (slug != null) {
                    d.setSlug(slug);
                }

                // Set the content disposition
                d.setContentDisposition(request.getHeader(HttpHeaders.CONTENT_DISPOSITION));

                // Set the IP address
                d.setIPAddress(request.getRemoteAddr());

                // Set the deposit location
                d.setLocation(getUrl(request));

                // Set the content type
                d.setContentType(request.getContentType());

                // Set the content length
                String cl = request.getHeader(HttpHeaders.CONTENT_LENGTH);
                if ((cl != null) && (!cl.equals(""))) {
                    d.setContentLength(Integer.parseInt(cl));
                }

                // Get the DepositResponse
                DepositResponse dr = myRepository.doDeposit(d);

                // Echo back the user agent
                if (request.getHeader(HttpHeaders.USER_AGENT) != null) {
                    dr.getEntry().setUserAgent(request.getHeader(HttpHeaders.USER_AGENT));
                }

                // Echo back the packaging format
                if (request.getHeader(HttpHeaders.X_PACKAGING) != null) {
                    dr.getEntry().setPackaging(request.getHeader(HttpHeaders.X_PACKAGING));
                }

                // Print out the Deposit Response
                response.setStatus(dr.getHttpResponse());
                if ((dr.getLocation() != null) && (!dr.getLocation().equals(""))) {
                    response.setHeader("Location", dr.getLocation());
                }
                response.setContentType("application/atom+xml; charset=UTF-8");
                PrintWriter out = response.getWriter();
                out.write(dr.marshall());
                out.flush();
            }
        } catch (SWORDAuthenticationException sae) {
            // Ask for credentials again
            if (authN.equals("Basic")) {
                String s = "Basic realm=\"SWORD\"";
                response.setHeader("WWW-Authenticate", s);
                response.setStatus(401);
            }
        } catch (SWORDErrorException see) {
            // Get the details and send the right SWORD error document
            log.error(see.toString());
            this.makeErrorDocument(see.getErrorURI(),
                                   see.getStatus(),
                                   see.getDescription(),
                                   request,
                                   response);
            return;
        } catch (SWORDException | NoSuchAlgorithmException se) {
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            log.error(se.toString());
        } finally {
            // Try deleting the temp file
            if (filename != null) {
                File f = new File(filename);
                if (!f.delete()) {
                    log.error("Unable to delete file: " + filename);
                }
            }
        }
    }

    /**
     * Utility method to construct a SWORDErrorDocumentTest
     *
     * @param errorURI The error URI to pass
     * @param status   The HTTP status to return
     * @param summary  The textual description to give the user
     * @param request  The HttpServletRequest object
     * @param response The HttpServletResponse to send the error document to
     * @throws IOException A general class of exceptions produced by failed or interrupted I/O operations.
     */
    protected void makeErrorDocument(String errorURI, int status, String summary,
                                     HttpServletRequest request, HttpServletResponse response)
        throws IOException {
        SWORDErrorDocument sed = new SWORDErrorDocument(errorURI);
        Title title = new Title();
        title.setContent("ERROR");
        sed.setTitle(title);
        Calendar calendar = Calendar.getInstance();
        String utcformat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
        SimpleDateFormat zulu = new SimpleDateFormat(utcformat);
        String serializeddate = zulu.format(calendar.getTime());
        sed.setUpdated(serializeddate);
        Summary sum = new Summary();
        sum.setContent(summary);
        sed.setSummary(sum);
        if (request.getHeader(HttpHeaders.USER_AGENT) != null) {
            sed.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT));
        }
        response.setStatus(status);
        response.setContentType("application/atom+xml; charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.write(sed.marshall().toXML());
        out.flush();
    }

    /**
     * Utility method to return the username and password (separated by a colon
     * ':')
     *
     * @param request Servlet's HTTP request object.
     * @return The username and password combination
     */
    protected String getUsernamePassword(HttpServletRequest request) {
        try {
            String authHeader = request.getHeader("Authorization");
            if (authHeader != null) {
                StringTokenizer st = new StringTokenizer(authHeader);
                if (st.hasMoreTokens()) {
                    String basic = st.nextToken();
                    if (basic.equalsIgnoreCase("Basic")) {
                        String credentials = st.nextToken();
                        String userPass = new String(Base64
                                                         .decodeBase64(credentials.getBytes()));
                        return userPass;
                    }
                }
            }
        } catch (Exception e) {
            log.debug(e.toString());
        }
        return null;
    }

    /**
     * Utility method to decide if we are using HTTP Basic authentication
     *
     * @return if HTTP Basic authentication is in use or not
     */
    protected boolean authenticateWithBasic() {
        return (authN.equalsIgnoreCase("Basic"));
    }

    /**
     * Utility method to construct the URL called for this Servlet
     *
     * @param req The request object
     * @return The URL
     */
    protected static String getUrl(HttpServletRequest req) {
        String reqUrl = req.getRequestURL().toString();
        String queryString = req.getQueryString();
        if (queryString != null) {
            reqUrl += "?" + queryString;
        }
        return reqUrl;
    }
}
