package org.postgresforest.tool.recovery;

import java.io.*;
import java.sql.*;
import org.postgresforest.tool.Logger;

public class ForestRecovery
{
	private static RecoveryManager rm = null;

	private static String gscURL = null;
	private static String remoteTemp = "/tmp";
	private static String localTemp  = "/tmp";
	private static String userName = "postgres";
	private static String userPass = "postgres";
	private static int refreshInterval = 0;
	private static int retryMax = 3;
	private static String sourceURL = null;
	private static String destURL = null;
	private static int timeout = 60;

	private static int server_id_src  = -1;
	private static int server_id_dest = -1;

	private static final int MODE_ALL      = 0;
	private static final int MODE_PHASE1   = 1;
	private static final int MODE_PHASE2   = 2;
	private static final int MODE_START_VM = 3;
	private static final int MODE_STOP_VM  = 4;
	private static final int MODE_CLEANUP  = 5;

	private static String dbname = null;

	private static int RECOVERY_MODE = MODE_ALL;

	private static String[] tableNames = null;

	private static int nLogRecordAll = 0;

	private static void printServerStatus()
	{
		try {
			Logger.debug("===== SERVER STATUS =====");
			Logger.debug("  serverId=" + server_id_src + ", status=" + rm.getServerStatus(server_id_src));
			Logger.debug("  serverId=" + server_id_dest + ", status=" + rm.getServerStatus(server_id_dest));
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(-1);
		}
	}

	private static void printUsage()
	{
		System.out.println("Usage: ForestRecovery [<options>...] [source server_id] [dest server_id] [dbname]");
		System.out.println("");
		System.out.println("Options:");
		System.out.println("        -user [str]         Administrator's user name");
		System.out.println("        -pass [str]         Administrator's password.");
		System.out.println("        -tmpdir [str]       Temporary directory used to dump and restore table(s).");
		System.out.println("        -remotetmpdir [str] Temporary directory for the master node.");
		System.out.println("        -localtmpdir [str]  Temporary directory for the recovering node.");
		System.out.println("                            This directory must be writable by backends.");
		System.out.println("        -gsc_url [str]      GSC URL");
		System.out.println("        -retry_max [num]    Max count for retrying.");
		System.out.println("        -timeout [num]      Socket timeout time in sec. (default:" + timeout + ")");
		System.out.println("");
		System.out.println("        -phase1             Run recovery phase 1.");
		System.out.println("        -phase2             Run recovery phase 2.");
		System.out.println("        -start_vm           Start PostgresForest VM.");
		System.out.println("        -stop_vm            Stop PostgresForest VM.");
		System.out.println("        -cleanup            Run to cleanup recovering.");
		System.out.println("");
		System.out.println("        -debug              Enable debug log output.");
		System.out.println("        -help               Print this help.");
		System.out.println("");
	}

