package com.github.ruediste1.btrbck.cli;

import java.io.Console;
import java.io.File;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.TreeMap;

import javax.inject.Inject;

import org.apache.log4j.Level;
import org.joda.time.Instant;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import com.github.ruediste1.btrbck.BtrfsService;
import com.github.ruediste1.btrbck.DisplayException;
import com.github.ruediste1.btrbck.GuiceModule;
import com.github.ruediste1.btrbck.SnapshotTransferService;
import com.github.ruediste1.btrbck.SshService;
import com.github.ruediste1.btrbck.StreamRepositoryService;
import com.github.ruediste1.btrbck.StreamService;
import com.github.ruediste1.btrbck.Util;
import com.github.ruediste1.btrbck.dom.ApplicationStreamRepository;
import com.github.ruediste1.btrbck.dom.BackupStreamRepository;
import com.github.ruediste1.btrbck.dom.RemoteRepository;
import com.github.ruediste1.btrbck.dom.Snapshot;
import com.github.ruediste1.btrbck.dom.SshTarget;
import com.github.ruediste1.btrbck.dom.Stream;
import com.github.ruediste1.btrbck.dom.StreamRepository;
import com.github.ruediste1.btrbck.dom.SyncConfiguration;
import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;
import com.google.common.io.ByteStreams;
import com.google.inject.Guice;
import com.google.inject.Injector;

public class CliMain {
	Logger log = LoggerFactory.getLogger(CliMain.class);

	@Option(name = "-r", usage = "the location of the stream repository to use")
	File repositoryLocation;

	@Option(name = "-c", usage = "if given, missing target streams will be created during the push, pull and the sync command")
	boolean createTargetStreams;

	@Option(name = "-a", usage = "if given, the initialize command creates an application stream repository")
	boolean applicationRepository;

	@Option(name = "-sudo", usage = "if given, use sudo to execute local btrfs commands")
	boolean sudoLocalBtrfs;

	@Option(name = "-strace", usage = "if given, use strace for certain commands. The output will be logged to strace.XXX.log files")
	boolean useStrace;

	@Option(name = "-sudoRemoteBtrbck", usage = "if given, use sudo to execute remote btrbck commands")
	boolean sudoRemoteBtrbck;

	@Option(name = "-sudoRemoteBtrfs", usage = "if given, use sudo to execute remote btrfs commands")
	boolean sudoRemoteBtrfs;

	@Option(name = "-v", usage = "show verbose output")
	boolean verbose;

	@Argument(hidden = true)
	List<String> arguments = new ArrayList<>();

	@Inject
	StreamRepositoryService streamRepositoryService;

	@Inject
	StreamService streamService;

	@Inject
	SnapshotTransferService streamTransferService;

	@Inject
	BtrfsService btrfsService;

	@Inject
	SshService sshService;

	private FileLock repositoryLock;

	public static void main(String... args) throws Exception {
		new CliMain().doMain(args);
	}

	private void doMain(String[] args) throws Exception {
		byte[] buf = new byte[4];
		new Random().nextBytes(buf);
		MDC.put("id", BaseEncoding.base16().encode(buf));

		Injector injector = Guice.createInjector(new GuiceModule());
		Util.setInjector(injector);
		Util.injectMembers(this);

		try {
			processCommand(args);
		}
		catch (DisplayException e) {
			System.err.println("Error: " + e.getMessage());
			System.exit(1);
		}
	}

	void processCommand(String... args) throws IOException {
		try {
			parseCmdLine(args);

			log.debug("args: " + Arrays.toString(args));
			log.debug("Arguments: " + arguments);

			String command = arguments.get(0);
			if (null != command) {
				switch (command) {
					case "snapshot":
						cmdSnapshot();
						break;
					case "list":
						cmdList();
						break;
					case "push":
						cmdPush();
						break;
					case "pull":
						cmdPull();
						break;
					case "process":
						cmdProcess();
						break;
					case "prune":
						cmdPrune();
						break;
					case "create":
						cmdCreate();
						break;
					case "delete":
						cmdDelete();
						break;
					case "restore":
						cmdRestore();
						break;
					case "receiveSnapshots":
						cmdReceiveSnapshots();
						break;
					case "sendSnapshots":
						cmdSendSnapshots();
						break;
					case "lock":
						cmdLock();
						break;
					case "version":
						cmdVersion();
						break;
					default:
						throw new DisplayException("Unknown command " + command);
				}
			}
		}
		finally {
			if (repositoryLock != null) {
				repositoryLock.release();
			}
		}
	}

