package cokr.xit.ens.modules.common.ctgy.intgrnbill.kko.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.modules.common.ctgy.intgrnbill.kko.api.BillKkoPayApiSpec;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.kko.entity.BillKkoPay;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.kko.entity.QBillKkoPay;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.kko.entity.repository.BillKkoPayRepository;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.kko.service.process.model.KkoPayUrlRespData;
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.kit.PayUrlService;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.service.process.model.PayUrlData;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.service.process.model.ReqAndRespInf;
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 java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Slf4j
@RequiredArgsConstructor
@Service
public class KkoPayUrlService implements PayUrlService<PayUrlData<BillKkoPay>, String, PayApiRespDTO<Map<String, Object>>> {

    private final JPAQueryFactory query;
    private final BillKkoPayApiSpec billKkoPayApi;
    private final BillKkoPayRepository billKkoPayRepository;
    private final BillHistRepository billHistRepository;
    private final PayUseSysApi payUseSysApi;

    private Gson gson = new GsonBuilder().disableHtmlEscaping().create();

    private enum DataKeys {
        gnrUrlReqMessage,
        gnrUrlRespMessage,
        gnrReUrlReqMessage,
        gnrReUrlRespMessage,
    }

    @Override
    public PayUrlData<BillKkoPay> getData(String billUid) {
        if (billUid == null) billUid = "";
        Bill bill = query.selectFrom(QBill.bill)
                .innerJoin(QBill.bill.orgMng, QOrgMng.orgMng).fetchJoin()
                .where(QBill.bill.billUid.eq(billUid))
                .fetchOne();
        BillKkoPay billKkoPay = query.selectFrom(QBillKkoPay.billKkoPay)
                .where(QBillKkoPay.billKkoPay.billUid.eq(billUid))
                .fetchOne();

        return PayUrlData.<BillKkoPay>builder()
                .bill(bill)
                .billDetail(billKkoPay)
                .build();
    }

    @Override
    public void validate(String billUid, PayUrlData<BillKkoPay> data) {
       
        if (CmmnUtil.isEmpty(data.getBill()))
            throw new EnsException(EnsErrCd.NO_DATA_FOUND, "일치하는 청구서 자료가 없습니다.");
        if (CmmnUtil.isEmpty(data.getBillDetail()))
            throw new EnsException(EnsErrCd.NO_DATA_FOUND, "일치하는 청구서 상세 자료가 없습니다.");
        if (!CmmnUtil.isEmpty(data.getBillDetail().getLastPayId()))
            throw new EnsException(EnsErrCd.PAYMENT_ALREADY_CMPLTED, "결제가 완료된 자료 입니다.");
        if (!CmmnUtil.isEmpty(data.getBillDetail().getUrl()))
            throw new EnsException(EnsErrCd.URL_ALREADY_CREATED, "결제URL이 생성된 자료 입니다.");
    }

    @Override
    public void callApi(String billUid, PayUrlData<BillKkoPay> data) {
        Bill bill = data.getBill();
        OrgMng orgMng = bill.getOrgMng();

       
        final String useSysUrl = orgMng.getKkoBpUrlApi();
        final String billerCode = orgMng.getKkoBpBillerCode();
        final String authorization = orgMng.getKkoBpAuthorization();


       
        Optional.ofNullable(data.getBillDetail())
                .map(billKkoPay -> createMessage(billKkoPay.getBillerUserKey()))            
                .map(param -> loadReqData(data, useSysUrl, param))                   
                .map(param -> payUseSysApi.payUrl(useSysUrl, param, null))   
                .map(response -> loadRespData(data, response))                      
                .map(response -> checkResponse(response))                          
                .map(document -> savePayUrlInfo(data, document))                    
                .map(document -> createGnrUrlMessage(data, document))               
                .map(param -> loadGnrUrlReqData(data, param))                       
                .map(param -> billKkoPayApi.url(billerCode, authorization, param))  
                .map(response -> loadGnrUrlRespData(data, response))                
                .map(response -> checkGnrUrlResponse(false, response))       
                .map(respData -> saveUrl(data, respData))                           
                .filter(respData -> respData == null)                               
                .map(respData -> loadGnrReUrlReqData(data))                         
                .map(param -> billKkoPayApi.reUrl(billerCode, authorization, param))
                .map(response -> checkGnrUrlResponse(true, response))        
                .map(respData -> saveUrl(data, respData))                           
        ;
    }

