package com.gxwebsoft.house.ai;

import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.gxwebsoft.house.entity.HouseAiIntent;
import com.gxwebsoft.house.entity.HouseInfo;
import com.gxwebsoft.house.param.HouseInfoParam;
import com.gxwebsoft.house.service.HouseInfoService;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * AI找房搜索引擎，封装精确匹配和近似推荐。
 */
@Component
public class HouseAiSearchEngine {

    private static final Pattern NUMBER_PATTERN = Pattern.compile("(\\d+(?:\\.\\d+)?)");
    private static final Pattern HOUSE_TYPE_COMPARTMENT_PATTERN = Pattern.compile("([一二两三四五六七八九十0-9]{1,2})\\s*隔间");
    private static final Pattern HOUSE_TYPE_ROOM_HALL_PATTERN = Pattern.compile("([一二两三四五六七八九十0-9]{1,2})\\s*室\\s*([一二两三四五六七八九十0-9]{1,2})\\s*厅");
    private static final int EXACT_HOUSE_LIMIT = 10;
    private static final int APPROXIMATE_HOUSE_LIMIT = 5;
    private static final BigDecimal RELAX_RATE = new BigDecimal("0.20");
    private static final BigDecimal RELAX_MIN_RATE = BigDecimal.ONE.subtract(RELAX_RATE);
    private static final BigDecimal RELAX_MAX_RATE = BigDecimal.ONE.add(RELAX_RATE);
    private static final long PRICE_SCORE_WEIGHT = 1000000L;
    private static final long EXTENT_SCORE_WEIGHT = 10000L;
    private static final long HOUSE_TYPE_SCORE_WEIGHT = 1000L;
    private static final long DETAIL_SCORE_WEIGHT = 100L;

    @Resource
    private HouseInfoService houseInfoService;

    public HouseAiSearchResult search(HouseAiIntent intent, String question) {
        return search(intent, question, null);
    }

    public HouseAiSearchResult search(HouseAiIntent intent, String question, Integer tenantId) {
        List<HouseInfo> structuredHouses = searchStructuredHouses(intent, question, tenantId);
        if (!structuredHouses.isEmpty()) {
            return HouseAiSearchResult.exact(structuredHouses);
        }

        List<HouseInfo> approximateHouses = searchApproximateHouses(intent, tenantId);
        if (!approximateHouses.isEmpty()) {
            return HouseAiSearchResult.approximate(approximateHouses);
        }

        return HouseAiSearchResult.none();
    }

    private List<HouseInfo> searchStructuredHouses(HouseAiIntent intent, String question, Integer tenantId) {
        HouseInfoParam param = new HouseInfoParam();
        param.setStatus(0);
        param.setTenantId(tenantId);
        if (intent.getExtentMin() != null) {
            param.setExtentStart(intent.getExtentMin());
        }
        if (intent.getExtentMax() != null) {
            param.setExtentEnd(intent.getExtentMax());
        }
        if (StrUtil.isNotBlank(intent.getCityKeyword())) {
            param.setCity(intent.getCityKeyword());
        }
        if (StrUtil.isNotBlank(intent.getRegionKeyword())) {
            param.setLocationKeyword(intent.getRegionKeyword());
        }
        if (StrUtil.isNotBlank(intent.getToward())) {
            param.setToward(intent.getToward());
        }
        if (StrUtil.isNotBlank(intent.getHouseType())) {
            String houseTypeKeyword = normalizeHouseTypeKeyword(intent.getHouseType());
            if (!isSingleRoomKeyword(houseTypeKeyword)) {
                param.setHouseType(houseTypeKeyword);
            }
        }
        if (StrUtil.isNotBlank(intent.getDecorationType())) {
            param.setHouseLabel(intent.getDecorationType());
        }
        if (StrUtil.isNotBlank(intent.getSupportingKeyword())) {
            param.setContent(intent.getSupportingKeyword());
        }
        if (!hasStructuredQueryCondition(intent)) {
            param.setKeywords(shortenQuestion(question));
        }

        List<HouseInfo> houses = houseInfoService.listRel(param);
        return filterHouses(houses, intent).stream().limit(EXACT_HOUSE_LIMIT).collect(Collectors.toList());
    }

