package com.zioinfo.kintex.webhook;

import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.security.SecretCipher;
import com.zioinfo.kintex.webhook.dto.WebhookDtos.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
 * F099 웹훅 게이트웨이 서비스.
 * <p><b>아웃바운드는 스텁</b> — {@link #enqueueOutbound} 는 발송 큐 레코드(status=disabled)만 기록하고
 * 실 HTTP 콜은 수행하지 않는다(외부 API 금지 원칙 · 리스크 G4). <b>인바운드만 실동작</b> — 토큰 조회 +
 * HMAC-SHA256 서명 검증 후 저장({@link #receiveInbound}).
 * <p>시크릿 원문/해시/봉인값은 어떤 응답에도 노출하지 않는다.
 */
@Service
public class WebhookService {

    private static final Logger log = LoggerFactory.getLogger(WebhookService.class);

    /** 등록 허용 이벤트 유형(계약 고정 집합 — 임의 유형 등록 차단). */
    public static final Set<String> KNOWN_EVENT_TYPES = Set.of(
            "lead.hot", "invoice.paid", "auction.awarded", "booth.sold", "campaign.done");

    private final WebhookMapper mapper;
    private final WebhookSignature signature;
    private final SecretCipher cipher;

    public WebhookService(WebhookMapper mapper, WebhookSignature signature, SecretCipher cipher) {
        this.mapper = mapper;
        this.signature = signature;
        this.cipher = cipher;
    }

    // ── 아웃바운드 구독 CRUD ──
    public List<SubscriptionDto> listSubscriptions() {
        return toSubscriptionList(mapper.findSubscriptions());
    }

    public SubscriptionDto createSubscription(SubscriptionCreateRequest req, String actorId) {
        String eventType = requireKnownType(req == null ? null : req.eventType());
        String url = trimOrNull(req.callbackUrl());
        String secret = trimOrNull(req.secret());
        if (url == null) {
            throw new ApiException(ErrorCode.VALIDATION, "콜백 URL을 입력해 주세요.");
        }
        if (secret == null) {
            throw new ApiException(ErrorCode.VALIDATION, "서명 시크릿을 입력해 주세요.");
        }
        String id = "wh-" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
        // 원문은 즉시 SHA-256 해시 후 폐기(발송 disabled → 재계산 불요).
        mapper.insertSubscription(id, eventType, url, signature.hashSecret(secret),
                trimOrNull(req.description()), actorId);
        return toSubscription(mapper.findSubscription(id));
    }

    public SubscriptionDto updateSubscription(String id, SubscriptionUpdateRequest req) {
        if (mapper.findSubscription(id) == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "존재하지 않는 구독입니다.");
        }
        String secretHash = null;
        String secret = req == null ? null : trimOrNull(req.secret());
        if (secret != null) {
            secretHash = signature.hashSecret(secret);
        }
        mapper.updateSubscription(id,
                req == null ? null : trimOrNull(req.callbackUrl()),
                secretHash,
                req == null ? null : trimOrNull(req.description()),
                req == null ? null : req.active());
        return toSubscription(mapper.findSubscription(id));
    }

    public void deleteSubscription(String id) {
        if (mapper.deleteSubscription(id) == 0) {
            throw new ApiException(ErrorCode.NOT_FOUND, "존재하지 않는 구독입니다.");
        }
    }

    /**
     * 아웃바운드 발송 트리거 — <b>스텁</b>. 실 HTTP 콜 대신 발송 큐 레코드(status=disabled)만 기록한다.
     * 다른 모듈이 도메인 이벤트 발생 시 호출할 수 있는 통합 지점(현재 자동 배선 없음 · 후속 웨이브).
     */
    public int enqueueOutbound(String eventType, String subscriptionId, String payloadJson) {
        String id = "wd-" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
        return mapper.insertDelivery(id, subscriptionId, eventType,
                payloadJson == null ? "{}" : payloadJson,
                "outbound disabled(stub) — no external HTTP call");
    }

    public List<DeliveryDto> listDeliveries() {
        List<Map<String, Object>> rows = mapper.findDeliveries();
        List<DeliveryDto> out = new ArrayList<>(rows == null ? 0 : rows.size());
        if (rows != null) {
            for (Map<String, Object> r : rows) {
                out.add(new DeliveryDto(str(r.get("id")), str(r.get("subscriptionId")),
                        str(r.get("eventType")), str(r.get("status")), str(r.get("note")),
                        odt(r.get("createdAt"))));
            }
        }
        return out;
    }

    // ── 인바운드 엔드포인트 ──
    public List<InboundEndpointDto> listInboundEndpoints() {
        List<Map<String, Object>> rows = mapper.findInboundEndpoints();
        List<InboundEndpointDto> out = new ArrayList<>(rows == null ? 0 : rows.size());
        if (rows != null) {
            for (Map<String, Object> r : rows) {
                out.add(new InboundEndpointDto(str(r.get("id")), str(r.get("token")),
                        str(r.get("sourceLabel")), bool(r.get("active")), odt(r.get("createdAt"))));
            }
        }
        return out;
    }

    public InboundEndpointDto createInboundEndpoint(InboundEndpointCreateRequest req, String actorId) {
        String secret = req == null ? null : trimOrNull(req.secret());
        if (secret == null) {
            throw new ApiException(ErrorCode.VALIDATION, "HMAC 시크릿을 입력해 주세요.");
        }
        String id = "wi-" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
        // 토큰은 추측 불가 랜덤(공개 경로 식별자). 시크릿은 봉인 저장(HMAC 검증에 원문 필요).
        String token = "in_" + UUID.randomUUID().toString().replace("-", "");
        mapper.insertInboundEndpoint(id, token, cipher.seal(secret),
                trimOrNull(req.sourceLabel()), actorId);
        // 응답에 token 은 포함(공개 식별자), 시크릿은 미포함.
        return new InboundEndpointDto(id, token, req.sourceLabel(), true, OffsetDateTime.now());
    }

    /**
     * 인바운드 수신 — 토큰 유효성 + HMAC-SHA256 서명 이중 검증 후 이벤트를 저장한다.
     * <p>토큰 미존재/비활성 또는 서명 불일치는 모두 401 로 귀결(존재 여부·사유 미노출 — 정보 최소화).
     * 검증 통과 시에만 저장하고 accepted receipt 를 반환한다.
     *
     * @param token         경로 토큰(공개 식별자)
     * @param rawBody       원문 바디(HMAC 계산 대상 — 파싱 전 raw)
     * @param providedSig   발신자 제공 서명 헤더값(sha256=hex 또는 hex)
     * @param declaredType  페이로드 declared 이벤트 유형(검증 후 저장, 신뢰 최소)
     */
    public InboundReceipt receiveInbound(String token, String rawBody, String providedSig, String declaredType) {
        Map<String, Object> row = mapper.findInboundSecretEnc(token);
        if (row == null) {
            // 토큰 미존재/비활성 — 존재 여부 노출 없이 401.
            throw new ApiException(ErrorCode.UNAUTHORIZED, "웹훅 인증 실패");
        }
        String secret = cipher.open(str(row.get("secretEnc")));
        boolean valid = signature.verify(secret, rawBody == null ? "" : rawBody, providedSig);
        if (!valid) {
            // 서명 불일치도 저장하지 않고 401(무효 트래픽 미적재 — 로그 스팸/DoS 완화).
            log.warn("Inbound webhook signature rejected token={}", mask(token));
            throw new ApiException(ErrorCode.UNAUTHORIZED, "웹훅 인증 실패");
        }
        String id = "wie-" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
        mapper.insertInboundEvent(id, token, trimOrNull(declaredType),
                rawBody == null ? "{}" : rawBody, true);
        return new InboundReceipt(true, id);
    }

    // ── helpers ──
    private String requireKnownType(String type) {
        String t = trimOrNull(type);
        if (t == null || !KNOWN_EVENT_TYPES.contains(t)) {
            throw new ApiException(ErrorCode.VALIDATION,
                    "지원하지 않는 이벤트 유형입니다(허용: " + String.join(", ", KNOWN_EVENT_TYPES) + ").");
        }
        return t;
    }

    private List<SubscriptionDto> toSubscriptionList(List<Map<String, Object>> rows) {
        List<SubscriptionDto> out = new ArrayList<>(rows == null ? 0 : rows.size());
        if (rows != null) {
            for (Map<String, Object> r : rows) {
                out.add(toSubscription(r));
            }
        }
        return out;
    }

    private SubscriptionDto toSubscription(Map<String, Object> r) {
        if (r == null) {
            return null;
        }
        return new SubscriptionDto(str(r.get("id")), str(r.get("eventType")), str(r.get("callbackUrl")),
                str(r.get("description")), bool(r.get("active")),
                odt(r.get("createdAt")), odt(r.get("updatedAt")));
    }

    private static String mask(String s) {
        if (s == null || s.length() <= 6) {
            return "***";
        }
        return s.substring(0, 4) + "***";
    }

    private static String trimOrNull(String s) {
        if (s == null) {
            return null;
        }
        String t = s.trim();
        return t.isEmpty() ? null : t;
    }

    private static String str(Object o) {
        return o == null ? null : String.valueOf(o);
    }

    private static boolean bool(Object o) {
        return o instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(o));
    }

    private static OffsetDateTime odt(Object o) {
        if (o instanceof OffsetDateTime t) {
            return t;
        }
        if (o instanceof java.time.Instant i) {
            return i.atOffset(java.time.ZoneOffset.UTC);
        }
        return null;
    }
}
