package cn.iocoder.yudao.module.energy.service.bill;

import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.energy.controller.admin.bill.vo.*;
import cn.iocoder.yudao.module.energy.controller.admin.detail.vo.HydrogenDetailRespVO;
import cn.iocoder.yudao.module.energy.convert.bill.EnergyBillConvert;
import cn.iocoder.yudao.module.energy.convert.detail.HydrogenDetailConvert;
import cn.iocoder.yudao.module.energy.dal.dataobject.bill.EnergyBillAdjustmentDO;
import cn.iocoder.yudao.module.energy.dal.dataobject.bill.EnergyBillDO;
import cn.iocoder.yudao.module.energy.dal.dataobject.detail.EnergyHydrogenDetailDO;
import cn.iocoder.yudao.module.energy.dal.mysql.bill.EnergyBillAdjustmentMapper;
import cn.iocoder.yudao.module.energy.dal.mysql.bill.EnergyBillMapper;
import cn.iocoder.yudao.module.energy.dal.mysql.detail.EnergyHydrogenDetailMapper;
import cn.iocoder.yudao.module.energy.enums.*;
import cn.iocoder.yudao.module.energy.event.BillAuditPassedEvent;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.energy.enums.ErrorCodeConstants.*;

@Service
@Validated
@Slf4j
public class EnergyBillServiceImpl implements EnergyBillService {

