package aQute.bnd.deployer.obr;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import aQute.bnd.deployer.repository.AbstractIndexedRepo;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Verifier;
import aQute.bnd.version.Version;
import aQute.lib.base64.Base64;
import aQute.lib.io.IO;
import aQute.lib.io.IOConstants;

/**
 * A read-write nexus OBR-based repository.
 * <p>
 * You will need to install the nexus-obr-plugin in nexus (you can download it
 * on <a href="http://search.maven.org/#search|ga|1|a%3A%22nexus-obr-plugin%22">
 * maven central</a>)
 * </p>
 * <p>
 * <h2>Properties</h2>
 * <ul>
 * <li><b>repositoryUrl</b>: the nexus repository url
 * (http://localhost:8081/nexus/content/repositories/obr/ for example)</li>
 * <li><b>username</b>: the username; defaults to "deployment"</li>
 * <li><b>password</b>: the password; defaults to "deployment123"</li>
 * <li><b>name</b>: repository name; defaults to the nexus repository url</li>
 * <li><b>cache</b>: local cache directory. May be omitted, in which case a
 * default directory will be used.</li>
 * <li><b>readonly</b>: if readonly, no bundle can be added to the repository
 * </ul>
 * </p>
 * <p>
 * <h2>Example</h2>
 * 
 * <pre>
 *  -plugin:
 * aQute.bnd.deployer.obr.NexusOBR;readonly=false;repositoryUrl=http://localhost
 * :8081/nexus/content/repositories/obr/;username=deployment;password=
 * deployment123;name=nexus-obr
 * </pre>
 * </p>
 * 
 * @author Cedric Chabanois <cchabanois at gmail.com>
 */
public class NexusOBR extends AbstractIndexedRepo {
	static final int			BUFFER_SIZE				= IOConstants.PAGE_SIZE * 2;

	private static final String	DEFAULT_PASSWORD		= "deployment123";
	private static final String	DEFAULT_USERNAME		= "deployment";
	private static final String	EMPTY_REPOSITORY_URL	= "";
	private static final String	DEFAULT_CACHE_DIR		= ".bnd" + File.separator + "cache";
	public static final String	PROP_CACHE				= "cache";
	public static final String	PROP_REPOSITORY_URL		= "repositoryUrl";
	public static final String	PROP_READONLY			= "readonly";
	public static final String	PROP_USERNAME			= "username";
	public static final String	PROP_PASSWORD			= "password";
	public static final String	PROP_PUTURL				= "puturl";
	protected File				cacheDir				= new File(
			System.getProperty("user.home") + File.separator + DEFAULT_CACHE_DIR);
	private String				nexusRepositoryUrl;
	private boolean				readOnly;
	private String				username				= DEFAULT_USERNAME;
	private String				password				= DEFAULT_PASSWORD;
	/**
	 * if null, nexusRepositoryUrl is used for puts.
	 */
	private String				puturl					= null;

	@Override
	public synchronized void setProperties(Map<String,String> map) {
		super.setProperties(map);
		readOnly = Boolean.parseBoolean(map.get(PROP_READONLY));
		if (map.containsKey(PROP_USERNAME)) {
			username = map.get(PROP_USERNAME);
		}
		if (map.containsKey(PROP_PASSWORD)) {
			password = map.get(PROP_PASSWORD);
		}
		nexusRepositoryUrl = map.get(PROP_REPOSITORY_URL);
		if (nexusRepositoryUrl != null && !nexusRepositoryUrl.endsWith("/")) {
			nexusRepositoryUrl = nexusRepositoryUrl + '/';
		}
		if (map.containsKey(PROP_PUTURL)) {
			puturl = map.get(PROP_PUTURL);
		}
		if (puturl != null && !puturl.endsWith("/")) {
			puturl = puturl + '/';
		}

		String cachePath = map.get(PROP_CACHE);
		if (cachePath != null) {
			cacheDir = new File(cachePath);
			if (!cacheDir.isDirectory())
				try {
					throw new IllegalArgumentException(String.format(
							"Cache path '%s' does not exist, or is not a directory.", cacheDir.getCanonicalPath()));
				} catch (IOException e) {
					throw new IllegalArgumentException("Could not get cacheDir canonical path", e);
				}
		}
	}

	@Override
	public boolean canWrite() {
		return true;
	}

	public File getCacheDirectory() {
		return cacheDir;
	}

	public String getLocation() {
		if (nexusRepositoryUrl == null)
			return EMPTY_REPOSITORY_URL;
		else
			return nexusRepositoryUrl;

	}

