package com.jeesharp.business.modules.coupon.application;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.jeesharp.business.modules.coupon.api.CouponCheckoutApi;
import com.jeesharp.business.modules.coupon.dto.CouponCheckoutContextDTO;
import com.jeesharp.business.modules.coupon.dto.CouponFreezeCommandDTO;
import com.jeesharp.business.modules.coupon.dto.MemberCouponCancelDTO;
import com.jeesharp.business.modules.coupon.dto.MemberCouponGrantDTO;
import com.jeesharp.business.modules.coupon.dto.MemberCouponOfflineConfirmDTO;
import com.jeesharp.business.modules.coupon.enums.CouponSceneCodeEnum;
import com.jeesharp.business.modules.coupon.entity.CouponTemplate;
import com.jeesharp.business.modules.member.entity.Member;
import com.jeesharp.business.modules.coupon.entity.MemberCoupon;
import com.jeesharp.business.modules.coupon.enums.CouponDiscountTypeEnum;
import com.jeesharp.business.modules.coupon.enums.CouponGetTypeEnum;
import com.jeesharp.business.modules.coupon.enums.CouponTemplateStatusEnum;
import com.jeesharp.business.modules.coupon.enums.CouponTemplateTypeEnum;
import com.jeesharp.business.modules.coupon.enums.CouponUseScopeEnum;
import com.jeesharp.business.modules.coupon.enums.CouponValidityTypeEnum;
import com.jeesharp.business.modules.coupon.enums.MemberCouponUseStatusEnum;
import com.jeesharp.business.modules.coupon.service.CouponTemplateService;
import com.jeesharp.business.modules.coupon.service.MemberCouponService;
import com.jeesharp.business.modules.member.api.MemberPointsApi;
import com.jeesharp.business.modules.member.dto.MemberPointsDeductDTO;
import com.jeesharp.business.modules.member.enums.MemberPointsSceneEnum;
import com.jeesharp.business.modules.member.service.MemberService;
import com.jeesharp.business.modules.coupon.vo.CouponCheckoutListVO;
import com.jeesharp.business.modules.coupon.vo.CouponDemoVO;
import com.jeesharp.business.modules.coupon.vo.CouponTemplateQueryVO;
import com.jeesharp.business.modules.coupon.vo.MemberCouponInfoVO;
import com.jeesharp.business.modules.coupon.vo.MemberCouponListVO;
import com.jeesharp.business.modules.coupon.vo.MemberCouponQueryVO;
import com.jeesharp.framework.exception.BizErrorException;
import com.jeesharp.framework.pojo.Page;
import com.jeesharp.framework.pojo.query.PageRespVO;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 会员优惠券应用服务：领券、卡包、发放/作废；结账试算委托 {@link CouponCheckoutApi}。
 *
 * <p><b>职责划分</b></p>
 * <ul>
 *   <li>本类：模板领取、积分兑换、管理端发放、会员券 CRUD、领券中心展示</li>
 *   <li>{@link CouponCheckoutApi}：下单试算、冻结、核销、释放、退款退回</li>
 * </ul>
 *
 * @author jeesharp
 * @since 2026-05-07
 */
@Service
@AllArgsConstructor
@Transactional(readOnly = true)
public class MemberCouponApplicationService {

    private static final DateTimeFormatter DEMO_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final String GET_SOURCE_MINI = "MINI";
    private static final String GET_SOURCE_ADMIN = "ADMIN";

    private final CouponTemplateService couponTemplateService;
    private final MemberCouponService memberCouponService;
    private final MemberService memberService;
    private final MemberPointsApi memberPointsApi;
    private final CouponCheckoutApi couponCheckoutApi;

    public PageRespVO<CouponDemoVO> pageTemplateDemos(String memberId, CouponTemplateQueryVO pageParam) {
        CouponTemplateQueryVO q = new CouponTemplateQueryVO();
        if (pageParam != null) {
            q.setPageNo(pageParam.getPageNo());
            q.setPageSize(pageParam.getPageSize());
            q.setCouponName(pageParam.getCouponName());
            q.setCouponCode(pageParam.getCouponCode());
            q.setCouponType(pageParam.getCouponType());
        }
        q.setStatus(CouponTemplateStatusEnum.IN_PROGRESS.getValue());
        q.setOrderBy("create_date desc");
        Page<CouponTemplate> page = new Page<>(q);
        Page<CouponTemplate> res = couponTemplateService.findPage(page, q);
        List<CouponDemoVO> demos = new ArrayList<>();
        for (CouponTemplate t : res.getRecords()) {
            if (!"0".equals(t.getDelFlag()) || !inReceiveWindow(t)) {
                continue;
            }
            long received = StrUtil.isBlank(memberId) ? 0L : memberCouponService.countByMemberAndTemplate(memberId, t.getId());
            CouponDemoVO demo = toDemoFromTemplate(t, memberId, received);
            enrichTemplateDemoWalletCode(demo, memberId, t.getId(), received);
            demos.add(demo);
        }
        PageRespVO<CouponDemoVO> out = new PageRespVO<>();
        out.setTotal(res.getTotal());
        out.setPageNo(res.getPageNo());
        out.setPageSize(res.getPageSize());
        out.setRecords(demos);
        return out;
    }

    public List<CouponDemoVO> listPublishedTemplatesForDemo(String memberId) {
        CouponTemplateQueryVO q = new CouponTemplateQueryVO();
        q.setStatus(CouponTemplateStatusEnum.IN_PROGRESS.getValue());
        q.setOrderBy("create_date desc");
        List<CouponTemplate> list = couponTemplateService.findList(q);
        List<CouponDemoVO> out = new ArrayList<>();
        for (CouponTemplate t : list) {
            if (!"0".equals(t.getDelFlag())) {
                continue;
            }
            if (!inReceiveWindow(t)) {
                continue;
            }
            long received = StrUtil.isBlank(memberId) ? 0L : memberCouponService.countByMemberAndTemplate(memberId, t.getId());
            CouponDemoVO demo = toDemoFromTemplate(t, memberId, received);
            enrichTemplateDemoWalletCode(demo, memberId, t.getId(), received);
            out.add(demo);
        }
        return out;
    }

