package com.ga.api.aop;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.ga.api.service.external.ExternalCallLogService;
import com.ga.common.adapter.bank.BankTransferRequest;
import com.ga.common.adapter.bank.BankTransferResponse;
import com.ga.common.adapter.message.MessageRequest;
import com.ga.common.adapter.message.MessageResponse;
import com.ga.common.util.SecurityUtil;
import com.ga.core.vo.external.ExternalCallLogVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 외부 연동 어댑터 호출 감사 이력 자동 영속화 (V85 external_call_log).
 *
 * <p>대상: {@link com.ga.common.adapter.bank.BankTransferAdapter},
 * {@link com.ga.common.adapter.message.MessageAdapter} 모든 메서드.
 *
 * <p>정책:
 * <ul>
 *   <li>호출 전/후 (정상·예외) 모두 영속화. 예외는 그대로 propagate.
 *   <li>request/response 는 마스킹된 JSON 요약만 저장 (계좌·전화 뒤 4자리만).
 *   <li>영속화는 별도 트랜잭션({@link com.ga.api.service.external.ExternalCallLogService}#recordCall = REQUIRES_NEW).
 *   <li>영속화 자체가 실패해도 비즈니스 흐름을 깨지 않는다 (warn 로그만).
 * </ul>
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class ExternalCallLoggingAspect {

    private final ExternalCallLogService logService;
    private final ObjectMapper om;

    @Around("execution(* com.ga.common.adapter.bank.BankTransferAdapter+.*(..))")
    public Object aroundBank(ProceedingJoinPoint jp) throws Throwable {
        return around(jp, "BANK");
    }

    @Around("execution(* com.ga.common.adapter.message.MessageAdapter+.*(..))")
    public Object aroundMessage(ProceedingJoinPoint jp) throws Throwable {
        return around(jp, "MESSAGE");
    }

    @Around("execution(* com.ga.common.adapter.push.PushAdapter+.*(..))")
    public Object aroundPush(ProceedingJoinPoint jp) throws Throwable {
        return around(jp, "PUSH");
    }

    private Object around(ProceedingJoinPoint jp, String callType) throws Throwable {
        long start = System.currentTimeMillis();
        ExternalCallLogVO vo = new ExternalCallLogVO();
        vo.setCallType(callType);
        vo.setAdapterClass(simpleName(jp.getTarget()));
        vo.setMethodName(jp.getSignature().getName());
        vo.setCalledAt(LocalDateTime.now());
        try {
            vo.setCreatedBy(SecurityUtil.getCurrentUserId());
        } catch (Exception ignore) {
            // 스케줄러/배치 컨텍스트에서는 인증 정보 없을 수 있음
        }
        applyRequest(vo, jp.getArgs());

        try {
            Object result = jp.proceed();
            vo.setSuccess(true);
            applyResponse(vo, result);
            return result;
        } catch (Throwable t) {
            vo.setSuccess(false);
            vo.setErrorClass(t.getClass().getName());
            vo.setErrorMessage(truncate(t.getMessage(), 4000));
            throw t;
        } finally {
            vo.setDurationMs((int) (System.currentTimeMillis() - start));
            try {
                logService.recordCall(vo);
            } catch (Exception persistEx) {
                log.warn("[external_call_log] persist failed: {}", persistEx.getMessage());
            }
        }
    }

    private void applyRequest(ExternalCallLogVO vo, Object[] args) {
        if (args == null || args.length == 0) return;
        Object a0 = args[0];
        if (a0 instanceof BankTransferRequest btr) {
            vo.setTargetRefType("WITHDRAW");
            vo.setTargetRefId(btr.getWithdrawRequestId());
            Map<String, Object> m = new LinkedHashMap<>();
            m.put("withdrawRequestId", btr.getWithdrawRequestId());
            m.put("bankCode",          btr.getBankCode());
            m.put("accountNoMasked",   maskAccount(btr.getAccountNo()));
            m.put("holder",            maskName(btr.getAccountHolderName()));
            m.put("amount",            btr.getAmount());
            m.put("memo",              btr.getMemo());
            vo.setRequestSummary(toJson(m));
        } else if (a0 instanceof MessageRequest mr) {
            vo.setTargetRefType(mr.getRefType());
            vo.setTargetRefId(mr.getRefId());
            Map<String, Object> m = new LinkedHashMap<>();
            m.put("channel",        mr.getChannel());
            m.put("phoneMasked",    maskPhone(mr.getPhoneNumber()));
            m.put("title",          mr.getTitle());
            m.put("templateCode",   mr.getTemplateCode());
            m.put("refUserId",      mr.getRefUserId());
            m.put("contentPreview", truncate(mr.getContent(), 80));
            vo.setRequestSummary(toJson(m));
        } else if (a0 instanceof com.ga.common.adapter.push.PushRequest pr) {
            vo.setTargetRefType(pr.getRefType());
            vo.setTargetRefId(pr.getRefId());
            Map<String, Object> m = new LinkedHashMap<>();
            m.put("tokenMasked",    maskToken(pr.getDeviceToken()));
            m.put("title",          pr.getTitle());
            m.put("refUserId",      pr.getRefUserId());
            m.put("bodyPreview",    truncate(pr.getBody(), 80));
            vo.setRequestSummary(toJson(m));
        } else if (a0 instanceof String s) {
            // BankTransferAdapter.queryStatus(txId)
            vo.setRequestSummary("{\"arg\":\"" + safeJsonString(s) + "\"}");
        }
    }

    private void applyResponse(ExternalCallLogVO vo, Object result) {
        if (result instanceof BankTransferResponse btr) {
            vo.setResultCode(btr.getResultCode());
            vo.setResultMessage(truncate(btr.getResultMessage(), 1000));
            Map<String, Object> m = new LinkedHashMap<>();
            m.put("txId",        btr.getTxId());
            m.put("status",      btr.getStatus());
            m.put("processedAt", btr.getProcessedAt());
            vo.setResponseSummary(toJson(m));
        } else if (result instanceof MessageResponse mr) {
            vo.setResultCode(mr.getResultCode());
            vo.setResultMessage(truncate(mr.getResultMessage(), 1000));
            Map<String, Object> m = new LinkedHashMap<>();
            m.put("messageId", mr.getMessageId());
            m.put("sent",      mr.isSent());
            m.put("sentAt",    mr.getSentAt());
            vo.setResponseSummary(toJson(m));
        } else if (result instanceof com.ga.common.adapter.push.PushResponse pr) {
            vo.setResultCode(pr.getResultCode());
            vo.setResultMessage(truncate(pr.getResultMessage(), 1000));
            Map<String, Object> m = new LinkedHashMap<>();
            m.put("messageId", pr.getMessageId());
            m.put("sent",      pr.isSent());
            m.put("sentAt",    pr.getSentAt());
            vo.setResponseSummary(toJson(m));
        }
    }

    private String simpleName(Object o) {
        return o == null ? null : o.getClass().getSimpleName();
    }

    private String toJson(Object o) {
        try {
            return om.writeValueAsString(o);
        } catch (Exception e) {
            return "{}";
        }
    }

    private String maskAccount(String accountNo) {
        if (accountNo == null) return null;
        String d = accountNo.replaceAll("[^0-9]", "");
        if (d.length() <= 4) return "****";
        return "****" + d.substring(d.length() - 4);
    }

    private String maskPhone(String phone) {
        if (phone == null) return null;
        String d = phone.replaceAll("[^0-9]", "");
        if (d.length() <= 4) return "****";
        return "***-****-" + d.substring(d.length() - 4);
    }

    private String maskName(String name) {
        if (name == null || name.isEmpty()) return null;
        return name.charAt(0) + "*".repeat(Math.max(0, name.length() - 1));
    }

    private String maskToken(String token) {
        if (token == null) return null;
        return token.length() <= 8 ? "****" : token.substring(0, 8) + "****";
    }

    private String truncate(String s, int max) {
        if (s == null) return null;
        return s.length() <= max ? s : s.substring(0, max);
    }

    private String safeJsonString(String s) {
        return s == null ? "" : s.replace("\\", "\\\\").replace("\"", "\\\"");
    }
}
