package com.eactive.eai.adapter.handler;

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 com.eactive.eai.common.property.PropManager;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.message.StandardItem;
import com.eactive.eai.message.StandardMessage;

/**
 * 템플릿 기반 어댑터 에러 메시지 핸들러.
 *
 * [프로퍼티 설정]
 * 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);

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

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

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

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

    /** {{#foreach path}}...{{/foreach}} 배열 반복 블록 패턴 */
    private static final Pattern FOREACH_PATTERN =
            Pattern.compile("\\{\\{#foreach\\s+([^}]+)\\}\\}(.*?)\\{\\{/foreach\\}\\}",
                    Pattern.DOTALL);

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

        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);
    }

    /**
     * 템플릿에 StandardMessage 및 callProp 값을 치환해 최종 문자열을 반환한다.
     * package-private: 단위 테스트에서 직접 호출 가능
     */
    String render(String template, StandardMessage msg, Properties callProp) {
        // 1단계: {{#foreach path}}...{{/foreach}} 배열 반복 처리
        StringBuffer sb = new StringBuffer();
        Matcher foreachMatcher = FOREACH_PATTERN.matcher(template);
        while (foreachMatcher.find()) {
            String arrayPath    = foreachMatcher.group(1).trim();
            String blockContent = foreachMatcher.group(2);
            String rendered     = renderForeach(arrayPath, blockContent, msg);
            foreachMatcher.appendReplacement(sb, Matcher.quoteReplacement(rendered));
        }
        foreachMatcher.appendTail(sb);

        // 2단계: 나머지 ${path} / ${callprop.키} 스칼라 변수 치환
        String intermediate = sb.toString();
        StringBuffer result = new StringBuffer();
        Matcher varMatcher = VAR_PATTERN.matcher(intermediate);
        while (varMatcher.find()) {
            String expr  = varMatcher.group(1).trim();
            String value = resolveScalar(expr, msg, callProp);
            varMatcher.appendReplacement(result, Matcher.quoteReplacement(value));
        }
        varMatcher.appendTail(result);
        return result.toString();
    }

    /**
     * 변수 표현식을 해석해 값을 반환한다.
     * 모든 형태에서 ':기본값' 접미사를 지원한다. 기본값 미지정 시 빈 문자열.
     * - "callprop[경로][:기본값]"  → 표준전문 경로 값을 키로 callProp 간접 조회
     * - "callprop.키[:기본값]"     → callProp 직접/중첩 조회
     * - "경로[:기본값]"            → StandardMessage.findItemValue(경로)
     */
    private String resolveScalar(String expr, StandardMessage msg, Properties callProp) {
        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;
        }
        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. findItemValue(path) 로 직접 조회한다.
     * 2. 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) {
        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;
    }

    /**
     * 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();
    }
}