    private boolean hasStructuredQueryCondition(HouseAiIntent intent) {
        return intent.getExtentMin() != null
            || intent.getExtentMax() != null
            || intent.getFloorMin() != null
            || intent.getFloorMax() != null
            || intent.getMonthlyRentMin() != null
            || intent.getMonthlyRentMax() != null
            || intent.getSalePriceMin() != null
            || intent.getSalePriceMax() != null
            || intent.getTotalPriceMin() != null
            || intent.getTotalPriceMax() != null
            || StrUtil.isNotBlank(intent.getCityKeyword())
            || StrUtil.isNotBlank(intent.getRegionKeyword())
            || StrUtil.isNotBlank(intent.getToward())
            || StrUtil.isNotBlank(intent.getHouseType())
            || StrUtil.isNotBlank(intent.getDecorationType())
            || StrUtil.isNotBlank(intent.getSupportingKeyword());
    }

    private List<HouseInfo> filterHouses(List<HouseInfo> houses, HouseAiIntent intent) {
        if (houses == null || houses.isEmpty()) {
            return Collections.emptyList();
        }
        return houses.stream()
            .filter(item -> matchExtent(item, intent))
            .filter(item -> matchFloor(item.getFloor(), intent))
            .filter(item -> matchMoney(item.getMonthlyRent(), intent.getMonthlyRentMin(), intent.getMonthlyRentMax()))
            .filter(item -> matchMoney(parseDecimal(item.getSalePrice()), intent.getSalePriceMin(), intent.getSalePriceMax()))
            .filter(item -> matchMoney(parseDecimal(item.getTotalPrice()), intent.getTotalPriceMin(), intent.getTotalPriceMax()))
            .filter(item -> matchTradeType(item, intent))
            .filter(item -> matchText(item, intent))
            .filter(item -> matchResidenceConditions(item, intent))
            .collect(Collectors.toList());
    }

    private List<HouseInfo> searchApproximateHouses(HouseAiIntent intent, Integer tenantId) {
        HouseInfoParam param = new HouseInfoParam();
        param.setStatus(0);
        param.setTenantId(tenantId);

        List<HouseInfo> candidates = houseInfoService.listRel(param);
        if (candidates == null || candidates.isEmpty()) {
            return Collections.emptyList();
        }

        return candidates.stream()
            .filter(item -> matchHardConditions(item, intent))
            .filter(item -> hasKnownValuesForExpressedConditions(item, intent))
            .filter(item -> matchRequiredConditions(item, intent))
            .filter(item -> matchRelaxedMoney(item.getMonthlyRent(), intent.getMonthlyRentMin(), intent.getMonthlyRentMax()))
            .filter(item -> matchRelaxedMoney(parseDecimal(item.getSalePrice()), intent.getSalePriceMin(), intent.getSalePriceMax()))
            .filter(item -> matchRelaxedMoney(parseDecimal(item.getTotalPrice()), intent.getTotalPriceMin(), intent.getTotalPriceMax()))
            .filter(item -> matchRelaxedExtent(item, intent))
            .filter(item -> matchRelaxedResidenceCosts(item, intent))
            .sorted((left, right) -> compareApproximateHouses(left, right, intent))
            .limit(APPROXIMATE_HOUSE_LIMIT)
            .collect(Collectors.toList());
    }