	private static void parseOptions(String[] args)
	{
		for (int i=0 ; i<args.length ; i++)
		{
			if ( args[i].equals("-gsc_url") )
			{
				gscURL = args[++i];
			}
			else if ( args[i].equals("-tmpdir") )
			{
				localTemp = remoteTemp = args[++i];
			}
			else if ( args[i].equals("-remptetmpdir") )
			{
				remoteTemp = args[++i];
			}
			else if ( args[i].equals("-localtmpdir") )
			{
				localTemp = args[++i];
			}
			else if ( args[i].equals("-user") )
			{
				userName = args[++i];
			}
			else if ( args[i].equals("-pass") )
			{
				userPass = args[++i];
			}
			else if ( args[i].equals("-retry_max") )
			{
				retryMax = Integer.parseInt(args[++i]);
			}
			else if ( args[i].equals("-help") )
			{
				printUsage();
				System.exit(0);
			}
			else if ( args[i].equals("-timeout") )
			{
				timeout = Integer.parseInt(args[++i]);
			}
			else if ( args[i].equals("-debug") )
			{
				Logger.setLogLevel(Logger.DEBUG);
			}
			else if ( args[i].equals("-phase1") )
			{
				RECOVERY_MODE = MODE_PHASE1;
			}
			else if ( args[i].equals("-phase2") )
			{
				RECOVERY_MODE = MODE_PHASE2;
			}
			else if ( args[i].equals("-start_vm") )
			{
				RECOVERY_MODE = MODE_START_VM;
			}
			else if ( args[i].equals("-stop_vm") )
			{
				RECOVERY_MODE = MODE_STOP_VM;
			}
			else if ( args[i].equals("-cleanup") )
			{
				RECOVERY_MODE = MODE_CLEANUP;
			}
			else
			{
				/*
				if ( sourceURL==null )
					sourceURL = args[i];
				else if ( destURL==null )
					destURL = args[i];
				*/
				if ( server_id_src<0 )
					server_id_src = Integer.parseInt(args[i]);
				else if ( server_id_dest<0 )
					server_id_dest = Integer.parseInt(args[i]);
				else if ( dbname==null )
					dbname = args[i];
				else
				{
					System.err.println("Too many arguments.\n");
					printUsage();
					System.exit(-1);
				}
			}
		}
		//		if ( sourceURL==null || destURL==null )
		if ( server_id_src<0 || server_id_dest<0 )
		{
			System.err.println("Too few arguments.\n");
			printUsage();
			System.exit(-1);
		}
	}

	private static int applyAllRecords() throws RecoveryException
	{
		int rec = 0;

		while (true)
		{
			String log = rm.readLogRecord();
			
			if ( log==null ) // end of records
				break;

			rm.applyLogRecord(log);
			//			String flog = log + ";\n";
			//			fos.write( flog.getBytes() );

			rec++;

			if ( rec%50 == 0 )
				Logger.debug("   " + rec + " log records applied.");
		}
		return rec;
	}

	private static boolean preventUpdatingServer(int serverId)
		throws RecoveryException
	{
		boolean rc = false;
		
		Logger.debug("Set the table status to 1.");
		rm.setTableStatus(serverId, 1);  // to prevet updating tables on the server

		try {
			int sleep = 0;

			if ( refreshInterval<=10 )
				sleep = 15;
			else
				sleep = refreshInterval + 10;

			Logger.notice("  WAITING " + sleep + " SECONDS...");
			Logger.debug("Sleeping " + sleep + " secs...");
			Thread.sleep(sleep*1000);

		} catch (Exception e) {
			throw new RecoveryException("Thread.sleep() interrupted.");
		}

		int n = rm.getActiveTransactions();
		if ( n==1 )
			rc = true;
		Logger.debug("active backends = " + n);
		
		if ( rc )
			Logger.notice("  READY TO GO RECOVERY PHASE 2");

		//		Logger.debug("Set the server status to 1.");
		//		rm.setServerStatus(serverId, 1);  // activate the server

		return rc;
	}

	private static void abortRecovery()
	{
		String[] tableNames = null;

		Logger.notice("Aborting recovery...");

		try {
			tableNames = rm.getPhysicalTableNames(dbname);

			for (int i=0 ; i<tableNames.length ; i++)
				rm.stopRecoveryLogging(tableNames[i]);
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(-1);
		}

		Logger.notice("Recovery has been aborted.");
		System.exit(-1);
	}

	public static void initRecovery()
	{
		Logger.notice("RECOVERY / INITIALIZATION");

		try {
			tableNames = rm.getPhysicalTableNames(dbname);
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(-1);
		}

		Logger.notice("TABLE(S) TO COPY:");
		for (int i=0 ; i<tableNames.length ; i++)
			Logger.notice("  " + tableNames[i]);

		if ( tableNames==null )
		{
			System.err.println("No table found to be recovered.");
			System.exit(0);
		}

	}