    public CouponDemoVO getTemplateDemo(String templateId, String memberId) {
        CouponTemplate t = couponTemplateService.get(templateId);
        if (t == null || !"0".equals(t.getDelFlag())) {
            throw new BizErrorException("优惠券不存在");
        }
        long received = StrUtil.isBlank(memberId) ? 0L : memberCouponService.countByMemberAndTemplate(memberId, t.getId());
        CouponDemoVO vo = toDemoFromTemplate(t, memberId, received);
        enrichTemplateDemoWalletCode(vo, memberId, t.getId(), received);
        return vo;
    }

    public CouponDemoVO getMemberCouponDemo(String memberCouponId, String memberId) {
        memberCouponService.markExpiredIfPastEnd(memberId);
        MemberCoupon c = memberCouponService.get(memberCouponId);
        if (c == null || !"0".equals(c.getDelFlag()) || !memberId.equals(c.getMemberId())) {
            throw new BizErrorException("优惠券不存在");
        }
        return toDemoFromMemberCoupon(c);
    }

    public PageRespVO<MemberCouponListVO> pageMyCoupons(String memberId, Page<MemberCoupon> page, MemberCouponQueryVO query) {
        memberCouponService.markExpiredIfPastEnd(memberId);
        if (query == null) {
            query = new MemberCouponQueryVO();
        }
        query.setMemberId(memberId);
        Page<MemberCoupon> result = memberCouponService.findPage(page, query);
        PageRespVO<MemberCouponListVO> resp = new PageRespVO<>();
        resp.setTotal(result.getTotal());
        resp.setPageNo(result.getPageNo());
        resp.setPageSize(result.getPageSize());
        List<MemberCouponListVO> rows = result.getRecords().stream().map(this::toListVo).collect(Collectors.toList());
        resp.setRecords(rows);
        return resp;
    }

    public MemberCouponInfoVO getMemberCouponInfo(String memberId, String couponId) {
        memberCouponService.markExpiredIfPastEnd(memberId);
        MemberCoupon c = memberCouponService.get(couponId);
        if (c == null || !"0".equals(c.getDelFlag()) || !memberId.equals(c.getMemberId())) {
            throw new BizErrorException("优惠券不存在");
        }
        return toInfoVo(c);
    }

    @Transactional(readOnly = false)
    public Map<String, Object> receiveManual(String memberId, String templateId) {
        return issueCouponInternal(memberId, templateId, CouponGetTypeEnum.MANUAL.getValue(), GET_SOURCE_MINI, null, false, null);
    }

    @Transactional(readOnly = false)
    public Map<String, Object> exchangeByPoints(String memberId, String templateId) {
        CouponTemplate t = loadTemplateOrThrow(templateId);
        if (!CouponTemplateStatusEnum.IN_PROGRESS.getValue().equals(t.getStatus())) {
            throw new BizErrorException("优惠券不可领取");
        }
        if (!inReceiveWindow(t)) {
            throw new BizErrorException("不在领取时间内");
        }
        if (!CouponGetTypeEnum.EXCHANGE.getValue().equals(t.getGetType())) {
            throw new BizErrorException("该券不支持积分兑换");
        }
        if (t.getExchangePoints() == null || t.getExchangePoints() <= 0) {
            throw new BizErrorException("兑换所需积分未配置");
        }
        memberPointsApi.deduct(MemberPointsDeductDTO.builder()
                .memberId(memberId)
                .sceneCode(MemberPointsSceneEnum.COUPON_EXCHANGE.getCode())
                .bizId(memberId + ":" + templateId)
                .points(t.getExchangePoints())
                .bizDesc("兑换优惠券")
                .operateSource("MINI")
                .build());
        return issueCouponInternal(memberId, templateId, CouponGetTypeEnum.EXCHANGE.getValue(), GET_SOURCE_MINI, t.getExchangePoints(), false, null);
    }

    @Transactional(readOnly = false)
    public Map<String, Object> grantByAdmin(MemberCouponGrantDTO dto) {
        int ok = 0;
        int fail = 0;
        for (String memberKey : dto.getMemberIds()) {
            try {
                String memberId = resolveMemberIdOrThrow(memberKey);
                issueCouponInternal(memberId, dto.getTemplateId(), CouponGetTypeEnum.ACTIVITY.getValue(), GET_SOURCE_ADMIN, null, true, dto.getRemarks());
                ok++;
            } catch (Exception ex) {
                fail++;
            }
        }
        Map<String, Object> data = new HashMap<>();
        data.put("successCount", ok);
        data.put("failCount", fail);
        return data;
    }

    @Transactional(readOnly = false)
    public void cancelByAdmin(MemberCouponCancelDTO dto) {
        MemberCoupon c = findAdminByIdOrThrow(dto.getId());
        if (MemberCouponUseStatusEnum.USED.getValue().equals(c.getStatus())) {
            throw new BizErrorException("已使用的券不可作废");
        }
        c.setStatus(MemberCouponUseStatusEnum.VOID.getValue());
        c.setRemarks(dto.getRemarks());
        memberCouponService.update(c);
    }

    /**
     * 订单可用券列表（兼容旧参数：仅金额，不按场景/范围过滤）。
     *
     * @see CouponCheckoutApplicationService#listForCheckout
     */
    public Map<String, Object> usableForOrder(String memberId, BigDecimal orderAmount) {
        return couponCheckoutApi.listForCheckoutLegacyMap(memberId, orderAmount);
    }

    /**
     * 按结账上下文列出可用/不可用券（推荐：含 sceneCode、商品项等）。
     */
    public CouponCheckoutListVO usableForCheckout(CouponCheckoutContextDTO context) {
        return couponCheckoutApi.listForCheckout(context);
    }

    /**
     * @deprecated 请使用 {@link CouponCheckoutApplicationService#resolveDiscount} 并传入完整结账上下文
     */
    @Deprecated
    public BigDecimal resolveMallCouponDiscount(String memberId, String memberCouponId, BigDecimal orderAmount) {
        CouponCheckoutContextDTO context = new CouponCheckoutContextDTO();
        context.setMemberId(memberId);
        context.setOrderAmount(orderAmount);
        context.setSceneCode(CouponSceneCodeEnum.MALL.getCode());
        return couponCheckoutApi.resolveDiscount(context, memberCouponId);
    }