    private int compareApproximateHouses(HouseInfo left, HouseInfo right, HouseAiIntent intent) {
        int scoreCompare = Long.compare(buildApproximateScore(left, intent), buildApproximateScore(right, intent));
        if (scoreCompare != 0) {
            return scoreCompare;
        }
        Integer leftSort = left.getSortNumber() == null ? Integer.MAX_VALUE : left.getSortNumber();
        Integer rightSort = right.getSortNumber() == null ? Integer.MAX_VALUE : right.getSortNumber();
        return leftSort.compareTo(rightSort);
    }

    private long buildApproximateScore(HouseInfo item, HouseAiIntent intent) {
        long score = 0L;
        score += moneyDistanceScore(item.getMonthlyRent(), intent.getMonthlyRentMin(), intent.getMonthlyRentMax()) * PRICE_SCORE_WEIGHT;
        score += moneyDistanceScore(parseDecimal(item.getSalePrice()), intent.getSalePriceMin(), intent.getSalePriceMax()) * PRICE_SCORE_WEIGHT;
        score += moneyDistanceScore(parseDecimal(item.getTotalPrice()), intent.getTotalPriceMin(), intent.getTotalPriceMax()) * PRICE_SCORE_WEIGHT;
        score += extentDistanceScore(item, intent) * EXTENT_SCORE_WEIGHT;
        score += textMissPenalty(item.getHouseType(), intent.getHouseType()) * HOUSE_TYPE_SCORE_WEIGHT;
        score += floorDistanceScore(item.getFloor(), intent) * DETAIL_SCORE_WEIGHT;
        score += textMissPenalty(item.getToward(), intent.getToward()) * DETAIL_SCORE_WEIGHT;
        score += textMissPenalty(safeText(item.getHouseLabel()) + " " + safeText(item.getSupporting()) + " " + safeText(item.getContent()), intent.getDecorationType()) * DETAIL_SCORE_WEIGHT;
        score += textMissPenalty(safeText(item.getSupporting()) + " " + safeText(item.getContent()) + " " + safeText(item.getHouseLabel()), intent.getSupportingKeyword()) * DETAIL_SCORE_WEIGHT;
        score += booleanMissPenalty(item.getAirConditioningAvailable(), intent.getAirConditioningAvailable()) * DETAIL_SCORE_WEIGHT;
        score += booleanMissPenalty(item.getParkingAvailable(), intent.getParkingAvailable()) * DETAIL_SCORE_WEIGHT;
        score += textMissPenalty(item.getWaterBillingType(), intent.getWaterBillingType()) * DETAIL_SCORE_WEIGHT;
        score += textMissPenalty(item.getElectricityBillingType(), intent.getElectricityBillingType()) * DETAIL_SCORE_WEIGHT;
        score += moneyDistanceScore(item.getPropertyFees(), null, intent.getPropertyFeesMax()) * DETAIL_SCORE_WEIGHT;
        score += moneyDistanceScore(item.getWaterUnitPrice(), null, intent.getWaterUnitPriceMax()) * DETAIL_SCORE_WEIGHT;
        score += moneyDistanceScore(item.getElectricityUnitPrice(), null, intent.getElectricityUnitPriceMax()) * DETAIL_SCORE_WEIGHT;
        if (item.getRecommend() != null && item.getRecommend() == 1) {
            score -= 50L;
        }
        return score;
    }

    private boolean matchHardConditions(HouseInfo item, HouseAiIntent intent) {
        return matchTradeType(item, intent)
            && matchCity(item, intent)
            && matchRegion(item, intent);
    }

