package com.ruoyi.app.order;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.app.utils.ezPay.EzPay;
import com.ruoyi.app.utils.ezPay.EzPayConfig;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.domain.PosLoveOrg;
import com.ruoyi.system.domain.PosOrder;
import com.ruoyi.system.domain.PosOrderInvoice;
import com.ruoyi.system.domain.PosStore;
import com.ruoyi.system.domain.PosStoreEzpay;
import com.ruoyi.system.domain.InfoUser;
import com.ruoyi.system.domain.dto.ApplyInvoiceDto;
import com.ruoyi.system.domain.vo.PosOrderInvoiceVo;
import com.ruoyi.system.mapper.PosLoveOrgMapper;
import com.ruoyi.system.mapper.PosOrderInvoiceMapper;
import com.ruoyi.system.mapper.PosOrderMapper;
import com.ruoyi.system.mapper.PosStoreEzpayMapper;
import com.ruoyi.system.mapper.PosStoreMapper;
import com.ruoyi.system.service.IInfoUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * 订单电子发票 开票 / 作废 / 查询 业务（放 admin，因需调用 {@link EzPay}）。
 *
 * <p>开票链路（{@link #applyInvoice}）：校验订单归属/状态/支付 → 校验门店可开票（009 凭证 + 免用发票）
 * → 金额拆分（不含运费）→ 组装 ezPay issue 参数 + 逐商品明细 → 调 {@link EzPay#issueInvoice}
 * → 判读回应 → 落库。作废走 {@link EzPay#doPost} + invoice_invalid。
 *
 * @author ruoyi
 * @date 2026-06-16
 */
@Service
public class OrderInvoiceService {

    @Autowired
    private EzPay ezPay;

    @Autowired
    private PosOrderMapper posOrderMapper;

    @Autowired
    private PosStoreMapper posStoreMapper;

    @Autowired
    private PosStoreEzpayMapper posStoreEzpayMapper;

    @Autowired
    private PosOrderInvoiceMapper posOrderInvoiceMapper;
    @Autowired
    private PosLoveOrgMapper posLoveOrgMapper;
    @Autowired
    private IInfoUserService infoUserService;

    /** ezPay 接口根地址（从 application.yml 的 ezpay.base-url 注入，测试/正式在此切换） */
    @Value("${ezpay.base-url}")
    private String ezpayBaseUrl;

    /** 手机条码联网校验开关：测试环境 false 跳过（ezPay 测试网关无真实条码数据，会误判为不存在），正式环境 true 开启 */
    @Value("${ezpay.barcode-check:true}")
    private boolean barcodeCheckEnabled;

    /** 发票状态：0未开/1已开/2失败/3作废 */
    private static final int STATUS_NOT_ISSUED = 0;
    private static final int STATUS_ISSUED = 1;
    private static final int STATUS_FAILED = 2;
    private static final int STATUS_INVALID = 3;

    /** 订单完成态、已支付 */
    private static final long ORDER_STATE_DONE = 3L;
    private static final long ORDER_PAID = 1L;

    // ==================== US1/US2/US3：客户开票 ====================

    /**
     * 客户申请开票。校验 → 金额拆分 → 调 ezPay 即时开立 → 落库。
     * B2C/B2B/载具由 {@link #buildIssueData} 按 {@code category}/{@code carrierType} 分支处理。
     */
    public PosOrderInvoiceVo applyInvoice(ApplyInvoiceDto dto, Long currentUserId) {
        Long orderId = dto.getOrderId();
        PosOrder order = posOrderMapper.selectOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getDdId,orderId.toString()));
        if (order == null) {
            throw new ServiceException("订单不存在");
        }
        if (!Objects.equals(order.getUserId(), currentUserId)) {
            throw new ServiceException("无权操作该订单");
        }
        if (order.getState() == null || order.getState() != ORDER_STATE_DONE) {
            throw new ServiceException("订单未完成，暂不可开票");
        }
        if (order.getPayStatus() == null || order.getPayStatus() != ORDER_PAID) {
            throw new ServiceException("订单未支付，暂不可开票");
        }
        Long storeId = order.getMdId();
        PosStoreEzpay ez = assertInvoiceable(storeId);

        // 场景入参校验（B2B 统编 / B2C 邮箱 / 载具号码）
        validateInvoiceInput(dto);

        // 防重复开票
        PosOrderInvoice row = posOrderInvoiceMapper.selectOne(
                new LambdaQueryWrapper<PosOrderInvoice>().eq(PosOrderInvoice::getOrderId, orderId));
        if (row != null && Integer.valueOf(STATUS_ISSUED).equals(row.getInvoiceStatus())) {
            throw new ServiceException("该订单已开票，不可重复开票");
        }

        // 金额拆分（不含运费：invoiceTotal = amount - freight）
        int amount = order.getAmount() == null ? 0 : order.getAmount();
        int freight = (int) Math.round(order.getFreight() == null ? 0d : order.getFreight());
        int invoiceTotal = amount - freight;
        if (invoiceTotal <= 0) {
            throw new ServiceException("订单可开票金额异常，无法开票");
        }
        int sales = (int) Math.round(invoiceTotal / 1.05);
        int tax = invoiceTotal - sales;

        // ezPay 凭证（捐赠机构 checkLoveCode 兜底也用到）
        EzPayConfig cfg = new EzPayConfig(ez.getMerchantId(), ez.getHashKey(), ez.getHashIv());

        // 捐赠发票：解析机构（local 优先，未命中 checkLoveCode 验真），回填买方名为机构名
        String loveOrgName = null;
        if ("DONATION".equals(dto.getCategory())) {
            loveOrgName = resolveLoveOrg(dto.getLoveCode(), cfg);
            dto.setBuyerName(loveOrgName);
        }

        // 组装 ezPay issue 参数 + 调用
        Map<String, Object> inv = buildIssueData(dto, order, invoiceTotal, sales, tax);

        int newStatus;
        String invoiceNumber = null, randomNum = null, invoiceTransNo = null,
                invoiceBarCode = null, invoiceQrcodeL = null, invoiceQrcodeR = null, failReason = null;
        try {
            JSONObject resp = ezPay.issueInvoice(ezpayBaseUrl, cfg, inv);            String status = resp == null ? "" : resp.getString("Status");
            JSONObject result = resp == null ? null : resp.getJSONObject("Result");
            if ("SUCCESS".equals(status) && result != null && StrUtil.isNotBlank(result.getString("InvoiceNumber"))) {
                invoiceNumber = result.getString("InvoiceNumber");
                randomNum = result.getString("RandomNum");
                invoiceTransNo = result.getString("InvoiceTransNo");
                invoiceBarCode = result.getString("BarCode");
                invoiceQrcodeL = result.getString("QRcodeL");
                invoiceQrcodeR = result.getString("QRcodeR");
                newStatus = STATUS_ISSUED;
            } else {
                failReason = resp == null ? "ezPay 无回应" : (status + ":" + resp.getString("Message"));
                newStatus = STATUS_FAILED;
            }
        } catch (Exception e) {
            failReason = "ezPay 服务暂不可用：" + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());
            newStatus = STATUS_FAILED;
        }

        // 落库（uk_order_id 兜底防并发重复）
        Date now = new Date();
        PosOrderInvoice saveRow = row == null ? new PosOrderInvoice() : row;
        saveRow.setOrderId(orderId);
        saveRow.setOrderNo(order.getDdId());
        saveRow.setStoreId(storeId);
        saveRow.setInvoiceCategory(dto.getCategory());
        saveRow.setBuyerName(dto.getBuyerName());
        saveRow.setBuyerUbn(dto.getBuyerUbn());
        saveRow.setBuyerEmail(dto.getBuyerEmail());
        saveRow.setCarrierType(dto.getCarrierType());
        saveRow.setCarrierNum(dto.getCarrierNum());
        saveRow.setLoveCode(dto.getLoveCode());
        saveRow.setLoveOrgName(loveOrgName);
        saveRow.setTotalAmt(invoiceTotal);
        saveRow.setSalesAmt(sales);
        saveRow.setTaxAmt(tax);
        saveRow.setApplyTime(now);
        if (newStatus == STATUS_ISSUED) {
            saveRow.setInvoiceNumber(invoiceNumber);
            saveRow.setRandomNum(randomNum);
            saveRow.setInvoiceTransNo(invoiceTransNo);
            saveRow.setInvoiceBarCode(invoiceBarCode);
            saveRow.setInvoiceQrcodeL(invoiceQrcodeL);
            saveRow.setInvoiceQrcodeR(invoiceQrcodeR);
            saveRow.setIssueTime(now);
            saveRow.setFailReason(null);
        } else {
            saveRow.setFailReason(StrUtil.sub(failReason, 0, 480));
        }
        saveRow.setInvoiceStatus(newStatus);
        try {
            saveOrUpdate(saveRow);
        } catch (DuplicateKeyException dup) {
            throw new ServiceException("请勿重复提交开票申请");
        }

        return posOrderInvoiceMapper.selectInvoiceDetail(orderId);
    }

    /**
     * 下单捕获发票意图（010 Phase 11）：把下单选的发票类型/载具/统编/捐赠码落成 pos_order_invoice
     * 意图行（issueTriggered=0 未触发开票、invoiceStatus=0 未开）；出餐 {@link #autoIssue} 在该行
     * 上更新开票结果并置 issueTriggered=1。仅在门店可开票时由 createOrder 调用。
     */
    public void captureInvoiceIntent(PosOrderInvoice intent) {
        intent.setInvoiceStatus(STATUS_NOT_ISSUED);
        intent.setIssueTriggered(0);
        if (intent.getApplyTime() == null) {
            intent.setApplyTime(new Date());
        }
        saveOrUpdate(intent);
    }

    /**
     * 商家出餐自动开票（010 Phase 11）：读 pos_order_invoice 发票意图行 → 组装 dto（个人 BuyerName=手机号、
     * 捐赠=机构名）→ 复用开票核心 {@link #reIssueAndSave}。系统触发，不校验客户归属；货到付款未付款也开
     * （送达失败/取消时作废兜底）。门店不可开票 / 订单无发票意图 / 已开 → 静默跳过，不影响出餐。
     *
     * @param orderId PosOrder.id（PK，与 invalid/retry/selectInvoiceDetail 一致）
     * @return 1=已开 0=跳过或失败
     */
    public int autoIssue(Long orderId) {
        PosOrder order = posOrderMapper.selectPosOrderById(orderId);
        if (order == null) {
            return 0; // 订单不存在
        }
        // 发票意图自 Phase 11 起落 pos_order_invoice（下单捕获的意图行），不再读 pos_order
        PosOrderInvoice row = posOrderInvoiceMapper.selectOne(
                new LambdaQueryWrapper<PosOrderInvoice>().eq(PosOrderInvoice::getOrderId, orderId));
        if (row == null || StrUtil.isBlank(row.getInvoiceChoice())) {
            return 0; // 无发票意图（门店不可开票时 createOrder 未插意图行）
        }
        Long storeId = order.getMdId();
        if (!canInvoice(storeId)) {
            return 0; // 出餐时门店已不可开票（被停用等），跳过
        }
        PosStoreEzpay ez = posStoreEzpayMapper.selectOne(
                new LambdaQueryWrapper<PosStoreEzpay>().eq(PosStoreEzpay::getStoreId, storeId));
        if (ez == null) {
            return 0;
        }
        // 防重复：已开则跳过
        if (Integer.valueOf(STATUS_ISSUED).equals(row.getInvoiceStatus())) {
            return 0;
        }
        ApplyInvoiceDto dto = buildAutoIssueDto(row);
        EzPayConfig cfg = new EzPayConfig(ez.getMerchantId(), ez.getHashKey(), ez.getHashIv());
        // 捐赠=机构名；个人=手机号
        String loveOrgName = null;
        if ("LOVE_CODE".equals(row.getInvoiceChoice())) {
            loveOrgName = resolveLoveOrg(dto.getLoveCode(), cfg);
            dto.setBuyerName(loveOrgName);
        } else if (isPersonalChoice(row.getInvoiceChoice())) {
            InfoUser u = infoUserService.getOne(
                    new LambdaQueryWrapper<InfoUser>().eq(InfoUser::getUserId, order.getUserId()));
            String phone = u != null && StrUtil.isNotBlank(u.getPhone()) ? u.getPhone() : "會員";
            dto.setBuyerName(phone);
            // 【决定 2026-08-05】MEMBER 不再设 carrierType=2 电子载具（见 buildAutoIssueDto 决策备注），走 PrintFlag=Y 纸本；会员手机号已写入 BuyerName。
        }
        // 出餐触发开票：在意图行上落结果，并标记已触发（与 invoiceStatus 独立）
        row.setInvoiceCategory(dto.getCategory());
        row.setBuyerName(dto.getBuyerName());
        row.setLoveOrgName(loveOrgName);
        row.setIssueTriggered(1);
        return reIssueAndSave(order, row, dto, ez);
    }

    /** 出餐自动开票：pos_order_invoice 发票意图行 → ApplyInvoiceDto（捐赠走 DONATION 复用既有分支）。 */
    private ApplyInvoiceDto buildAutoIssueDto(PosOrderInvoice intent) {
        ApplyInvoiceDto dto = new ApplyInvoiceDto();
        dto.setOrderId(intent.getOrderId());
        String choice = intent.getInvoiceChoice();
        switch (choice) {
            case "PAPER":
            case "PHONE_BARCODE":
            case "CITIZEN":
                dto.setCategory("B2C");
                dto.setCarrierType(intent.getCarrierType()); // PAPER 时为 null → 个人纸本分支
                dto.setCarrierNum(intent.getCarrierNum());
                break;
            case "MEMBER":
                // 【决定 2026-08-05】会员载具(MEMBER) 走 PrintFlag=Y 索取纸本，不传 ezPay CarrierType=2 电子载具。
                // 原因：ezPay 电子载具(type2+PrintFlag=N)强制要 BuyerEmail，缺失回 INV10013；且「载具+PrintFlag=Y」
                // 法定互斥(一票两份)。故不设 carrierType，buildIssueData 走「无载具→PrintFlag=Y」分支，ezPay 不校验邮箱。
                // 会员身份由 BuyerName=手机号 + invoice_choice=MEMBER 体现，供会员发票夹归集。
                dto.setCategory("B2C");
                break;
            case "LOVE_CODE":
                dto.setCategory("DONATION");
                dto.setLoveCode(intent.getLoveCode());
                break;
            case "COMPANY":
                dto.setCategory("B2B");
                dto.setBuyerUbn(intent.getBuyerUbn());
                dto.setBuyerName(intent.getBuyerName());
                dto.setBuyerEmail(intent.getBuyerEmail());
                break;
            default:
                break;
        }
        return dto;
    }

    private boolean isPersonalChoice(String choice) {
        return "PAPER".equals(choice) || "PHONE_BARCODE".equals(choice) || "CITIZEN".equals(choice)
                || "MEMBER".equals(choice);
    }

    /** 客户查询订单发票；门店不可开票时 invoiceStatus=null（客户端据此隐藏入口）。 */
    public PosOrderInvoiceVo getInvoice(Long orderId, Long currentUserId) {
        PosOrder order = posOrderMapper.selectOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getDdId,String.valueOf(orderId)));
        if (order == null || !Objects.equals(order.getUserId(), currentUserId)) {
            throw new ServiceException("订单不存在");
        }
        boolean can = canInvoice(order.getMdId());
        PosOrderInvoiceVo vo = posOrderInvoiceMapper.selectInvoiceDetail(order.getId());
        if (vo == null) {
            vo = new PosOrderInvoiceVo();
            vo.setOrderId(order.getId());
            vo.setOrderNo(order.getDdId());
            vo.setInvoiceStatus(can ? STATUS_NOT_ISSUED : null);
        }
        return vo;
    }

    /** 商家端查询订单发票（校验商家为该订单门店归属人）。 */
    public PosOrderInvoiceVo getInvoiceForMerchant(Long orderId, Long merchantUserId) {
        requireOwnedOrder(orderId, merchantUserId);
        return posOrderInvoiceMapper.selectInvoiceDetail(orderId);
    }

    /** 商家端作废发票（校验归属后复用作废逻辑）。 */
    public int invalidForMerchant(Long orderId, String reason, Long merchantUserId) {
        requireOwnedOrder(orderId, merchantUserId);
        return invalid(orderId, reason);
    }

    /** 校验订单存在且属于该商家门店，返回订单；否则抛异常。 */
    private PosOrder requireOwnedOrder(Long orderId, Long merchantUserId) {
        PosOrder order = posOrderMapper.selectPosOrderById(orderId);
        if (order == null) {
            throw new ServiceException("订单不存在");
        }
        InfoUser user = infoUserService.getOne(
                new LambdaQueryWrapper<InfoUser>().eq(InfoUser::getUserId, merchantUserId));
        if (user == null) {
            throw new ServiceException("用户不存在");
        }
        String ut = user.getUserType();
        boolean own;
        if ("1".equals(ut) || "3".equals(ut)) {
            // 普通/夜市商家：订单 shId 即商家 userId
            own = merchantUserId.equals(order.getShId());
        } else {
            // 摊位商家：订单门店 mdId 即商家 storeId
            own = user.getStoreId() != null && user.getStoreId().equals(order.getMdId());
        }
        if (!own) {
            throw new ServiceException("无权操作该订单发票");
        }
        return order;
    }

    // ==================== US4/US5：管理端 ====================

    /** 管理端：订单发票列表（配合 PageHelper 分页）。 */
    public List<PosOrderInvoiceVo> list(PosOrderInvoiceVo query) {
        if (query != null && query.getDateRange() != null && query.getDateRange().length == 2) {
            query.setCretimStart(query.getDateRange()[0]);
            query.setCretimEnd(query.getDateRange()[1]);
        }
        return posOrderInvoiceMapper.selectInvoiceList(query);
    }

    /** 会员发票夹（010 Phase 11）：按 userId 列已开发票（不含码图）。配合 PageHelper 分页。 */
    public List<PosOrderInvoiceVo> listMyInvoices(Long userId) {
        return posOrderInvoiceMapper.selectMyInvoices(userId);
    }

    /** 管理端：订单发票详情。 */
    public PosOrderInvoiceVo detail(Long orderId) {
        return posOrderInvoiceMapper.selectInvoiceDetail(orderId);
    }

    /** 管理端：重新开票（仅 失败/作废 单；运营身份不校验客户归属）。 */
    public int retry(Long orderId) {
        PosOrderInvoice row = posOrderInvoiceMapper.selectOne(
                new LambdaQueryWrapper<PosOrderInvoice>().eq(PosOrderInvoice::getOrderId, orderId));
        if (row == null) {
            throw new ServiceException("发票记录不存在");
        }
        int st = row.getInvoiceStatus() == null ? STATUS_NOT_ISSUED : row.getInvoiceStatus();
        if (st != STATUS_FAILED && st != STATUS_INVALID) {
            throw new ServiceException("仅失败或作废的发票可重新开票");
        }
        // 重置为未开，复用客户开票参数重开（运营身份 currentUserId 传 null 跳过归属校验）
        // orderId 实为 pos_order.id（主键，发票行 order_id 已证实），按主键查最稳，不依赖 order_no 是否回填
        PosOrder order = posOrderMapper.selectPosOrderById(orderId);
        if (order == null) {
            throw new ServiceException("订单不存在");
        }
        PosStoreEzpay ez = assertInvoiceable(order.getMdId());
        ApplyInvoiceDto dto = new ApplyInvoiceDto();
        dto.setOrderId(orderId);
        dto.setCategory(row.getInvoiceCategory());
        dto.setBuyerName(row.getBuyerName());
        dto.setBuyerUbn(row.getBuyerUbn());
        dto.setBuyerEmail(row.getBuyerEmail());
        dto.setCarrierType(row.getCarrierType());
        dto.setCarrierNum(row.getCarrierNum());
        dto.setLoveCode(row.getLoveCode());
        // 复用核心开票（不含归属校验）：直接走 issue + 落库
        return reIssueAndSave(order, row, dto, ez);
    }

    /** 管理端：作废发票（仅 已开 单，调 ezPay invoice_invalid）。 */
    public int invalid(Long orderId, String reason) {
        PosOrderInvoice row = posOrderInvoiceMapper.selectOne(
                new LambdaQueryWrapper<PosOrderInvoice>().eq(PosOrderInvoice::getOrderId, orderId));
        if (row == null || !Integer.valueOf(STATUS_ISSUED).equals(row.getInvoiceStatus())) {
            throw new ServiceException("仅已开发票可作废");
        }
        PosOrder order = posOrderMapper.selectPosOrderById(orderId);
        PosStoreEzpay ez = posStoreEzpayMapper.selectOne(
                new LambdaQueryWrapper<PosStoreEzpay>().eq(PosStoreEzpay::getStoreId, order.getMdId()));
        if (ez == null) {
            throw new ServiceException("门店 ezPay 凭证缺失，无法作废");
        }

        Map<String, Object> postData = new LinkedHashMap<>();
        postData.put("RespondType", "JSON");
        postData.put("Version", "1.0");
        postData.put("InvoiceNumber", row.getInvoiceNumber());
        postData.put("InvalidReason", StrUtil.isBlank(reason) ? "商家作废" : reason);

        try {
            EzPayConfig cfg = new EzPayConfig(ez.getMerchantId(), ez.getHashKey(), ez.getHashIv());
            JSONObject resp = ezPay.doPost(ezpayBaseUrl + EzPay.URL_INVALID, cfg, postData);            String status = resp == null ? "" : resp.getString("Status");
            if ("SUCCESS".equals(status)) {
                row.setInvoiceStatus(STATUS_INVALID);
                row.setInvalidTime(new Date());
                saveOrUpdate(row);
                return 1;
            }
            // ezPay 回非 SUCCESS 且消息含「已作廢」：该票在 ezPay 端已是作废态，本地对齐成作废，
            // 避免与 ezPay 不同步时（如作废后 retry 复活死票）陷入「再作废也改不回 status」的死锁
            String msg = resp == null ? "" : resp.getString("Message");
            if (msg != null && msg.contains("已作廢")) {
                row.setInvoiceStatus(STATUS_INVALID);
                saveOrUpdate(row);
                return 1;
            }
            throw new ServiceException("作废失败：" + (resp == null ? "ezPay 无回应" : msg));
        } catch (ServiceException se) {
            throw se;
        } catch (Exception e) {
            throw new ServiceException("ezPay 服务暂不可用：" + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
        }
    }

    // ==================== 内部辅助 ====================

    /** 复用开票核心（运营重开，无归属校验）。 */
    private int reIssueAndSave(PosOrder order, PosOrderInvoice row, ApplyInvoiceDto dto, PosStoreEzpay ez) {
        int amount = order.getAmount() == null ? 0 : order.getAmount();
        int freight = (int) Math.round(order.getFreight() == null ? 0d : order.getFreight());
        int invoiceTotal = amount - freight;
        int sales = (int) Math.round(invoiceTotal / 1.05);
        int tax = invoiceTotal - sales;

        Map<String, Object> inv = buildIssueData(dto, order, invoiceTotal, sales, tax);
        EzPayConfig cfg = new EzPayConfig(ez.getMerchantId(), ez.getHashKey(), ez.getHashIv());

        int newStatus;
        String invoiceNumber = null, randomNum = null, invoiceTransNo = null,
                invoiceBarCode = null, invoiceQrcodeL = null, invoiceQrcodeR = null, failReason = null;
        try {
            JSONObject resp = ezPay.issueInvoice(ezpayBaseUrl, cfg, inv);
            String status = resp == null ? "" : resp.getString("Status");
            JSONObject result = resp == null ? null : resp.getJSONObject("Result");
            if ("SUCCESS".equals(status) && result != null && StrUtil.isNotBlank(result.getString("InvoiceNumber"))) {
                invoiceNumber = result.getString("InvoiceNumber");
                randomNum = result.getString("RandomNum");
                invoiceTransNo = result.getString("InvoiceTransNo");
                invoiceBarCode = result.getString("BarCode");
                invoiceQrcodeL = result.getString("QRcodeL");
                invoiceQrcodeR = result.getString("QRcodeR");
                newStatus = STATUS_ISSUED;
            } else {
                failReason = resp == null ? "ezPay 无回应" : (status + ":" + resp.getString("Message"));
                newStatus = STATUS_FAILED;
            }
        } catch (Exception e) {
            failReason = "ezPay 服务暂不可用：" + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());
            newStatus = STATUS_FAILED;
        }

        Date now = new Date();
        if (newStatus == STATUS_ISSUED) {
            row.setInvoiceNumber(invoiceNumber);
            row.setRandomNum(randomNum);
            row.setInvoiceTransNo(invoiceTransNo);
            row.setInvoiceBarCode(invoiceBarCode);
            row.setInvoiceQrcodeL(invoiceQrcodeL);
            row.setInvoiceQrcodeR(invoiceQrcodeR);
            row.setIssueTime(now);
            row.setFailReason(null);
        } else {
            row.setFailReason(StrUtil.sub(failReason, 0, 480));
        }
        row.setTotalAmt(invoiceTotal);
        row.setSalesAmt(sales);
        row.setTaxAmt(tax);
        row.setInvoiceStatus(newStatus);
        saveOrUpdate(row);
        return newStatus == STATUS_ISSUED ? 1 : 0;
    }

    /** 校验门店可开票并返回凭证；不可开票抛异常。 */
    private PosStoreEzpay assertInvoiceable(Long storeId) {
        if (storeId == null) {
            throw new ServiceException("订单未关联门店，暂不可开票");
        }
        PosStore store = posStoreMapper.selectPosStoreById(storeId);
        if (store != null && Integer.valueOf(1).equals(store.getInvoiceExempt())) {
            throw new ServiceException("该门店免用发票，无需开票");
        }
        PosStoreEzpay ez = posStoreEzpayMapper.selectOne(
                new LambdaQueryWrapper<PosStoreEzpay>().eq(PosStoreEzpay::getStoreId, storeId));
        if (ez == null || !Integer.valueOf(2).equals(ez.getEzpayStatus())
                || !Integer.valueOf(1).equals(ez.getIsEnabled())) {
            throw new ServiceException("该门店暂不支持电子发票");
        }
        return ez;
    }

    /** 门店是否可开票（不抛异常）：免用发票或 ezPay 未开通启用均返回 false。 */
    public boolean canInvoice(Long storeId) {
        if (storeId == null) {
            return false;
        }
        PosStore store = posStoreMapper.selectPosStoreById(storeId);
        if (store != null && Integer.valueOf(1).equals(store.getInvoiceExempt())) {
            return false;
        }
        PosStoreEzpay ez = posStoreEzpayMapper.selectOne(
                new LambdaQueryWrapper<PosStoreEzpay>().eq(PosStoreEzpay::getStoreId, storeId));
        return ez != null && Integer.valueOf(2).equals(ez.getEzpayStatus())
                && Integer.valueOf(1).equals(ez.getIsEnabled());
    }

    /** 取门店 ezPay 凭证配置；未开通/未启用返回 null（不抛异常，供校验场景容忍使用）。 */
    private EzPayConfig getEzpayConfig(Long storeId) {
        if (storeId == null) {
            return null;
        }
        PosStoreEzpay ez = posStoreEzpayMapper.selectOne(
                new LambdaQueryWrapper<PosStoreEzpay>().eq(PosStoreEzpay::getStoreId, storeId));
        if (ez == null || !Integer.valueOf(2).equals(ez.getEzpayStatus())
                || !Integer.valueOf(1).equals(ez.getIsEnabled())) {
            return null;
        }
        return new EzPayConfig(ez.getMerchantId(), ez.getHashKey(), ez.getHashIv());
    }

    /**
     * 结账时校验发票意图（010 Phase 11）：载具/捐赠联网验真、统编/凭证格式校验。
     * PAPER（默认纸本）不校验。联网校验（手机条码/捐赠码）用 storeId 对应门店的 ezPay 凭证调 BDV。
     *
     * @param storeId 一个可开票门店（调用方负责选 canInvoice 的门店，用于取 ezPay 凭证）
     */
    public void validateCheckoutInvoiceIntent(String invoiceChoice, String carrierNum, String loveCode,
                                              String buyerUbn, String buyerEmail, Long storeId) {
        if (StrUtil.isBlank(invoiceChoice) || "PAPER".equals(invoiceChoice)) {
            return;
        }
        switch (invoiceChoice) {
            case "PHONE_BARCODE":
                if (StrUtil.isBlank(carrierNum) || !carrierNum.matches("^/[0-9A-Z+\\-.]{7}$")) {
                    throw new ServiceException(MessageUtils.message("no.invoice.barcode.format"));
                }
                EzPayConfig barCfg = getEzpayConfig(storeId);
                if (barCfg == null) {
                    throw new ServiceException(MessageUtils.message("no.invoice.store.notsupport.barcode"));
                }
                if (barcodeCheckEnabled) {
                    boolean barExist;
                    try {
                        barExist = ezPay.checkBarCode(ezpayBaseUrl, barCfg, carrierNum);
                    } catch (Exception e) {
                        throw new ServiceException(MessageUtils.message("no.invoice.barcode.service.unavailable"));
                    }
                    if (!barExist) {
                        throw new ServiceException(MessageUtils.message("no.invoice.barcode.notexist"));
                    }
                }
                break;
            case "CITIZEN":
                if (StrUtil.isBlank(carrierNum) || !carrierNum.matches("^[A-Z]{2}\\d{14}$")) {
                    throw new ServiceException(MessageUtils.message("no.invoice.citizen.format"));
                }
                break;
            case "MEMBER":
                // 会员载具走 PrintFlag=Y 纸本（不传 ezPay 载具，见 buildAutoIssueDto 决策备注），免联网验真、免格式校验、免邮箱
                break;
            case "LOVE_CODE":
                if (StrUtil.isBlank(loveCode) || !loveCode.matches("^\\d{3,7}$")) {
                    throw new ServiceException(MessageUtils.message("no.invoice.lovecode.format"));
                }
                EzPayConfig loveCfg = getEzpayConfig(storeId);
                if (loveCfg == null) {
                    throw new ServiceException(MessageUtils.message("no.invoice.store.notsupport.lovecode"));
                }
                resolveLoveOrg(loveCode, loveCfg); // 抛异常=捐赠码无效
                break;
            case "COMPANY":
                if (StrUtil.isBlank(buyerUbn) || !buyerUbn.matches("^\\d{8}$")) {
                    throw new ServiceException(MessageUtils.message("no.invoice.ubn.format"));
                }
                if (StrUtil.isBlank(buyerEmail) || !buyerEmail.matches("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) {
                    throw new ServiceException(MessageUtils.message("no.invoice.company.email"));
                }
                break;
            default:
                throw new ServiceException(MessageUtils.message("no.invoice.choice.invalid", invoiceChoice));
        }
    }

    /**
     * 捐赠机构列表（客户端选择器，仅 enabled=1，配合 PageHelper 分页）。
     */
    public List<PosLoveOrg> listLoveOrg(String keyword) {
        return posLoveOrgMapper.selectListByKeyword(keyword);
    }

    /**
     * 解析捐赠机构并验真：local 命中返回简称/全名；local 未命中调 ezPay checkLoveCode
     * （IsExist=N 抛「捐赠码无效」，调用异常抛「验证失败」）。返回用于展示 / BuyerName 的机构名。
     */
    private String resolveLoveOrg(String loveCode, EzPayConfig cfg) {
        PosLoveOrg org = posLoveOrgMapper.selectOne(new LambdaQueryWrapper<PosLoveOrg>()
                .eq(PosLoveOrg::getLoveCode, loveCode)
                .eq(PosLoveOrg::getEnabled, 1));
        if (org != null) {
            return StrUtil.isNotBlank(org.getOrgShortName()) ? org.getOrgShortName() : org.getOrgName();
        }
        try {
            if (!ezPay.checkLoveCode(ezpayBaseUrl, cfg, loveCode)) {
                throw new ServiceException(MessageUtils.message("no.invoice.lovecode.invalid"));
            }
        } catch (ServiceException se) {
            throw se;
        } catch (Exception e) {
            throw new ServiceException(MessageUtils.message("no.invoice.lovecode.verify.fail"));
        }
        return "未知机构";
    }

    /**
     * 场景入参校验（service 内强校验，拦截非法请求不调 ezPay）。对齐 ezPay 开票模型：
     * B2B：统编(8位)+邮箱必填、无载具；B2C：载具(0/1/2)+号码必填；会员载具(MEMBER)走 PrintFlag=Y 纸本、不传载具不收邮箱(见 buildAutoIssueDto 2026-08-05 决策)。
     */
    private void validateInvoiceInput(ApplyInvoiceDto dto) {
        if ("B2B".equals(dto.getCategory())) {
            if (StrUtil.isBlank(dto.getBuyerName())) {
                throw new ServiceException(MessageUtils.message("no.invoice.b2b.buyername.blank"));
            }
            if (StrUtil.isBlank(dto.getBuyerUbn()) || !dto.getBuyerUbn().matches("\\d{8}")) {
                throw new ServiceException(MessageUtils.message("no.invoice.ubn.format"));
            }
            if (StrUtil.isBlank(dto.getBuyerEmail())) {
                throw new ServiceException(MessageUtils.message("no.invoice.b2b.buyeremail.blank"));
            }
            if (StrUtil.isNotBlank(dto.getCarrierType()) || StrUtil.isNotBlank(dto.getCarrierNum())) {
                throw new ServiceException(MessageUtils.message("no.invoice.b2b.nocarrier"));
            }
        } else if ("DONATION".equals(dto.getCategory())) {
            // 捐赠：loveCode 3-7 位必填，与载具/统编互斥
            if (StrUtil.isBlank(dto.getLoveCode()) || !dto.getLoveCode().matches("\\d{3,7}")) {
                throw new ServiceException(MessageUtils.message("no.invoice.lovecode.format"));
            }
            if (StrUtil.isNotBlank(dto.getCarrierType()) || StrUtil.isNotBlank(dto.getCarrierNum())) {
                throw new ServiceException(MessageUtils.message("no.invoice.donation.nocarrier"));
            }
            if (StrUtil.isNotBlank(dto.getBuyerUbn())) {
                throw new ServiceException(MessageUtils.message("no.invoice.donation.noubn"));
            }
        } else if ("B2C".equals(dto.getCategory())) {
            // 载具分支
            if (StrUtil.isBlank(dto.getBuyerName())) {
                throw new ServiceException(MessageUtils.message("no.invoice.buyername.blank"));
            }
            String ct = dto.getCarrierType();
            if (StrUtil.isBlank(ct)) {
                // 无载具 → 个人纸本（PrintFlag=Y，零信息兜底），无需载具校验
                return;
            }
            if (!"0".equals(ct) && !"1".equals(ct) && !"2".equals(ct)) {
                throw new ServiceException(MessageUtils.message("no.invoice.carriertype.invalid"));
            }
            if (StrUtil.isBlank(dto.getCarrierNum())) {
                throw new ServiceException(MessageUtils.message("no.invoice.carriernum.blank"));
            }
            // ezPay doc：载具号码前后不得含空白
            dto.setCarrierNum(dto.getCarrierNum().trim());
            // ezPay doc：手机条码(0)= / +7码[0-9A-Z+\-.]；自然人凭证(1)=2大写英+14数字；ezPay会员(2)=卖方自订代号，不校验
            switch (ct) {
                case "0":
                    if (!dto.getCarrierNum().matches("^/[0-9A-Z+\\-.]{7}$")) {
                        throw new ServiceException(MessageUtils.message("no.invoice.barcode.format"));
                    }
                    break;
                case "1":
                    if (!dto.getCarrierNum().matches("^[A-Z]{2}\\d{14}$")) {
                        throw new ServiceException(MessageUtils.message("no.invoice.citizen.format"));
                    }
                    break;
                case "2":
                default:
                    break;
            }
        }
    }

    /** 组装 ezPay invoice_issue 业务参数（Category/PrintFlag/Buyer/Amt/明细）。 */
    private Map<String, Object> buildIssueData(ApplyInvoiceDto dto, PosOrder order,
                                               int invoiceTotal, int sales, int tax) {
        Map<String, Object> inv = new LinkedHashMap<>();
        inv.put("MerchantOrderNo", order.getDdId());
        // ezPay Category 只有 B2C/B2B；DONATION 是我方语义，对应 ezPay B2C + LoveCode
        inv.put("Category", "DONATION".equals(dto.getCategory()) ? "B2C" : dto.getCategory());
        inv.put("BuyerName", dto.getBuyerName());
        if ("B2B".equals(dto.getCategory())) {
            inv.put("BuyerUBN", dto.getBuyerUbn());
            inv.put("BuyerEmail", dto.getBuyerEmail());   // B2B 邮箱必填：ezPay 发开立通知给买方，凭此查看
            inv.put("PrintFlag", "Y");
        } else if ("DONATION".equals(dto.getCategory())) {
            // 捐赠：LoveCode + PrintFlag=N，无载具/邮箱/统编（买方名=机构名，已在 applyInvoice 回填）
            inv.put("LoveCode", dto.getLoveCode());
            inv.put("PrintFlag", "N");
        } else {
            // B2C：有载具 → PrintFlag=N 存载具；无载具 → PrintFlag=Y 个人纸本（零信息兜底，010 Phase 11）
            if (StrUtil.isNotBlank(dto.getCarrierType())) {
                inv.put("CarrierType", dto.getCarrierType());
                inv.put("CarrierNum", dto.getCarrierNum());
                inv.put("PrintFlag", "N");
                // ezPay 手册：CarrierType=2(ezPay会员载具)时 BuyerEmail 必填(INV10013)；CarrierNum=卖方自订代号(手机/邮箱/会员编号)
                if ("2".equals(dto.getCarrierType()) && StrUtil.isNotBlank(dto.getBuyerEmail())) {
                    inv.put("BuyerEmail", dto.getBuyerEmail());
                }
            } else {
                // 个人纸本：无载具无捐赠，法定 PrintFlag=Y；不传载具/邮箱
                inv.put("PrintFlag", "Y");
            }
        }
        inv.put("TaxType", "1");
        inv.put("TaxRate", "5");
        inv.put("Amt", String.valueOf(sales));
        inv.put("TaxAmt", String.valueOf(tax));
        inv.put("TotalAmt", String.valueOf(invoiceTotal));
        appendItems(inv, order.getFood(), invoiceTotal);
        return inv;
    }

    /**
     * 逐商品明细（research D2/D3）：从 food JSON 解析，优惠按比例分摊到单价（方案 B）。
     * 每行满足 ItemCount × ItemPrice = ItemAmt（单价决定金额，ezPay 逐商品校验通过）。
     */
    private void appendItems(Map<String, Object> inv, String foodJson, int invoiceTotal) {
        JSONArray arr = StrUtil.isBlank(foodJson) ? null : com.alibaba.fastjson.JSON.parseArray(foodJson);
        if (arr == null || arr.isEmpty()) {
            inv.put("ItemName", "餐点");
            inv.put("ItemCount", "1");
            inv.put("ItemUnit", "份");
            inv.put("ItemPrice", String.valueOf(invoiceTotal));
            inv.put("ItemAmt", String.valueOf(invoiceTotal));
            return;
        }
        int foodTotal = foodOriginalTotal(arr);
        double ratio = foodTotal > 0 ? (double) invoiceTotal / foodTotal : 1.0;
        StringBuilder name = new StringBuilder(), count = new StringBuilder(),
                unit = new StringBuilder(), price = new StringBuilder(), amt = new StringBuilder();
        for (int i = 0; i < arr.size(); i++) {
            com.alibaba.fastjson.JSONObject it = arr.getJSONObject(i);
            int p = it.getIntValue("price") + it.getIntValue("otherPrice");
            int c = it.getIntValue("number");
            if (c <= 0) {
                c = 1;
            }
            int itemPrice = (int) Math.round(p * ratio);
            int itemAmt = itemPrice * c;
            if (i > 0) {
                name.append("|"); count.append("|"); unit.append("|"); price.append("|"); amt.append("|");
            }
            String nm = it.getString("name");
            name.append(StrUtil.isBlank(nm) ? "餐点" : nm);
            count.append(c);
            unit.append("個");
            price.append(itemPrice);
            amt.append(itemAmt);
        }
        inv.put("ItemName", name.toString());
        inv.put("ItemCount", count.toString());
        inv.put("ItemUnit", unit.toString());
        inv.put("ItemPrice", price.toString());
        inv.put("ItemAmt", amt.toString());
    }

    /** food 商品原价总额（含税）= Σ(price + otherPrice) × number。 */
    private int foodOriginalTotal(JSONArray arr) {
        int sum = 0;
        for (int i = 0; i < arr.size(); i++) {
            com.alibaba.fastjson.JSONObject it = arr.getJSONObject(i);
            int p = it.getIntValue("price") + it.getIntValue("otherPrice");
            int c = it.getIntValue("number");
            sum += p * (c <= 0 ? 1 : c);
        }
        return sum;
    }

    /** 新增或更新（显式设置时间与人）。 */
    private void saveOrUpdate(PosOrderInvoice row) {
        Date now = new Date();
        String user = currentUser();
        if (row.getId() == null) {
            row.setCreateTime(now);
            row.setUpdateTime(now);
            row.setCreateBy(user);
            row.setUpdateBy(user);
            posOrderInvoiceMapper.insert(row);
        } else {
            row.setUpdateTime(now);
            row.setUpdateBy(user);
            posOrderInvoiceMapper.updateById(row);
        }
    }

    private String currentUser() {
        try {
            return SecurityUtils.getUsername();
        } catch (Exception e) {
            return "system";
        }
    }
}