    /**
     * @deprecated 请使用 {@link CouponCheckoutApplicationService#freezeForOrder}
     */
    @Deprecated
    @Transactional(readOnly = false)
    public void freezeForMallOrder(String memberId, String memberCouponId, String orderId, String orderNo,
                                   BigDecimal orderAmount, BigDecimal discount) {
        CouponCheckoutContextDTO context = new CouponCheckoutContextDTO();
        context.setMemberId(memberId);
        context.setOrderAmount(orderAmount);
        context.setSceneCode(CouponSceneCodeEnum.MALL.getCode());
        CouponFreezeCommandDTO command = new CouponFreezeCommandDTO();
        command.setMemberId(memberId);
        command.setMemberCouponId(memberCouponId);
        command.setOrderId(orderId);
        command.setOrderNo(orderNo);
        command.setCheckoutContext(context);
        command.setExpectedDiscount(discount);
        command.setFreezeReason("商城订单待支付");
        couponCheckoutApi.freezeForOrder(command);
    }

    /**
     * 商城订单支付成功：将冻结券置为已使用。
     */
    @Transactional(readOnly = false)
    public void confirmMallOrderCouponPaid(String orderId) {
        couponCheckoutApi.confirmOrderPaid(orderId);
    }

    public PageRespVO<MemberCouponListVO> pageAdmin(MemberCouponQueryVO query, Page<MemberCoupon> page) {
        MemberCouponQueryVO q = prepareAdminQuery(query);
        if (q == null) {
            return emptyAdminPage(page);
        }
        Page<MemberCoupon> result = memberCouponService.findPage(page, q);
        PageRespVO<MemberCouponListVO> resp = new PageRespVO<>();
        resp.setTotal(result.getTotal());
        resp.setPageNo(result.getPageNo());
        resp.setPageSize(result.getPageSize());
        List<MemberCouponListVO> rows = result.getRecords().stream().map(this::toListVo).collect(Collectors.toList());
        memberService.enrichMemberDisplay(rows);
        resp.setRecords(rows);
        return resp;
    }

    public MemberCouponInfoVO getAdminInfo(String id) {
        MemberCoupon c = findAdminByIdOrThrow(id);
        MemberCouponInfoVO vo = toInfoVo(c);
        memberService.enrichMemberDisplay(vo);
        return vo;
    }

    public PageRespVO<CouponDemoVO> pageStoreOfflineTemplates(String memberId, String tenantId, CouponTemplateQueryVO pageParam) {
        if (StrUtil.isBlank(tenantId)) {
            throw new BizErrorException("店铺租户不能为空");
        }
        CouponTemplateQueryVO q = new CouponTemplateQueryVO();
        if (pageParam != null) {
            q.setPageNo(pageParam.getPageNo());
            q.setPageSize(pageParam.getPageSize());
            q.setCouponName(pageParam.getCouponName());
            q.setCouponCode(pageParam.getCouponCode());
            q.setCouponType(pageParam.getCouponType());
        }
        q.setStatus(CouponTemplateStatusEnum.IN_PROGRESS.getValue());
        q.setSceneCode(CouponSceneCodeEnum.STORE_OFFLINE.getCode());
        q.setTenantId(tenantId);
        q.setOrderBy("create_date desc");
        Page<CouponTemplate> page = new Page<>(q);
        Page<CouponTemplate> res = couponTemplateService.findPage(page, q);
        List<CouponDemoVO> demos = new ArrayList<>();
        for (CouponTemplate t : res.getRecords()) {
            if (!"0".equals(t.getDelFlag()) || !inReceiveWindow(t)) {
                continue;
            }
            if (!StrUtil.equals(tenantId, t.getTenantId())) {
                continue;
            }
            long received = StrUtil.isBlank(memberId) ? 0L : memberCouponService.countByMemberAndTemplate(memberId, t.getId());
            CouponDemoVO demo = toDemoFromTemplate(t, memberId, received);
            enrichTemplateDemoWalletCode(demo, memberId, t.getId(), received);
            demos.add(demo);
        }
        PageRespVO<CouponDemoVO> out = new PageRespVO<>();
        out.setTotal(res.getTotal());
        out.setPageNo(res.getPageNo());
        out.setPageSize(res.getPageSize());
        out.setRecords(demos);
        return out;
    }

    @Transactional(readOnly = false)
    public Map<String, Object> receiveStoreOfflineCoupon(String memberId, String tenantId, String templateId) {
        CouponTemplate t = loadTemplateOrThrow(templateId);
        assertStoreOfflineTemplate(t, tenantId);
        return issueCouponInternal(memberId, templateId, CouponGetTypeEnum.MANUAL.getValue(), GET_SOURCE_MINI, null, false, null);
    }

    public PageRespVO<MemberCouponListVO> pageStoreOfflineMyCoupons(String memberId, String tenantId, Page<MemberCoupon> page,
                                                                    MemberCouponQueryVO query) {
        if (StrUtil.isBlank(tenantId)) {
            throw new BizErrorException("店铺租户不能为空");
        }
        memberCouponService.markExpiredIfPastEnd(memberId);
        MemberCouponQueryVO q = query != null ? query : new MemberCouponQueryVO();
        q.setMemberId(memberId);
        q.setTenantId(tenantId);
        q.setSceneCode(CouponSceneCodeEnum.STORE_OFFLINE.getCode());
        Page<MemberCoupon> result = memberCouponService.findPage(page, q);
        PageRespVO<MemberCouponListVO> resp = new PageRespVO<>();
        resp.setTotal(result.getTotal());
        resp.setPageNo(result.getPageNo());
        resp.setPageSize(result.getPageSize());
        resp.setRecords(result.getRecords().stream().map(this::toListVo).collect(Collectors.toList()));
        return resp;
    }

    public MemberCouponInfoVO getStoreOfflineMemberCouponInfo(String memberId, String tenantId, String memberCouponId) {
        MemberCouponInfoVO vo = getMemberCouponInfo(memberId, memberCouponId);
        assertStoreOfflineMemberCoupon(vo.getSceneCode(), tenantId, memberCouponId, memberId);
        return vo;
    }

