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.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.code.NvEpCtgyIdCd;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.nv.service.process.code.NvEpCtgyTypeCd;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.nv.service.process.code.NvEpPayReferrerCd;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.nv.service.process.model.NvPayNoticeRespData;
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.PayNoticeService;
import cokr.xit.ens.modules.common.ctgy.intgrnbill.support.service.process.model.PayNoticeData;
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.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 javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@RequiredArgsConstructor
@Service
public class NvPayNoticeService implements PayNoticeService<PayNoticeData<BillNvEasyPay>, String, PayApiRespDTO<NvPayNoticeRespData>> {

    private final JPAQueryFactory query;
    private final PayUseSysApi payUseSysApi;
    private final BillNvEasyPayRepository billNvEasyPayRepository;
    private final BillHistRepository billHistRepository;

    private Gson gson = new Gson();

    @Override
    public PayNoticeData<BillNvEasyPay> 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();
        BillNvEasyPay billNvEasyPay = query.selectFrom(QBillNvEasyPay.billNvEasyPay)
                .where(QBillNvEasyPay.billNvEasyPay.billUid.eq(billUid))
                .fetchOne();

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

    @Override
    public void validate(String billUid, PayNoticeData<BillNvEasyPay> 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().getLastPaymentId()))
            throw new EnsException(EnsErrCd.PAYMENT_ALREADY_CMPLTED, "결제가 완료된 자료 입니다.");
    }

    @Override
    public void callApi(String billUid, PayNoticeData<BillNvEasyPay> data) {
        Bill bill = data.getBill();
        OrgMng orgMng = bill.getOrgMng();
        BillNvEasyPay billNvEasyPay = data.getBillDetail();

       
        final String useSysUrl = orgMng.getNvBpNoticeApi();

       
        Optional.of(billNvEasyPay.getBillerUserKey())
                .map(billerUserKey -> createMessage(billerUserKey)) 
                .map(param -> loadReqData(data, useSysUrl, param))  
                .map(param -> payUseSysApi.payNotice(useSysUrl, param, null))   
                .map(response -> loadRespData(data, response))      
                .map(response -> checkResponse(response))          
                .map(respDTO -> validateData(respDTO))              
                .map(respDTO -> savePayNoticeInfo(data, respDTO))   
        ;

    }

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

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

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


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

    protected PayApiRespDTO<NvPayNoticeRespData> validateData(PayApiRespDTO<NvPayNoticeRespData> respDTO) {
        final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        Set<ConstraintViolation<NvPayNoticeRespData>> list = validator.validate(respDTO.getData());
        if (list.size() > 0) {
            throw new EnsException(EnsErrCd.INVALID_RES_DATA, EnsErrCd.INVALID_RES_DATA.getCodeNm(), list.stream()
                    .map(row -> String.format("%s [ %s ]", row.getMessageTemplate(), row.getPropertyPath()))
                    .collect(Collectors.toList()));
        }

        Optional.ofNullable(respDTO.getData())
                .filter(respData -> respData != null)
                .map(respData -> validProductCount(respData))                   
                .map(respData -> validMinimumTotalPayAmount(respData))          
                .map(respData -> validTaxAndTaxExAmount(respData))              
                .map(respData -> validRequirePurchaserNameAndBirthday(respData))
                .map(respData -> validRequireExtraDeduction(respData))          
                .map(respData -> validRequireUseCfmYmdt(respData))              
                .map(respData -> validProductItems(respData))                   
                .map(respData -> validFieldTypeCdInProductItems(respData))      
                .map(respData -> validFieldTypeCdInPayReferrer(respData))       
                .map(respData -> validRequireNameAndCountInProductItems(respData))   
                .map(respData -> validRequireStartAndEndDate(respData))         
        ;


        return respDTO;
    }

    /**
     * <Description>
     * productCount(상품 수량)은 총 수량으로 전달하는지 확인
     * ex. "A 상품 2개 + B 상품 1개 + 사은품 1개"의 경우 productCount(상품 수량) 4로 전달
     * </Description>
     *
     * @param respData
     * @return
     */
    private NvPayNoticeRespData validProductCount(NvPayNoticeRespData respData) {
        final Integer productCount = respData.getProductCount();
        final Integer sumCount = respData.getProductItems().stream()
                .map(productItem -> productItem.getCount())
                .reduce(Integer::sum)
                .orElseGet(() -> 0);
        if (!productCount.equals(sumCount))
            throw new EnsException(EnsErrCd.INVALID_RES_DATA, String.format("상품수량과 결제상품수량 합계가 일치하지 않습니다.(상품수량: %d 실제합계: %d)", productCount, sumCount));

        return respData;
    }

    /**
     * <Description>
     * totalPayAmount 최소 결제금액(100원)에 대한 예외처리 확인
     * (가맹점 주문서 페이지 총 결제금액이 100원 미만일 경우 결제창 호출을 지양하고, 최소 결제금액에 대한 사용자 안내 처리 필요)
     * </Description>
     *
     * @param respData
     * @return
     */
    private NvPayNoticeRespData validMinimumTotalPayAmount(NvPayNoticeRespData respData) {
        if (respData.getTotalPayAmount() < 100)
            throw new EnsException(EnsErrCd.INVALID_RES_DATA, "최소 결제금액은 100원 입니다.");

        return respData;
    }

    /**
     * <Description>
     * taxScopeAmount(과세), taxExScopeAmount(면세) 해당 금액 확인
     * </Description>
     *
     * @param respData
     * @return
     */
    private NvPayNoticeRespData validTaxAndTaxExAmount(NvPayNoticeRespData respData) {
        final Integer totalPayAmount = respData.getTotalPayAmount();
        final Integer sumAmount = respData.getTaxScopeAmount() + respData.getTaxExScopeAmount() + (respData.getEnvironmentDepositAmount() == null ? 0 : respData.getEnvironmentDepositAmount());
        if (!totalPayAmount.equals(sumAmount))
            throw new EnsException(EnsErrCd.INVALID_RES_DATA, "결제금액과 세부(과세/면세/컵보증금)금액 합계 불일치.");

        return respData;
    }

    /**
     * <Description>
     * purchaserName, purchaserBirthday (구매자 성명, 생년월일) 결제 상품이 보험 및 게임 업종 등인 경우 필수 전달하는지 확인
     * </Description>
     *
     * @param respData
     * @return
     */
    private NvPayNoticeRespData validRequirePurchaserNameAndBirthday(NvPayNoticeRespData respData) {
        boolean isTarget = respData.getProductItems().stream().anyMatch(productItem -> NvEpCtgyTypeCd.INSURANCE.equals(productItem.getCategoryType()));
        if (isTarget)
            if (CmmnUtil.isEmpty(respData.getPurchaserName()) || CmmnUtil.isEmpty(respData.getPurchaserBirthday()))
                throw new EnsException(EnsErrCd.INVALID_RES_DATA, "구매자 성명/생년월일 정보가 없습니다. (보험 및 위험 업종인 경우 필수)");

        return respData;
    }

    /**
     * <Description>
     * 도서/공연비 소득공제 대상 가맹점인 경우 extraDeduction 전달하는지 확인
     * (true : 대상, false : 비 대상)
     * </Description>
     *
     * @param respData
     * @return
     */
    private NvPayNoticeRespData validRequireExtraDeduction(NvPayNoticeRespData respData) {
        /**
         * TODO 향후 소득공제 대상 가맹점 여부 관리 필요 시 네이버페이와 협의된 계약사항에 해당 하므로 아래의 해결안 참고
         * 해결안 1. ENS 에서 가맹/비가맹점 관리
         *  -. 기관관리 테이블(ENS_ORG_MNG)에 컬럼 추가
         * 해결안 2. 이용시스템으로부터 데이터 응답
         *  -. 응답데이터 클래스(NvPayNoticeRespData) 에 필드 추가
         */
        return respData;
    }

    /**
     * <Description>
     * 가맹점 타입이 이용완료일 정산 또는 이용완료일 포인트 적립인 경우 useCfmYmdt 전달하는지 확인
     * </Description>
     *
     * @param respData
     * @return
     */
    private NvPayNoticeRespData validRequireUseCfmYmdt(NvPayNoticeRespData respData) {
        /**
         * TODO 향후 정산/포인트적립 기준 관리 필요 시 네이버페이와 협의된 계약사항에 해당 하므로 아래의 해결안 참고
         * 해결안 1. ENS 에서 정산/포인트적립 기준 관리
         *  -. 기관관리 테이블(ENS_ORG_MNG)에 컬럼 추가
         * 해결안 2. 이용시스템으로부터 데이터 응답
         *  -. 응답데이터 클래스(NvPayNoticeRespData) 에 필드 추가
         *
         * ※ 정산/포인트적립 기준 항목 참고
         *    -. 정산 기준: 결제일, 이용완료일, 거래완료일
         *    -. 포인트적립 기준: 결제일 기준 자동적립, 이용완료일 기준 적립, 포인트 적립 요청 API 적립, 거래 완료 API 적립, 포인트 적립 대상 아님
         */
        return respData;
    }

    /**
     * <Description>
     * productItems 상품정보 개발 유무 확인
     * (categoryType, categoryId, uid, payReferrer 값 등)
     * </Description>
     *
     * @param respData
     * @return
     */
    private NvPayNoticeRespData validProductItems(NvPayNoticeRespData respData) {
        /**
         * TODO 선행처리하는 Spring Validation 으로 대체 함. 향후 필요 시 로직 추가
         */
        return respData;
    }

    /**
     * <Description>
     * categoryType, categoryId, uid 값이 상품정보 연동가이드 (Naverpay_Payments_Product_Items_Integration_Guide) 식별값으로 전달하는지 확인
     * </Description>
     *
     * @param respData
     * @return
     */
    private NvPayNoticeRespData validFieldTypeCdInProductItems(NvPayNoticeRespData respData) {
        respData.getProductItems()
                .forEach(productItem -> {
                    final String ctgyId = productItem.getCategoryType().getCode() + "_" + productItem.getCategoryId();
                    if (Arrays.stream(NvEpCtgyIdCd.values()).noneMatch(cd -> cd.getCode().equals(ctgyId)))
                        throw new EnsException(EnsErrCd.INVALID_RES_DATA, "유효하지 않은 결제상품 분류 입니다.");
                    if (!productItem.getCategoryType().equals(NvEpCtgyIdCd.valueOf(ctgyId).getCategoryTypeCd()))
                        throw new EnsException(EnsErrCd.INVALID_RES_DATA, "결제상품 유형의 분류가 유효하지 않습니다.");
                    if (NvEpCtgyTypeCd.INSURANCE.equals(productItem.getCategoryType())) {
                        if (Arrays.asList("001", "002", "003", "004", "005", "006", "007", "008", "009", "010", "011", "012", "013", "014", "015", "016")
                                .stream()
                                .noneMatch(uid -> uid.equals(productItem.getUid())))
                            throw new EnsException(EnsErrCd.INVALID_RES_DATA, "보험상품의 식별값이 유효하지 않습니다.");
                    }
                });

        return respData;
    }

    /**
     * <Description>
     * payReferrer 값이 상품정보 연동가이드(Naverpay_Payments_Product_Items_Integration_Guide) 식별값으로 전달하는지 확인
     * </Description>
     *
     * @param respData
     * @return
     */
    private NvPayNoticeRespData validFieldTypeCdInPayReferrer(NvPayNoticeRespData respData) {
        respData.getProductItems().stream()
                .filter(productItem -> !CmmnUtil.isEmpty(productItem.getPayReferrer()))
                .forEach(productItem -> {
                    try {
                        NvEpPayReferrerCd.valueOf(productItem.getPayReferrer());
                    } catch (Exception e) {
                        throw new EnsException(EnsErrCd.INVALID_RES_DATA, "유효하지 않은 유입경로 입니다.");
                    }
                });
        return respData;
    }

    /**
     * <Description>
     * productItems 상품정보 내 name(상품명), count(결제 상품 개수) 확인
     * </Description>
     *
     * @param respData
     * @return
     */
    private NvPayNoticeRespData validRequireNameAndCountInProductItems(NvPayNoticeRespData respData) {
        /**
         * TODO 선행처리하는 Spring Validation 으로 대체 함. 향후 필요 시 로직 추가
         */
        return respData;
    }

    /**
     * <Description>
     * startDate(시작일), endDate(종료일) 필드는 결제상품이 공연, 영화, 보험, 여행, 항공, 숙박 등인 경우 입력을 권장
     * </Description>
     *
     * @param respData
     * @return
     */
    private NvPayNoticeRespData validRequireStartAndEndDate(NvPayNoticeRespData respData) {
        respData.getProductItems().stream()
                .filter(productItem ->
                        NvEpCtgyTypeCd.PLAY.equals(productItem.getCategoryType())
                                || NvEpCtgyTypeCd.MOVIE.equals(productItem.getCategoryType())
                                || NvEpCtgyTypeCd.INSURANCE.equals(productItem.getCategoryType())
                                || NvEpCtgyTypeCd.TRAVEL.equals(productItem.getCategoryType())
                                || NvEpCtgyTypeCd.FLIGHT.equals(productItem.getCategoryType())
                )
                .filter(productItem -> CmmnUtil.isEmpty(productItem.getStartDate()) || CmmnUtil.isEmpty(productItem.getEndDate()))
                .forEach(productItem -> {
                    throw new EnsException(EnsErrCd.INVALID_RES_DATA, "시작일(또는 종료일)이 없습니다. (공연/영화/보험/여행/항공 상품인 경우 필수)");
                });


        return respData;
    }

    protected PayApiRespDTO<NvPayNoticeRespData> savePayNoticeInfo(PayNoticeData<BillNvEasyPay> data, PayApiRespDTO<NvPayNoticeRespData> respDTO) {
        NvPayNoticeRespData noticeData = respDTO.getData();

        data.getBillDetail().setProductName(noticeData.getProductName());
        data.getBillDetail().setTotalPayAmount(noticeData.getTotalPayAmount());
        data.getBillDetail().setTaxScopeAmount(noticeData.getTaxScopeAmount());
        data.getBillDetail().setTaxExScopeAmount(noticeData.getTaxExScopeAmount());
        data.getBillDetail().setEnvironmentDepositAmount(noticeData.getEnvironmentDepositAmount());
        data.getBillDetail().setReturnUrl(noticeData.getReturnUrl());
        data.getBillDetail().setError(FieldError.initBuilder().build());
        billNvEasyPayRepository.save(data.getBillDetail());

        return respDTO;
    }

    @Override
    public PayApiRespDTO<NvPayNoticeRespData> generateResult(String billUid, PayNoticeData<BillNvEasyPay> data, EnsException e) {
        if (e == null) {    
            Gson gson = new Gson();
            final String respRawData = data.getReqAndRespInf().getRespRawData();
            return gson.fromJson(respRawData, new TypeToken<PayApiRespDTO<NvPayNoticeRespData>>() {
            }.getType());
        } else {   
            if (EnsErrCd.RESPONSED_FAILURE_CODE.equals(e.getErrCd())) {
                PayApiRespDTO respDTO = (PayApiRespDTO) e.getData();
                return PayApiRespDTO.<NvPayNoticeRespData>errDataBuilder()
                        .resCode(respDTO.getResCode())
                        .message(respDTO.getMessage())
                        .data(null)
                        .build();
            } else {
                return PayApiRespDTO.<NvPayNoticeRespData>errDataBuilder()
                        .resCode(e.getErrCd().getCode())
                        .message(e.getMessage())
                        .data(null)
                        .build();
            }
        }
    }

    @Override
    public void saveHist(String billUid, PayNoticeData<BillNvEasyPay> data, EnsException e) {
        saveHistByUseSysNotice(billUid, 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());
            }
        }
    }

    private void saveHistByUseSysNotice(String billUid, PayNoticeData<BillNvEasyPay> data, EnsException e) {
        ReqAndRespInf reqAndRespInf = data.getReqAndRespInf();

        String errDetail = "";
        if (e != null)
            if (EnsErrCd.INVALID_RES_DATA.equals(e.getErrCd())) {
                if (e.getData() != null && e.getData() instanceof List)
                    errDetail = gson.toJson(e.getData());
            }

        BillHist billHist = BillHist.builder()
                .billUid(billUid)
                .billSe(BillSeCd.bpNv)  
                .reqSe(BillReqSeCd.US_NOTICE)  
                .orgMng(CmmnUtil.isEmpty(data.getBill()) ? null : data.getBill().getOrgMng())
                .linkedUuid(CmmnUtil.isEmpty(data.getBillDetail()) ? null : data.getBillDetail().getBillerUserKey())
                .requestData(reqAndRespInf.getReqData())
                .responseData(reqAndRespInf.getRespRawData())
                .error(e == null
                        ? FieldError.initBuilder().build()
                        : FieldError.initBuilder()
                        .errorCode(e.getErrCd().getCode())
                        .errorMessage(e.getMessage() + errDetail)
                        .build())
                .build();

        billHistRepository.save(billHist);

    }
}
