package com.jeesharp.business.modules.finance.merchantsettlement.application;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.jeesharp.business.modules.finance.merchantreceivable.entity.MerchantReceivable;
import com.jeesharp.business.modules.finance.merchantreceivable.enums.MerchantReceivableEntryTypeEnum;
import com.jeesharp.business.modules.finance.merchantreceivable.service.MerchantReceivableService;
import com.jeesharp.business.modules.finance.merchantreceivable.vo.MerchantReceivableSettlementSummaryVO;
import com.jeesharp.business.modules.finance.merchantsettle.enums.SettleModeEnum;
import com.jeesharp.business.modules.finance.merchantsettle.vo.MerchantSettleConfigSnapshotVO;
import com.jeesharp.business.modules.finance.api.MerchantSettleConfigApi;
import com.jeesharp.business.modules.finance.merchantsettlement.convert.MerchantSettlementBillConvert;
import com.jeesharp.business.modules.finance.merchantsettlement.dto.MerchantSettlementBillGenerateDTO;
import com.jeesharp.business.modules.finance.merchantsettlement.dto.MerchantSettlementPayoutCreateDTO;
import com.jeesharp.business.modules.finance.merchantsettlement.entity.MerchantSettlementBill;
import com.jeesharp.business.modules.finance.merchantsettlement.entity.MerchantSettlementBillLine;
import com.jeesharp.business.modules.finance.merchantsettlement.entity.MerchantSettlementPayout;
import com.jeesharp.business.modules.finance.merchantsettlement.enums.MerchantSettlementBillStatusEnum;
import com.jeesharp.business.modules.finance.merchantsettlement.enums.MerchantSettlementPayoutMethodEnum;
import com.jeesharp.business.modules.finance.merchantsettlement.enums.MerchantSettlementPayoutStatusEnum;
import com.jeesharp.business.modules.finance.merchantsettlement.service.MerchantSettlementBillLineService;
import com.jeesharp.business.modules.finance.merchantsettlement.service.MerchantSettlementBillService;
import com.jeesharp.business.modules.finance.merchantsettlement.service.MerchantSettlementPayoutService;
import com.jeesharp.business.modules.finance.merchantsettlement.vo.MerchantSettlementBillInfoVO;
import com.jeesharp.business.modules.finance.merchantsettlement.vo.MerchantSettlementBillLineListVO;
import com.jeesharp.business.modules.finance.merchantsettlement.vo.MerchantSettlementBillLineQueryVO;
import com.jeesharp.business.modules.finance.merchantsettlement.vo.MerchantSettlementBillListVO;
import com.jeesharp.business.modules.finance.merchantsettlement.vo.MerchantSettlementBillQueryVO;
import com.jeesharp.business.modules.finance.paymentrecord.enums.PayRecordBizSceneEnum;
import com.jeesharp.business.modules.finance.settle.support.PaySettleBizOrderResolver;
import com.jeesharp.framework.exception.BizErrorException;
import com.jeesharp.framework.pojo.Page;
import com.jeesharp.framework.pojo.query.PageRespVO;
import com.jeesharp.framework.utils.WebFrameworkUtils;
import lombok.AllArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

/**
 * 商户结算单应用服务：按账期生成结算单（固化构成明细）、登记打款、商户确认收款。
 *
 * @author jeesharp
 * @since 2026-06-29
 */
@Service
@AllArgsConstructor
@Transactional(readOnly = true)
public class MerchantSettlementBillApplicationService {

    private static final String PERM_GENERATE = "manager:business:finance:merchantSettlementBill:generate";
    private static final String PERM_RECORD_PAYOUT = "manager:business:finance:merchantSettlementBill:recordPayout";
    private static final String PERM_CANCEL = "manager:business:finance:merchantSettlementBill:cancel";

    private final MerchantSettlementBillService merchantSettlementBillService;
    private final MerchantSettlementPayoutService merchantSettlementPayoutService;
    private final MerchantSettlementBillLineService merchantSettlementBillLineService;
    private final MerchantReceivableService merchantReceivableService;
    private final MerchantSettleConfigApi merchantSettleConfigApi;
    private final PaySettleBizOrderResolver paySettleBizOrderResolver;

    public MerchantSettlementBillInfoVO get(String id) {
        MerchantSettlementBill entity = requireBill(id);
        assertReadable(entity);
        MerchantSettlementBillInfoVO vo = MerchantSettlementBillConvert.INSTANCE.toInfoVO(entity);
        vo.setPayouts(MerchantSettlementBillConvert.INSTANCE.toPayoutListVOList(
                merchantSettlementPayoutService.listBySettlementBillId(entity.getId())));
        long lineCount = merchantSettlementBillLineService.countBySettlementBillId(entity.getId());
        vo.setLineCount(lineCount);
        vo.setHasLineSnapshot(lineCount > 0);
        return vo;
    }

