package cokr.xit.ens.modules.common.ctgy.intgrnbill.nv.service.process;

import cokr.xit.ens.core.exception.EnsException;
import cokr.xit.ens.core.exception.code.EnsErrCd;
import cokr.xit.ens.core.utils.CmmnUtil;
import cokr.xit.ens.core.utils.MapDeserailizer;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.nv.api.BillNvEasyPayApiSpec;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.nv.entity.BillNvEasyPay;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.nv.entity.QBillNvEasyPay;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.nv.entity.repository.BillNvEasyPayRepository;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.nv.service.process.model.NvPayAbleReqData;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.nv.service.process.model.NvPayApplyRespData;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.api.PayUseSysApi;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.code.BillLogSeCd;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.code.BillReqSeCd;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.code.BillSeCd;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.code.PayUseSysResCd;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.entity.Bill;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.entity.BillHist;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.entity.QBill;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.entity.repository.BillHistRepository;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.model.PayApiRespDTO;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.service.process.Pay;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.service.process.kit.PayAbleService;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.service.process.model.PayAbleData;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.service.process.model.PayCancelData;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.service.process.model.ReqAndRespInf;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.service.process.template.PayHelper;
import cokr.xit.ens.modules.common.ctgy.sys.mng.domain.OrgMng;
import cokr.xit.ens.modules.common.ctgy.sys.mng.domain.QOrgMng;
import cokr.xit.ens.modules.common.domain.support.FieldError;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Slf4j
@RequiredArgsConstructor
@Service
public class NvPayAbleService implements PayAbleService<PayAbleData<BillNvEasyPay>, Map<String, Object>, PayApiRespDTO<Map<String, Object>>> {
    private final BillNvEasyPayRepository billNvEasyPayRepository;

    private final JPAQueryFactory query;
    private final BillNvEasyPayApiSpec billNvEasyPayApi;
    private final PayUseSysApi payUseSysApi;
    private final BillHistRepository billHistRepository;
    private final NvPayCancelService nvPayCancelService;
    private final NvPayResultService nvPayResultService;

    //    private Gson gson = new Gson();
    private Gson gson = new GsonBuilder().registerTypeAdapter(Map.class, new MapDeserailizer()).disableHtmlEscaping().create();


    private enum DataKeys {
        payApplyReqMessage,
        payApplyRespMessage,
    }

    @Override
    public PayAbleData<BillNvEasyPay> getData(Map<String, Object> reqParam) {
        String billUid = (String) reqParam.getOrDefault("billUid", "");

        Bill bill = query.selectFrom(QBill.bill)
                .innerJoin(QBill.bill.orgMng, QOrgMng.orgMng).fetchJoin()
                .where(QBill.bill.billUid.eq(billUid))
                .fetchOne();
        BillNvEasyPay billNvEasyPay = query.selectFrom(QBillNvEasyPay.billNvEasyPay)
                .where(QBillNvEasyPay.billNvEasyPay.billUid.eq(billUid))
                .fetchOne();

        return PayAbleData.<BillNvEasyPay>builder()
                .bill(bill)
                .billDetail(billNvEasyPay)
                .build();
    }

    @Override
    public void validate(Map<String, Object> reqParam, PayAbleData<BillNvEasyPay> data) {
       
        if (CmmnUtil.isEmpty(reqParam.get("resultCode")))
            throw new EnsException(EnsErrCd.NO_REQUIRED_PARAM, "결과코드는 필수값 입니다.");
        if (!"Success".equals(reqParam.get("resultCode")))
//            throw new EnsException(EnsErrCd.INVALID_REQUEST, String.format("[%s] %s %s", BillLogSeCd.VD, reqParam.get("resultCode"), reqParam.get("resultMessage")));
            //2023.03.09 네이버페이 검수 결과 - 결제창 실패응답 화면에 resultMessage 원문만 노출 요함
            throw new EnsException(EnsErrCd.INVALID_REQUEST, String.format("[%s] %s", BillLogSeCd.VD, reqParam.get("resultMessage")));
        if (CmmnUtil.isEmpty(reqParam.get("billUid")))
            throw new EnsException(EnsErrCd.NO_REQUIRED_PARAM, "청구서UID는 필수값 입니다.");
        if (CmmnUtil.isEmpty(reqParam.get("paymentId")))
            throw new EnsException(EnsErrCd.NO_REQUIRED_PARAM, "결제ID는 필수값 입니다.");
       
        if (CmmnUtil.isEmpty(data.getBill()))
            throw new EnsException(EnsErrCd.NO_DATA_FOUND, "일치하는 청구서 자료가 없습니다.");
        if (CmmnUtil.isEmpty(data.getBillDetail()))
            throw new EnsException(EnsErrCd.NO_DATA_FOUND, "일치하는 청구서 상세 자료가 없습니다.");
    }

