package com.moka.gdtauto.service;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.moka.gdtauto.common.AutoPlanRule;
import com.moka.gdtauto.entity.AdAutoConf;
import com.moka.gdtauto.entity.AdAutoExecuteLog;
import com.moka.gdtauto.entity.AdPlan;
import com.moka.gdtauto.entity.AdPlanConf;
import com.tencent.ads.model.v3.ConfiguredStatus;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 广告巡检执行器
 * 根据 AdAutoConf 配置的规则，对广告进行巡检并执行暂停操作
 *
 * @author moka
 * @since 2026-03-12
 */
@Slf4j
@Service
@AllArgsConstructor
public class AdPlanAutoExecutor {

    private final AdAutoExecuteLogService adAutoExecuteLogService;
    private final AdPlanConfService adPlanConfService;
    private final AdPlanDailyReportService adPlanDailyReportService;
    private final AdPlanService adPlanService;

    /**
     * 执行广告巡检
     *
     * @param conf 巡检配置
     */
    @Async
    public void asyncExecute(AdAutoConf conf) {
        log.info("[AdAutoExecutor] 开始执行巡检，confId={}, jobNumber={}, adGroupId={}",
                conf.getId(), conf.getJobNumber(), conf.getAdGroupId());

        // 0. 创建执行日志
        AdAutoExecuteLog executeLog = new AdAutoExecuteLog();
        executeLog.setConfId(conf.getId());
        executeLog.setParentLogId(0L);
        executeLog.setAdPlanConfId(0L);
        executeLog.setExecuteTime(LocalDateTime.now());
        executeLog.setLogType("日预算");
        adAutoExecuteLogService.save(executeLog);

        try {
            doExecute(conf, executeLog.getId());
        } catch (Exception e) {
            log.error("[AdAutoExecutor] 巡检执行异常，confId={}", conf.getId(), e);
            updateLogFailed(executeLog, "巡检异常: " + e.getMessage());
        }

        adAutoExecuteLogService.updateExecuteInfo(executeLog.getId());
        
    }

    private void doExecute(AdAutoConf autoConf, Long parentExecuteLogId) {

        // 解析规则
        List<AutoPlanRule> rules = parseRules(autoConf.getRules());

        // 1. 根据 jobNumber 查询 AdPlanConf 列表
        List<AdPlanConf> adPlanConfs = adPlanConfService.lambdaQuery()
                .eq(autoConf.getAdGroupId() > 0, AdPlanConf::getAccountGroupId, autoConf.getAdGroupId())
                .eq(AdPlanConf::getJobNumber, autoConf.getJobNumber())
                .in(AdPlanConf::getStatus, List.of(4, 5))
                .eq(AdPlanConf::getDeleted, 0)
                .list();
        if (adPlanConfs == null || adPlanConfs.isEmpty()) {
            log.warn("[AdAutoExecutor] 未找到 AdPlanConf，jobNumber={}", autoConf.getJobNumber());
            throw new RuntimeException("未找到关联广告计划配置");
        }
        log.info("[AdAutoExecutor] 找到 {} 个 AdPlanConf，jobNumber={}", adPlanConfs.size(), autoConf.getJobNumber());

        for (AdPlanConf adPlanConf : adPlanConfs) {
            processOneConf(autoConf, adPlanConf, rules, parentExecuteLogId);
        }
        log.info("[AdAutoExecutor] 巡检执行完成，confId={}", autoConf.getId());
    }

    /**
     * 处理单个 AdPlanConf：同步日报 → 分页查询 AdPlan → 规则检测 → 并发暂停
     *
     * @param adPlanConf        当前广告计划配置
     * @param rules             巡检规则列表
     * @param newAdDays         新广告天数阈值
     * @param allPlansToSuspend 收集所有待暂停广告（跨 conf 汇总用）
     * @return
     */