    public PageRespVO<MerchantSettlementBillListVO> findPage(Page<MerchantSettlementBill> page,
            MerchantSettlementBillQueryVO query) {
        applyMerchantPortalListScope(query);
        Page<MerchantSettlementBill> result = merchantSettlementBillService.findPage(page, query);
        return MerchantSettlementBillConvert.INSTANCE.toPageRespVO(result);
    }

    public PageRespVO<MerchantSettlementBillLineListVO> findLinePage(Page<MerchantSettlementBillLine> page,
            MerchantSettlementBillLineQueryVO query) {
        if (query == null || StrUtil.isBlank(query.getSettlementBillId())) {
            throw new BizErrorException("结算单ID不能为空");
        }
        MerchantSettlementBill bill = requireBill(query.getSettlementBillId().trim());
        assertReadable(bill);
        Page<MerchantSettlementBillLine> result = merchantSettlementBillLineService.findPage(page, query);
        PageRespVO<MerchantSettlementBillLineListVO> pageVo = new PageRespVO<>();
        pageVo.setRecords(MerchantSettlementBillConvert.INSTANCE.toLineListVOList(result.getRecords()));
        pageVo.setTotal(result.getTotal());
        pageVo.setPageNo(result.getPageNo());
        pageVo.setPageSize(result.getPageSize());
        pageVo.setPages(result.getPages());
        return pageVo;
    }

    @Transactional(readOnly = false)
    public String generate(MerchantSettlementBillGenerateDTO dto) {
        validatePeriod(dto.getPeriodStart(), dto.getPeriodEnd());
        String merchantTenantId = dto.getMerchantTenantId().trim();
        assertPlatformCollectMerchant(merchantTenantId);

        MerchantSettlementBill existing = merchantSettlementBillService.findByMerchantAndPeriod(
                merchantTenantId, dto.getPeriodStart(), dto.getPeriodEnd());
        if (existing != null) {
            throw new BizErrorException("该商户账期结算单已存在：" + existing.getSettlementNo());
        }

        List<MerchantReceivable> receivables = merchantReceivableService.listUnsettledForSettlement(
                merchantTenantId, dto.getPeriodStart(), dto.getPeriodEnd());
        if (receivables == null || receivables.isEmpty()) {
            throw new BizErrorException("账期内无待结算应收流水");
        }

        MerchantReceivableSettlementSummaryVO summary = summarizeFromReceivables(receivables, merchantTenantId);
        String merchantTenantCode = merchantReceivableService.resolveTenantCodeByMerchantTenantId(merchantTenantId);
        if (StrUtil.isBlank(merchantTenantCode)) {
            merchantTenantCode = merchantTenantId;
        }

        MerchantSettlementBill bill = new MerchantSettlementBill();
        bill.setSettlementNo("MSB" + IdUtil.getSnowflakeNextIdStr());
        bill.setMerchantTenantId(merchantTenantId);
        bill.setSettleMode(SettleModeEnum.PLATFORM_COLLECT.getValue());
        bill.setPeriodStart(dto.getPeriodStart());
        bill.setPeriodEnd(dto.getPeriodEnd());
        bill.setPayCount(defaultLong(summary.getPayCount()));
        bill.setRefundCount(defaultLong(summary.getRefundCount()));
        bill.setPayBaseAmount(defaultAmount(summary.getPayBaseAmount()));
        bill.setRefundBaseAmount(defaultAmount(summary.getRefundBaseAmount()));
        bill.setNetCommissionAmount(defaultAmount(summary.getNetCommissionAmount()));
        bill.setNetMerchantAmount(defaultAmount(summary.getNetMerchantAmount()));
        bill.setPaidAmount(BigDecimal.ZERO);
        bill.setStatus(MerchantSettlementBillStatusEnum.PENDING_PAYOUT.getValue());
        bill.setRemarks(dto.getRemarks());
        bill.setTenantId(merchantTenantId);
        bill.setTenantCode(merchantTenantCode);

        merchantSettlementBillService.createBill(bill);

        Map<String, String> orderNoMap = resolveBizOrderNoMap(receivables);
        List<MerchantSettlementBillLine> lines = new ArrayList<>(receivables.size());
        List<String> receivableIds = new ArrayList<>(receivables.size());
        for (MerchantReceivable receivable : receivables) {
            receivableIds.add(receivable.getId());
            MerchantSettlementBillLine line = new MerchantSettlementBillLine();
            line.setSettlementBillId(bill.getId());
            line.setSettlementNo(bill.getSettlementNo());
            line.setReceivableId(receivable.getId());
            line.setReceivableNo(receivable.getReceivableNo());
            line.setEntryType(receivable.getEntryType());
            line.setBizScene(StrUtil.blankToDefault(receivable.getBizScene(),
                    PayRecordBizSceneEnum.MALL_ORDER.getCode()));
            line.setBizOrderId(receivable.getMallOrderId());
            if (StrUtil.isNotBlank(receivable.getMallOrderId())) {
                line.setBizOrderNo(orderNoMap.get(receivable.getMallOrderId()));
            }
            line.setPayBatchId(receivable.getPayBatchId());
            line.setFinancePayOrderId(receivable.getFinancePayOrderId());
            line.setBaseAmount(defaultAmount(receivable.getBaseAmount()));
            line.setCommissionAmount(defaultAmount(receivable.getCommissionAmount()));
            line.setMerchantAmount(defaultAmount(receivable.getMerchantAmount()));
            line.setBizTime(receivable.getBizTime());
            line.setTenantId(bill.getTenantId());
            line.setTenantCode(bill.getTenantCode());
            lines.add(line);
        }
        merchantSettlementBillLineService.batchCreateLines(lines);
        merchantReceivableService.batchBindSettlementBill(receivableIds, bill.getId(), bill.getSettlementNo());
        return bill.getId();
    }