    public CouponDemoVO getStoreOfflineMemberCouponDemo(String memberId, String tenantId, String memberCouponId) {
        memberCouponService.markExpiredIfPastEnd(memberId);
        MemberCoupon c = memberCouponService.get(memberCouponId);
        if (c == null || !"0".equals(c.getDelFlag()) || !memberId.equals(c.getMemberId())) {
            throw new BizErrorException("优惠券不存在");
        }
        assertStoreOfflineMemberCoupon(c.getSceneCode(), tenantId, memberCouponId, memberId);
        if (!StrUtil.equals(tenantId, c.getTenantId())) {
            throw new BizErrorException("优惠券不属于当前店铺");
        }
        return toDemoFromMemberCoupon(c);
    }

    /**
     * 店铺线下券核销确认（幂等：同一 verifyToken 重复调用返回成功）。
     */
    @Transactional(readOnly = false)
    public void offlineConfirmUsed(MemberCouponOfflineConfirmDTO dto) {
        if (dto == null || StrUtil.isBlank(dto.getMemberCouponId())) {
            throw new BizErrorException("会员券ID不能为空");
        }
        MemberCoupon coupon = memberCouponService.get(dto.getMemberCouponId());
        if (coupon == null || !"0".equals(coupon.getDelFlag())) {
            throw new BizErrorException("优惠券不存在");
        }
        if (StrUtil.isNotBlank(dto.getMemberId()) && !StrUtil.equals(dto.getMemberId(), coupon.getMemberId())) {
            throw new BizErrorException("优惠券不属于该会员");
        }
        if (StrUtil.isNotBlank(dto.getTenantId()) && !StrUtil.equals(dto.getTenantId(), coupon.getTenantId())) {
            throw new BizErrorException("优惠券不属于该店铺");
        }
        if (!CouponSceneCodeEnum.STORE_OFFLINE.getCode().equalsIgnoreCase(StrUtil.blankToDefault(coupon.getSceneCode(), ""))) {
            throw new BizErrorException("不是店铺线下券");
        }
        String verifyToken = StrUtil.blankToDefault(dto.getVerifyToken(), "").trim();
        if (MemberCouponUseStatusEnum.USED.getValue().equals(coupon.getStatus())) {
            if (StrUtil.isNotBlank(verifyToken) && verifyToken.equals(coupon.getUseOrderNo())) {
                return;
            }
            throw new BizErrorException("优惠券已使用");
        }
        if (!MemberCouponUseStatusEnum.UNUSED.getValue().equals(coupon.getStatus())) {
            throw new BizErrorException("优惠券状态不可核销");
        }
        memberCouponService.markExpiredIfPastEnd(coupon.getMemberId());
        coupon = memberCouponService.get(dto.getMemberCouponId());
        if (MemberCouponUseStatusEnum.EXPIRED.getValue().equals(coupon.getStatus())) {
            throw new BizErrorException("优惠券已过期");
        }
        LocalDateTime now = LocalDateTime.now();
        if (coupon.getValidEndDate() != null && now.isAfter(coupon.getValidEndDate())) {
            throw new BizErrorException("优惠券已过期");
        }
        if (coupon.getValidStartDate() != null && now.isBefore(coupon.getValidStartDate())) {
            throw new BizErrorException("优惠券尚未生效");
        }
        coupon.setStatus(MemberCouponUseStatusEnum.USED.getValue());
        coupon.setUseDate(now);
        coupon.setUseOrderNo(verifyToken);
        coupon.setUseOrderId("OFFLINE_" + verifyToken);
        memberCouponService.update(coupon);
        if (StrUtil.isNotBlank(coupon.getTemplateId())) {
            couponTemplateService.increaseUsedCount(coupon.getTemplateId());
        }
    }

    private void assertStoreOfflineTemplate(CouponTemplate t, String tenantId) {
        if (!CouponSceneCodeEnum.STORE_OFFLINE.getCode().equalsIgnoreCase(StrUtil.blankToDefault(t.getSceneCode(), ""))) {
            throw new BizErrorException("不是店铺线下券");
        }
        if (!StrUtil.equals(tenantId, t.getTenantId())) {
            throw new BizErrorException("优惠券不属于当前店铺");
        }
    }

    private void assertStoreOfflineMemberCoupon(String sceneCode, String tenantId, String memberCouponId, String memberId) {
        if (!CouponSceneCodeEnum.STORE_OFFLINE.getCode().equalsIgnoreCase(StrUtil.blankToDefault(sceneCode, ""))) {
            throw new BizErrorException("不是店铺线下券");
        }
        MemberCoupon c = memberCouponService.get(memberCouponId);
        if (c == null || !memberId.equals(c.getMemberId()) || !StrUtil.equals(tenantId, c.getTenantId())) {
            throw new BizErrorException("优惠券不存在或不属于当前店铺");
        }
    }

    // ==================== 内部 ====================

    /**
     * 管理端按主键查单条：与列表共用 {@code findList} + {@code dataScope}，避免 {@code get(id)} 与分页列表租户/数据权限不一致。
     */
    private MemberCoupon findAdminByIdOrThrow(String id) {
        if (StrUtil.isBlank(id)) {
            throw new BizErrorException("优惠券不存在");
        }
        MemberCouponQueryVO q = new MemberCouponQueryVO();
        q.setId(id.trim());
        List<MemberCoupon> rows = memberCouponService.findList(q);
        if (CollUtil.isEmpty(rows)) {
            throw new BizErrorException("优惠券不存在");
        }
        return rows.get(0);
    }

    /**
     * 管理端列表查询：会员关键词 → memberIdIn。
     */
    private MemberCouponQueryVO prepareAdminQuery(MemberCouponQueryVO query) {
        MemberCouponQueryVO q = query != null ? query : new MemberCouponQueryVO();
        if (StrUtil.isBlank(q.getMemberKeyword())) {
            return q;
        }
        List<String> memberIds = memberService.findIdsByKeyword(q.getMemberKeyword().trim());
        q.setMemberKeyword(null);
        if (CollUtil.isEmpty(memberIds)) {
            return null;
        }
        q.setMemberIdIn(memberIds);
        return q;
    }

    private PageRespVO<MemberCouponListVO> emptyAdminPage(Page<MemberCoupon> page) {
        PageRespVO<MemberCouponListVO> empty = new PageRespVO<>();
        empty.setRecords(Collections.emptyList());
        empty.setTotal(0L);
        empty.setPageNo(page.getPageNo());
        empty.setPageSize(page.getPageSize());
        empty.setPages(0L);
        return empty;
    }