	private void cmdVersion() {
		Properties properties = new Properties();
		try {
			properties.load(getClass()
					.getResourceAsStream("version.properties"));
		}
		catch (IOException e) {
			throw new RuntimeException(e);
		}
		System.out.println("BTRBCK version "
				+ properties.getProperty("btrbck.version"));
	}

	private void cmdLock() {
		readAndLockRepository();
		Console console = System.console();
		if (console != null) {
			console.printf("Repository locked. Press enter to unlock.\n");
			console.readLine();
			console.printf("Repository unlocked.\n");
		}
	}

	private void cmdSendSnapshots() {
		if (arguments.size() != 2) {
			throw new DisplayException("Usage: sendSnapshots <streamName>");
		}
		StreamRepository repo = readAndLockRepository();
		String streamName = arguments.get(1);
		streamTransferService.sendSnapshots(repo, streamName, System.in,
				System.out);
		streamService
				.pruneSnapshots(streamService.readStream(repo, streamName));
	}

	private void cmdReceiveSnapshots() {
		if (arguments.size() != 2) {
			throw new DisplayException("Usage: receiveSnapshots <streamName>");
		}
		StreamRepository repo = readAndLockRepository();
		String streamName = arguments.get(1);
		streamTransferService.receiveSnapshots(repo, streamName, System.in,
				System.out, createTargetStreams);
		streamService
				.pruneSnapshots(streamService.readStream(repo, streamName));
	}

	private void parseCmdLine(String[] args) {
		CmdLineParser parser = new CmdLineParser(this);

		// if you have a wider console, you could increase the value;
		// here 80 is also the default
		parser.setUsageWidth(80);

		try {
			// parse the arguments.
			parser.parseArgument(args);

			if (arguments.isEmpty()) {
				throw new CmdLineException(parser, "No command given");
			}
		}
		catch (CmdLineException e) {
			// if there's a problem in the command line,
			// you'll get this exception. this will report
			// an error message.
			System.err.println("Error: " + e.getMessage());

			try {
				ByteStreams.copy(getClass().getResourceAsStream("usage.txt"),
						System.err);
			}
			catch (IOException e1) {
				throw new RuntimeException("Error while printing usage", e1);
			}

			System.err.println("\n\nOptions: ");
			// print the list of available options
			parser.printUsage(System.err);
			System.err.println();

			System.exit(1);
		}

		// initialize sudoConfig
		btrfsService.setUseSudo(sudoLocalBtrfs);
		btrfsService.setUseStrace(useStrace);
		sshService.setSudoRemoteBtrbck(sudoRemoteBtrbck);
		sshService.setSudoRemoteBtrfs(sudoRemoteBtrfs);

		// configure loglevel
		if (verbose) {
			org.apache.log4j.Logger.getRootLogger().setLevel(Level.DEBUG);
			sshService.setVerboseRemote(verbose);
		}
	}

	private void cmdPrune() {
		if (arguments.size() == 1) {
			// prune all streams
			StreamRepository repo = readAndLockRepository();
			for (String streamName : streamService.getStreamNames(repo)) {
				Stream stream = streamService.readStream(repo, streamName);
				streamService.pruneSnapshots(stream);
				System.out.println("Pruned snapshots of " + streamName);
			}
		} else if (arguments.size() == 2) {
			// prune single stream
			String streamName = arguments.get(1);
			StreamRepository repo = readAndLockRepository();
			Stream stream = streamService.readStream(repo, streamName);
			streamService.pruneSnapshots(stream);
			System.out.println("Pruned snapshots of " + streamName);
		} else {
			throw new DisplayException("Illegal number of arguments");
		}
	}