    public void checkRule(Long adPlanConfId, AdAutoConf autoConf,
            List<AutoPlanRule> rules) {
                // 1. 根据 jobNumber 查询 AdPlanConf 列表
        List<AdPlanConf> adPlanConfs = adPlanConfService.lambdaQuery()
                .eq(autoConf.getAdGroupId() > 0, AdPlanConf::getAccountGroupId, autoConf.getAdGroupId())
                .eq(AdPlanConf::getId, adPlanConfId)
                .list();
        if (adPlanConfs == null || adPlanConfs.isEmpty()) {
            log.warn("[AdAutoExecutor] 未找到 AdPlanConf，jobNumber={}", autoConf.getJobNumber());
            throw new RuntimeException("未找到关联广告计划配置");
        }
        
        List<AdPlan> plansToSuspend = new ArrayList<>();
        for (AdPlanConf adPlanConf : adPlanConfs) {
            List<AdPlan> plans = adPlanService.lambdaQuery()
                    .eq(AdPlan::getAdPlanConfId, adPlanConf.getId())
                    .eq(AdPlan::getDeleted, 0)
                    .list();
                 for (AdPlan plan : plans) {
                    if (shouldSuspend(plan, rules, autoConf.getNewAdDays())) {
                        plansToSuspend.add(plan);
                    }
                }
        }
        for (AdPlan plan : plansToSuspend) {
            adPlanService.lambdaUpdate().set(AdPlan::getFailReason, plan.getFailReason()).eq(AdPlan::getId, plan.getId()).update();
        }
               
    }

    private void processOneConf(AdAutoConf autoConf,
            AdPlanConf adPlanConf,
            List<AutoPlanRule> rules,
            Long parentExecuteLogId) {
        long confId = adPlanConf.getId();
        LocalDateTime now = LocalDateTime.now();
        AdAutoExecuteLog executeLog = new AdAutoExecuteLog();
        executeLog.setConfId(autoConf.getId());
        executeLog.setAdPlanConfId(adPlanConf.getId());
        executeLog.setParentLogId(parentExecuteLogId);
        executeLog.setExecuteTime(LocalDateTime.now());
        adAutoExecuteLogService.save(executeLog);

        List<Long> dailyBudgetLevel = JSON.parseArray(autoConf.getDailyBudgetLevel(), Long.class);
        // 同步日报
        try {
            log.info("[AdAutoExecutor] 同步日报，confId={}", confId);
            adPlanDailyReportService.syncReqDailyReport(confId, now, now);

            // 分页查询当前 conf 下的 AdPlan，进行规则检测
            long adCount = 0;
            List<AdPlan> plansToSuspend = new ArrayList<>();
            List<AdPlan> plansToAddDailyBudget = new ArrayList<>();
            int pageNum = 1;
            int pageSize = 100;
            while (true) {
                LambdaQueryWrapper<AdPlan> queryWrapper = new LambdaQueryWrapper<>();
                queryWrapper.eq(AdPlan::getAdPlanConfId, confId)
                        .isNotNull(AdPlan::getAdgroupId)
                        .gt(AdPlan::getAdgroupId, 0)
                        .eq(AdPlan::getDeleted, 0)
                        .eq(AdPlan::getStatus, 4) // status=4 已成功的广告
                        .orderByAsc(AdPlan::getId);
                Page<AdPlan> page = adPlanService.page(new Page<>(pageNum, pageSize), queryWrapper);
                List<AdPlan> plans = page.getRecords();

                if (plans.isEmpty()) {
                    break;
                }
                plans = plans.stream()
                .filter(plan -> {
                    return plan.getConfiguredStatus().equals("") || plan.getConfiguredStatus().equals(ConfiguredStatus.NORMAL.getValue());
                }).collect(Collectors.toList());
                
                adCount += plans.size();
                for (AdPlan plan : plans) {
                    // if (shouldSuspend(plan, rules, autoConf.getNewAdDays())) {
                    //     plansToSuspend.add(plan);
                    // }
                    if (shouldAddDailyBudget(plan, dailyBudgetLevel, autoConf.getDailyBudgetRateTop())) {
                        plansToAddDailyBudget.add(plan);
                    }
                }

                if (!page.hasNext()) {
                    break;
                }
                pageNum++;
            }

            if (!plansToSuspend.isEmpty()) {
                adPlanConfService.gdtSuspendByPlans(adPlanConf, plansToSuspend);
            }

            if (!plansToAddDailyBudget.isEmpty()) {
                for (AdPlan plan : plansToAddDailyBudget) {
                    adPlanService.updateAdgroupDailyBudget(adPlanConf, plan, plan.getDailyBudget());
                }
            }

            log.info("[AdAutoExecutor] confId={} 巡检：广告数={}, 提日预算数={}", confId, adCount, plansToAddDailyBudget.size());
            executeLog.setAdCount(adCount);
            executeLog.setSuspendCount(plansToAddDailyBudget.size());
            executeLog.setExecuteStatus(1);
            executeLog.setSuspendAdGroupIds(plansToSuspend.stream().map(x -> String.format("[%d-%d-{%s}]", x.getAdgroupId(), x.getDailyBudget(), x.getAdgroupName())).collect(Collectors.joining("\n")));
            executeLog.setExecuteResult("成功");
            executeLog.setSuccessTime(LocalDateTime.now());
            adAutoExecuteLogService.updateById(executeLog);
        } catch (Exception e) {
            executeLog.setExecuteStatus(2);
            executeLog.setExecuteResult(e.getMessage());
            adAutoExecuteLogService.updateById(executeLog);
        }

    }

