package kr.xit.biz.ens.service;

import static kr.xit.core.support.utils.JsonUtils.*;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;

import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import kr.xit.biz.common.ApiConstants;
import kr.xit.biz.common.ApiConstants.SndngSeCode;
import kr.xit.biz.ens.cmm.CmmEnsBizUtils;
import kr.xit.biz.ens.mapper.IEnsBatchMapper;
import kr.xit.biz.ens.model.EnsDTO;
import kr.xit.biz.ens.model.EnsDTO.SendKakaoTgt;
import kr.xit.biz.ens.model.EnsKtBcDTO;
import kr.xit.biz.ens.model.EnsKtGbsDTO;
import kr.xit.biz.ens.model.cmm.SndngMssageParam;
import kr.xit.biz.ens.model.kakao.pay.KkopayDocAttrDTO;
import kr.xit.biz.ens.model.kakao.pay.KkopayDocBulkDTO;
import kr.xit.biz.ens.model.kakao.talk.KkotalkApiDTO;
import kr.xit.biz.ens.model.kakao.talk.KkotalkDTO;
import kr.xit.biz.ens.model.kt.KtCommonDTO.ErrorMsg;
import kr.xit.biz.ens.model.kt.KtCommonDTO.KtCommonResponse;
import kr.xit.biz.ens.model.kt.KtMmsSendDTO.KtMainSendReqData;
import kr.xit.biz.ens.model.ktgbs.KtGbsDTO;
import kr.xit.core.exception.BizRuntimeException;
import kr.xit.core.model.ApiResponseDTO;
import kr.xit.core.service.AbstractService;
import kr.xit.core.spring.util.ApiWebClientUtil;
import kr.xit.core.support.utils.Checks;
import kr.xit.core.support.utils.DateUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * <pre>
 * description : 모바일 전자고지 배치 서비스
 *               - 배치에서 호출되는 클래스로 배치 로그등 Transaction 분리 필요
 *               - 메소드 트랜잭션 Transactional(propagation = Propagation.REQUIRES_NEW)로 선언
 * packageName : kr.xit.biz.ens.service
 * fileName    : EnsBatchService
 * author      : limju
 * date        : 2023-08-31
 * ======================================================================
 * 변경일         변경자        변경 내용
 * ----------------------------------------------------------------------
 * 2023-08-31    limju       최초 생성
 *
 * </pre>
 */
@Slf4j
@RequiredArgsConstructor
@Service
public class EnsBatchSendService extends AbstractService implements IEnsBatchSendService {
    @Value("${app.contract.host}")
    private String apiHost;

    @Value("${app.contract.kakao.api.pay.bulksend}")
    private String apiKkopayBulkSend;

    @Value("${app.contract.kakao.api.talk.bulksend}")
    private String apiKkotalkBulkSend;

    @Value("${app.contract.kt.bc.api.bulksend}")
    private String apiKtBcBulkSend;

    @Value("${app.contract.kt.gibis.api.bulksend}")
    private String apiKtGbsBulkSend;

    @Value("${app.contract.kakao.bulk-max-cnt}")
    private int bulkKkoMaxCnt;

    @Value("${app.contract.kt.bulk-max-cnt}")
    private int bulkKtMaxCnt;

    @Value("${app.batch.period.send:day}")
    private String batchSendPeriod;

    private static final String SNDNG_PROCESS_STTUS = "sndngProcessSttus";
    private static final String UNITY_SNDNG_MST_ID = "unitySndngMastrId";
    private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
    private final EnsBatchExtractService extractService;
    private final ApiWebClientUtil apiWebClient;
    private final IEnsBatchMapper mapper;

    /**
     * 발송대상 조회
     * @param reqDTO BatchEnsRequest
     * @return List<SndngMssageParam>
     */
    @Override
    @Transactional(readOnly = true)
    public List<SndngMssageParam> findBulks(final EnsDTO.BatchEnsRequest reqDTO) {

        // 발송/통합발송 마스터에서 대상 조회
        reqDTO.setBatchPeriod(batchSendPeriod);
        return mapper.selectSendBulkTgts(reqDTO);
    }