    /**
     * 将运营填写的会员编号、手机号等解析为会员主键（不限当前登录租户）。
     */
    private String resolveMemberIdOrThrow(String memberKey) {
        if (StrUtil.isBlank(memberKey)) {
            throw new BizErrorException("会员信息不能为空");
        }
        String trimmed = memberKey.trim();
        List<String> ids = memberService.findIdsByKeywordForGrant(trimmed);
        if (CollUtil.isEmpty(ids)) {
            throw new BizErrorException("未找到会员：" + trimmed);
        }
        if (ids.size() > 1) {
            throw new BizErrorException("会员信息不唯一，请使用会员编号或手机号：" + trimmed);
        }
        return ids.get(0);
    }

    private CouponTemplate loadTemplateOrThrow(String templateId) {
        CouponTemplate t = couponTemplateService.get(templateId);
        if (t == null || !"0".equals(t.getDelFlag())) {
            throw new BizErrorException("优惠券模板不存在");
        }
        return t;
    }

    private Map<String, Object> issueCouponInternal(String memberId, String templateId, int memberGetType, String getSource,
                                                    Integer exchangePoints, boolean adminGrant, String grantRemark) {
        CouponTemplate t = loadTemplateOrThrow(templateId);
        if (!CouponTemplateStatusEnum.IN_PROGRESS.getValue().equals(t.getStatus())) {
            throw new BizErrorException("优惠券不可领取");
        }
        if (!adminGrant && !inReceiveWindow(t)) {
            throw new BizErrorException("不在领取时间内");
        }
        if (!adminGrant && CouponGetTypeEnum.MANUAL.getValue().equals(memberGetType)) {
            if (!CouponGetTypeEnum.MANUAL.getValue().equals(t.getGetType())) {
                throw new BizErrorException("该券不支持手动领取");
            }
        }
        long owned = memberCouponService.countByMemberAndTemplate(memberId, templateId);
        if (t.getPerMemberLimit() != null && owned >= t.getPerMemberLimit()) {
            throw new BizErrorException("已达到每人限领数量");
        }
        if (!couponTemplateService.increaseIssuedIfQuotaAllows(templateId)) {
            throw new BizErrorException("券已领完");
        }
        MemberCoupon mc = buildMemberCouponEntity(memberId, t, memberGetType, getSource, exchangePoints, grantRemark);
        mc.preInsert();
        memberCouponService.save(mc);
        Map<String, Object> data = new HashMap<>();
        data.put("couponId", mc.getId());
        data.put("couponSn", mc.getCouponSn());
        data.put("couponName", mc.getCouponName());
        data.put("validEndDate", mc.getValidEndDate() != null ? DEMO_DATE.format(mc.getValidEndDate().toLocalDate()) : "");
        return data;
    }

    private boolean inReceiveWindow(CouponTemplate t) {
        LocalDateTime now = LocalDateTime.now();
        if (t.getGetStartDate() != null && now.isBefore(t.getGetStartDate())) {
            return false;
        }
        if (t.getGetEndDate() != null && now.isAfter(t.getGetEndDate())) {
            return false;
        }
        return true;
    }

    private MemberCoupon buildMemberCouponEntity(String memberId, CouponTemplate t, int getType, String getSource,
                                                 Integer exchangePoints, String grantRemark) {
        Member m = memberService.get(memberId);
        if (m == null) {
            throw new BizErrorException("会员不存在");
        }
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime validStart;
        LocalDateTime validEnd;
        if (CouponValidityTypeEnum.FIXED_RANGE.getValue().equals(t.getValidityType())) {
            validStart = t.getValidStartDate() != null ? t.getValidStartDate() : now;
            validEnd = t.getValidEndDate() != null ? t.getValidEndDate() : now.plusDays(30).withHour(23).withMinute(59).withSecond(59);
        } else {
            int days = t.getValidDays() != null && t.getValidDays() > 0 ? t.getValidDays() : 30;
            validStart = now;
            validEnd = now.toLocalDate().plusDays(days).atTime(23, 59, 59);
        }
        MemberCoupon mc = new MemberCoupon();
        mc.setCouponSn("CPN" + RandomUtil.randomNumbers(16));
        mc.setMemberId(memberId);
        mc.setMemberNo(m.getMemberNo());
        mc.setMemberMobile(m.getMobile());
        mc.setTemplateId(t.getId());
        mc.setCouponCode(t.getCouponCode());
        mc.setCouponName(t.getCouponName());
        mc.setCouponType(t.getCouponType());
        mc.setDiscountType(t.getDiscountType());
        mc.setDiscountValue(t.getDiscountValue());
        mc.setMinConsumeAmount(t.getMinConsumeAmount() != null ? t.getMinConsumeAmount() : BigDecimal.ZERO);
        mc.setMaxDiscountAmount(t.getMaxDiscountAmount());
        mc.setSceneCode(t.getSceneCode());
        mc.setUseScope(t.getUseScope());
        mc.setScopeIds(t.getScopeIds());
        mc.setExcludeScopeIds(t.getExcludeScopeIds());
        mc.setGetType(getType);
        mc.setGetDate(now);
        mc.setGetSource(getSource);
        mc.setExchangePoints(exchangePoints);
        mc.setValidStartDate(validStart);
        mc.setValidEndDate(validEnd);
        mc.setStatus(MemberCouponUseStatusEnum.UNUSED.getValue());
        if (CouponSceneCodeEnum.STORE_OFFLINE.getCode().equalsIgnoreCase(StrUtil.blankToDefault(t.getSceneCode(), ""))) {
            mc.setTenantId(t.getTenantId());
            mc.setTenantCode(t.getTenantCode());
        } else {
            mc.setTenantId(m.getTenantId());
            mc.setTenantCode(m.getTenantCode());
        }
        // 关闭 preInsert 的租户自动填充，避免 C 端会员登录上下文（无后台租户）覆盖已写入的租户信息
        mc.closeTenantCodeAutoSet();
        if (StrUtil.isNotBlank(grantRemark)) {
            mc.setRemarks(grantRemark);
        }
        return mc;
    }

