package zmo.technology.lite.core;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import zmo.technology.lite.core.ZeroQuery.ZeroCondition;

public class ZeroQueryOperation<T extends ZeroCoreStructs> extends ZeroProcessor {

	public static final String CURACAO_QUERY_FETCH_METHOD = "fmethod";
	public static final String CURACAO_QUERY_OPTIONS = "options";

	public static final String AUTHORIZATION_TOKEN = "Authorization";
	public static final String CURACAO_AUTHORIZATION = "curacao-auth";
	public static final String CURACAO_TOKEN_AUTHORIZATION = "curacao-token-auth";

	public static final String CURACAO_PRODUCT = "curacao-product";
	public static final String CURACAO_CHANNEL = "curacao-channel";
	public static final String CURACAO_MAC_ADDRESS = "curacao-mac-address";

	public static final String CURACAO_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";

	public static final String RELATION_AND = "AND";
	public static final String RELATION_OR = "OR";

	public static final String SYMBOL_EQ = "EQ"; // （等于）
	public static final String SYMBOL_NEQ = "NEQ"; // （不等于）
	public static final String SYMBOL_GT = "GT"; // （大于）
	public static final String SYMBOL_LT = "LT"; // （小于）
	public static final String SYMBOL_EQGT = "EQGT"; // （大于等于）
	public static final String SYMBOL_EQLT = "EQLT"; // （小于等于）
	public static final String SYMBOL_LIKE = "LIKE"; // （模糊匹配）

	public static final String ORDER_BY_ASC = "ASC"; // （正序）
	public static final String ORDER_BY_DESC = "DESC"; // （逆序）

	public static final Map<String, String> RELATIONS;
	public static final Map<String, String> SYMBOLS;
	public static final Map<String, String> ORDER_BY;

	static {
		RELATIONS = new HashMap<String, String>();
		RELATIONS.put(RELATION_AND, " AND ");
		RELATIONS.put(RELATION_OR, " OR ");

		SYMBOLS = new HashMap<String, String>();
		SYMBOLS.put(SYMBOL_EQ, " = ");
		SYMBOLS.put(SYMBOL_NEQ, " <> ");
		SYMBOLS.put(SYMBOL_GT, " > ");
		SYMBOLS.put(SYMBOL_LT, " < ");
		SYMBOLS.put(SYMBOL_EQGT, " >= ");
		SYMBOLS.put(SYMBOL_EQLT, " <= ");
		SYMBOLS.put(SYMBOL_LIKE, " LIKE ");

		ORDER_BY = new HashMap<String, String>();
		ORDER_BY.put(ORDER_BY_ASC, " ASC ");
		ORDER_BY.put(ORDER_BY_DESC, " DESC ");
	}

	protected static final Pattern LINE_PATTERN = Pattern.compile("_(\\w)");

	public static String upperCase(String str) {
		char[] ch = str.toCharArray();
		if (ch[0] >= 'a' && ch[0] <= 'z') {
			ch[0] = (char) (ch[0] - 32);
		}
		return new String(ch);
	}

	protected static Pattern HUMP_PATTERN = Pattern.compile("[A-Z]");

	public static String humpToLine(String src) {
		String str = src;
		if (str.startsWith("ID"))
			str = str.replace("ID", "id_");
		if (str.endsWith("ID"))
			str = str.replace("ID", "_id");

		str = str.replace("ID", "_id");

		Matcher matcher = HUMP_PATTERN.matcher(str);
		StringBuffer sb = new StringBuffer();
		while (matcher.find()) {
			matcher.appendReplacement(sb, "_" + matcher.group(0).toLowerCase());
		}
		matcher.appendTail(sb);
		return sb.toString().toUpperCase();
	}

	public static String lineToHump(String src) {
		String str = src;
		str = str.toLowerCase();
		str = str.replace("_id", "ID");
		str = str.replace("_id_", "ID");
		str = str.replace("id_", "ID");
		Matcher matcher = LINE_PATTERN.matcher(str);
		StringBuffer sb = new StringBuffer();
		while (matcher.find()) {
			matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
		}
		matcher.appendTail(sb);
		return sb.toString();
	}