    /**
     * <pre>
     * 전자고지(문서) send bulks - 문서 중개자별 send
     * - 문서 중개자별 처리(BIZ DB) -> 트랜잭션이 분리 되어야 한다
     * - 발송마스터의 발송처리 상태 'make-ok' 대상
     * - 처리완료 : 발송상세(문서중개자별 상세 테이블) 전송 결과 반영
     *            연계마스터, 통합발송마스터, 발송마스터의 발송처리 상태 변경 - 'send-ok'
     * - 실패 : 연계마스터, 통합발송마스터, 발송마스터의 발송처리 상태 'send-fail1,2,3'
     *
     * 문서 중개자별 send 처리
     *   - 카카오
     *      - 카카오페이 연계 결과 반영 : tb_ens_kakao_my_doc
     *      - 모바일 페이지 생성 : tb_ens_mobile_page_manage
     *      - 연계발송결과 생성 : tb_cntc_sndng_result
     * @param dto SndngMssageParam
     * </pre>
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendBulks(final SndngMssageParam dto) {
        dto.setProfile(ApiConstants.IS_PROFILE_LOCAL? "local" : "prod");

        MDC.put(UNITY_SNDNG_MST_ID, dto.getUnitySndngMastrId());
        MDC.put("sndngMastrId", dto.getSndngMastrId());
        MDC.put(SNDNG_PROCESS_STTUS, "send-fail" + dto.getTrySeq());

        final SndngSeCode sndngSeCode = CmmEnsBizUtils.getSndngSeCodFromTrySeq(dto);

        // 마스터 상태 변경값을 파라메터에서 받은 상태값으로 set
        dto.setNewSndngProcessSttus(dto.getSndngProcessSttus());

        // 업무 문서 구분에 따른 분기
        switch (sndngSeCode) {
            /*
             * 카카오페이 연계 결과 반영 : tb_ens_kakao_my_doc
             * 모바일 페이지 생성 : tb_ens_mobile_page_manage
             * 연계발송결과 생성 : tb_cntc_sndng_result
             */
            case KAKAO -> sendBulkKakaopay(dto);
            case KAKAO_NEW -> sendBulkKakaotalk(dto);
            case KT_BC -> sendBulkKtBc(dto);
            case KT_GIBIS -> sendBulkKtGibis(dto);
            default -> throw BizRuntimeException.create(String.format("정의 되지 않은 문서 중개자[%s] 입니다", sndngSeCode.getCode()));
        }
    }

    //-----------------------------------------------------------------------------------------------------------------
    // send
    //-----------------------------------------------------------------------------------------------------------------

    /**
     * <pre>
     * 카카오 문서 send
     * 1. 카카오페이 문서요청 대상 조회
     *    - {@link IEnsBatchMapper#selectKakaopaySendTgts(Object) selectKakaopaySendTgts}
     * 2. 요청 대상 파라메터 set, validtion check
     *    - {@link #setKkopaySendBulks}
     *    - {@link #validatedKkopaySendBulks}
     * 3. 카카오페이 문서요청  API 호출
     *    -> 결과 목록 획득
     * 4. 카카오페이 문서요청 결과 반영
     *    - bulk 전송 기준 모든건이 실패인 경우만 실패 처리
     *    -
     *    -> {@link EnsBatchExtractService#saveKkopaySendResult(String, String, List) saveKkoMyDocResult}
     *       - 카카오페이 연계 결과 반영 : tb_ens_kakao_my_doc
     *       - 모바일 페이지 생성 : tb_ens_mobile_page_manage
     *       - 연계발송결과 생성 : tb_cntc_sndng_result
     * @param dto SndngMssageParam
     * </pre>
     */
    private void sendBulkKakaopay(final SndngMssageParam dto) {
        final String url = apiHost + apiKkopayBulkSend;

        final List<SendKakaoTgt> list = mapper.selectKakaopaySendTgts(dto);
        if(list.isEmpty())  return;

        final String mstId =  list.get(0).getSndngMastrId();
        final List<KkopayDocBulkDTO.BulkSendReq> bulkList = setKkopaySendBulks(list);

       // validation check
        try {
            validatedKkopaySendBulks(bulkList);
        } catch (Exception e) {
            extractService.updateSndngMstFailStatus(mstId, SndngSeCode.KAKAO, "", e.getMessage(), "[send]카카오 문서 발송(bulks)요청 실패(파라메터 오류)");
            throw e;
        }

        final List<List<KkopayDocBulkDTO.BulkSendReq>> partitions = ListUtils.partition(bulkList, bulkKkoMaxCnt);
        //noinspection rawtypes
        final List<ApiResponseDTO> apiResults = partitions.stream()
            .map(bulkSendList -> apiWebClient.exchange(
                url,
                HttpMethod.POST,
                KkopayDocBulkDTO.BulkSendRequests.builder()
                    .signguCode(dto.getSignguCode())
                    .ffnlgCode(dto.getFfnlgCode())
                    .documents(bulkSendList)
                    .build(),
                ApiResponseDTO.class,
                CmmEnsBizUtils.getHeadeMap())
            )
            .toList();

        final List<KkopayDocBulkDTO.BulkSendResponses> resList = new ArrayList<>();
        boolean isSuccess = false;
        String errMsg = null;
        //noinspection rawtypes
        for(ApiResponseDTO apiResult : apiResults) {
            if(apiResult.getData() != null) {
                resList.add(toObjByObj(apiResult.getData(), KkopayDocBulkDTO.BulkSendResponses.class));
                isSuccess = true;
                continue;
            }
            errMsg = apiResult.getMessage();
        }
        // 카카오 send 결과 반영
        if(!isSuccess){
            extractService.updateSndngMstFailStatus(mstId, SndngSeCode.KAKAO, "", errMsg, errMsg);
        }
        extractService.saveKkopaySendResult(mstId, dto.getUnitySndngMastrId(), resList);
    }

    /**
     * <pre>
     * 카카오톡 전자고지 send
     * 1. 카카오톡 전자고지 요청 대상 조회
     *    - {@link IEnsBatchMapper#selectKakaotalkSendTgts(Object) selectKakaotalkSendTgts}
     * 2. 요청 대상 파라메터 set, validtion check
     *    - {@link #setKkotalkSendBulks}
     *    - {@link #validatedKkotalkSendBulks}
     * 3. 카카오페이 문서요청  API 호출
     *    -> 결과 목록 획득
     * 4. 카카오페이 문서요청 결과 반영
     *    - bulk 전송 기준 모든건이 실패인 경우만 실패 처리
     *    -
     *    -> {@link EnsBatchExtractService#saveKkotalkSendResult(String, String, List) saveKkoNewSendResult}
     *       - 카카오페이 연계 결과 반영 : tb_ens_kakao_my_doc
     *       - 모바일 페이지 생성 : tb_ens_mobile_page_manage
     *       - 연계발송결과 생성 : tb_cntc_sndng_result
     * @param dto SndngMssageParam
     * </pre>
     */
    private void sendBulkKakaotalk(final SndngMssageParam dto) {
        final String url = apiHost + apiKkotalkBulkSend;

        final List<EnsDTO.SendKakaotalkTgt> list = mapper.selectKakaotalkSendTgts(dto);
        if(list.isEmpty())  return;

        final String mstId =  list.get(0).getSndngMastrId();
        final List<KkotalkApiDTO.Envelope> bulkList = setKkotalkSendBulks(list);

        // validation check
        try {
            validatedKkotalkSendBulks(bulkList);
        } catch (Exception e) {
            extractService.updateSndngMstFailStatus(mstId, SndngSeCode.KAKAO_NEW, "", e.getMessage(), "[send]카카오톡 전자고지 발송(bulks)요청 실패(파라메터 오류)");
            throw e;
        }

        final List<List<KkotalkApiDTO.Envelope>> partitions = ListUtils.partition(bulkList, bulkKkoMaxCnt);
        //noinspection rawtypes
        final List<ApiResponseDTO> apiResults = partitions.stream()
            .map(bulkSendList -> apiWebClient.exchange(
                url,
                HttpMethod.POST,
                KkotalkDTO.BulkSendRequest.builder()
                    .signguCode(dto.getSignguCode())
                    .ffnlgCode(dto.getFfnlgCode())
                    .envelopes(bulkSendList)
                    .build(),
                ApiResponseDTO.class,
                CmmEnsBizUtils.getHeadeMap())
            )
            .toList();

        final List<KkotalkDTO.BulkSendResponse> resList = new ArrayList<>();
        boolean isSuccess = false;
        String errMsg = null;
        //noinspection rawtypes
        for(ApiResponseDTO apiResult : apiResults) {
            if(apiResult.getData() != null) {
                resList.add(toObjByObj(apiResult.getData(), KkotalkDTO.BulkSendResponse.class));
                isSuccess = true;
                continue;
            }
            errMsg = apiResult.getMessage();
        }
        // 카카오 send 결과 반영
        if(!isSuccess){
            extractService.updateSndngMstFailStatus(mstId, SndngSeCode.KAKAO_NEW, "", errMsg, errMsg);
        }
        extractService.saveKkotalkSendResult(mstId, dto.getUnitySndngMastrId(), resList);
    }

    /**
     * GET 카카오 문서발송요청 파라메터 목록
     * - CI 인증인 경우 개인 정보 삭제
     * @param list      List<EnsDTO.SendKakaoTgt> 문서발송요청 대상 목록
     * @return List<KkopayDocBulkDTO.BulkSendReq>
     */
    private static List<KkopayDocBulkDTO.BulkSendReq> setKkopaySendBulks(List<SendKakaoTgt> list) {
        final List<KkopayDocBulkDTO.BulkSendReq> bulkList = new ArrayList<>();

        for (SendKakaoTgt sendTgtDTO : list) {
            /*
              방어 코드 추가 : CI 인증인 경우 개인정보 삭제
             */
            KkopayDocAttrDTO.Receiver receiver = null;
            if(StringUtils.isNotEmpty(sendTgtDTO.getCi())){
                receiver = KkopayDocAttrDTO.Receiver.builder()
                    .ci(sendTgtDTO.getCi())
                    .phone_number(StringUtils.EMPTY)
                    .name(StringUtils.EMPTY)
                    .birthday(StringUtils.EMPTY)
                    .is_required_verify_name(false)
                    .build();
            }else{
                receiver = KkopayDocAttrDTO.Receiver.builder()
                    .phone_number(sendTgtDTO.getRecvPhoneNumber())
                    .name(sendTgtDTO.getRecvName())
                    .birthday(sendTgtDTO.getRecvBirthday())
                    .is_required_verify_name(false)
                    .build();
            }

            final KkopayDocBulkDTO.PropertyBulk property = KkopayDocBulkDTO.PropertyBulk.builder()
                .link(sendTgtDTO.getPropLink())
                .payload(sendTgtDTO.getPropPayload())
                .message(sendTgtDTO.getPropMessage())
                .cs_name(sendTgtDTO.getPropCsName())
                .cs_number(sendTgtDTO.getPropCsNumber())
                .external_document_uuid(sendTgtDTO.getUnitySndngDetailId())
                .build();

            final KkopayDocBulkDTO.BulkSendReq bulkReqDTO = KkopayDocBulkDTO.BulkSendReq.builder()
                .title(sendTgtDTO.getTitle())
                .common_categories(Collections.singletonList(ApiConstants.Categories.NOTICE))
                .read_expired_at(sendTgtDTO.getClosDt())
                //
                .hash(sendTgtDTO.getHash())
                //.hash(encryptor.encryptPassword(sendTgtDTO.getUnitySndngDetailId()))
                .receiver(receiver)
                .property(property)
                .build();

            bulkList.add(bulkReqDTO);
        }
        return bulkList;
    }

    /**
     * GET 카카오톡 전자고지 발송요청 파라메터 목록
     * - CI 인증인 경우 개인 정보 삭제
     * @param list      List<EnsDTO.SendKakaoNewTgt> 문서발송요청 대상 목록
     * @return List<KkoNewDocAttrDTO.Send>
     */
    private static List<KkotalkApiDTO.Envelope> setKkotalkSendBulks(List<EnsDTO.SendKakaotalkTgt> list) {
        final List<KkotalkApiDTO.Envelope> bulkList = new ArrayList<>();

        for (EnsDTO.SendKakaotalkTgt sendTgtDTO : list) {
            /*
              방어 코드 추가 : CI 인증인 경우 개인정보 삭제
             */
            KkotalkApiDTO.Envelope bulkReqDTO = null;
            if(StringUtils.isNotEmpty(sendTgtDTO.getCi())){
                bulkReqDTO = KkotalkApiDTO.Envelope.builder()
                    .ci(sendTgtDTO.getCi())
                    .build();
            }else{
                bulkReqDTO = KkotalkApiDTO.Envelope.builder()
                    .build();
            }

            final KkotalkApiDTO.Content content = KkotalkApiDTO.Content.builder()
                .link(sendTgtDTO.getContentLink())
                .build();


            bulkReqDTO.setExternalId(sendTgtDTO.getUnitySndngDetailId());
            bulkReqDTO.setTitle(sendTgtDTO.getTitle());
            bulkReqDTO.setPayload(sendTgtDTO.getPayload());
            bulkReqDTO.setReadExpiresAt(sendTgtDTO.getReadExpiresAt());
            bulkReqDTO.setReviewExpiresAt(sendTgtDTO.getReviewExpiresAt());
            bulkReqDTO.setHash(sendTgtDTO.getHash());
            bulkReqDTO.setGuide(sendTgtDTO.getGuide());
            bulkReqDTO.setContent(content);

            bulkList.add(bulkReqDTO);
        }
        return bulkList;
    }

    /**
     * 카카오문서 발송요청 파라메터 유효성 체크
     *
     * @param bulkList List<KkopayDocBulkDTO.BulkSendReq> 카카오내문서함 발송요청 파라메터 목록
     */
    private void validatedKkopaySendBulks(List<KkopayDocBulkDTO.BulkSendReq> bulkList) {
        List<String> errors = new ArrayList<>();
        int idx = 0;

        for (KkopayDocBulkDTO.BulkSendReq dto : bulkList) {
            final Set<ConstraintViolation<KkopayDocBulkDTO.BulkSendReq>> errList = validator.validate(dto);

            if(!errList.isEmpty()) {
                int finalIdx = idx;
                errors.addAll(errList.stream()
                    .map(row -> String.format("%s[%d]=%s", row.getPropertyPath(), finalIdx + 1,
                        row.getMessageTemplate()))
                    .toList()
                );
            }

            if (dto.getRead_expired_at() != null && dto.getRead_expired_sec() != null) {
                errors.add("처리마감시간(절대시간 또는 상대시간)은 하나만 지정해야 합니다.");
            }
            if (dto.getRead_expired_at() == null && dto.getRead_expired_sec() == null) {
                errors.add("처리마감시간(절대시간 또는 상대시간)을 지정해야 합니다.");
            }

            final KkopayDocAttrDTO.Receiver receiver = dto.getReceiver();
            if (Checks.isEmpty(receiver.getCi())) {
                if (Checks.isEmpty(receiver.getName())) {
                    errors.add(
                        String.format("받는이 이름은 필수입니다(receiver.name[%d] 번째 오류)", idx + 1));
                }
                if (Checks.isEmpty(receiver.getPhone_number())) {
                    errors.add(String.format("받는이 전화번호는 필수입니다(receiver.phone_number[%d] 번째 오류)",
                        idx + 1));
                }
                if (Checks.isEmpty(receiver.getBirthday())) {
                    errors.add(
                        String.format("받는이 생년월일은 필수입니다(receiver.birthday[%d] 번째 오류)", idx + 1));
                }
            }
            idx++;
        }

        if (!errors.isEmpty()) {
            throw BizRuntimeException.create(errors.toString());
        }
    }

    /**
     * 카카오톡 전자고지 발송요청 파라메터 유효성 체크
     *
     * @param bulkList List<KkoNewDocBulkDTO.Send> 카카오톡 전자고지 발송요청 파라메터 목록
     */
    private void validatedKkotalkSendBulks(List<KkotalkApiDTO.Envelope> bulkList) {
        List<String> errors = new ArrayList<>();
        int idx = 0;

        for (KkotalkApiDTO.Envelope dto : bulkList) {
            final Set<ConstraintViolation<KkotalkApiDTO.Envelope>> errList = validator.validate(dto);

            if(!errList.isEmpty()) {
                int finalIdx = idx;
                errors.addAll(errList.stream()
                    .map(row -> String.format("%s[%d]=%s", row.getPropertyPath(), finalIdx + 1,
                        row.getMessageTemplate()))
                    .toList()
                );
            }

            if(dto.getReadExpiresAt() == null){
                Objects.requireNonNull(errors).add("최초 열람 만료 일시는 필수입니다.");
            }
            if(dto.getReadExpiresAt() != null && dto.getReviewExpiresAt() != null){
                DateTimeFormatter pattern1 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
                LocalDateTime parse1 = LocalDateTime.parse(dto.getReadExpiresAt(), pattern1);
                LocalDateTime parse2 = LocalDateTime.parse(dto.getReviewExpiresAt(), pattern1);
                if(parse1.isAfter(parse2)) Objects.requireNonNull(errors).add("재열람 만료 일시는 최초 열람 만료 일시보다 이전일 수 없습니다.");
            }

            if (Checks.isEmpty(dto.getCi())) {
                //                if (Checks.isEmpty(dto.getName())) {
                //                    errors.add(
                //                        String.format("받는이 이름은 필수입니다(receiver.name[%d] 번째 오류)", idx + 1));
                //                }
                //                if (Checks.isEmpty(dto.getPhoneNumber())) {
                //                    errors.add(String.format("받는이 전화번호는 필수입니다(receiver.phone_number[%d] 번째 오류)",
                //                        idx + 1));
                //                }
                //                if (Checks.isEmpty(dto.getBirthday())) {
                //                    errors.add(
                //                        String.format("받는이 생년월일은 필수입니다(receiver.birthday[%d] 번째 오류)", idx + 1));
                //                }
            }
            idx++;
        }

        if (!errors.isEmpty()) {
            throw BizRuntimeException.create(errors.toString());
        }
    }

    /**
     * <pre>
     * KT BC 문서 send
     * @param dto SndngMssageParam
     * </pre>
     */
    private void sendBulkKtBc(final SndngMssageParam dto) {
        final String url = apiHost + apiKtBcBulkSend;

        EnsKtBcDTO mstDTO = mapper.selectKtBcMstTgtInfo(dto)
            .orElseThrow(() -> BizRuntimeException.create("KT BC 본문자 수신 등록 대상이 존재 하지 않습니다 "));

        final String mstId = mstDTO.getSndngMastrId();
        final String unitySndMstId = mstDTO.getUnitySndngMastrId();
        final List<KtMainSendReqData> sendReqs = mapper.selectKtBcSendTgts(dto);

        // validation check
        try {
            validatedKtBcSendBulks(sendReqs);
        } catch (Exception e) {
            extractService.updateSndngMstFailStatus(mstId, SndngSeCode.KT_BC, "", e.getMessage(), "[send]KT-BC 발송(bulks)요청 실패(파라메터 오류)");
            throw e;
        }

        final List<List<KtMainSendReqData>> partitions = ListUtils.partition(sendReqs, bulkKtMaxCnt);
        @SuppressWarnings("rawtypes")
        List<ApiResponseDTO> apiResults = partitions.stream()
            .map(bulkSendList -> {
                mstDTO.setReqs(bulkSendList);
                    return apiWebClient.exchange(
                        url,
                        HttpMethod.POST,
                        mstDTO,
                        ApiResponseDTO.class,
                        CmmEnsBizUtils.getHeadeMap());
                }
            )
            .toList();

        boolean isSuccess = false;
        List<ErrorMsg> errList = new ArrayList<>();
        //noinspection rawtypes
        for(ApiResponseDTO apiResult : apiResults) {
            // KT-BC API 정상 호출
            if(ObjectUtils.isNotEmpty(apiResult.getData())) {
                KtCommonResponse resDTO = toObjByObj(apiResult.getData(), KtCommonResponse.class);
                assert resDTO != null;
                dto.setResultDt(resDTO.getResultDt());

                if(ObjectUtils.isNotEmpty(resDTO) && "00".equals(resDTO.getResultCd())){
                    isSuccess = true;
                    dto.setErrorMssage("정상");
                }else{
                    //TODO:: 모바일 콘텐츠 삭제??
                    errList.addAll(resDTO.getErrors());
                }

            }else{
                // KT-BC API 호출전 에러 발생
                if(ObjectUtils.isEmpty(dto.getResultDt())) {
                    dto.setResultDt(DateUtils.getTodayAndNowTime(ApiConstants.FMT_DT_EMPTY_DLT));
                }
                dto.setErrorMssage(apiResult.getMessage());
                errList.add(new ErrorMsg(apiResult.getMessage()));
            }
        }

        // 모두 실패한 경우
        if(!isSuccess) {
            dto.setResultCd("01");
            dto.setErrorMssage(
                errList.stream()
                    .map(ErrorMsg::getErrorMsg)
                    .collect(Collectors.joining(","))
            );
            extractService.saveKtBcResult(dto, null);
            extractService.updateSndngMstFailStatus(mstId, SndngSeCode.KT_BC, dto.getErrorCode(), dto.getErrorMssage(), "[send] KT-BC 발송(bulks)요청 실패");
            return;
        }
        // 성공 건수 존재시 성공 처리
        dto.setResultCd("00");
        dto.setErrorMssage("정상");
        extractService.saveKtBcResult(dto, sendReqs);
        extractService.updateSendSndngMstStatus(mstId, unitySndMstId, SndngSeCode.KT_BC, "KT-BC 실패(발송마스터 데이타 오류)");
    }


    private void sendBulkKtGibis(final SndngMssageParam dto) {
        final String url = apiHost + apiKtGbsBulkSend;

        EnsKtGbsDTO mstDTO = mapper.selectKtGbsMstTgtInfo(dto)
            .orElseThrow(() -> BizRuntimeException.create("KT GBIS 본문자 수신 등록 대상이 존재 하지 않습니다 "));

        final String mstId = dto.getSndngMastrId();
        final String unitySndMstId = dto.getUnitySndngMastrId();
        final List<KtGbsDTO.MsgSendReqsData> sendReqs = mapper.selectKtGbsSendTgts(dto);

        // validation check
        try {
            validatedKtGbsSendBulks(sendReqs);
        } catch (Exception e) {
            extractService.updateSndngMstFailStatus(mstId, SndngSeCode.KT_GIBIS, "", e.getMessage(), "[send]KT-GIBIS 발송(bulks)요청 실패(파라메터 오류)");
            throw e;
        }

        final List<List<KtGbsDTO.MsgSendReqsData>> partitions = ListUtils.partition(sendReqs, bulkKtMaxCnt);
        @SuppressWarnings("rawtypes")
        List<ApiResponseDTO> apiResults = partitions.stream()
            .map(bulkSendList -> {
                    mstDTO.setReqs(bulkSendList);
                    return apiWebClient.exchange(
                        url,
                        HttpMethod.POST,
                        mstDTO,
                        ApiResponseDTO.class,
                        CmmEnsBizUtils.getHeadeMap());
                }
            )
            .toList();

        boolean isSuccess = false;
        List<ErrorMsg> errList = new ArrayList<>();
        //noinspection rawtypes
        for(ApiResponseDTO apiResult : apiResults) {
            // KT-BC API 정상 호출
            if(ObjectUtils.isNotEmpty(apiResult.getData())) {
                KtCommonResponse resDTO = toObjByObj(apiResult.getData(), KtCommonResponse.class);
                assert resDTO != null;
                dto.setResultDt(resDTO.getResultDt());

                if(ObjectUtils.isNotEmpty(resDTO) && "00".equals(resDTO.getResultCd())){
                    isSuccess = true;
                    dto.setErrorMssage("정상");
                }else{
                    //TODO:: 모바일 콘텐츠 삭제??
                    errList.addAll(resDTO.getErrors());
                }

            }else{
                // KT-BC API 호출전 에러 발생
                if(ObjectUtils.isEmpty(dto.getResultDt())) {
                    dto.setResultDt(DateUtils.getTodayAndNowTime(ApiConstants.FMT_DT_EMPTY_DLT));
                }
                dto.setErrorMssage(apiResult.getMessage());
                errList.add(new ErrorMsg(apiResult.getMessage()));
            }
        }

        // 모두 실패한 경우
        if(!isSuccess) {
            dto.setResultCd("01");
            dto.setErrorMssage(
                errList.stream()
                    .map(ErrorMsg::getErrorMsg)
                    .collect(Collectors.joining(","))
            );
            extractService.saveKtGbsResult(dto, null);
            extractService.updateSndngMstFailStatus(mstId, SndngSeCode.KT_BC, dto.getErrorCode(), dto.getErrorMssage(), "[send] KT-BC 발송(bulks)요청 실패");
            return;
        }
        // 성공 건수 존재시 성공 처리
        dto.setResultCd("00");
        dto.setErrorMssage("정상");
        extractService.saveKtGbsResult(dto, sendReqs);
        extractService.updateSendSndngMstStatus(mstId, unitySndMstId, SndngSeCode.KT_BC, "KT-BC 실패(발송마스터 데이타 오류)");
    }

    private void validatedKtBcSendBulks(List<KtMainSendReqData> sendReqs) {
        List<String> errors = new ArrayList<>();
        int idx = 0;

        for (KtMainSendReqData dto : sendReqs) {
            Set<ConstraintViolation<KtMainSendReqData>> errList = validator.validate(dto);

            if (!errList.isEmpty()) {
                int finalIdx = idx;
                errors.addAll(errList.stream()
                    .map(row -> String.format("%s[%d]=%s", row.getPropertyPath(), finalIdx + 1,
                        row.getMessageTemplate()))
                    .toList()
                );
            }
            idx++;
        }

        if (!errors.isEmpty()) {
            throw BizRuntimeException.create(errors.toString());
        }
    }

    private void validatedKtGbsSendBulks(List<KtGbsDTO.MsgSendReqsData> sendReqs) {
        List<String> errors = new ArrayList<>();
        int idx = 0;

        for (KtGbsDTO.MsgSendReqsData dto : sendReqs) {
            Set<ConstraintViolation<KtGbsDTO.MsgSendReqsData>> errList = validator.validate(dto);

            if (!errList.isEmpty()) {
                int finalIdx = idx;
                errors.addAll(errList.stream()
                    .map(row -> String.format("%s[%d]=%s", row.getPropertyPath(), finalIdx + 1,
                        row.getMessageTemplate()))
                    .toList()
                );
            }
            idx++;
        }

        if (!errors.isEmpty()) {
            throw BizRuntimeException.create(errors.toString());
        }
    }
    //-----------------------------------------------------------------------------------------------------------------
}
