package com.eactive.eai.rms.ext.djb.webhook.service;

import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;

import com.eactive.apim.portal.portalorg.entity.QPortalOrg;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.util.ContainerUtil;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReq;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqApi;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqEvent;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookPayload;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
import com.eactive.eai.rms.ext.djb.webhook.repository.WebhookSendLogRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQueryFactory;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class WebhookService {

    private final WebhookSendLogRepository sendLogRepository;
    private final ObjectMapper objectMapper;
    private final RestTemplate restTemplate;
    private final MonitoringContext monitoringContext;

    @PersistenceContext(unitName = "entityManagerFactoryForEMS")
    private EntityManager entityManager;

    private static final String HMAC_ALGORITHM = "HmacSHA256";
    private static final int DEFAULT_RETRY_COUNT = 3;
    private static final int DEFAULT_RETRY_TIME  = 10000;
    
    
    @SuppressWarnings("unchecked")
    public List<WebhookSendRequest> findSendList(String event, List<String> apiIds) {
        QWebhookReq req = QWebhookReq.webhookReq;
        QWebhookReqEvent evt = QWebhookReqEvent.webhookReqEvent;
        QWebhookReqApi api = QWebhookReqApi.webhookReqApi;
        QPortalOrg org = QPortalOrg.portalOrg;
        
        String reverseProxyUrl = monitoringContext.getProperty(MonitoringContext.RMS_WEBHOOK_REVERSE_PROXY_URL);

        List<Tuple> tuples = new JPAQueryFactory(entityManager)
                .select(req.orgId, req.targetUrl, req.secret, org.reverseProxyPath, api.id.apiId)
                .from(req)
                .join(evt).on(req.id.eq(evt.id.webhookReqId))
                .join(api).on(req.id.eq(api.id.webhookReqId))
                .join(org).on(req.orgId.eq(org.id))
                .where(evt.id.eventType.eq(event)
                        .and(api.id.apiId.in(apiIds)))
                .fetch();

        // orgId + targetUrl + secret 기준으로 그룹핑, apiIds를 data(List)로 집계
        Map<String, WebhookSendRequest> resultMap = new LinkedHashMap<>();
        for (Tuple t : tuples) {
            String key = t.get(req.orgId) + "|" + t.get(req.targetUrl);
            resultMap.computeIfAbsent(key, k -> {
                WebhookSendRequest wr = new WebhookSendRequest();
                wr.setOrgId(t.get(req.orgId));
                wr.setTargetUrl(t.get(req.targetUrl));
                wr.setProxyUrl(this.getProxyUrl(reverseProxyUrl, t.get(org.reverseProxyPath), t.get(req.targetUrl)));
                wr.setSecret(t.get(req.secret));
                wr.setEventType(event);
                wr.setData(new ArrayList<String>());
                return wr;
            });
            ((List<String>) resultMap.get(key).getData()).add(t.get(api.id.apiId));
        }

        return new ArrayList<>(resultMap.values());
    }
    
    //reverse proxy 서버로 발송하기 위한 url 만든다
    private String getProxyUrl(String proxyUrl, String path, String targetUrl) {
    	String base = stripTrailingSlash(proxyUrl);
    	String normalizedPath = stripSlashes(path);
    	// scheme://host[:port] 부분만 제거하고 이후 path/query/fragment는 그대로 유지
    	String targetPath = targetUrl.replaceFirst("^[a-zA-Z][a-zA-Z0-9+.-]*://[^/]+", "");

    	StringBuilder url = new StringBuilder(base);
    	if (!normalizedPath.isEmpty()) {
    		url.append('/').append(normalizedPath);
    	}
    	url.append(targetPath);
    	return url.toString();
    }

    private String stripTrailingSlash(String value) {
    	if (value == null) {
    		return "";
    	}
    	int end = value.length();
    	while (end > 0 && value.charAt(end - 1) == '/') {
    		end--;
    	}
    	return value.substring(0, end);
    }

    private String stripSlashes(String value) {
    	if (value == null) {
    		return "";
    	}
    	String result = value.trim();
    	while (result.startsWith("/")) {
    		result = result.substring(1);
    	}
    	while (result.endsWith("/")) {
    		result = result.substring(0, result.length() - 1);
    	}
    	return result;
    }

    /**
     * 웹훅 발송 메인 메서드
     */
    @Transactional
    public void send(WebhookSendRequest req) {
    	
    	String targetUrl = req.getTargetUrl();
    	String proxyUrl = req.getProxyUrl();
    	String secret = req.getSecret();
    	String eventType = req.getEventType();
    	Object data = req.getData();
    	
        WebhookSendLog sendLog = new WebhookSendLog();
        sendLog.setOrgId(req.getOrgId());
        sendLog.setTargetUrl(targetUrl);
        sendLog.setProxyUrl(proxyUrl);
        sendLog.setEventType(eventType);
        sendLog.setSendBy(ContainerUtil.getInstanceName());

        try {
            // 1. Payload 생성
            WebhookPayload webhookPayload = WebhookPayload.of(eventType, data);
            String payloadJson = objectMapper.writeValueAsString(webhookPayload);
            sendLog.setPayload(payloadJson);

            // 2. Secret Key로 HMAC-SHA256 서명 생성
            String signature = generateSignature(secret, payloadJson);
            sendLog.setSignature(signature);

            // 3. HTTP 요청 헤더 구성
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.set("X-Webhook-Signature", "sha256=" + signature);
            headers.set("X-Webhook-Event", eventType);
            headers.set("X-Webhook-Timestamp", String.valueOf(webhookPayload.getTimestamp()));

            // 4. 발송 (재시도 포함)
            int retryCount = monitoringContext.getIntProperty(MonitoringContext.RMS_WEBHOOK_RETRY_COUNT, DEFAULT_RETRY_COUNT);
            int retryTime  = monitoringContext.getIntProperty(MonitoringContext.RMS_WEBHOOK_RETRY_TIME,  DEFAULT_RETRY_TIME);
            sendLog.setSentAt(LocalDateTime.now());
            HttpEntity<String> httpRequest = new HttpEntity<>(payloadJson, headers);
            ResponseEntity<String> response = sendWithRetry(proxyUrl, httpRequest, sendLog, retryCount, retryTime);

            // 5. 성공 로그
            sendLog.setStatusCode(response.getStatusCode().value());
            sendLog.setResponseBody(response.getBody());
            sendLog.setSuccess(response.getStatusCode().is2xxSuccessful());

            log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}, retryCount: {}",
                    eventType, proxyUrl, response.getStatusCode(), sendLog.getRetryCount());

        } catch (HttpClientErrorException | HttpServerErrorException e) {
            sendLog.setStatusCode(e.getStatusCode().value());
            sendLog.setResponseBody(e.getResponseBodyAsString());
            sendLog.setSuccess(false);
            sendLog.setErrorMessage(e.getMessage());
            log.error("[Webhook] HTTP 오류 - eventType: {}, url: {}, status: {}",
                    eventType, proxyUrl, e.getStatusCode());

        } catch (Exception e) {
            sendLog.setSuccess(false);
            sendLog.setErrorMessage(e.getMessage());
            log.error("[Webhook] 발송 실패 - eventType: {}, url: {}, error: {}",
                    eventType, proxyUrl, e.getMessage());
        }

        sendLogRepository.save(sendLog);
        
    }

    

    /**
     * 재시도 포함 HTTP 발송
     * - 4xx: 클라이언트 오류이므로 즉시 throw (재시도 불필요)
     * - 5xx / 네트워크 오류: 최대 RETRY_COUNT회까지 선형 증가 backoff 재시도 (1s → 2s → 3s → 4s → 5s)
     */
    private ResponseEntity<String> sendWithRetry(String proxyUrl, HttpEntity<String> request, WebhookSendLog sendLog, int retryCount, int retryTime) throws Exception {
        Exception lastException = null;
        for (int attempt = 0; attempt <= retryCount; attempt++) {
            try {
                if (attempt > 0) {
                    long delayMs = (long) retryTime * attempt;
                    log.info("[Webhook] 재시도 {}/{} - url: {}, delay: {}ms", attempt, retryCount, proxyUrl, delayMs);
                    sendLog.setRetryCount(attempt);
                    Thread.sleep(delayMs);
                }
                return restTemplate.postForEntity(proxyUrl, request, String.class);
            } catch (HttpClientErrorException e) {
                //throw e; // 4xx는 재시도 불필요
            	lastException = e; 	//TODO: 재시도 테스트 
            } catch (Exception e) {
                lastException = e;
                log.warn("[Webhook] 발송 실패 (attempt {}/{}) - url: {}, error: {}", attempt, retryCount, proxyUrl, e.getMessage());
            }
        }
        throw lastException;
    }

    /**
     * HMAC-SHA256 서명 생성
     * Java 17 미만은 HexFormat 미지원이므로 직접 변환
     */
    private String generateSignature(String secret, String payload) throws Exception {
        Mac mac = Mac.getInstance(HMAC_ALGORITHM);
        SecretKeySpec keySpec = new SecretKeySpec(
                secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
        mac.init(keySpec);
        byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
        return bytesToHex(hash);
    }

    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}