    private Map<String, Object> createMessage(String billerUserKey) {
        Map<String, Object> data = new HashMap<>();
        data.put("billerUserKey", billerUserKey);
        Map<String, Object> param = new HashMap<>();
        param.put("data", data);
        return param;
    }

    private Map<String, Object> loadReqData(PayUrlData<BillKkoPay> data, String url, Map<String, Object> param) {
        
        data.getReqAndRespInf().setUrl(url);
        data.getReqAndRespInf().setReqData(CmmnUtil.toJsonString(param));
        return param;
    }

    private ResponseEntity<String> loadRespData(PayUrlData<BillKkoPay> data, ResponseEntity<String> response) {
        
        data.getReqAndRespInf().setRespRawData(response.getBody());
        return response;
    }


    private PayApiRespDTO<KkoPayUrlRespData> 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<KkoPayUrlRespData> result = null;
        try {
            result = gson.fromJson(response.getBody(), new TypeToken<PayApiRespDTO<KkoPayUrlRespData>>() {
            }.getType());
        } 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;
    }

    private PayApiRespDTO<KkoPayUrlRespData> savePayUrlInfo(PayUrlData<BillKkoPay> data, PayApiRespDTO<KkoPayUrlRespData> document) {
        final KkoPayUrlRespData urlData = document.getData();

        data.getBillDetail().setBilledYearMonth(urlData.getBilledYearMonth());
        data.getBillDetail().setOrdinal(urlData.getOrdinal());
        data.getBillDetail().setBillerNoticeKey(data.getBillDetail().getBillUid());
        data.getBillDetail().setExpireAt(urlData.getExpireAt());
        data.getBillDetail().setParameters(CmmnUtil.isEmpty(urlData.getParameters()) ? null : gson.toJson(urlData.getParameters()));
        data.getBillDetail().setCustomUrl(CmmnUtil.isEmpty(urlData.getCustomUrl()) ? null : gson.toJson(urlData.getCustomUrl()));

        billKkoPayRepository.save(data.getBillDetail());

        return document;
    }

    private String createGnrUrlMessage(PayUrlData<BillKkoPay> data, PayApiRespDTO<KkoPayUrlRespData> document) {
        final OrgMng orgMng = data.getBill().getOrgMng();
        final BillKkoPay billKkoPay = data.getBillDetail();
        final KkoPayUrlRespData urlData = document.getData();

        Map<String, Object> parameters = null;  
        parameters = CmmnUtil.isEmpty(urlData.getParameters()) ? new HashMap<>() : urlData.getParameters();
        parameters.put("orgCd", orgMng == null ? null : orgMng.getOrgCd());

        Map<String, Object> param = new HashMap<String, Object>();
        param.put("biller_user_key", billKkoPay.getBillerUserKey());    //[필수] 기관에서 관리하는 해당 고객번호 혹은 계약번호
        param.put("billed_year_month", urlData.getBilledYearMonth());   //[선택] 청구 연월(YYYYMM) 형식, 없을 시 생략
        param.put("ordinal", urlData.getOrdinal());                     //[선택] 동일 고객번호, 청구 연월의 복수개 청구서 발생 등 몇번째 청구인지 구분
        // 중요 : biller_notice_key 사용 할 경우 청구서 조회, 납부 가능 조회 응답에도 필수로 포함, 아니면 오류로 결제 진행이 안됨
        /*param.put("biller_notice_key", billKkoPay.getBillerNoticeKey());*///[선택] 개별 청구서를 식별하는 키 값. 처리 결과 조회에 사용되므로, 유니크한 값을 가지도록 생성
        param.put("expire_at", urlData.getExpireAt());                  //[필수] URL 만료일, YYYYMMDDHH24MISS
        param.put("parameters", parameters);                          //[선택] key/value 형태의 청구서 조회 등 API 호출 시 함께 전달할 값
//        param.put("parameters", urlData.getParameters());               //[선택] key/value 형태의 청구서 조회 등 API 호출 시 함께 전달할 값
        param.put("custom_url", urlData.getCustomUrl());                             //[선택] 사용자가 청구서 결제 시 호출(KKO -> 이용시스템)할 청구서조회/납부가능조회/납부결과전달 API URL


        Map<String, Object> m = new HashMap<String, Object>();
        m.put("data", param);

        return gson.toJson(m);
    }

    private String loadGnrUrlReqData(PayUrlData<BillKkoPay> data, String message) {
        
        data.getReqAndRespInf().getEtc().put(DataKeys.gnrUrlReqMessage.name(), message);

        return message;
    }

    private ResponseEntity<String> loadGnrUrlRespData(PayUrlData<BillKkoPay> data, ResponseEntity<String> response) {
        
        data.getReqAndRespInf().getEtc().put(DataKeys.gnrUrlRespMessage.name(), response.getBody());
        return response;
    }

    private PayApiRespDTO<Map<String, Object>> checkGnrUrlResponse(boolean isReurl, 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());
        
        if (!isReurl && response.getBody().contains("Read timed out"))  
            return null;
        PayApiRespDTO<Map<String, Object>> result = null;
        try {
            result = gson.fromJson(response.getBody(), new TypeToken<PayApiRespDTO<Map<String, Object>>>() {
            }.getType());
        } catch (Exception ex) {
            if (isReurl)
                throw new EnsException(EnsErrCd.INVALID_RESPONSE, "["+ BillLogSeCd.VD +"] " + EnsErrCd.INVALID_RESPONSE.getCodeNm(), ex);
            else
                return null;    
        }
        
        if (!"OK".equals(result.getResCode()))
            if (!isReurl && "E818".equals(result.getResCode())) 
                return null;
            else {
                result.setMessage("["+ BillLogSeCd.VD +"] " + result.getMessage());
                throw new EnsException(EnsErrCd.RESPONSED_FAILURE_CODE, EnsErrCd.RESPONSED_FAILURE_CODE.getCodeNm(), result);
            }

        return result;
    }

    private PayApiRespDTO<Map<String, Object>> saveUrl(PayUrlData<BillKkoPay> data, PayApiRespDTO<Map<String, Object>> respData) {
        if (!CmmnUtil.isEmpty(respData)) {
            Map<String, Object> body = respData.getData();
            data.getBillDetail().setUrl((String) body.get("url"));
            data.getBillDetail().setError(FieldError.initBuilder().build());
            billKkoPayRepository.save(data.getBillDetail());
        }

        return respData;
    }

    private String loadGnrReUrlReqData(PayUrlData<BillKkoPay> data) {
        
        final String message = (String) data.getReqAndRespInf().getEtc().get(DataKeys.gnrReUrlReqMessage.name());
        data.getReqAndRespInf().getEtc().put(DataKeys.gnrReUrlReqMessage.name(), message);

        return message;
    }

    @Override
    public PayApiRespDTO<Map<String, Object>> generateResult(String billUid, PayUrlData<BillKkoPay> data, EnsException e) {
        if (e == null) {    
            final String urlResponse = (String) data.getReqAndRespInf().getEtc().get(DataKeys.gnrUrlRespMessage.name());
            final String reurlResponse = (String) data.getReqAndRespInf().getEtc().get(DataKeys.gnrReUrlRespMessage.name());
            if (CmmnUtil.isEmpty(reurlResponse))
                return gson.fromJson(urlResponse, new TypeToken<PayApiRespDTO<Map<String, Object>>>() {
                }.getType());
            else
                return gson.fromJson(reurlResponse, new TypeToken<PayApiRespDTO<Map<String, Object>>>() {
                }.getType());
        } else {   
            if (EnsErrCd.URL_ALREADY_CREATED.equals(e.getErrCd())) {
                Map<String, Object> m = new HashMap<>();
                m.put("url", data.getBillDetail().getUrl());
                return PayApiRespDTO.<Map<String, Object>>okBuilder()
                        .data(m)
                        .build();
            } else if (EnsErrCd.RESPONSED_FAILURE_CODE.equals(e.getErrCd())) {
                PayApiRespDTO respDTO = (PayApiRespDTO) e.getData();
                return PayApiRespDTO.<Map<String, Object>>errDataBuilder()
                        .resCode(respDTO.getResCode())
                        .message(respDTO.getMessage())
                        .data(null)
                        .build();
            } else {
                return PayApiRespDTO.<Map<String, Object>>errDataBuilder()
                        .resCode(e.getErrCd().getCode())
                        .message(e.getMessage())
                        .data(null)
                        .build();
            }
        }
    }

    @Override