    @Resource
    private EnergyBillMapper billMapper;
    @Resource
    private EnergyBillAdjustmentMapper adjustmentMapper;
    @Resource
    private EnergyHydrogenDetailMapper detailMapper;
    @Resource
    private ApplicationEventPublisher eventPublisher;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long generateBill(EnergyBillGenerateReqVO reqVO) {
        // 1. Query unbilled, audited, deducted details
        List<EnergyHydrogenDetailDO> details = detailMapper.selectUnbilledByCondition(
                reqVO.getCustomerId(), reqVO.getContractId(), reqVO.getStationId(),
                reqVO.getBillPeriodStart(), reqVO.getBillPeriodEnd());
        if (details.isEmpty()) {
            throw exception(ENERGY_BILL_NO_DETAILS);
        }

        // 2. Calculate totals
        BigDecimal receivableAmount = details.stream()
                .map(EnergyHydrogenDetailDO::getCustomerAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalQuantity = details.stream()
                .map(EnergyHydrogenDetailDO::getHydrogenQuantity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        // 3. Create bill (draft status)
        EnergyBillDO bill = EnergyBillDO.builder()
                .billCode(generateBillCode())
                .energyType(reqVO.getEnergyType())
                .customerId(reqVO.getCustomerId())
                .contractId(reqVO.getContractId())
                .stationId(reqVO.getStationId())
                .billPeriodStart(reqVO.getBillPeriodStart())
                .billPeriodEnd(reqVO.getBillPeriodEnd())
                .receivableAmount(receivableAmount)
                .actualAmount(receivableAmount)
                .adjustmentAmount(BigDecimal.ZERO)
                .paidAmount(BigDecimal.ZERO)
                .totalQuantity(totalQuantity)
                .detailCount(details.size())
                .status(BillStatusEnum.DRAFT.getStatus())
                .auditStatus(BillAuditStatusEnum.PENDING.getStatus())
                .submitStatus(BillSubmitStatusEnum.NOT_SUBMITTED.getStatus())
                .paymentStatus(PaymentStatusEnum.UNPAID.getStatus())
                .generateTime(LocalDateTime.now())
                .build();
        billMapper.insert(bill);

        // 4. Update detail billIds directly (no event needed)
        List<Long> detailIds = details.stream().map(EnergyHydrogenDetailDO::getId).toList();
        detailMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<EnergyHydrogenDetailDO>()
                .set(EnergyHydrogenDetailDO::getBillId, bill.getId())
                .in(EnergyHydrogenDetailDO::getId, detailIds));

        return bill.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<Long> batchGenerateBills(List<EnergyBillGenerateReqVO> reqVOs) {
        List<Long> billIds = new ArrayList<>();
        for (EnergyBillGenerateReqVO reqVO : reqVOs) {
            try {
                billIds.add(generateBill(reqVO));
            } catch (Exception e) {
                log.warn("[batchGenerateBills] Failed for customer={}, contract={}",
                        reqVO.getCustomerId(), reqVO.getContractId(), e);
            }
        }
        return billIds;
    }

    @Override
    public PageResult<EnergyBillDO> getBillPage(EnergyBillPageReqVO reqVO) {
        return billMapper.selectPage(reqVO);
    }

    @Override
    public EnergyBillDO getBill(Long id) {
        return billMapper.selectById(id);
    }

    @Override
    public void updateBill(EnergyBillSaveReqVO reqVO) {
        EnergyBillDO bill = validateBillExists(reqVO.getId());
        bill.setAuditRemark(reqVO.getAuditRemark());
        billMapper.updateById(bill);
    }

    @Override
    public void deleteBill(Long id) {
        EnergyBillDO bill = validateBillExists(id);
        if (!BillStatusEnum.DRAFT.getStatus().equals(bill.getStatus())) {
            throw exception(ENERGY_BILL_NOT_DRAFT);
        }
        billMapper.deleteById(id);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void auditBill(Long id, Boolean approved, String remark) {
        EnergyBillDO bill = validateBillExists(id);
        if (!BillAuditStatusEnum.PENDING.getStatus().equals(bill.getAuditStatus())) {
            throw exception(ENERGY_BILL_ALREADY_AUDITED);
        }

        Integer newAuditStatus = approved ? BillAuditStatusEnum.APPROVED.getStatus() : BillAuditStatusEnum.REJECTED.getStatus();
        Integer newStatus = approved ? BillStatusEnum.GENERATED.getStatus() : bill.getStatus();

        bill.setAuditStatus(newAuditStatus);
        bill.setStatus(newStatus);
        bill.setAuditRemark(remark);
        bill.setAuditTime(LocalDateTime.now());
        billMapper.updateById(bill);

        // If approved, publish event to update detail settlement status
        if (approved) {
            List<EnergyHydrogenDetailDO> details = detailMapper.selectListByBillId(id);
            List<Long> detailIds = details.stream().map(EnergyHydrogenDetailDO::getId).toList();
            eventPublisher.publishEvent(new BillAuditPassedEvent(id, detailIds));
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createAdjustment(EnergyBillAdjustmentSaveReqVO reqVO) {
        EnergyBillDO bill = validateBillExists(reqVO.getBillId());

        EnergyBillAdjustmentDO adjustment = EnergyBillAdjustmentDO.builder()
                .billId(reqVO.getBillId())
                .detailId(reqVO.getDetailId())
                .adjustmentType(reqVO.getAdjustmentType())
                .amount(reqVO.getAmount())
                .reason(reqVO.getReason())
                .attachmentUrls(reqVO.getAttachmentUrls())
                .operateTime(LocalDateTime.now())
                .build();
        adjustmentMapper.insert(adjustment);

        // Update bill amounts
        BigDecimal delta = AdjustmentTypeEnum.INCREASE.getType().equals(reqVO.getAdjustmentType())
                ? reqVO.getAmount() : reqVO.getAmount().negate();
        bill.setAdjustmentAmount(bill.getAdjustmentAmount().add(delta));
        bill.setActualAmount(bill.getReceivableAmount().add(bill.getAdjustmentAmount()));
        billMapper.updateById(bill);

        return adjustment.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteAdjustment(Long adjustmentId) {
        EnergyBillAdjustmentDO adjustment = adjustmentMapper.selectById(adjustmentId);
        if (adjustment == null) return;

        EnergyBillDO bill = billMapper.selectById(adjustment.getBillId());
        if (bill != null) {
            // Reverse the adjustment
            BigDecimal delta = AdjustmentTypeEnum.INCREASE.getType().equals(adjustment.getAdjustmentType())
                    ? adjustment.getAmount().negate() : adjustment.getAmount();
            bill.setAdjustmentAmount(bill.getAdjustmentAmount().add(delta));
            bill.setActualAmount(bill.getReceivableAmount().add(bill.getAdjustmentAmount()));
            billMapper.updateById(bill);
        }
        adjustmentMapper.deleteById(adjustmentId);
    }

    @Override
    public List<HydrogenDetailRespVO> getBillDetailList(Long billId) {
        List<EnergyHydrogenDetailDO> details = detailMapper.selectListByBillId(billId);
        return HydrogenDetailConvert.INSTANCE.convertList(details);
    }

    @Override
    public List<EnergyBillAdjustmentRespVO> getAdjustmentList(Long billId) {
        List<EnergyBillAdjustmentDO> adjustments = adjustmentMapper.selectListByBillId(billId);
        return EnergyBillConvert.INSTANCE.convertAdjustmentList(adjustments);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Integer> batchGenerateByPeriod(String billPeriod) {
        YearMonth ym = YearMonth.parse(billPeriod);
        LocalDate start = ym.atDay(1);
        LocalDate end = ym.atEndOfMonth();

        // Query all unbilled, approved details for this period
        List<EnergyHydrogenDetailDO> allDetails = detailMapper.selectUnbilledByPeriod(start, end);
        // Group by customerId + contractId + stationId
        Map<String, List<EnergyHydrogenDetailDO>> grouped = allDetails.stream()
                .collect(Collectors.groupingBy(d ->
                        d.getCustomerId() + "_" + d.getContractId() + "_" + d.getStationId()));

        int generatedCount = 0;
        int skippedCount = 0;
        for (Map.Entry<String, List<EnergyHydrogenDetailDO>> entry : grouped.entrySet()) {
            List<EnergyHydrogenDetailDO> details = entry.getValue();
            EnergyHydrogenDetailDO first = details.get(0);
            try {
                EnergyBillGenerateReqVO reqVO = new EnergyBillGenerateReqVO();
                reqVO.setCustomerId(first.getCustomerId());
                reqVO.setContractId(first.getContractId());
                reqVO.setStationId(first.getStationId());
                reqVO.setBillPeriodStart(start);
                reqVO.setBillPeriodEnd(end);
                reqVO.setEnergyType(EnergyTypeEnum.HYDROGEN.getType());
                generateBill(reqVO);
                generatedCount++;
            } catch (Exception e) {
                log.warn("[batchGenerateByPeriod] Skipped group: {}", entry.getKey(), e);
                skippedCount++;
            }
        }
        Map<String, Integer> result = new HashMap<>();
        result.put("generatedCount", generatedCount);
        result.put("skippedCount", skippedCount);
        return result;
    }

    private EnergyBillDO validateBillExists(Long id) {
        EnergyBillDO bill = billMapper.selectById(id);
        if (bill == null) {
            throw exception(ENERGY_BILL_NOT_EXISTS);
        }
        return bill;
    }

    private String generateBillCode() {
        return "EB" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
                + UUID.randomUUID().toString().substring(0, 4).toUpperCase();
    }
}
