/**
 * 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.identifier.ezid;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.dspace.identifier.DOI;
import org.dspace.identifier.IdentifierException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A request to EZID concerning a given (or expected) identifier.
 *
 * @author Mark H. Wood
 */
public class EZIDRequest {
    private static final Logger log = LoggerFactory.getLogger(EZIDRequest.class);

    private static final String ID_PATH = "/id/" + DOI.SCHEME;

    private static final String SHOULDER_PATH = "/shoulder/" + DOI.SCHEME;

    private static final String UTF_8 = "UTF-8";

    private static final String MD_KEY_STATUS = "_status";

    private final CloseableHttpClient client;

    private final HttpClientContext httpContext;

    private final String scheme;

    private final String host;

    private final String path;

    private final String authority;

    /**
     * Prepare a context for requests concerning a specific identifier or
     * authority prefix.
     *
     * @param scheme    URL scheme for access to the EZID service.
     * @param host      Host name for access to the EZID service.
     * @param authority DOI authority prefix, e.g. "10.5072/FK2".
     * @param username  an EZID user identity.
     * @param password  user's password, or {@code null} for none.
     * @throws URISyntaxException if host or authority is bad.
     * @deprecated since 4.1
     */
    @Deprecated
    EZIDRequest(String scheme, String host, String authority, String username, String password)
        throws URISyntaxException {
        this.scheme = scheme;

        this.host = host;

        this.path = "ezid";

        if (authority.charAt(authority.length() - 1) == '/') {
            this.authority = authority.substring(0, authority.length() - 1);
        } else {
            this.authority = authority;
        }

        client = HttpClientBuilder.create().build();
        httpContext = HttpClientContext.create();
        if (null != username) {
            URI uri = new URI(scheme, host, path, null);
            CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
            credentialsProvider.setCredentials(new AuthScope(uri.getHost(), uri.getPort()),
                    new UsernamePasswordCredentials(username, password));
        }
    }

    /**
     * Prepare a context for requests concerning a specific identifier or
     * authority prefix.
     *
     * @param scheme    URL scheme for access to the EZID service.
     * @param host      Host name for access to the EZID service.
     * @param path      Local-path to the EZID service.
     * @param authority DOI authority prefix, e.g. "10.5072/FK2".
     * @param username  an EZID user identity.
     * @param password  user's password, or {@code null} for none.
     * @throws URISyntaxException if host or authority is bad.
     */
    EZIDRequest(String scheme, String host, String path,
                String authority, String username, String password)
        throws URISyntaxException {
        this.scheme = scheme;

        this.host = host;

        this.path = path;

        if (authority.charAt(authority.length() - 1) == '/') {
            this.authority = authority.substring(0, authority.length() - 1);
        } else {
            this.authority = authority;
        }

        client = HttpClientBuilder.create().build();
        httpContext = HttpClientContext.create();
        if (null != username) {
            CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
            URI uri = new URI(scheme, host, path, null);
            credentialsProvider.setCredentials(new AuthScope(uri.getHost(), uri.getPort()),
                    new UsernamePasswordCredentials(username, password));
            httpContext.setCredentialsProvider(credentialsProvider);
        }
    }

    /**
     * Fetch the metadata bound to an identifier.
     *
     * @param name identifier name
     * @return Decoded response data evoked by a request made to EZID.
     * @throws IdentifierException if the response is error or body malformed.
     * @throws IOException         if the HTTP request fails.
     * @throws URISyntaxException  if host or authority is bad.
     */
    public EZIDResponse lookup(String name)
        throws IdentifierException, IOException, URISyntaxException {
        // GET path
        HttpGet request;
        URI uri = new URI(scheme, host, path + ID_PATH + authority + name, null);
        log.debug("EZID lookup {}", uri.toASCIIString());
        request = new HttpGet(uri);
        HttpResponse response = client.execute(request, httpContext);
        return new EZIDResponse(response);
    }

    /**
     * Create an identifier with a given name. The name is the end of the
     * request path. Note: to "reserve" a given identifier, include "_status =
     * reserved" in {@code metadata}.
     *
     * @param name     identifier name
     * @param metadata ANVL-encoded key/value pairs.
     * @return Decoded response data evoked by a request made to EZID.
     * @throws IdentifierException if the response is error or body malformed.
     * @throws IOException         if the HTTP request fails.
     * @throws URISyntaxException  if host or authority is bad.
     */
    public EZIDResponse create(String name, Map<String, String> metadata)
        throws IOException, IdentifierException, URISyntaxException {
        // PUT path [+metadata]
        HttpPut request;
        URI uri = new URI(scheme, host, path + ID_PATH + authority + '/' + name, null);
        log.debug("EZID create {}", uri.toASCIIString());
        request = new HttpPut(uri);
        if (null != metadata) {
            request.setEntity(new StringEntity(formatMetadata(metadata), UTF_8));
        }
        HttpResponse response = client.execute(request, httpContext);
        return new EZIDResponse(response);
    }

