from __future__ import annotations

import json
import re
from decimal import Decimal
from typing import Any
from zipfile import BadZipFile

from openpyxl import load_workbook
from openpyxl.utils.exceptions import InvalidFileException
from pydantic import ValidationError
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.core.agent_enums import AgentAssetDomain, AgentAssetStatus, AgentAssetType
from app.models.agent_asset import AgentAsset, AgentAssetVersion
from app.services.agent_asset_spreadsheet import (
    COMPANY_TRAVEL_EXPENSE_RULE_CODE,
    COMPANY_TRAVEL_TRANSPORT_ESTIMATE_RULE_CODE,
    TRAVEL_SPREADSHEET_RULE_CODES,
    AgentAssetSpreadsheetManager,
)
from app.services.expense_rule_runtime_defaults import (
    DEFAULT_SCENE_MATRIX_CONFIG,
    DEFAULT_SCENE_RULE_ASSET_CODE,
    DEFAULT_TRAVEL_POLICY_CONFIG,
    DEFAULT_TRAVEL_RULE_ASSET_CODE,
    DOCUMENT_TYPE_LABELS,
    EXPENSE_RULE_CODE_BLOCK_PATTERN,
    SCENE_LABELS,
)
from app.services.expense_rule_runtime_models import (
    AmountLimitConfig,
    ExpenseRuleCatalog,
    ExpenseScenePolicy,
    RuntimeTravelPolicy,
    SceneMatrixRuleConfig,
    TravelPolicyConfig,
    build_default_expense_rule_catalog,
    resolve_document_type_label,
)
from app.services.expense_rule_runtime_spreadsheet_extractors import (
    extract_hotel_season_limits,
    extract_normalized_transport_class_limits,
    extract_normalized_travel_allowance_limits,
    map_transport_grade_row_to_bands,
    transport_class_level_for_text,
)
from app.services.expense_rule_runtime_standards import (
    build_scene_submission_standard_markdown,
    build_travel_risk_control_standard_markdown,
)
from app.services.travel_policy_grades import TRAVEL_GRADE_KEYS

__all__ = [
    "DEFAULT_SCENE_MATRIX_CONFIG",
    "DEFAULT_SCENE_RULE_ASSET_CODE",
    "DEFAULT_TRAVEL_POLICY_CONFIG",
    "DEFAULT_TRAVEL_RULE_ASSET_CODE",
    "DOCUMENT_TYPE_LABELS",
    "ExpenseRuleRuntimeService",
    "RuntimeTravelPolicy",
    "SCENE_LABELS",
    "build_default_expense_rule_catalog",
    "build_scene_submission_standard_markdown",
    "build_travel_risk_control_standard_markdown",
    "resolve_document_type_label",
]