    private CouponDemoVO toDemoFromTemplate(CouponTemplate t, String memberId, long receivedCount) {
        CouponDemoVO vo = new CouponDemoVO();
        vo.setId(t.getId());
        vo.setName(t.getCouponName());
        vo.setSubtitle(StrUtil.blankToDefault(t.getDescription(), "文旅消费券"));
        vo.setFace(buildFace(t));
        vo.setCouponType(displayCouponType(t.getCouponType()));
        vo.setThreshold(buildThreshold(t));
        vo.setScope(buildScopeLabels(t));
        vo.setStatus(resolveTemplateDemoStatus(t, memberId, receivedCount));
        vo.setValidFrom(t.getValidStartDate() != null ? DEMO_DATE.format(t.getValidStartDate().toLocalDate()) : "");
        vo.setValidTo(t.getValidEndDate() != null ? DEMO_DATE.format(t.getValidEndDate().toLocalDate()) : "");
        if (CouponValidityTypeEnum.AFTER_RECEIVE_DAYS.getValue().equals(t.getValidityType())) {
            vo.setValidFrom("领取后生效");
            vo.setValidTo((t.getValidDays() != null ? t.getValidDays() : 30) + " 天内有效");
        }
        vo.setIssuePeriod(buildIssuePeriod(t));
        vo.setRemainQuota(remainQuota(t));
        vo.setRules(buildRules(t));
        vo.setStackRule(Integer.valueOf(1).equals(t.getCanOverlay())
                ? "可与部分优惠叠加，以门店规则为准。"
                : "单笔限用 1 张，不可与其他券叠加。");
        vo.setSceneCode(t.getSceneCode());
        vo.setVerifyTip(buildDemoVerifyTip(t.getSceneCode()));
        vo.setIssuer("平台发券");
        vo.setWalletCode(null);
        vo.setGetType(t.getGetType());
        vo.setGetTypeName(CouponGetTypeEnum.getDescByValue(t.getGetType()));
        vo.setExchangePoints(t.getExchangePoints());
        return vo;
    }

    private CouponDemoVO toDemoFromMemberCoupon(MemberCoupon c) {
        CouponTemplate t = couponTemplateService.get(c.getTemplateId());
        CouponDemoVO vo = new CouponDemoVO();
        vo.setId(c.getId());
        vo.setName(c.getCouponName());
        vo.setSubtitle(t != null ? StrUtil.blankToDefault(t.getDescription(), "") : "");
        vo.setFace(buildFace(c));
        vo.setCouponType(displayCouponType(c.getCouponType()));
        vo.setThreshold(buildThreshold(c));
        vo.setScope(buildDemoScopeLabels(c));
        vo.setStatus(displayMemberStatus(c));
        vo.setValidFrom(c.getValidStartDate() != null ? DEMO_DATE.format(c.getValidStartDate().toLocalDate()) : "");
        vo.setValidTo(c.getValidEndDate() != null ? DEMO_DATE.format(c.getValidEndDate().toLocalDate()) : "");
        vo.setIssuePeriod(null);
        vo.setRemainQuota(0);
        vo.setRules(t != null ? buildRules(t) : defaultRules());
        vo.setStackRule(t != null && Integer.valueOf(1).equals(t.getCanOverlay())
                ? "可与部分优惠叠加，以门店规则为准。"
                : "单笔限用 1 张，不可与其他券叠加。");
        vo.setSceneCode(StrUtil.blankToDefault(c.getSceneCode(), t != null ? t.getSceneCode() : null));
        vo.setVerifyTip(buildDemoVerifyTip(vo.getSceneCode()));
        vo.setIssuer("平台发券");
        vo.setWalletCode(resolveWalletCode(c));
        return vo;
    }

    private static String buildDemoVerifyTip(String sceneCode) {
        CouponSceneCodeEnum scene = CouponSceneCodeEnum.fromCode(sceneCode);
        if (scene != null && scene.isOnlinePayment()) {
            return "下单支付时选择本券抵扣（" + scene.getDesc() + "）。";
        }
        return "到店出示券码，由商户扫码或手工核销（线下使用）。";
    }

    /**
     * 模板详情：会员已领取时补全券码，便于 C 端在领券中心详情展示。
     */
    private void enrichTemplateDemoWalletCode(CouponDemoVO vo, String memberId, String templateId, long received) {
        if (vo == null || StrUtil.isBlank(memberId) || received <= 0) {
            return;
        }
        if (StrUtil.isNotBlank(vo.getWalletCode())) {
            return;
        }
        MemberCoupon owned = memberCouponService.findLatestByMemberAndTemplate(memberId, templateId);
        if (owned == null) {
            return;
        }
        vo.setWalletCode(resolveWalletCode(owned));
    }

    private static String resolveWalletCode(MemberCoupon c) {
        if (c == null) {
            return null;
        }
        if (StrUtil.isNotBlank(c.getCouponSn())) {
            return c.getCouponSn().trim();
        }
        return StrUtil.blankToDefault(c.getId(), null);
    }

    private String displayMemberStatus(MemberCoupon c) {
        if (MemberCouponUseStatusEnum.USED.getValue().equals(c.getStatus())) {
            return "已使用";
        }
        if (MemberCouponUseStatusEnum.EXPIRED.getValue().equals(c.getStatus())) {
            return "已过期";
        }
        if (MemberCouponUseStatusEnum.FROZEN.getValue().equals(c.getStatus())) {
            return "已领取";
        }
        if (MemberCouponUseStatusEnum.VOID.getValue().equals(c.getStatus())) {
            return "已过期";
        }
        LocalDateTime now = LocalDateTime.now();
        if (c.getValidEndDate() != null && c.getValidEndDate().isBefore(now)) {
            return "已过期";
        }
        return "已领取";
    }

    private String resolveTemplateDemoStatus(CouponTemplate t, String memberId, long receivedCount) {
        LocalDateTime now = LocalDateTime.now();
        if (t.getGetEndDate() != null && now.isAfter(t.getGetEndDate())) {
            return "已过期";
        }
        if (t.getPerMemberLimit() != null && receivedCount >= t.getPerMemberLimit()) {
            return "已领取";
        }
        if (remainQuota(t) == 0) {
            return "已过期";
        }
        return "可领取";
    }

