package kr.xit.ens.kakao.talk.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;

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

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;

import kr.xit.biz.common.ApiConstants.SndngSeCode;
import kr.xit.biz.ens.model.cmm.CmmEnsRequestDTO;
import kr.xit.biz.ens.model.cmm.CmmEnsRlaybsnmDTO;
import kr.xit.biz.ens.model.kakao.talk.KkotalkApiDTO;
import kr.xit.biz.ens.model.kakao.talk.KkotalkDTO;
import kr.xit.core.exception.BizRuntimeException;
import kr.xit.core.model.ApiResponseDTO;
import kr.xit.core.service.AbstractService;
import kr.xit.core.spring.annotation.TraceLogging;
import kr.xit.core.spring.util.ApiWebClientUtil;
import kr.xit.core.support.utils.Checks;
import kr.xit.core.support.utils.JsonUtils;
import kr.xit.ens.cmm.CmmEnsUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * <pre>
 * description : 카카오 페이 전자 문서 발송 요청 서비스
 * packageName : kr.xit.ens.kakao.talk.service
 * fileName    : KkopayEltrcDocService
 * author      : julim
 * date        : 2023-04-28
 * ======================================================================
 * 변경일         변경자        변경 내용
 * ----------------------------------------------------------------------
 * 2023-04-28    julim       최초 생성
 *
 * </pre>
 */
