/* $This file is distributed under the terms of the license in LICENSE$ */

package edu.cornell.mannlib.vivo.utilities.rdbmigration;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;

import org.apache.commons.lang3.StringUtils;

import org.apache.jena.db.DBConnection;
import org.apache.jena.db.GraphRDB;
import org.apache.jena.graph.Node;
import org.apache.jena.query.Dataset;
import org.apache.jena.sparql.core.DatasetGraph;
import org.apache.jena.tdb.TDBFactory;

/**
 * A free-standing application that walks the user through the process of
 * copying VIVO configuration data from RDB to TDB.
 */
public class RdbMigrator {
	private static final String DIRECTORY_TDB = "tdbModels";
	private static final String TABLE_RDB = "jena_graph";
	private static final String TABLE_MIGRATED = "vivo_rdb_migrated";
	private static final List<String> EXPECTED_MODELS = Arrays
			.asList(new String[] {
					"http://vitro.mannlib.cornell.edu/default/vitro-kb-displayMetadata",
					"http://vitro.mannlib.cornell.edu/default/vitro-kb-displayMetadata-displayModel",
					"http://vitro.mannlib.cornell.edu/default/vitro-kb-displayMetadataTBOX",
					"http://vitro.mannlib.cornell.edu/default/vitro-kb-userAccounts" });

	private final String vivoHomeDir;
	private final String jdbcUrl;
	private final String username;
	private final String password;

	private File targetDir;
	private boolean alreadyMigrated;
	private List<String> modelsToCopy;

	/**
	 * Confirm all of the parameters. Ask the user for approval. Do the
	 * migration.
	 * 
	 * @throws UserDeclinedException
	 *             If the user decides not to continue.
	 */
	public RdbMigrator(String vivoHomeDir, String jdbcUrl, String username,
			String password) throws UserDeclinedException, IOException,
			SQLException {
		this.vivoHomeDir = vivoHomeDir;
		this.jdbcUrl = jdbcUrl;
		this.username = username;
		this.password = password;

		testDbConnection();

		checkThatRdbExists();

		confirmTargetDirectory();

		if (doesTdbExist()) {
			askContinueOverTdb();
		}

		if (isAlreadyMigrated()) {
			askMigrateAgain();
		}

		getListOfRdbModels();

		if (isUnexpectedModels()) {
			askMigrateAllModels();
		}

		askApprovalForMigrationPlan();

		migrate();
	}

	private void testDbConnection() {
		try (Connection conn = getSqlConnection()) {
			// Just open and close it.
		} catch (SQLException e) {
			quit("Can't log in to database: '" + jdbcUrl + "', '" + username
					+ "', '" + password + "'\n" + e.getMessage());
		}
	}

	private void checkThatRdbExists() throws SQLException {
		try (Connection conn = getSqlConnection()) {
			DatabaseMetaData md = conn.getMetaData();
			try (ResultSet rs = md.getTables(null, null, TABLE_RDB, null);) {
				if (!rs.next()) {
					quit("The database at '" + jdbcUrl
							+ "' contains no RDB tables.");
				}
			}
		}
	}

	private void confirmTargetDirectory() {
		File vivoHome = new File(vivoHomeDir);
		if (!vivoHome.isDirectory()) {
			quit("'" + vivoHome + "' is not a directory.");
		}
		if (!vivoHome.canWrite()) {
			quit("Can't write to '" + vivoHome + "'.");
		}
		targetDir = new File(vivoHome, DIRECTORY_TDB);
	}

	private boolean doesTdbExist() {
		if (!targetDir.exists()) {
			return false;
		}

		if (!targetDir.isDirectory()) {
			quit("'" + targetDir + "' is not a directory.");
		}
		if (!targetDir.canWrite()) {
			quit("Can't write to '" + targetDir + "'.");
		}

		String[] filenames = targetDir.list();
		if (filenames == null || filenames.length == 0) {
			return false;
		}

		return true;
	}

	private void askContinueOverTdb() throws UserDeclinedException, IOException {
		ask("A directory of TDB files exists at '" + targetDir + "'.\n"
				+ "   Migration will replace the existing triples.\n"
				+ "Continue? (y/n)");
	}

	private boolean isAlreadyMigrated() throws SQLException {
		try (Connection conn = getSqlConnection()) {
			DatabaseMetaData md = conn.getMetaData();
			try (ResultSet rs = md.getTables(null, null, TABLE_MIGRATED, null);) {
				if (rs.next()) {
					alreadyMigrated = true;
					announceMigrationDate(conn);
					return true;
				} else {
					return false;
				}
			}
		}
	}

	private void announceMigrationDate(Connection conn) {
		String migrationDate = "UNKNOWN DATE";
		String query = String.format("SELECT date FROM %s LIMIT 1",
				TABLE_MIGRATED);

		try (Statement stmt = conn.createStatement();
				java.sql.ResultSet rs = stmt.executeQuery(query)) {
			if (rs.next()) {
				migrationDate = rs.getString("DATE");
			}
		} catch (SQLException e) {
			// go with default answer.
		}

		System.out.println("It looks like this RDB data has already been "
				+ "migrated to TDB, on " + migrationDate
				+ "\n   (found a table named '" + TABLE_MIGRATED + "')");
	}

	private void askMigrateAgain() throws UserDeclinedException, IOException {
		ask("Migrate again? (y/n)");
	}

	private void getListOfRdbModels() throws SQLException {
		try (Connection conn = getSqlConnection();
				ClosingDBConnection rdb = new ClosingDBConnection(conn)) {
			modelsToCopy = rdb.getAllModelNames().toList();
		}
	}