//    @Transactional(propagation = Propagation.REQUIRED)
    public void saveHist(String billUid, PayUrlData<BillKkoPay> data, EnsException e) {
        saveHistByGetUrlInf(data, e);   
        saveHistByGnrUrl(data, e);      
        saveHistByGnrReUrl(data, e);    

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

    private void saveHistByGetUrlInf(PayUrlData<BillKkoPay> data, EnsException e) {

        ReqAndRespInf reqAndRespInf = data.getReqAndRespInf();
        final String requestData = reqAndRespInf.getReqData();
        final String responseData = reqAndRespInf.getRespRawData();

        if (!CmmnUtil.isEmpty(requestData)) {
            BillHist billHist = BillHist.builder()
                    .billUid(CmmnUtil.isEmpty(data.getBill()) ? null : data.getBill().getBillUid())
                    .billSe(BillSeCd.bpKko)  
                    .reqSe(BillReqSeCd.US_URLINF)  
                    .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);
        }

    }

    private void saveHistByGnrUrl(PayUrlData<BillKkoPay> data, EnsException e) {

        ReqAndRespInf reqAndRespInf = data.getReqAndRespInf();
        final String requestData = (String) reqAndRespInf.getEtc().get(DataKeys.gnrUrlReqMessage.name());
        final String responseData = (String) reqAndRespInf.getEtc().get(DataKeys.gnrUrlRespMessage.name());

        if (!CmmnUtil.isEmpty(requestData)) {
            BillHist billHist = BillHist.builder()
                    .billUid(CmmnUtil.isEmpty(data.getBill()) ? null : data.getBill().getBillUid())
                    .billSe(BillSeCd.bpKko)  
                    .reqSe(BillReqSeCd.VD_URL)  
                    .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);
        }

    }

    private void saveHistByGnrReUrl(PayUrlData<BillKkoPay> data, EnsException e) {

        ReqAndRespInf reqAndRespInf = data.getReqAndRespInf();
        final String requestData = (String) reqAndRespInf.getEtc().get(DataKeys.gnrReUrlReqMessage.name());
        final String responseData = (String) reqAndRespInf.getEtc().get(DataKeys.gnrReUrlRespMessage.name());

        if (!CmmnUtil.isEmpty(requestData)) {
            BillHist billHist = BillHist.builder()
                    .billUid(CmmnUtil.isEmpty(data.getBill()) ? null : data.getBill().getBillUid())
                    .billSe(BillSeCd.bpKko)  
                    .reqSe(BillReqSeCd.VD_REURL)  
                    .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);

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

    }
}