    /**
     * Ask EZID to create a unique identifier and return its name. NOTE: to
     * "reserve" a unique identifier, include "_status = reserved" in {@code metadata}.
     *
     * @param metadata ANVL-encoded key/value pairs.
     * @return Decoded response data evoked by a request made to EZID.
     * @throws IdentifierException if the response is error or body malformed.
     * @throws IOException         if the HTTP request fails.
     * @throws URISyntaxException  if host or authority is bad.
     */
    public EZIDResponse mint(Map<String, String> metadata)
        throws IOException, IdentifierException, URISyntaxException {
        // POST path [+metadata]
        HttpPost request;
        URI uri = new URI(scheme, host, path + SHOULDER_PATH + authority, null);
        log.debug("EZID mint {}", uri.toASCIIString());
        request = new HttpPost(uri);
        if (null != metadata) {
            request.setEntity(new StringEntity(formatMetadata(metadata), UTF_8));
        }
        HttpResponse response = client.execute(request, httpContext);
        EZIDResponse myResponse = new EZIDResponse(response);
        return myResponse;
    }

    /**
     * Alter the metadata bound to an identifier.
     *
     * @param name     identifier name
     * @param metadata metadata fields to be altered. Leave the value of a field's empty
     *                 to delete the field.
     * @return Decoded response data evoked by a request made to EZID.
     * @throws IdentifierException if the response is error or body malformed.
     * @throws IOException         if the HTTP request fails.
     * @throws URISyntaxException  if host or authority is bad.
     */
    public EZIDResponse modify(String name, Map<String, String> metadata)
        throws IOException, IdentifierException, URISyntaxException {
        if (null == metadata) {
            throw new IllegalArgumentException("metadata must not be null");
        }
        // POST path +metadata
        HttpPost request;
        URI uri = new URI(scheme, host, path + ID_PATH + authority + name, null);
        log.debug("EZID modify {}", uri.toASCIIString());
        request = new HttpPost(uri);
        request.setEntity(new StringEntity(formatMetadata(metadata), UTF_8));
        HttpResponse response = client.execute(request, httpContext);
        return new EZIDResponse(response);
    }

    /**
     * Destroy a reserved identifier. Fails if ID was ever public.
     *
     * @param name identifier name
     * @return Decoded response data evoked by a request made to EZID.
     * @throws IdentifierException if the response is error or body malformed.
     * @throws IOException         if the HTTP request fails.
     * @throws URISyntaxException  if host or authority is bad.
     */
    public EZIDResponse delete(String name)
        throws IOException, IdentifierException, URISyntaxException {
        // DELETE path
        HttpDelete request;
        URI uri = new URI(scheme, host, path + ID_PATH + authority + name, null);
        log.debug("EZID delete {}", uri.toASCIIString());
        request = new HttpDelete(uri);
        HttpResponse response = client.execute(request, httpContext);
        return new EZIDResponse(response);
    }

    /**
     * Remove a public identifier from view.
     *
     * @param name identifier name
     * @return Decoded response data evoked by a request made to EZID.
     * @throws IdentifierException if the response is error or body malformed.
     * @throws IOException         if the HTTP request fails.
     * @throws URISyntaxException  if host or authority is bad.
     */
    public EZIDResponse withdraw(String name)
        throws IOException, IdentifierException, URISyntaxException {
        Map<String, String> metadata = new HashMap<>();
        metadata.put(MD_KEY_STATUS, "unavailable");
        return modify(name, metadata);
    }

    /**
     * Remove a public identifier from view, with a reason.
     *
     * @param name   identifier name
     * @param reason annotation for the item's unavailability.
     * @return Decoded response data evoked by a request made to EZID.
     * @throws IdentifierException if the response is error or body malformed.
     * @throws IOException         if the HTTP request fails.
     * @throws URISyntaxException  if host or authority is bad.
     */
    public EZIDResponse withdraw(String name, String reason)
        throws IOException, IdentifierException, URISyntaxException {
        Map<String, String> metadata = new HashMap<>();
        metadata.put(MD_KEY_STATUS, "unavailable | " + escape(reason));
        return modify(name, metadata);
    }

    /**
     * Create ANVL-formatted name/value pairs from a Map.
     *
     * @param raw
     */
    private static String formatMetadata(Map<String, String> raw) {
        StringBuilder formatted = new StringBuilder();
        for (Entry<String, String> entry : raw.entrySet()) {
            formatted.append(escape(entry.getKey()))
                     .append(": ")
                     .append(escape(entry.getValue()))
                     .append('\n');
        }

        return formatted.toString();
    }

    /**
     * Percent-encode a few EZID-specific characters.
     *
     * @return null for null input.
     */
    private static String escape(String s) {
        if (null == s) {
            return s;
        }

        return s.replace("%", "%25")
                .replace("\n", "%0A")
                .replace("\r", "%0D")
                .replace(":", "%3A");
    }
}
