package cokr.xit.ens.modules.kkotalk.service.support;

import java.util.*;
import java.util.stream.*;

import org.apache.commons.lang3.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.*;
import org.springframework.http.*;
import org.springframework.stereotype.*;
import org.springframework.transaction.annotation.*;

import com.google.common.collect.*;
import com.google.gson.*;

import cokr.xit.ens.core.aop.*;
import cokr.xit.ens.core.exception.*;
import cokr.xit.ens.core.exception.code.*;
import cokr.xit.ens.core.monitor.slack.event.*;
import cokr.xit.ens.core.utils.*;
import cokr.xit.ens.modules.common.biztmplt.*;
import cokr.xit.ens.modules.common.code.*;
import cokr.xit.ens.modules.common.ctgy.sys.mng.domain.*;
import cokr.xit.ens.modules.common.ctgy.sys.mng.service.*;
import cokr.xit.ens.modules.common.domain.*;
import cokr.xit.ens.modules.common.domain.repository.*;
import cokr.xit.ens.modules.common.domain.support.*;
import cokr.xit.ens.modules.common.event.*;
import cokr.xit.ens.modules.common.monitor.*;
import cokr.xit.ens.modules.kkotalk.code.*;
import cokr.xit.ens.modules.kkotalk.domain.*;
import cokr.xit.ens.modules.kkotalk.domain.repository.*;
import cokr.xit.ens.modules.kkotalk.mapper.*;
import cokr.xit.ens.modules.kkotalk.model.*;
import lombok.*;
import lombok.extern.slf4j.*;

@Slf4j
@Component
@RequiredArgsConstructor
public class KkoTalkSender extends SendProcTemplate {

    private final ApplicationEventPublisher applicationEventPublisher;
    private final SendMastRepository sendMastRepository;
    private final IKkoTalkMapper talkMapper;
    private final SendDetailKkoTalkReqHistRepository sendDetailKkoTalkReqHistRepository;
    private final OrgMngService orgMngService;
    private final KkoTalkApiService kkoTalkApi;