    @Transactional(readOnly = false)
    public void recordPayout(MerchantSettlementPayoutCreateDTO dto) {
        MerchantSettlementBill bill = requireBill(dto.getSettlementBillId());
        assertReadable(bill);
        MerchantSettlementPayoutMethodEnum payoutMethod =
                MerchantSettlementPayoutMethodEnum.fromValue(dto.getPayoutMethod());
        if (payoutMethod == null) {
            throw new BizErrorException("打款方式无效");
        }

        BigDecimal payoutAmount = dto.getPayoutAmount();
        merchantSettlementBillService.validateRecordPayout(bill, payoutAmount);

        MerchantSettlementPayout payout = new MerchantSettlementPayout();
        payout.setSettlementBillId(bill.getId());
        payout.setSettlementNo(bill.getSettlementNo());
        payout.setPayoutNo("MSP" + IdUtil.getSnowflakeNextIdStr());
        payout.setPayoutAmount(payoutAmount);
        payout.setPayoutMethod(payoutMethod.getValue());
        payout.setPayoutTime(dto.getPayoutTime() != null ? dto.getPayoutTime() : LocalDateTime.now());
        payout.setOperatorId(WebFrameworkUtils.getLoginUserId());
        payout.setOperatorName(WebFrameworkUtils.getLoginUserName());
        payout.setVoucherUrl(dto.getVoucherUrl());
        payout.setStatus(MerchantSettlementPayoutStatusEnum.SUCCESS.getValue());
        payout.setRemarks(dto.getRemarks());
        payout.setTenantId(bill.getTenantId());
        payout.setTenantCode(bill.getTenantCode());
        merchantSettlementPayoutService.createPayout(payout);

        merchantSettlementBillService.updateBill(merchantSettlementBillService.applyPayout(bill, payoutAmount));
    }

    @Transactional(readOnly = false)
    public void merchantConfirmReceipt(String id) {
        MerchantSettlementBill bill = requireBill(id);
        assertReadable(bill);
        String loginTenantId = WebFrameworkUtils.getLoginUserTenantId();
        merchantSettlementBillService.validateMerchantConfirm(bill, loginTenantId);
        merchantSettlementBillService.updateBill(merchantSettlementBillService.applyMerchantConfirm(
                bill, WebFrameworkUtils.getLoginUserName()));
    }

    @Transactional(readOnly = false)
    public void cancel(String id) {
        MerchantSettlementBill bill = requireBill(id);
        assertReadable(bill);
        merchantSettlementBillService.validateCancel(bill);
        merchantSettlementBillService.updateBill(merchantSettlementBillService.applyCancel(bill));
        merchantReceivableService.clearSettlementBillByBillId(bill.getId());
    }