    @Override
    public void callApi(Map<String, Object> reqParam, PayAbleData<BillNvEasyPay> data) {
        Bill bill = data.getBill();
        OrgMng orgMng = bill.getOrgMng();

       
        final String partnerId = orgMng.getNvBpPartnerId();
        final String clientId = orgMng.getNvBpClientId();
        final String clientSecret = orgMng.getNvBpClientSecret();
        final String useSysUrl = orgMng.getNvBpPrepayApi();
        final String paymentId = (String) reqParam.get("paymentId");
        LinkedMultiValueMap reqData = new LinkedMultiValueMap<>();
        reqData.add("paymentId", paymentId);

       
        Optional.of(data.getBillDetail())
                .map(billDetail -> createMessage(billDetail))               
                .map(param -> loadReqData(data, useSysUrl, param))          
                .map(param -> payUseSysApi.payAble(useSysUrl, param, null)) 
                .map(response -> loadRespData(data, response))              
                .map(response -> checkResponse(response))                  
                .map(respDto -> loadPayApplyReqData(data, reqData))         
                .map(respDto -> billNvEasyPayApi.payApply(partnerId, clientId, clientSecret, reqData)) 
                .map(response -> loadPayApplyRespData(data, response))      
                .map(response -> checkApplyResponse(response))              
                .map(respDto -> {
                    try {
                        
                        validateApplyData(data, respDto);
                    } catch (EnsException e) {
                        
                        callPayCancelService(data, respDto, e);
                        throw e;
                    }
                    return respDto;
                })
                .map(respDto -> callPayResultService(respDto))                  
        ;
    }

    protected Map<String, NvPayAbleReqData> createMessage(BillNvEasyPay billDetail) {
        NvPayAbleReqData nvPayAbleReqData = NvPayAbleReqData.builder()
                .billerUserKey(billDetail.getBillerUserKey())
                .productName(billDetail.getProductName())
                .taxExScopeAmount(billDetail.getTaxExScopeAmount())
                .taxScopeAmount(billDetail.getTaxScopeAmount())
                .totalPayAmount(billDetail.getTotalPayAmount())
                .build();
        Map<String, NvPayAbleReqData> m = new HashMap<>();
        m.put("data", nvPayAbleReqData);
        return m;
    }

    protected Map<String, Object> loadReqData(PayAbleData<BillNvEasyPay> data, String url, Map<String, NvPayAbleReqData> param) {
        
        data.getReqAndRespInf().setUrl(url);
        data.getReqAndRespInf().setReqData(CmmnUtil.toJsonString(param));

        return gson.fromJson(gson.toJson(param), Map.class);
    }

    protected ResponseEntity<String> loadRespData(PayAbleData<BillNvEasyPay> data, ResponseEntity<String> response) {
        
        data.getReqAndRespInf().setRespRawData(response.getBody());
        return response;
    }

    protected PayApiRespDTO<Map<String, Object>> checkResponse(ResponseEntity<String> response) {
        
        if (!HttpStatus.OK.equals(response.getStatusCode()))
            throw new EnsException(EnsErrCd.API_COMM_ERROR, "[" + BillLogSeCd.US + "] " + EnsErrCd.API_COMM_ERROR.getCodeNm() + " " + response.getStatusCode().toString(), response.getBody());
        
        PayApiRespDTO<Map<String, Object>> result = null;
        try {
            result = gson.fromJson(response.getBody(), PayApiRespDTO.class);
        } catch (Exception ex) {
            throw new EnsException(EnsErrCd.INVALID_RESPONSE, "[" + BillLogSeCd.US + "] " + EnsErrCd.INVALID_RESPONSE.getCodeNm(), ex);
        }
        
        try {
            PayUseSysResCd.valueOf(result.getResCode());
        } catch (Exception ex) {
            throw new EnsException(EnsErrCd.INVALID_RES_CODE, "[" + BillLogSeCd.US + "] " + EnsErrCd.INVALID_RES_CODE.getCodeNm(), ex);
        }
        if (!PayUseSysResCd.OK.getCode().equals(result.getResCode())) {
            result.setMessage("[" + BillLogSeCd.US + "] " + result.getMessage());
            throw new EnsException(EnsErrCd.RESPONSED_FAILURE_CODE, EnsErrCd.RESPONSED_FAILURE_CODE.getCodeNm(), result);
        }

        return result;
    }