    private int remainQuota(CouponTemplate t) {
        if (t.getTotalCount() != null && t.getTotalCount() == -1) {
            return 999999;
        }
        if (t.getTotalCount() == null || t.getIssuedCount() == null) {
            return 0;
        }
        int left = t.getTotalCount() - t.getIssuedCount();
        return Math.max(left, 0);
    }

    private String buildIssuePeriod(CouponTemplate t) {
        if (t.getGetStartDate() == null && t.getGetEndDate() == null) {
            return "详见活动规则";
        }
        String a = t.getGetStartDate() != null ? DEMO_DATE.format(t.getGetStartDate().toLocalDate()) : "";
        String b = t.getGetEndDate() != null ? DEMO_DATE.format(t.getGetEndDate().toLocalDate()) : "";
        return a + " ~ " + b;
    }

    private List<String> buildRules(CouponTemplate t) {
        if (StrUtil.isNotBlank(t.getDescription())) {
            String[] parts = t.getDescription().split("\n");
            List<String> list = new ArrayList<>();
            for (String p : parts) {
                if (StrUtil.isNotBlank(p)) {
                    list.add(p.trim());
                }
            }
            if (!list.isEmpty()) {
                return list;
            }
        }
        return defaultRules();
    }

    private List<String> defaultRules() {
        List<String> list = new ArrayList<>();
        list.add("请在有效期内使用，过期自动失效。");
        list.add("仅限本人账户使用，不可转让。");
        return list;
    }

    private List<String> buildScopeLabels(CouponTemplate t) {
        if (StrUtil.isNotBlank(t.getScopeIds())) {
            try {
                List<String> parsed = JSONUtil.toList(JSONUtil.parseArray(t.getScopeIds()), String.class);
                if (parsed != null && !parsed.isEmpty()) {
                    List<String> labels = parsed.stream()
                            .map(this::resolveScopeLabel)
                            .filter(StrUtil::isNotBlank)
                            .distinct()
                            .collect(Collectors.toList());
                    if (!labels.isEmpty()) {
                        return labels;
                    }
                }
            } catch (Exception ignored) {
                // fall through
            }
        }
        String fallback = CouponUseScopeEnum.getDescByValue(t.getUseScope());
        return Collections.singletonList(StrUtil.isNotBlank(fallback) ? fallback : CouponUseScopeEnum.ALL.getDesc());
    }

    /**
     * 将 scope_ids 中的占位符（如 ALL）或枚举名转为 C 端展示中文。
     */
    private String resolveScopeLabel(String token) {
        if (StrUtil.isBlank(token)) {
            return "";
        }
        String key = token.trim();
        if ("ALL".equalsIgnoreCase(key)) {
            return CouponUseScopeEnum.ALL.getDesc();
        }
        for (CouponUseScopeEnum e : CouponUseScopeEnum.values()) {
            if (e.name().equalsIgnoreCase(key)) {
                return e.getDesc();
            }
        }
        return key;
    }

    private String buildThreshold(CouponTemplate t) {
        BigDecimal min = t.getMinConsumeAmount() != null ? t.getMinConsumeAmount() : BigDecimal.ZERO;
        if (min.compareTo(BigDecimal.ZERO) <= 0) {
            return "无门槛";
        }
        return "满" + min.stripTrailingZeros().toPlainString() + "元可用";
    }

    private String buildThreshold(MemberCoupon c) {
        BigDecimal min = c.getMinConsumeAmount() != null ? c.getMinConsumeAmount() : BigDecimal.ZERO;
        if (min.compareTo(BigDecimal.ZERO) <= 0) {
            return "无门槛";
        }
        return "满" + min.stripTrailingZeros().toPlainString() + "元可用";
    }

    private String buildFace(CouponTemplate t) {
        if (CouponDiscountTypeEnum.RATE.getValue().equals(t.getDiscountType()) && t.getDiscountValue() != null) {
            return t.getDiscountValue().stripTrailingZeros().toPlainString() + "折";
        }
        if (t.getDiscountValue() != null) {
            return "¥" + t.getDiscountValue().setScale(0, RoundingMode.DOWN).toPlainString();
        }
        return "券";
    }

    private String buildFace(MemberCoupon c) {
        if (CouponDiscountTypeEnum.RATE.getValue().equals(c.getDiscountType()) && c.getDiscountValue() != null) {
            return c.getDiscountValue().stripTrailingZeros().toPlainString() + "折";
        }
        if (c.getDiscountValue() != null) {
            return "¥" + c.getDiscountValue().setScale(0, RoundingMode.DOWN).toPlainString();
        }
        return "券";
    }

    private String displayCouponType(Integer couponType) {
        String d = CouponTemplateTypeEnum.getDescByValue(couponType);
        return d.replace("券", "");
    }

    private MemberCouponListVO toListVo(MemberCoupon c) {
        MemberCouponListVO vo = new MemberCouponListVO();
        vo.setId(c.getId());
        vo.setCouponSn(c.getCouponSn());
        vo.setMemberId(c.getMemberId());
        vo.setMemberNo(c.getMemberNo());
        vo.setMemberMobile(c.getMemberMobile());
        vo.setTemplateId(c.getTemplateId());
        vo.setCouponCode(c.getCouponCode());
        vo.setCouponName(c.getCouponName());
        vo.setCouponType(c.getCouponType());
        vo.setCouponTypeName(CouponTemplateTypeEnum.getDescByValue(c.getCouponType()));
        vo.setDiscountType(c.getDiscountType());
        vo.setDiscountTypeName(CouponDiscountTypeEnum.getDescByValue(c.getDiscountType()));
        vo.setDiscountValue(c.getDiscountValue());
        vo.setMinConsumeAmount(c.getMinConsumeAmount());
        vo.setMaxDiscountAmount(c.getMaxDiscountAmount());
        vo.setGetType(c.getGetType());
        vo.setGetTypeName(CouponGetTypeEnum.getDescByValue(c.getGetType()));
        vo.setGetDate(c.getGetDate());
        vo.setGetSource(c.getGetSource());
        vo.setValidStartDate(c.getValidStartDate());
        vo.setValidEndDate(c.getValidEndDate());
        vo.setStatus(c.getStatus());
        vo.setStatusName(MemberCouponUseStatusEnum.getDescByValue(c.getStatus()));
        vo.setUseDate(c.getUseDate());
        vo.setUseOrderNo(c.getUseOrderNo());
        vo.setUseAmount(c.getUseAmount());
        enrichScopeFields(c, vo);
        return vo;
    }