    private MerchantReceivableSettlementSummaryVO summarizeFromReceivables(List<MerchantReceivable> receivables,
            String merchantTenantId) {
        MerchantReceivableSettlementSummaryVO summary = new MerchantReceivableSettlementSummaryVO();
        summary.setMerchantTenantId(merchantTenantId);
        long payCount = 0L;
        long refundCount = 0L;
        BigDecimal payBase = BigDecimal.ZERO;
        BigDecimal refundBase = BigDecimal.ZERO;
        BigDecimal netCommission = BigDecimal.ZERO;
        BigDecimal netMerchant = BigDecimal.ZERO;
        for (MerchantReceivable receivable : receivables) {
            if (MerchantReceivableEntryTypeEnum.PAY.getValue().equals(receivable.getEntryType())) {
                payCount++;
                payBase = payBase.add(defaultAmount(receivable.getBaseAmount()));
            } else if (MerchantReceivableEntryTypeEnum.REFUND.getValue().equals(receivable.getEntryType())) {
                refundCount++;
                refundBase = refundBase.add(defaultAmount(receivable.getBaseAmount()).abs());
            }
            netCommission = netCommission.add(defaultAmount(receivable.getCommissionAmount()));
            netMerchant = netMerchant.add(defaultAmount(receivable.getMerchantAmount()));
        }
        summary.setPayCount(payCount);
        summary.setRefundCount(refundCount);
        summary.setPayBaseAmount(payBase);
        summary.setRefundBaseAmount(refundBase);
        summary.setNetCommissionAmount(netCommission);
        summary.setNetMerchantAmount(netMerchant);
        return summary;
    }

    private Map<String, String> resolveBizOrderNoMap(List<MerchantReceivable> receivables) {
        Map<String, List<String>> idsByScene = receivables.stream()
                .filter(r -> StrUtil.isNotBlank(r.getMallOrderId()))
                .collect(Collectors.groupingBy(
                        r -> StrUtil.blankToDefault(r.getBizScene(), PayRecordBizSceneEnum.MALL_ORDER.getCode()),
                        Collectors.mapping(MerchantReceivable::getMallOrderId, Collectors.toList())));
        Map<String, String> orderNoMap = new HashMap<>();
        for (Map.Entry<String, List<String>> entry : idsByScene.entrySet()) {
            List<String> distinctIds = entry.getValue().stream().filter(StrUtil::isNotBlank).distinct().toList();
            if (!distinctIds.isEmpty()) {
                orderNoMap.putAll(paySettleBizOrderResolver.mapOrderNoByIds(entry.getKey(), distinctIds));
            }
        }
        return orderNoMap;
    }

    private MerchantSettlementBill requireBill(String id) {
        if (StrUtil.isBlank(id)) {
            throw new BizErrorException("结算单ID不能为空");
        }
        MerchantSettlementBill entity = merchantSettlementBillService.get(id.trim());
        if (entity == null) {
            throw new BizErrorException("结算单不存在");
        }
        return entity;
    }

    /**
     * 商户门户列表：无平台写权限时强制本店筛选。
     */
    private void applyMerchantPortalListScope(MerchantSettlementBillQueryVO query) {
        if (hasPlatformSettlementPermission()) {
            return;
        }
        String loginTenantId = WebFrameworkUtils.getLoginUserTenantId();
        if (StrUtil.isNotBlank(loginTenantId)) {
            query.setMerchantTenantId(loginTenantId.trim());
        }
    }

    /**
     * 详情/写操作读权限：本商户或具备平台结算写权限。
     */
    private void assertReadable(MerchantSettlementBill bill) {
        if (hasPlatformSettlementPermission()) {
            return;
        }
        String loginTenantId = WebFrameworkUtils.getLoginUserTenantId();
        if (StrUtil.isBlank(loginTenantId) || !loginTenantId.trim().equals(bill.getMerchantTenantId())) {
            throw new BizErrorException("无权访问该结算单");
        }
    }

    private boolean hasPlatformSettlementPermission() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            return false;
        }
        for (GrantedAuthority authority : authentication.getAuthorities()) {
            String perm = authority.getAuthority();
            if (PERM_GENERATE.equals(perm) || PERM_RECORD_PAYOUT.equals(perm) || PERM_CANCEL.equals(perm)) {
                return true;
            }
        }
        return false;
    }

    private void assertPlatformCollectMerchant(String merchantTenantId) {
        MerchantSettleConfigSnapshotVO snapshot = merchantSettleConfigApi.resolveSnapshot(merchantTenantId);
        if (snapshot == null || !SettleModeEnum.PLATFORM_COLLECT.getValue().equals(snapshot.getSettleMode())) {
            throw new BizErrorException("仅平台代收商户可生成结算单");
        }
    }

    private void validatePeriod(LocalDateTime periodStart, LocalDateTime periodEnd) {
        if (periodStart == null || periodEnd == null) {
            throw new BizErrorException("账期起止时间不能为空");
        }
        if (periodEnd.isBefore(periodStart)) {
            throw new BizErrorException("账期结束时间不能早于开始时间");
        }
    }

    private BigDecimal defaultAmount(BigDecimal value) {
        return value != null ? value : BigDecimal.ZERO;
    }

    private Long defaultLong(Long value) {
        return value != null ? value : 0L;
    }
}