	public static boolean checkGscEntries()
		throws RecoveryException
	{
		Logger.notice("GSC CHECK:");
		boolean rc = true;

		int src_dbno = rm.checkGscServdbEntry(dbname, server_id_src);

		int dest_dbno = rm.checkGscServdbEntry(dbname, server_id_dest);
		if ( dest_dbno>=0 )
		{
			Logger.notice("  forest_servdb, " + dbname + ": OK");
		}
		else
		{
			Logger.notice("  forest_servdb, " + dbname + ": NG");
			rc = false;

			/*
			 * エントリ作成
			 */
			Logger.notice("  CREATING:");
			dest_dbno = rm.createGscServdbEntry(dbname, server_id_dest);
			if ( rm.checkGscServdbEntry(dbname, server_id_dest) >= 0 )
			{
				Logger.notice("  forest_servdb, " + dbname + ": OK");
				rc = true;
			}
			else
				Logger.notice("  forest_servdb, " + dbname + ": NG");
		}

		String[] logicalTableNames = rm.getLogicalTableNames(dbname);

		for (int i=0 ; i<logicalTableNames.length ; i++)
		{
			if ( rm.checkGscTablepartdtlEntry(dest_dbno,
											  dbname, 
											  logicalTableNames[i]) )
			{
				Logger.notice("  forest_tablepartdtl, " + logicalTableNames[i] + ": OK");
			}
			else
			{
				Logger.notice("  forest_tablepartdtl, " + logicalTableNames[i] + ": NG");
				rc = false;

				/*
				 * エントリ作成
				 */
				Logger.notice("  CREATING:");
				rm.createGscTablepartdtlEntry(src_dbno, dest_dbno, dbname, logicalTableNames[i]);
				if ( rm.checkGscTablepartdtlEntry(dest_dbno,
												  dbname, 
												  logicalTableNames[i]) )
				{
					Logger.notice("  forest_tablepartdtl, " + logicalTableNames[i] + ": OK");
					rc = true;
				}
				else
					Logger.notice("  forest_tablepartdtl, " + logicalTableNames[i] + ": NG");

			}
		}

		return rc;
	}

	public static void RecoveryPhase1()
	{
		Logger.notice("RECOVERY / PHASE 1: start.");

		try {
			if ( !checkGscEntries() )
			{
				Logger.error("GSC entries invalid.");
				System.exit(-1);
			}
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(-1);
		}

		for (int i=0 ; i<tableNames.length ; i++)
		{
			try {
				rm.prepareRecoveryLogging(tableNames[i]);
			} catch (Exception e) {
				e.printStackTrace();
				System.exit(-1);
			}
		}


		/**
		 * Before database copy, need start logging modifications on ALL TABLES.
		 */
		for (int i=0 ; i<tableNames.length ; i++)
		{
			//Transaction is commited at each process of tables.
			try {
				rm.beginRecoveryLogging(tableNames[i]);
				Logger.debug("  Logger started, table => " + tableNames[i]);
			} catch (Exception e) {
				Logger.error(e.getMessage());
				System.exit(-1);
			}
		}

		/*
		 * Dump out all tables at once.
		 */
		//build output file names(include path)
		String[] tableFiles = new String[tableNames.length];
		String[] tableFilePaths = new String[tableNames.length];
		
		Logger.notice("COPYING ALL TABLES: start.");

		for (int i=0 ; i<tableNames.length ; i++)
		{
			tableFiles[i] = tableNames[i] + ".db";
			tableFilePaths[i] = remoteTemp + "/" + tableFiles[i];
		}
		
		try
		{
			//dump all tables out to file at once.
			//need to use full path file name.
			Logger.notice("  CREATE SNAPSHOT: start.");

			int xid = rm.createSnapshotCopy(tableNames,tableFilePaths);
			Logger.debug("   Snapshot xid=" + xid);
			
			//			rm.vacuumLogRecords();

            Logger.notice("  CREATE SNAPSHOT: done.");

            Logger.notice("  REBUILD FROM SNAPSHOT: start.");

            rm.disableFKs(tableNames);
            rm.truncateTables(tableNames);

			//send files and rebuild each tables.
			for (int i=0 ; i<tableNames.length ; i++)
			{
				rm.copyFileToLocal(remoteTemp + "/" + tableFiles[i],
								   localTemp + "/" + tableFiles[i]);
				rm.rebuildTableFromFile(tableNames[i], localTemp+"/"+tableFiles[i]);
			}
				
			rm.enableFKs(tableNames);

            Logger.notice("  REBUILD FROM SNAPSHOT: done.");

		} catch (Exception e) {
			Logger.error(e.getMessage());
			System.exit(-1);
		}

        Logger.notice("COPYING ALL TABLES: done.");

		Logger.notice("APPLYING LOG RECORD(S): start.");
		try {
			/**
			 * 1st pass for recoverying
			 */
			rm.openLogRecords(RecoveryManager.LOG_MINXID,
							  RecoveryManager.LOG_MAXXID);

			int max = rm.getLogMaxXid();
			int min = rm.getLogMinXid();
				
			Logger.debug("# of log records = " + rm.getNumberOfLogRecords());
				
			rm.beginApplyingLogRecords();
			nLogRecordAll = 0;
			int count = 0;

			{
				int rec = applyAllRecords();

				nLogRecordAll += rec;
				
				//				rm.removeLogRecords(min, max);
				rm.removeLogRecords();
				rm.commitApplyingLogRecords();

				rm.closeLogRecords();
				rm.vacuumLogRecords();
				Logger.debug("Num of applied log records: " + rec);
				Logger.debug("Total num of applied log records: " + nLogRecordAll);
			}

		} catch (Exception e) {
			e.printStackTrace();
			System.exit(-1);
		}
		Logger.notice("APPLYING LOG RECORD(S): done.");

		Logger.notice("RECOVERY / PHASE 1: done.");
	}

