/*
 * Copyright 2018-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.vault.core;

import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.fasterxml.jackson.databind.JsonNode;

import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.vault.client.VaultResponses;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultResponseSupport;
import org.springframework.vault.support.Versioned;
import org.springframework.vault.support.Versioned.Metadata;
import org.springframework.vault.support.Versioned.Metadata.MetadataBuilder;
import org.springframework.vault.support.Versioned.Version;
import org.springframework.web.client.HttpStatusCodeException;

/**
 * Default implementation of {@link VaultVersionedKeyValueOperations}.
 *
 * @author Mark Paluch
 * @author Maciej Drozdzowski
 * @since 2.1
 */
public class VaultVersionedKeyValueTemplate extends VaultKeyValue2Accessor implements VaultVersionedKeyValueOperations {

	private final VaultOperations vaultOperations;

	private final String path;

	/**
	 * Create a new {@link VaultVersionedKeyValueTemplate} given {@link VaultOperations}
	 * and the mount {@code path}.
	 * @param vaultOperations must not be {@literal null}.
	 * @param path must not be empty or {@literal null}.
	 */
	public VaultVersionedKeyValueTemplate(VaultOperations vaultOperations, String path) {

		super(vaultOperations, path);

		this.vaultOperations = vaultOperations;
		this.path = path;
	}

	@Nullable
	@Override
	@SuppressWarnings("unchecked")
	public Versioned<Map<String, Object>> get(String path, Version version) {

		Assert.hasText(path, "Path must not be empty");
		Assert.notNull(version, "Version must not be null");

		return (Versioned) doRead(path, version, Map.class);
	}

	@Nullable
	@Override
	public <T> Versioned<T> get(String path, Version version, Class<T> responseType) {

		Assert.hasText(path, "Path must not be empty");
		Assert.notNull(version, "Version must not be null");
		Assert.notNull(responseType, "Response type must not be null");

		return doRead(path, version, responseType);
	}

	@Nullable
	private <T> Versioned<T> doRead(String path, Version version, Class<T> responseType) {

		String secretPath = version.isVersioned()
				? String.format("%s?version=%d", createDataPath(path), version.getVersion()) : createDataPath(path);

		VersionedResponse response = this.vaultOperations.doWithSession(restOperations -> {

			try {
				return restOperations.exchange(secretPath, HttpMethod.GET, null, VersionedResponse.class).getBody();
			}
			catch (HttpStatusCodeException e) {

				if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
					if (e.getResponseBodyAsString().contains("deletion_time")) {

						return VaultResponses.unwrap(e.getResponseBodyAsString(), VersionedResponse.class);
					}

					return null;
				}

				throw VaultResponses.buildException(e, path);
			}
		});

		if (response == null) {
			return null;
		}

		VaultResponseSupport<JsonNode> data = response.getRequiredData();
		Metadata metadata = getMetadata(data.getMetadata());

		T body = deserialize(data.getRequiredData(), responseType);

		return Versioned.create(body, metadata);
	}

	@Override
	public Metadata put(String path, Object body) {

		Assert.hasText(path, "Path must not be empty");

		Map<Object, Object> data = new LinkedHashMap<>();
		Map<Object, Object> requestOptions = new LinkedHashMap<>();

		if (body instanceof Versioned) {

			Versioned<?> versioned = (Versioned<?>) body;

			data.put("data", versioned.getData());
			data.put("options", requestOptions);

			requestOptions.put("cas", versioned.getVersion().getVersion());
		}
		else {
			data.put("data", body);
		}

		VaultResponse response = doWrite(createDataPath(path), data);

		if (response == null) {
			throw new IllegalStateException(
					"VaultVersionedKeyValueOperations cannot be used with a Key-Value version 1 mount");
		}

		return getMetadata(response.getRequiredData());
	}

	private static Metadata getMetadata(Map<String, Object> responseMetadata) {

		MetadataBuilder builder = Metadata.builder();
		TemporalAccessor created_time = getDate(responseMetadata, "created_time");
		TemporalAccessor deletion_time = getDate(responseMetadata, "deletion_time");

		builder.createdAt(Instant.from(created_time));

		if (deletion_time != null) {
			builder.deletedAt(Instant.from(deletion_time));
		}

		if (Boolean.TRUE.equals(responseMetadata.get("destroyed"))) {
			builder.destroyed();
		}

		Integer version = (Integer) responseMetadata.get("version");
		builder.version(Version.from(version));

		return builder.build();
	}

	@Nullable
	private static TemporalAccessor getDate(Map<String, Object> responseMetadata, String key) {

		String date = (String) responseMetadata.getOrDefault(key, "");
		if (StringUtils.hasText(date)) {
			return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(date);
		}
		return null;
	}

	@Override
	public void delete(String path, Version... versionsToDelete) {

		Assert.hasText(path, "Path must not be empty");
		Assert.noNullElements(versionsToDelete, "Versions must not be null");

		if (versionsToDelete.length == 0) {
			delete(path);
			return;
		}

		List<Integer> versions = toVersionList(versionsToDelete);

		doWrite(createBackendPath("delete", path), Collections.singletonMap("versions", versions));
	}

	private static List<Integer> toVersionList(Version[] versionsToDelete) {
		return Arrays.stream(versionsToDelete).filter(Version::isVersioned).map(Version::getVersion)
				.collect(Collectors.toList());
	}

	@Override
	public void undelete(String path, Version... versionsToDelete) {

		Assert.hasText(path, "Path must not be empty");
		Assert.noNullElements(versionsToDelete, "Versions must not be null");

		List<Integer> versions = toVersionList(versionsToDelete);

		doWrite(createBackendPath("undelete", path), Collections.singletonMap("versions", versions));
	}

	@Override
	public void destroy(String path, Version... versionsToDelete) {

		Assert.hasText(path, "Path must not be empty");
		Assert.noNullElements(versionsToDelete, "Versions must not be null");

		List<Integer> versions = toVersionList(versionsToDelete);

		doWrite(createBackendPath("destroy", path), Collections.singletonMap("versions", versions));
	}

	@Override
	public VaultKeyValueMetadataOperations opsForKeyValueMetadata() {
		return new VaultKeyValueMetadataTemplate(this.vaultOperations, this.path);
	}

	private static class VersionedResponse extends VaultResponseSupport<VaultResponseSupport<JsonNode>> {

	}

}