    private boolean hasKnownValuesForExpressedConditions(HouseInfo item, HouseAiIntent intent) {
        return hasValue(item.getExtent(), intent.getExtentMin() != null || intent.getExtentMax() != null)
            && hasValue(item.getFloor(), intent.getFloorMin() != null || intent.getFloorMax() != null)
            && hasValue(item.getMonthlyRent(), intent.getMonthlyRentMin() != null || intent.getMonthlyRentMax() != null)
            && hasValue(parseDecimal(item.getSalePrice()), intent.getSalePriceMin() != null || intent.getSalePriceMax() != null)
            && hasValue(parseDecimal(item.getTotalPrice()), intent.getTotalPriceMin() != null || intent.getTotalPriceMax() != null)
            && hasValue(item.getHouseType(), StrUtil.isNotBlank(intent.getHouseType()))
            && hasValue(item.getToward(), StrUtil.isNotBlank(intent.getToward()))
            && hasValue(safeText(item.getHouseLabel()) + safeText(item.getSupporting()) + safeText(item.getContent()),
                StrUtil.isNotBlank(intent.getDecorationType()))
            && hasValue(safeText(item.getSupporting()) + safeText(item.getContent()) + safeText(item.getHouseLabel()),
                StrUtil.isNotBlank(intent.getSupportingKeyword()))
            && hasValue(item.getAirConditioningAvailable(), intent.getAirConditioningAvailable() != null)
            && hasValue(item.getParkingAvailable(), intent.getParkingAvailable() != null)
            && hasValue(item.getWaterBillingType(), StrUtil.isNotBlank(intent.getWaterBillingType()))
            && hasValue(item.getElectricityBillingType(), StrUtil.isNotBlank(intent.getElectricityBillingType()))
            && hasValue(item.getPropertyFees(), intent.getPropertyFeesMax() != null)
            && hasValue(item.getWaterUnitPrice(), intent.getWaterUnitPriceMax() != null)
            && hasValue(item.getElectricityUnitPrice(), intent.getElectricityUnitPriceMax() != null);
    }

    private boolean hasValue(Object value, boolean required) {
        if (!required) {
            return true;
        }
        return value instanceof String ? StrUtil.isNotBlank((String) value) : value != null;
    }

    private boolean matchRequiredConditions(HouseInfo item, HouseAiIntent intent) {
        if (intent.getRequiredFields() == null || intent.getRequiredFields().isEmpty()) {
            return true;
        }
        for (String field : intent.getRequiredFields()) {
            if (!matchRequiredCondition(item, intent, field)) {
                return false;
            }
        }
        return true;
    }

    private boolean matchRequiredCondition(HouseInfo item, HouseAiIntent intent, String field) {
        if (StrUtil.isBlank(field)) {
            return false;
        }
        String normalizedField = field.trim();
        if (!hasRequiredConditionValue(intent, normalizedField)) {
            return false;
        }
        switch (normalizedField) {
            case "extent":
                return matchExtent(item, intent);
            case "floor":
                return matchFloor(item.getFloor(), intent);
            case "monthlyRent":
                return matchMoney(item.getMonthlyRent(), intent.getMonthlyRentMin(), intent.getMonthlyRentMax());
            case "salePrice":
                return matchMoney(parseDecimal(item.getSalePrice()), intent.getSalePriceMin(), intent.getSalePriceMax());
            case "totalPrice":
                return matchMoney(parseDecimal(item.getTotalPrice()), intent.getTotalPriceMin(), intent.getTotalPriceMax());
            case "houseType":
                return matchHouseType(item.getHouseType(), intent.getHouseType());
            case "toward":
                return normalize(safeText(item.getToward())).contains(normalize(intent.getToward()));
            case "decorationType":
                return containsInHouseText(item, intent.getDecorationType(), true);
            case "supportingKeyword":
                return containsInHouseText(item, intent.getSupportingKeyword(), false);
            case "airConditioningAvailable":
                return intent.getAirConditioningAvailable().equals(item.getAirConditioningAvailable());
            case "parkingAvailable":
                return intent.getParkingAvailable().equals(item.getParkingAvailable());
            case "waterBillingType":
                return normalize(safeText(item.getWaterBillingType())).contains(normalize(intent.getWaterBillingType()));
            case "electricityBillingType":
                return normalize(safeText(item.getElectricityBillingType())).contains(normalize(intent.getElectricityBillingType()));
            case "propertyFeesMax":
                return matchMoney(item.getPropertyFees(), null, intent.getPropertyFeesMax());
            case "waterUnitPriceMax":
                return matchMoney(item.getWaterUnitPrice(), null, intent.getWaterUnitPriceMax());
            case "electricityUnitPriceMax":
                return matchMoney(item.getElectricityUnitPrice(), null, intent.getElectricityUnitPriceMax());
            default:
                return false;
        }
    }