	public static void RecoveryPhase2()
	{
		Logger.notice("RECOVERY / PHASE 2: start.");

		try {
			if ( rm.getTableStatus(server_id_src)==0 )
			{
				Logger.error("TABLE IS NOT CLOSED.");
				System.exit(-1);
			}
		}
		catch (Exception e)
		{
			Logger.error(e.getMessage());
			System.exit(-1);
		}

		try {
			/**
			 * 2nd pass for recoverying
			 */
			Logger.debug("Entering 2nd pass for recoverying...");

			int max;
			int min;
			{
				/* open log records again */
				rm.openLogRecords(RecoveryManager.LOG_MINXID,
								  RecoveryManager.LOG_MAXXID);
				
				max = rm.getLogMaxXid();
				min = rm.getLogMinXid();

				rm.beginApplyingLogRecords();

				int rec = applyAllRecords();

				nLogRecordAll += rec;
				
				//				rm.removeLogRecords(min, max);
				rm.removeLogRecords();
				rm.commitApplyingLogRecords();

				rm.closeLogRecords();
				rm.vacuumLogRecords();
			}

			Logger.debug("avoiding from recovery loop.");
				
			/* open log records again */
			rm.openLogRecords(RecoveryManager.LOG_MINXID,
							  RecoveryManager.LOG_MAXXID);

			max = rm.getLogMaxXid();
			min = rm.getLogMinXid();

			// end of recoverying
			if ( rm.getNumberOfLogRecords()>0 )
			{
				Logger.error("Log record(s) left. Recovery failed.");
				abortRecovery();
			}

			rm.closeLogRecords();
			rm.vacuumLogRecords();
			Logger.debug("Total num of applied log records: " + nLogRecordAll);
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(-1);
		}

		Logger.notice("RECOVERY / PHASE 2: done");
	}

	public static void StartVM()
	{
		Logger.notice("RECOVERY / STARTING VM: start.");

		try {
			Logger.debug("Set the server status to 1.");
			rm.setServerStatus(server_id_dest, 1);  // activate the server

			rm.setTableStatus(server_id_src, 0);
			Logger.debug("Waking up.");

			printServerStatus();
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(-1);
		}

		Logger.notice("RECOVERY / STARTING VM: done.");
	}

