package com.eactive.eai.adapter.http.dynamic.filter;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;

import com.eactive.eai.common.security.CryptoModuleService;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.util.JsonPathUtil;

/**
 * HTTP 어댑터 요청/응답 암복호화 필터 기반 클래스.
 *
 * 서브클래스는 {@link #buildRuntimeContext}를 구현하여
 * DYNAMIC 키 도출에 필요한 컨텍스트 맵을 제공한다.
 * STATIC 키 모듈을 사용하는 경우 빈 Map을 반환하면 된다.
 * (CryptoModuleManager가 KEY_SOURCE_TYPE=STATIC이면 runtimeContext를 무시하고 고정 키를 사용한다.)
 *
 * 어댑터 프로퍼티 키:
 *   CRYPTO_MODULE_NAME   필수. DB에 등록된 암호화 모듈명.
 *   CRYPTO_SCOPE         BODY | FIELD (기본 FIELD)
 *                        BODY  : 메시지 전체를 암복호화 (Base64 인코딩/디코딩)
 *                        FIELD : 특정 JSON 경로의 값만 암복호화
 *
 *   FIELD 범위 전용:
 *   CRYPTO_DEC_FROM_PATH 복호화 대상 JSON 경로 (예: /encrypted_data)
 *   CRYPTO_DEC_TO_PATH   복호화 결과 반영 경로. "/" = 전체 body 교체.
 *   CRYPTO_ENC_FROM_PATH 암호화 대상 JSON 경로. "/" = 전체 body 암호화.
 *   CRYPTO_ENC_TO_PATH   암호화 결과(Base64)를 넣을 JSON 경로 (예: /encrypted_data)
 *
 *   GCM AAD 전용 (선택):
 *   CRYPTO_AAD_HEADER    AAD로 사용할 HTTP 요청 헤더명 (예: X-Api-Request-Id).
 *                        미설정 또는 헤더값 없으면 aad=null → IV를 AAD 대체값으로 사용.
 */
public abstract class CryptoFilter implements HttpAdapterFilter {

    protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);

    public static final String PROP_MODULE_NAME   = "CRYPTO_MODULE_NAME";
    public static final String PROP_SCOPE         = "CRYPTO_SCOPE";
    public static final String PROP_DEC_FROM_PATH = "CRYPTO_DEC_FROM_PATH";
    public static final String PROP_DEC_TO_PATH   = "CRYPTO_DEC_TO_PATH";
    public static final String PROP_ENC_FROM_PATH = "CRYPTO_ENC_FROM_PATH";
    public static final String PROP_ENC_TO_PATH   = "CRYPTO_ENC_TO_PATH";
    public static final String PROP_AAD_HEADER    = "CRYPTO_AAD_HEADER";

    public static final String SCOPE_BODY  = "BODY";
    public static final String SCOPE_FIELD = "FIELD";
    public static final String PATH_ROOT   = "/";
    public static final String PATH_ENCDATA   = "/encrypted_data";

    // ------------------------------------------------------------------
    // 추상 메서드
    // ------------------------------------------------------------------

    /**
     * DYNAMIC 키 도출에 필요한 runtimeContext 맵을 구성한다.
     * STATIC 키 모듈이면 빈 Map({@code new HashMap<>()})을 반환한다.
     */
    protected abstract Map<String, String> buildRuntimeContext(Properties prop, HttpServletRequest request);

    // ------------------------------------------------------------------
    // doPreFilter: 요청 복호화
    // ------------------------------------------------------------------

    @Override
    public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
            HttpServletRequest request, HttpServletResponse response) throws Exception {

        String moduleName = required(prop, PROP_MODULE_NAME);
        String scope      = getProp(prop, PROP_SCOPE, SCOPE_FIELD).toUpperCase();
        String body       = toBodyString(message);
        Map<String, String> runtimeCtx = buildRuntimeContext(prop, request);
        byte[] aad        = buildAad(prop, request);

        if (SCOPE_FIELD.equals(scope)) {
            return decryptField(body, moduleName, runtimeCtx, aad,
                    getProp(prop, PROP_DEC_FROM_PATH, PATH_ENCDATA),
                    getProp(prop, PROP_DEC_TO_PATH, PATH_ROOT));
        }
        return decryptBody(body, moduleName, runtimeCtx, aad);
    }

    // ------------------------------------------------------------------
    // doPostFilter: 응답 암호화
    // ------------------------------------------------------------------

    @Override
    public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
            HttpServletRequest request, HttpServletResponse response) throws Exception {

        String moduleName = required(prop, PROP_MODULE_NAME);
        String scope      = getProp(prop, PROP_SCOPE, SCOPE_FIELD).toUpperCase();
        String body       = toBodyString(resultMessage);
        Map<String, String> runtimeCtx = buildRuntimeContext(prop, request);
        byte[] aad        = buildAad(prop, request);

        if (SCOPE_FIELD.equals(scope)) {
            return encryptField(body, moduleName, runtimeCtx, aad,
                    getProp(prop, PROP_ENC_FROM_PATH, PATH_ROOT),
                    getProp(prop, PROP_ENC_TO_PATH, PATH_ENCDATA));
        }
        return encryptBody(body, moduleName, runtimeCtx, aad);
    }

    // ------------------------------------------------------------------
    // 복호화 구현
    // ------------------------------------------------------------------

    private String decryptBody(String body, String moduleName, Map<String, String> runtimeCtx, byte[] aad) throws Exception {
        byte[] cipherBytes = Base64.getDecoder().decode(body.trim());
        byte[] plainBytes  = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, aad, cipherBytes);
        return new String(plainBytes, StandardCharsets.UTF_8);
    }

    /**
     * fromPath의 Base64 값을 복호화하여 toPath에 반영.
     * toPath = "/" → 복호화된 텍스트로 body 전체 교체.
     */
    private String decryptField(String body, String moduleName, Map<String, String> runtimeCtx, byte[] aad,
            String fromPath, String toPath) throws Exception {

        String encBase64 = JsonPathUtil.getValueAtPath(body, fromPath);
        if (StringUtils.isBlank(encBase64)) {
            logger.warn("CryptoFilter] 복호화 대상 필드 없음: path=" + fromPath);
            return body;
        }

        byte[] cipherBytes = Base64.getDecoder().decode(encBase64.trim());
        byte[] plainBytes  = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, aad, cipherBytes);
        String plainText   = new String(plainBytes, StandardCharsets.UTF_8);

        if (PATH_ROOT.equals(toPath)) {
            return plainText;
        }
        return JsonPathUtil.setValueAtPath(body, toPath, plainText, false);
    }

    // ------------------------------------------------------------------
    // 암호화 구현
    // ------------------------------------------------------------------

    private String encryptBody(String body, String moduleName, Map<String, String> runtimeCtx, byte[] aad) throws Exception {
        byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
                body.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(cipherBytes);
    }

    /**
     * fromPath 값을 암호화하여 toPath(Base64)에 반영.
     * fromPath = "/" → body 전체를 암호화하여 toPath 필드명으로 래핑.
     */
    private String encryptField(String body, String moduleName, Map<String, String> runtimeCtx, byte[] aad,
            String fromPath, String toPath) throws Exception {

        String plainText;
        if (PATH_ROOT.equals(fromPath)) {
            plainText = body;
        } else {
            plainText = JsonPathUtil.getValueAtPath(body, fromPath);
            if (StringUtils.isBlank(plainText)) {
                logger.warn("CryptoFilter] 암호화 대상 필드 없음: path=" + fromPath);
                return body;
            }
        }

        byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
                plainText.getBytes(StandardCharsets.UTF_8));
        String encBase64 = Base64.getEncoder().encodeToString(cipherBytes);

        if (PATH_ROOT.equals(fromPath)) {
            String fieldName = toPath.replaceFirst("^/", "");
            return "{\"" + fieldName + "\":\"" + encBase64 + "\"}";
        }
        return JsonPathUtil.setValueAtPath(body, toPath, encBase64, false);
    }

    // ------------------------------------------------------------------
    // 유틸
    // ------------------------------------------------------------------

    /**
     * 필터로 전달되는 message 객체를 JSON 문자열로 변환한다.
     * <ul>
     *   <li>{@code String} — 그대로 반환</li>
     *   <li>{@code byte[]} — UTF-8 디코딩</li>
     *   <li>{@code JSONObject} / 기타 — {@code toString()} 호출
     *       (json-simple JSONObject는 toString()이 toJSONString()을 위임함)</li>
     * </ul>
     */
    protected String toBodyString(Object message) {
        if (message instanceof String) {
            return (String) message;
        }
        if (message instanceof byte[]) {
            return new String((byte[]) message, StandardCharsets.UTF_8);
        }
        return message.toString();
    }

    /**
     * crypto.aad.header 프로퍼티에 지정된 헤더값을 UTF-8 바이트로 반환한다.
     * 프로퍼티 미설정 또는 헤더값 없으면 null을 반환하여 GCM 기본 동작(IV를 AAD 대체값으로 사용)을 따른다.
     */
    protected byte[] buildAad(Properties prop, HttpServletRequest request) {
        String headerName = prop.getProperty(PROP_AAD_HEADER);
        if (StringUtils.isBlank(headerName) || request == null) {
            return null;
        }
        String headerValue = request.getHeader(headerName);
        return StringUtils.isBlank(headerValue) ? null : headerValue.getBytes(StandardCharsets.UTF_8);
    }

    protected String required(Properties prop, String key) {
        String v = prop.getProperty(key);
        if (StringUtils.isBlank(v)) {
            throw new IllegalArgumentException("CryptoFilter 필수 프로퍼티 누락: " + key);
        }
        return v;
    }

    private String getProp(Properties prop, String key, String defaultVal) {
        String v = prop.getProperty(key);
        return StringUtils.isBlank(v) ? defaultVal : v;
    }
}
