package com.writeoff.module.meeting.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.writeoff.common.exception.BusinessException;
import com.writeoff.common.exception.ErrorCodes;
import com.writeoff.module.expert.dto.AddBankCardRequest;
import com.writeoff.module.expert.dto.CreateExpertRequest;
import com.writeoff.module.expert.model.ExpertInfo;
import com.writeoff.module.expert.service.PlatformExpertService;
import com.writeoff.module.meeting.dto.BindMeetingExpertsRequest;
import com.writeoff.module.meeting.dto.MeetingLaborAgreementExtractApplyRequest;
import com.writeoff.module.meeting.dto.MeetingLaborAgreementExtractSubmitRequest;
import com.writeoff.module.meeting.model.Meeting;
import com.writeoff.module.meeting.model.MeetingAuditStatus;
import com.writeoff.module.meeting.model.MeetingLaborAgreementExtractResult;
import com.writeoff.module.ocr.dto.DocumentExtractTaskQueryResponse;
import com.writeoff.module.ocr.dto.DocumentExtractTaskSubmitRequest;
import com.writeoff.module.ocr.dto.DocumentExtractTaskSubmitResponse;
import com.writeoff.module.ocr.service.BaiduDocumentExtractService;
import com.writeoff.module.system.model.PlatformDictionaryItem;
import com.writeoff.module.system.service.PlatformDictionaryService;
import com.writeoff.security.AuthContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