    @Value("${contract.kakao.talk.bulksend-batch-unit}")
    private int SEND_BATCH_UNIT;

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

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public EnsResponseVO<?> execute(Long sendMastId) {

        SendMast sendMast = null;
        List<SendDetailKkoTalkDTO> sendDetails = null;
        EnsResponseVO respVO = null;
        try {
            if (CmmnUtil.isEmpty(sendMastId))
                throw new EnsException(EnsErrCd.SEND410, "발송마스터ID(은)는 필수조건 입니다.");

            sendMast = sendMastRepository.findById(sendMastId)
                .orElseThrow(() -> new EnsException(EnsErrCd.SEND404,
                    String.format("일치하는 발송마스터 자료가 없습니다. [ sendMastId %s ]", sendMastId)));
            sendDetails = talkMapper.findAllFetchBySendMastId(sendMast.getSendMastId());
            OrgMng orgMng = orgMngService.find(sendMast.getOrgCd()).getResultInfo();

            Lists.partition(sendDetails, SEND_BATCH_UNIT).stream()
                .forEach(list -> {

                    List<KkotalkApiDTO.Envelope> envelopes = null;
                    String sendRespBody = null;

                    try {
                        envelopes = this.makeMessage(list);
                        ResponseEntity<String> resp = kkoTalkApi.requestSendBulk(
                            orgMng,
                            KkotalkDTO.BulkSendRequest.builder()
                                .envelopes(envelopes)
                                .build()
                        );

                        sendRespBody = resp.getBody();
                        if (resp.getStatusCode() != HttpStatus.OK)
                            throw new EnsException(EnsErrCd.SEND620, String.format("전송요청 중.. %s %s", resp.getStatusCode().toString(), resp.getBody()));
                        EnsResponseVO<?> mResponse = this.respMsgToMap(sendRespBody);
                        if (!EnsErrCd.OK.equals(mResponse.getErrCode()))
                            throw new EnsException(mResponse.getErrCode(), mResponse.getErrMsg());


                        List<Map<String, Object>> envelopeIds = (List<Map<String, Object>>)((Map<String, Object>)mResponse.getResultInfo()).get(
                            "envelopes");

                        Map<String, KkoTalkApiRespVO> mApiResp = envelopeIds.stream()
                            .map(this::toApiRespVOMap)
                            .collect(Collectors.toMap(m -> String.valueOf(m.get("key")),
                                m -> (KkoTalkApiRespVO)m.get("value"), (k1, k2) -> k1));

                        list.stream()
                            .forEach(row -> {
                                this.modifyTalkSendRsltByExternalId(row, mApiResp);

                            });

                    } catch (EnsException e) {

                        list.stream()
                            .forEach(row -> {
                                row.setStatus(KkoTalkStatusCd.SENT_FAIL);
                                row.setError(
                                    FieldError.initBuilder()
                                        .errorCode(e.getErrCd().getCode())
                                        .errorMessage(e.getMessage())
                                        .build()
                                );
                                row.setErrorCode(e.getErrCd().getCode());
                                row.setErrorMessage(e.getMessage());
                            });
                    } catch (Exception e) {

                        list.stream()
                            .forEach(row -> {
                                row.setStatus(KkoTalkStatusCd.SENT_FAIL);
                                row.setError(
                                    FieldError.initBuilder()
                                        .errorCode(EnsErrCd.SEND500.getCode())
                                        .errorMessage(e.getMessage())
                                        .build()
                                );
                                row.setErrorCode(EnsErrCd.SEND500.getCode());
                                row.setErrorMessage(e.getMessage());
                            });
                    } finally {
                        list.forEach(talkMapper::updateKakaotalkSendBulksResult);

                        if (!CmmnUtil.isEmpty(envelopes))
                            sendDetailKkoTalkReqHistRepository.saveAll(
                                this.toSendDetailReqHist(list, JsonUtils.toJson(envelopes), sendRespBody));
                    }

                });

            Optional<Integer> cntSuccess = sendDetails.stream()
                .map(row -> CmmnUtil.isEmpty(row.getErrorCode()) ? 1 : 0)
                .reduce(Integer::sum);
            if (cntSuccess.get() > 0)
                respVO = EnsResponseVO.okBuilder()
                    .resultInfo(sendDetails.stream()
                        .map(this::toResultInfo)
                        .collect(Collectors.toList()))
                    .build();
            else
                throw new EnsException(EnsErrCd.SEND500, String.format("전체건수 전송 실패(총 %d 건)", sendDetails.size()));

        } catch (EnsException e) {
            respVO = EnsResponseVO.errBuilder().errCode(e.getErrCd()).errMsg(e.getMessage()).build();

        } catch (Exception e) {
            respVO = EnsResponseVO.errBuilder().errCode(EnsErrCd.SEND500).errMsg(e.getMessage()).build();

        } finally {
            if (!CmmnUtil.isEmpty(sendMast)) {
                if (EnsErrCd.OK.equals(respVO.getErrCode())) {
                    sendMast.setStatCd(StatCd.sendok);
                    sendMast.setError(FieldError.initBuilder().build());
                } else {
                    sendMast.setStatCd(StatCd.sendfail);
                    sendMast.setError(FieldError.initBuilder()
                            .errorCode(respVO.getErrCode().getCode())
                            .errorMessage(respVO.getErrMsg())
                            .build());

                    applicationEventPublisher.publishEvent(MonitorEvent.builder()
                            .message(MessageByPhase.builder()
                                    .oClass(getClass().getSimpleName() + "." + new Throwable().getStackTrace()[0].getMethodName())
                                    .postSeCd(PostSeCd.kkoTalk)
                                    .statCd(StatCd.sendfail)
                                    .errCd(respVO.getErrCode())
                                    .message("-.SendMastId: " + sendMastId + "\n" + respVO.getErrMsg())
                                    .build())
                            .build());
                }
            }
            applicationEventPublisher.publishEvent(SendMastStatUpdateEvent.builder().sendMastId(sendMastId).build());
        }


        return respVO;
    }


    /**
     * 전송요청API body Message로 변환하여 반환 한다.
     *
     * @param list
     * @return
     */
    private List<KkotalkApiDTO.Envelope> makeMessage(List<SendDetailKkoTalkDTO> list) {
        final List<KkotalkApiDTO.Envelope> bulkList = new ArrayList<>();

        for (SendDetailKkoTalkDTO sendTgtDTO : list) {

            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.getLink())
                .build();

            bulkReqDTO.setExternalId(sendTgtDTO.getExternalId().toString());
            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;
    }


