package syndiff;

import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.stream.Collectors;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.yaml.snakeyaml.Yaml;

public class Syndiff {
	private String cfgFileName;
	private Config cfg;
	private MetaDic src;
	private MetaDic tgt;
	private Map<String, String> schemap;
	private final int DIFF_TABLE = 1;
	private final int DIFF_VIEW  = 2;
	private final int DIFF_INDEX = 3;
	private final int DIFF_TABLE_ROWS = 4;
	private final String DF_MATCHED = "S";
	private final String DF_SOURCE_MISSED = "L";
	private final String DF_TARGET_MISSED = "R";
	public Syndiff(String cfgfile) {
		cfgFileName = cfgfile;
	}
	public void doCompare() {
		cfg = getConfig(cfgFileName);
		if (cfg == null) {
			System.out.println("Cann't get confilg.");
			System.exit(1);
		}
		schemap = cfg.getSchemaMapping();
		displayParam();
		src = new MetaDic(cfg.getSourcedb());
		tgt = new MetaDic(cfg.getTidb());
		if (!src.initConnection() || !tgt.initConnection()) {
			System.out.println("Cann't init database connection.");
			System.exit(2);
		}
		
		String[] schemas = getSchemas();
		if (cfg.checkTable()) {
			logMessage("begin to check table info ......\n");
			if (cfg.checkRows()) {
				int srcthreads = cfg.getSourceCheckThreads();
				int tgtthreads = cfg.getTargetCheckThreads();
				Thread t1,t2;
				List<String> L1 = new ArrayList<String> ();
				List<String> L2 = new ArrayList<String> ();
				t1 = new Thread(() -> { L1.addAll(src.getTablesWithRows(schemas[0], srcthreads, cfg.getCheckRowTimeout()));});
				t2 = new Thread(() -> { L2.addAll(tgt.getTablesWithRows(schemas[1], tgtthreads, cfg.getCheckRowTimeout()));});
				t1.start(); 
				t2.start();
				try {
					t1.join();
					t2.join();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				
				Map<String, String>[] dt = compare(L1,L2,DIFF_TABLE_ROWS);
				printDiff(dt,DIFF_TABLE_ROWS);
			} else {
				Map<String, String>[] dt = compare(src.getTables(schemas[0]), tgt.getTables(schemas[1]),DIFF_TABLE);
				printDiff(dt,DIFF_TABLE);
			}
		}
		if (cfg.checkView()) {
			System.out.println("");
			logMessage("begin to check view info ......\n");
			Map<String, String>[] dv = compare(src.getViews(schemas[0]), tgt.getViews(schemas[1]),DIFF_VIEW);
			printDiff(dv,DIFF_VIEW);
		}
		if (cfg.checkIndex()) {
			System.out.println("");
			logMessage("begin to check index info ......\n");
			Map<String, String>[] di = compare(src.getIndexes(schemas[0]), tgt.getIndexes(schemas[1]),DIFF_INDEX);
			printDiff(di,DIFF_INDEX);
		}
		logMessage("check completed.\n");
	}

	/**
	 * print schema different detail
	 * @param dt: dt[0] all source objects   dt[1] source missed objects
	 * @param objtype
	 */
	private void printDiff(Map<String, String>[] dt, int objtype) {
		int sourcemissed = dt[1].size();
		int targetmissed = 0;
		int matched = 0;
		int maxlength = 0;
		int rows_not_matched = 0;
		int len = 0;
		String strTgtMissed = "does not exist";
		for(Entry<String, String> e:dt[0].entrySet()) {
			String[] value = e.getValue().split(";");
			for(String v: value) {
				if (v.startsWith(DF_MATCHED)) {
					matched++;
					len = v.length();
				} else if (v.startsWith(DF_TARGET_MISSED)) {
					targetmissed++;
					len = strTgtMissed.length();
				} else {
					rows_not_matched++;
					len = v.length();
				}
			};
			len += e.getKey().length();
			if (maxlength < len) maxlength = len;
		};
		if (dt[1].size() > 0) {
			len = dt[1].keySet().stream().mapToInt(String::length).max().getAsInt();
			if (maxlength < len) maxlength = len;
		}
		
		maxlength = maxlength > 60 ? maxlength : 60;
		if(objtype == DIFF_INDEX) maxlength +=  6;
		if(objtype == DIFF_TABLE_ROWS) maxlength += 10;
		if (maxlength % 2 != 0) maxlength++;
		String stype = objtype == DIFF_VIEW ? "views" : objtype == DIFF_INDEX ? "indexes" : "tables";
		
		System.out.printf("\n%-16s:%6d\n", "Source " + stype, dt[0].size());
		System.out.printf("%-16s:%6d\n", "Target " + stype, (matched + sourcemissed + rows_not_matched));
		if (objtype == this.DIFF_TABLE_ROWS) {
			System.out.printf("%-16s:%6d\n", "Rows matched", matched);
			System.out.printf("%-16s:%6d\n", "Rows not matched", rows_not_matched);
		} else {
			System.out.printf("%-16s:%6d\n", "Both matched", matched);
		}
		
		System.out.printf("%-16s:%6d\n", "Source missed", sourcemissed);
		System.out.printf("%-16s:%6d\n", "Target missed", targetmissed);
		
		System.out.printf(String.format("%%-%ds%%%ds\n", maxlength/2, maxlength/2)
				,"======== " + cfg.getSourcedb().get("dbtype") + " " + stype + " ========"
				,"======== " + cfg.getTidb().get("dbtype")     + " " + stype + " ========");
		
		int linelength = maxlength;
		dt[0].keySet().stream().sorted().forEach(x -> {
			String[] value = dt[0].get(x).split(";");
			Arrays.stream(value).sorted().forEach( s -> {
				String[] mapstr = s.split(":");
				String schema = mapstr.length > 1 ? ":" + mapstr[1] : "";
				String k = x;
				String v = "";
				if (objtype == DIFF_TABLE_ROWS) {
					k = x + " Rows";
				} else if(objtype == DIFF_INDEX) {
					k = formatIndexString(x);
				}
				if (s.startsWith(DF_MATCHED)) {
					v =  "√" + schema;
				} else if (s.startsWith(DF_TARGET_MISSED)) {
					v = strTgtMissed;
				} else {
					if (objtype == DIFF_TABLE_ROWS) {
						v = String.format("%s Rows%s", mapstr[0], schema);
					}
				}
				System.out.printf(String.format("%%-%ss%%%ss\n", k.length(),linelength - k.length()),k, v);
				
			});
		});
		String fmt = String.format("-%%%ds\n", maxlength - 1);
		dt[1].keySet().stream().sorted().forEach(x -> {
			System.out.printf(fmt, (objtype == DIFF_INDEX ? formatIndexString(x):x) + (objtype == DIFF_TABLE_ROWS ? " Rows" : "")); 
		});
		
	}
	private String formatIndexString(String str) {
		String[] s = str.split("\\.");
		return String.format("[%s] %s.%s:(%s)", s[2],s[0],s[1],s[3]);
	}
	private String[] splitTableNameAndRows(String str) {
		return str.split(":");
	}
	private Map<String, String>[]compare(List<String> source, List<String> target, int objtype) {
		Map<String, String> src = new HashMap<String, String>(256);
		source.forEach(x -> {
			String key = x.toLowerCase();
			if (objtype == DIFF_TABLE_ROWS) {
				key = splitTableNameAndRows(key)[0];
			}
			src.put(key, x);
		});
		
		Map<String, String> tmpSrcAll = new HashMap<String, String>(256);
		Map<String, String> mapSrcMissed = new HashMap<String, String>(256);
		target.forEach(t -> {
			String key = t.toLowerCase();
			String schema = t.substring(0, t.indexOf('.'));
			String mapschema = schemap.get(schema);
			if (mapschema != null) {
				key = key.replaceFirst(schema, mapschema);
			}
			
			if (objtype == DIFF_TABLE_ROWS) {
				String[] namecount = splitTableNameAndRows(key); 
				key = namecount[0];
				if (src.get(key) != null) {
					String rowvalue = "";
					if (!namecount[1].equals("-1") && splitTableNameAndRows(src.get(key))[1].equals(namecount[1])) {
						rowvalue = String.format("%s:%s", DF_MATCHED, mapschema != null ? "(" + schema + ")" : ""); 
					} else {
						rowvalue = String.format("%s:%s", namecount[1], mapschema != null ? "(" + schema + ")" : ""); 
					}
					
					String v = tmpSrcAll.get(key);
					if (v == null) {
						tmpSrcAll.put(key, rowvalue);
					} else {
						tmpSrcAll.put(key, v + ";" + rowvalue);
					}
				} else {
					mapSrcMissed.put(t, DF_SOURCE_MISSED + ":");
				}
			} else {
				if (src.get(key) != null) {
					String v = tmpSrcAll.get(key);
					if (v == null) {
						tmpSrcAll.put(key, String.format("%s:%s",  DF_MATCHED, mapschema != null ? "(" + schema + ")" : ""));
					}else {
						tmpSrcAll.put(key, v + String.format(";%s:%s",  DF_MATCHED, mapschema != null ? "(" + schema + ")" : ""));
					}
				} else {
					mapSrcMissed.put(t, DF_SOURCE_MISSED + ":"); 
				}
			}
		});
		Map<String, String> mapSrcAll = new HashMap<String, String>(256);
		src.entrySet().forEach(e -> {
			String v = tmpSrcAll.get(e.getKey());
			if ( v != null) {
				mapSrcAll.put(e.getValue(), v);
			} else {
				mapSrcAll.put(e.getValue(), DF_TARGET_MISSED + ":");
			}
		});
		@SuppressWarnings("unchecked")
		Map<String, String>[] ret = new Map[2];
		ret[0] = mapSrcAll;
		ret[1] = mapSrcMissed;
		return ret;
	}
	
	//ret[0]: source schemas ret[1]:target schemas
	private String[] getSchemas() {
		String schemas = "";
		Map<String, String> m = cfg.getCheckconfig();
		if (m != null) {
			schemas = m.get("check-tidb-schemas");
			if (schemas != null) {
				schemas = Arrays.stream(schemas.replace(" ","").split(",")).filter(x->x != null && x.length()>0)
				.collect(Collectors.joining(","));
			}
		}
		String[] ret = new String[2];
		if (schemas == null || schemas.trim().length() == 0) {
			//if check-tidb-schemas is not configed, then source use all
			ret[0] = "'" + src.getAllSchemas().stream().collect(Collectors.joining("','")).toLowerCase() + "'";
			ret[1] = "'" + tgt.getAllSchemas().stream().collect(Collectors.joining("','")).toLowerCase() + "'";
			return ret;
		}
		ret[0] = "'" + schemas.replace(",", "','") + "'";
		ret[1] = ret[0];
		return ret;
	}
	private Config getConfig(String filename) {
		if (Objects.nonNull(cfg)) {
			return cfg;
		}
		Yaml yaml = new Yaml();
		try {
        		cfg = yaml.loadAs(new FileInputStream(new File(filename)), Config.class);
			} catch (Exception e) {
				e.printStackTrace();
			}
        return cfg;
	}
	private void displayParam() {
		System.out.println("Config info:");
		System.out.printf("     %s\n",
					String.format("Source: type[%s] host[%s] port[%s] user[%s]",
					cfg.getSourcedb().get("dbtype"),
					cfg.getSourcedb().get("host"),
					cfg.getSourcedb().get("port"),
					cfg.getSourcedb().get("user")));
		String dbname = cfg.getTidb().get("dbname");
		if (dbname == null) dbname = "";
		System.out.printf("     %s\n",
				String.format("Target: type[%s] host[%s] port[%s] user[%s] dbname[%s]",
				cfg.getTidb().get("dbtype"),
				cfg.getTidb().get("host"),
				cfg.getTidb().get("port"),
				cfg.getTidb().get("user"),
				dbname));
		System.out.println("     Schema mapping:");
		schemap.entrySet().stream().map(x -> {return "            " + x.getValue() + " -> " + x.getKey();}).sorted().forEach(System.out::println);
		System.out.println("");
	}
	private void logMessage(String msg) {
		System.out.printf("[%s] %s", new java.sql.Timestamp(System.currentTimeMillis()).toString(), msg);
	}
		public static void main(String[] args) {
			Options opt = new Options();
			opt.addOption("c", true, "yaml config file");
			CommandLine cmd = null;
			try {
				cmd = new DefaultParser().parse(opt, args);
			} catch (ParseException e) {
			}
			if (cmd == null || !cmd.hasOption("c")) {
				System.out.printf("usage: java -jar %s -c filename\n",System.getProperty("java.class.path"));
				System.exit(-1);
			}
			Syndiff diff = new Syndiff(cmd.getOptionValue("c"));
			diff.doCompare();
		}
}