@Service
public class MeetingLaborAgreementExtractService {
    private static final String MODULE_CODE = "EXPERT_LIST";
    private static final String HOSPITAL_DICT_TYPE = "EXPERT_HOSPITAL";
    private static final List<String> OCR_KEYS_NAME = asList("涔欐柟");
    private static final List<String> OCR_KEYS_HOSPITAL = asList("宸ヤ綔鍗曚綅");
    private static final List<String> OCR_KEYS_PHONE = asList("鑱旂郴鐢佃瘽");
    private static final List<String> OCR_KEYS_FEE = asList("鍔冲姟璐?);
    private static final List<String> OCR_KEYS_BANK_NAME = asList("寮€鎴烽摱琛?);
    private static final List<String> OCR_KEYS_BANK_CARD = asList("寮€鎴峰笎鍙?, "寮€鎴疯处鍙?);
    private static final List<String> OCR_KEYS_ID_NO = asList("韬唤璇佸彿鐮?, "韬唤璇佸彿");
    private static final List<String> OCR_KEYS_ACCOUNT_NAME = asList("璐︽埛鍚?);

    private final MeetingService meetingService;
    private final BaiduDocumentExtractService documentExtractService;
    private final PlatformExpertService platformExpertService;
    private final PlatformDictionaryService platformDictionaryService;
    private final MeetingExpertBindingService meetingExpertBindingService;
    private final MeetingMaterialService meetingMaterialService;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public MeetingLaborAgreementExtractService(MeetingService meetingService,
                                               BaiduDocumentExtractService documentExtractService,
                                               PlatformExpertService platformExpertService,
                                               PlatformDictionaryService platformDictionaryService,
                                               MeetingExpertBindingService meetingExpertBindingService,
                                               MeetingMaterialService meetingMaterialService) {
        this.meetingService = meetingService;
        this.documentExtractService = documentExtractService;
        this.platformExpertService = platformExpertService;
        this.platformDictionaryService = platformDictionaryService;
        this.meetingExpertBindingService = meetingExpertBindingService;
        this.meetingMaterialService = meetingMaterialService;
    }

    public DocumentExtractTaskSubmitResponse submit(Long meetingId, MeetingLaborAgreementExtractSubmitRequest request) {
        assertMeetingEditable(meetingId);
        DocumentExtractTaskSubmitRequest submitRequest = new DocumentExtractTaskSubmitRequest();
        submitRequest.setObjectKey(trimToNull(request.getObjectKey()));
        submitRequest.setFileName(trimToNull(request.getFileName()));
        submitRequest.setManifest(buildManifest());
        submitRequest.setRemoveDuplicates(Boolean.TRUE);
        submitRequest.setEraseWatermark(Boolean.TRUE);
        return documentExtractService.submitTask(submitRequest);
    }

    public MeetingLaborAgreementExtractResult query(Long meetingId, String taskId) {
        assertMeetingEditable(meetingId);
        return buildResult(documentExtractService.queryTask(taskId));
    }

    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> apply(Long meetingId, MeetingLaborAgreementExtractApplyRequest request) {
        assertMeetingEditable(meetingId);
        MeetingLaborAgreementExtractResult result = buildResult(documentExtractService.queryTask(request.getTaskId()));
        if (!"Success".equalsIgnoreCase(trimToEmpty(result.getStatus()))) {
            throw new BusinessException(ErrorCodes.INVALID_STATE, "OCR浠诲姟鏈畬鎴愶紝涓嶈兘搴旂敤");
        }
        MeetingLaborAgreementExtractResult.ParsedExpert parsed = result.getParsedExpert();
        if (parsed == null) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "鏈В鏋愬埌涓撳淇℃伅");
        }
        String idNo = trimToNull(parsed.getIdNo());
        if (idNo == null) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "鏈瘑鍒埌韬唤璇佸彿鐮?);
        }

        ExpertInfo existing = platformExpertService.findByExactIdNo(idNo);
        Long requestedExpertId = request.getExistingExpertId();
        boolean updateExisting = Boolean.TRUE.equals(request.getUpdateExistingExpert());
        ExpertInfo targetExpert;
        boolean created = false;
        boolean updated = false;

        if (existing != null) {
            if (!updateExisting) {
                throw new BusinessException(ErrorCodes.INVALID_STATE, "宸插瓨鍦ㄥ悓韬唤璇佷笓瀹讹紝璇风‘璁ゆ槸鍚﹀鐢ㄥ苟鏇存柊");
            }
            if (requestedExpertId == null || !existing.getId().equals(requestedExpertId)) {
                throw new BusinessException(ErrorCodes.INVALID_STATE, "纭涓撳涓庣郴缁熷尮閰嶇粨鏋滀笉涓€鑷?);
            }
            targetExpert = updateExistingExpert(existing, parsed);
            updated = true;
        } else {
            targetExpert = createExpert(parsed);
            created = true;
        }

        upsertBankCard(targetExpert.getId(), parsed);
        bindExpertToMeeting(meetingId, targetExpert.getId());
        saveLaborToMeeting(meetingId, targetExpert, parsed, request.getObjectKey(), request.getFileName());

        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("taskId", request.getTaskId());
        data.put("expertId", targetExpert.getId());
        data.put("createdExpert", created);
        data.put("updatedExpert", updated);
        data.put("boundMeeting", true);
        data.put("nameMismatchFlag", Boolean.TRUE.equals(parsed.getNameMismatchFlag()));
        return data;
    }

    private MeetingLaborAgreementExtractResult buildResult(DocumentExtractTaskQueryResponse response) {
        MeetingLaborAgreementExtractResult result = new MeetingLaborAgreementExtractResult();
        result.setTaskId(response.getTaskId());
        result.setStatus(response.getStatus());
        result.setReason(response.getReason());
        result.setCreatedAt(response.getCreatedAt());
        result.setStartedAt(response.getStartedAt());
        result.setFinishedAt(response.getFinishedAt());
        result.setDuration(response.getDuration());
        result.setLogId(response.getLogId());
        if (!"Success".equalsIgnoreCase(trimToEmpty(response.getStatus()))) {
            return result;
        }
        MeetingLaborAgreementExtractResult.ParsedExpert parsedExpert = parseExtractedExpert(response.getRaw());
        result.setParsedExpert(parsedExpert);
        ExpertInfo existing = parsedExpert == null ? null : platformExpertService.findByExactIdNo(parsedExpert.getIdNo());
        if (existing != null) {
            MeetingLaborAgreementExtractResult.ExistingExpert existingExpert = new MeetingLaborAgreementExtractResult.ExistingExpert();
            existingExpert.setExpertId(existing.getId());
            existingExpert.setExpertName(existing.getExpertName());
            existingExpert.setPhoneMasked(existing.getPhone());
            existingExpert.setIdNoMasked(existing.getIdNo());
            existingExpert.setHospitalName(existing.getOrganization());
            result.setExistingExpert(existingExpert);
            result.setNeedsConfirm(Boolean.TRUE);
        } else {
            result.setNeedsConfirm(Boolean.FALSE);
        }
        result.setNameMismatchFlag(parsedExpert != null && Boolean.TRUE.equals(parsedExpert.getNameMismatchFlag()));
        return result;
    }

    private MeetingLaborAgreementExtractResult.ParsedExpert parseExtractedExpert(Map<String, Object> raw) {
        Map<String, Object> resultMap = asMap(raw == null ? null : raw.get("result"));
        List<Map<String, Object>> extractResults = listOfMap(resultMap.get("extractResult"));
        if (extractResults.isEmpty()) {
            return null;
        }
        Map<String, Object> first = extractResults.get(0);
        Map<String, Object> data = asMap(first.get("data"));
        Map<String, Object> singleKey = asMap(data.get("singleKey"));
        MeetingLaborAgreementExtractResult.ParsedExpert expert = new MeetingLaborAgreementExtractResult.ParsedExpert();
        expert.setExpertName(firstWord(singleKey, OCR_KEYS_NAME));
        expert.setHospitalName(firstWord(singleKey, OCR_KEYS_HOSPITAL));
        expert.setPhone(normalizePhone(firstWord(singleKey, OCR_KEYS_PHONE)));
        String laborFeeText = firstWord(singleKey, OCR_KEYS_FEE);
        expert.setLaborFeeText(laborFeeText);
        expert.setLaborFeeCent(parseAmountCent(laborFeeText));
        expert.setBankName(firstWord(singleKey, OCR_KEYS_BANK_NAME));
        expert.setBankCardNo(normalizeBankCardNo(firstWord(singleKey, OCR_KEYS_BANK_CARD)));
        expert.setAccountName(firstWord(singleKey, OCR_KEYS_ACCOUNT_NAME));
        expert.setIdNo(normalizeIdNo(firstWord(singleKey, OCR_KEYS_ID_NO)));
        boolean mismatch = hasText(expert.getExpertName())
                && hasText(expert.getAccountName())
                && !trimToEmpty(expert.getExpertName()).equals(trimToEmpty(expert.getAccountName()));
        expert.setNameMismatchFlag(mismatch);
        return expert;
    }

    private ExpertInfo createExpert(MeetingLaborAgreementExtractResult.ParsedExpert parsed) {
        PlatformDictionaryItem hospitalItem = ensureHospitalDictionary(parsed.getHospitalName());
        CreateExpertRequest request = new CreateExpertRequest();
        request.setExpertName(trimToEmpty(parsed.getExpertName()));
        request.setIdNo(trimToEmpty(parsed.getIdNo()));
        request.setPhone(trimToEmpty(parsed.getPhone()));
        request.setHospitalCode(hospitalItem == null ? null : hospitalItem.getDictCode());
        request.setOrganization(hospitalItem == null ? trimToEmpty(parsed.getHospitalName()) : hospitalItem.getDictName());
        return platformExpertService.create(request);
    }

    private ExpertInfo updateExistingExpert(ExpertInfo existing, MeetingLaborAgreementExtractResult.ParsedExpert parsed) {
        PlatformDictionaryItem hospitalItem = ensureHospitalDictionary(parsed.getHospitalName());
        CreateExpertRequest request = new CreateExpertRequest();
        request.setExpertName(hasText(parsed.getExpertName()) ? parsed.getExpertName().trim() : existing.getExpertName());
        request.setIdNo(existing.getIdNo());
        request.setPhone(hasText(parsed.getPhone()) ? parsed.getPhone().trim() : existing.getPhone());
        request.setGender(existing.getGender());
        request.setBirthday(existing.getBirthday());
        request.setIdCardValidUntil(existing.getIdCardValidUntil());
        request.setIdCardFrontOssKey(existing.getIdCardFrontOssKey());
        request.setIdCardBackOssKey(existing.getIdCardBackOssKey());
        request.setTitleCode(existing.getTitleCode());
        request.setTitle(existing.getTitle());
        request.setHospitalCode(hospitalItem == null ? existing.getHospitalCode() : hospitalItem.getDictCode());
        request.setOrganization(hospitalItem == null
                ? trimToEmpty(existing.getOrganization())
                : hospitalItem.getDictName());
        request.setExportRestricted(existing.getExportRestricted());
        return platformExpertService.update(existing.getId(), request);
    }

    private void upsertBankCard(Long expertId, MeetingLaborAgreementExtractResult.ParsedExpert parsed) {
        if (expertId == null || expertId <= 0L) {
            return;
        }
        AddBankCardRequest request = new AddBankCardRequest();
        request.setBankName(trimToEmpty(parsed.getBankName()));
        request.setBankCardNo(trimToEmpty(parsed.getBankCardNo()));
        request.setAccountName(hasText(parsed.getAccountName()) ? parsed.getAccountName().trim() : trimToEmpty(parsed.getExpertName()));
        request.setIsDefault(Boolean.TRUE);
        boolean mismatch = Boolean.TRUE.equals(parsed.getNameMismatchFlag());
        request.setInconsistentNameApproved(mismatch ? Boolean.TRUE : Boolean.FALSE);
        request.setChangeReason(mismatch ? "鍔冲姟鍗忚OCR璇嗗埆鍒拌处鎴峰悕涓庝箼鏂逛笉涓€鑷达紝宸叉墦鏍囦繚瀛? : "鍔冲姟鍗忚OCR鑷姩瀵煎叆");
        platformExpertService.addOrUpdateDefaultCard(expertId, request);
    }

    private void bindExpertToMeeting(Long meetingId, Long expertId) {
        List<Long> ids = new ArrayList<Long>();
        List<?> existing = meetingExpertBindingService.listByMeetingId(meetingId);
        for (Object obj : existing) {
            if (!(obj instanceof com.writeoff.module.meeting.model.MeetingExpertBinding)) {
                continue;
            }
            com.writeoff.module.meeting.model.MeetingExpertBinding row = (com.writeoff.module.meeting.model.MeetingExpertBinding) obj;
            if (row.getExpertId() != null && row.getExpertId() > 0L) {
                ids.add(row.getExpertId());
            }
        }
        ids.add(expertId);
        Set<Long> unique = new LinkedHashSet<Long>(ids);
        BindMeetingExpertsRequest bindRequest = new BindMeetingExpertsRequest();
        bindRequest.setExpertIds(new ArrayList<Long>(unique));
        meetingExpertBindingService.bind(meetingId, bindRequest);
    }

    private void saveLaborToMeeting(Long meetingId, ExpertInfo expert, MeetingLaborAgreementExtractResult.ParsedExpert parsed, String protocolObjectKey, String protocolFileName) {
        String contentJson = meetingMaterialService.currentContentJsonOrEmpty(meetingId, MODULE_CODE);
        Map<String, Object> root = parseJsonObject(contentJson);
        Map<String, Object> onsitePhoto = ensureMap(root, "onsitePhoto");
        if (!(onsitePhoto.get("photos") instanceof List)) {
            onsitePhoto.put("photos", new ArrayList<Object>());
        }
        Map<String, Object> laborProtocol = ensureMap(root, "laborProtocol");
        List<Map<String, Object>> details = ensureListOfMap(laborProtocol, "details");
        Map<String, Object> invoiceDetail = ensureMap(root, "invoiceDetail");
        if (!(invoiceDetail.get("invoices") instanceof List)) {
            invoiceDetail.put("invoices", new ArrayList<Object>());
        }

        long expertId = expert.getId() == null ? 0L : expert.getId();
        Map<String, Object> targetRow = null;
        for (Map<String, Object> row : details) {
            if (longValue(row.get("expertId")) == expertId) {
                targetRow = row;
                break;
            }
        }
        if (targetRow == null) {
            targetRow = new LinkedHashMap<String, Object>();
            details.add(targetRow);
        }
        targetRow.put("expertId", expertId);
        targetRow.put("expertName", trimToEmpty(expert.getExpertName()));
        Map<String, Object> protocolFile = ensureMap(targetRow, "protocolFile");
        String protocolOssKey = trimToEmpty(protocolObjectKey);
        String protocolName = trimToEmpty(protocolFileName);
        if (hasText(protocolOssKey)) {
            protocolFile.put("name", protocolName);
            protocolFile.put("fileName", protocolName);
            protocolFile.put("ossKey", protocolOssKey);
        } else if (!hasText(protocolFile.get("ossKey"))) {
            protocolFile.put("name", "");
            protocolFile.put("fileName", "");
            protocolFile.put("ossKey", "");
        }
        Map<String, Object> invoiceFile = ensureMap(targetRow, "invoiceFile");
        if (!hasText(invoiceFile.get("ossKey"))) {
            invoiceFile.put("name", "");
            invoiceFile.put("ossKey", "");
        }
        if (!(targetRow.get("invoiceFiles") instanceof List)) {
            targetRow.put("invoiceFiles", new ArrayList<Object>());
        }
        targetRow.put("amountCent", parsed.getLaborFeeCent() == null ? 0L : parsed.getLaborFeeCent());
        String remark = Boolean.TRUE.equals(parsed.getNameMismatchFlag())
                ? "OCR璇嗗埆鎻愮ず锛氫箼鏂逛笌璐︽埛鍚嶄笉涓€鑷达紝璇蜂汉宸ュ鏍?
                : "";
        targetRow.put("remark", remark);
        meetingMaterialService.saveRawContent(meetingId, MODULE_CODE, toJson(root), "鍔冲姟鍗忚OCR鑷姩瀵煎叆");
    }

    private PlatformDictionaryItem ensureHospitalDictionary(String hospitalName) {
        String name = trimToNull(hospitalName);
        if (name == null) {
            return null;
        }
        PlatformDictionaryItem existing = platformDictionaryService.findEnabledItemByName(HOSPITAL_DICT_TYPE, name);
        if (existing != null) {
            return existing;
        }
        return platformDictionaryService.createEnabledItem(HOSPITAL_DICT_TYPE, name, "AUTO_HOSPITAL", "鍔冲姟鍗忚OCR鑷姩鍒涘缓");
    }

    private void assertMeetingEditable(Long meetingId) {
        Meeting meeting = meetingService.getById(meetingId);
        MeetingAuditStatus auditStatus = meeting.getAuditStatus();
        if (auditStatus == MeetingAuditStatus.IN_REVIEW || auditStatus == MeetingAuditStatus.APPROVED) {
            throw new BusinessException(ErrorCodes.INVALID_STATE, "璇ヤ細璁祫鏂欏鏍镐腑鎴栧凡瀹℃牳閫氳繃锛屼笉鍏佽鍐嶄慨鏀?);
        }
    }

    private List<DocumentExtractTaskSubmitRequest.ManifestField> buildManifest() {
        List<DocumentExtractTaskSubmitRequest.ManifestField> list = new ArrayList<DocumentExtractTaskSubmitRequest.ManifestField>();
        list.add(manifestField("涔欐柟"));
        list.add(manifestField("宸ヤ綔鍗曚綅"));
        list.add(manifestField("鑱旂郴鐢佃瘽"));
        list.add(manifestField("鍔冲姟璐?));
        list.add(manifestField("寮€鎴烽摱琛?));
        list.add(manifestField("寮€鎴峰笎鍙?));
        list.add(manifestField("璐︽埛鍚?));
        list.add(manifestField("韬唤璇佸彿鐮?));
        return list;
    }

    private DocumentExtractTaskSubmitRequest.ManifestField manifestField(String key) {
        DocumentExtractTaskSubmitRequest.ManifestField field = new DocumentExtractTaskSubmitRequest.ManifestField();
        field.setKey(key);
        field.setParentKey("root");
        field.setDescription("");
        return field;
    }

    private static List<String> asList(String... values) {
        List<String> list = new ArrayList<String>();
        if (values != null) {
            for (String value : values) {
                list.add(value);
            }
        }
        return list;
    }

    private String firstWord(Map<String, Object> singleKey, List<String> keys) {
        for (String key : keys) {
            Object rowsObj = singleKey.get(key);
            if (!(rowsObj instanceof Collection)) {
                continue;
            }
            for (Object one : (Collection<?>) rowsObj) {
                if (!(one instanceof Map)) {
                    continue;
                }
                String word = trimToNull(((Map<?, ?>) one).get("word"));
                if (word != null) {
                    return word;
                }
            }
        }
        return "";
    }

    private Long parseAmountCent(String text) {
        String raw = trimToNull(text);
        if (raw == null) {
            return 0L;
        }
        String normalized = raw.replace(",", "").replace("锛?, "").replace("鍏?, "").replace("浜烘皯甯?, "").trim();
        if (normalized.isEmpty()) {
            return 0L;
        }
        StringBuilder builder = new StringBuilder();
        boolean dotSeen = false;
        for (int i = 0; i < normalized.length(); i++) {
            char ch = normalized.charAt(i);
            if (Character.isDigit(ch)) {
                builder.append(ch);
            } else if (ch == '.' && !dotSeen) {
                builder.append(ch);
                dotSeen = true;
            }
        }
        if (builder.length() == 0) {
            return 0L;
        }
        try {
            BigDecimal yuan = new BigDecimal(builder.toString());
            return yuan.movePointRight(2).setScale(0, BigDecimal.ROUND_HALF_UP).longValue();
        } catch (Exception ex) {
            return 0L;
        }
    }

    private Map<String, Object> parseJsonObject(String json) {
        if (!hasText(json)) {
            return new LinkedHashMap<String, Object>();
        }
        try {
            return objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {});
        } catch (Exception ex) {
            return new LinkedHashMap<String, Object>();
        }
    }

    private String toJson(Object value) {
        try {
            return objectMapper.writeValueAsString(value);
        } catch (Exception ex) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "JSON搴忓垪鍖栧け璐?);
        }
    }

    private Map<String, Object> ensureMap(Map<String, Object> parent, String key) {
        Map<String, Object> value = asMap(parent.get(key));
        if (value.isEmpty() && !(parent.get(key) instanceof Map)) {
            value = new LinkedHashMap<String, Object>();
            parent.put(key, value);
        } else if (!(parent.get(key) instanceof Map)) {
            parent.put(key, value);
        }
        return value;
    }

    private List<Map<String, Object>> ensureListOfMap(Map<String, Object> parent, String key) {
        Object value = parent.get(key);
        if (!(value instanceof List)) {
            List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
            parent.put(key, list);
            return list;
        }
        List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
        for (Object one : (List<?>) value) {
            if (one instanceof Map) {
                list.add(new LinkedHashMap<String, Object>((Map<String, Object>) one));
            }
        }
        parent.put(key, list);
        return list;
    }

    private Map<String, Object> asMap(Object value) {
        if (!(value instanceof Map)) {
            return new LinkedHashMap<String, Object>();
        }
        return new LinkedHashMap<String, Object>((Map<String, Object>) value);
    }

    private List<Map<String, Object>> listOfMap(Object value) {
        List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
        if (!(value instanceof Collection)) {
            return list;
        }
        for (Object one : (Collection<?>) value) {
            if (one instanceof Map) {
                list.add(new LinkedHashMap<String, Object>((Map<String, Object>) one));
            }
        }
        return list;
    }

    private long longValue(Object value) {
        if (value instanceof Number) {
            return ((Number) value).longValue();
        }
        try {
            return Long.parseLong(trimToEmpty(value));
        } catch (Exception ex) {
            return 0L;
        }
    }

    private String normalizeBankCardNo(String value) {
        String raw = trimToNull(value);
        if (raw == null) {
            return "";
        }
        return raw.replaceAll("\\s+", "");
    }

    private String normalizePhone(String value) {
        String raw = trimToNull(value);
        if (raw == null) {
            return "";
        }
        return raw.replaceAll("[^0-9]", "");
    }

    private String normalizeIdNo(String value) {
        String raw = trimToNull(value);
        if (raw == null) {
            return "";
        }
        return raw.replace(" ", "").toUpperCase(Locale.ROOT);
    }

    private boolean hasText(Object value) {
        return trimToNull(value) != null;
    }

    private String trimToEmpty(Object value) {
        String result = trimToNull(value);
        return result == null ? "" : result;
    }

    private String trimToNull(Object value) {
        if (value == null) {
            return null;
        }
        String text = String.valueOf(value).trim();
        return text.isEmpty() ? null : text;
    }
}