    private MemberCouponInfoVO toInfoVo(MemberCoupon c) {
        MemberCouponInfoVO vo = new MemberCouponInfoVO();
        vo.setId(c.getId());
        vo.setCouponSn(c.getCouponSn());
        vo.setMemberId(c.getMemberId());
        vo.setMemberNo(c.getMemberNo());
        vo.setMemberMobile(c.getMemberMobile());
        vo.setTemplateId(c.getTemplateId());
        vo.setCouponCode(c.getCouponCode());
        vo.setCouponName(c.getCouponName());
        vo.setCouponType(c.getCouponType());
        vo.setCouponTypeName(CouponTemplateTypeEnum.getDescByValue(c.getCouponType()));
        vo.setDiscountType(c.getDiscountType());
        vo.setDiscountTypeName(CouponDiscountTypeEnum.getDescByValue(c.getDiscountType()));
        vo.setDiscountValue(c.getDiscountValue());
        vo.setMinConsumeAmount(c.getMinConsumeAmount());
        vo.setMaxDiscountAmount(c.getMaxDiscountAmount());
        vo.setGetType(c.getGetType());
        vo.setGetTypeName(CouponGetTypeEnum.getDescByValue(c.getGetType()));
        vo.setGetDate(c.getGetDate());
        vo.setGetSource(c.getGetSource());
        vo.setActivityId(c.getActivityId());
        vo.setExchangePoints(c.getExchangePoints());
        vo.setValidStartDate(c.getValidStartDate());
        vo.setValidEndDate(c.getValidEndDate());
        vo.setStatus(c.getStatus());
        vo.setStatusName(MemberCouponUseStatusEnum.getDescByValue(c.getStatus()));
        vo.setUseDate(c.getUseDate());
        vo.setUseOrderId(c.getUseOrderId());
        vo.setUseOrderNo(c.getUseOrderNo());
        vo.setUseAmount(c.getUseAmount());
        vo.setOrderAmount(c.getOrderAmount());
        vo.setReturnDate(c.getReturnDate());
        vo.setReturnReason(c.getReturnReason());
        vo.setIsReturned(c.getIsReturned());
        vo.setFreezeDate(c.getFreezeDate());
        vo.setFreezeReason(c.getFreezeReason());
        vo.setFreezeOrderId(c.getFreezeOrderId());
        vo.setCreateDate(c.getCreateDate());
        enrichScopeFields(c, vo);
        return vo;
    }

    private void enrichScopeFields(MemberCoupon c, MemberCouponListVO vo) {
        vo.setSceneCode(resolveSceneCode(c));
        vo.setSceneName(resolveSceneName(vo.getSceneCode()));
        vo.setUseScope(resolveUseScope(c));
        vo.setUseScopeName(buildUseScopeName(c));
    }

    private void enrichScopeFields(MemberCoupon c, MemberCouponInfoVO vo) {
        vo.setSceneCode(resolveSceneCode(c));
        vo.setSceneName(resolveSceneName(vo.getSceneCode()));
        vo.setUseScope(resolveUseScope(c));
        vo.setUseScopeName(buildUseScopeName(c));
    }

    private List<String> buildDemoScopeLabels(MemberCoupon c) {
        List<String> labels = new ArrayList<>();
        String sceneName = resolveSceneName(resolveSceneCode(c));
        if (StrUtil.isNotBlank(sceneName)) {
            labels.add(sceneName);
        }
        labels.addAll(buildScopeLabelsFromMemberCoupon(c));
        return labels.stream().filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList());
    }

    private String buildUseScopeName(MemberCoupon c) {
        List<String> labels = buildScopeLabelsFromMemberCoupon(c);
        if (labels.isEmpty()) {
            return CouponUseScopeEnum.ALL.getDesc();
        }
        return String.join("、", labels);
    }

    private List<String> buildScopeLabelsFromMemberCoupon(MemberCoupon c) {
        CouponTemplate effective = new CouponTemplate();
        effective.setUseScope(resolveUseScope(c));
        effective.setScopeIds(resolveScopeIds(c));
        effective.setExcludeScopeIds(resolveExcludeScopeIds(c));
        return buildScopeLabels(effective);
    }

    private Integer resolveUseScope(MemberCoupon c) {
        if (c.getUseScope() != null) {
            return c.getUseScope();
        }
        CouponTemplate t = loadTemplateForSnapshot(c);
        if (t != null && t.getUseScope() != null) {
            return t.getUseScope();
        }
        return CouponUseScopeEnum.ALL.getValue();
    }

    private String resolveScopeIds(MemberCoupon c) {
        if (StrUtil.isNotBlank(c.getScopeIds())) {
            return c.getScopeIds();
        }
        CouponTemplate t = loadTemplateForSnapshot(c);
        return t != null ? t.getScopeIds() : null;
    }

    private String resolveExcludeScopeIds(MemberCoupon c) {
        if (StrUtil.isNotBlank(c.getExcludeScopeIds())) {
            return c.getExcludeScopeIds();
        }
        CouponTemplate t = loadTemplateForSnapshot(c);
        return t != null ? t.getExcludeScopeIds() : null;
    }

    private String resolveSceneCode(MemberCoupon c) {
        if (StrUtil.isNotBlank(c.getSceneCode())) {
            return c.getSceneCode().trim();
        }
        CouponTemplate t = loadTemplateForSnapshot(c);
        return t != null && StrUtil.isNotBlank(t.getSceneCode()) ? t.getSceneCode().trim() : null;
    }

    private String resolveSceneName(String sceneCode) {
        if (StrUtil.isBlank(sceneCode)) {
            return null;
        }
        CouponSceneCodeEnum scene = CouponSceneCodeEnum.fromCode(sceneCode);
        return scene != null ? scene.getDesc() : sceneCode;
    }

    private CouponTemplate loadTemplateForSnapshot(MemberCoupon c) {
        if (c == null || StrUtil.isBlank(c.getTemplateId())) {
            return null;
        }
        return couponTemplateService.get(c.getTemplateId());
    }

}