    protected LinkedMultiValueMap loadPayApplyReqData(PayAbleData<BillNvEasyPay> data, LinkedMultiValueMap reqData) {
        data.getReqAndRespInf().getEtc().put(DataKeys.payApplyReqMessage.name(), gson.toJson(reqData));
        return reqData;
    }

    protected ResponseEntity<String> loadPayApplyRespData(PayAbleData<BillNvEasyPay> data, ResponseEntity<String> response) {
        data.getReqAndRespInf().getEtc().put(DataKeys.payApplyRespMessage.name(), response.getBody());
        return response;
    }

    protected PayApiRespDTO<NvPayApplyRespData> checkApplyResponse(ResponseEntity<String> response) {
        
        if (!HttpStatus.OK.equals(response.getStatusCode()))
            throw new EnsException(EnsErrCd.API_COMM_ERROR, "[" + BillLogSeCd.VD + "] " + EnsErrCd.API_COMM_ERROR.getCodeNm() + " " + response.getStatusCode().toString(), response.getBody());
        
        PayApiRespDTO<NvPayApplyRespData> result = null;
        try {
            result = gson.fromJson(response.getBody(), new TypeToken<PayApiRespDTO<NvPayApplyRespData>>() {
            }.getType());
        } catch (Exception ex) {
            throw new EnsException(EnsErrCd.INVALID_RESPONSE, "[" + BillLogSeCd.VD + "] " + EnsErrCd.INVALID_RESPONSE.getCodeNm(), ex);
        }
        
        if (!"Success".equals(result.getResCode())) {
            result.setMessage("[" + BillLogSeCd.VD + "] " + result.getMessage());
            throw new EnsException(EnsErrCd.RESPONSED_FAILURE_CODE, EnsErrCd.RESPONSED_FAILURE_CODE.getCodeNm(), result);
        }

        return result;
    }

    protected void validateApplyData(PayAbleData<BillNvEasyPay> data, PayApiRespDTO<NvPayApplyRespData> respDTO) {
        final BillNvEasyPay billNvEasyPay = data.getBillDetail();
        if (!billNvEasyPay.getTotalPayAmount().equals(respDTO.getData().getDetail().getTotalPayAmount()))
            throw new EnsException(EnsErrCd.INVALID_RES_DATA, "요청한 총결제금액과 결제승인된 금액이 상이 합니다.");
        if (!billNvEasyPay.getTaxScopeAmount().equals(respDTO.getData().getDetail().getTaxScopeAmount()))
            throw new EnsException(EnsErrCd.INVALID_RES_DATA, "요청한 과세금액과 결제승인된 금액이 상이 합니다.");
        if (!billNvEasyPay.getTaxExScopeAmount().equals(respDTO.getData().getDetail().getTaxExScopeAmount()))
            throw new EnsException(EnsErrCd.INVALID_RES_DATA, "요청한 면세금액과 결제승인된 금액이 상이 합니다.");
    }

    protected void callPayCancelService(PayAbleData<BillNvEasyPay> data, PayApiRespDTO<NvPayApplyRespData> respDTO, EnsException e) {
        
        data.getBillDetail().setLastPaymentId(respDTO.getData().getPaymentId());
        data.getBillDetail().setTotalPayAmount(respDTO.getData().getDetail().getTotalPayAmount());
        data.getBillDetail().setTaxScopeAmount(respDTO.getData().getDetail().getTaxScopeAmount());
        data.getBillDetail().setTaxExScopeAmount(respDTO.getData().getDetail().getTaxExScopeAmount());
        data.getBillDetail().setEnvironmentDepositAmount(respDTO.getData().getDetail().getEnvironmentDepositAmount());
        billNvEasyPayRepository.save(data.getBillDetail());

       
        Map<String, Object> param = new HashMap<>();
        param.put("billUid", data.getBillDetail().getBillUid());
        param.put("cancelReason", e.getMessage());
        param.put("cancelRequester", "2");
        
        Pay<PayApiRespDTO<Map<String, Object>>> pay = PayHelper.<PayCancelData<BillNvEasyPay>, Map<String, Object>, PayApiRespDTO<Map<String, Object>>>builder()
                .param(param)
                .payHelperService(nvPayCancelService)
                .build();
        
        pay.exec();
    }


    protected PayApiRespDTO<Map<String, Object>> callPayResultService(PayApiRespDTO<NvPayApplyRespData> respDTO) {
        Map<String, Object> param = new HashMap<>();
        param.put("code", respDTO.getResCode());
        param.put("message", respDTO.getMessage());
        param.put("body", gson.fromJson(gson.toJson(respDTO.getData()), Map.class));

        Pay<PayApiRespDTO<Map<String, Object>>> pay = PayHelper.<PayAbleData<BillNvEasyPay>, Map<String, Object>, PayApiRespDTO<Map<String, Object>>>builder()
                .param(param)
                .payHelperService(nvPayResultService)
                .build();
        pay.exec();

        return pay.getResult();
    }


