package com.eactive.eai.adapter.handler;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;

import com.eactive.eai.adapter.AdapterGroupVO;
import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.AdapterPropManager;
import com.eactive.eai.adapter.AdapterVO;
import com.eactive.eai.adapter.http.HttpStatusException;
import com.eactive.eai.adapter.http.dynamic.filter.FilterCryptoException;
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
import com.eactive.eai.common.message.EAIMessage;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.common.util.MessageUtil;
import com.eactive.eai.message.StandardItem;
import com.eactive.eai.message.StandardMessage;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;

/**
 * 템플릿 기반 어댑터 에러 메시지 핸들러.
 *
 * [프로퍼티 설정]
 * AdapterPropManager 의 "AdapterErrorMessageHandler" 프로퍼티 그룹에서
 * "{adapterGroupId}.template" 키로 등록된 템플릿 문자열을 읽어
 * StandardMessage 의 필드 값으로 치환한 결과를 반환한다.
 *
 * [변수 치환 규칙]
 * 1. "${callprop.키}"           → callProp.getProperty("키") 로 치환
 *    "${callprop.키:기본값}"    → 키 없거나 callProp null 이면 기본값 반환
 * 2. "${callprop[경로]}"        → StandardMessage 경로 값을 키로 callProp 간접 조회
 *    "${callprop[경로]:기본값}" → 키 결정 실패 또는 키 없으면 기본값 반환
 * 3. "${경로}"                  → StandardMessage.findItemValue("경로") 로 치환
 *    "${경로:기본값}"           → 경로 값이 null/공백이면 기본값 반환
 * 모든 형태에서 기본값 미지정 시 빈 문자열을 반환한다.
 *
 * [배열 반복 문법]  (MSG_LIST 등 GRID 타입)
 *   {{#foreach MSG.MSG_LIST}}
 *     { "code":"${outp_msg_cd}", "msg":"${outp_msg_ctnt}" }
 *   {{/foreach}}
 *   → MSG_LIST 각 행을 반복하며 행 내 필드명(접두어 없이)으로 치환,
 *     반복 결과를 쉼표(,)로 이어 붙임
 *
 * [사용 예 – JSON 템플릿]
 *   {
 *     "adapterName": "${callprop.ADAPTER_NAME}",
 *     "resultCode":  "${MSG.MAIN_MSG.outp_msg_cd}",
 *     "resultMsg":   "${MSG.MAIN_MSG.outp_msg_ctnt}",
 *     "errors": [
 *       {{#foreach MSG.MSG_LIST}}
 *       {
 *         "code":  "${outp_msg_cd}",
 *         "msg":   "${outp_msg_ctnt}",
 *         "desc":  "${outp_msg_desc}",
 *         "field": "${err_occu_item_nm}"
 *       }
 *       {{/foreach}}
 *     ]
 *   }
 *
 * [사용 예 – XML 템플릿]
 *   <error>
 *     <adapter>${callprop.ADAPTER_NAME}</adapter>
 *     <code>${MSG.MAIN_MSG.outp_msg_cd}</code>
 *     <errors>
 *       {{#foreach MSG.MSG_LIST}}
 *       <item><code>${outp_msg_cd}</code><msg>${outp_msg_ctnt}</msg></item>
 *       {{/foreach}}
 *     </errors>
 *   </error>
 *
 * [callProp 중첩 Properties 참조]
 * callProp 의 값이 Properties 인 경우 점(.) 으로 체인 탐색이 가능하다.
 *   callProp.put("OUTBOUND", subProps)  // subProps.setProperty("IF_ID", "SVC001")
 *   → 템플릿에서 ${callprop.OUTBOUND.IF_ID} 로 참조
 * 중간 값이 Properties 가 아닌 경우 전체 keyPath 를 키로 폴백 조회한다.
 */