    private boolean hasRequiredConditionValue(HouseAiIntent intent, String field) {
        switch (field) {
            case "extent":
                return intent.getExtentMin() != null || intent.getExtentMax() != null;
            case "floor":
                return intent.getFloorMin() != null || intent.getFloorMax() != null;
            case "monthlyRent":
                return intent.getMonthlyRentMin() != null || intent.getMonthlyRentMax() != null;
            case "salePrice":
                return intent.getSalePriceMin() != null || intent.getSalePriceMax() != null;
            case "totalPrice":
                return intent.getTotalPriceMin() != null || intent.getTotalPriceMax() != null;
            case "houseType":
                return StrUtil.isNotBlank(intent.getHouseType());
            case "toward":
                return StrUtil.isNotBlank(intent.getToward());
            case "decorationType":
                return StrUtil.isNotBlank(intent.getDecorationType());
            case "supportingKeyword":
                return StrUtil.isNotBlank(intent.getSupportingKeyword());
            case "airConditioningAvailable":
                return intent.getAirConditioningAvailable() != null;
            case "parkingAvailable":
                return intent.getParkingAvailable() != null;
            case "waterBillingType":
                return StrUtil.isNotBlank(intent.getWaterBillingType());
            case "electricityBillingType":
                return StrUtil.isNotBlank(intent.getElectricityBillingType());
            case "propertyFeesMax":
                return intent.getPropertyFeesMax() != null;
            case "waterUnitPriceMax":
                return intent.getWaterUnitPriceMax() != null;
            case "electricityUnitPriceMax":
                return intent.getElectricityUnitPriceMax() != null;
            default:
                return false;
        }
    }

    private boolean containsInHouseText(HouseInfo item, String expected, boolean decoration) {
        if (StrUtil.isBlank(expected)) {
            return false;
        }
        String text = decoration
            ? safeText(item.getHouseLabel()) + " " + safeText(item.getSupporting()) + " " + safeText(item.getContent())
            : safeText(item.getSupporting()) + " " + safeText(item.getContent()) + " " + safeText(item.getHouseLabel());
        return normalize(text).contains(normalize(expected));
    }

    private boolean matchResidenceConditions(HouseInfo item, HouseAiIntent intent) {
        return matchResidenceHardConditions(item, intent)
            && matchMoney(item.getPropertyFees(), null, intent.getPropertyFeesMax())
            && matchMoney(item.getWaterUnitPrice(), null, intent.getWaterUnitPriceMax())
            && matchMoney(item.getElectricityUnitPrice(), null, intent.getElectricityUnitPriceMax());
    }

    private boolean matchResidenceHardConditions(HouseInfo item, HouseAiIntent intent) {
        if (intent.getAirConditioningAvailable() != null
            && !intent.getAirConditioningAvailable().equals(item.getAirConditioningAvailable())) {
            return false;
        }
        if (intent.getParkingAvailable() != null
            && !intent.getParkingAvailable().equals(item.getParkingAvailable())) {
            return false;
        }
        if (StrUtil.isNotBlank(intent.getWaterBillingType())
            && !normalize(safeText(item.getWaterBillingType())).contains(normalize(intent.getWaterBillingType()))) {
            return false;
        }
        if (StrUtil.isNotBlank(intent.getElectricityBillingType())
            && !normalize(safeText(item.getElectricityBillingType())).contains(normalize(intent.getElectricityBillingType()))) {
            return false;
        }
        return true;
    }