    private boolean shouldAddDailyBudget(AdPlan plan, List<Long> dailyBudgetLevels, BigDecimal dailyBudgetRateTop) {
        Long cost = plan.getCost();
        if (cost == null || cost == 0) {
            return false;
        }
        Long dailyBudget = plan.getDailyBudget();
        Long dailyBudgetLimit = dailyBudgetRateTop.multiply(BigDecimal.valueOf(dailyBudget)).longValue();
        if (cost >= dailyBudgetLimit) {
            Long targetDailyBudget = 0L;
            for (int i = 0; i < dailyBudgetLevels.size(); i++) {
                if (dailyBudgetLevels.get(i) > cost) {
                    targetDailyBudget = dailyBudgetLevels.get(i+1);
                    break;
                }
            }
            if (targetDailyBudget > 0) {
                plan.setDailyBudget(targetDailyBudget);
                return true;
            }
        }
        return false;
    }

    /**
     * 判断广告是否应该被暂停
     * 遍历所有规则，有任一规则满足条件则暂停
     */
    private boolean shouldSuspend(AdPlan plan, List<AutoPlanRule> rules, Integer newAdDays) {
        for (AutoPlanRule rule : rules) {
            if (matchRule(plan, rule, newAdDays)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 单条规则匹配
     * rule.type: 1-ROI规则, 2-粉价规则
     * rule.newAd: 1-新广告, 2-老广告
     * 
     */
    public boolean matchRule(AdPlan plan, AutoPlanRule rule, Integer newAdDays) {
        // 判断新老广告
        boolean isNewAd = isNewAd(plan, newAdDays);
        if (rule.getNewAd() == 1 && !isNewAd) {
            return false; // 规则针对新广告，但当前是老广告
        }
        if (rule.getNewAd() == 2 && isNewAd) {
            return false; // 规则针对老广告，但当前是新广告
        }

        Long cost = Optional.ofNullable(plan.getCost()).orElse(0L);
        Long constYuan = cost / 100;
        Long scanFollowUserCost = Optional.ofNullable(plan.getScanFollowUserCost()).orElse(0L);
        Long scanCostYuan = scanFollowUserCost / 100;

        // 1 roi 2 粉价
        if (rule.getType() == 1) {
            if (rule.getMinCost() == 0) {
                if (plan.getReqIncomeRoi1().compareTo(rule.getMinIor()) < 0) {
                String s = String.format((isNewAd? "[新广告]" : "[老广告]") + " 不考虑消耗 ROI:%.2f < 阈值:%.2f",
                            plan.getReqIncomeRoi1(), rule.getMinIor());
                    plan.setFailReason(s);
                    return true;
                }
            } else {
                // getReqIncomeRoi1 < minIor 关闭
                if (constYuan > rule.getMinCost() && plan.getReqIncomeRoi1().compareTo(rule.getMinIor()) < 0) {
                    String s = String.format((isNewAd? "[新广告]" : "[老广告]") + " 消耗%d > 阈值%d ROI:%.2f < 阈值:%.2f",
                       constYuan, rule.getMinCost(), plan.getReqIncomeRoi1(), rule.getMinIor());
                    plan.setFailReason(s);
                    return true;
                }
            }
        } else if (rule.getType() == 2) {
            // minCost == 0 表示不限制消耗门槛
            if (rule.getMinCost() == 0) {
                if (scanCostYuan > rule.getScanMaxCost()) {
                    String s = String.format((isNewAd? "[新广告]" : "[老广告]") + " 不考虑消耗 粉价消耗%d > 阈值%d",
                            scanCostYuan, rule.getScanMaxCost());
                    plan.setFailReason(s);
                    return true;
                }
            } else {
                if (scanCostYuan > rule.getScanMaxCost() && constYuan > rule.getMinCost()) {
                    String s = String.format((isNewAd? "[新广告]" : "[老广告]") + " 消耗%d > 阈值%d； 粉价消耗%d > 阈值%d",
                        constYuan, rule.getMinCost(), scanCostYuan, rule.getScanMaxCost());
                    plan.setFailReason(s);
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * 判断是否为新广告
     * 新广告定义：成功时间 createTime 距今不超过 newAdDays 天
     */
    private boolean isNewAd(AdPlan plan, Integer newAdDays) {
        LocalDateTime threshold = LocalDateTime.now().minusDays(newAdDays);
        return plan.getCreateTime().isAfter(threshold);
    }

    /**
     * 解析规则JSON
     */
    public List<AutoPlanRule> parseRules(String rulesJson) {
        try {
            List<AutoPlanRule> rules = JSON.parseArray(rulesJson, AutoPlanRule.class);
            // rules排序 新广告在前 ROI在前 粉价在后，minCost大的在前，老广告在后
            List<AutoPlanRule> newRules = new ArrayList<>();
            List<AutoPlanRule> oldRules = new ArrayList<>();
            rules.forEach(r -> {
                if (r.getNewAd() == 1) {
                    newRules.add(r);
                } else {
                    oldRules.add(r);
                }
            });
            // ROI在前 规则类型：1 roi 2 粉价
            newRules.sort((a, b) -> Integer.compare(a.getType(), b.getType()));
            oldRules.sort((a, b) -> Integer.compare(a.getType(), b.getType()));
            newRules.sort((a, b) -> Double.compare(b.getMinCost(), a.getMinCost()));
            oldRules.sort((a, b) -> Double.compare(b.getMinCost(), a.getMinCost()));
            newRules.sort((a, b) -> Double.compare(b.getScanMaxCost(), a.getScanMaxCost()));
            oldRules.sort((a, b) -> Double.compare(b.getScanMaxCost(), a.getScanMaxCost()));

            List<AutoPlanRule> allRules = new ArrayList<>();
            allRules.addAll(newRules);
            allRules.addAll(oldRules);
            return allRules;
        } catch (Exception e) {
            log.error("[AdAutoExecutor] 解析规则JSON失败: {}", rulesJson, e);
            return List.of();
        }
    }

    private void updateLogFailed(AdAutoExecuteLog executeLog, String reason) {
        executeLog.setExecuteStatus(2);
        executeLog.setExecuteResult(reason);
        adAutoExecuteLogService.updateById(executeLog);
    }
}
