/*******************************************************************************
 * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the accompanying LICENSE file.
 *******************************************************************************/
package org.cryptomator.cryptofs.migration.v7;

import org.cryptomator.cryptofs.FileNameTooLongException;
import org.cryptomator.cryptofs.common.DeletingFileVisitor;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptofs.common.BackupHelper;
import org.cryptomator.cryptofs.migration.Migrators;
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener;
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener.ContinuationEvent;
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener.ContinuationResult;
import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
import org.cryptomator.cryptofs.migration.api.Migrator;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.EnumSet;

/**
 * Renames ciphertext names:
 *
 * <ul>
 *     <li>Files: BASE32== → base64==.c9r</li>
 *     <li>Dirs: 0BASE32== → base64==.c9r/dir.c9r</li>
 *     <li>Symlinks: 1SBASE32== → base64.c9r/symlink.c9r</li>
 * </ul>
 * <p>
 * Shortened names:
 * <ul>
 *     <li>shortened.lng → shortened.c9s</li>
 *     <li>m/shortened.lng → shortened.c9s/contents.c9r</li>
 * </ul>
 */
public class Version7Migrator implements Migrator {

	private static final Logger LOG = LoggerFactory.getLogger(Version7Migrator.class);

	private final SecureRandom csprng;

	@Inject
	public Version7Migrator(SecureRandom csprng) {
		this.csprng = csprng;
	}

	@Override
	public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws CryptoException, IOException {
		LOG.info("Upgrading {} from version 6 to version 7.", vaultRoot);
		progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0);
		final Path masterkeyFile = vaultRoot.resolve(masterkeyFilename);
		MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng);
		try (Masterkey masterkey = masterkeyFileAccess.load(masterkeyFile, passphrase)) {
			// create backup, as soon as we know the password was correct:
			Path masterkeyBackupFile = BackupHelper.attemptBackup(masterkeyFile);
			LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());

			// check file system capabilities:
			int filenameLengthLimit = new FileSystemCapabilityChecker().determineSupportedCiphertextFileNameLength(vaultRoot.resolve("c"), 46, 28, 220);
			int pathLengthLimit = filenameLengthLimit + 48; // TODO
			PreMigrationVisitor preMigrationVisitor;
			if (filenameLengthLimit >= 220) {
				LOG.info("Underlying file system meets filename length requirements.");
				preMigrationVisitor = new PreMigrationVisitor(vaultRoot, false);
			} else {
				LOG.warn("Underlying file system only supports names with up to {} chars (required: 220). Asking for user feedback...", filenameLengthLimit);
				ContinuationResult result = continuationListener.continueMigrationOnEvent(ContinuationEvent.REQUIRES_FULL_VAULT_DIR_SCAN);
				switch (result) {
					case PROCEED -> preMigrationVisitor = new PreMigrationVisitor(vaultRoot, true);
					case CANCEL -> {
						LOG.info("Migration canceled by user.");
						return;
					}
					default -> throw new IllegalStateException("Unexpected result " + result);
				}
			}

			// dry-run to collect stats:
			Path dataDir = vaultRoot.resolve("d");
			Files.walkFileTree(dataDir, EnumSet.noneOf(FileVisitOption.class), 3, preMigrationVisitor);

			// fail if ciphertext paths are too long:
			if (preMigrationVisitor.getMaxCiphertextPathLength() > pathLengthLimit) {
				LOG.error("Migration aborted due to unsupported path length (required {}) of underlying file system (supports {}). Vault is unchanged.", preMigrationVisitor.getMaxCiphertextPathLength(), pathLengthLimit);
				throw new FileNameTooLongException(preMigrationVisitor.getLongestPath().toString(), filenameLengthLimit);
			}

			// fail if ciphertext names are too long:
			if (preMigrationVisitor.getMaxCiphertextNameLength() > filenameLengthLimit) {
				LOG.error("Migration aborted due to unsupported filename length (required {}) of underlying file system (supports {}). Vault is unchanged.", preMigrationVisitor.getMaxCiphertextNameLength(), filenameLengthLimit);
				throw new FileNameTooLongException(preMigrationVisitor.getPathWithLongestName().toString(), filenameLengthLimit);
			}

			// start migration:
			long toBeMigrated = preMigrationVisitor.getTotalFileCount();
			LOG.info("Starting migration of {} files", toBeMigrated);
			if (toBeMigrated > 0) {
				migrateFileNames(vaultRoot, progressListener, toBeMigrated);
			}

			// cleanup:
			progressListener.update(MigrationProgressListener.ProgressState.FINALIZING, 0.0);
			Files.walkFileTree(vaultRoot.resolve("m"), DeletingFileVisitor.INSTANCE);

			// rewrite masterkey file with normalized passphrase:
			masterkeyFileAccess.persist(masterkey, masterkeyFile, passphrase, 7);
			LOG.info("Updated masterkey.");
		}
		LOG.info("Upgraded {} from version 6 to version 7.", vaultRoot);
	}

	private void migrateFileNames(Path vaultRoot, MigrationProgressListener progressListener, long totalFiles) throws IOException {
		assert totalFiles > 0;
		Path dataDir = vaultRoot.resolve("d");
		Files.walkFileTree(dataDir, EnumSet.noneOf(FileVisitOption.class), 3, new MigratingVisitor(vaultRoot, progressListener, totalFiles));
	}

	private void createBackup(Path masterkeyFile, Path configFile) throws IOException {
		final Path configBackupFile = BackupHelper.attemptBackup(configFile);
		LOG.info("Backed up config from {} to {}.", configFile.getFileName(), configBackupFile.getFileName());

		Path masterkeyBackupFile = BackupHelper.attemptBackup(masterkeyFile);
		LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());
	}
}

@@ -6192,458 +6192,6 @@
 %09%7D%0A%0A
-%09private void createBackup(Path masterkeyFile, Path configFile) throws IOException %7B%0A%09%09final Path configBackupFile = BackupHelper.attemptBackup(configFile);%0A%09%09LOG.info(%22Backed up config from %7B%7D to %7B%7D.%22, configFile.getFileName(), configBackupFile.getFileName());%0A%0A%09%09Path masterkeyBackupFile = BackupHelper.attemptBackup(masterkeyFile);%0A%09%09LOG.info(%22Backed up masterkey from %7B%7D to %7B%7D.%22, masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());%0A%09%7D%0A
 %7D%0A

remove unused function