    private boolean matchRelaxedResidenceCosts(HouseInfo item, HouseAiIntent intent) {
        return matchRelaxedMoney(item.getPropertyFees(), null, intent.getPropertyFeesMax())
            && matchRelaxedMoney(item.getWaterUnitPrice(), null, intent.getWaterUnitPriceMax())
            && matchRelaxedMoney(item.getElectricityUnitPrice(), null, intent.getElectricityUnitPriceMax());
    }

    private boolean matchText(HouseInfo item, HouseAiIntent intent) {
        if (!matchCity(item, intent) || !matchRegion(item, intent)) {
            return false;
        }
        if (StrUtil.isNotBlank(intent.getHouseType())) {
            if (!matchHouseType(item.getHouseType(), intent.getHouseType())) {
                return false;
            }
        }
        if (StrUtil.isNotBlank(intent.getToward())) {
            if (!normalize(safeText(item.getToward())).contains(normalize(intent.getToward()))) {
                return false;
            }
        }
        if (StrUtil.isNotBlank(intent.getDecorationType())) {
            String text = normalize(safeText(item.getHouseLabel()) + " " + safeText(item.getSupporting()) + " " + safeText(item.getContent()));
            if (!text.contains(normalize(intent.getDecorationType()))) {
                return false;
            }
        }
        if (StrUtil.isNotBlank(intent.getSupportingKeyword())) {
            String text = normalize(safeText(item.getSupporting()) + " " + safeText(item.getContent()) + " " + safeText(item.getHouseLabel()));
            if (!text.contains(normalize(intent.getSupportingKeyword()))) {
                return false;
            }
        }
        return true;
    }

    private boolean matchCity(HouseInfo item, HouseAiIntent intent) {
        if (StrUtil.isNotBlank(intent.getCityKeyword())) {
            String cityText = normalize(safeText(item.getCity()) + " " + safeText(item.getCityByHouse()));
            if (!cityText.contains(normalize(intent.getCityKeyword()))) {
                return false;
            }
        }
        return true;
    }

    private boolean matchRegion(HouseInfo item, HouseAiIntent intent) {
        if (StrUtil.isNotBlank(intent.getRegionKeyword())) {
            String text = normalize(safeText(item.getHouseTitle()) + " " + safeText(item.getRegion()) + " "
                + safeText(item.getArea()) + " " + safeText(item.getAddress()) + " "
                + safeText(item.getCity()) + " " + safeText(item.getCityByHouse()));
            if (!text.contains(normalize(intent.getRegionKeyword()))) {
                return false;
            }
        }
        return true;
    }

    private boolean matchTradeType(HouseInfo item, HouseAiIntent intent) {
        if (StrUtil.isBlank(intent.getTradeType())) {
            return true;
        }
        if ("sale".equals(intent.getTradeType())) {
            return parseDecimal(item.getSalePrice()) != null || parseDecimal(item.getTotalPrice()) != null;
        }
        if ("rent".equals(intent.getTradeType())) {
            return item.getMonthlyRent() != null || item.getRent() != null;
        }
        return true;
    }

    private boolean matchExtent(HouseInfo item, HouseAiIntent intent) {
        if (intent.getExtentMin() == null && intent.getExtentMax() == null) {
            return true;
        }
        BigDecimal current = parseDecimal(item.getExtent());
        if (current == null) {
            return false;
        }
        if (intent.getExtentMin() != null && current.compareTo(new BigDecimal(intent.getExtentMin())) < 0) {
            return false;
        }
        if (intent.getExtentMax() != null && current.compareTo(new BigDecimal(intent.getExtentMax())) > 0) {
            return false;
        }
        return true;
    }

    private boolean matchRelaxedMoney(BigDecimal current, BigDecimal min, BigDecimal max) {
        if (min == null && max == null) {
            return true;
        }
        if (current == null) {
            return false;
        }
        if (min != null && current.compareTo(min.multiply(RELAX_MIN_RATE)) < 0) {
            return false;
        }
        if (max != null && current.compareTo(max.multiply(RELAX_MAX_RATE)) > 0) {
            return false;
        }
        return true;
    }