	private boolean isUnexpectedModels() {
		List<String> unexpectedModels = new ArrayList<>(modelsToCopy);
		unexpectedModels.removeAll(EXPECTED_MODELS);
		if (unexpectedModels.isEmpty()) {
			return false;
		}

		System.out
				.println("VIVO requires only these models from RDB:\n   "
						+ StringUtils.join(EXPECTED_MODELS, "\n   ")
						+ "\nYour RDB triple-store contains these additional models:\n   "
						+ StringUtils.join(unexpectedModels, "\n   "));
		return true;
	}

	private void askMigrateAllModels() throws IOException, UserDeclinedException {
		System.out.println("Migrate all models, or only the required models?\n"
				+ "   Enter 'all' or 'only', or anything else to quit.");
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		String s = br.readLine();
		if (s != null) {
			if (s.trim().toLowerCase().equals("all")) {
				return;
			} else if (s.trim().toLowerCase().equals("only")) {
				modelsToCopy = EXPECTED_MODELS;
				return;
			}
		}
		throw new UserDeclinedException("Enter 'all' or 'only'.");
	}

	private void askApprovalForMigrationPlan() throws SQLException,
			UserDeclinedException, IOException {
		int modelCount = 0;
		int tripleCount = 0;
		try (Connection conn = getSqlConnection();
				ClosingDBConnection rdb = new ClosingDBConnection(conn)) {
			for (String modelName : modelsToCopy) {
				modelCount++;
				try (ClosingGraphRDB graph = new ClosingGraphRDB(rdb, modelName)) {
					tripleCount += graph.size();
				}
			}
		}
		String warning = alreadyMigrated ? "   Existing triples will be over-written.\n"
				: "";
		String question = String.format("Migrating %d triples in %d models "
				+ "to TDB files in '%s'\n%sContinue? (y/n)", tripleCount,
				modelCount, targetDir, warning);
		ask(question);
	}

	private void quit(String message) {
		throw new IllegalArgumentException(
				"--------------------------------------------------------------\n"
						+ message
						+ "\n--------------------------------------------------------------");
	}

	private void ask(String string) throws UserDeclinedException, IOException {
		System.out.println(string);
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		String s = br.readLine();
		if ((s == null) || (!s.trim().toLowerCase().equals("y"))) {
			throw new UserDeclinedException("OK.");
		}
	}

	private static class UserDeclinedException extends Exception {
		public UserDeclinedException(String message) {
			super(message);
		}
	}

	public void migrate() throws SQLException {
		copyData();
		writeMigrationRecord();
	}

	private void copyData() throws SQLException {
		try (Connection conn = getSqlConnection();
				ClosingDBConnection rdb = new ClosingDBConnection(conn)) {
			Dataset tdbDataset = TDBFactory.createDataset(targetDir
					.getAbsolutePath());
			copyGraphs(rdb, tdbDataset);
		}
	}

	@SuppressWarnings("deprecation")
	private void copyGraphs(ClosingDBConnection rdb, Dataset tdbDataset) {
		DatasetGraph tdbDsGraph = tdbDataset.asDatasetGraph();
		for (String modelName : modelsToCopy) {
			try (ClosingGraphRDB graph = new ClosingGraphRDB(rdb, modelName)) {
				tdbDsGraph.addGraph(Node.createURI(modelName), graph);
				System.out
						.println(String.format("  copied %4d triples from %s",
								graph.size(), modelName));
			}
		}
	}

	private void writeMigrationRecord() throws SQLException {
		String createTable = String.format("CREATE TABLE %s (date DATE)",
				TABLE_MIGRATED);
		String deleteOldDates = String.format("DELETE FROM %s", TABLE_MIGRATED);
		String insertDate = String.format("INSERT INTO %s (date) VALUES (?)",
				TABLE_MIGRATED);
		try (Connection conn = getSqlConnection();
				Statement stmt = conn.createStatement();
				PreparedStatement pstmt = conn.prepareStatement(insertDate)) {
			if (alreadyMigrated) {
				stmt.executeUpdate(deleteOldDates);
			} else {
				stmt.executeUpdate(createTable);
			}
			pstmt.setDate(1, new Date(System.currentTimeMillis()));
			pstmt.executeUpdate();
		}
	}

	private Connection getSqlConnection() throws SQLException {
		Properties connectionProps;
		connectionProps = new Properties();
		connectionProps.put("user", username);
		connectionProps.put("password", password);
		return DriverManager.getConnection(jdbcUrl, connectionProps);
	}

	private static class ClosingDBConnection extends DBConnection implements
			AutoCloseable {
		ClosingDBConnection(Connection sqlConnection) {
			super(sqlConnection, "MySQL");
		}

	}

	private static class ClosingGraphRDB extends GraphRDB implements
			AutoCloseable {
		ClosingGraphRDB(DBConnection rdb, String modelName) {
			super(rdb, modelName, null,
					GraphRDB.OPTIMIZE_ALL_REIFICATIONS_AND_HIDE_NOTHING, false);
		}
	}

	// ----------------------------------------------------------------------
	// Main routine
	// ----------------------------------------------------------------------

	@SuppressWarnings("unused")
	public static void main(String[] args) {
		if (args.length != 4) {
			System.out.println("Usage: RdbMigrator vivoHomeDir, jdbcUrl, "
					+ "username, password");
		}

		try {
			new RdbMigrator(args[0], args[1], args[2], args[3]);
		} catch (IllegalArgumentException | UserDeclinedException e) {
			System.out.println(e.getMessage());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}