    @Override
    public PayApiRespDTO<Map<String, Object>> generateResult(Map<String, Object> reqParam, PayAbleData<BillNvEasyPay> data, EnsException e) {
        if (e == null) {    
            final String respRawData = (String) data.getReqAndRespInf().getEtc().get(DataKeys.payApplyRespMessage.name());
            Map<String, Object> result = gson.fromJson(respRawData, Map.class);
            return PayApiRespDTO.<Map<String, Object>>okBuilder()
                    .data(result)
                    .build();
        } else {   
            if (EnsErrCd.RESPONSED_FAILURE_CODE.equals(e.getErrCd())) {
                PayApiRespDTO respDTO = (PayApiRespDTO) e.getData();
                return respDTO;
            } else {
                return PayApiRespDTO.<Map<String, Object>>errDataBuilder()
                        .resCode(e.getErrCd().getCode())
                        .message(e.getMessage())
                        .data(null)
                        .build();
            }
        }
    }

    @Override
    public void saveHist(Map<String, Object> reqParam, PayAbleData<BillNvEasyPay> data, EnsException e) {
        saveHistByUseSysPayAble(reqParam, data, e); 
        saveHistByPayApply(data, e);                


        if (e != null) {    
            if (data.getBillDetail() != null) {
                data.getBillDetail().setError(FieldError.initBuilder()
                        .errorCode(e.getErrCd().getCode())
                        .errorMessage(e.getMessage())
                        .build());
                billNvEasyPayRepository.save(data.getBillDetail());
            }
        } else {
            data.getBillDetail().setError(FieldError.initBuilder().build());
            billNvEasyPayRepository.save(data.getBillDetail());
        }
    }


    protected void saveHistByUseSysPayAble(Map<String, Object> reqParam, PayAbleData<BillNvEasyPay> data, EnsException e) {
        ReqAndRespInf reqAndRespInf = data.getReqAndRespInf();

        String requestData = reqAndRespInf.getReqData();
        if (e != null) {
            switch (e.getErrCd()) {
                case NO_DATA_FOUND:
                case NO_REQUIRED_PARAM:
                case INVALID_REQUEST:
                    requestData = gson.toJson(reqParam);
                    break;
            }
        }
        BillHist billHist = BillHist.builder()
                .billUid(CmmnUtil.isEmpty(data.getBill()) ? null : data.getBill().getBillUid())
                .billSe(BillSeCd.bpNv)  
                .reqSe(BillReqSeCd.US_PREPAY)  
                .orgMng(CmmnUtil.isEmpty(data.getBill()) ? null : data.getBill().getOrgMng())
                .linkedUuid(CmmnUtil.isEmpty(data.getBillDetail()) ? null : data.getBillDetail().getBillerUserKey())
                .requestData(requestData)
                .responseData(reqAndRespInf.getRespRawData())
                .error(e == null
                        ? FieldError.initBuilder().build()
                        : FieldError.initBuilder()
                        .errorCode(e.getErrCd().getCode())
                        .errorMessage(e.getMessage())
                        .build())
                .build();

        billHistRepository.save(billHist);
    }

    protected void saveHistByPayApply(PayAbleData<BillNvEasyPay> data, EnsException e) {
        ReqAndRespInf reqAndRespInf = data.getReqAndRespInf();
        final String requestData = (String) reqAndRespInf.getEtc().get(DataKeys.payApplyReqMessage.name());
        final String responseData = (String) reqAndRespInf.getEtc().get(DataKeys.payApplyRespMessage.name());

        if (!CmmnUtil.isEmpty(requestData)) {
            BillHist billHist = BillHist.builder()
                    .billUid(CmmnUtil.isEmpty(data.getBill()) ? null : data.getBill().getBillUid())
                    .billSe(BillSeCd.bpNv)  
                    .reqSe(BillReqSeCd.VD_PAYAPPLY)  
                    .orgMng(CmmnUtil.isEmpty(data.getBill()) ? null : data.getBill().getOrgMng())
                    .linkedUuid(CmmnUtil.isEmpty(data.getBillDetail()) ? null : data.getBillDetail().getBillerUserKey())
                    .requestData(requestData)
                    .responseData(responseData)
                    .error(e == null
                            ? FieldError.initBuilder().build()
                            : FieldError.initBuilder()
                            .errorCode(e.getErrCd().getCode())
                            .errorMessage(e.getMessage())
                            .build())
                    .build();

            billHistRepository.save(billHist);
        }
    }
}
