package com.zioinfo.kintex.livenotice;

import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.common.audit.AuditLogService;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.common.text.HtmlSanitizer;
import com.zioinfo.kintex.livenotice.dto.LiveNoticeDtos.LiveNoticeCreateRequest;
import com.zioinfo.kintex.livenotice.dto.LiveNoticeDtos.LiveNoticeDto;
import com.zioinfo.kintex.work.notification.NotificationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
 * 행사 라이브 공지(F-B6) 서비스 — 주최자 발행 + 공개/관람객 조회 + 보관/고정.
 *
 * <p><b>§5B 알림 통합(중복 신설 금지)</b>: 발행 시 별도 per-user 푸시 인프라를 만들지 않고 기존
 * {@link NotificationService}(공통 알림 모듈)로 행사 멤버에게 in-app 알림을 팬아웃한다. 공개 피드는 본
 * 모듈의 {@code event_live_notice} 테이블이 정본(비로그인 관람객 대상 broadcast).
 *
 * <p>본문/제목은 {@link HtmlSanitizer}로 sanitize(XSS 방어). 발행/보관은 감사 로그를 남긴다.
 */
@Service
public class LiveNoticeService {

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

    private static final Set<String> CATEGORIES = Set.of("URGENT", "PROGRAM", "GENERAL", "INFO");
    private static final int MAX_TITLE = 200;
    private static final int MAX_BODY = 2000;
    private static final int DEFAULT_LIMIT = 100;
    /** 발행 시 §5B 알림 팬아웃 상한(대형 행사 폭주 방지). 초과분은 공개 피드로만 노출. */
    private static final int FANOUT_LIMIT = 500;

    private final LiveNoticeMapper mapper;
    private final NotificationService notificationService;
    private final AuditLogService audit;

    public LiveNoticeService(LiveNoticeMapper mapper, NotificationService notificationService,
                             AuditLogService audit) {
        this.mapper = mapper;
        this.notificationService = notificationService;
        this.audit = audit;
    }

    /** 공개 피드(비인증) — 게시 건만, 고정 우선·최신순. */
    public List<LiveNoticeDto> publicFeed(String eventId, int limit) {
        int lim = limit <= 0 ? DEFAULT_LIMIT : Math.min(limit, 500);
        return mapper.findPublicFeed(eventId, lim).stream().map(LiveNoticeService::toDto).toList();
    }

    /** 주최자 목록 — 전체 상태(status 필터 선택). */
    public List<LiveNoticeDto> organizerList(String eventId, String status, int limit) {
        int lim = limit <= 0 ? DEFAULT_LIMIT : Math.min(limit, 500);
        return mapper.findForOrganizer(eventId, blankToNull(status), lim)
                .stream().map(LiveNoticeService::toDto).toList();
    }

    /** 발행(주최자) — 검증·sanitize·저장 후 §5B 알림 팬아웃 + 감사. */
    @Transactional
    public LiveNoticeDto publish(KintexPrincipal principal, String eventId, LiveNoticeCreateRequest req) {
        if (mapper.eventExists(eventId) == 0) {
            throw new ApiException(ErrorCode.NOT_FOUND, "존재하지 않는 행사입니다.");
        }
        String category = req == null ? null : upper(req.category());
        if (category == null || !CATEGORIES.contains(category)) {
            throw new ApiException(ErrorCode.VALIDATION, "알 수 없는 공지 분류입니다(URGENT/PROGRAM/GENERAL/INFO).");
        }
        String title = req.title() == null ? null : req.title().trim();
        if (title == null || title.isEmpty()) {
            throw new ApiException(ErrorCode.VALIDATION, "공지 제목을 입력해 주세요.");
        }
        if (title.length() > MAX_TITLE) {
            throw new ApiException(ErrorCode.VALIDATION, "제목이 너무 깁니다(최대 " + MAX_TITLE + "자).");
        }
        String body = req.body() == null ? null : req.body();
        if (body != null && body.length() > MAX_BODY) {
            throw new ApiException(ErrorCode.VALIDATION, "본문이 너무 깁니다(최대 " + MAX_BODY + "자).");
        }
        String safeTitle = HtmlSanitizer.sanitize(title);
        String safeBody = body == null ? null : HtmlSanitizer.sanitize(body);
        boolean pinned = Boolean.TRUE.equals(req.pinned());

        String id = "ln-" + UUID.randomUUID().toString().replace("-", "").substring(0, 18);
        Map<String, Object> p = new HashMap<>();
        p.put("id", id);
        p.put("eventId", eventId);
        p.put("category", category);
        p.put("title", safeTitle);
        p.put("body", safeBody);
        p.put("pinned", pinned);
        p.put("authorName", principal.displayName());
        mapper.insert(p);

        // §5B 알림 팬아웃 — 행사 멤버에게 in-app 알림(기존 알림 모듈 재사용, 인프라 신설 없음).
        fanoutToMembers(eventId, id, category, safeTitle);

        audit.record(principal.userId(), principal.displayName(), "LIVE_NOTICE_PUBLISH",
                "event_live_notice", id, eventId,
                "라이브 공지 발행(" + category + ")", "SUCCESS");

        return toDto(mapper.findById(id));
    }