    private boolean matchRelaxedExtent(HouseInfo item, HouseAiIntent intent) {
        if (intent.getExtentMin() == null && intent.getExtentMax() == null) {
            return true;
        }
        BigDecimal current = parseDecimal(item.getExtent());
        if (current == null) {
            return false;
        }
        if (intent.getExtentMin() != null) {
            BigDecimal min = new BigDecimal(intent.getExtentMin()).multiply(RELAX_MIN_RATE);
            if (current.compareTo(min) < 0) {
                return false;
            }
        }
        if (intent.getExtentMax() != null) {
            BigDecimal max = new BigDecimal(intent.getExtentMax()).multiply(RELAX_MAX_RATE);
            if (current.compareTo(max) > 0) {
                return false;
            }
        }
        return true;
    }

    private long moneyDistanceScore(BigDecimal current, BigDecimal min, BigDecimal max) {
        if (min == null && max == null) {
            return 0L;
        }
        return distanceScore(current, min, max);
    }

    private long extentDistanceScore(HouseInfo item, HouseAiIntent intent) {
        if (intent.getExtentMin() == null && intent.getExtentMax() == null) {
            return 0L;
        }
        BigDecimal min = intent.getExtentMin() == null ? null : new BigDecimal(intent.getExtentMin());
        BigDecimal max = intent.getExtentMax() == null ? null : new BigDecimal(intent.getExtentMax());
        return distanceScore(parseDecimal(item.getExtent()), min, max);
    }

    private long distanceScore(BigDecimal current, BigDecimal min, BigDecimal max) {
        if (current == null) {
            return 10000L;
        }
        if (min != null && current.compareTo(min) < 0) {
            return percentDistance(min.subtract(current), min);
        }
        if (max != null && current.compareTo(max) > 0) {
            return percentDistance(current.subtract(max), max);
        }
        return 0L;
    }

    private long percentDistance(BigDecimal distance, BigDecimal base) {
        double divisor = Math.max(Math.abs(base.doubleValue()), 1D);
        return Math.round(distance.abs().doubleValue() * 100D / divisor);
    }

    private long textMissPenalty(String text, String keyword) {
        if (StrUtil.isBlank(keyword)) {
            return 0L;
        }
        return normalizeSearchText(safeText(text)).contains(normalizeSearchText(keyword)) ? 0L : 1L;
    }

    private long booleanMissPenalty(Boolean current, Boolean expected) {
        if (expected == null) {
            return 0L;
        }
        return expected.equals(current) ? 0L : 1L;
    }

    private long floorDistanceScore(String floor, HouseAiIntent intent) {
        if (intent.getFloorMin() == null && intent.getFloorMax() == null) {
            return 0L;
        }
        Integer currentFloor = extractFirstInteger(floor);
        if (currentFloor == null) {
            return 1L;
        }
        if (intent.getFloorMin() != null && currentFloor < intent.getFloorMin()) {
            return intent.getFloorMin() - currentFloor;
        }
        if (intent.getFloorMax() != null && currentFloor > intent.getFloorMax()) {
            return currentFloor - intent.getFloorMax();
        }
        return 0L;
    }

    private boolean matchFloor(String floor, HouseAiIntent intent) {
        if (intent.getFloorMin() == null && intent.getFloorMax() == null) {
            return true;
        }
        Integer currentFloor = extractFirstInteger(floor);
        if (currentFloor == null) {
            return false;
        }
        if (intent.getFloorMin() != null && currentFloor < intent.getFloorMin()) {
            return false;
        }
        if (intent.getFloorMax() != null && currentFloor > intent.getFloorMax()) {
            return false;
        }
        return true;
    }

