package com.liquidnet.service.goblin.service.impl.manage;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.liquidnet.commons.lang.util.CollectionUtil;
import com.liquidnet.commons.lang.util.IDGenerator;
import com.liquidnet.commons.lang.util.JsonUtils;
import com.liquidnet.service.base.PagedResult;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.base.SqlMapping;
import com.liquidnet.service.base.constant.MQConst;
import com.liquidnet.service.goblin.constant.GoblinArtistConst;
import com.liquidnet.service.goblin.constant.GoblinRedisConst;
import com.liquidnet.service.goblin.dto.manage.GoblinStoreMgtArtistApplyRevokeParams;
import com.liquidnet.service.goblin.dto.manage.GoblinStoreMgtArtistApplySubmitParams;
import com.liquidnet.service.goblin.dto.manage.GoblinStoreMgtArtistTopicCreateParams;
import com.liquidnet.service.goblin.dto.manage.GoblinStoreMgtArtistTopicDeleteParams;
import com.liquidnet.service.goblin.dto.manage.GoblinStoreMgtArtistTopicGoodsItemParams;
import com.liquidnet.service.goblin.dto.manage.GoblinStoreMgtArtistTopicStatusParams;
import com.liquidnet.service.goblin.dto.manage.GoblinStoreMgtArtistTopicUpdateParams;
import com.liquidnet.service.goblin.dto.manage.vo.GoblinStoreMgtArtistApplyListItemVo;
import com.liquidnet.service.goblin.dto.manage.vo.GoblinStoreMgtArtistAuthListVo;
import com.liquidnet.service.goblin.dto.manage.vo.GoblinStoreMgtArtistSearchItemVo;
import com.liquidnet.service.goblin.dto.manage.vo.GoblinStoreMgtArtistTopicDetailVo;
import com.liquidnet.service.goblin.dto.manage.vo.GoblinStoreMgtArtistTopicGoodsSearchItemVo;
import com.liquidnet.service.goblin.dto.manage.vo.GoblinStoreMgtArtistTopicPageVo;
import com.liquidnet.service.goblin.dto.vo.GoblinArtistAuthApplyVo;
import com.liquidnet.service.goblin.dto.vo.GoblinArtistTopicVo;
import com.liquidnet.service.goblin.dto.vo.GoblinGoodsInfoVo;
import com.liquidnet.service.goblin.dto.vo.GoblinStoreInfoVo;
import com.liquidnet.service.goblin.service.manage.IGoblinStoreMgtArtistService;
import com.liquidnet.service.goblin.util.GoblinRedisUtils;
import com.liquidnet.service.goblin.util.QueueUtils;
import com.liquidnet.service.kylin.entity.KylinArtist;
import com.liquidnet.service.kylin.mapper.KylinArtistMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Service
@Slf4j
public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistService {

    private static final long TOPIC_TTL = 86400L;

    @Autowired
    private GoblinRedisUtils redisUtils;
    @Autowired
    private MongoTemplate mongoTemplate;
    @Autowired
    private QueueUtils queueUtils;
    @Autowired
    private KylinArtistMapper kylinArtistMapper;

    private ResponseDto<GoblinStoreInfoVo> requireStore() {
        String uid = com.liquidnet.commons.lang.util.CurrentUtil.getCurrentUid();
        GoblinStoreInfoVo storeInfoVo = redisUtils.getStoreInfoVoByUid(uid);
        if (storeInfoVo == null || StringUtils.isBlank(storeInfoVo.getStoreId())) {
            return ResponseDto.failure("无法查看");
        }
        return ResponseDto.success(storeInfoVo);
    }

    // ======================== 艺人搜索 / 申请 ========================

    @Override
    public ResponseDto<PagedResult<GoblinStoreMgtArtistSearchItemVo>> searchArtist(String keyword, int page, int size) {
        ResponseDto<GoblinStoreInfoVo> storeResp = requireStore();
        if (!storeResp.isSuccess()) {
            return ResponseDto.failure(storeResp.getMessage());
        }

        LambdaQueryWrapper<KylinArtist> wrapper = new LambdaQueryWrapper<KylinArtist>()
                .eq(KylinArtist::getStatus, 1)
                .like(StringUtils.isNotBlank(keyword), KylinArtist::getArtistName, keyword)
                .orderByDesc(KylinArtist::getSort)
                .orderByDesc(KylinArtist::getCreatedAt);
        long total = kylinArtistMapper.selectCount(wrapper);
        wrapper.last("LIMIT " + ((page - 1) * size) + ", " + size);
        List<KylinArtist> pageList = total > 0 ? kylinArtistMapper.selectList(wrapper) : Collections.emptyList();

        PagedResult<GoblinStoreMgtArtistSearchItemVo> result = new PagedResult<>();
        result.setCurrentPage(page);
        result.setPageSize(size);
        result.setTotal(total, size);
        result.setList(pageList.stream().map(a -> {
            GoblinStoreMgtArtistSearchItemVo item = new GoblinStoreMgtArtistSearchItemVo();
            item.setArtistId(a.getArtistId());
            item.setArtistName(a.getArtistName());
            item.setAvatarUrl(a.getAvatarUrl());
            item.setArtistType(a.getArtistType());
            return item;
        }).collect(Collectors.toList()));
        return ResponseDto.success(result);
    }

    @Override
    public ResponseDto<String> submitApply(GoblinStoreMgtArtistApplySubmitParams params) {
        ResponseDto<GoblinStoreInfoVo> storeResp = requireStore();
        if (!storeResp.isSuccess()) {
            return ResponseDto.failure(storeResp.getMessage());
        }
        String storeId = storeResp.getData().getStoreId();

        KylinArtist artist = kylinArtistMapper.selectOne(new LambdaQueryWrapper<KylinArtist>()
                .eq(KylinArtist::getArtistId, params.getArtistId())
                .eq(KylinArtist::getStatus, 1)
                .last("limit 1"));
        if (artist == null) {
            return ResponseDto.failure("艺人不存在或已禁用");
        }

        long pendingCount = mongoTemplate.count(new Query(Criteria.where("storeId").is(storeId)
                .and("artistId").is(params.getArtistId())
                .and("status").is(GoblinArtistConst.APPLY_PENDING)),
                GoblinArtistAuthApplyVo.class);
        if (pendingCount > 0) {
            return ResponseDto.failure("该艺人已有待审核申请");
        }

        LocalDateTime now = LocalDateTime.now();
        String applyId = IDGenerator.nextSnowId();

        GoblinArtistAuthApplyVo applyVo = new GoblinArtistAuthApplyVo();
        applyVo.setApplyId(applyId);
        applyVo.setStoreId(storeId);
        applyVo.setArtistId(params.getArtistId());
        applyVo.setAttachmentUrls(JsonUtils.toJson(params.getAttachmentUrls()));
        applyVo.setStatus(GoblinArtistConst.APPLY_PENDING);
        applyVo.setCreatedAt(now);
        applyVo.setUpdatedAt(now);

        mongoTemplate.insert(applyVo, GoblinArtistAuthApplyVo.class.getSimpleName());

        LinkedList<String> sqls = CollectionUtil.linkedListString();
        sqls.add(SqlMapping.get("goblin_artist_auth_apply.insert"));
        LinkedList<Object[]> applyObjs = CollectionUtil.linkedListObjectArr();
        applyObjs.add(new Object[]{applyId, storeId, params.getArtistId(),
                applyVo.getAttachmentUrls(), GoblinArtistConst.APPLY_PENDING, now, now});
        queueUtils.sendMsgByRedis(MQConst.GoblinQueue.SQL_STORE.getKey(),
                SqlMapping.gets(sqls, applyObjs));

        return ResponseDto.success(applyId);
    }

    @Override
    public ResponseDto<PagedResult<GoblinStoreMgtArtistApplyListItemVo>> applyList(Integer status, int page, int size) {
        ResponseDto<GoblinStoreInfoVo> storeResp = requireStore();
        if (!storeResp.isSuccess()) {
            return ResponseDto.failure(storeResp.getMessage());
        }
        String storeId = storeResp.getData().getStoreId();

        List<GoblinArtistAuthApplyVo> all = loadAppliesByStore(storeId);
        if (status != null) {
            all = all.stream().filter(a -> Objects.equals(a.getStatus(), status))
                    .collect(Collectors.toList());
        }
        all.sort((a, b) -> {
            if (a.getCreatedAt() == null || b.getCreatedAt() == null) return 0;
            return b.getCreatedAt().compareTo(a.getCreatedAt());
        });

        Map<String, KylinArtist> artistMap = loadArtistMap(all.stream()
                .map(GoblinArtistAuthApplyVo::getArtistId).collect(Collectors.toSet()));

        List<GoblinStoreMgtArtistApplyListItemVo> list = new ArrayList<>();
        for (GoblinArtistAuthApplyVo apply : pageSlice(all, page, size)) {
            GoblinStoreMgtArtistApplyListItemVo item = new GoblinStoreMgtArtistApplyListItemVo();
            item.setApplyId(apply.getApplyId());
            item.setArtistId(apply.getArtistId());
            KylinArtist artist = artistMap.get(apply.getArtistId());
            if (artist != null) {
                item.setArtistName(artist.getArtistName());
                item.setAvatarUrl(artist.getAvatarUrl());
            }
            item.setAttachmentUrls(parseUrlList(apply.getAttachmentUrls()));
            item.setApplyTime(apply.getCreatedAt());
            item.setStatus(apply.getStatus());
            item.setReviewTime(apply.getReviewTime());
            item.setReviewerName(apply.getReviewerName());
            item.setAuthExpireAt(apply.getAuthExpireAt());
            item.setRejectReason(apply.getRejectReason());
            list.add(item);
        }

        PagedResult<GoblinStoreMgtArtistApplyListItemVo> result = new PagedResult<>();
        result.setCurrentPage(page);
        result.setPageSize(size);
        result.setTotal(all.size(), size);
        result.setList(list);
        return ResponseDto.success(result);
    }

    @Override
    public ResponseDto<Void> revokeApply(GoblinStoreMgtArtistApplyRevokeParams params) {
        ResponseDto<GoblinStoreInfoVo> storeResp = requireStore();
        if (!storeResp.isSuccess()) {
            return ResponseDto.failure(storeResp.getMessage());
        }
        String storeId = storeResp.getData().getStoreId();

        Query query = new Query(Criteria.where("applyId").is(params.getApplyId())
                .and("storeId").is(storeId));
        GoblinArtistAuthApplyVo apply = mongoTemplate.findOne(query, GoblinArtistAuthApplyVo.class,
                GoblinArtistAuthApplyVo.class.getSimpleName());
        if (apply == null) {
            return ResponseDto.failure("申请不存在");
        }
        if (!Objects.equals(apply.getStatus(), GoblinArtistConst.APPLY_PENDING)) {
            return ResponseDto.failure("仅待审核申请可撤销");
        }

        LocalDateTime now = LocalDateTime.now();
        mongoTemplate.updateFirst(query,
                new Update().set("status", GoblinArtistConst.APPLY_REVOKED).set("updatedAt", now),
                GoblinArtistAuthApplyVo.class.getSimpleName());

        LinkedList<String> sqls = CollectionUtil.linkedListString();
        sqls.add(SqlMapping.get("goblin_artist_auth_apply.revoke"));
        LinkedList<Object[]> sqlObjs = CollectionUtil.linkedListObjectArr();
        sqlObjs.add(new Object[]{now, params.getApplyId()});
        queueUtils.sendMsgByRedis(MQConst.GoblinQueue.SQL_STORE.getKey(),
                SqlMapping.gets(sqls, sqlObjs));

        return ResponseDto.success(null);
    }

    // ======================== 已授权艺人 ========================

    @Override
    public ResponseDto<GoblinStoreMgtArtistAuthListVo> authList() {
        ResponseDto<GoblinStoreInfoVo> storeResp = requireStore();
        if (!storeResp.isSuccess()) {
            return ResponseDto.failure(storeResp.getMessage());
        }
        String storeId = storeResp.getData().getStoreId();
        LocalDateTime now = LocalDateTime.now();

        List<GoblinArtistAuthApplyVo> all = loadAppliesByStore(storeId);
        List<GoblinArtistAuthApplyVo> approved = all.stream()
                .filter(a -> Objects.equals(a.getStatus(), GoblinArtistConst.APPLY_APPROVED)
                        && a.getAuthExpireAt() != null && a.getAuthExpireAt().isAfter(now))
                .collect(Collectors.toList());

        Map<String, GoblinArtistAuthApplyVo> authByArtist = new LinkedHashMap<>();
        for (GoblinArtistAuthApplyVo a : approved) {
            GoblinArtistAuthApplyVo exist = authByArtist.get(a.getArtistId());
            if (exist == null || a.getAuthExpireAt().isAfter(exist.getAuthExpireAt())) {
                authByArtist.put(a.getArtistId(), a);
            }
        }

        Map<String, KylinArtist> artistMap = loadArtistMap(authByArtist.keySet());

        GoblinStoreMgtArtistAuthListVo result = new GoblinStoreMgtArtistAuthListVo();
        for (GoblinArtistAuthApplyVo apply : authByArtist.values()) {
            GoblinStoreMgtArtistAuthListVo.Item item = new GoblinStoreMgtArtistAuthListVo.Item();
            item.setArtistId(apply.getArtistId());
            item.setExpireAt(apply.getAuthExpireAt());
            KylinArtist artist = artistMap.get(apply.getArtistId());
            if (artist != null) {
                item.setArtistName(artist.getArtistName());
                item.setAvatarUrl(artist.getAvatarUrl());
            }
            result.getList().add(item);
        }
        return ResponseDto.success(result);
    }

    // ======================== 专题列表 / 详情 ========================

    @Override
    public ResponseDto<GoblinStoreMgtArtistTopicPageVo> topicList(Integer status, int page, int size) {
        ResponseDto<GoblinStoreInfoVo> storeResp = requireStore();
        if (!storeResp.isSuccess()) {
            return ResponseDto.failure(storeResp.getMessage());
        }
        String storeId = storeResp.getData().getStoreId();

        List<GoblinArtistTopicVo> all = loadTopicsByStore(storeId);
        int countPending = 0, countOnline = 0, countOffline = 0;
        for (GoblinArtistTopicVo t : all) {
            if (Objects.equals(t.getStatus(), GoblinArtistConst.TOPIC_PENDING)) countPending++;
            else if (Objects.equals(t.getStatus(), GoblinArtistConst.TOPIC_ONLINE)) countOnline++;
            else if (Objects.equals(t.getStatus(), GoblinArtistConst.TOPIC_OFFLINE)) countOffline++;
        }

        if (status != null) {
            final Integer fs = status;
            all = all.stream().filter(t -> Objects.equals(t.getStatus(), fs))
                    .collect(Collectors.toList());
        }
        all.sort((a, b) -> {
            int sortCmp = Integer.compare(
                    a.getSort() == null ? 0 : a.getSort(),
                    b.getSort() == null ? 0 : b.getSort());
            if (sortCmp != 0) return sortCmp;
            if (a.getCreatedAt() == null || b.getCreatedAt() == null) return 0;
            return b.getCreatedAt().compareTo(a.getCreatedAt());
        });

        Map<String, KylinArtist> artistMap = loadArtistMap(all.stream()
                .map(GoblinArtistTopicVo::getArtistId)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toSet()));

        GoblinStoreMgtArtistTopicPageVo result = new GoblinStoreMgtArtistTopicPageVo();
        result.setTotal(all.size());
        result.setCountPending(countPending);
        result.setCountOnline(countOnline);
        result.setCountOffline(countOffline);
        for (GoblinArtistTopicVo topic : pageSlice(all, page, size)) {
            GoblinStoreMgtArtistTopicPageVo.Item item = new GoblinStoreMgtArtistTopicPageVo.Item();
            item.setTopicId(topic.getTopicId());
            item.setTopicName(topic.getTopicName());
            item.setArtistId(topic.getArtistId());
            item.setCustomAvatarUrl(topic.getCustomAvatarUrl());
            KylinArtist artist = artistMap.get(topic.getArtistId());
            if (artist != null) {
                item.setArtistName(artist.getArtistName());
                item.setAvatarUrl(artist.getAvatarUrl());
            } else {
                item.setAvatarUrl(topic.getCustomAvatarUrl());
            }
            item.setGoodsCount(topic.getGoods() != null ? topic.getGoods().size() : 0);
            item.setOnSaleStart(topic.getOnSaleStart());
            item.setOnSaleEnd(topic.getOnSaleEnd());
            item.setStatus(topic.getStatus());
            result.getList().add(item);
        }
        return ResponseDto.success(result);
    }

    @Override
    public ResponseDto<GoblinStoreMgtArtistTopicDetailVo> topicDetail(String topicId) {
        ResponseDto<GoblinStoreInfoVo> storeResp = requireStore();
        if (!storeResp.isSuccess()) {
            return ResponseDto.failure(storeResp.getMessage());
        }
        String storeId = storeResp.getData().getStoreId();

        GoblinArtistTopicVo topic = loadTopic(storeId, topicId);
        if (topic == null) {
            return ResponseDto.failure("专题不存在");
        }

        GoblinStoreMgtArtistTopicDetailVo vo = new GoblinStoreMgtArtistTopicDetailVo();
        vo.setTopicId(topic.getTopicId());
        vo.setTopicName(topic.getTopicName());
        vo.setArtistId(topic.getArtistId());
        vo.setCustomAvatarUrl(topic.getCustomAvatarUrl());
        vo.setTemplateType(topic.getTemplateType());
        vo.setBannerUrl(topic.getBannerUrl());
        vo.setBannerHeight(topic.getBannerHeight());
        vo.setOnSaleStart(topic.getOnSaleStart());
        vo.setOnSaleEnd(topic.getOnSaleEnd());
        vo.setStatus(topic.getStatus());
        if (StringUtils.isNotBlank(topic.getArtistId())) {
            KylinArtist artist = kylinArtistMapper.selectOne(new LambdaQueryWrapper<KylinArtist>()
                    .eq(KylinArtist::getArtistId, topic.getArtistId()).last("limit 1"));
            if (artist != null) {
                vo.setArtistName(artist.getArtistName());
                vo.setAvatarUrl(artist.getAvatarUrl());
            }
        } else {
            vo.setAvatarUrl(topic.getCustomAvatarUrl());
        }

        if (!CollectionUtils.isEmpty(topic.getGoods())) {
            Set<String> spuIds = topic.getGoods().stream()
                    .map(GoblinArtistTopicVo.GoodsItem::getSpuId)
                    .filter(StringUtils::isNotBlank)
                    .collect(Collectors.toSet());
            Map<String, GoblinGoodsInfoVo> goodsMap = loadGoodsMapBySpuIds(spuIds);
            for (GoblinArtistTopicVo.GoodsItem goods : topic.getGoods()) {
                GoblinStoreMgtArtistTopicDetailVo.GoodsItem item = new GoblinStoreMgtArtistTopicDetailVo.GoodsItem();
                item.setSpuId(goods.getSpuId());
                item.setSort(goods.getSort());
                item.setName(goods.getName());
                item.setCoverPic(goods.getCoverPic());
                GoblinGoodsInfoVo dbGoods = goodsMap.get(goods.getSpuId());
                if (dbGoods != null) {
                    item.setName(dbGoods.getName());
                    item.setCoverPic(dbGoods.getCoverPic());
                    item.setPriceGe(dbGoods.getPriceGe());
                    item.setPriceLe(dbGoods.getPriceLe());
                } else {
                    item.setPriceGe(goods.getPriceGe());
                    item.setPriceLe(goods.getPriceLe());
                }
                vo.getGoods().add(item);
            }
        }
        return ResponseDto.success(vo);
    }

    // ======================== 专题 创建 / 编辑 / 删除 / 上下架 ========================

    @Override
    public ResponseDto<String> topicCreate(GoblinStoreMgtArtistTopicCreateParams params) {
        ResponseDto<GoblinStoreInfoVo> storeResp = requireStore();
        if (!storeResp.isSuccess()) {
            return ResponseDto.failure(storeResp.getMessage());
        }
        String storeId = storeResp.getData().getStoreId();

        ResponseDto<Void> artistCheck = checkArtistAuthIfPresent(storeId, params.getArtistId());
        if (!artistCheck.isSuccess()) {
            return ResponseDto.failure(artistCheck.getMessage());
        }
        if (params.getTemplateType() == null
                || (params.getTemplateType() != GoblinArtistConst.TEMPLATE_GRID
                && params.getTemplateType() != GoblinArtistConst.TEMPLATE_LIST)) {
            return ResponseDto.failure("模版类型不正确");
        }

        long nameCount = mongoTemplate.count(
                new Query(Criteria.where("storeId").is(storeId)
                        .and("topicName").is(params.getTopicName())
                        .and("delFlg").is(GoblinArtistConst.DEL_NO)),
                GoblinArtistTopicVo.class.getSimpleName());
        if (nameCount > 0) {
            return ResponseDto.failure("专题名称已存在");
        }

        LocalDateTime now = LocalDateTime.now();
        String topicId = IDGenerator.nextSnowId();

        GoblinArtistTopicVo topicVo = new GoblinArtistTopicVo();
        topicVo.setTopicId(topicId);
        topicVo.setStoreId(storeId);
        topicVo.setTopicName(params.getTopicName());
        topicVo.setArtistId(StringUtils.trimToNull(params.getArtistId()));
        topicVo.setCustomAvatarUrl(resolveCustomAvatarUrl(params.getArtistId(), params.getCustomAvatarUrl()));
        topicVo.setTemplateType(params.getTemplateType());
        topicVo.setBannerUrl(params.getBannerUrl());
        topicVo.setBannerHeight(params.getBannerHeight() == null ? GoblinArtistConst.DEFAULT_BANNER_HEIGHT : params.getBannerHeight());
        topicVo.setOnSaleStart(params.getOnSaleStart());
        topicVo.setOnSaleEnd(params.getOnSaleEnd());
        topicVo.setStatus(GoblinArtistConst.TOPIC_PENDING);
        topicVo.setSort(0);
        topicVo.setRecommendPosition(0);
        topicVo.setTag(null);
        topicVo.setDelFlg(GoblinArtistConst.DEL_NO);
        topicVo.setCreatedAt(now);
        topicVo.setUpdatedAt(now);

        Map<String, GoblinGoodsInfoVo> goodsMap = loadGoodsMap(params.getGoods());
        List<GoblinArtistTopicVo.GoodsItem> goodsItems = buildGoodsItems(params.getGoods(), goodsMap);
        ResponseDto<Void> goodsCheck = checkGoodsBelong(storeId, goodsMap, params.getGoods());
        if (!goodsCheck.isSuccess()) {
            return ResponseDto.failure(goodsCheck.getMessage());
        }
        topicVo.setGoods(goodsItems);

        mongoTemplate.insert(topicVo, GoblinArtistTopicVo.class.getSimpleName());
        String topicKey = GoblinRedisConst.ARTIST_TOPIC + topicId;
        redisUtils.set(topicKey, topicVo, TOPIC_TTL);

        queueTopicInsertMysql(topicVo, now);

        return ResponseDto.success(topicId);
    }

    @Override
    public ResponseDto<Void> topicUpdate(GoblinStoreMgtArtistTopicUpdateParams params) {
        ResponseDto<GoblinStoreInfoVo> storeResp = requireStore();
        if (!storeResp.isSuccess()) {
            return ResponseDto.failure(storeResp.getMessage());
        }
        String storeId = storeResp.getData().getStoreId();

        GoblinArtistTopicVo existing = loadTopic(storeId, params.getTopicId());
        if (existing == null) {
            return ResponseDto.failure("专题不存在");
        }

        ResponseDto<Void> artistCheck = checkArtistAuthIfPresent(storeId, params.getArtistId());
        if (!artistCheck.isSuccess()) {
            return ResponseDto.failure(artistCheck.getMessage());
        }
        if (params.getTemplateType() == null
                || (params.getTemplateType() != GoblinArtistConst.TEMPLATE_GRID
                && params.getTemplateType() != GoblinArtistConst.TEMPLATE_LIST)) {
            return ResponseDto.failure("模版类型不正确");
        }

        if (!Objects.equals(params.getTopicName(), existing.getTopicName())) {
            long nameCount = mongoTemplate.count(
                    new Query(Criteria.where("storeId").is(storeId)
                            .and("topicName").is(params.getTopicName())
                            .and("delFlg").is(GoblinArtistConst.DEL_NO)
                            .and("topicId").ne(params.getTopicId())),
                    GoblinArtistTopicVo.class.getSimpleName());
            if (nameCount > 0) {
                return ResponseDto.failure("专题名称已存在");
            }
        }

        LocalDateTime now = LocalDateTime.now();
        Map<String, GoblinGoodsInfoVo> goodsMap = loadGoodsMap(params.getGoods());
        ResponseDto<Void> goodsCheck = checkGoodsBelong(storeId, goodsMap, params.getGoods());
        if (!goodsCheck.isSuccess()) {
            return ResponseDto.failure(goodsCheck.getMessage());
        }

        Query query = new Query(Criteria.where("topicId").is(params.getTopicId()).and("storeId").is(storeId));
        Update update = new Update();
        update.set("topicName", params.getTopicName());
        update.set("artistId", StringUtils.trimToNull(params.getArtistId()));
        update.set("customAvatarUrl", resolveCustomAvatarUrl(params.getArtistId(), params.getCustomAvatarUrl()));
        update.set("templateType", params.getTemplateType());
        update.set("bannerUrl", params.getBannerUrl());
        update.set("bannerHeight", params.getBannerHeight() == null ? GoblinArtistConst.DEFAULT_BANNER_HEIGHT : params.getBannerHeight());
        update.set("onSaleStart", params.getOnSaleStart());
        update.set("onSaleEnd", params.getOnSaleEnd());
        update.set("goods", buildGoodsItems(params.getGoods(), goodsMap));
        update.set("updatedAt", now);
        mongoTemplate.updateFirst(query, update, GoblinArtistTopicVo.class.getSimpleName());

        String topicKey = GoblinRedisConst.ARTIST_TOPIC + params.getTopicId();
        redisUtils.del(topicKey);

        queueTopicUpdateMysql(params.getTopicId(), params, now);

        return ResponseDto.success(null);
    }

    @Override
    public ResponseDto<Void> topicDelete(GoblinStoreMgtArtistTopicDeleteParams params) {
        ResponseDto<GoblinStoreInfoVo> storeResp = requireStore();
        if (!storeResp.isSuccess()) {
            return ResponseDto.failure(storeResp.getMessage());
        }
        String storeId = storeResp.getData().getStoreId();

        GoblinArtistTopicVo existing = loadTopic(storeId, params.getTopicId());
        if (existing == null) {
            return ResponseDto.failure("专题不存在");
        }

        LocalDateTime now = LocalDateTime.now();
        Query query = new Query(Criteria.where("topicId").is(params.getTopicId()).and("storeId").is(storeId));
        mongoTemplate.updateFirst(query,
                new Update().set("delFlg", GoblinArtistConst.DEL_YES).set("updatedAt", now),
                GoblinArtistTopicVo.class.getSimpleName());

        String topicKey = GoblinRedisConst.ARTIST_TOPIC + params.getTopicId();
        redisUtils.del(topicKey);

        LinkedList<String> sqls = CollectionUtil.linkedListString();
        sqls.add(SqlMapping.get("goblin_artist_topic.delete"));
        LinkedList<Object[]> sqlObjs = CollectionUtil.linkedListObjectArr();
        sqlObjs.add(new Object[]{now, params.getTopicId()});
        queueUtils.sendMsgByRedis(MQConst.GoblinQueue.SQL_STORE.getKey(),
                SqlMapping.gets(sqls, sqlObjs));

        return ResponseDto.success(null);
    }

    @Override
    public ResponseDto<Void> topicStatus(GoblinStoreMgtArtistTopicStatusParams params) {
        ResponseDto<GoblinStoreInfoVo> storeResp = requireStore();
        if (!storeResp.isSuccess()) {
            return ResponseDto.failure(storeResp.getMessage());
        }
        String storeId = storeResp.getData().getStoreId();

        GoblinArtistTopicVo existing = loadTopic(storeId, params.getTopicId());
        if (existing == null) {
            return ResponseDto.failure("专题不存在");
        }
        Integer status = params.getStatus();
        if (status == null || (status != GoblinArtistConst.TOPIC_PENDING
                && status != GoblinArtistConst.TOPIC_ONLINE
                && status != GoblinArtistConst.TOPIC_OFFLINE)) {
            return ResponseDto.failure("状态不正确");
        }

        LocalDateTime now = LocalDateTime.now();
        Query query = new Query(Criteria.where("topicId").is(params.getTopicId()).and("storeId").is(storeId));
        mongoTemplate.updateFirst(query,
                new Update().set("status", status).set("updatedAt", now),
                GoblinArtistTopicVo.class.getSimpleName());

        String topicKey = GoblinRedisConst.ARTIST_TOPIC + params.getTopicId();
        redisUtils.del(topicKey);

        LinkedList<String> sqls = CollectionUtil.linkedListString();
        sqls.add(SqlMapping.get("goblin_artist_topic.status"));
        LinkedList<Object[]> sqlObjs = CollectionUtil.linkedListObjectArr();
        sqlObjs.add(new Object[]{status, now, params.getTopicId()});
        queueUtils.sendMsgByRedis(MQConst.GoblinQueue.SQL_STORE.getKey(),
                SqlMapping.gets(sqls, sqlObjs));

        return ResponseDto.success(null);
    }

    // ======================== 搜索商品（不改 MySQL） ========================

    @Override
    public ResponseDto<PagedResult<GoblinStoreMgtArtistTopicGoodsSearchItemVo>> searchGoods(String keyword, int page, int size) {
        ResponseDto<GoblinStoreInfoVo> storeResp = requireStore();
        if (!storeResp.isSuccess()) {
            return ResponseDto.failure(storeResp.getMessage());
        }
        String storeId = storeResp.getData().getStoreId();

        Criteria criteria = Criteria.where("storeId").is(storeId)
                .and("delFlg").is("0")
                .and("marketId").exists(false)
                .orOperator(Criteria.where("spuType").exists(false), Criteria.where("spuType").is(0));
        if (StringUtils.isNotBlank(keyword)) {
            criteria.and("name").regex(Pattern.compile("^.*" + keyword + ".*$", Pattern.CASE_INSENSITIVE));
        }
        Query query = Query.query(criteria);

        long total = mongoTemplate.count(query, GoblinGoodsInfoVo.class.getSimpleName());
        PagedResult<GoblinStoreMgtArtistTopicGoodsSearchItemVo> result = new PagedResult<>();
        result.setCurrentPage(page);
        result.setPageSize(size);
        result.setTotal(total, size);
        if (total > 0) {
            query.with(PageRequest.of(page - 1, size));
            query.with(Sort.by(Sort.Order.desc("createdAt")));
            result.setList(mongoTemplate.find(query, GoblinStoreMgtArtistTopicGoodsSearchItemVo.class,
                    GoblinGoodsInfoVo.class.getSimpleName()));
        }
        return ResponseDto.success(result);
    }

    // ======================== 私有：校验 ========================

    private ResponseDto<Void> checkArtistAuthIfPresent(String storeId, String artistId) {
        if (StringUtils.isBlank(artistId)) {
            return ResponseDto.success(null);
        }
        LocalDateTime now = LocalDateTime.now();
        long count = mongoTemplate.count(
                new Query(Criteria.where("storeId").is(storeId)
                        .and("artistId").is(artistId)
                        .and("status").is(GoblinArtistConst.APPLY_APPROVED)
                        .and("authExpireAt").gt(now)),
                GoblinArtistAuthApplyVo.class.getSimpleName());
        if (count == 0) {
            return ResponseDto.failure("未获得该艺人有效授权");
        }
        return ResponseDto.success(null);
    }

    /** 有艺人时不存自定义头像 */
    private String resolveCustomAvatarUrl(String artistId, String customAvatarUrl) {
        if (StringUtils.isNotBlank(artistId)) {
            return null;
        }
        return StringUtils.trimToNull(customAvatarUrl);
    }

    private ResponseDto<Void> checkGoodsBelong(String storeId, Map<String, GoblinGoodsInfoVo> goodsMap,
                                               List<GoblinStoreMgtArtistTopicGoodsItemParams> paramsGoods) {
        if (CollectionUtils.isEmpty(paramsGoods)) {
            return ResponseDto.success(null);
        }
        for (GoblinStoreMgtArtistTopicGoodsItemParams item : paramsGoods) {
            if (StringUtils.isBlank(item.getSpuId())) continue;
            GoblinGoodsInfoVo goods = goodsMap.get(item.getSpuId());
            if (goods == null || !Objects.equals(goods.getStoreId(), storeId)
                    || !Objects.equals(goods.getDelFlg(), "0")) {
                return ResponseDto.failure("存在非本店或已删除商品");
            }
        }
        return ResponseDto.success(null);
    }

    // ======================== 私有：数据加载 ========================

    private List<GoblinArtistAuthApplyVo> loadAppliesByStore(String storeId) {
        List<GoblinArtistAuthApplyVo> applies = mongoTemplate.find(
                new Query(Criteria.where("storeId").is(storeId)),
                GoblinArtistAuthApplyVo.class, GoblinArtistAuthApplyVo.class.getSimpleName());
        return applies == null ? Collections.emptyList() : applies;
    }

    private List<GoblinArtistTopicVo> loadTopicsByStore(String storeId) {
        List<GoblinArtistTopicVo> topics = mongoTemplate.find(
                new Query(Criteria.where("storeId").is(storeId).and("delFlg").is(GoblinArtistConst.DEL_NO)),
                GoblinArtistTopicVo.class, GoblinArtistTopicVo.class.getSimpleName());
        return topics == null ? Collections.emptyList() : topics;
    }

    private GoblinArtistTopicVo loadTopic(String storeId, String topicId) {
        String redisKey = GoblinRedisConst.ARTIST_TOPIC + topicId;
        Object cached = redisUtils.get(redisKey);
        if (cached instanceof GoblinArtistTopicVo) {
            GoblinArtistTopicVo topic = (GoblinArtistTopicVo) cached;
            if (Objects.equals(topic.getDelFlg(), GoblinArtistConst.DEL_NO)) {
                return topic;
            }
            return null;
        }
        GoblinArtistTopicVo topic = mongoTemplate.findOne(
                new Query(Criteria.where("topicId").is(topicId).and("storeId").is(storeId)
                        .and("delFlg").is(GoblinArtistConst.DEL_NO)),
                GoblinArtistTopicVo.class, GoblinArtistTopicVo.class.getSimpleName());
        if (topic != null) {
            redisUtils.set(redisKey, topic, TOPIC_TTL);
        }
        return topic;
    }

    // ======================== 私有：商品 / 艺人 Map ========================

    private Map<String, GoblinGoodsInfoVo> loadGoodsMap(List<GoblinStoreMgtArtistTopicGoodsItemParams> goods) {
        if (CollectionUtils.isEmpty(goods)) {
            return Collections.emptyMap();
        }
        Set<String> spuIds = goods.stream().map(GoblinStoreMgtArtistTopicGoodsItemParams::getSpuId)
                .filter(StringUtils::isNotBlank).collect(Collectors.toSet());
        return loadGoodsMapBySpuIds(spuIds);
    }

    private Map<String, GoblinGoodsInfoVo> loadGoodsMapBySpuIds(Set<String> spuIds) {
        if (CollectionUtils.isEmpty(spuIds)) {
            return Collections.emptyMap();
        }
        List<GoblinGoodsInfoVo> list = mongoTemplate.find(
                new Query(Criteria.where("spuId").in(spuIds)),
                GoblinGoodsInfoVo.class, GoblinGoodsInfoVo.class.getSimpleName());
        Map<String, GoblinGoodsInfoVo> map = new HashMap<>();
        if (list != null) {
            for (GoblinGoodsInfoVo g : list) {
                if (g != null && StringUtils.isNotBlank(g.getSpuId())) {
                    map.put(g.getSpuId(), g);
                }
            }
        }
        return map;
    }

    private List<GoblinArtistTopicVo.GoodsItem> buildGoodsItems(
            List<GoblinStoreMgtArtistTopicGoodsItemParams> paramsGoods,
            Map<String, GoblinGoodsInfoVo> goodsMap) {
        List<GoblinArtistTopicVo.GoodsItem> items = new ArrayList<>();
        if (CollectionUtils.isEmpty(paramsGoods)) {
            return items;
        }
        int index = 0;
        for (GoblinStoreMgtArtistTopicGoodsItemParams p : paramsGoods) {
            if (StringUtils.isBlank(p.getSpuId())) continue;
            GoblinArtistTopicVo.GoodsItem item = new GoblinArtistTopicVo.GoodsItem();
            item.setSpuId(p.getSpuId());
            item.setSort(p.getSort() == null ? index : p.getSort());
            GoblinGoodsInfoVo goods = goodsMap.get(p.getSpuId());
            if (goods != null) {
                item.setName(goods.getName());
                item.setCoverPic(goods.getCoverPic());
                item.setPriceGe(goods.getPriceGe());
                item.setPriceLe(goods.getPriceLe());
            }
            items.add(item);
            index++;
        }
        return items;
    }

    private Map<String, KylinArtist> loadArtistMap(Set<String> artistIds) {
        if (CollectionUtils.isEmpty(artistIds)) {
            return Collections.emptyMap();
        }
        List<KylinArtist> list = kylinArtistMapper.selectList(new LambdaQueryWrapper<KylinArtist>()
                .in(KylinArtist::getArtistId, artistIds));
        Map<String, KylinArtist> map = new HashMap<>();
        for (KylinArtist artist : list) {
            map.put(artist.getArtistId(), artist);
        }
        return map;
    }

    // ======================== 私有：异步 MySQL ========================

    private void queueTopicInsertMysql(GoblinArtistTopicVo topicVo, LocalDateTime now) {
        LinkedList<String> sqls = CollectionUtil.linkedListString();
        sqls.add(SqlMapping.get("goblin_artist_topic.insert"));
        LinkedList<Object[]> topicObjs = CollectionUtil.linkedListObjectArr();
        topicObjs.add(new Object[]{
                topicVo.getTopicId(), topicVo.getStoreId(), topicVo.getTopicName(),
                topicVo.getArtistId(), topicVo.getCustomAvatarUrl(), topicVo.getTemplateType(),
                topicVo.getBannerUrl(), topicVo.getBannerHeight(),
                topicVo.getOnSaleStart(), topicVo.getOnSaleEnd(),
                topicVo.getStatus(), topicVo.getSort(), topicVo.getRecommendPosition(),
                topicVo.getDelFlg(), topicVo.getCreatedAt(), topicVo.getUpdatedAt()
        });

        LinkedList<Object[]> goodsObjs = CollectionUtil.linkedListObjectArr();
        if (!CollectionUtils.isEmpty(topicVo.getGoods())) {
            sqls.add(SqlMapping.get("goblin_artist_topic_goods.insert"));
            for (GoblinArtistTopicVo.GoodsItem goods : topicVo.getGoods()) {
                goodsObjs.add(new Object[]{
                        IDGenerator.nextSnowId(), topicVo.getTopicId(), goods.getSpuId(),
                        goods.getSort(), now, now
                });
            }
        }

        LinkedList<Object[]>[] args;
        if (goodsObjs.isEmpty()) {
            args = new LinkedList[]{topicObjs};
        } else {
            args = new LinkedList[]{topicObjs, goodsObjs};
        }
        queueUtils.sendMsgByRedis(MQConst.GoblinQueue.SQL_STORE.getKey(),
                SqlMapping.gets(sqls, args));
    }

    private void queueTopicUpdateMysql(String topicId, GoblinStoreMgtArtistTopicUpdateParams params, LocalDateTime now) {
        LinkedList<String> sqls = CollectionUtil.linkedListString();
        LinkedList<Object[]>[] argsArr;

        sqls.add(SqlMapping.get("goblin_artist_topic.update"));
        LinkedList<Object[]> topicObjs = CollectionUtil.linkedListObjectArr();
        topicObjs.add(new Object[]{
                params.getTopicName(), StringUtils.trimToNull(params.getArtistId()),
                resolveCustomAvatarUrl(params.getArtistId(), params.getCustomAvatarUrl()),
                params.getTemplateType(), params.getBannerUrl(),
                params.getBannerHeight() == null ? GoblinArtistConst.DEFAULT_BANNER_HEIGHT : params.getBannerHeight(),
                params.getOnSaleStart(), params.getOnSaleEnd(), now, topicId
        });

        sqls.add(SqlMapping.get("goblin_artist_topic_goods.delete_by_topic"));
        LinkedList<Object[]> delObjs = CollectionUtil.linkedListObjectArr();
        delObjs.add(new Object[]{topicId});

        LinkedList<Object[]> goodsObjs = CollectionUtil.linkedListObjectArr();
        if (!CollectionUtils.isEmpty(params.getGoods())) {
            sqls.add(SqlMapping.get("goblin_artist_topic_goods.insert"));
            for (GoblinStoreMgtArtistTopicGoodsItemParams goods : params.getGoods()) {
                if (StringUtils.isBlank(goods.getSpuId())) continue;
                goodsObjs.add(new Object[]{
                        IDGenerator.nextSnowId(), topicId, goods.getSpuId(),
                        goods.getSort() == null ? 0 : goods.getSort(), now, now
                });
            }
        }

        if (goodsObjs.isEmpty()) {
            argsArr = new LinkedList[]{topicObjs, delObjs};
        } else {
            argsArr = new LinkedList[]{topicObjs, delObjs, goodsObjs};
        }
        queueUtils.sendMsgByRedis(MQConst.GoblinQueue.SQL_STORE.getKey(),
                SqlMapping.gets(sqls, argsArr));
    }

    // ======================== 私有：工具 ========================

    private List<String> parseUrlList(String json) {
        if (StringUtils.isBlank(json)) {
            return Collections.emptyList();
        }
        List<String> list = JsonUtils.fromJson(json, new com.fasterxml.jackson.core.type.TypeReference<List<String>>() {
        });
        return list == null ? Collections.emptyList() : list;
    }

    private <T> List<T> pageSlice(List<T> all, int page, int size) {
        int safePage = Math.max(page, 1);
        int safeSize = Math.max(size, 1);
        int from = (safePage - 1) * safeSize;
        if (from >= all.size()) {
            return Collections.emptyList();
        }
        int to = Math.min(from + safeSize, all.size());
        return all.subList(from, to);
    }
}