    /**
     * 전송요청 응답결과를 API응답VO 객체에 매핑하여 반환
     *
     * @param row
     * @return
     */
    private Map<String, Object> toApiRespVOMap(Map<String, Object> row) {
        KkoTalkApiRespVO apiRespVO = KkoTalkApiRespVO.builder()
                .errorCode(row.containsKey("errorCode") ? String.valueOf(row.get("errorCode")) : null)
                .errorMessage(row.containsKey("errorMessage") ? String.valueOf(row.get("errorMessage")) : null)
                .data(row.containsKey("envelopeId") ? String.valueOf(row.get("envelopeId")) : null)
                .build();

        Map<String, Object> m = new HashMap<>();
        m.put("key", row.get("externalId"));
        m.put("value", apiRespVO);
        return m;
    }

    /**
     * 전송요청결과 갱신(update)
     * -. 성공/실패 여부에 따라 문서식별번호(document_binder_uuid) 및 Error 필드 갱신
     *
     * @param row SendDetailKkoTalkDTO
     * @param map Map<String, KkoTalkApiRespVO>
     */
    private void modifyTalkSendRsltByExternalId(SendDetailKkoTalkDTO row, Map<String, KkoTalkApiRespVO> map) {
        KkoTalkApiRespVO apiRespVO = map.get(row.getExternalId());
        if (CmmnUtil.isEmpty(apiRespVO.getErrorCode())) {
            row.setStatus(KkoTalkStatusCd.SENT);
            row.setEnvelopeId(String.valueOf(apiRespVO.getData()));
            row.setError(FieldError.initBuilder().build());
        } else {
            row.setStatus(KkoTalkStatusCd.valueOfEnum(apiRespVO.getErrorCode()));
            row.setError(FieldError.initBuilder()
                .errorCode(EnsErrCd.SEND630.getCode())
                .errorMessage(String.format("%s %s", apiRespVO.getErrorCode(), apiRespVO.getErrorMessage()))
                .build());
            row.setErrorCode(EnsErrCd.SEND630.getCode());
            row.setErrorMessage(String.format("%s %s", apiRespVO.getErrorCode(), apiRespVO.getErrorMessage()));
        }

    }


    private Map<String, String> toResultInfo(SendDetailKkoTalkDTO row) {
        Map<String, String> m = new HashMap<>();
        m.put("envelopeId", row.getEnvelopeId());
        m.put("externalId", row.getExternalId());
        if (!CmmnUtil.isEmpty(row.getErrorCode())) {
            m.put("errCd", row.getErrorCode());
            m.put("errMsg", row.getErrorMessage());
        }
        return m;
    }