	private void cmdProcess() {
		Instant now = Instant.now();
		StreamRepository repo = readAndLockRepository();
		for (String name : streamService.getStreamNames(repo)) {
			Stream stream = streamService.readStream(repo, name);
			streamService.takeSnapshotIfRequired(stream, now);
			streamService.pruneSnapshots(stream);

			for (SyncConfiguration config : repo.syncConfigurations) {
				if (config.isSynced(name)) {
					RemoteRepository remote = new RemoteRepository();
					remote.location = config.remoteRepoLocation;
					remote.sshTarget = SshTarget.parse(config.sshTarget);
					String remoteStreamName = name;
					if (!Strings.isNullOrEmpty(config.remoteStreamName)) {
						remoteStreamName = config.remoteStreamName;
					}
					switch (config.direction) {
						case PULL:
							streamTransferService.pull(repo, name, remote,
									remoteStreamName,
									config.createRemoteIfNecessary);
							break;

						case PUSH:
							streamTransferService.push(stream, remote,
									remoteStreamName,
									config.createRemoteIfNecessary);
							break;
						default:
							throw new RuntimeException("Should not happen");

					}
				}
			}
		}
		System.out.println("Processed repository");
	}

	private void cmdPush() {
		if (arguments.size() < 4) {
			throw new DisplayException("Not enough arguments");
		}

		if (arguments.size() > 5) {
			throw new DisplayException("Too many arguments");
		}

		String streamName = arguments.get(1);
		SshTarget sshTarget = SshTarget.parse(arguments.get(2));

		String remoteStreamName = streamName;
		if (arguments.size() == 5) {
			remoteStreamName = arguments.get(4);
		}

		RemoteRepository remoteRepo = new RemoteRepository();
		remoteRepo.location = arguments.get(3);
		remoteRepo.sshTarget = sshTarget;

		StreamRepository repo = readAndLockRepository();
		Stream stream = streamService.readStream(repo, streamName);

		streamTransferService.push(stream, remoteRepo, remoteStreamName,
				createTargetStreams);
		System.out.println("pushed shapshots of " + streamName);
	}

	private void cmdPull() {
		if (arguments.size() < 4) {
			throw new DisplayException("Not enought arguments");
		}

		if (arguments.size() > 5) {
			throw new DisplayException("Too many arguments");
		}

		SshTarget sshTarget = SshTarget.parse(arguments.get(1));
		String remoteRepoPath = arguments.get(2);

		String remoteStreamName = arguments.get(3);

		String streamName = remoteStreamName;
		if (arguments.size() == 5) {
			streamName = arguments.get(4);
		}

		RemoteRepository remoteRepo = new RemoteRepository();
		remoteRepo.location = remoteRepoPath;
		remoteRepo.sshTarget = sshTarget;

		StreamRepository repo = readAndLockRepository();

		streamTransferService.pull(repo, streamName, remoteRepo,
				remoteStreamName, createTargetStreams);

		System.out.println("pulled shapshots to " + streamName);
	}

	private void cmdList() {
		if (arguments.size() == 1) {
			// list streams in repository
			StreamRepository repo = readAndLockRepository();
			System.out.println("Streams in repository "
					+ repo.rootDirectory.toAbsolutePath() + ":");
			for (String name : streamService.getStreamNames(repo)) {
				System.out.println(name);
			}
		} else if (arguments.size() == 2) {
			StreamRepository repo = readAndLockRepository();
			Stream stream = streamService.readStream(repo, arguments.get(1));
			TreeMap<Integer, Snapshot> snapshots = streamService
					.getSnapshots(stream);
			System.out.println("Snapshots in stream " + stream.name
					+ " in repository " + repo.rootDirectory.toAbsolutePath()
					+ ":");
			for (Snapshot s : snapshots.values()) {
				System.out.println(s.getSnapshotName());
			}
		} else {
			throw new DisplayException("too many arguments");
		}

	}