public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandler {

    private static final Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);

    /** PropManager 에서 템플릿을 조회할 프로퍼티 그룹 이름 */
    static final String PROP_GROUP = "AdapterErrorMessageHandler";

    /** callProp 참조 변수 접두어: ${callprop.키} */
    static final String CALLPROP_PREFIX = "callprop.";

    /** callProp 간접 참조 접두어: ${callprop[표준전문경로]} */
    static final String CALLPROP_INDIRECT_PREFIX = "callprop[";

    /** 예외 객체 필드 참조 접두어: ${exception.필드명} */
    static final String EXCEPTION_PREFIX = "exception.";

    /** ${path} 또는 ${callprop.key} 스칼라 변수 패턴 (renderRow 내부에서 사용) */
    private static final Pattern VAR_PATTERN =
            Pattern.compile("\\$\\{([^}]+)\\}");

    /**
     * foreach 블록과 스칼라 변수를 순서대로 단일 패스로 처리하는 통합 패턴.
     * group(1) != null → foreach 블록: group(1)=경로, group(2)=블록 본문
     * group(3) != null → 스칼라 변수: group(3)=표현식
     */
    private static final Pattern COMBINED_PATTERN =
            Pattern.compile(
                "\\{\\{#foreach\\s+([^}]+)\\}\\}(.*?)\\{\\{/foreach\\}\\}|\\$\\{([^}]+)\\}",
                Pattern.DOTALL);

    @Override
    public Object generateNonStandardErrorResponseMessage(
            String inboundAdapterGroupName,
            String inboundAdapterName,
            Properties callProp,
            Object outboundRequestData,
            EAIMessage resEaiMsg) throws Exception {

    	StandardMessage resStandardMessage = resEaiMsg.getStandardMessage();
		String templateKey = inboundAdapterGroupName + ".template";
        String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);

        if (StringUtils.isBlank(template)) {
            if (logger.isWarn()) {
                logger.warn("TemplateAdapterErrorMsgHandler] template not found. group=" + PROP_GROUP
                        + ", key=" + templateKey);
            }
            return null;
        }

        return render(template, resStandardMessage, callProp);
    }

    /** 3-인자 위임: exception 없이 호출하는 기존 경로 유지 */
    String render(String template, StandardMessage msg, Properties callProp) {
        return render(template, msg, callProp, null);
    }

    /**
     * 템플릿에 StandardMessage, callProp, exception 값을 치환해 최종 문자열을 반환한다.
     * foreach 블록과 스칼라 변수를 좌→우 순서로 단일 패스 처리한다.
     * package-private: 단위 테스트에서 직접 호출 가능
     */
    String render(String template, StandardMessage msg, Properties callProp, Throwable exception) {
        // 관리 UI 등에서 HTML 인코딩된 엔티티를 복원 (예: &#35; → #)
        template = template.replace("&#35;", "#")
                           .replace("&amp;", "&")
                           .replace("&lt;",  "<")
                           .replace("&gt;",  ">")
                           .replace("&quot;", "\"");
        StringBuffer result = new StringBuffer();
        Matcher matcher = COMBINED_PATTERN.matcher(template);
        while (matcher.find()) {
            String replacement;
            if (matcher.group(1) != null) {
                // {{#foreach path}}...{{/foreach}} 블록
                String arrayPath    = matcher.group(1).trim();
                String blockContent = matcher.group(2);
                replacement = (msg != null) ? renderForeach(arrayPath, blockContent, msg) : "";
            } else {
                // ${path} / ${callprop.키} / ${exception.필드} 스칼라 변수
                String expr = matcher.group(3).trim();
                replacement = resolveScalar(expr, msg, callProp, exception);
            }
            matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
        }
        matcher.appendTail(result);
        return result.toString();
    }

    /**
     * 변수 표현식을 해석해 값을 반환한다.
     * 모든 형태에서 ':기본값' 접미사를 지원한다. 기본값 미지정 시 빈 문자열.
     * - "callprop[경로][:기본값]"   → 표준전문 경로 값을 키로 callProp 간접 조회
     * - "callprop.키[:기본값]"      → callProp 직접/중첩 조회
     * - "exception.필드[:기본값]"   → 리플렉션으로 예외 객체 getter/필드 조회
     * - "경로[:기본값]"             → StandardMessage.findItemValue(경로)
     */
    private String resolveScalar(String expr, StandardMessage msg, Properties callProp, Throwable exception) {
        if (expr.startsWith(CALLPROP_INDIRECT_PREFIX)) {
            return resolveIndirectCallProp(expr, msg, callProp);
        }
        if (expr.startsWith(CALLPROP_PREFIX)) {
            String rest = expr.substring(CALLPROP_PREFIX.length());
            int colonIdx = rest.indexOf(':');
            String keyPath    = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest;
            String defaultVal = colonIdx >= 0 ? rest.substring(colonIdx + 1) : "";
            if (callProp == null) return defaultVal;
            String value = resolveCallProp(callProp, keyPath);
            return value != null ? value : defaultVal;
        }
        if (expr.startsWith(EXCEPTION_PREFIX)) {
            String rest = expr.substring(EXCEPTION_PREFIX.length());
            int colonIdx = rest.indexOf(':');
            String fieldName  = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest;
            String defaultVal = colonIdx >= 0 ? rest.substring(colonIdx + 1) : "";
            if (exception == null) return defaultVal;
            String value = resolveExceptionProperty(exception, fieldName);
            return value != null ? value : defaultVal;
        }
        int colonIdx = expr.indexOf(':');
        String path      = colonIdx >= 0 ? expr.substring(0, colonIdx) : expr;
        String defaultVal = colonIdx >= 0 ? expr.substring(colonIdx + 1) : "";
        String value = resolveMessagePath(msg, path);
        return StringUtils.isNotEmpty(value) ? value : defaultVal;
    }

    /**
     * StandardMessage 경로를 해석한다.
     * 1. 경로에 '[n]' 구문이 있으면 배열 인덱스 접근으로 처리한다.
     *    예) MSG.MSG_LIST[0].outp_msg_cd → MSG_LIST 첫 번째 행의 outp_msg_cd 값
     * 2. findItemValue(path) 로 직접 조회한다.
     * 3. null 이면 경로를 뒤에서부터 분리하며, 부모 경로 값이 JSON 문자열인 경우
     *    나머지 경로(subPath)를 키로 JSON 필드를 추출한다.
     *    예) DATA.BIZDATA.acctNo → findItemValue("DATA.BIZDATA") → JSON 파싱 → acctNo
     *        DATA.BIZDATA.addr.city → findItemValue("DATA.BIZDATA") → JSON 파싱 → addr.city
     */
    private String resolveMessagePath(StandardMessage msg, String path) {
        if (msg == null) return null;

        int bracketStart = path.indexOf('[');
        if (bracketStart >= 0) {
            return resolveArrayIndex(msg, path, bracketStart);
        }

        String value = msg.findItemValue(path);
        if (value != null) return value;

        int dotIdx = path.lastIndexOf('.');
        while (dotIdx > 0) {
            String parentPath = path.substring(0, dotIdx);
            String subPath    = path.substring(dotIdx + 1);
            String parentVal  = msg.findItemValue(parentPath);
            if (StringUtils.isNotBlank(parentVal)) {
                String extracted = extractFromJson(parentVal, subPath);
                if (extracted != null) return extracted;
            }
            dotIdx = parentPath.lastIndexOf('.');
        }
        return null;
    }

    /**
     * 배열 인덱스 접근 구문 처리: {arrayPath}[{n}].{fieldName}
     * - arrayPath : GRID 타입 아이템 경로 (예: MSG.MSG_LIST)
     * - n         : 0-based 행 인덱스
     * - fieldName : 행 내 컬럼명 (예: outp_msg_cd)
     * 인덱스가 범위를 벗어나거나 필드가 없으면 null 반환.
     */
    private String resolveArrayIndex(StandardMessage msg, String path, int bracketStart) {
        if (msg == null) return null;

        int bracketEnd = path.indexOf(']', bracketStart);
        if (bracketEnd < 0) return null;

        String arrayPath = path.substring(0, bracketStart);
        String indexStr  = path.substring(bracketStart + 1, bracketEnd);
        int index;
        try {
            index = Integer.parseInt(indexStr.trim());
        } catch (NumberFormatException e) {
            return null;
        }

        String fieldName = null;
        if (bracketEnd + 2 <= path.length() - 1 && path.charAt(bracketEnd + 1) == '.') {
            fieldName = path.substring(bracketEnd + 2);
        }
        if (StringUtils.isBlank(fieldName)) return null;

        StandardItem arrayItem = msg.findItem(arrayPath);
        if (arrayItem == null) return null;

        List<LinkedHashMap<String, StandardItem>> rows = arrayItem.getList();
        if (rows == null || index < 0 || index >= rows.size()) return null;

        StandardItem item = rows.get(index).get(fieldName);
        return item != null ? StringUtils.defaultString(item.getValue()) : null;
    }

    /**
     * JSON 문자열에서 dot 구분 경로로 필드 값을 추출한다.
     * 중간 경로가 JSON 오브젝트면 계속 탐색하고, 최종 값이 primitive 면 문자열로,
     * 오브젝트/배열이면 JSON 문자열 그대로 반환한다. 파싱 실패 시 null 반환.
     */
    private String extractFromJson(String json, String fieldPath) {
        try {
            JsonElement el = new JsonParser().parse(json);
            for (String part : fieldPath.split("\\.", -1)) {
                if (!el.isJsonObject()) return null;
                el = el.getAsJsonObject().get(part);
                if (el == null) return null;
            }
            return el.isJsonPrimitive() ? el.getAsString() : el.toString();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * ${callprop[경로]} 또는 ${callprop[경로]:기본값} 처리.
     * 1. 표준전문 경로로 callProp 키를 동적으로 결정한다.
     * 2. 결정된 키로 callProp 에서 값을 조회한다.
     * 3. 키 또는 값을 찾지 못하면 기본값(없으면 빈 문자열)을 반환한다.
     */
    private String resolveIndirectCallProp(String expr, StandardMessage msg, Properties callProp) {
        int closeIdx = expr.indexOf(']');
        if (closeIdx < 0) return "";

        String msgPath   = expr.substring(CALLPROP_INDIRECT_PREFIX.length(), closeIdx);
        String defaultVal = (closeIdx + 1 < expr.length() && expr.charAt(closeIdx + 1) == ':')
                ? expr.substring(closeIdx + 2)
                : "";

        String key = msg.findItemValue(msgPath);
        if (StringUtils.isBlank(key)) return defaultVal;
        if (callProp == null)         return defaultVal;

        String value = resolveCallProp(callProp, key);
        return value != null ? value : defaultVal;
    }

    /**
     * Properties 체인을 점(.) 구분자로 재귀 탐색한다.
     * - 첫 세그먼트의 값이 Properties 이면 나머지 경로로 재귀
     * - String 이면 반환, 그 외 Object 이면 toString() 반환
     * - 첫 세그먼트 값이 Properties 가 아닌 경우 전체 keyPath 로 폴백 조회
     */
    private String resolveCallProp(Properties props, String keyPath) {
        int dotIdx = keyPath.indexOf('.');
        if (dotIdx < 0) {
            Object val = props.get(keyPath);
            if (val == null)              return null;
            if (val instanceof String)    return (String) val;
            return val.toString();
        }

        String first = keyPath.substring(0, dotIdx);
        String rest  = keyPath.substring(dotIdx + 1);
        Object val = props.get(first);
        if (val instanceof Properties) {
            return resolveCallProp((Properties) val, rest);
        }

        // 중간 값이 Properties 가 아닌 경우: 전체 keyPath 를 키로 폴백
        Object direct = props.get(keyPath);
        if (direct == null)              return null;
        if (direct instanceof String)    return (String) direct;
        return direct.toString();
    }

    /**
     * GRID 타입 배열 아이템을 반복하며 blockContent 를 각 행으로 치환,
     * 결과를 쉼표로 이어 반환한다. (foreach 블록 내부는 callProp 참조 미지원)
     */
    private String renderForeach(String arrayPath, String blockContent, StandardMessage msg) {
    	StandardItem arrayItem = msg.findItem(arrayPath);
        if (arrayItem == null) {
            if (logger.isWarn()) logger.warn("TemplateAdapterErrorMsgHandler] foreach path not found: " + arrayPath);
            return "";
        }

        List<LinkedHashMap<String, StandardItem>> rows = arrayItem.getList();
        if (rows == null || rows.isEmpty()) {
            return "";
        }

        List<String> renderedItems = new ArrayList<>();
        for (LinkedHashMap<String, StandardItem> row : rows) {
            String rowText = renderRow(blockContent, row);
            if (StringUtils.isNotBlank(rowText)) {
                renderedItems.add(rowText);
            }
        }
        return String.join(",", renderedItems);
    }

    /**
     * 배열 한 행(LinkedHashMap) 기준으로 blockContent 의 ${fieldName} 을 치환한다.
     * fieldName 은 경로 없이 해당 행의 컬럼명만 사용한다.
     */
    private String renderRow(String blockContent, LinkedHashMap<String, StandardItem> row) {
        StringBuffer result = new StringBuffer();
        Matcher varMatcher = VAR_PATTERN.matcher(blockContent);
        while (varMatcher.find()) {
            String fieldName = varMatcher.group(1).trim();
            String value = "";
            StandardItem item = row.get(fieldName);
            if (item != null) {
                value = StringUtils.defaultString(item.getValue());
            }
            varMatcher.appendReplacement(result, Matcher.quoteReplacement(value));
        }
        varMatcher.appendTail(result);
        return result.toString().trim();
    }

	@Override
	public Object generateNonStandardInternalErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName,
			Properties callProp, Object inboundRequestData, EAIMessage resEaiMsg) throws Exception {
		String templateKey = inboudnAdapterGroupName + ".sys.template";
		String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);

		if (StringUtils.isBlank(template)) {
			if (logger.isWarn()) {
				logger.warn("TemplateAdapterErrorMsgHandler] template not found. group=" + PROP_GROUP
						+ ", key=" + templateKey);
			}
			return null;
		}

		return render(template, resEaiMsg.getStandardMessage(), callProp);
	}
	
	@Override
	public Object generateNonStandardInboundErrorResponseMessage(String adapterGroupName, String adapterName,
			Properties callProp, Object inboundRequestData, Object inboundResponseData, Throwable e) throws Exception {
		
		AdapterGroupVO adapterGroupVO = AdapterManager.getInstance().getAdapterGroupVO(adapterGroupName);
		AdapterVO adapterVO = AdapterManager.getInstance().getAdapterVO(adapterGroupName, adapterName);
		Properties httpProp = AdapterPropManager.getInstance().getProperties(adapterVO.getPropGroupName());
		String adptMsgType = adapterVO.getAdapterGroupVO().getMessageType();
		String encode = StringUtils.defaultIfBlank(adapterGroupVO.getMessageEncode(), "UTF-8");

		if (e instanceof FilterCryptoException) {
			return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e, 550);
		} else if (e instanceof HttpStatusException) {
			HttpStatusException e1 = (HttpStatusException) e;
			int httpCode = e1.getStatus();

			return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e1, httpCode);
		} else if (e instanceof JwtAuthException) {
			return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e,
					HttpStatus.UNAUTHORIZED.value());
		} else {
			return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e,
					HttpStatus.INTERNAL_SERVER_ERROR.value());
		}
	}
	
	/**
	 * 표준 -> 비표준 거래
	 * 아웃바운드 어댑터에서 Exception이 발생했을 때, Outbound Process 응답 메시지 생성
	 */
	@Override
	public Object generateOutboundErrorResponseMessage(String outboundadapterGroupName,
			String outboundadapterName, Properties callProp, Object outboundRequestData, EAIMessage resEaiMsg)
			throws Exception {
		String templateKey = outboundadapterGroupName + ".template";
        String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
    	if (StringUtils.isBlank(template)) {
			return null;
		}
		return render(template, resEaiMsg.getStandardMessage(), callProp);
	}

	private Object genInboundErrorResponse(String adapterGroupName, Properties callProp, Properties httpProp,
			String adptMsgType, String encode, Throwable e1, int httpCode) {
		String templateKey = adapterGroupName + ".in." + httpCode + ".template";
		String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
		if (StringUtils.isBlank(template)) {
			templateKey = adapterGroupName + ".in.template";
			template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
			if (StringUtils.isBlank(template)) {
				String errorResponseFormat = httpProp.getProperty("ERROR_RESPONSE_FORMAT");
				return MessageUtil.makeErrorMessageByMessageType(adptMsgType, encode,
						MessageUtil.ERROR_CODE_AP_ERROR, e1.getMessage(), errorResponseFormat);
			}
		}
		return render(template, null, callProp, e1);
	}

    /**
     * 리플렉션으로 예외 객체의 필드 값을 동적으로 조회한다.
     * 탐색 순서: getXxx() getter → isXxx() getter → 필드 직접 접근
     * 상위 클래스(Throwable 포함)까지 계층적으로 탐색한다.
     */
    private String resolveExceptionProperty(Throwable e, String fieldName) {
        if (StringUtils.isBlank(fieldName)) return null;
        String cap = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
        for (String prefix : new String[]{"get", "is"}) {
            Method m = findMethod(e.getClass(), prefix + cap);
            if (m != null) {
                try {
                    Object val = m.invoke(e);
                    return val != null ? val.toString() : null;
                } catch (Exception ignored) {}
            }
        }
        Field f = findField(e.getClass(), fieldName);
        if (f != null) {
            try {
                f.setAccessible(true);
                Object val = f.get(e);
                return val != null ? val.toString() : null;
            } catch (Exception ignored) {}
        }
        return null;
    }

    private Method findMethod(Class<?> clazz, String name) {
        while (clazz != null) {
            try {
                Method m = clazz.getDeclaredMethod(name);
                m.setAccessible(true);
                return m;
            } catch (NoSuchMethodException e) {
                clazz = clazz.getSuperclass();
            }
        }
        return null;
    }

    private Field findField(Class<?> clazz, String name) {
        while (clazz != null) {
            try {
                return clazz.getDeclaredField(name);
            } catch (NoSuchFieldException e) {
                clazz = clazz.getSuperclass();
            }
        }
        return null;
    }

}
