package com.cim.idm.solution.service.management;

import com.cim.idm.framework.IDMFrameServiceProxy;
import com.cim.idm.framework.action.EventUtil;
import com.cim.idm.framework.data.EventInfo;
import com.cim.idm.framework.exception.ErrorSignal;
import com.cim.idm.framework.exception.IDMFrameDBErrorSignal;
import com.cim.idm.framework.exception.ext.ExceptionKey;
import com.cim.idm.framework.exception.ext.FrameworkErrorSignal;
import com.cim.idm.framework.orm.SQLLogUtil;
import com.cim.idm.framework.orm.info.access.FieldAccessor;
import com.cim.idm.framework.orm.support.ObjectHelper;
import com.cim.idm.framework.util.time.TimeUtils;
import com.cim.idm.wmsextend.generic.GenericServiceProxy;
import com.cim.idm.wmsextend.generic.util.CommonUtil;
import com.cim.idm.solution.service.ORMTemplate;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;

import java.lang.reflect.Field;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @author 10862131
 * @date 2022年08月24日 14:23
 */
public class ORMService<DATA> {

    public List<DATA> select(String condition, Object[] bindSet, Class clazz) throws IDMFrameDBErrorSignal {
        String tableName = ORMUtil.getTableNameByClassName(clazz);

        // String sql = "select * from " + tableName;
        String sql = ORMUtil.getSql(clazz, tableName);

        if (StringUtils.isNotEmpty(condition)) {
            sql = ORMUtil.getConditionSql(sql, condition);
        }

        // query
        List resultList = IDMFrameServiceProxy.getSqlTemplate().queryForList(sql, bindSet);

        // case not found
        if (resultList.size() == 0) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.NotFoundSignal, ObjectHelper.getString(bindSet), SQLLogUtil.getLogFormatSqlStatement(sql, bindSet, ORMUtil.getLog()));
        }

        // refine result data
        Object result = ormExecute(ORMUtil.createDataInfo(clazz), resultList);

        // return
        if (result instanceof List) {
            return (List<DATA>) result;
        } else if (result instanceof FieldAccessor) {
            List<DATA> resultSet = new ArrayList<DATA>();
            resultSet.add((DATA) result);

            return resultSet;
        } else {
            throw new IDMFrameDBErrorSignal(ErrorSignal.CouldNotMatchData, ObjectHelper.getString(bindSet), SQLLogUtil.getLogFormatSqlStatement(sql, bindSet, ORMUtil.getLog()), condition);
        }
    }

    public DATA selectByKey(Class clazz, boolean isLock, Object... keyValue) throws IDMFrameDBErrorSignal {
        // create base already
        Object dataInfo = ORMUtil.createDataInfo(clazz);

        String tableName = ORMUtil.getTableNameByClassName(clazz);

        // String sql = "select * from " + tableName;
        String sql = ORMUtil.getSql(clazz, tableName);
        sql = ORMUtil.getKeySql(sql, dataInfo);

        // suffix for lock
        if (isLock) {
            sql = new StringBuffer(sql).append(" FOR UPDATE").toString();
        }

        String param = CommonUtil.toStringFromCollection(keyValue);

        if (!ORMUtil.validateKeyParam(dataInfo, keyValue).isEmpty()) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.NullPointKeySignal, param, SQLLogUtil.getLogFormatSqlStatement(sql, param, ORMUtil.getLog()));
        }

        // generate bind parameter
        // by sequence

        // query with just one
        List resultList = IDMFrameServiceProxy.getSqlTemplate().queryForList(sql, keyValue);

        // case not found
        if (resultList.size() == 0) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.NotFoundSignal, param, SQLLogUtil.getLogFormatSqlStatement(sql, param, ORMUtil.getLog()));
        }

        // refine result data
        Object result = ormExecute(ORMUtil.createDataInfo(clazz), resultList);

        // return
        if (result instanceof FieldAccessor) {
            return (DATA) result;
        } else {
            throw new IDMFrameDBErrorSignal(ErrorSignal.CouldNotMatchData, param, SQLLogUtil.getLogFormatSqlStatement(sql, param, ORMUtil.getLog()), keyValue.toString());
        }
    }

    public void update(DATA dataInfo) throws IDMFrameDBErrorSignal {
        String tableName = ORMUtil.getTableNameByClassName(dataInfo.getClass());

        // String sql = "select * from " + tableName;
        String sql = ORMUtil.getUpdateSql(dataInfo.getClass(), tableName);

        // generate bind parameter
        // by sequence
        List<Object> keySet = ORMUtil.makeKeyParam(dataInfo);
        String param = CommonUtil.toStringFromCollection(keySet.toArray());
        if (!ORMUtil.validateKeyParam(dataInfo, keySet.toArray()).isEmpty()) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.NullPointKeySignal, param, SQLLogUtil.getLogFormatSqlStatement(sql, param, ORMUtil.getLog()));
        }

        List<Object> bindSet = ORMUtil.makeNonKeyParam(dataInfo);
        bindSet.addAll(keySet);
        param = CommonUtil.toStringFromCollection(bindSet.toArray());

        // query with just one
        int result = IDMFrameServiceProxy.getSqlTemplate().update(sql, bindSet.toArray());

        // case not found
        if (result == 0) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.NotFoundSignal, param, SQLLogUtil.getLogFormatSqlStatement(sql, param, ORMUtil.getLog()));
        }
    }

    public void update(DATA dataInfo, EventInfo eventInfo) throws IDMFrameDBErrorSignal {

        setEventInfo(dataInfo, eventInfo);

        String tableName = ORMUtil.getTableNameByClassName(dataInfo.getClass());

        // String sql = "select * from " + tableName;
        String sql = ORMUtil.getUpdateSql(dataInfo.getClass(), tableName);

        // generate bind parameter
        // by sequence
        List<Object> keySet = ORMUtil.makeKeyParam(dataInfo);
        String param = CommonUtil.toStringFromCollection(keySet.toArray());
        if (!ORMUtil.validateKeyParam(dataInfo, keySet.toArray()).isEmpty()) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.NullPointKeySignal, param, SQLLogUtil.getLogFormatSqlStatement(sql, param, ORMUtil.getLog()));
        }

        List<Object> bindSet = ORMUtil.makeNonKeyParam(dataInfo);
        bindSet.addAll(keySet);
        param = CommonUtil.toStringFromCollection(bindSet.toArray());

        // query with just one
        int result = IDMFrameServiceProxy.getSqlTemplate().update(sql, bindSet.toArray());

        // case not found
        if (result == 0) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.NotFoundSignal, param, SQLLogUtil.getLogFormatSqlStatement(sql, param, ORMUtil.getLog()));
        }
    }

    public void delete(DATA dataInfo) throws IDMFrameDBErrorSignal {
        List<Object> keySet = ORMUtil.makeKeyParam(dataInfo);

        delete(dataInfo.getClass(), keySet.toArray());
    }

    public void delete(Class clazz, Object... keyValue) throws IDMFrameDBErrorSignal {
        // create base already
        Object dataInfo = ORMUtil.createDataInfo(clazz);

        String tableName = ORMUtil.getTableNameByClassName(dataInfo.getClass());

        // String sql = "select * from " + tableName;
        String sql = ORMUtil.getDeleteSql(dataInfo.getClass(), tableName);

        // generate bind parameter
        // by sequence


        String param = CommonUtil.toStringFromCollection(keyValue);

        if (!ORMUtil.validateKeyParam(dataInfo, keyValue).isEmpty()) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.NullPointKeySignal, param, SQLLogUtil.getLogFormatSqlStatement(sql, param, ORMUtil.getLog()));
        }


        // query with just one
        int result = IDMFrameServiceProxy.getSqlTemplate().update(sql, keyValue);

        // case not found
        if (result == 0) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.NotFoundSignal, param, SQLLogUtil.getLogFormatSqlStatement(sql, param, ORMUtil.getLog()));
        }
    }

    public void insert(DATA dataInfo) throws IDMFrameDBErrorSignal {
        String tableName = ORMUtil.getTableNameByClassName(dataInfo.getClass());

        // String sql = "select * from " + tableName;
        String sql = ORMUtil.getInsertSql(dataInfo.getClass(), tableName);

        // generate bind parameter
        // by sequence
        List<Object> keySet = ORMUtil.makeKeyParam(dataInfo);
        String param = CommonUtil.toStringFromCollection(keySet.toArray());
        if (!ORMUtil.validateKeyParam(dataInfo, keySet.toArray()).isEmpty()) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.NullPointKeySignal, param, SQLLogUtil.getLogFormatSqlStatement(sql, param, ORMUtil.getLog()));
        }

        List<Object> bindSet = ORMUtil.makeNonKeyParam(dataInfo);
        keySet.addAll(bindSet);
        param = CommonUtil.toStringFromCollection(bindSet.toArray());

        // query with just one
        int result = IDMFrameServiceProxy.getSqlTemplate().update(sql, keySet.toArray());

        // case not found
        if (result == 0) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.InvalidQueryState, param, SQLLogUtil.getLogFormatSqlStatement(sql, param, ORMUtil.getLog()));
        }
    }

    public void insert(DATA dataInfo, EventInfo eventInfo) throws IDMFrameDBErrorSignal {

        setEventInfo(dataInfo, eventInfo);

        ObjectHelper.setFieldValue(dataInfo, "createUser", eventInfo.getEventUser());

        ObjectHelper.setFieldValue(dataInfo, "createTime", eventInfo.getEventTime());

        String tableName = ORMUtil.getTableNameByClassName(dataInfo.getClass());

        // String sql = "select * from " + tableName;
        String sql = ORMUtil.getInsertSql(dataInfo.getClass(), tableName);

        // generate bind parameter
        // by sequence
        List<Object> keySet = ORMUtil.makeKeyParam(dataInfo);
        String param = CommonUtil.toStringFromCollection(keySet.toArray());
        if (!ORMUtil.validateKeyParam(dataInfo, keySet.toArray()).isEmpty()) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.NullPointKeySignal, param, SQLLogUtil.getLogFormatSqlStatement(sql, param, ORMUtil.getLog()));
        }

        List<Object> bindSet = ORMUtil.makeNonKeyParam(dataInfo);
        keySet.addAll(bindSet);
        param = CommonUtil.toStringFromCollection(bindSet.toArray());

        // query with just one
        int result = IDMFrameServiceProxy.getSqlTemplate().update(sql, keySet.toArray());

        // case not found
        if (result == 0) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.InvalidQueryState, param, SQLLogUtil.getLogFormatSqlStatement(sql, param, ORMUtil.getLog()));
        }
    }

    public Object ormExecute(Object dataObject, List<Map<String, Object>> resultList) throws IDMFrameDBErrorSignal {
        Object dataInfo = null;
        List<Object> resultDataInfos = null;

        for (int j = 0; j < resultList.size(); j++) {
            // with pure data
            dataInfo = ORMUtil.createDataInfo(dataObject.getClass());
            setProperties(dataInfo, (Map<String, Object>) resultList.get(j));

            if (resultList.size() > 1) {
                if (resultDataInfos == null) {
                    resultDataInfos = new ArrayList<Object>();
                }
                resultDataInfos.add(dataInfo);
            }
        }

        resultList.clear();

        if (resultDataInfos != null) {
            return resultDataInfos;
        } else {
            return dataInfo;
        }
    }

    private void setProperties(Object dataInfo, Map<String, Object> dataMap) {
        for (Field column : dataInfo.getClass().getDeclaredFields()) {
            if (column.isAnnotationPresent(ORMTemplate.class)) {
                ORMTemplate annotation = column.getAnnotation(ORMTemplate.class);

                String name = annotation.name();
                String type = annotation.type();
                String dataType = annotation.dataType();
                String initial = annotation.initial();
                Object resultValue = dataMap.get(name);
                // class to object converting
                if (resultValue != null) {
                    ObjectHelper.setFieldValue(dataInfo, name, resultValue);
                } else if (resultValue == null && "timestamp".equalsIgnoreCase(dataType)) {
                    ObjectHelper.setFieldValue(dataInfo, name, null);
                }
            }
        }
    }

    public void addHistory(EventInfo eventInfo, String historyEntityName, DATA dataInfo, Log logger) throws IDMFrameDBErrorSignal {
        try {
            if (StringUtils.isEmpty(historyEntityName)) {
                return;
            } else {
                historyEntityName = ORMUtil.getHeader(dataInfo.getClass()) + historyEntityName;
            }
            insertHistory(eventInfo, historyEntityName, dataInfo);
        } catch (Exception ex) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.UnexprectedSignal, ex.getMessage());
        }
    }

    private void insertHistory(EventInfo eventInfo, String historyEntityName, DATA dataInfo) throws IDMFrameDBErrorSignal {
        List<Object> bindList = new ArrayList<Object>();
        List<String> attrList = new ArrayList<String>();
        // make attribute list
        for (Field column : dataInfo.getClass().getDeclaredFields()) {
            if (column.isAnnotationPresent(ORMTemplate.class)) {
                ORMTemplate annotation = column.getAnnotation(ORMTemplate.class);
                String name = annotation.name();
                String type = annotation.type();
                String dataType = annotation.dataType();
                String initial = annotation.initial();
                // mirror table must be consistent with data type
                String mirror = annotation.history();
                Object value = ObjectHelper.getFieldValue(dataInfo, column.getName());
                if (value == null
                        || "never".equalsIgnoreCase(initial)
                        || mirror.equalsIgnoreCase(GenericServiceProxy.getConstantMap().FLAG_N)) {
                    continue;
                }
                if (StringUtils.isNotEmpty(mirror)) {
                    attrList.add(mirror);
                } else {
                    attrList.add(name);
                }
                // set value, reflect all at current and assure sequence
                String historyColName = mirror.toUpperCase();
                if (attrList.contains(historyColName)) {
                    if (StringUtils.equals(historyColName, "CREATEUSER")) {
                        bindList.add(eventInfo.getEventUser());
                    }
                    if (StringUtils.equals(historyColName, "CREATETIME")) {
                        bindList.add(eventInfo.getEventTime());
                    }
                    if (StringUtils.equals(historyColName, "EVENTUSER")) {
                        bindList.add(eventInfo.getEventUser());
                    }
                    if (StringUtils.equals(historyColName, "EVENTTIME")) {
                        bindList.add(eventInfo.getEventTime());
                    }
                    if (StringUtils.equals(historyColName, "EVENTNAME")) {
                        bindList.add(eventInfo.getEventName());
                    }
                    if (StringUtils.equals(historyColName, "EVENTCOMMENT")) {
                        bindList.add(eventInfo.getEventComment());
                    }
                    if (StringUtils.equals(historyColName, "TIMEKEY") || StringUtils.equals(historyColName, "EVENTTIMEKEY")) {
                        bindList.add(StringUtils.isEmpty(eventInfo.getEventTimeKey()) ? new TimeUtils().getCurrentEventTimeKey() : eventInfo.getEventTimeKey());
                    }
                } else {
                    bindList.add(value);
                }
            }
        }
        // query generation
        StringBuffer sql = new StringBuffer();
        StringBuffer bindBuilder = new StringBuffer();
        sql.append("INSERT").append(" ").append("INTO").append(" ").append(historyEntityName).append(" ");
        sql.append("(");
        for (int idx = 0; idx < attrList.size(); idx++) {
            sql.append(attrList.get(idx));
            if (idx < attrList.size() - 1) {
                sql.append(",");
            }
            // bind value is equal with size
            bindBuilder.append("?");
            if (idx < attrList.size() - 1) {
                bindBuilder.append(",");
            }
        }
        sql.append(")");
        sql.append(" VALUES ").append("(").append(bindBuilder).append(")");
        // radical validation
        List<Object> keySet = ORMUtil.makeKeyParam(dataInfo);
        String param = CommonUtil.toStringFromCollection(keySet.toArray());
        if (!ORMUtil.validateKeyParam(dataInfo, keySet.toArray()).isEmpty()) {
            throw new IDMFrameDBErrorSignal(ErrorSignal.NullPointKeySignal, param, SQLLogUtil.getLogFormatSqlStatement(sql.toString(), param, ORMUtil.getLog()));
        }
        // generate bind parameter
        // by sequence
        // only DDL as form of INSERT
        int result = IDMFrameServiceProxy.getSqlTemplate().update(sql.toString(), bindList.toArray());
        // case not found
        if (result == 0) {
            param = CommonUtil.toStringFromCollection(bindList.toArray());
            throw new IDMFrameDBErrorSignal(ErrorSignal.InvalidQueryState, param, SQLLogUtil.getLogFormatSqlStatement(sql.toString(), param, ORMUtil.getLog()));
        }
    }

    public void setEventInfo(DATA dataInfo, EventInfo eventInfo) throws FrameworkErrorSignal {
        if (StringUtils.isEmpty(eventInfo.getEventName())) {
            String eventName = dataInfo.getClass().getSimpleName();
            eventInfo.setEventName(eventName);
        }
        String dataLastEventTimeKey = (String) ObjectHelper.getFieldValue(dataInfo, "lastEventTimeKey");
        if (StringUtils.isNotEmpty(eventInfo.getLastEventTimekey())) {
            if (eventInfo.getLastEventTimekey().equals(dataLastEventTimeKey) == false) {
                throw new FrameworkErrorSignal(ExceptionKey.InvalidTransaction_Exception, dataLastEventTimeKey, "EventInfo", eventInfo.getLastEventTimekey());
            }
        } else {
            eventInfo.setLastEventTimekey("");
        }
        // if eventInfo's lastEventtimekey is Empty, so set eventInfo's lastEventTimekey. and if lastEventtime is null , set lastEventTime .
        EventUtil.setTime4EventInfo(dataLastEventTimeKey, eventInfo);
        if (StringUtils.isNotEmpty(dataLastEventTimeKey)) {
            int timeKeyDiff = dataLastEventTimeKey.compareTo(eventInfo.getEventTimeKey());
            if (timeKeyDiff >= 0) {
                if (eventInfo.isCheckTimekeyValidation()) {
                    throw new FrameworkErrorSignal(ExceptionKey.InvalidEventTime_Exception,
                            eventInfo.getEventTimeKey(), (String) ObjectHelper.getFieldValue(dataInfo, "lastEventName"),
                            dataLastEventTimeKey);
                } else {
                    // if checkTimekeyValidation is false, set a new eventTimekey for this eventInfo.
                    try {
                        // if using Sequence4Timekey , set EventTimekey with DB sequence
                        // if not, using lastEventTimekey+ 1(s)
                        if (new TimeUtils().isUseSequence4TimeKey()) {
                            eventInfo.setEventTimeKey(new TimeUtils().getCurrentEventTimeKey());
                        } else {
                            SimpleDateFormat sdf = new SimpleDateFormat(TimeUtils.FORMAT_SEQTIMEKEY);
                            Date date;
                            date = sdf.parse(dataLastEventTimeKey);
                            Calendar calendar = Calendar.getInstance();
                            calendar.setTime(date);
                            calendar.add(Calendar.MILLISECOND, 1);
                            String newTimekey = sdf.format(calendar.getTime());
                            eventInfo.setEventTimeKey(newTimekey);
                        }
                    } catch (ParseException e) {
                        throw new FrameworkErrorSignal(ExceptionKey.UnExpected_Exception, e);
                    }
                }
            }
        }
        ObjectHelper.setFieldValue(dataInfo, "lastEventName", eventInfo.getEventName());
        ObjectHelper.setFieldValue(dataInfo, "lastEventTimeKey", eventInfo.getEventTimeKey());
        ObjectHelper.setFieldValue(dataInfo, "lastEventTime", eventInfo.getEventTime());
        ObjectHelper.setFieldValue(dataInfo, "lastEventUser", eventInfo.getEventUser());
        ObjectHelper.setFieldValue(dataInfo, "lastEventComment", eventInfo.getEventComment());
        String lastEventFlagStr = "lastEventFlag";
        String reasonCodeTypeStr = "reasonCodeType";
        String reasonCodeStr = "reasonCode";
        String lotStr = "Lot";
        String productStr = "Product";
        if (ObjectHelper.hasFiled(dataInfo, lastEventFlagStr)) {
            ObjectHelper.setFieldValue(dataInfo, "lastEventFlag", "N");
        }
        if (ObjectHelper.hasFiled(dataInfo, reasonCodeTypeStr)) {
            ObjectHelper.setFieldValue(dataInfo, "reasonCodeType", eventInfo.getReasonCodeType());
        }
        if (ObjectHelper.hasFiled(dataInfo, reasonCodeStr)) {
            ObjectHelper.setFieldValue(dataInfo, "reasonCode", eventInfo.getReasonCode());
        }
        if (lotStr.equalsIgnoreCase(dataInfo.getClass().getSimpleName())) {
            ObjectHelper.setFieldValue(dataInfo, "sourceLotName", "");
            ObjectHelper.setFieldValue(dataInfo, "destinationLotName", "");
        } else if (productStr.equalsIgnoreCase(dataInfo.getClass().getSimpleName())) {
            ObjectHelper.setFieldValue(dataInfo, "sourceProductName", "");
            ObjectHelper.setFieldValue(dataInfo, "destinationProductName", "");
        }
    }
}