    /** 보관(주최자). */
    @Transactional
    public LiveNoticeDto archive(KintexPrincipal principal, String eventId, String id) {
        Map<String, Object> cur = mapper.findById(id);
        if (cur == null || !eventId.equals(str(cur.get("eventId")))) {
            throw new ApiException(ErrorCode.NOT_FOUND);
        }
        mapper.archive(id, eventId);
        audit.record(principal.userId(), principal.displayName(), "LIVE_NOTICE_ARCHIVE",
                "event_live_notice", id, eventId, "라이브 공지 보관", "SUCCESS");
        return toDto(mapper.findById(id));
    }

    /** 고정 토글(주최자). */
    @Transactional
    public LiveNoticeDto setPinned(KintexPrincipal principal, String eventId, String id, boolean pinned) {
        Map<String, Object> cur = mapper.findById(id);
        if (cur == null || !eventId.equals(str(cur.get("eventId")))) {
            throw new ApiException(ErrorCode.NOT_FOUND);
        }
        mapper.setPinned(id, eventId, pinned);
        return toDto(mapper.findById(id));
    }

    // ── helpers ──
    private void fanoutToMembers(String eventId, String noticeId, String category, String title) {
        try {
            List<Map<String, Object>> members = mapper.findEventMemberUserIds(eventId);
            if (members == null || members.isEmpty()) {
                return;
            }
            String link = "/events/" + eventId + "/live-notices";
            int sent = 0;
            for (Map<String, Object> m : members) {
                if (sent >= FANOUT_LIMIT) {
                    break;
                }
                String uid = str(m.get("userId"));
                if (uid == null || uid.isBlank()) {
                    continue;
                }
                // notiType 은 §5B 공통 코드(SYSTEM) 사용 — 라이브 공지는 시스템성 알림.
                notificationService.notify(uid, eventId, "SYSTEM",
                        "[라이브] " + title, "행사 라이브 공지(" + category + ")가 발행되었습니다.", link);
                sent++;
            }
        } catch (RuntimeException e) {
            // 팬아웃 실패는 발행을 막지 않는다(공개 피드가 정본). 민감정보 미노출.
            log.warn("라이브 공지 §5B 알림 팬아웃 실패(무시): {}", e.getClass().getSimpleName());
        }
    }

    private static LiveNoticeDto toDto(Map<String, Object> r) {
        if (r == null) {
            throw new ApiException(ErrorCode.INTERNAL);
        }
        return new LiveNoticeDto(
                str(r.get("id")), str(r.get("eventId")), str(r.get("category")),
                str(r.get("title")), str(r.get("body")),
                Boolean.TRUE.equals(r.get("pinned")) || "true".equals(str(r.get("pinned"))),
                str(r.get("status")), str(r.get("authorName")),
                str(r.get("createdAt")), str(r.get("updatedAt")));
    }

    private static String upper(String s) {
        return s == null || s.isBlank() ? null : s.trim().toUpperCase(Locale.ROOT);
    }

    private static String blankToNull(String s) {
        return s == null || s.isBlank() ? null : s.trim();
    }

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