	public static void StopVM()
	{
		Logger.notice("RECOVERY / STOPPING VM: start.");

		try {
			int i;
			for (i=0 ; i<retryMax ; i++)
			{
				if ( preventUpdatingServer(server_id_dest) )
					break;
				else
					Logger.debug("preventUpdatingServer() retrying...");
			}
			if ( i==retryMax )
			{
				Logger.error("Can't deter the transactions.");
				abortRecovery();
			}
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(-1);
		}
		Logger.notice("RECOVERY / STOPPING VM: done.");
	}

	public static void RecoveryCleanup()
	{
		Logger.notice("RECOVERY / CLEANING UP");

		try {
			for (int i=0 ; i<tableNames.length ; i++)
			{
				String tableName = tableNames[i];
				rm.stopRecoveryLogging(tableName);
			}

			rm.cleanupRecoveryLogging();
		} catch (Exception e) {
			Logger.error( e.getMessage() );
			System.exit(-1);
		}
		
	}

	public static void main(String[] args)
	{
		parseOptions(args);
	
		try {
			rm = new RecoveryManager();

			rm.connectToGSC(gscURL, userName, userPass);

			destURL   = rm.getUrlFromServerId(server_id_dest) + dbname;
			sourceURL = rm.getUrlFromServerId(server_id_src) + dbname;

			Logger.notice("RECOVERING:");
			Logger.notice("  FROM [" + server_id_src + "]" + sourceURL);
			Logger.notice("    TO [" + server_id_dest + "]" + destURL);

			rm.setTimeout(timeout);

			rm.createDatabaseConnections(destURL, userName, userPass,
										 sourceURL, userName, userPass);

			refreshInterval = rm.getCacheRefresh();

			Logger.notice("SERVER STATUS:");
			Logger.notice("  Src: Server " + server_id_src + ", Status "
						  + rm.getServerStatus(server_id_src));
			Logger.notice("  Dst: Server " + server_id_dest + ", Status "
						  + rm.getServerStatus(server_id_dest));

			Logger.notice("RECOVERY_MODE = " + RECOVERY_MODE);

			/* -- check the server status -- */
			if ( RECOVERY_MODE!=MODE_CLEANUP )
			{
				if ( rm.getServerStatus(server_id_src)!=1 )
				{
					Logger.error("Server " + server_id_src + " is not running. " +
								 "Can't recovery from the unavailable server.");
					System.exit(-1);
				}
				
				if ( rm.getServerStatus(server_id_dest)==1 )
				{
					Logger.error("Server " + server_id_dest + " is running. " +
								 "Can't start recovery on the running server.");
					System.exit(-1);
				}
			}
		}
		catch (Exception e)
		{
			e.printStackTrace();
			System.exit(-1);
		}

		/* --------------------------------
		 * Get table names to be recovered.
		 * --------------------------------
		 */
		initRecovery();

		/* ---------------------------------
		 * Prepare and start logging, and
		 * dump and restore tables.
		 * ---------------------------------
		 */
		if ( RECOVERY_MODE==MODE_ALL || RECOVERY_MODE==MODE_PHASE1 )
			RecoveryPhase1();

		/* ---------------------------------
		 * Stop Forest VM to apply all log records
		 * ---------------------------------
		 */
		if ( RECOVERY_MODE==MODE_ALL || RECOVERY_MODE==MODE_STOP_VM )
			StopVM();

		/* ---------------------------------
		 * Apply remaining log records,
		 * and restart Forest VM processing.
		 * ---------------------------------
		 */
		if ( RECOVERY_MODE==MODE_ALL || RECOVERY_MODE==MODE_PHASE2 )
			RecoveryPhase2();

		/* ---------------------------------
		 * Start Forest VM
		 * ---------------------------------
		 */
		if ( RECOVERY_MODE==MODE_ALL || RECOVERY_MODE==MODE_START_VM )
			StartVM();

		/* ---------------------------------
		 * Clean up the logging stuffs.
		 * ---------------------------------
		 */
		if ( RECOVERY_MODE==MODE_ALL || RECOVERY_MODE==MODE_CLEANUP )
			RecoveryCleanup();

		rm.closeAllConnections();

		Logger.debug("====== Recovery finished ======");
	}
}
