package com.rj.service.impl;

import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rj.dto.hxr.HxrUserRow;
import com.rj.entity.LbDailyUserTrade;
import com.rj.entity.LbDailyUserTradeReport;
import com.rj.entity.LbPurchaseApply;
import com.rj.util.IsoWorkdayUtils;
import com.rj.mapper.LbDailyUserTradeMapper;
import com.rj.mapper.LbDailyUserTradeReportMapper;
import com.rj.mapper.LbPurchaseApplyMapper;
import com.rj.service.HxrAdminUserService;
import com.rj.service.ILbPurchaseApplyService;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFColor;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;

import java.awt.Color;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

@Slf4j
@Service
public class LbPurchaseApplyServiceImpl
        extends ServiceImpl<LbPurchaseApplyMapper, LbPurchaseApply>
        implements ILbPurchaseApplyService {

    @Autowired
    private LbDailyUserTradeReportMapper lbDailyUserTradeReportMapper;

    @Autowired
    private LbDailyUserTradeMapper lbDailyUserTradeMapper;

    @Autowired
    private HxrAdminUserService hxrAdminUserService;

    @Autowired
    private ObjectMapper objectMapper;

    @Value("${langchain4j.open-ai.chat-model.model-name:qwen-plus}")
    private String dashScopeChatModel;

    private static final DateTimeFormatter TRY_DATE_MM_DD = DateTimeFormatter.ofPattern("MM.dd");
    private static final DateTimeFormatter PRIVILEGE_RANGE_FMT = DateTimeFormatter.ofPattern("MM.dd");
    private static final DateTimeFormatter EXPORT_FILENAME_TS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");

    /** 试用期包含的工作日天数（周一至周五），周六日不计入。 */
    private static final int TRIAL_WEEKDAY_COUNT = 3;

    /** 同事特权开通提醒中「特权开通日期」包含的工作日天数（与 syncHxrAdminColleagueVip 的 viptime 规则一致）。 */
    private static final int COLLEAGUE_PRIVILEGE_WEEKDAY_COUNT = 2;

    private static final String COLLEAGUE_VIP_GROUP_LABEL = "开通特权提醒";

    private static final String PARSE_SYSTEM_PROMPT = """
            你是信息抽取助手。用户会提供一段「进货申请」相关的自然语言订货信息。
            请只输出一个 JSON 对象，不要 markdown 代码块，不要解释性文字。
            字段均为可选；无法从原文推断的字段请省略或设为 null。
            tryDate 试用日期：若同时给出 applyDate，服务端会按 3 个工作日规则重算并覆盖本字段；模型可省略 tryDate。
            字段名与含义（JSON 键名必须完全一致）：
            applyUser 申请人姓名；
            applyPhone 申请人电话；
            tryDate 试用日期（可省略；有 applyDate 时由服务端按 3 个工作日重算）；
            age 年龄（整数）；
            workExperience 从业经历；
            colleagueName 同事姓名；
            colleaguePhone 同事电话；
            teamLeader 团队长；
            teamBigLeader 双收益团队长；
            applyDate 申请日期，格式 yyyy-MM-dd，如果年的信息为空，请用系统的当前年替换。
            不要输出 id、tenantId、createTime、updateTime。
            """;

    @Override
    public Map<String, Object> parseFromOrderInfoByLlm(String orderInfo, String tenantId) {
        Map<String, Object> result = new HashMap<>();
        try {
            if (orderInfo == null || orderInfo.trim().isEmpty()) {
                result.put("success", false);
                result.put("message", "订货信息不能为空");
                return result;
            }
            if (tenantId == null || tenantId.trim().isEmpty()) {
                result.put("success", false);
                result.put("message", "tenantId不能为空");
                return result;
            }

            String apiKey = System.getenv("DASHSCOPE_API_KEY");
            if (apiKey == null || apiKey.isEmpty()) {
                result.put("success", false);
                result.put("message", "未配置环境变量 DASHSCOPE_API_KEY，无法调用大模型");
                return result;
            }

            Generation gen = new Generation();
            Message systemMsg = Message.builder()
                    .role(Role.SYSTEM.getValue())
                    .content(PARSE_SYSTEM_PROMPT)
                    .build();
            Message userMsg = Message.builder()
                    .role(Role.USER.getValue())
                    .content("订货信息如下：\n" + orderInfo.trim())
                    .build();

            GenerationParam param = GenerationParam.builder()
                    .apiKey(apiKey)
                    .model(dashScopeChatModel)
                    .messages(Arrays.asList(systemMsg, userMsg))
                    .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                    .build();

            GenerationResult call = gen.call(param);
            String raw = call.getOutput().getChoices().get(0).getMessage().getContent();
            if (raw == null || raw.trim().isEmpty()) {
                result.put("success", false);
                result.put("message", "大模型返回内容为空");
                return result;
            }

            String json = normalizeJson(raw);
            LbPurchaseApply entity = objectMapper.readValue(json, LbPurchaseApply.class);
            log.debug(
                    "parseFromOrderInfoByLlm：LLM JSON 解析后 applyDate={} tryDate={}",
                    entity.getApplyDate(),
                    entity.getTryDate());

            entity.setTenantId(tenantId.trim());
            entity.setId(null);
            entity.setCreateTime(null);
            entity.setUpdateTime(null);

            fillTryDateFromApplyDate(entity);

            result.put("success", true);
            result.put("message", "解析成功");
            result.put("data", entity);
            return result;
        } catch (NoApiKeyException e) {
            log.error("DashScope API Key 异常", e);
            result.put("success", false);
            result.put("message", "API密钥异常：" + e.getMessage());
            return result;
        } catch (InputRequiredException e) {
            log.error("DashScope 入参异常", e);
            result.put("success", false);
            result.put("message", "调用大模型入参错误：" + e.getMessage());
            return result;
        } catch (Exception e) {
            log.error("订货信息大模型解析失败", e);
            result.put("success", false);
            result.put("message", "解析失败：" + e.getMessage());
            return result;
        }
    }

    /**
     * 试用日期：只要有 {@code applyDate}，一律按服务端规则计算（覆盖模型可能错填的 tryDate）。
     * 试用期为连续 {@link #TRIAL_WEEKDAY_COUNT} 个工作日（周一至周五），周六日为无效日不计入。
     * 展示起始为申请日 applyDate（日历日），结束日为从「申请日及之后首个工作日」起计的第 3 个工作日（含该首日）。
     * 若申请日年份不是当年，先按 {@link LocalDate#withYear(int)} 校正为当年同月同日（闰年 2 月 29 日在平年变为 2 月 28 日）。
     */
    private void fillTryDateFromApplyDate(LbPurchaseApply entity) {
        if (entity == null) {
            return;
        }
        LocalDate applyDate = entity.getApplyDate();
        if (applyDate == null) {
            log.info("试用日期：applyDate 为空，不覆盖，保留 LLM tryDate={}", entity.getTryDate());
            return;
        }
        int currentYear = LocalDate.now().getYear();
        if (applyDate.getYear() != currentYear) {
            applyDate = applyDate.withYear(currentYear);
            entity.setApplyDate(applyDate);
        }
        String existingTryDate = entity.getTryDate();
        LocalDate trialFirstWeekday = firstWeekdayOnOrAfter(applyDate);
        LocalDate endDate = endOfInclusiveWeekdaySpan(trialFirstWeekday, TRIAL_WEEKDAY_COUNT, log);
        
        String computed =
                applyDate.format(TRY_DATE_MM_DD) + " 到 " + endDate.format(TRY_DATE_MM_DD);
         
        entity.setTryDate(computed);
    }

    /** 申请日及之后第一个周一至周五（申请日本身为工作日时即为当天）。 */
    private static LocalDate firstWeekdayOnOrAfter(LocalDate d) {
        LocalDate x = d;
        while (IsoWorkdayUtils.isWeekend(x)) {
            x = x.plusDays(1);
        }
        return x;
    }

    /**
     * 从 {@code firstWeekday} 起连续 {@code inclusiveWeekdays} 个工作日（仅周一至周五，含起点当日），返回该段最后一天。
     * 若起点落在周六日，会先顺延到下一工作日再计数，且返回值保证不为周六日。
     */
    private static LocalDate endOfInclusiveWeekdaySpan(LocalDate firstWeekday, int inclusiveWeekdays, Logger logger) {
        if (inclusiveWeekdays < 1) {
            throw new IllegalArgumentException("inclusiveWeekdays must be >= 1");
        }
        LocalDate d = firstWeekday;
        while (IsoWorkdayUtils.isWeekend(d)) {
            if (logger != null && logger.isDebugEnabled()) {
                logger.debug(
                        "trialSpan：起点为周末，顺延 cursor={} dow={} isoDow={}",
                        d,
                        d.getDayOfWeek(),
                        IsoWorkdayUtils.isoDayOfWeek(d));
            }
            d = d.plusDays(1);
        }
        int weekdaysRemaining = inclusiveWeekdays;
        int guard = 0;
        while (true) {
            boolean weekend = IsoWorkdayUtils.isWeekend(d);
            if (logger != null && logger.isDebugEnabled()) {
                logger.debug(
                        "trialSpan：cursor={} dow={} isoDow={} weekend={} weekdaysRemaining={}",
                        d,
                        d.getDayOfWeek(),
                        IsoWorkdayUtils.isoDayOfWeek(d),
                        weekend,
                        weekdaysRemaining);
            }
            if (!weekend) {
                weekdaysRemaining--;
                if (weekdaysRemaining == 0) {
                    return d;
                }
            }
            d = d.plusDays(1);
            guard++;
            if (guard > 400) {
                throw new IllegalStateException(
                        "trialSpan：超过 400 步仍未结束，firstWeekday="
                                + firstWeekday
                                + " inclusiveWeekdays="
                                + inclusiveWeekdays);
            }
        }
    }

    private static String normalizeJson(String content) {
        if (content == null) {
            return "";
        }
        String trimmed = content.trim();
        if (trimmed.startsWith("```")) {
            int firstLineBreak = trimmed.indexOf('\n');
            int lastFence = trimmed.lastIndexOf("```");
            if (firstLineBreak >= 0 && lastFence > firstLineBreak) {
                trimmed = trimmed.substring(firstLineBreak + 1, lastFence).trim();
            }
        }
        return trimmed;
    }

    @Override
    public Map<String, Object> add(LbPurchaseApply entity) {
        Map<String, Object> result = new HashMap<>();
        try {
            if (entity.getTenantId() == null || entity.getTenantId().trim().isEmpty()) {
                result.put("success", false);
                result.put("message", "tenantId不能为空");
                return result;
            }
            if (entity.getApplyUser() == null || entity.getApplyUser().trim().isEmpty()) {
                result.put("success", false);
                result.put("message", "applyUser不能为空");
                return result;
            }
            if (entity.getApplyPhone() == null || entity.getApplyPhone().trim().isEmpty()) {
                result.put("success", false);
                result.put("message", "applyPhone不能为空");
                return result;
            }
            if (entity.getApplyDate() == null) {
                result.put("success", false);
                result.put("message", "applyDate不能为空");
                return result;
            }

            String tenantId = entity.getTenantId().trim();
            String phone = entity.getApplyPhone().trim();
            entity.setTenantId(tenantId);
            entity.setApplyPhone(phone);

            LocalDateTime now = LocalDateTime.now();
            LambdaQueryWrapper<LbPurchaseApply> phoneQuery = new LambdaQueryWrapper<>();
            phoneQuery.eq(LbPurchaseApply::getTenantId, tenantId)
                    .eq(LbPurchaseApply::getApplyPhone, phone)
                    .orderByDesc(LbPurchaseApply::getId)
                    .last("LIMIT 1");
            LbPurchaseApply existing = this.getOne(phoneQuery);

            boolean ok;
            if (existing != null) {
                entity.setId(existing.getId());
                entity.setCreateTime(existing.getCreateTime());
                entity.setUpdateTime(now);
                ok = this.updateById(entity);
                result.put("success", ok);
                result.put("message", ok ? "按申请人电话已更新" : "更新失败");
            } else {
                entity.setId(null);
                if (entity.getCreateTime() == null) {
                    entity.setCreateTime(now);
                }
                entity.setUpdateTime(now);
                ok = this.save(entity);
                result.put("success", ok);
                result.put("message", ok ? "新增成功" : "新增失败");
            }
            if (ok) {
                result.put("data", this.getById(entity.getId()));
            }
            return result;
        } catch (Exception e) {
            result.put("success", false);
            result.put("message", "新增异常：" + e.getMessage());
            return result;
        }
    }

    @Override
    public Map<String, Object> update(LbPurchaseApply entity) {
        Map<String, Object> result = new HashMap<>();
        try {
            if (entity.getId() == null) {
                result.put("success", false);
                result.put("message", "id不能为空");
                return result;
            }
            entity.setUpdateTime(LocalDateTime.now());
            boolean ok = this.updateById(entity);
            result.put("success", ok);
            result.put("message", ok ? "编辑成功" : "编辑失败");
            if (ok) {
                result.put("data", this.getById(entity.getId()));
            }
            return result;
        } catch (Exception e) {
            result.put("success", false);
            result.put("message", "编辑异常：" + e.getMessage());
            return result;
        }
    }

    @Override
    public Map<String, Object> deleteById(Long id) {
        Map<String, Object> result = new HashMap<>();
        try {
            boolean ok = this.removeById(id);
            result.put("success", ok);
            result.put("message", ok ? "删除成功" : "删除失败");
            return result;
        } catch (Exception e) {
            result.put("success", false);
            result.put("message", "删除异常：" + e.getMessage());
            return result;
        }
    }

    @Override
    public Map<String, Object> pageQuery(Integer current,
                                         Integer size,
                                         String tenantId,
                                         String applyUser,
                                         String applyPhone,
                                         String colleagueName,
                                         String teamLeader,
                                         String teamBigLeader,
                                         LocalDate applyDate) {
        Map<String, Object> result = new HashMap<>();
        try {
            if (current == null || current < 1) {
                current = 1;
            }
            if (size == null || size < 1) {
                size = 10;
            }

            LambdaQueryWrapper<LbPurchaseApply> queryWrapper = new LambdaQueryWrapper<>();
            if (tenantId != null && !tenantId.trim().isEmpty()) {
                queryWrapper.eq(LbPurchaseApply::getTenantId, tenantId.trim());
            }
            if (applyUser != null && !applyUser.trim().isEmpty()) {
                queryWrapper.like(LbPurchaseApply::getApplyUser, applyUser.trim());
            }
            if (applyPhone != null && !applyPhone.trim().isEmpty()) {
                queryWrapper.like(LbPurchaseApply::getApplyPhone, applyPhone.trim());
            }
            if (colleagueName != null && !colleagueName.trim().isEmpty()) {
                queryWrapper.like(LbPurchaseApply::getColleagueName, colleagueName.trim());
            }
            if (teamLeader != null && !teamLeader.trim().isEmpty()) {
                queryWrapper.like(LbPurchaseApply::getTeamLeader, teamLeader.trim());
            }
            if (teamBigLeader != null && !teamBigLeader.trim().isEmpty()) {
                queryWrapper.like(LbPurchaseApply::getTeamBigLeader, teamBigLeader.trim());
            }
            if (applyDate != null) {
                queryWrapper.eq(LbPurchaseApply::getApplyDate, applyDate);
            }
            queryWrapper.orderByDesc(LbPurchaseApply::getUpdateTime)
                    .orderByDesc(LbPurchaseApply::getCreateTime);

            Page<LbPurchaseApply> page = this.page(new Page<>(current, size), queryWrapper);
            result.put("success", true);
            result.put("message", "查询成功");
            result.put("data", page.getRecords());
            result.put("total", page.getTotal());
            result.put("current", page.getCurrent());
            result.put("size", page.getSize());
            result.put("pages", page.getPages());
            return result;
        } catch (Exception e) {
            result.put("success", false);
            result.put("message", "查询异常：" + e.getMessage());
            return result;
        }
    }

    @Override
    public Map<String, Object> listNeedPrivilegeRecommenders(LocalDate startDate, LocalDate endDate) {
        Map<String, Object> result = new HashMap<>();
        try {
            if (startDate == null || endDate == null) {
                result.put("success", false);
                result.put("message", "开始日期和结束日期不能为空");
                return result;
            }
            if (endDate.isBefore(startDate)) {
                result.put("success", false);
                result.put("message", "结束日期不能早于开始日期");
                return result;
            }

            LambdaQueryWrapper<LbPurchaseApply> purchaseQuery = new LambdaQueryWrapper<>();
            purchaseQuery.ge(LbPurchaseApply::getApplyDate, startDate)
                    .le(LbPurchaseApply::getApplyDate, endDate)
                    .orderByDesc(LbPurchaseApply::getApplyDate)
                    .orderByDesc(LbPurchaseApply::getApplyDate) ;
            List<LbPurchaseApply> purchaseInfo = this.list(purchaseQuery);

            if (purchaseInfo == null || purchaseInfo.isEmpty()) {
                result.put("success", true);
                result.put("message", "查询成功");
                result.put("data", List.of());
                result.put("total", 0);
                return result;
            }

            Set<String> applyUsers = new HashSet<>();
            for (LbPurchaseApply item : purchaseInfo) {
                if (item.getApplyUser() != null && !item.getApplyUser().trim().isEmpty()) {
                    applyUsers.add(item.getApplyUser().trim());
                }
            }
            if (applyUsers.isEmpty()) {
                result.put("success", true);
                result.put("message", "查询成功");
                result.put("data", List.of());
                result.put("total", 0);
                return result;
            }

            LambdaQueryWrapper<LbDailyUserTradeReport> reportQuery = new LambdaQueryWrapper<>();
            reportQuery.in(LbDailyUserTradeReport::getNickname, applyUsers)
                    .select(LbDailyUserTradeReport::getNickname);
            List<LbDailyUserTradeReport> matchedReports = lbDailyUserTradeReportMapper.selectList(reportQuery);

            Set<String> matchedNicknames = new HashSet<>();
            for (LbDailyUserTradeReport report : matchedReports) {
                if (report.getNickname() != null && !report.getNickname().trim().isEmpty()) {
                    matchedNicknames.add(report.getNickname().trim());
                }
            }

            List<LbPurchaseApply> needPrivilegeList = new ArrayList<>();
            for (LbPurchaseApply item : purchaseInfo) {
                String applyUser = item.getApplyUser() == null ? null : item.getApplyUser().trim();
                if (applyUser != null && !applyUser.isEmpty() && matchedNicknames.contains(applyUser)) {
                    needPrivilegeList.add(item);
                }
            }

            result.put("success", true);
            result.put("message", "查询成功");
            result.put("data", needPrivilegeList);
            result.put("total", needPrivilegeList.size());
            result.put("startDate", startDate);
            result.put("endDate", endDate);
            return result;
        } catch (Exception e) {
            result.put("success", false);
            result.put("message", "查询异常：" + e.getMessage());
            return result;
        }
    }

    @Override
    public Map<String, Object> syncHxrAdminUserVipByApplyDateRange(
            LocalDate applyDateStart, LocalDate applyDateEnd, String tenantId, Integer maxOrder) {
        Map<String, Object> result = new HashMap<>();
        try {
            if (applyDateStart == null || applyDateEnd == null) {
                result.put("success", false);
                result.put("message", "申请开始日期与结束日期不能为空");
                return result;
            }
            if (applyDateEnd.isBefore(applyDateStart)) {
                result.put("success", false);
                result.put("message", "结束日期不能早于开始日期");
                return result;
            }
            if (tenantId == null || tenantId.trim().isEmpty()) {
                result.put("success", false);
                result.put("message", "租户id不能为空");
                return result;
            }
            if (maxOrder == null) {
                result.put("success", false);
                result.put("message", "max_order不能为空");
                return result;
            }

            LambdaQueryWrapper<LbPurchaseApply> q = new LambdaQueryWrapper<>();
            q.eq(LbPurchaseApply::getTenantId, tenantId.trim())
                    .ge(LbPurchaseApply::getApplyDate, applyDateStart)
                    .le(LbPurchaseApply::getApplyDate, applyDateEnd);
            List<LbPurchaseApply> rows = this.list(q);
            if (rows == null || rows.isEmpty()) {
                result.put("success", true);
                result.put("message", "该条件下无申请记录，未调用第三方");
                result.put("totalRecords", 0);
                result.put("details", List.of());
                result.put("applyDateStart", applyDateStart);
                result.put("applyDateEnd", applyDateEnd);
                result.put("tenantId", tenantId.trim());
                return result;
            }

            List<Map<String, Object>> details = new ArrayList<>();
            int ok = 0;
            int fail = 0;
            for (LbPurchaseApply row : rows) {
                Map<String, Object> one = new HashMap<>();
                one.put("applyId", row.getId());
                one.put("applyDate", row.getApplyDate());
                String phone = row.getApplyPhone() != null ? row.getApplyPhone().trim() : "";
                one.put("applyPhone", phone);
                if (phone.isEmpty()) {
                    one.put("success", false);
                    one.put("message", "apply_phone 为空，跳过");
                    fail++;
                    details.add(one);
                    continue;
                }
                if (row.getApplyDate() == null) {
                    one.put("success", false);
                    one.put("message", "applyDate 为空，跳过");
                    fail++;
                    details.add(one);
                    continue;
                }
                try {
                    LocalDateTime vipTime = HxrAdminUserService.computeVipExpireTime(row.getApplyDate(), 2);
                    String vipTimeStr = HxrAdminUserService.formatVipTime(vipTime);
                    one.put("viptime", vipTimeStr);

                    Optional<HxrUserRow> userOpt = hxrAdminUserService.fetchFirstUserByMobile(phone);
                    if (userOpt.isEmpty()) {
                        one.put("success", false);
                        one.put("message", "user/select 无用户或接口失败");
                        fail++;
                        details.add(one);
                        continue;
                    }
                    long hxrUserId = userOpt.get().id();
                    one.put("hxrUserId", hxrUserId);
                    boolean updated =
                            hxrAdminUserService.updateUserVipFields(hxrUserId, maxOrder, vipTimeStr);
                    if (updated) {
                        one.put("success", true);
                        one.put("message", "user/update 成功");
                        ok++;
                    } else {
                        one.put("success", false);
                        one.put("message", "user/update 失败");
                        fail++;
                    }
                } catch (Exception ex) {
                    one.put("success", false);
                    one.put("message", "调用异常：" + ex.getMessage());
                    fail++;
                }
                details.add(one);
            }

            result.put("success", fail == 0);
            result.put("message", fail == 0 ? "全部处理成功" : "部分或全部失败，见 details");
            result.put("totalRecords", rows.size());
            result.put("successCount", ok);
            result.put("failCount", fail);
            result.put("maxOrder", maxOrder);
            result.put("details", details);
            result.put("applyDateStart", applyDateStart);
            result.put("applyDateEnd", applyDateEnd);
            result.put("tenantId", tenantId.trim());
            return result;
        } catch (Exception e) {
            result.put("success", false);
            result.put("message", "处理异常：" + e.getMessage());
            return result;
        }
    }

    @Override
    public Map<String, Object> syncHxrAdminColleagueVipByApplyDateRange(
            LocalDate applyDateStart, LocalDate applyDateEnd, String tenantId) {
        Map<String, Object> result = new HashMap<>();
        try {
            ColleagueVipContext ctx = resolveColleagueVipContext(applyDateStart, applyDateEnd, tenantId);
            if (ctx.validationError() != null) {
                result.put("success", false);
                result.put("message", ctx.validationError());
                return result;
            }
            List<LbPurchaseApply> rows = ctx.rows();
            if (rows.isEmpty()) {
                result.put("success", true);
                result.put("message", "该条件下无申请记录，未调用第三方");
                result.put("totalRecords", 0);
                result.put("details", List.of());
                result.put("applyDateStart", applyDateStart);
                result.put("applyDateEnd", applyDateEnd);
                result.put("tenantId", tenantId.trim());
                return result;
            }

            Set<String> tradeMatchedNicknames = ctx.tradeMatchedNicknames();
            List<Map<String, Object>> details = new ArrayList<>();
            int ok = 0;
            int fail = 0;
            for (LbPurchaseApply row : rows) {
                Map<String, Object> one = new HashMap<>();
                one.put("applyId", row.getId());
                one.put("applyDate", row.getApplyDate());
                String applyUserName = row.getApplyUser() != null ? row.getApplyUser().trim() : "";
                one.put("applyUserName", applyUserName);
                String phone = row.getColleaguePhone() != null ? row.getColleaguePhone().trim() : "";
                one.put("colleaguePhone", phone);
                String skipReason = colleagueVipSkipReason(row, tradeMatchedNicknames);
                if (skipReason != null) {
                    one.put("success", false);
                    one.put("message", skipReason);
                    fail++;
                    details.add(one);
                    continue;
                }
                try {
                    LocalDateTime vipTime = HxrAdminUserService.computeVipExpireTime(row.getApplyDate());
                    String vipTimeStr = HxrAdminUserService.formatVipTime(vipTime);
                    one.put("viptime", vipTimeStr);

                    Optional<HxrUserRow> userOpt = hxrAdminUserService.fetchFirstUserByMobile(phone);
                    if (userOpt.isEmpty()) {
                        one.put("success", false);
                        one.put("message", "user/select 无用户或接口失败");
                        fail++;
                        details.add(one);
                        continue;
                    }
                    HxrUserRow hxrUser = userOpt.get();
                    long hxrUserId = hxrUser.id();
                    one.put("hxrUserId", hxrUserId);
                    Integer maxOrder = hxrUser.maxOrder();
                    if (maxOrder == null) {
                        one.put("success", false);
                        one.put("message", "user/select 未返回 max_order，跳过");
                        fail++;
                        details.add(one);
                        continue;
                    }
                    one.put("maxOrder", maxOrder);
                    boolean updated =
                            hxrAdminUserService.updateUserVipFields(hxrUserId, maxOrder, vipTimeStr);
                    if (updated) {
                        one.put("success", true);
                        one.put("message", "user/update 成功");
                        ok++;
                    } else {
                        one.put("success", false);
                        one.put("message", "user/update 失败");
                        fail++;
                    }
                } catch (Exception ex) {
                    one.put("success", false);
                    one.put("message", "调用异常：" + ex.getMessage());
                    fail++;
                }
                details.add(one);
            }

            result.put("success", fail == 0);
            result.put("message", fail == 0 ? "全部处理成功" : "部分或全部失败，见 details");
            result.put("totalRecords", rows.size());
            result.put("successCount", ok);
            result.put("failCount", fail);
            result.put("details", details);
            result.put("applyDateStart", applyDateStart);
            result.put("applyDateEnd", applyDateEnd);
            result.put("tenantId", tenantId.trim());
            return result;
        } catch (Exception e) {
            result.put("success", false);
            result.put("message", "处理异常：" + e.getMessage());
            return result;
        }
    }

    @Override
    public void exportColleagueVipExcel(
            LocalDate applyDateStart, LocalDate applyDateEnd, String tenantId, HttpServletResponse response) {
        try {
            ColleagueVipContext ctx = resolveColleagueVipContext(applyDateStart, applyDateEnd, tenantId);
            if (ctx.validationError() != null) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, ctx.validationError());
                return;
            }

            List<LbPurchaseApply> exportRows = new ArrayList<>();
            for (LbPurchaseApply row : ctx.rows()) {
                if (colleagueVipSkipReason(row, ctx.tradeMatchedNicknames()) == null) {
                    exportRows.add(row);
                }
            }

            try (Workbook workbook = new XSSFWorkbook()) {
                Sheet sheet = workbook.createSheet("开通特权提醒");

                CellStyle headerStyle = createHeaderStyle(workbook);
                CellStyle groupStyle = createGroupStyle(workbook);
                CellStyle dataStyle = createDataStyle(workbook);

                String[] headers = {"", "昵称", "手机号", "特权开通日期", "开通日期"};
                Row headerRow = sheet.createRow(0);
                headerRow.setHeightInPoints(22);
                for (int i = 0; i < headers.length; i++) {
                    createExportCell(headerRow, i, headers[i], headerStyle);
                }

                int rowIndex = 1;
                if (!exportRows.isEmpty()) {
                    int groupStartRow = rowIndex;
                    for (LbPurchaseApply item : exportRows) {
                        Row row = sheet.createRow(rowIndex);
                        row.setHeightInPoints(20);
                        createExportCell(row, 1, formatColleagueVipNickname(item), dataStyle);
                        createExportCell(row, 2, nullToEmpty(item.getColleaguePhone()), dataStyle);
                        createExportCell(row, 3, formatColleaguePrivilegeRange(item.getApplyDate()), dataStyle);
                        createExportCell(row, 4, formatActivationDate(item.getApplyDate()), dataStyle);
                        rowIndex++;
                    }
                    int groupEndRow = rowIndex - 1;
                    Row firstRow = sheet.getRow(groupStartRow);
                    createExportCell(firstRow, 0, COLLEAGUE_VIP_GROUP_LABEL, groupStyle);
                    if (groupEndRow > groupStartRow) {
                        sheet.addMergedRegion(new CellRangeAddress(groupStartRow, groupEndRow, 0, 0));
                    }
                }

                adjustExportColumnWidths(sheet, headers);

                String filename = "开通特权提醒_" + LocalDateTime.now().format(EXPORT_FILENAME_TS) + ".xlsx";
                String encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
                response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
                response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded);

                try (ServletOutputStream os = response.getOutputStream()) {
                    workbook.write(os);
                    os.flush();
                }
            }
        } catch (Exception e) {
            log.error("同事特权开通提醒导出异常", e);
            try {
                response.reset();
                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                response.setContentType("text/plain;charset=UTF-8");
                response.getWriter().write("导出异常：" + e.getMessage());
            } catch (Exception ignored) {
                // ignore
            }
        }
    }

    private record ColleagueVipContext(
            String validationError,
            List<LbPurchaseApply> rows,
            Set<String> tradeMatchedNicknames) {}

    private ColleagueVipContext resolveColleagueVipContext(
            LocalDate applyDateStart, LocalDate applyDateEnd, String tenantId) {
        if (applyDateStart == null || applyDateEnd == null) {
            return new ColleagueVipContext("申请开始日期与结束日期不能为空", List.of(), Set.of());
        }
        if (applyDateEnd.isBefore(applyDateStart)) {
            return new ColleagueVipContext("结束日期不能早于开始日期", List.of(), Set.of());
        }
        if (tenantId == null || tenantId.trim().isEmpty()) {
            return new ColleagueVipContext("租户id不能为空", List.of(), Set.of());
        }

        LambdaQueryWrapper<LbPurchaseApply> q = new LambdaQueryWrapper<>();
        q.eq(LbPurchaseApply::getTenantId, tenantId.trim())
                .ge(LbPurchaseApply::getApplyDate, applyDateStart)
                .le(LbPurchaseApply::getApplyDate, applyDateEnd);
        List<LbPurchaseApply> rows = this.list(q);
        if (rows == null) {
            rows = List.of();
        }

        Set<String> applyUserNames = new HashSet<>();
        for (LbPurchaseApply row : rows) {
            if (row.getColleagueName() != null && !row.getColleagueName().trim().isEmpty()
                    && row.getApplyUser() != null && !row.getApplyUser().trim().isEmpty()) {
                applyUserNames.add(row.getApplyUser().trim());
            }
        }
        Set<String> tradeMatchedNicknames = new HashSet<>();
        if (!applyUserNames.isEmpty()) {
            LambdaQueryWrapper<LbDailyUserTrade> tradeQuery = new LambdaQueryWrapper<>();
            tradeQuery.eq(LbDailyUserTrade::getTenantId, tenantId.trim())
                    .in(LbDailyUserTrade::getNickname, applyUserNames)
                    .select(LbDailyUserTrade::getNickname);
            List<LbDailyUserTrade> matchedTrades = lbDailyUserTradeMapper.selectList(tradeQuery);
            for (LbDailyUserTrade trade : matchedTrades) {
                if (trade.getNickname() != null && !trade.getNickname().trim().isEmpty()) {
                    tradeMatchedNicknames.add(trade.getNickname().trim());
                }
            }
        }
        return new ColleagueVipContext(null, rows, tradeMatchedNicknames);
    }

    private static String colleagueVipSkipReason(LbPurchaseApply row, Set<String> tradeMatchedNicknames) {
        String applyUserName = row.getApplyUser() != null ? row.getApplyUser().trim() : "";
        if (applyUserName.isEmpty()) {
            return "colleague_name 为空，跳过";
        }
        if (!tradeMatchedNicknames.contains(applyUserName)) {
            return "申请人姓名在 lb_daily_user_trade 中未找到 ，说明今天没进货 ，推荐人不能享受特权，跳过开通特权";
        }
        String phone = row.getColleaguePhone() != null ? row.getColleaguePhone().trim() : "";
        if (phone.isEmpty()) {
            return "colleague_phone 为空，跳过";
        }
        if (row.getApplyDate() == null) {
            return "applyDate 为空，跳过";
        }
        return null;
    }

    private static String formatColleagueVipNickname(LbPurchaseApply item) {
        String colleagueName = item.getColleagueName();
        if (colleagueName != null && !colleagueName.trim().isEmpty()) {
            return colleagueName.trim();
        }
        return item.getApplyUser() == null ? "" : item.getApplyUser().trim();
    }

    private static String formatColleaguePrivilegeRange(LocalDate applyDate) {
        if (applyDate == null) {
            return "";
        }
        LocalDate start = firstWeekdayOnOrAfter(applyDate);
        LocalDate endDate = endOfInclusiveWeekdaySpan(start, COLLEAGUE_PRIVILEGE_WEEKDAY_COUNT, null);
        return start.format(PRIVILEGE_RANGE_FMT) + "-" + endDate.format(PRIVILEGE_RANGE_FMT);
    }

    @Override
    public void exportExcel(LocalDate applyDateStart, LocalDate applyDateEnd, String tenantId, HttpServletResponse response) {
        try {
            if (tenantId == null || tenantId.trim().isEmpty()) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "tenantId不能为空");
                return;
            }
            if (applyDateStart == null || applyDateEnd == null) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "申请开始日期与结束日期不能为空");
                return;
            }
            if (applyDateEnd.isBefore(applyDateStart)) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "结束日期不能早于开始日期");
                return;
            }

            LambdaQueryWrapper<LbPurchaseApply> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(LbPurchaseApply::getTenantId, tenantId.trim())
                    .ge(LbPurchaseApply::getApplyDate, applyDateStart)
                    .le(LbPurchaseApply::getApplyDate, applyDateEnd)
                    .orderByAsc(LbPurchaseApply::getApplyDate)
                    .orderByAsc(LbPurchaseApply::getId);
            List<LbPurchaseApply> rows = this.list(queryWrapper);

            LinkedHashMap<LocalDate, List<LbPurchaseApply>> grouped = new LinkedHashMap<>();
            for (LbPurchaseApply row : rows) {
                LocalDate key = row.getApplyDate();
                if (key == null) {
                    key = LocalDate.MIN;
                }
                grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(row);
            }

            try (Workbook workbook = new XSSFWorkbook()) {
                Sheet sheet = workbook.createSheet("进货申请");

                CellStyle headerStyle = createHeaderStyle(workbook);
                CellStyle groupStyle = createGroupStyle(workbook);
                CellStyle dataStyle = createDataStyle(workbook);

                String[] headers = {
                        "日期和人数", "序号", "昵称", "手机号", "特权开通日期",
                        "推荐人", "手机号", "领导人", "开通日期"
                };
                Row headerRow = sheet.createRow(0);
                headerRow.setHeightInPoints(22);
                for (int i = 0; i < headers.length; i++) {
                    createExportCell(headerRow, i, headers[i], headerStyle);
                }

                int rowIndex = 1;
                for (Map.Entry<LocalDate, List<LbPurchaseApply>> entry : grouped.entrySet()) {
                    LocalDate applyDate = entry.getKey();
                    List<LbPurchaseApply> groupRows = entry.getValue();
                    int groupStartRow = rowIndex;
                    String groupLabel = formatDatePeopleLabel(applyDate, groupRows.size());

                    int serial = 1;
                    for (LbPurchaseApply item : groupRows) {
                        Row row = sheet.createRow(rowIndex);
                        row.setHeightInPoints(20);
                        createExportCell(row, 1, String.valueOf(serial), dataStyle);
                        createExportCell(row, 2, nullToEmpty(item.getApplyUser()), dataStyle);
                        createExportCell(row, 3, nullToEmpty(item.getApplyPhone()), dataStyle);
                        createExportCell(row, 4, formatPrivilegeRange(item), dataStyle);
                        createExportCell(row, 5, formatRecommender(item), dataStyle);
                        createExportCell(row, 6, nullToEmpty(item.getColleaguePhone()), dataStyle);
                        createExportCell(row, 7, nullToEmpty(item.getTeamLeader()), dataStyle);
                        createExportCell(row, 8, formatActivationDate(item.getApplyDate()), dataStyle);
                        serial++;
                        rowIndex++;
                    }

                    int groupEndRow = rowIndex - 1;
                    Row firstRow = sheet.getRow(groupStartRow);
                    createExportCell(firstRow, 0, groupLabel, groupStyle);
                    if (groupEndRow > groupStartRow) {
                        sheet.addMergedRegion(new CellRangeAddress(groupStartRow, groupEndRow, 0, 0));
                    }
                }

                adjustExportColumnWidths(sheet, headers);

                String filename = "进货申请_" + LocalDateTime.now().format(EXPORT_FILENAME_TS) + ".xlsx";
                String encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
                response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
                response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded);

                try (ServletOutputStream os = response.getOutputStream()) {
                    workbook.write(os);
                    os.flush();
                }
            }
        } catch (Exception e) {
            log.error("进货申请导出异常", e);
            try {
                response.reset();
                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                response.setContentType("text/plain;charset=UTF-8");
                response.getWriter().write("导出异常：" + e.getMessage());
            } catch (Exception ignored) {
                // ignore
            }
        }
    }

    private static String formatDatePeopleLabel(LocalDate applyDate, int count) {
        if (applyDate == null || applyDate.equals(LocalDate.MIN)) {
            return "未知日期招商" + count + "人";
        }
        return applyDate.getMonthValue() + "." + applyDate.getDayOfMonth() + "招商" + count + "人";
    }

    private static String formatPrivilegeRange(LbPurchaseApply item) {
        LocalDate applyDate = item.getApplyDate();
        if (applyDate == null) {
            return item.getTryDate() != null ? item.getTryDate().trim() : "";
        }
        LocalDate start = firstWeekdayOnOrAfter(applyDate);
        LocalDate endDate = endOfInclusiveWeekdaySpan(start, TRIAL_WEEKDAY_COUNT, null);
        return start.format(PRIVILEGE_RANGE_FMT) + " 到 " + endDate.format(PRIVILEGE_RANGE_FMT);
    }

    private static String formatRecommender(LbPurchaseApply item) {
        String name = item.getColleagueName();
        if (name == null || name.trim().isEmpty()) {
            return "";
        }
        return name.trim();
    }

    private static String formatActivationDate(LocalDate applyDate) {
        if (applyDate == null || applyDate.equals(LocalDate.MIN)) {
            return "";
        }
        LocalDate activation = firstWeekdayOnOrAfter(applyDate);
        return activation.getYear() + "/" + activation.getMonthValue() + "/" + activation.getDayOfMonth();
    }

    private static CellStyle createHeaderStyle(Workbook workbook) {
        CellStyle style = workbook.createCellStyle();
        Font font = workbook.createFont();
        font.setBold(true);
        style.setFont(font);
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        ((XSSFCellStyle) style).setFillForegroundColor(new XSSFColor(new Color(255, 217, 102), null));
        setThinBorders(style);
        return style;
    }

    private static CellStyle createGroupStyle(Workbook workbook) {
        CellStyle style = workbook.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        ((XSSFCellStyle) style).setFillForegroundColor(new XSSFColor(new Color(255, 230, 204), null));
        setThinBorders(style);
        return style;
    }

    private static CellStyle createDataStyle(Workbook workbook) {
        CellStyle style = workbook.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        setThinBorders(style);
        return style;
    }

    private static void setThinBorders(CellStyle style) {
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
    }

    private static void createExportCell(Row row, int col, String text, CellStyle style) {
        Cell cell = row.createCell(col);
        cell.setCellValue(text == null ? "" : text);
        cell.setCellStyle(style);
    }

    private static String nullToEmpty(String value) {
        return value == null ? "" : value;
    }

    private static void adjustExportColumnWidths(Sheet sheet, String[] headers) {
        int columnCount = headers.length;
        int[] maxWidths = new int[columnCount];
        for (int i = 0; i < columnCount; i++) {
            maxWidths[i] = calcExportDisplayWidth(headers[i]);
        }
        for (int r = 0; r <= sheet.getLastRowNum(); r++) {
            Row row = sheet.getRow(r);
            if (row == null) {
                continue;
            }
            for (int c = 0; c < columnCount; c++) {
                Cell cell = row.getCell(c);
                if (cell != null && cell.getCellType() == CellType.STRING) {
                    maxWidths[c] = Math.max(maxWidths[c], calcExportDisplayWidth(cell.getStringCellValue()));
                }
            }
        }
        for (int i = 0; i < columnCount; i++) {
            int width = Math.min(255 * 256, (maxWidths[i] + 3) * 256);
            sheet.setColumnWidth(i, width);
        }
    }

    private static int calcExportDisplayWidth(String text) {
        if (text == null || text.isEmpty()) {
            return 0;
        }
        int width = 0;
        for (char ch : text.toCharArray()) {
            width += ch > 127 ? 2 : 1;
        }
        return width;
    }
}