class ExpenseRuleRuntimeService:
    def __init__(self, db: Session, *, tenant_id: str | None = None) -> None:
        self.db = db
        self.tenant_id = str(tenant_id or "").strip()

    def _asset_scope_clause(self):
        platform = (AgentAsset.scope == "platform") & (AgentAsset.tenant_id == "platform")
        if not self.tenant_id:
            return platform
        return platform | (
            (AgentAsset.scope == "tenant") & (AgentAsset.tenant_id == self.tenant_id)
        )

    def load_catalog(self) -> ExpenseRuleCatalog:
        catalog = build_default_expense_rule_catalog()
        assets = list(
            self.db.scalars(
                select(AgentAsset)
                .where(self._asset_scope_clause())
                .where(AgentAsset.asset_type == AgentAssetType.RULE.value)
                .where(AgentAsset.status == AgentAssetStatus.ACTIVE.value)
                .where(AgentAsset.domain == AgentAssetDomain.EXPENSE.value)
                .order_by(AgentAsset.updated_at.desc(), AgentAsset.created_at.desc())
            ).all()
        )
        if not assets:
            assets = []

        asset_ids = {asset.id for asset in assets}
        travel_spreadsheet_assets = list(
            self.db.scalars(
                select(AgentAsset)
                .where(self._asset_scope_clause())
                .where(AgentAsset.asset_type == AgentAssetType.RULE.value)
                .where(AgentAsset.domain == AgentAssetDomain.EXPENSE.value)
                .where(AgentAsset.code.in_(TRAVEL_SPREADSHEET_RULE_CODES))
                .order_by(AgentAsset.updated_at.desc(), AgentAsset.created_at.desc())
            ).all()
        )
        for travel_spreadsheet_asset in travel_spreadsheet_assets:
            if travel_spreadsheet_asset.id not in asset_ids:
                assets.append(travel_spreadsheet_asset)

        spreadsheet_assets: list[tuple[AgentAsset, AgentAssetVersion]] = []
        for asset in assets:
            version = self._get_current_version(asset)
            if version is None:
                continue
            is_travel_spreadsheet_asset = (
                str(asset.code or "").strip() in TRAVEL_SPREADSHEET_RULE_CODES
                and str((asset.config_json or {}).get("detail_mode") or "").strip() == "spreadsheet"
            )
            runtime_payload = self._extract_runtime_payload(
                markdown_content=str(version.content or ""),
                config_json=asset.config_json,
            )
            if not isinstance(runtime_payload, dict):
                spreadsheet_assets.append((asset, version))
                continue
            self._apply_runtime_payload(
                catalog,
                runtime_payload=runtime_payload,
                asset=asset,
                version=version,
            )
            if is_travel_spreadsheet_asset:
                spreadsheet_assets.append((asset, version))

        for asset, version in spreadsheet_assets:
            self._apply_spreadsheet_runtime_payload(
                catalog,
                asset=asset,
                version=version,
            )

        return catalog

    def _get_current_version(self, asset: AgentAsset) -> AgentAssetVersion | None:
        published_version = str(asset.published_version or asset.current_version or "").strip()
        if not published_version:
            return None
        return self.db.scalar(
            select(AgentAssetVersion).where(
                AgentAssetVersion.asset_id == asset.id,
                AgentAssetVersion.version == published_version,
            )
        )

    @staticmethod
    def _extract_runtime_payload(
        *,
        markdown_content: str,
        config_json: dict[str, Any] | None,
    ) -> dict[str, Any] | None:
        match = EXPENSE_RULE_CODE_BLOCK_PATTERN.search(str(markdown_content or ""))
        if match is not None:
            try:
                payload = json.loads(match.group(1))
            except json.JSONDecodeError:
                payload = None
            if isinstance(payload, dict):
                return payload

        runtime_payload = (config_json or {}).get("runtime_rule")
        return runtime_payload if isinstance(runtime_payload, dict) else None

    def _apply_runtime_payload(
        self,
        catalog: ExpenseRuleCatalog,
        *,
        runtime_payload: dict[str, Any],
        asset: AgentAsset,
        version: AgentAssetVersion,
    ) -> None:
        kind = str(runtime_payload.get("kind") or "").strip().lower()
        try:
            if kind == "scene_matrix":
                config = SceneMatrixRuleConfig.model_validate(runtime_payload)
                for expense_type, scene_config in config.scenes.items():
                    catalog.scene_policies[expense_type] = ExpenseScenePolicy(
                        expense_type=expense_type,
                        rule_code=asset.code,
                        rule_name=asset.name,
                        rule_version=version.version,
                        **scene_config.model_dump(),
                    )
                return

            if kind == "travel_policy":
                config = TravelPolicyConfig.model_validate(runtime_payload)
                catalog.travel_policy = RuntimeTravelPolicy(
                    rule_code=asset.code,
                    rule_name=asset.name,
                    rule_version=version.version,
                    **config.model_dump(),
                )
        except ValidationError:
            return

    def _apply_spreadsheet_runtime_payload(
        self,
        catalog: ExpenseRuleCatalog,
        *,
        asset: AgentAsset,
        version: AgentAssetVersion,
    ) -> None:
        if str(asset.code or "").strip() not in TRAVEL_SPREADSHEET_RULE_CODES:
            return
        if str((asset.config_json or {}).get("detail_mode") or "").strip() != "spreadsheet":
            return

        manager = AgentAssetSpreadsheetManager()
        metadata = manager.parse_version_markdown(str(version.content or ""))
        rule_document = (asset.config_json or {}).get("rule_document")
        if not isinstance(rule_document, dict):
            rule_document = {}
        storage_key = str(rule_document.get("storage_key") or "").strip()
        if not storage_key and metadata is not None:
            storage_key = str(metadata.storage_key or "").strip()
        if storage_key:
            try:
                workbook_path = manager.resolve_storage_path(storage_key)
            except FileNotFoundError:
                workbook_path = None
            if workbook_path is not None and not workbook_path.exists():
                workbook_path = None
        else:
            workbook_path = None

        if workbook_path is None:
            fallback_storage_key = str(rule_document.get("storage_key") or "").strip()
            if not fallback_storage_key:
                return
            try:
                workbook_path = manager.resolve_storage_path(fallback_storage_key)
            except FileNotFoundError:
                return
        if not workbook_path.exists():
            return

        try:
            workbook = load_workbook(
                workbook_path,
                read_only=True,
                data_only=True,
            )
        except (BadZipFile, FileNotFoundError, InvalidFileException, OSError):
            return

        try:
            standards = self._extract_travel_amount_standards_from_workbook(workbook)
            hotel_city_limits = self._extract_hotel_city_limits_from_workbook(workbook)
            hotel_season_limits = self._extract_hotel_season_limits_from_workbook(workbook)
            allowance_limits = self._extract_travel_allowance_limits_from_workbook(workbook)
            transport_limits = self._extract_transport_class_limits_from_workbook(workbook)
            transport_estimates = self._extract_transport_estimates_from_workbook(workbook)
        finally:
            workbook.close()

        standard_rule_version = str(
            rule_document.get("asset_version") or asset.current_version or version.version
        ).strip()
        if (
            hotel_city_limits
            or hotel_season_limits.get("hotel_peak_periods")
            or hotel_season_limits.get("hotel_peak_city_limits")
            or allowance_limits
            or transport_limits
            or transport_estimates
        ) and catalog.travel_policy is not None:
            payload = catalog.travel_policy.model_dump()
            if str(asset.code or "").strip() == COMPANY_TRAVEL_EXPENSE_RULE_CODE:
                payload["standard_rule_code"] = asset.code
                payload["standard_rule_name"] = asset.name
                payload["standard_rule_version"] = standard_rule_version
            if str(asset.code or "").strip() == COMPANY_TRAVEL_TRANSPORT_ESTIMATE_RULE_CODE:
                payload["transport_estimate_rule_code"] = asset.code
                payload["transport_estimate_rule_name"] = asset.name
                payload["transport_estimate_rule_version"] = standard_rule_version
            if hotel_city_limits:
                payload["hotel_city_limits"] = {
                    **payload.get("hotel_city_limits", {}),
                    **hotel_city_limits,
                }
            if hotel_season_limits.get("hotel_peak_periods"):
                payload["hotel_peak_periods"] = {
                    **payload.get("hotel_peak_periods", {}),
                    **hotel_season_limits["hotel_peak_periods"],
                }
            if hotel_season_limits.get("hotel_peak_city_limits"):
                payload["hotel_peak_city_limits"] = {
                    **payload.get("hotel_peak_city_limits", {}),
                    **hotel_season_limits["hotel_peak_city_limits"],
                }
            if allowance_limits:
                payload["allowance_limits"] = {
                    **payload.get("allowance_limits", {}),
                    **allowance_limits,
                }
            if transport_limits:
                payload["transport_limits"] = {
                    **payload.get("transport_limits", {}),
                    **transport_limits,
                }
            if transport_estimates:
                existing_estimates = list(payload.get("transport_estimates") or [])
                payload["transport_estimates"] = [
                    *existing_estimates,
                    *transport_estimates,
                ]
            catalog.travel_policy = RuntimeTravelPolicy(**payload)

        for expense_type, amount in standards.items():
            current = catalog.scene_policies.get(expense_type)
            if current is None:
                continue
            limit_attr = "item_amount_limit" if expense_type == "transport" else "claim_amount_limit"
            base_limit = getattr(current, limit_attr, None)
            next_limit = self._replace_amount_limit_warn_amount(
                base_limit,
                amount=amount,
                metric_label=self._spreadsheet_metric_label(expense_type),
            )
            payload = current.model_dump()
            payload["rule_code"] = asset.code
            payload["rule_name"] = asset.name
            payload["rule_version"] = standard_rule_version
            payload[limit_attr] = next_limit.model_dump()
            catalog.scene_policies[expense_type] = ExpenseScenePolicy(**payload)

    @staticmethod
    def _extract_travel_amount_standards_from_workbook(workbook: Any) -> dict[str, Decimal]:
        standards: dict[str, Decimal] = {}
        for sheet in workbook.worksheets:
            rows = list(sheet.iter_rows(values_only=True))
            if not rows:
                continue
            header_index = -1
            category_index = -1
            standard_index = -1
            for index, row in enumerate(rows[:8]):
                values = [str(value or "").strip() for value in row]
                if "费用分类" in values and "报销标准" in values:
                    header_index = index
                    category_index = values.index("费用分类")
                    standard_index = values.index("报销标准")
                    break
            if header_index < 0:
                continue

            for row in rows[header_index + 1 :]:
                category = str(row[category_index] or "").strip() if len(row) > category_index else ""
                standard_text = str(row[standard_index] or "").strip() if len(row) > standard_index else ""
                amount = ExpenseRuleRuntimeService._extract_first_standard_amount(standard_text)
                if not category or amount is None:
                    continue
                normalized_type = ExpenseRuleRuntimeService._map_spreadsheet_category_to_expense_type(category)
                if normalized_type:
                    standards[normalized_type] = amount
        return standards

    @staticmethod
    def _extract_hotel_city_limits_from_workbook(workbook: Any) -> dict[str, dict[str, Decimal]]:
        city_limits: dict[str, dict[str, Decimal]] = {}
        for sheet in workbook.worksheets:
            rows = list(sheet.iter_rows(values_only=True))
            if not rows:
                continue

            header_index = -1
            city_index = -1
            band_indexes: dict[str, int] = {}
            for index, row in enumerate(rows[:10]):
                values = [str(value or "").strip() for value in row]
                for candidate in ("地区（城市）", "城市", "地区"):
                    if candidate in values:
                        city_index = values.index(candidate)
                        break
                if city_index < 0:
                    continue
                for column_index, header in enumerate(values):
                    compact = re.sub(r"\s+", "", header)
                    grade_key = ExpenseRuleRuntimeService._extract_exact_grade_header(compact)
                    if grade_key:
                        band_indexes[grade_key] = column_index
                        continue
                    if any(keyword in compact for keyword in ("P1-P3", "其他员工")):
                        band_indexes["junior"] = column_index
                    if any(keyword in compact for keyword in ("P4-P6", "基层经理", "中层经理")):
                        band_indexes["mid"] = column_index
                        band_indexes["senior"] = column_index
                    if any(keyword in compact for keyword in ("P7", "高层经理", "公司级管理")):
                        band_indexes["manager"] = column_index
                        band_indexes["executive"] = column_index
                if band_indexes:
                    header_index = index
                    break

            if header_index < 0:
                continue

            for row in rows[header_index + 1 :]:
                raw_city = str(row[city_index] or "").strip() if len(row) > city_index else ""
                cities = ExpenseRuleRuntimeService._extract_city_names_from_cell(raw_city)
                if not cities:
                    continue
                for city in cities:
                    city_entry = city_limits.setdefault(city, {})
                    for band, column_index in band_indexes.items():
                        amount = ExpenseRuleRuntimeService._coerce_decimal_cell(
                            row[column_index] if len(row) > column_index else None
                        )
                        if amount is not None:
                            city_entry[band] = amount
        return city_limits

    @staticmethod
    def _extract_exact_grade_header(value: str) -> str:
        compact = re.sub(r"\s+", "", str(value or "").upper())
        if not compact or any(token in compact for token in ("-", "+", "及以上")):
            return ""
        match = re.match(r"^(P[0-8])(?:级|董事会)?$", compact)
        if match is None:
            return ""
        grade_key = match.group(1)
        return grade_key if grade_key in TRAVEL_GRADE_KEYS else ""

    @staticmethod
    def _extract_travel_allowance_limits_from_workbook(workbook: Any) -> dict[str, dict[str, Decimal]]:
        allowance_limits: dict[str, dict[str, Decimal]] = {}
        for sheet in workbook.worksheets:
            rows = list(sheet.iter_rows(values_only=True))
            if not rows:
                continue

            normalized_limits = extract_normalized_travel_allowance_limits(rows)
            if normalized_limits:
                for allowance_key, region_amounts in normalized_limits.items():
                    allowance_limits.setdefault(allowance_key, {}).update(region_amounts)
                continue

            header_index = -1
            type_index = -1
            region_indexes: dict[str, int] = {}
            for index, row in enumerate(rows[:10]):
                values = [str(value or "").strip() for value in row]
                if "补助类型" not in values:
                    continue
                header_index = index
                type_index = values.index("补助类型")
                for column_index, header in enumerate(values):
                    if column_index <= type_index:
                        continue
                    normalized = str(header or "").strip()
                    if not normalized or normalized == "项目":
                        continue
                    region_indexes[normalized] = column_index
                break

            if header_index < 0 or type_index < 0 or not region_indexes:
                continue

            for row in rows[header_index + 1 :]:
                raw_type = str(row[type_index] or "").strip() if len(row) > type_index else ""
                allowance_key = ExpenseRuleRuntimeService._map_allowance_type_to_key(raw_type)
                if not allowance_key:
                    continue

                entry: dict[str, Decimal] = {}
                for region_label, column_index in region_indexes.items():
                    amount = ExpenseRuleRuntimeService._coerce_decimal_cell(
                        row[column_index] if len(row) > column_index else None
                    )
                    if amount is not None:
                        entry[region_label] = amount
                if entry:
                    allowance_limits[allowance_key] = entry
        return allowance_limits

    @staticmethod
    def _extract_hotel_season_limits_from_workbook(workbook: Any) -> dict[str, dict[str, Any]]:
        peak_periods: dict[str, str] = {}
        peak_limits: dict[str, Decimal] = {}
        for sheet in workbook.worksheets:
            rows = list(sheet.iter_rows(values_only=True))
            if not rows:
                continue
            limits = extract_hotel_season_limits(rows)
            peak_periods.update(limits.get("hotel_peak_periods") or {})
            peak_limits.update(limits.get("hotel_peak_city_limits") or {})
        return {"hotel_peak_periods": peak_periods, "hotel_peak_city_limits": peak_limits}

    @staticmethod
    def _map_allowance_type_to_key(value: str) -> str:
        normalized = re.sub(r"\s+", "", str(value or ""))
        if "伙食" in normalized or "餐" in normalized:
            return "meal"
        if "基本" in normalized:
            return "basic"
        if "合计" in normalized or "总计" in normalized:
            return "total"
        return ""

    @staticmethod
    def _extract_transport_class_limits_from_workbook(workbook: Any) -> dict[str, dict[str, int]]:
        limits: dict[str, dict[str, int]] = {}
        for sheet in workbook.worksheets:
            rows = list(sheet.iter_rows(values_only=True))
            if not rows:
                continue

            normalized_limits = extract_normalized_transport_class_limits(rows)
            if normalized_limits:
                for grade_band, transport_levels in normalized_limits.items():
                    limits.setdefault(grade_band, {}).update(transport_levels)
                continue

            employee_index = -1
            flight_index = -1
            train_index = -1
            for row_index, row in enumerate(rows[:10]):
                values = [str(value or "").strip() for value in row]
                if "员工职级" in values:
                    employee_index = values.index("员工职级")
                    for next_row in rows[row_index + 1 : row_index + 4]:
                        next_values = [str(value or "").strip() for value in next_row]
                        if "飞机" in next_values:
                            flight_index = next_values.index("飞机")
                        if "火车" in next_values:
                            train_index = next_values.index("火车")
                        if flight_index >= 0 and train_index >= 0:
                            break
                    break

            if employee_index < 0 or (flight_index < 0 and train_index < 0):
                continue

            for row in rows:
                employee_text = str(row[employee_index] or "").strip() if len(row) > employee_index else ""
                bands = map_transport_grade_row_to_bands(employee_text)
                if not bands:
                    continue
                flight_level = (
                    transport_class_level_for_text(
                        row[flight_index] if len(row) > flight_index else None,
                        kind="flight",
                    )
                    if flight_index >= 0
                    else None
                )
                train_level = (
                    transport_class_level_for_text(
                        row[train_index] if len(row) > train_index else None,
                        kind="train",
                    )
                    if train_index >= 0
                    else None
                )
                for band in bands:
                    entry = limits.setdefault(band, {})
                    if flight_level is not None:
                        entry["flight"] = flight_level
                    if train_level is not None:
                        entry["train"] = train_level
        return limits

    @staticmethod
    def _extract_transport_estimates_from_workbook(workbook: Any) -> list[dict[str, object]]:
        estimates: list[dict[str, object]] = []
        for sheet in workbook.worksheets:
            rows = list(sheet.iter_rows(values_only=True))
            if not rows:
                continue

            header_index = -1
            indexes: dict[str, int] = {}
            header_aliases = {
                "origin_city": ("出发城市", "出发地", "起点城市"),
                "destination_city": ("目的地", "到达城市", "目的城市"),
                "location_band": ("目的地范围", "城市范围", "地区范围"),
                "transport_mode": ("交通方式", "出行方式", "交通工具"),
                "one_way_amount": ("单程预估金额", "单程金额", "单程费用"),
                "round_trip_amount": ("往返预估金额", "往返金额", "往返费用", "预算占用金额"),
                "confidence": ("置信度", "匹配级别"),
                "basis": ("预算占用口径", "预估依据", "口径说明"),
            }
            for index, row in enumerate(rows[:10]):
                values = [str(value or "").strip() for value in row]
                if "交通方式" not in values and "出行方式" not in values:
                    continue
                for key, aliases in header_aliases.items():
                    for alias in aliases:
                        if alias in values:
                            indexes[key] = values.index(alias)
                            break
                if "transport_mode" in indexes and (
                    "round_trip_amount" in indexes or "one_way_amount" in indexes
                ):
                    header_index = index
                    break

            if header_index < 0:
                continue

            for row in rows[header_index + 1 :]:
                mode = ExpenseRuleRuntimeService._map_transport_mode_text(
                    ExpenseRuleRuntimeService._row_text(row, indexes.get("transport_mode", -1))
                )
                if not mode:
                    continue
                one_way_amount = ExpenseRuleRuntimeService._coerce_decimal_cell(
                    row[indexes["one_way_amount"]]
                    if "one_way_amount" in indexes and len(row) > indexes["one_way_amount"]
                    else None
                )
                round_trip_amount = ExpenseRuleRuntimeService._coerce_decimal_cell(
                    row[indexes["round_trip_amount"]]
                    if "round_trip_amount" in indexes and len(row) > indexes["round_trip_amount"]
                    else None
                )
                if round_trip_amount is None and one_way_amount is not None:
                    round_trip_amount = (one_way_amount * Decimal("2")).quantize(Decimal("0.01"))
                if round_trip_amount is None or round_trip_amount <= Decimal("0.00"):
                    continue
                estimates.append(
                    {
                        "origin_city": ExpenseRuleRuntimeService._row_text(
                            row, indexes.get("origin_city", -1)
                        ),
                        "destination_city": ExpenseRuleRuntimeService._row_text(
                            row, indexes.get("destination_city", -1)
                        ),
                        "location_band": ExpenseRuleRuntimeService._map_location_band_text(
                            ExpenseRuleRuntimeService._row_text(
                                row, indexes.get("location_band", -1)
                            )
                        ),
                        "transport_mode": mode,
                        "one_way_amount": one_way_amount or Decimal("0.00"),
                        "round_trip_amount": round_trip_amount,
                        "confidence": ExpenseRuleRuntimeService._row_text(
                            row, indexes.get("confidence", -1)
                        )
                        or "basic_rule",
                        "basis": ExpenseRuleRuntimeService._row_text(
                            row, indexes.get("basis", -1)
                        ),
                    }
                )
        return estimates

    @staticmethod
    def _row_text(row: Any, index: int) -> str:
        if index < 0 or len(row) <= index:
            return ""
        return str(row[index] or "").strip()

    @staticmethod
    def _map_transport_mode_text(value: str) -> str:
        normalized = re.sub(r"\s+", "", str(value or ""))
        if any(keyword in normalized for keyword in ("飞机", "机票", "航班", "经济舱")):
            return "飞机"
        if any(keyword in normalized for keyword in ("火车", "高铁", "动车", "铁路", "二等座", "硬卧")):
            return "火车"
        if any(keyword in normalized for keyword in ("轮船", "船票", "客轮", "渡轮", "邮轮")):
            return "轮船"
        return normalized if normalized in {"飞机", "火车", "轮船"} else ""

    @staticmethod
    def _map_location_band_text(value: str) -> str:
        normalized = re.sub(r"\s+", "", str(value or ""))
        if not normalized:
            return ""
        if any(keyword in normalized for keyword in ("高频", "一线", "核心", "重点")):
            return "premium"
        if any(keyword in normalized for keyword in ("远途", "偏远", "新疆", "西藏", "海南", "港澳台", "海外")):
            return "remote"
        if any(keyword in normalized for keyword in ("沿海", "海滨", "港口")):
            return "coastal"
        if any(keyword in normalized for keyword in ("普通", "默认", "其他")):
            return "default"
        return normalized

    @staticmethod
    def _extract_city_names_from_cell(value: str) -> list[str]:
        normalized = re.sub(r"[；;，,、/]+", "、", str(value or "").strip())
        if not normalized:
            return []
        names: list[str] = []
        for part in normalized.split("、"):
            cleaned = re.sub(r"\s+", "", part)
            cleaned = re.sub(r"[（(].*?[）)]", "", cleaned)
            if not cleaned or any(keyword in cleaned for keyword in ("不含", "中心城区", "新区")):
                continue
            if len(cleaned) <= 12:
                names.append(cleaned)
        return list(dict.fromkeys(names))

    @staticmethod
    def _coerce_decimal_cell(value: Any) -> Decimal | None:
        if value is None:
            return None
        try:
            return Decimal(str(value).strip()).quantize(Decimal("0.01"))
        except (ArithmeticError, ValueError):
            return None

    @staticmethod
    def _extract_first_standard_amount(text: str) -> Decimal | None:
        match = re.search(r"([0-9]+(?:\.[0-9]{1,2})?)\s*/\s*(?:天|人|晚|次|笔)", str(text or ""))
        if match is None:
            match = re.search(r"([0-9]+(?:\.[0-9]{1,2})?)", str(text or ""))
        if match is None:
            return None
        try:
            return Decimal(match.group(1)).quantize(Decimal("0.01"))
        except (ArithmeticError, ValueError):
            return None

    @staticmethod
    def _map_spreadsheet_category_to_expense_type(category: str) -> str:
        normalized = re.sub(r"\s+", "", str(category or ""))
        if any(keyword in normalized for keyword in ("市内交通", "打车", "网约车", "出租车")):
            return "transport"
        if "招待" in normalized and "餐" in normalized:
            return "meal"
        if "餐补" in normalized or normalized in {"餐费", "业务招待费"}:
            return "meal"
        return ""

    @staticmethod
    def _spreadsheet_metric_label(expense_type: str) -> str:
        return {
            "transport": "单笔交通金额",
            "meal": "业务招待费金额",
            "entertainment": "人均招待餐费",
        }.get(expense_type, "金额")

    @staticmethod
    def _replace_amount_limit_warn_amount(
        base_limit: AmountLimitConfig | None,
        *,
        amount: Decimal,
        metric_label: str,
    ) -> AmountLimitConfig:
        if base_limit is None:
            return AmountLimitConfig(
                warn_amount=amount,
                block_amount=None,
                metric_label=metric_label,
            )
        payload = base_limit.model_dump()
        payload["warn_amount"] = amount
        payload["metric_label"] = metric_label
        return AmountLimitConfig(**payload)