	protected Object columnValue(Object value, Class<?> type) throws SQLException {
		if (value.getClass().equals(java.sql.Timestamp.class))
			return dateFormat.format(new Date(((java.sql.Timestamp) value).getTime()));

		else if (value.getClass().equals(UUID.class))
			return ((UUID) value).toString();
		else if (value.getClass().equals(BigDecimal.class))
			return ((BigDecimal) (value)).doubleValue();
		else if (type.equals(UUID.class))
			return UUID.fromString(value.toString());
		else
			return value;
	}

	@FunctionalInterface
	public interface CuracaoQueryData {

		void data() throws Exception;
	}

	protected String conditionPrefix;
	protected String pretreatSQL;
	protected Class<T> baseClass;
	protected ZeroQuery<T> query;
	protected Map<String, Field> fieldMap;

	protected String distinctID;
	protected String filterTableName;

	protected ZeroQueryOperation(Class<T> baseClass, ZeroRequestGuider guider) {
		super(guider);
		this.baseClass = baseClass;
		conditionPrefix = "";
		pretreatSQL = "";
		dateFormat = new SimpleDateFormat(CURACAO_DATE_FORMAT);
		try {
			fieldMap = getBaseClass().getConstructor().newInstance().fetchFieldsMap();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static <T extends ZeroCoreStructs> ZeroQueryOperation<T> build(Class<T> baseClass,
			ZeroRequestGuider guider) {
		return new ZeroQueryOperation<T>(baseClass, guider);
	}

	public ZeroQueryOperation<T> openConnect(ZeroQuery<T> query) throws SQLException {
		this.query = query;
		return this;
	}

	public ZeroQueryOperation<T> setConditionPrefix(String conditionPrefix) {
		this.conditionPrefix = conditionPrefix;
		return this;
	}

	public ZeroQueryOperation<T> setPretreatSQL(String pretreatSQL) {
		this.pretreatSQL = pretreatSQL;
		return this;
	}

	public ZeroQueryOperation<T> setDistinctID(String distinctID) {
		this.distinctID = distinctID;
		return this;
	}

	public ZeroQueryOperation<T> setFilterTableName(String filterTableName) {
		this.filterTableName = filterTableName;
		return this;
	}

	public void close() {
	}

	protected SimpleDateFormat dateFormat;

	protected String columns;
	protected String conditions;
	protected String orderby;
	protected String limit;

	protected String target;

	protected int start;
	protected int length;

	public String parserConditionKeyName(ZeroCondition condition) {
		return conditionPrefix + condition.getColumn().split(":")[0];
	}

	public Class<?> parserConditionValueType(ZeroCondition condition) {
		String[] keys = condition.getColumn().split(":");
		if (keys.length > 1) {
			String type = keys[1];
			switch (type) {
				case "string":
					return String.class;
				case "date":
					return Date.class;
				default:
					return Long.class;
			}
		} else {
			if (fieldMap.containsKey(condition.getColumn()))
				return fieldMap.get(condition.getColumn()).getType();
			return String.class;
		}
	}

	public String parserConditionValue(Class<?> columnType, String value, String symbol) {
		StringBuffer valueBuffer = new StringBuffer();

		if (columnType.equals(String.class)) {
			if (symbol.equals(SYMBOL_LIKE)) {
				valueBuffer.append("'%");
				valueBuffer.append(value);
				valueBuffer.append("%'");
			} else {
				valueBuffer.append("'");
				valueBuffer.append(value);
				valueBuffer.append("'");
			}
		} else if (columnType.equals(Date.class)) {
			valueBuffer.append("STR_TO_DATE('" + value.toString() + "','%Y-%m-%dT%H:%i:%sZ')");
		} else if (columnType.equals(UUID.class)) {
			valueBuffer.append("'");
			valueBuffer.append(value);
			valueBuffer.append("'");
		} else {
			valueBuffer.append(value);
		}
		return valueBuffer.toString();
	}

	public String parserConditionValue(String value) {
		return parserConditionValue(String.class, value, "");
	}

	protected String parserConditions(ZeroCondition condition) throws Exception {
		if (condition.getRelation().isEmpty()) {

			if (!SYMBOLS.containsKey(condition.getSymbol()))
				throw new ZeroException(110614, "symbol : " + condition.getSymbol() + " not found");

			StringBuffer columnsBuffer = new StringBuffer();
			columnsBuffer.append("(");
			columnsBuffer.append(humpToLine(parserConditionKeyName(condition)));
			columnsBuffer.append(SYMBOLS.get(condition.getSymbol()));
			columnsBuffer.append(" ");

			columnsBuffer.append(parserConditionValue(parserConditionValueType(condition), condition.getValue(),
					condition.getSymbol()));

			columnsBuffer.append(")");
			return columnsBuffer.toString();
		} else {
			if (!RELATIONS.containsKey(condition.getSymbol()))
				throw new ZeroException(110615, "relation : " + condition.getSymbol() + " not found");

			List<String> relations = new ArrayList<String>();
			for (ZeroCondition subCondition : condition.getRelation())
				relations.add(parserConditions(subCondition));

			return "(" + String.join(" " + RELATIONS.get(condition.getSymbol()) + " ", relations) + ")";
		}
	}

	public static final String MULTIPLE_SEPARATOR = "|";
	public static final String MULTIPLE_SPLIT_SEPARATOR = "\\|";

	@FunctionalInterface
	public static interface CuracaoQueryConditionExpand {

		String expand();
	}

	public void appendCondition(CuracaoQueryConditionExpand conditionExpand) throws Exception {
		StringBuffer columnsBuffer = new StringBuffer();
		if (getConditions().trim().length() <= 0)
			columnsBuffer.append(" WHERE ");
		else
			columnsBuffer.append(RELATIONS.get(RELATION_AND));

		String exp = conditionExpand.expand();

		if (!exp.isEmpty()) {
			columnsBuffer.append(" ( ");
			columnsBuffer.append(exp);
			columnsBuffer.append(" ) ");
			conditions += columnsBuffer.toString();
		}
	}

	public void appendCondition(String key, String symbol, Object value) throws Exception {
		appendCondition(key, symbol, value, RELATION_AND);
	}

	public void appendCondition(String key, String symbol, Object value, String relation) throws Exception {
		String[] values = value.toString().split(MULTIPLE_SPLIT_SEPARATOR);
		if (values.length > 0) {
			StringBuffer columnsBuffer = new StringBuffer();
			if (getConditions().trim().length() <= 0)
				columnsBuffer.append(" WHERE ");
			else
				columnsBuffer.append(RELATIONS.get(relation));

			List<String> conds = new ArrayList<String>();

			for (String v : values)
				if (v.trim().length() > 0)
					conds.add(key + SYMBOLS.get(symbol) + parserConditionValue(v));

			if (!conds.isEmpty()) {
				columnsBuffer.append(" ( ");
				columnsBuffer.append(String.join(" OR ", conds));
				columnsBuffer.append(" ) ");
				conditions += columnsBuffer.toString();
			}
		}
	}

	public Class<? extends ZeroCoreStructs> getBaseClass() {
		return baseClass;
	}

	public ZeroQuery<T> getQuery() {
		return query;
	}

	public String getColumns() {
		if (columns == null) {
			String columnsPrefix = "";
			if (distinctID != null && filterTableName != null)
				columnsPrefix = "a.";

			if (query.getColumns().isEmpty())
				columns = " " + columnsPrefix + "* ";
			else {
				StringBuffer columnsBuffer = new StringBuffer();
				for (String column : query.getColumns()) {
					if (columnsBuffer.length() > 0)
						columnsBuffer.append(",");
					else
						columnsBuffer.append(" ");
					columnsBuffer.append(columnsPrefix + humpToLine(column));
				}
				columns = " " + columnsBuffer.toString() + " ";
			}
		}
		return columns;
	}

	public String getConditions() throws Exception {
		if (conditions == null) {
			if (query.getCondition().getSymbol().length() <= 0) {
				conditions = "";
			} else {
				conditions = " WHERE " + parserConditions(query.getCondition()) + " ";
			}
		}
		return conditions;
	}

	public String getOrderby() {
		if (orderby == null) {
			if (query.getOrderby().getColumns().isEmpty()) {
				orderby = "";
			} else {
				StringBuffer columnsBuffer = new StringBuffer();
				for (String column : query.getOrderby().getColumns()) {
					if (columnsBuffer.length() > 0)
						columnsBuffer.append(",");
					else
						columnsBuffer.append(" ");
					columnsBuffer.append(conditionPrefix + humpToLine(column));
				}
				orderby = " ORDER BY " + columnsBuffer.toString() + ORDER_BY.get(query.getOrderby().getSeq());
			}
		}
		return orderby;
	}

	public String getTarget() throws Exception {
		if (target == null) {
			if (query.getTargets().isEmpty()) {
				target = "";
			} else {
				Field param = fieldMap.get("id");
				param.setAccessible(true);
				target = param.get(query.getTargets().get(0)).toString();
			}
		}
		return target;
	}

	public String getLimit() throws Exception {
		if (limit == null) {
			if (getTarget().isEmpty()) {
				if (query.getLimit().getStart() >= 0 && query.getLimit().getLength() > 0) {
					start = query.getLimit().getStart();
					length = query.getLimit().getLength() > 100 ? 100 : query.getLimit().getLength();
					limit = " LIMIT " + start + "," + length + " ";
				} else {
					start = 0;
					length = 100;
					limit = " LIMIT 0 ,100 ";
				}
			} else {
				if (query.getLimit().getLength() > 0) {
					length = query.getLimit().getLength() > 100 ? 100 : query.getLimit().getLength();
					limit = " LIMIT 0 ," + length + " ";
				} else {
					length = 100;
					limit = " LIMIT 0 ,100 ";
				}
			}
		}
		return limit;
	}

	public int getStart() throws Exception {
		if (limit == null) {
			getLimit();
		}
		return start;
	}

	public int getLength() throws Exception {
		if (limit == null) {
			getLimit();
		}
		return length;
	}

	private static final String TARGET_QUERY_SQL_TEMPLATE;
	static {
		StringBuffer buffer = new StringBuffer();

		buffer.append("	SELECT ");
		buffer.append("		{{columns}}");
		buffer.append("	FROM ");
		buffer.append("		(SELECT ");
		buffer.append("		 	(@rn1 :=@rn1 + 1) AS rownum ,");
		buffer.append("		 	 z.* ");
		buffer.append("		 FROM ");
		buffer.append("		 	{{tableName}} z, ");
		buffer.append("		 	(SELECT @rn1 := 0) irn1 ");
		buffer.append("		 {{conditions}} {{orderby}}) t");
		buffer.append("	WHERE ");
		buffer.append("		t.rownum > (SELECT ");
		buffer.append("			rownums.rownum ");
		buffer.append("		FROM ");
		buffer.append("			(SELECT ");
		buffer.append("				(@rn2 :=@rn2 + 1) AS rownum ,");
		buffer.append("				z.ID ");
		buffer.append("			 FROM ");
		buffer.append("				{{tableName}} z ,(SELECT @rn2 := 0) irn2 {{conditions}} {{orderby}}) rownums ");
		buffer.append("		WHERE ");
		buffer.append("			rownums.ID = '{{target}}')");
		buffer.append("	{{limit}}");

		TARGET_QUERY_SQL_TEMPLATE = buffer.toString();
	}

	private static final String DISTINCT_QUERY_SQL_TEMPLATE;
	static {
		StringBuffer buffer = new StringBuffer();

		buffer.append("	SELECT ");
		buffer.append("		{{columns}}");
		buffer.append("	FROM ");
		buffer.append("		(SELECT ");
		buffer.append("		 	distinct {{distinctID}} ");
		buffer.append("		 FROM ");
		buffer.append("		 	{{filterTableName}} ");
		buffer.append("		 {{conditions}}) t,");
		buffer.append("		 {{tableName}} a");
		buffer.append("	WHERE ");
		buffer.append("	t.{{distinctID}} = a.ID ");
		buffer.append("	{{orderby}} {{limit}} ");

		DISTINCT_QUERY_SQL_TEMPLATE = buffer.toString();
	}

	private static final String DISTINCT_QUERY_COUNT_SQL_TEMPLATE;
	static {
		StringBuffer buffer = new StringBuffer();

		buffer.append("	SELECT ");
		buffer.append("		count(distinct {{distinctID}}) AS QUERY_COUNT");
		buffer.append("	FROM ");
		buffer.append("		{{filterTableName}} ");
		buffer.append("	{{conditions}}");

		DISTINCT_QUERY_COUNT_SQL_TEMPLATE = buffer.toString();
	}

	protected String querySQL;
	protected String queryCountSQL;

	public String getQuerySQL() throws Exception {
		if (querySQL == null) {
			if (!getTarget().isEmpty()) {
				querySQL = TARGET_QUERY_SQL_TEMPLATE
						.replace("{{columns}}", getColumns())
						.replace("{{tableName}}", query.getTableName())
						.replace("{{conditions}}", getConditions())
						.replace("{{orderby}}", getOrderby())
						.replace("{{limit}}", getLimit())
						.replace("{{target}}", getTarget());
			} else if (distinctID != null && filterTableName != null) {
				querySQL = DISTINCT_QUERY_SQL_TEMPLATE
						.replace("{{columns}}", getColumns())
						.replace("{{tableName}}", query.getTableName())
						.replace("{{conditions}}", getConditions())
						.replace("{{orderby}}", getOrderby())
						.replace("{{limit}}", getLimit())
						.replace("{{distinctID}}", distinctID)
						.replace("{{filterTableName}}", filterTableName);
			} else {
				if (pretreatSQL.isEmpty())
					querySQL = "SELECT" + getColumns() + "FROM " + query.getTableName() + getConditions() + getOrderby()
							+ getLimit();
				else
					querySQL = pretreatSQL
							.replace("{{columns}}", getColumns())
							.replace("{{conditions}}", getConditions())
							.replace("{{orderby}}", getOrderby())
							.replace("{{limit}}", getLimit());
			}
		}
		return querySQL;
	}

	public String getQueryCountSQL() throws Exception {
		if (queryCountSQL == null) {
			if (distinctID != null && filterTableName != null) {
				queryCountSQL = DISTINCT_QUERY_COUNT_SQL_TEMPLATE
						.replace("{{conditions}}", getConditions())
						.replace("{{distinctID}}", distinctID)
						.replace("{{filterTableName}}", filterTableName);
			} else {
				if (pretreatSQL.isEmpty())
					queryCountSQL = "SELECT count(1) AS QUERY_COUNT FROM " + query.getTableName() + getConditions();
				else
					queryCountSQL = pretreatSQL
							.replace("{{columns}}", "count(1) AS QUERY_COUNT")
							.replace("{{conditions}}", getConditions())
							.replace("{{orderby}}", "")
							.replace("{{limit}}", "");
			}
		}
		return queryCountSQL;
	}

	protected T current;
	protected long rownum;
	protected Map<String, String> expands;

	public T getCurrent() {
		return current;
	}

	public long getRownum() {
		return rownum;
	}

	public Map<String, String> getExpands() {
		return expands;
	}

	@FunctionalInterface
	public interface ExpandsForEach {

		void each(String key, String value);
	}

	public ZeroQueryOperation<T> eachExpands(ExpandsForEach forEach) {
		getExpands().entrySet().forEach(entry -> forEach.each(entry.getKey(), entry.getValue()));
		return this;
	}

	public ZeroQueryOperation<T> visibleOnly() throws Exception {
		appendCondition(conditionPrefix + "FLAG", SYMBOL_EQ, 0);
		return this;
	}

	@SuppressWarnings("unchecked")
	public ZeroQueryOperation<T> exec(CuracaoQueryData data) throws Exception {

		expands = new HashMap<>();

		if (getTarget().isEmpty())
			expands.put("start", String.valueOf(getStart()));
		else
			expands.put("target", getTarget());
		expands.put("length", String.valueOf(getLength()));

		Connection conn = guider.getConnection();
		Statement stat = conn.createStatement();

		ResultSet resultSet = stat.executeQuery(getQueryCountSQL());
		if (!resultSet.next())
			throw new ZeroException(110616, "err : query count");

		expands.put("total", String.valueOf(resultSet.getLong(1)));
		if (data != null) {
			resultSet = stat.executeQuery(getQuerySQL());
			while (resultSet.next()) {
				T instance = (T) getBaseClass().getConstructor(ResultSet.class).newInstance(resultSet);
				if (ZeroCoreStructs.inResultSet(resultSet, "ROWNUM"))
					rownum = resultSet.getLong("ROWNUM");
				current = instance;
				data.data();
			}
		}
		return this;
	}
}
