package com.ruoyi.info.collection.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.info.collection.domain.CcdiAccountInfo;
import com.ruoyi.info.collection.domain.CcdiBaseStaff;
import com.ruoyi.info.collection.domain.CcdiStaffFmyRelation;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiAccountInfoExcel;
import com.ruoyi.info.collection.domain.vo.AccountInfoImportFailureVO;
import com.ruoyi.info.collection.domain.vo.CcdiAccountInfoVO;
import com.ruoyi.info.collection.domain.vo.CcdiAccountRelationOptionVO;
import com.ruoyi.info.collection.domain.vo.ImportResult;
import com.ruoyi.info.collection.mapper.CcdiAccountInfoMapper;
import com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper;
import com.ruoyi.info.collection.service.ICcdiAccountInfoService;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 账户库服务实现
 *
 * @author ruoyi
 * @date 2026-04-13
 */
@Service
public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {

    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

    private static final Set<String> OWNER_TYPES = Set.of("EMPLOYEE", "RELATION", "INTERMEDIARY", "EXTERNAL");
    private static final Set<String> ACCOUNT_TYPES = Set.of("BANK", "SECURITIES", "PAYMENT", "OTHER");
    private static final Set<String> BANK_SCOPES = Set.of("INTERNAL", "EXTERNAL");
    private static final Set<String> LEVELS = Set.of("LOW", "MEDIUM", "HIGH");
    private static final String OWNER_TYPE_INTERMEDIARY = "INTERMEDIARY";
    private static final String BANK_SCOPE_EXTERNAL = "EXTERNAL";
    private static final String RISK_LEVEL_LOW = "LOW";
    private static final String RISK_LEVEL_HIGH = "HIGH";
    private final List<AccountInfoImportFailureVO> latestImportFailures = new CopyOnWriteArrayList<>();

    @Resource
    private CcdiAccountInfoMapper accountInfoMapper;

    @Resource
    private CcdiBaseStaffMapper baseStaffMapper;

    @Resource
    private CcdiStaffFmyRelationMapper staffFmyRelationMapper;

    @Override
    public Page<CcdiAccountInfoVO> selectAccountInfoPage(Page<CcdiAccountInfoVO> page, CcdiAccountInfoQueryDTO queryDTO) {
        return accountInfoMapper.selectAccountInfoPage(page, queryDTO);
    }

    @Override
    public CcdiAccountInfoVO selectAccountInfoById(Long id) {
        return accountInfoMapper.selectAccountInfoById(id);
    }

    @Override
    @Transactional
    public int insertAccountInfo(CcdiAccountInfoAddDTO addDTO) {
        normalizeAddDto(addDTO);
        validateDto(addDTO.getOwnerType(), addDTO.getOwnerId(), addDTO.getAccountType(), addDTO.getBankScope(),
            addDTO.getStatus(), addDTO.getEffectiveDate(), addDTO.getInvalidDate(), addDTO.getTxnFrequencyLevel(),
            addDTO.getTxnRiskLevel(), addDTO.getAvgMonthTxnAmount(), addDTO.getDebitSingleMaxAmount(),
            addDTO.getCreditSingleMaxAmount(), addDTO.getDebitDailyMaxAmount(), addDTO.getCreditDailyMaxAmount());
        validateDuplicateAccountNo(addDTO.getAccountNo(), null);

        CcdiAccountInfo accountInfo = new CcdiAccountInfo();
        BeanUtils.copyProperties(addDTO, accountInfo);
        prepareAnalysisFields(accountInfo);
        return accountInfoMapper.insert(accountInfo);
    }

    @Override
    @Transactional
    public int updateAccountInfo(CcdiAccountInfoEditDTO editDTO) {
        normalizeEditDto(editDTO);
        validateDto(editDTO.getOwnerType(), editDTO.getOwnerId(), editDTO.getAccountType(), editDTO.getBankScope(),
            editDTO.getStatus(), editDTO.getEffectiveDate(), editDTO.getInvalidDate(), editDTO.getTxnFrequencyLevel(),
            editDTO.getTxnRiskLevel(), editDTO.getAvgMonthTxnAmount(), editDTO.getDebitSingleMaxAmount(),
            editDTO.getCreditSingleMaxAmount(), editDTO.getDebitDailyMaxAmount(), editDTO.getCreditDailyMaxAmount());

        CcdiAccountInfo existing = accountInfoMapper.selectById(editDTO.getId());
        if (existing == null) {
            throw new RuntimeException("账户不存在");
        }

        validateDuplicateAccountNo(editDTO.getAccountNo(), editDTO.getId());

        CcdiAccountInfo accountInfo = new CcdiAccountInfo();
        BeanUtils.copyProperties(editDTO, accountInfo);
        prepareAnalysisFields(accountInfo);
        return accountInfoMapper.updateById(accountInfo);
    }

    @Override
    @Transactional
    public int deleteAccountInfoByIds(Long[] ids) {
        return accountInfoMapper.deleteBatchIds(Arrays.asList(ids));
    }

    @Override
    public List<CcdiAccountRelationOptionVO> selectRelationOptionsByStaffId(Long staffId) {
        if (staffId == null) {
            return List.of();
        }
        return accountInfoMapper.selectRelationOptionsByStaffId(staffId);
    }

    @Override
    public List<CcdiAccountInfoExcel> selectAccountInfoListForExport(CcdiAccountInfoQueryDTO queryDTO) {
        return accountInfoMapper.selectAccountInfoListForExport(queryDTO).stream().map(this::toExcel).toList();
    }

    @Override
    @Transactional
    public ImportResult importAccountInfo(List<CcdiAccountInfoExcel> excelList, boolean updateSupport) {
        latestImportFailures.clear();
        ImportResult result = new ImportResult();
        result.setTotalCount(excelList.size());

        int successCount = 0;
        int rowNum = 1;
        for (CcdiAccountInfoExcel excel : excelList) {
            rowNum++;
            try {
                importSingleRow(excel, updateSupport);
                successCount++;
            } catch (Exception e) {
                latestImportFailures.add(buildFailure(rowNum, excel, e.getMessage()));
            }
        }

        result.setSuccessCount(successCount);
        result.setFailureCount(latestImportFailures.size());
        return result;
    }

    @Override
    public List<AccountInfoImportFailureVO> getLatestImportFailures() {
        return new ArrayList<>(latestImportFailures);
    }

    private void validateDto(String ownerType, String ownerId, String accountType, String bankScope, Integer status,
                             java.util.Date effectiveDate, java.util.Date invalidDate, String txnFrequencyLevel,
                             String txnRiskLevel, BigDecimal avgMonthTxnAmount, BigDecimal debitSingleMaxAmount,
                             BigDecimal creditSingleMaxAmount, BigDecimal debitDailyMaxAmount,
                             BigDecimal creditDailyMaxAmount) {
        if (!OWNER_TYPES.contains(ownerType)) {
            throw new RuntimeException("所属人类型不合法");
        }
        if (!ACCOUNT_TYPES.contains(accountType)) {
            throw new RuntimeException("账户类型不合法");
        }
        if (!BANK_SCOPES.contains(bankScope)) {
            throw new RuntimeException("账户范围不合法");
        }
        if (status == null || (status != 1 && status != 2)) {
            throw new RuntimeException("状态不合法");
        }
        if (effectiveDate == null) {
            throw new RuntimeException("生效日期不能为空");
        }
        if (invalidDate != null && invalidDate.before(effectiveDate)) {
            throw new RuntimeException("失效日期不能早于生效日期");
        }
        if (StringUtils.isNotEmpty(txnFrequencyLevel) && !LEVELS.contains(txnFrequencyLevel)) {
            throw new RuntimeException("频率等级不合法");
        }
        if (StringUtils.isNotEmpty(txnRiskLevel) && !LEVELS.contains(txnRiskLevel)) {
            throw new RuntimeException("风险等级不合法");
        }
        validateAmount(avgMonthTxnAmount, "月均交易金额");
        validateAmount(debitSingleMaxAmount, "借方单笔最高额");
        validateAmount(creditSingleMaxAmount, "贷方单笔最高额");
        validateAmount(debitDailyMaxAmount, "借方日累计最高额");
        validateAmount(creditDailyMaxAmount, "贷方日累计最高额");
        validateOwner(ownerType, ownerId);
    }

    private void validateOwner(String ownerType, String ownerId) {
        if (StringUtils.isEmpty(ownerId)) {
            if ("EXTERNAL".equals(ownerType) || OWNER_TYPE_INTERMEDIARY.equals(ownerType)) {
                throw new RuntimeException("证件号不能为空");
            }
            throw new RuntimeException("所属人不能为空");
        }
        if ("EXTERNAL".equals(ownerType) || OWNER_TYPE_INTERMEDIARY.equals(ownerType)) {
            return;
        }
        if ("EMPLOYEE".equals(ownerType)) {
            LambdaQueryWrapper<CcdiBaseStaff> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(CcdiBaseStaff::getIdCard, ownerId);
            CcdiBaseStaff staff = baseStaffMapper.selectOne(wrapper);
            if (staff == null) {
                throw new RuntimeException("员工不存在");
            }
            return;
        }
        LambdaQueryWrapper<CcdiStaffFmyRelation> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(CcdiStaffFmyRelation::getRelationCertNo, ownerId);
        CcdiStaffFmyRelation relation = staffFmyRelationMapper.selectOne(wrapper);
        if (relation == null) {
            throw new RuntimeException("关系人不存在");
        }
    }

    private void validateDuplicateAccountNo(String accountNo, Long excludeId) {
        LambdaQueryWrapper<CcdiAccountInfo> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(CcdiAccountInfo::getAccountNo, accountNo);
        if (excludeId != null) {
            wrapper.ne(CcdiAccountInfo::getId, excludeId);
        }
        if (accountInfoMapper.selectCount(wrapper) > 0) {
            throw new RuntimeException("账户号码已存在");
        }
    }

    private void prepareAnalysisFields(CcdiAccountInfo accountInfo) {
        if (!BANK_SCOPE_EXTERNAL.equals(accountInfo.getBankScope())) {
            clearAnalysisFields(accountInfo);
            applyIntermediaryRiskLevel(accountInfo);
            return;
        }
        if (accountInfo.getIsActualControl() == null) {
            accountInfo.setIsActualControl(1);
        }
        if (accountInfo.getAvgMonthTxnCount() == null) {
            accountInfo.setAvgMonthTxnCount(0);
        }
        if (accountInfo.getAvgMonthTxnAmount() == null) {
            accountInfo.setAvgMonthTxnAmount(BigDecimal.ZERO);
        }
        if (StringUtils.isEmpty(accountInfo.getTxnFrequencyLevel())) {
            accountInfo.setTxnFrequencyLevel("MEDIUM");
        }
        if (StringUtils.isEmpty(accountInfo.getTxnRiskLevel())) {
            accountInfo.setTxnRiskLevel(RISK_LEVEL_LOW);
        }
        applyIntermediaryRiskLevel(accountInfo);
    }

    private void clearAnalysisFields(CcdiAccountInfo accountInfo) {
        accountInfo.setIsActualControl(null);
        accountInfo.setAvgMonthTxnCount(null);
        accountInfo.setAvgMonthTxnAmount(null);
        accountInfo.setTxnFrequencyLevel(null);
        accountInfo.setDebitSingleMaxAmount(null);
        accountInfo.setCreditSingleMaxAmount(null);
        accountInfo.setDebitDailyMaxAmount(null);
        accountInfo.setCreditDailyMaxAmount(null);
        accountInfo.setTxnRiskLevel(null);
    }

    private void applyIntermediaryRiskLevel(CcdiAccountInfo accountInfo) {
        if (OWNER_TYPE_INTERMEDIARY.equals(accountInfo.getOwnerType())) {
            accountInfo.setTxnRiskLevel(RISK_LEVEL_HIGH);
        }
    }

    private void validateAmount(BigDecimal amount, String fieldLabel) {
        if (amount == null) {
            return;
        }
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new RuntimeException(fieldLabel + "不能为负数");
        }
        if (amount.scale() > 2) {
            throw new RuntimeException(fieldLabel + "最多保留2位小数");
        }
    }

    private void normalizeAddDto(CcdiAccountInfoAddDTO addDTO) {
        addDTO.setOwnerType(toUpper(addDTO.getOwnerType()));
        addDTO.setAccountType(toUpper(addDTO.getAccountType()));
        addDTO.setBankScope(toUpper(addDTO.getBankScope()));
        addDTO.setCurrency(normalizeCurrency(addDTO.getCurrency()));
        addDTO.setTxnFrequencyLevel(toUpper(addDTO.getTxnFrequencyLevel()));
        addDTO.setTxnRiskLevel(toUpper(addDTO.getTxnRiskLevel()));
        addDTO.setOwnerId(normalizeOwnerId(addDTO.getOwnerId()));
        applyIntermediaryRiskLevel(addDTO);
    }

    private void normalizeEditDto(CcdiAccountInfoEditDTO editDTO) {
        editDTO.setOwnerType(toUpper(editDTO.getOwnerType()));
        editDTO.setAccountType(toUpper(editDTO.getAccountType()));
        editDTO.setBankScope(toUpper(editDTO.getBankScope()));
        editDTO.setCurrency(normalizeCurrency(editDTO.getCurrency()));
        editDTO.setTxnFrequencyLevel(toUpper(editDTO.getTxnFrequencyLevel()));
        editDTO.setTxnRiskLevel(toUpper(editDTO.getTxnRiskLevel()));
        editDTO.setOwnerId(normalizeOwnerId(editDTO.getOwnerId()));
        applyIntermediaryRiskLevel(editDTO);
    }

    private void applyIntermediaryRiskLevel(CcdiAccountInfoAddDTO addDTO) {
        if (OWNER_TYPE_INTERMEDIARY.equals(addDTO.getOwnerType())) {
            addDTO.setTxnRiskLevel(RISK_LEVEL_HIGH);
        }
    }

    private void applyIntermediaryRiskLevel(CcdiAccountInfoEditDTO editDTO) {
        if (OWNER_TYPE_INTERMEDIARY.equals(editDTO.getOwnerType())) {
            editDTO.setTxnRiskLevel(RISK_LEVEL_HIGH);
        }
    }

    private String normalizeCurrency(String currency) {
        if (StringUtils.isEmpty(currency)) {
            return "CNY";
        }
        return currency.trim().toUpperCase(Locale.ROOT);
    }

    private String toUpper(String value) {
        if (StringUtils.isEmpty(value)) {
            return value;
        }
        return value.trim().toUpperCase(Locale.ROOT);
    }

    private String normalizeOwnerId(String ownerId) {
        if (StringUtils.isEmpty(ownerId)) {
            return ownerId;
        }
        return ownerId.trim();
    }

    private CcdiAccountInfoExcel toExcel(CcdiAccountInfoVO vo) {
        CcdiAccountInfoExcel excel = new CcdiAccountInfoExcel();
        excel.setOwnerType(ownerTypeLabel(vo.getOwnerType()));
        excel.setOwnerId(vo.getOwnerId());
        excel.setAccountName(vo.getAccountName());
        excel.setAccountNo(vo.getAccountNo());
        excel.setAccountType(accountTypeLabel(vo.getAccountType()));
        excel.setBankScope(bankScopeLabel(vo.getBankScope()));
        excel.setOpenBank(vo.getOpenBank());
        excel.setBankCode(vo.getBankCode());
        excel.setCurrency(vo.getCurrency());
        excel.setStatus(vo.getStatus() == null ? "" : (vo.getStatus() == 1 ? "正常" : "已销户"));
        excel.setEffectiveDate(formatDate(vo.getEffectiveDate()));
        excel.setInvalidDate(formatDate(vo.getInvalidDate()));
        excel.setIsActualControl(formatYesNo(vo.getIsActualControl()));
        excel.setAvgMonthTxnCount(vo.getAvgMonthTxnCount() == null ? "" : String.valueOf(vo.getAvgMonthTxnCount()));
        excel.setAvgMonthTxnAmount(formatNumber(vo.getAvgMonthTxnAmount()));
        excel.setTxnFrequencyLevel(StringUtils.isEmpty(vo.getTxnFrequencyLevel()) ? "" : vo.getTxnFrequencyLevel());
        excel.setDebitSingleMaxAmount(formatNumber(vo.getDebitSingleMaxAmount()));
        excel.setCreditSingleMaxAmount(formatNumber(vo.getCreditSingleMaxAmount()));
        excel.setDebitDailyMaxAmount(formatNumber(vo.getDebitDailyMaxAmount()));
        excel.setCreditDailyMaxAmount(formatNumber(vo.getCreditDailyMaxAmount()));
        excel.setTxnRiskLevel(StringUtils.isEmpty(vo.getTxnRiskLevel()) ? "" : vo.getTxnRiskLevel());
        return excel;
    }

    private void importSingleRow(CcdiAccountInfoExcel excel, boolean updateSupport) {
        CcdiAccountInfo existing = findByAccountNo(excel.getAccountNo());
        if (existing != null && !updateSupport) {
            throw new RuntimeException("账户号码已存在，请勾选更新已存在数据后重试");
        }

        if (existing != null) {
            CcdiAccountInfoEditDTO editDTO = toEditDto(existing.getId(), excel);
            updateAccountInfo(editDTO);
            return;
        }

        CcdiAccountInfoAddDTO addDTO = toAddDto(excel);
        insertAccountInfo(addDTO);
    }

    private CcdiAccountInfoAddDTO toAddDto(CcdiAccountInfoExcel excel) {
        CcdiAccountInfoAddDTO dto = new CcdiAccountInfoAddDTO();
        String ownerType = parseOwnerType(excel.getOwnerType());
        dto.setOwnerType(ownerType);
        dto.setOwnerId(normalizeOwnerId(excel.getOwnerId()));
        dto.setAccountName(trimToNull(excel.getAccountName()));
        dto.setAccountNo(trimToNull(excel.getAccountNo()));
        dto.setAccountType(parseAccountType(excel.getAccountType()));
        dto.setBankScope(parseBankScope(excel.getBankScope()));
        dto.setOpenBank(trimToNull(excel.getOpenBank()));
        dto.setBankCode(trimToNull(excel.getBankCode()));
        dto.setCurrency(normalizeCurrency(excel.getCurrency()));
        dto.setStatus(parseStatus(excel.getStatus()));
        dto.setEffectiveDate(parseDateRequired(excel.getEffectiveDate(), "生效日期"));
        dto.setInvalidDate(parseDateOptional(excel.getInvalidDate()));
        dto.setIsActualControl(parseBooleanFlag(excel.getIsActualControl(), 1));
        dto.setAvgMonthTxnCount(parseInteger(excel.getAvgMonthTxnCount()));
        dto.setAvgMonthTxnAmount(parseDecimal(excel.getAvgMonthTxnAmount()));
        dto.setTxnFrequencyLevel(parseLevel(excel.getTxnFrequencyLevel(), "频率等级"));
        dto.setDebitSingleMaxAmount(parseDecimal(excel.getDebitSingleMaxAmount()));
        dto.setCreditSingleMaxAmount(parseDecimal(excel.getCreditSingleMaxAmount()));
        dto.setDebitDailyMaxAmount(parseDecimal(excel.getDebitDailyMaxAmount()));
        dto.setCreditDailyMaxAmount(parseDecimal(excel.getCreditDailyMaxAmount()));
        dto.setTxnRiskLevel(OWNER_TYPE_INTERMEDIARY.equals(ownerType)
            ? RISK_LEVEL_HIGH
            : parseLevel(excel.getTxnRiskLevel(), "风险等级"));
        return dto;
    }

    private CcdiAccountInfoEditDTO toEditDto(Long id, CcdiAccountInfoExcel excel) {
        CcdiAccountInfoEditDTO dto = new CcdiAccountInfoEditDTO();
        BeanUtils.copyProperties(toAddDto(excel), dto);
        dto.setId(id);
        return dto;
    }

    private CcdiAccountInfo findByAccountNo(String accountNo) {
        LambdaQueryWrapper<CcdiAccountInfo> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(CcdiAccountInfo::getAccountNo, trimToNull(accountNo));
        return accountInfoMapper.selectOne(wrapper);
    }

    private AccountInfoImportFailureVO buildFailure(int rowNum, CcdiAccountInfoExcel excel, String errorMessage) {
        AccountInfoImportFailureVO failure = new AccountInfoImportFailureVO();
        failure.setRowNum(rowNum);
        failure.setOwnerType(excel.getOwnerType());
        failure.setOwnerId(excel.getOwnerId());
        failure.setAccountNo(excel.getAccountNo());
        failure.setErrorMessage(errorMessage);
        return failure;
    }

    private String parseOwnerType(String value) {
        String normalized = toUpper(value);
        if ("员工".equals(value)) {
            return "EMPLOYEE";
        }
        if ("员工关系人".equals(value)) {
            return "RELATION";
        }
        if ("中介".equals(value)) {
            return OWNER_TYPE_INTERMEDIARY;
        }
        if ("外部人员".equals(value)) {
            return "EXTERNAL";
        }
        return normalized;
    }

    private String parseAccountType(String value) {
        String normalized = toUpper(value);
        return switch (normalized) {
            case "银行账户" -> "BANK";
            case "证券账户" -> "SECURITIES";
            case "支付账户" -> "PAYMENT";
            case "其他" -> "OTHER";
            default -> normalized;
        };
    }

    private String parseBankScope(String value) {
        String normalized = toUpper(value);
        return switch (normalized) {
            case "行内" -> "INTERNAL";
            case "行外" -> "EXTERNAL";
            default -> normalized;
        };
    }

    private Integer parseStatus(String value) {
        String normalized = trimToNull(value);
        if (normalized == null) {
            return null;
        }
        return switch (normalized) {
            case "1", "正常" -> 1;
            case "2", "已销户" -> 2;
            default -> throw new RuntimeException("状态仅支持“正常/已销户”或“1/2”");
        };
    }

    private Integer parseBooleanFlag(String value, Integer defaultValue) {
        String normalized = trimToNull(value);
        if (normalized == null) {
            return defaultValue;
        }
        return switch (normalized) {
            case "1", "是", "Y", "YES", "TRUE", "true" -> 1;
            case "0", "否", "N", "NO", "FALSE", "false" -> 0;
            default -> throw new RuntimeException("是否实控账户仅支持“是/否”或“1/0”");
        };
    }

    private Integer parseInteger(String value) {
        String normalized = trimToNull(value);
        if (normalized == null) {
            return null;
        }
        try {
            return Integer.valueOf(normalized);
        } catch (NumberFormatException e) {
            throw new RuntimeException("月均交易笔数格式不正确");
        }
    }

    private BigDecimal parseDecimal(String value) {
        String normalized = trimToNull(value);
        if (normalized == null) {
            return null;
        }
        try {
            return new BigDecimal(normalized);
        } catch (NumberFormatException e) {
            throw new RuntimeException("金额字段格式不正确");
        }
    }

    private String parseLevel(String value, String fieldLabel) {
        String normalized = toUpper(value);
        if (StringUtils.isEmpty(normalized)) {
            return null;
        }
        return switch (normalized) {
            case "低", "LOW" -> "LOW";
            case "中", "MEDIUM" -> "MEDIUM";
            case "高", "HIGH" -> "HIGH";
            default -> throw new RuntimeException(fieldLabel + "仅支持 LOW/MEDIUM/HIGH");
        };
    }

    private Date parseDateRequired(String value, String fieldLabel) {
        Date date = parseDateOptional(value);
        if (date == null) {
            throw new RuntimeException(fieldLabel + "不能为空");
        }
        return date;
    }

    private Date parseDateOptional(String value) {
        String normalized = trimToNull(value);
        if (normalized == null) {
            return null;
        }
        try {
            synchronized (DATE_FORMAT) {
                return DATE_FORMAT.parse(normalized);
            }
        } catch (ParseException e) {
            throw new RuntimeException("日期格式需为 yyyy-MM-dd");
        }
    }

    private String trimToNull(String value) {
        if (StringUtils.isEmpty(value)) {
            return null;
        }
        return value.trim();
    }

    private String ownerTypeLabel(String value) {
        return switch (value) {
            case "EMPLOYEE" -> "员工";
            case "RELATION" -> "员工关系人";
            case "INTERMEDIARY" -> "中介";
            case "EXTERNAL" -> "外部人员";
            default -> value;
        };
    }

    private String accountTypeLabel(String value) {
        return switch (value) {
            case "BANK" -> "银行账户";
            case "SECURITIES" -> "证券账户";
            case "PAYMENT" -> "支付账户";
            case "OTHER" -> "其他";
            default -> value;
        };
    }

    private String bankScopeLabel(String value) {
        return switch (value) {
            case "INTERNAL" -> "行内";
            case "EXTERNAL" -> "行外";
            default -> value;
        };
    }

    private String formatYesNo(Integer value) {
        if (value == null) {
            return "";
        }
        return value == 1 ? "是" : "否";
    }

    private String formatDate(Date value) {
        if (value == null) {
            return "";
        }
        synchronized (DATE_FORMAT) {
            return DATE_FORMAT.format(value);
        }
    }

    private String formatNumber(Number value) {
        return value == null ? "" : String.valueOf(value);
    }
}
