package com.zioinfo.cms.content;

import com.zioinfo.cms.admin.AuditService;
import com.zioinfo.cms.content.mapper.ContentMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 콘텐츠 서비스 — 헤드리스 콘텐츠 CRUD + 게시 워크플로우 + 버전/롤백 + 예약 게시.
 *
 * <p>워크플로우: DRAFT → REVIEW → APPROVED → PUBLISHED (역방향: REJECT→DRAFT, ARCHIVE).
 * 게시 승인/발행은 EDITOR 이상만 가능(메서드 가드). 모든 전이는 버전 스냅샷 + 감사로그.
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ContentService {

    private static final Set<String> EDITOR_ROLES = Set.of("EDITOR", "SUPERADMIN");
    private static final Set<String> TYPES = Set.of("PAGE", "POST", "BLOCK", "PRODUCT_DETAIL");

    private final ContentMapper mapper;
    private final AuditService auditService;

    public List<CmsContent> list(String contentType, String status, String locale, String keyword) {
        return mapper.findAll(contentType, status, locale, keyword);
    }

    public CmsContent get(Long id) {
        CmsContent c = mapper.findById(id);
        if (c == null) {
            throw new RuntimeException("ERR-CMS-CONTENT-404: 콘텐츠 없음");
        }
        return c;
    }

    @Transactional
    public CmsContent create(Map<String, Object> req, String actor) {
        CmsContent c = new CmsContent();
        bind(c, req);
        if (c.getContentType() == null || !TYPES.contains(c.getContentType())) {
            c.setContentType("PAGE");
        }
        if (c.getSlug() == null || c.getSlug().isBlank()) {
            throw new IllegalArgumentException("ERR-CMS-CONTENT-400: slug 필수");
        }
        if (mapper.countBySlug(c.getSlug(), c.getLocale(), null) > 0) {
            throw new RuntimeException("ERR-CMS-CONTENT-409: 동일 locale 내 slug 중복");
        }
        c.setStatus("DRAFT");
        c.setVersion(1);
        c.setCreatedBy(actor);
        c.setUpdatedBy(actor);
        mapper.insert(c);
        snapshot(c, "created");
        auditService.log("CONTENT_CREATE", c.getSlug(), "type=" + c.getContentType());
        return mapper.findById(c.getId());
    }

    @Transactional
    public CmsContent update(Long id, Map<String, Object> req, String actor) {
        CmsContent c = get(id);
        if ("PUBLISHED".equals(c.getStatus())) {
            // 발행본 직접 수정 시 DRAFT로 되돌려 재워크플로우 (콘텐츠 무결성)
            c.setStatus("DRAFT");
        }
        bind(c, req);
        if (c.getSlug() != null && mapper.countBySlug(c.getSlug(), c.getLocale(), id) > 0) {
            throw new RuntimeException("ERR-CMS-CONTENT-409: 동일 locale 내 slug 중복");
        }
        c.setVersion(c.getVersion() == null ? 1 : c.getVersion() + 1);
        c.setUpdatedBy(actor);
        mapper.update(c);
        snapshot(c, "updated");
        auditService.log("CONTENT_UPDATE", c.getSlug(), "v" + c.getVersion());
        return mapper.findById(id);
    }

    public void delete(Long id) {
        CmsContent c = get(id);
        mapper.delete(id);
        auditService.log("CONTENT_DELETE", c.getSlug(), "type=" + c.getContentType());
    }

    /**
     * 게시 워크플로우 전이.
     *
     * @param action submit(→REVIEW) / approve(→APPROVED) / publish(→PUBLISHED)
     *               / reject(→DRAFT) / archive(→ARCHIVED) / schedule(예약→APPROVED+scheduledAt)
     */
    @Transactional
    public CmsContent transition(Long id, String action, String note, LocalDateTime scheduledAt,
                                 String actor, String role) {
        CmsContent c = get(id);
        String from = c.getStatus();
        String to = switch (action == null ? "" : action.toLowerCase()) {
            case "submit" -> requireFrom(from, Set.of("DRAFT"), "REVIEW");
            case "approve" -> { requireEditor(role); yield requireFrom(from, Set.of("REVIEW"), "APPROVED"); }
            case "publish" -> { requireEditor(role); yield requireFrom(from, Set.of("APPROVED", "REVIEW"), "PUBLISHED"); }
            case "reject" -> { requireEditor(role); yield requireFrom(from, Set.of("REVIEW", "APPROVED"), "DRAFT"); }
            case "archive" -> { requireEditor(role); yield "ARCHIVED"; }
            case "schedule" -> {
                requireEditor(role);
                if (scheduledAt == null) throw new IllegalArgumentException("ERR-CMS-CONTENT-400: scheduledAt 필수");
                c.setScheduledAt(scheduledAt);
                yield "APPROVED";
            }
            default -> throw new IllegalArgumentException("ERR-CMS-CONTENT-400: 알 수 없는 action");
        };
        mapper.updateStatus(id, to, actor);
        if ("PUBLISHED".equals(to)) {
            mapper.updatePublished(id, to);
        }
        if ("schedule".equalsIgnoreCase(action)) {
            mapper.update(c);  // scheduledAt 반영
        }
        CmsContent updated = mapper.findById(id);
        snapshot(updated, "transition " + from + "->" + to + (note == null ? "" : " : " + note));
        auditService.log("CONTENT_" + (action == null ? "" : action.toUpperCase()), c.getSlug(), from + " -> " + to);
        return updated;
    }

    /** 특정 버전으로 롤백 — 해당 스냅샷 본문으로 새 버전 생성, 상태 DRAFT. */
    @Transactional
    public CmsContent rollback(Long id, Integer version, String actor) {
        CmsContent c = get(id);
        CmsContentVersion v = mapper.findVersion(id, version);
        if (v == null) {
            throw new RuntimeException("ERR-CMS-CONTENT-404: 버전 없음");
        }
        c.setTitle(v.getTitle());
        c.setSummary(v.getSummary());
        c.setBlocks(v.getBlocks());
        c.setStatus("DRAFT");
        c.setVersion(c.getVersion() + 1);
        c.setUpdatedBy(actor);
        mapper.update(c);
        snapshot(c, "rollback to v" + version);
        auditService.log("CONTENT_ROLLBACK", c.getSlug(), "to v" + version);
        return mapper.findById(id);
    }

    public List<CmsContentVersion> versions(Long id) {
        return mapper.findVersions(id);
    }

    /** 예약 게시 스케줄러 — 1분마다 예약 시각 도래한 APPROVED 콘텐츠를 PUBLISHED 처리. */
    @Scheduled(fixedDelay = 60000)
    public void publishDue() {
        try {
            List<CmsContent> due = mapper.findDueScheduled();
            for (CmsContent c : due) {
                mapper.updatePublished(c.getId(), "PUBLISHED");
                mapper.updateStatus(c.getId(), "PUBLISHED", "scheduler");
                auditService.log("scheduler", "CONTENT_SCHEDULED_PUBLISH", c.getSlug(), "scheduledAt 도래");
                log.info("예약 게시 발행: {}", c.getSlug());
            }
        } catch (Exception e) {
            log.warn("예약 게시 스케줄러 오류: {}", e.getMessage());
        }
    }

    // ── helpers ──
    private void requireEditor(String role) {
        if (role == null || !EDITOR_ROLES.contains(role.toUpperCase())) {
            throw new RuntimeException("ERR-CMS-403: 게시 승인/발행은 EDITOR 이상만 가능합니다");
        }
    }

    private String requireFrom(String from, Set<String> allowed, String to) {
        if (!allowed.contains(from)) {
            throw new RuntimeException("ERR-CMS-CONTENT-409: 현재 상태(" + from + ")에서 전이 불가");
        }
        return to;
    }

    private void snapshot(CmsContent c, String note) {
        CmsContentVersion v = new CmsContentVersion();
        v.setContentId(c.getId());
        v.setVersion(c.getVersion());
        v.setTitle(c.getTitle());
        v.setSummary(c.getSummary());
        v.setBlocks(c.getBlocks());
        v.setStatus(c.getStatus());
        v.setNote(note);
        v.setCreatedBy(c.getUpdatedBy() == null ? c.getCreatedBy() : c.getUpdatedBy());
        mapper.insertVersion(v);
    }

    private void bind(CmsContent c, Map<String, Object> r) {
        if (r.containsKey("contentType")) c.setContentType(str(r.get("contentType")));
        if (r.containsKey("slug")) c.setSlug(str(r.get("slug")));
        if (r.containsKey("title")) c.setTitle(str(r.get("title")));
        if (r.containsKey("summary")) c.setSummary(str(r.get("summary")));
        if (r.containsKey("blocks")) c.setBlocks(jsonStr(r.get("blocks")));
        if (r.containsKey("locale")) c.setLocale(str(r.get("locale")));
        if (r.containsKey("menuId")) c.setMenuId(longOf(r.get("menuId")));
        if (r.containsKey("tags")) c.setTags(str(r.get("tags")));
        if (r.containsKey("seoMeta")) c.setSeoMeta(jsonStr(r.get("seoMeta")));
        if (c.getLocale() == null) c.setLocale("ko");
    }

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

    /** Map/List 는 JSON 문자열로 직렬화(간단 toString — 실제 JSONB 컬럼). */
    private String jsonStr(Object o) {
        if (o == null) return null;
        if (o instanceof String s) return s;
        try {
            return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(o);
        } catch (Exception e) {
            return String.valueOf(o);
        }
    }

    private Long longOf(Object o) {
        if (o == null) return null;
        try { return Long.valueOf(String.valueOf(o).trim()); } catch (Exception e) { return null; }
    }
}