	@Override
	protected List<URI> loadIndexes() throws Exception {
		List<URI> result;
		if (nexusRepositoryUrl != null) {
			result = new ArrayList<URI>();

			result.add(new URL(nexusRepositoryUrl + ".meta/obr.xml").toURI());
		} else {
			result = Collections.emptyList();
		}
		return result;
	}

	public void setCacheDirectory(File cacheDir) {
		if (cacheDir == null)
			throw new IllegalArgumentException("null cache directory not permitted");
		this.cacheDir = cacheDir;
	}

	@Override
	public synchronized PutResult put(InputStream stream, PutOptions options) throws Exception {
		/* determine if the put is allowed */
		if (readOnly) {
			throw new IOException("Repository is read-only");
		}

		if (options == null)
			options = DEFAULTOPTIONS;

		/* both parameters are required */
		if (stream == null)
			throw new IllegalArgumentException("No stream and/or options specified");

		/*
		 * setup a new stream that encapsulates the stream and calculates (when
		 * needed) the digest
		 */
		DigestInputStream dis = new DigestInputStream(stream, MessageDigest.getInstance("SHA-1"));

		File tmpFile = null;
		try {
			/*
			 * copy the artifact from the (new/digest) stream into a temporary
			 * file in the root directory of the repository
			 */
			tmpFile = IO.createTempFile(null, "put", ".bnd");
			IO.copy(dis, tmpFile);

			/* beforeGet the digest if available */
			byte[] disDigest = dis.getMessageDigest().digest();

			if (options.digest != null && !Arrays.equals(options.digest, disDigest))
				throw new IOException("Retrieved artifact digest doesn't match specified digest");

			/* put the artifact into the repository (from the temporary file) */
			URL url = putArtifact(tmpFile);

			PutResult result = new PutResult();
			if (url != null) {
				result.digest = disDigest;
				result.artifact = url.toURI();
			}

			return result;
		} finally {
			if (tmpFile != null && tmpFile.exists()) {
				IO.delete(tmpFile);
			}
		}
	}

	protected URL putArtifact(File tmpFile) throws Exception {
		assert (tmpFile != null);
		assert (tmpFile.isFile());

		init();

		Version version;
		String bsn;
		try (Jar jar = new Jar(tmpFile)) {
			bsn = jar.getBsn();
			if (bsn == null || !Verifier.isBsn(bsn))
				throw new IllegalArgumentException(
						"Jar does not have a " + Constants.BUNDLE_SYMBOLICNAME + " manifest header");

			String versionString = jar.getVersion();
			if (versionString == null)
				versionString = "0";
			else if (!Verifier.isVersion(versionString))
				throw new IllegalArgumentException("Invalid version " + versionString + " in file " + tmpFile);

			version = Version.parseVersion(versionString);
		}
		URL url = put(tmpFile, bsn, version);
		reset();
		return url;
	}

	protected URL put(File file, String bsn, Version version) throws IOException {
		URL url = getTargetURL(bsn, version);
		try (InputStream is = IO.stream(file)) {
			HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
			httpUrlConnection.setDoOutput(true);
			httpUrlConnection.setFixedLengthStreamingMode((int) file.length());
			httpUrlConnection.setRequestMethod("PUT");
			if (username != null && password != null) {
				String userPassword = username + ":" + password;
				httpUrlConnection.setRequestProperty("Authorization",
						"Basic " + Base64.encodeBase64(userPassword.getBytes(UTF_8)));
			}

			try (OutputStream out = httpUrlConnection.getOutputStream()) {
				byte[] buffer = new byte[BUFFER_SIZE];
				while (true) {
					int length = is.read(buffer);
					if (length < 0)
						break;
					out.write(buffer, 0, length);
				}
				int respondeCode = httpUrlConnection.getResponseCode();
				// response code will be 201 (Created) if new bundle is
				// successfully
				// added
				if (respondeCode < 200 || respondeCode > 300) {
					throw new IOException(httpUrlConnection.getResponseMessage());
				}
			} finally {
				httpUrlConnection.disconnect();
			}
		}
		return url;
	}

	@Override
	public synchronized String getName() {
		if (name != null && !name.equals(this.getClass().getName()))
			return name;

		return nexusRepositoryUrl;
	}

	private URL getTargetURL(String bsn, Version version) throws MalformedURLException {
		String url = puturl != null ? puturl : nexusRepositoryUrl;
		return new URL(url + bsn + "/" + bsn + "-" + version + ".jar");
	}

	public File getRoot() {
		return cacheDir;
	}

}