    /**
     * 전송요청응답을 전송요청이력 Entity 데이터로 변환하여 반환 한다.
     *
     * @param sendMsg
     * @param respMsg
     * @return
     */
    private List<SendDetailKkoTalkReqHist> toSendDetailReqHist(List<SendDetailKkoTalkDTO> list, String sendMsg, String respMsg) {
        List<Map> sendMsgList = JsonUtils.toObjectList(sendMsg, Map.class);
        Map<String, Long> mSendDetailIds = list.stream()
                .map(row -> {
                    Map<String, Object> m = new HashMap<>();
                    m.put("key", row.getExternalId());
                    m.put("value", row.getSendDetailId());
                    return m;
                })
                .collect(Collectors.toMap(m -> String.valueOf(m.get("key")), m -> (Long) m.get("value"), (k1, k2) -> k1));

        if(JsonUtils.isJson(respMsg)) {
            EnsResponseVO<?> respVO = this.respMsgToMap(respMsg);

            if (EnsErrCd.OK.equals(respVO.getErrCode())) {
                Map<String, Object> mRespBody = (Map<String, Object>)respVO.getResultInfo();
                List<Map<String, String>> envelopeIds = (List<Map<String, String>>)mRespBody.get("envelopes");
                Map<String, Map<String, String>> mRespMsg = envelopeIds.stream()
                    .map(row -> {
                        Map<String, Object> m = new HashMap<>();
                        m.put("key", String.valueOf(row.get("externalId")));
                        Map<String, String> mValue = new HashMap<>();
                        mValue.put("envelopeId", row.get("envelopeId"));
                        mValue.put("errorCode", row.get("errorCode"));
                        mValue.put("errorMessage", row.get("errorMessage"));
                        mValue.put("rawMsg", gson.toJson(row));
                        m.put("value", mValue);
                        return m;
                    })
                    .collect(
                        Collectors.toMap(m -> String.valueOf(m.get("key")), m -> (Map<String, String>)m.get("value"),
                            (k1, k2) -> k1));

                return sendMsgList.stream()
                    .map(row -> {
                        //
                        //Map<String, String> property = (Map<String, String>) row.get("property");
                        String externalId = (String)row.get("externalId");
                        boolean isSuccess = CmmnUtil.isEmpty(mRespMsg.get(externalId).get("errorCode"));

                        return SendDetailKkoTalkReqHist.builder()
                            .sendDetailId(mSendDetailIds.get(externalId))
                            .sendRawMsg(gson.toJson(row))
                            .externalId(externalId)
                            .envelopeId(mRespMsg.get(externalId).get("envelopeId"))
                            .respRawMsg(mRespMsg.get(externalId).get("rawMsg"))
                            .error(FieldError.initBuilder()
                                .errorCode(isSuccess ? null : EnsErrCd.SEND620.getCode())
                                .errorMessage(isSuccess ? null : String.format("%s %s"
                                    , mRespMsg.get(externalId).get("errorCode")
                                    , mRespMsg.get(externalId).get("errorMessage")
                                ))
                                .build())
                            .build();
                    })
                    .collect(Collectors.toList());

            } else {
                return sendMsgList.stream()
                    .map(row -> {
                        //Map<String, String> property = (Map<String, String>) row.get("property");
                        String externalId = (String)row.get("externalId");

                        return SendDetailKkoTalkReqHist.builder()
                            .sendDetailId(mSendDetailIds.get(externalId))
                            .sendRawMsg(gson.toJson(row))
                            .externalId(externalId)
                            .respRawMsg(respMsg)
                            .error(FieldError.initBuilder()
                                .errorCode(respVO.getErrCode().getCode())
                                .errorMessage(respVO.getErrMsg())
                                .build())
                            .build();
                    })
                    .collect(Collectors.toList());
            }
        }else{
            return sendMsgList.stream()
                .map(row -> {
                    //Map<String, String> property = (Map<String, String>) row.get("property");
                    String externalId = (String)row.get("externalId");

                    return SendDetailKkoTalkReqHist.builder()
                        .sendDetailId(mSendDetailIds.get(externalId))
                        .sendRawMsg(gson.toJson(row))
                        .externalId(externalId)
                        .respRawMsg(respMsg)
                        .error(FieldError.initBuilder()
                            .errorCode("")
                            .errorMessage(respMsg)
                            .build())
                        .build();
                })
                .collect(Collectors.toList());
        }
    }

    private EnsResponseVO<?> respMsgToMap(String respMsg) {
        EnsErrCd errCode = EnsErrCd.OK;
        String errMsg = EnsErrCd.OK.getCodeNm();
        Map<String, Object> mResp = null;
        try {
            mResp = gson.fromJson(respMsg, Map.class);
            if (CmmnUtil.isEmpty(mResp.get("envelopes")))
                throw new EnsException(EnsErrCd.SEND620, String.format("전송요청API 오류. envelopes 키값이 없습니다. %s", respMsg));
        } catch (EnsException e) {
            errCode = e.getErrCd();
            errMsg = e.getMessage();
        } catch (Exception e) {
            errCode = EnsErrCd.SEND511;
            errMsg = String.format("전송요청API 응답데이터 JSON 객체 변환 실패. %s", respMsg);
        }
        return EnsErrCd.OK.equals(errCode) ?
                EnsResponseVO.okBuilder().resultInfo(mResp).build()
                : EnsResponseVO.errBuilder()
                .errCode(errCode)
                .errMsg(errMsg)
                .build();
    }
}