    private boolean matchMoney(BigDecimal current, BigDecimal min, BigDecimal max) {
        if (min == null && max == null) {
            return true;
        }
        if (current == null) {
            return false;
        }
        if (min != null && current.compareTo(min) < 0) {
            return false;
        }
        if (max != null && current.compareTo(max) > 0) {
            return false;
        }
        return true;
    }

    private String shortenQuestion(String question) {
        String normalized = normalize(question);
        return normalized.length() > 12 ? normalized.substring(0, 12) : normalized;
    }

    private BigDecimal parseDecimal(String raw) {
        if (StrUtil.isBlank(raw)) {
            return null;
        }
        String number = raw.replaceAll("[^0-9.]", "");
        if (StrUtil.isBlank(number)) {
            return null;
        }
        try {
            return new BigDecimal(number);
        } catch (Exception e) {
            return null;
        }
    }

    private Integer extractFirstInteger(String raw) {
        if (StrUtil.isBlank(raw)) {
            return null;
        }
        Matcher matcher = NUMBER_PATTERN.matcher(raw);
        if (matcher.find()) {
            return NumberUtil.parseInt(matcher.group(1));
        }
        return null;
    }

    private String normalizeHouseTypeKeyword(String keyword) {
        return normalizeSearchText(keyword);
    }

    private boolean matchHouseType(String houseType, String expectedHouseType) {
        String actual = normalizeSearchText(safeText(houseType));
        String expected = normalizeSearchText(expectedHouseType);
        if (isSingleRoomKeyword(expected)) {
            return actual.contains("单间") || actual.contains("一室");
        }
        return actual.contains(expected);
    }

    private boolean isSingleRoomKeyword(String houseType) {
        return "单间".equals(houseType) || "一室".equals(houseType);
    }

    private String normalizeSearchText(String text) {
        String normalized = normalize(text);
        normalized = replaceHouseNumberPattern(normalized, HOUSE_TYPE_COMPARTMENT_PATTERN, "隔间");
        Matcher matcher = HOUSE_TYPE_ROOM_HALL_PATTERN.matcher(normalized);
        StringBuffer buffer = new StringBuffer();
        while (matcher.find()) {
            String replacement = toChineseHouseNumber(matcher.group(1)) + "室" + toChineseHouseNumber(matcher.group(2)) + "厅";
            matcher.appendReplacement(buffer, Matcher.quoteReplacement(replacement));
        }
        matcher.appendTail(buffer);
        return buffer.toString();
    }

    private String replaceHouseNumberPattern(String text, Pattern pattern, String suffix) {
        Matcher matcher = pattern.matcher(text);
        StringBuffer buffer = new StringBuffer();
        while (matcher.find()) {
            matcher.appendReplacement(buffer, Matcher.quoteReplacement(toChineseHouseNumber(matcher.group(1)) + suffix));
        }
        matcher.appendTail(buffer);
        return buffer.toString();
    }

    private String toChineseHouseNumber(String raw) {
        String value = normalize(raw).replace("两", "二");
        switch (value) {
            case "1":
            case "一":
                return "一";
            case "2":
            case "二":
                return "二";
            case "3":
            case "三":
                return "三";
            case "4":
            case "四":
                return "四";
            case "5":
            case "五":
                return "五";
            case "6":
            case "六":
                return "六";
            case "7":
            case "七":
                return "七";
            case "8":
            case "八":
                return "八";
            case "9":
            case "九":
                return "九";
            case "10":
            case "十":
                return "十";
            default:
                return value;
        }
    }

    private String normalize(String text) {
        if (text == null) {
            return "";
        }
        return text.toLowerCase(Locale.ROOT)
            .replace("㎡", "平")
            .replace("平方", "平")
            .replace("m²", "平")
            .replace("m2", "平")
            .replace("Ｍ²", "平")
            .replace("（", "(")
            .replace("）", ")")
            .replace("＋", "+")
            .trim();
    }

    private String safeText(String text) {
        return text == null ? "" : text;
    }
}