	private void cmdCreate() throws IOException {
		if (arguments.size() == 1) {
			// create repository
			Path location;
			if (repositoryLocation != null) {
				location = repositoryLocation.toPath();
			} else {
				location = Paths.get("");
			}

			StreamRepository repo;
			if (applicationRepository) {
				repo = streamRepositoryService.createRepository(
						ApplicationStreamRepository.class, location);
			} else {
				repo = streamRepositoryService.createRepository(
						BackupStreamRepository.class, location);
			}

			System.out.println("Created "
					+ (applicationRepository ? "application" : "backup")
					+ " repository in " + repo.rootDirectory.toAbsolutePath());
		} else if (arguments.size() == 2) {
			// create stream
			String streamName = arguments.get(1);
			StreamRepository repo = readAndLockRepository();
			streamService.createStream(repo, streamName);
			System.out.println("created stream " + streamName);
		} else {
			throw new DisplayException("too many arguments");
		}
	}

	private void cmdDelete() {
		if (arguments.size() == 1) {
			StreamRepository repo = readAndLockRepository();
			// delete repository
			streamService.deleteStreams(repo);
			streamRepositoryService.deleteEmptyRepository(repo);
			System.out.println("Deleted repository");
		} else if (arguments.size() == 2) {
			StreamRepository repo = readAndLockRepository();
			String streamName = arguments.get(1);
			streamService.deleteStream(repo, streamName);
			System.out.println("Deleted " + streamName);
		} else {
			throw new DisplayException("too many arguments");
		}

	}

	private void cmdSnapshot() {
		if (arguments.size() == 1) {
			StreamRepository repo = readAndLockRepository();
			for (String streamName : streamService.getStreamNames(repo)) {
				Stream stream = streamService.readStream(repo, streamName);
				streamService.takeSnapshot(stream);
				System.out.println("took snapshot of " + streamName);
			}
		} else if (arguments.size() == 2) {
			StreamRepository repo = readAndLockRepository();
			String streamName = arguments.get(1);
			Stream stream = streamService.readStream(repo, streamName);
			streamService.takeSnapshot(stream);
			System.out.println("took snapshot of " + streamName);
		} else {
			throw new DisplayException("too many arguments");
		}
	}

	private void cmdRestore() {
		if (arguments.size() == 1) {
			// restore the latest snapshot of all streams
			StreamRepository repo = readAndLockRepository();
			for (String streamName : streamService.getStreamNames(repo)) {
				Stream stream = streamService.readStream(repo, streamName);
				streamService.restoreLatestSnapshot(stream);
				System.out.println("restored latest snapshot of " + streamName);
			}
		} else {
			if (arguments.size() == 2) {
				// restore the latest snapshot of a single stream
				String streamName = arguments.get(1);
				StreamRepository repo = readAndLockRepository();
				Stream stream = streamService.readStream(repo, streamName);
				streamService.restoreLatestSnapshot(stream);
				System.out.println("restored latest snapshot of " + streamName);
			} else if (arguments.size() == 3) {
				// restore a specific snapshot of a single stream
				String streamName = arguments.get(1);
				StreamRepository repo = readAndLockRepository();
				Stream stream = streamService.readStream(repo, streamName);
				int snapshotNr = Integer.parseInt(arguments.get(2));
				streamService.restoreSnapshot(stream, snapshotNr);
				System.out.println("restored snapshot " + snapshotNr + " of "
						+ streamName);
			} else {
				throw new DisplayException("too many arguments");
			}
		}

	}

	private StreamRepository readAndLockRepository() {
		File path = repositoryLocation;
		if (path == null) {
			path = Paths.get("").toFile();
		}
		StreamRepository repo = streamRepositoryService.readRepository(path
				.toPath());
		FileChannel f;
		try {
			if (!repo.getRepositoryLockFile().toFile().exists())
				throw new DisplayException(
						"Lock file not found: is this a stream repository?");
			f = FileChannel.open(repo.getRepositoryLockFile(),
					StandardOpenOption.WRITE);
			repositoryLock = f.lock(0L, Long.MAX_VALUE, false);
		}
		catch (IOException e) {
			throw new RuntimeException("Error while locking repository", e);
		}
		return repo;
	}
}