@Slf4j
@RequiredArgsConstructor
@Component
public class KkotalkEltrcDocService extends AbstractService implements
    IKkotalkEltrcDocService {

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

    @Value("#{'${app.contract.kakao.api.talk.send}'.split(';')}")
    private String[] API_SEND;

    @Value("#{'${app.contract.kakao.api.talk.bulksend}'.split(';')}")
    private String[] API_BULKSEND;

    @Value("#{'${app.contract.kakao.api.talk.validToken}'.split(';')}")
    private String[] API_VALID_TOKEN;

    @Value("#{'${app.contract.kakao.api.talk.modifyStatus}'.split(';')}")
    private String[] API_MODIFY_STATUS;


    @Value("#{'${app.contract.kakao.api.talk.bulkstatus}'.split(';')}")
    private String[] API_BULKSTATUS;

    private final ApiWebClientUtil webClient;
    private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
    private static final CharSequence ENVELOPE_ID = "{ENVELOPE_ID}";


    /**
     * <pre>
     * 모바일웹 연계 문서발송 요청 : POST
     * -.이용기관 서버에서 전자문서 서버로 문서발송 처리 요청
     * </pre>
     * @param reqDTO KkoPayEltrDocDTO.RequestSendReq
     * @return ApiResponseDTO<KkopayDocDTO.SendResponse>
     */
    @Override
    @TraceLogging
    public KkotalkDTO.SendResponse requestSend(final KkotalkDTO.SendRequest reqDTO) {
        if(Checks.isEmpty(reqDTO.getProductCode())){
            throw BizRuntimeException.create("상품 코드는 필수 입니다.");
        }
        List<String> errors = new ArrayList<>();
        errors = validate(reqDTO.getEnvelope(), errors);

        final KkotalkApiDTO.Envelope envelope = reqDTO.getEnvelope();
        if(envelope.getReviewExpiresAt() != null){
            if(envelope.getReviewExpiresAt().compareTo(envelope.getReadExpiresAt()) < 0){
                errors.add("reviewExpiresAt=재열람 만료일시를 최조 열람 만료일시 보다 큰 날짜로 입력해주세요.");
            }
        }

        if(Checks.isEmpty(envelope.getCi())){
            if(Checks.isEmpty(envelope.getName()))          Objects.requireNonNull(errors).add("name=받는이 이름은 필수입니다.");
            if(Checks.isEmpty(envelope.getPhoneNumber()))   Objects.requireNonNull(errors).add("phoneNumber=받는이 전화번호는 필수입니다.");
            if(Checks.isEmpty(envelope.getBirthday()))      Objects.requireNonNull(errors).add("birthday=받는이 생년월일은 필수입니다.");
        }
        if(!Objects.requireNonNull(errors).isEmpty()) throw BizRuntimeException.create(errors.toString());
        return webClient.exchangeKkotalk(
            HOST + API_SEND[0].replace("{PRODUCT_CODE}", reqDTO.getProductCode()),
            HttpMethod.valueOf(API_SEND[1]),
            JsonUtils.toJson(envelope),
            KkotalkDTO.SendResponse.class,
            getRlaybsnmInfo(reqDTO));
    }

    /**
     * <pre>
     * 토큰 유효성 검증(Redirect URL  접속 허용/불허) : GET
     * </pre>
     * @param reqDTO KkopayDocDTO.ValidTokenRequest
     * @return ApiResponseDTO<KkopayDocDTO.ValidTokenResponse>
     */
    @Override
    @TraceLogging
    public KkotalkApiDTO.ValidTokenResponse validToken(final KkotalkApiDTO.ValidTokenRequest reqDTO) {
        validate(reqDTO, null);

        return webClient.exchangeKkotalk(
            HOST
                + API_VALID_TOKEN[0].replace(ENVELOPE_ID, reqDTO.getEnvelopeId())
                .replace("{TOKEN}", reqDTO.getToken()),
            HttpMethod.valueOf(API_VALID_TOKEN[1]),
            null,
            KkotalkApiDTO.ValidTokenResponse.class,
            getRlaybsnmInfo(reqDTO));
    }

    /**
     * <pre>
     * 문서 열람 처리 API : POST
     * -.문서에 대해서 열람 상태로 변경. 사용자가 문서열람 시(OTT 검증 완료 후 페이지 로딩 완료 시점) 반드시 문서 열람 상태 변경 API를 호출해야 함.
     * -.미 호출 시 아래와 같은 문제 발생
     * 1)유통증명시스템을 사용하는 경우 해당 API를 호출한 시점으로 열람정보가 등록되어 미 호출 시 열람정보가 등록 되지 않음.
     * 2)문서상태조회 API(/v1/envelopes/${ENVELOPE_ID}/read) 호출 시 read_at최초 열람시간) 데이터가 내려가지 않음.
     * </pre>
     * @param reqDTO KkopayDocAttrDTO.EnvelopeId
     */
    @Override
    @TraceLogging
    public void modifyStatus(final KkotalkDTO.EnvelopeId reqDTO){
        validate(reqDTO.getEnvelopeId(), null);

        final String url = HOST + API_MODIFY_STATUS[0].replace(ENVELOPE_ID, reqDTO.getEnvelopeId());

        webClient.exchangeKkotalk(url, HttpMethod.valueOf(API_MODIFY_STATUS[1]), null, Void.class, getRlaybsnmInfo(reqDTO));
    }

    /**
     * <pre>
     * 문서 상태 조회 API : GET
     * -.이용기관 서버에서 카카오페이 전자문서 서버로 문서 상태에 대한 조회를 요청 합니다.
     * : 발송된 문서의 진행상태를 알고 싶은 경우, flow와 상관없이 요청 가능
     * : polling 방식으로 호출할 경우, 호출 간격은 5초를 권장.
     * : RECEIVE(수신,미수신) > READ(열람)/EXPIRED
     * </pre>
     * @param reqDTO KkotalkDTO.EnvelopeId
     * @return KkotalkApiDTO.EnvelopeStatusResponse
     */
    @Override
    @TraceLogging
    public KkotalkApiDTO.EnvelopeStatusResponse findStatus(final KkotalkApiDTO.EnvelopeId reqDTO){
        validate(reqDTO, null);

        String param = "{\"envelopeIds\":" + JsonUtils.toJson(List.of(reqDTO.getEnvelopeId())) + "}";
        KkotalkDTO.BulkStatusResponse res = webClient.exchangeKkotalk(
            HOST + API_BULKSTATUS[0],
            HttpMethod.valueOf(API_BULKSTATUS[1]),
            param,
            KkotalkDTO.BulkStatusResponse.class,
            getRlaybsnmInfo(reqDTO));
        return res.getEnvelopeStatus().get(0);
    }

    /**
     * <pre>
     * 모바일웹 연계 문서발송 요청 : POST
     * -.이용기관 서버에서 전자문서 서버로 문서발송 처리를 요청합니다.
     * </pre>
     * @param reqDTO KkotalkDTO.BulkSendRequest
     * @return KkotalkDTO.BulkSendResponse
     */
    @Override
    @TraceLogging
    public KkotalkDTO.BulkSendResponse requestSendBulk(final KkotalkDTO.BulkSendRequest reqDTO) {
        if(Checks.isEmpty(reqDTO.getProductCode())){
            throw BizRuntimeException.create("상품 코드는 필수 입니다.");
        }

        List<String> errors = new ArrayList<>();

        List<KkotalkApiDTO.Envelope> envelopes = reqDTO.getEnvelopes();
        for(int idx = 0; idx < envelopes.size(); idx++) {
            final Set<ConstraintViolation<KkotalkApiDTO.Envelope>> list = validator.validate(envelopes.get(idx));
            if (!list.isEmpty()) {

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

        for(int idx = 0; idx < envelopes.size(); idx++) {
            final KkotalkApiDTO.Envelope envelope = envelopes.get(idx);

            if(envelope.getReviewExpiresAt() != null){
                if(envelope.getReviewExpiresAt().compareTo(envelope.getReadExpiresAt()) < 0){
                    errors.add("reviewExpiresAt=재열람 만료일시를 최조 열람 만료일시 보다 큰 날짜로 입력해주세요.");
                }
            }

            if (Checks.isEmpty(envelope.getCi())) {
                if (Checks.isEmpty(envelope.getName()))         errors.add(String.format("받는이 이름은 필수입니다(name[%d] 번째 오류)", idx+1));
                if (Checks.isEmpty(envelope.getPhoneNumber())) errors.add(String.format("받는이 전화번호는 필수입니다(phoneNumber[%d] 번째 오류)", idx+1));
                if (Checks.isEmpty(envelope.getBirthday()))     errors.add(String.format("받는이 생년월일은 필수입니다(birthday[%d] 번째 오류)", idx+1));
            } else {
                final StringBuilder sb = new StringBuilder()
                    .append(StringUtils.defaultString(envelope.getName(), StringUtils.EMPTY))
                    .append(StringUtils.defaultString(envelope.getPhoneNumber(), StringUtils.EMPTY))
                    .append(StringUtils.defaultString(envelope.getBirthday(), StringUtils.EMPTY));

                if(Checks.isNotEmpty(sb.toString())){
                    errors.add(String.format("CI가 지정 되었습니다(받는이 정보 불필요:[%d] 번째 오류) .", idx+1));
                }
            }
        }
        if(!errors.isEmpty()){
            throw BizRuntimeException.create(errors.toString());
        }
        String param = "{\"envelopes\":" + JsonUtils.toJson(envelopes) + "}";
        return webClient.exchangeKkotalk(
            HOST + API_BULKSEND[0].replace("{PRODUCT_CODE}", reqDTO.getProductCode()),
            HttpMethod.valueOf(API_BULKSEND[1]),
            param,
            KkotalkDTO.BulkSendResponse.class,
            getRlaybsnmInfo(reqDTO));
    }

    /**
     * <pre>
     * 대량(bulk) 문서 상태 조회 API : POST
     * -.이용기관 서버에서 카카오페이 전자문서 서버로 문서 상태에 대한 조회를 요청 합니다.
     * : 발송된 문서의 진행상태를 알고 싶은 경우, flow와 상관없이 요청 가능
     * : polling 방식으로 호출할 경우, 호출 간격은 5초를 권장.
     * : RECEIVE(수신,미수신) > READ(열람)/EXPIRED
     * </pre>
     * @param reqDTO KkotalkDTO.BulkStatusRequest
     * @return KkotalkDTO.BulkStatusResponse
     */
    @Override
    @TraceLogging
    public KkotalkDTO.BulkStatusResponse findBulkStatus(final KkotalkDTO.BulkStatusRequest reqDTO) {
        List<String> errors = new ArrayList<>();

        List<String> envelopes = reqDTO.getEnvelopes();
        for(int idx = 0; idx < envelopes.size(); idx++) {
            final String binderUuid = envelopes.get(idx);
            if (Checks.isEmpty(binderUuid) || binderUuid.length() > 40) {
                errors.add(String.format("문서 식별 번호는 40자를 넘을 수 없습니다[%d번째]", idx+1));
            }
        }
        if(!errors.isEmpty()) {
            throw BizRuntimeException.create(errors.toString());
        }
        String param = "{\"envelopeIds\":" + JsonUtils.toJson(envelopes) + "}";
        return webClient.exchangeKkotalk(
            HOST + API_BULKSTATUS[0],
            HttpMethod.valueOf(API_BULKSTATUS[1]),
            param,
            KkotalkDTO.BulkStatusResponse.class,
            getRlaybsnmInfo(reqDTO));
    }

    @Override
    public ApiResponseDTO<KkotalkApiDTO.ValidTokenResponse> findKkotalkReadyAndMblPage(final KkotalkApiDTO.ValidTokenRequest reqDTO) {
        final String url = HOST + API_VALID_TOKEN[0].replace(ENVELOPE_ID, reqDTO.getEnvelopeId())
                .replace("{TOKEN}", reqDTO.getToken());

        // 유효성 검증
        final KkotalkApiDTO.ValidTokenResponse validTokenRes = webClient.exchangeKkotalk(url, HttpMethod.valueOf(API_VALID_TOKEN[1]), null,
                KkotalkApiDTO.ValidTokenResponse.class, getRlaybsnmInfo(reqDTO));

        // 문서상태 변경
        final String url2 = HOST + API_MODIFY_STATUS[0].replace(ENVELOPE_ID, reqDTO.getEnvelopeId());

        // 정상 : HttpStatus.NO_CONTENT(204) return
        // error : body에 error_code, error_message return
        final KkotalkApiDTO.KkotalkErrorDTO errorDTO = webClient.exchangeKkotalk(url2, HttpMethod.valueOf(API_MODIFY_STATUS[1]), null, KkotalkApiDTO.KkotalkErrorDTO.class, getRlaybsnmInfo(reqDTO));
        if(errorDTO != null){
            return ApiResponseDTO.error(errorDTO.getErrorCode(), errorDTO.getErrorMessage());
        }
        return ApiResponseDTO.success();
    }

    //-------------------------------------------------------------------------------------------------------------------
    private static <T> List<String> validate(T t, List<String> errList) {
        final Set<ConstraintViolation<T>> list = validator.validate(t);

        if(!list.isEmpty()) {
            final List<String> errors = list.stream()
                .map(row -> String.format("%s=%s", row.getPropertyPath(), row.getMessageTemplate()))
                .toList();

            // 추가적인 유효성 검증이 필요 없는 경우
            if(errList == null){
                if(!errors.isEmpty())   throw BizRuntimeException.create(errors.toString());
                return null;
            }
            errList.addAll(errors);
        }
        return errList;
    }

    private CmmEnsRlaybsnmDTO getRlaybsnmInfo(final CmmEnsRequestDTO request){
        return CmmEnsUtils.getRlaybsnmInfo(request.getSignguCode(), request.getFfnlgCode(), SndngSeCode.KAKAO);
    }
}
