package com.bcxin.tenant.open.domains.services.impls;

import com.bcxin.tenant.open.domains.entities.BillStatusCheckpointEntity;
import com.bcxin.tenant.open.domains.entities.ConfigOfBillEntity;
import com.bcxin.tenant.open.domains.entities.MonthlyBillEntity;
import com.bcxin.tenant.open.domains.events.MonthlyBillGenerateEvent;
import com.bcxin.tenant.open.domains.pojo.MonthBillPojo;
import com.bcxin.tenant.open.domains.repositories.*;
import com.bcxin.tenant.open.domains.services.MonthlyBillService;
import com.bcxin.tenant.open.domains.services.commands.GenerateMonthlyBillCommand;
import com.bcxin.tenant.open.domains.utils.BillUtils;
import com.bcxin.tenant.open.infrastructures.UnitWork;
import com.bcxin.tenant.open.infrastructures.enums.BillType;
import com.bcxin.tenant.open.infrastructures.events.EventDispatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class MonthlyBillServiceImpl implements MonthlyBillService {
    private final static Logger logger = LoggerFactory.getLogger(MonthlyBillServiceImpl.class);
    private final MonthlyBillRepository monthlyBillRepository;
    private final DailyBillRepository dailyBillRepository;
    private final UnitWork unitWork;
    private final ConfigOfBillRepository configOfBillRepository;
    private final RdCompanyRepository rdCompanyRepository;
    private final BillStatusCheckpointRepository billStatusCheckpointRepository;

    private final EventDispatcher eventDispatcher;

    private final static int BATCH_SIZE = 100;

    public MonthlyBillServiceImpl(UnitWork unitWork, ConfigOfBillRepository configOfBillRepository,
                                  DailyBillRepository dailyBillRepository, MonthlyBillRepository monthlyBillRepository,
                                  RdCompanyRepository rdCompanyRepository, BillStatusCheckpointRepository billStatusCheckpointRepository, EventDispatcher eventDispatcher) {
        this.unitWork = unitWork;
        this.configOfBillRepository = configOfBillRepository;
        this.dailyBillRepository = dailyBillRepository;
        this.monthlyBillRepository = monthlyBillRepository;
        this.rdCompanyRepository = rdCompanyRepository;
        this.billStatusCheckpointRepository = billStatusCheckpointRepository;
        this.eventDispatcher = eventDispatcher;
    }

    /**
     * 账单处理逻辑：
     * <p>
     * 	先获取日账单更新日期大于等于上个月最后一天，且当日是账单生辰日的公司的月账单；（正常情况下，不需要不漏，一次查询出所有公司的账单效率高。）
     * <p>
     * 	遍历所有的公司：
     * 	<p>
     * 		添加上面生成的上个月的月账单（只有等于账单生辰日的时候才有值）
     * <p>
     * 		进行补漏：
     * 	<p>
     * 	   获取账单补漏的截止日期endDate和补漏之后月账单的最后更新日期 billLastUpdate	。
         * 		1. 当日小于账单生成日（还没到账单生成的日期，上个月的账单也不会生成）。如果日账单的截止日是上个月及以后的，此时要补的是上上个月及之前的账单；如果日账单截止日在上个月之前，则补的是日账单截止之前的月账单。
         * 		补漏的截止日期 endDate：
         * 			上上个月最后一天和日账单的截止日较小的日期，日账单截止日为空则不需要补漏(没有日账单，更不会有月账单)。如果截止日期不是当月的最后一天，则取截止日期上个月的最后一天作为补漏截止日期。
         * 		月账单的最后更新日期 billLastUpdate：
         * 			为补漏截止日期的月份。
         * 		2.当日等于账单生成日（
         * 			如果有上个月账单，则日账单截止日肯定大于等于上个月最后一天，此时要补的是上上个月及之前的；
         * 			如果没有上个月账单，则可能没有月账单或者日账单截止日小于上个月最后一天：
         * 				如果日账单截止日大于等于上个月最后一天，则要补的是上上个月及之前的；
         * 				如果日账单截止日小于上个月最后一天：
         * 					如果日账单截止日（包括为空）小于上上个月最后一天，则要补的是日账单截止日之前的月账单；
         * 					如果日账单截止日大于等于上上个月最后一天，则要补的是上上个月及之前的月账单；
         * 		）。总之，日账单截止日是上上个月最后一天之后的，则补上上个月及之前的；如果日账单截止日是上上个月最后一天之前的，则补的是日账单截止之前的月账单。
         * 		补漏的截止日期 endDate：
         * 			上上个月最后一天和日账单的截止日较小的日期。日账单截止日为空则不需要补漏。如果截止日期不是当月的最后一天，则取截止日期上个月的最后一天作为补漏截止日期。
         * 		月账单的最后更新日期 billLastUpdate：
         * 			如果日账单截止日期大于等于上个月最后一天，则补偿之后，上个月及之前的月账单已生成，更新日期为上个月。否则，更新日期为上面补漏截止日期的月份。
         * 		3.当日大于账单生成日（超过了账单生成日期，上个月的账单可能生成也可能没生成）。要补的是上个月及之前的月账单。
         * 		补漏的截止日期 endDate：
         * 			上个月的最后一天和日账单的截止日较小的日期。日账单截止日为空则不需要补漏。如果截止日期不是当月的最后一天，则取截止日期上个月的最后一天作为补漏截止日期。
         * 		月账单最后更新日期 billLastUpdate：
         * 			补漏截止日所在的月份。
     * 	如果endDate为空，则表示日账单没有生成，也就不需要补漏
     * <p>
     * 		补漏开始的日期 startDate: 为数据库中保存的月账单最后成功生成的月份的下一个月的第一天。
     * <p>
     * 		当开始日期小于等于补漏截止日期时，才进行补漏操作。
     *
     */

    @Override
    public void dispatch(GenerateMonthlyBillCommand command) {
        //今天
        Date date = command.getDate();
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);

        ConfigOfBillEntity configOfBill = configOfBillRepository.get();
        //当前月的第几天
        int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
        //计算上一个月的月账单,从上个月1号开始
        calendar.add(Calendar.MONTH, -1);
        calendar.set(Calendar.DATE, 1);

        Collection<BillType> billTypes = command.getBillTypes();
        if (CollectionUtils.isEmpty(billTypes)) {
            return;
        }

        List<String> orgIds = rdCompanyRepository.selectAllCompanyIds();

        //日账单和月账单成功生成的最后更新状态。
        Map<String, List<BillStatusCheckpointEntity>> dailyBillStatusMap = billStatusCheckpointRepository.selectAll(0).stream()
                .collect(Collectors.groupingBy(BillStatusCheckpointEntity::getOrganizationId));
        Map<String, List<BillStatusCheckpointEntity>> monthlyBillStatusMap = billStatusCheckpointRepository.selectAll(1).stream()
                .collect(Collectors.groupingBy(BillStatusCheckpointEntity::getOrganizationId));

        //获取日账单更新日期大于等于上个月最后一天，且当日是账单生辰日的公司的月账单；（正常情况下，月账单能正常生成，不需要补漏，一次查询出所有公司的账单效率更高。）
        List<MonthlyBillEntity> currentMonthlyBillEntityList = getCurrentMonthlyBillEntities(billTypes, dayOfMonth, configOfBill, calendar)
                .stream().filter(ix->{
                    String orgId = ix.getOrgId();
                    List<BillStatusCheckpointEntity> entityList = dailyBillStatusMap.get(orgId);
                    if(CollectionUtils.isEmpty(entityList)) return false;
                    return BillUtils.compareDate(entityList.get(0).getLastUpdate(),BillUtils.getLastDateOfPreviousMonth(date)) >= 0;
                }).collect(Collectors.toList());
        Map<String, List<MonthlyBillEntity>> currentBillsMap =
                currentMonthlyBillEntityList.stream().collect(Collectors.groupingBy(MonthlyBillEntity::getOrgId));

        //是否有月账单生成。账单生成事件在月账单生成之后触发一次。
        boolean hasBillGenerated = false;
        String transactionID = unitWork.beginTransaction();
        List<MonthlyBillEntity> monthlyBillEntityBatch = new ArrayList<>();
        List<BillStatusCheckpointEntity> insertBillStatusList = new ArrayList<>();
        List<BillStatusCheckpointEntity> updateBillStatusList = new ArrayList<>();
        int index = 0;
        for (int i = 0; i < orgIds.size(); i++) {
            index++;
            String orgId = orgIds.get(i);
            try {
                //添加今天生成的账单
                if (!CollectionUtils.isEmpty(currentBillsMap.get(orgId))) {
                    monthlyBillEntityBatch.addAll(
                            currentBillsMap.get(orgId));
                }
                ////补漏的
                List<BillStatusCheckpointEntity> monthlyBillStatusEntities = monthlyBillStatusMap.get(orgId);
                List<BillStatusCheckpointEntity> dailyBillStatusEntites = dailyBillStatusMap.get(orgId);
                Date dailyLastUpdate = CollectionUtils.isEmpty(dailyBillStatusEntites) ? null : dailyBillStatusEntites.get(0).getLastUpdate();
                for (BillType billType : billTypes) {
                    //补漏的起始月份的第一天
                    Date startDate = null;
                    //补漏的截至月份的最后一天
                    Date endDate = null;
                    //公司该类型账单的最后成功的月份
                    Date billLastUpdate = null;
                    // 小于0，今天在月度账单生成日之前；0，今天是月度账单生成日；大于0，今天在月度账单生成日之后
                    int billDateCmp = 0;
                    switch (billType) {
                        case Attendance -> {
                            //签到签退
                            billDateCmp = dayOfMonth - (configOfBill.getSignGenBillMonth() == null ? 0 : configOfBill.getSignGenBillMonth());
                        }
                        case RollCall -> {
                            //点名轮换
                            billDateCmp = dayOfMonth - (configOfBill.getCallGenBillMonth() == null ? 0 : configOfBill.getCallGenBillMonth());
                        }
                        case Track -> {
                            //轨迹
                            billDateCmp = dayOfMonth - (configOfBill.getTrackGenBillMonth() == null ? 0 : configOfBill.getTrackGenBillMonth());
                        }
                        case Fence -> {
                            //电子围栏
                            billDateCmp = dayOfMonth - (configOfBill.getFenGenBillMonth() == null ? 0 : configOfBill.getFenGenBillMonth());
                        }
                    }
                    if(billDateCmp < 0){
                        //账单生成日：上上个月最后一天和日账单的截止日较小的日期，日账单截止日为空则不需要补漏(没有日账单，更不会有月账单)。如果截止日期不是当月的最后一天，则取截止日期上个月的最后一天作为补漏截止日期。
                        endDate = BillUtils.getLastDateOfCurOrPreMonth(BillUtils.getMin(BillUtils.getLastDateOfPreviousMonth(calendar.getTime()),dailyLastUpdate));
                        //月账单的最后更新日期 billLastUpdate：为补漏截止日期的月份。
                        billLastUpdate = endDate;
                    }else if(billDateCmp == 0){
                        //账单生成日：上上个月最后一天和日账单的截止日较小的日期。日账单截止日为空则不需要补漏。如果截止日期不是当月的最后一天，则取截止日期上个月的最后一天作为补漏截止日期。

                        endDate = BillUtils.getLastDateOfCurOrPreMonth(BillUtils.getMin(BillUtils.getLastDateOfPreviousMonth(calendar.getTime()),dailyLastUpdate));
                        //月账单的最后更新日期 billLastUpdate：如果日账单截止日期大于等于上个月最后一天，则补偿之后，上个月及之前的月账单已生成，更新日期为上个月。否则，更新日期为上面补漏截止日期的月份。
                        if(BillUtils.compareDate(dailyLastUpdate,BillUtils.getLastDateOfPreviousMonth(date)) >= 0){
                            billLastUpdate = calendar.getTime();
                        }else{
                            billLastUpdate = endDate;
                        }
                    }
                    else{
                        //账单生成日：上个月的最后一天和日账单的截止日较小的日期。日账单截止日为空则不需要补漏。如果截止日期不是当月的最后一天，则取截止日期上个月的最后一天作为补漏截止日期。
                        endDate = BillUtils.getLastDateOfCurOrPreMonth(BillUtils.getMin(BillUtils.getLastDateOfPreviousMonth(date),dailyLastUpdate));
                        //月账单的最后更新日期 billLastUpdate：为补漏截止日期的月份。
                        billLastUpdate = endDate;
                    }

                    //如果endDate为空，则表示日账单没有生成，也就不需要补漏
                    if(endDate != null){
                        //如果没有月账单状态记录，则需要新增，有的话，需要更新
                        if (CollectionUtils.isEmpty(monthlyBillStatusEntities) ||
                                !monthlyBillStatusEntities.stream().anyMatch(ix -> ix.getBillType() == billType)) {
                            startDate = null;
                            insertBillStatusList.add(BillStatusCheckpointEntity.create(null,
                                    orgId,
                                    1,
                                    billType,
                                    java.sql.Date.valueOf(new SimpleDateFormat("yyyy-MM-dd").format(billLastUpdate))));
                        } else {
                            //有记录的话，一个公司，一种类型只有1条
                            BillStatusCheckpointEntity entity =
                                    monthlyBillStatusEntities.stream()
                                            .filter(ix -> ix.getBillType() == billType)
                                            .collect(Collectors.toList()).get(0);
                            startDate = BillUtils.getFirstDateOfNextMonth(entity.getLastUpdate());
                            //如果同一个月份，则无需更新
                            if(BillUtils.compareMonth(entity.getLastUpdate(),billLastUpdate) != 0){
                                entity.setLastUpdate(java.sql.Date.valueOf(new SimpleDateFormat("yyyy-MM-dd").format(billLastUpdate)));
                                updateBillStatusList.add(entity);
                            }
                        }
                        if (BillUtils.compareMonth(startDate, endDate) <= 0) {
                            List<MonthBillPojo> missedMonthBills = dailyBillRepository.calcMonthBillRange(orgId, billType, startDate, endDate);
                            List<MonthlyBillEntity> missedMonthBillEntites = missedMonthBills.stream()
                                    .map(ix -> BillUtils.createMonthlyBillEntity(configOfBill, ix, billType, ix.getBillMonth()))
                                    .collect(Collectors.toList());
                            monthlyBillEntityBatch.addAll(missedMonthBillEntites);
                        }
                    }
                }


                if (index % BATCH_SIZE == 0 || i == orgIds.size() - 1) {
                    if (!monthlyBillEntityBatch.isEmpty()) {
                        monthlyBillRepository.generateBills(monthlyBillEntityBatch);
                    }
                    if (!CollectionUtils.isEmpty(insertBillStatusList)) {
                        billStatusCheckpointRepository.insertBatch(insertBillStatusList);
                    }
                    if (!CollectionUtils.isEmpty(updateBillStatusList)) {
                        billStatusCheckpointRepository.updateBatch(updateBillStatusList);
                    }

                    unitWork.commit(transactionID);
                    hasBillGenerated = hasBillGenerated || !monthlyBillEntityBatch.isEmpty();

                    index = 0;
                    if (i < orgIds.size() - 1) {
                        transactionID = unitWork.beginTransaction();
                    }
                    monthlyBillEntityBatch.clear();
                    insertBillStatusList.clear();
                    updateBillStatusList.clear();
                }
            } catch (Exception e) {
                logger.error("生成月账单发生异常。organizationId={},date={}",
                        monthlyBillEntityBatch.stream().map(ix -> ix.getOrgId()).collect(Collectors.toList()), date, e);
                unitWork.rollback(transactionID);
                index = 0;
                if (i < orgIds.size() - 1) {
                    transactionID = unitWork.beginTransaction();
                }
                monthlyBillEntityBatch.clear();
                insertBillStatusList.clear();
                updateBillStatusList.clear();
            }
        }
        if(hasBillGenerated){
            //有月度账单生成
            eventDispatcher.dispatch(MonthlyBillGenerateEvent.create(new SimpleDateFormat("yyyyMM").format(calendar.getTime())));
        }

    }

    private List<MonthlyBillEntity> getCurrentMonthlyBillEntities(Collection<BillType> billTypes, int dayOfMonth, ConfigOfBillEntity configOfBill, Calendar calendar) {
        List<MonthlyBillEntity> currentMonthlyBillEntityList = new ArrayList<>();
        for (BillType billType : billTypes) {
            switch (billType) {
                case Attendance -> {
                    //签到签退
                    if (dayOfMonth == (configOfBill.getSignGenBillMonth() == null ? 0 : configOfBill.getSignGenBillMonth())) {
                        //获取上一个月的账单
                        currentMonthlyBillEntityList.addAll(getMonthlyBills(calendar, configOfBill, billType));
                    }
                }
                case RollCall -> {
                    //点名轮换
                    if(dayOfMonth == (configOfBill.getCallGenBillMonth() == null ? 0 : configOfBill.getCallGenBillMonth())){
                        currentMonthlyBillEntityList.addAll(getMonthlyBills(calendar, configOfBill, billType));
                    }
                }
                case Track -> {
                    //轨迹
                    if (dayOfMonth == (configOfBill.getTrackGenBillMonth() == null ? 0 : configOfBill.getTrackGenBillMonth())) {
                        currentMonthlyBillEntityList.addAll(getMonthlyBills(calendar, configOfBill, billType));
                    }
                }
                case Fence -> {
                    //电子围栏
                    if (dayOfMonth == (configOfBill.getFenGenBillMonth() == null ? 0 : configOfBill.getFenGenBillMonth())) {
                        currentMonthlyBillEntityList.addAll(getMonthlyBills(calendar, configOfBill, billType));
                    }
                }
            }
        }
        return currentMonthlyBillEntityList;
    }

    private List<MonthlyBillEntity> getMonthlyBills(Calendar calendar, ConfigOfBillEntity configOfBill, BillType type) {
        String dateStr = new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime());
        List<MonthBillPojo> attendanceMonthBills = dailyBillRepository.calcMonthBill(dateStr, type);
        List<MonthlyBillEntity> monthlyBillEntityList = attendanceMonthBills.stream()
                .map(ix -> BillUtils.createMonthlyBillEntity(configOfBill, ix, type, calendar.getTime()))
                .collect(Collectors.toList());
        return monthlyBillEntityList;
    }
}
