from __future__ import annotations

import re
from datetime import UTC, datetime
from typing import Any

from app.schemas.agent_asset import (
    AgentAssetRiskRuleSimulationAttachment,
    AgentAssetRiskRuleSimulationRead,
    AgentAssetRiskRuleSimulationRequest,
)
from app.services.agent_asset_risk_rule_simulation_fields import (
    derive_attachment_field_value,
    extract_amount,
    extract_city_mentions,
    extract_invoice_no,
    extract_iso_date,
    field_matches_simulation_key,
    has_meaningful_value,
    infer_goods_name,
    infer_item_type,
    looks_like_city_field,
    refine_simulation_values_with_model,
)
from app.services.risk_rule_template_executor import RiskRuleTemplateExecutor


class AgentAssetRiskRuleSimulationMixin:
    def simulate_risk_rule_message(
        self,
        asset_id: str,
        body: AgentAssetRiskRuleSimulationRequest,
    ) -> AgentAssetRiskRuleSimulationRead:
        _, version, manifest = self._load_risk_rule_for_test(asset_id, body.version)
        attachments = self._normalize_simulation_attachments(body.attachments)
        field_values, source_map, recognized_fields = self._build_simulation_field_values(
            manifest,
            message=body.message,
            explicit_values=body.field_values,
            attachments=attachments,
        )
        recognition_summary = self._build_recognition_summary(attachments)
        fields = self._extract_manifest_fields(manifest)
        ocr_raw_fields = self._build_ocr_raw_fields(attachments)
        hermes_normalized_fields = self._build_hermes_normalized_fields(
            fields,
            field_values,
            source_map,
        )
        required_keys = self._extract_execution_field_keys(manifest)
        executor_input_fields = self._build_executor_input_fields(
            fields,
            field_values,
            source_map,
            required_keys,
        )
        missing_fields = self._build_missing_fields(
            manifest,
            field_values=field_values,
            source_map=source_map,
            required_keys=required_keys,
        )
        block = self._resolve_simulation_block(
            manifest,
            message=body.message,
            attachments=attachments,
            missing_fields=missing_fields,
        )
        if block:
            return AgentAssetRiskRuleSimulationRead(
                version=version,
                ready=False,
                stage=block["stage"],
                hit=False,
                severity="none",
                severity_label="待补充",
                summary=block["summary"],
                blocking_reason=block["reason"],
                field_values=field_values,
                normalized_fields=field_values,
                attachments=attachments,
                recognized_fields=recognized_fields,
                ocr_raw_fields=ocr_raw_fields,
                hermes_normalized_fields=hermes_normalized_fields,
                executor_input_fields=executor_input_fields,
                missing_fields=missing_fields,
                recognition_summary=recognition_summary,
                created_at=datetime.now(UTC),
            )

        claim, contexts = self._build_synthetic_claim(field_values, manifest)
        execution = RiskRuleTemplateExecutor().evaluate_with_trace(
            manifest,
            claim=claim,
            contexts=contexts,
        )
        result = execution["result"]
        hit = result is not None
        severity = (
            str((manifest.get("outcomes") or {}).get("fail", {}).get("severity") or "medium")
            if hit
            else "none"
        )
        severity_label = self._risk_severity_label(severity)
        message = str(result.get("message") or "") if isinstance(result, dict) else ""
        summary = (
            f"本次仿真命中{severity_label}，仅生成风险识别结果，不创建业务单据。"
            if hit
            else "本次仿真未命中风险，仅完成规则识别，不创建业务单据。"
        )
        evidence = result.get("evidence") if isinstance(result, dict) else {}
        return AgentAssetRiskRuleSimulationRead(
            version=version,
            ready=True,
            stage="executed",
            hit=hit,
            severity=severity,
            severity_label=severity_label,
            summary=summary,
            message=message,
            field_values=field_values,
            normalized_fields=field_values,
            evidence=evidence if isinstance(evidence, dict) else {},
            trace=execution["trace"] if isinstance(execution.get("trace"), dict) else {},
            attachments=attachments,
            recognized_fields=recognized_fields,
            ocr_raw_fields=ocr_raw_fields,
            hermes_normalized_fields=hermes_normalized_fields,
            executor_input_fields=executor_input_fields,
            missing_fields=[],
            recognition_summary=recognition_summary,
            created_at=datetime.now(UTC),
        )

    def _build_simulation_field_values(
        self,
        manifest: dict[str, Any],
        *,
        message: str,
        explicit_values: dict[str, Any],
        attachments: list[dict[str, Any]],
    ) -> tuple[dict[str, Any], dict[str, str], list[dict[str, Any]]]:
        fields = self._extract_manifest_fields(manifest)
        values: dict[str, Any] = {}
        source_map: dict[str, str] = {}
        safe_explicit_values = explicit_values if isinstance(explicit_values, dict) else {}
        corpus = self._build_simulation_corpus(message, attachments)
        city_mentions = self._extract_city_mentions(corpus)
        message_city_mentions = self._extract_city_mentions(message)

        for field in fields:
            key = field["key"]
            explicit_value = safe_explicit_values.get(key)
            if self._has_meaningful_value(explicit_value):
                values[key] = explicit_value
                source_map[key] = "manual"
                continue
            attachment_value = self._find_attachment_field_value(
                key,
                field.get("label") or key,
                attachments,
            )
            if self._has_meaningful_value(attachment_value):
                values[key] = attachment_value
                source_map[key] = "ocr"
                continue
            if attachments and key.startswith("attachment.") and (
                "city" in key or "location" in key
            ):
                inferred = None
            else:
                inferred = self._infer_simulation_value(
                    key,
                    field.get("label") or key,
                    corpus=corpus,
                    city_mentions=(
                        message_city_mentions
                        if key.startswith(("claim.", "item."))
                        else city_mentions
                    ),
                )
            if self._has_meaningful_value(inferred):
                values[key] = inferred
                source_map[key] = "inferred"

        allowed_keys = {field["key"] for field in fields}
        model_values = refine_simulation_values_with_model(
            getattr(self, "db", None),
            fields,
            message=message,
            attachments=attachments,
        )
        for key, value in model_values.items():
            if not self._has_meaningful_value(value) or key not in allowed_keys:
                continue
            if source_map.get(key) == "manual":
                continue
            values[key] = value
            source_map[key] = "model_refined"

        self._apply_compare_city_hints(manifest, values, source_map, message_city_mentions)
        recognized_fields = self._build_recognized_fields(fields, values, source_map)
        return values, source_map, recognized_fields

    def _infer_simulation_value(
        self,
        field_key: str,
        label: str,
        *,
        corpus: str,
        city_mentions: list[str],
    ) -> Any:
        key_text = f"{field_key} {label}".lower()
        if field_key.endswith("route_cities"):
            return city_mentions if self._looks_like_route_text(corpus) else []
        if field_key == "item.item_location":
            return self._extract_labeled_city(corpus, city_mentions, ("明细地点", "发生地点"))
        if field_key == "employee.location":
            return self._extract_labeled_city(corpus, city_mentions, ("员工常驻地", "常驻地", "办公地"))
        if "city" in field_key or "location" in field_key:
            if any(
                token in key_text
                for token in ("hotel", "invoice", "attachment", "发票", "酒店", "住宿")
            ):
                return city_mentions[0] if city_mentions else ""
            if any(token in key_text for token in ("route", "trip", "目的", "行程", "申报")):
                return (
                    city_mentions[1]
                    if len(city_mentions) > 1
                    else (city_mentions[0] if city_mentions else "")
                )
            return city_mentions[0] if city_mentions else ""
        if field_key.endswith("amount"):
            return self._extract_amount(corpus)
        if (
            field_key.endswith("issue_date")
            or field_key.endswith("item_date")
            or field_key.endswith("trip_start_date")
            or field_key.endswith("trip_end_date")
            or field_key.endswith("stay_start_date")
            or field_key.endswith("stay_end_date")
        ):
            return self._extract_iso_date(corpus)
        if field_key.endswith("invoice_no"):
            return self._extract_invoice_no(corpus)
        if field_key.endswith("ocr_text"):
            return corpus
        if field_key.endswith("goods_name"):
            return self._infer_goods_name(corpus)
        if field_key.endswith("item_type"):
            return self._infer_item_type(corpus)
        if field_key.endswith("reason") or field_key.endswith("item_reason"):
            return corpus or "仿真测试报销事由"
        return None

    @staticmethod
    def _looks_like_route_text(text: str) -> bool:
        return any(token in str(text or "") for token in ("交通票", "车票", "机票", "火车", "高铁", "行程", "路线", "从", "到", "至"))

    @staticmethod
    def _extract_labeled_city(text: str, city_mentions: list[str], labels: tuple[str, ...]) -> str:
        corpus = str(text or "")
        for label in labels:
            for city in city_mentions:
                if re.search(rf"{re.escape(label)}[^，。；;、\n]{{0,10}}{re.escape(city)}", corpus):
                    return city
        return ""

    def _apply_compare_city_hints(
        self,
        manifest: dict[str, Any],
        values: dict[str, Any],
        source_map: dict[str, str],
        city_mentions: list[str],
    ) -> None:
        if len(city_mentions) < 2:
            return
        params = manifest.get("params") if isinstance(manifest.get("params"), dict) else {}
        conditions = params.get("conditions") if isinstance(params.get("conditions"), list) else []
        for condition in conditions:
            if not isinstance(condition, dict):
                continue
            left = str(condition.get("left") or "").strip()
            right = str(condition.get("right") or "").strip()
            if not left or not right:
                continue
            if self._looks_like_city_field(left) and (
                not self._has_meaningful_value(values.get(left))
                or source_map.get(left) == "inferred"
            ):
                values[left] = city_mentions[0]
                source_map[left] = "inferred"
            if self._looks_like_city_field(right) and (
                not self._has_meaningful_value(values.get(right))
                or source_map.get(right) == "inferred"
            ):
                values[right] = city_mentions[1]
                source_map[right] = "inferred"

    @staticmethod
    def _normalize_simulation_attachments(
        attachments: list[AgentAssetRiskRuleSimulationAttachment],
    ) -> list[dict[str, Any]]:
        normalized: list[dict[str, Any]] = []
        for item in list(attachments or [])[:12]:
            normalized.append(
                {
                    "name": str(item.name or "").strip(),
                    "content_type": str(item.content_type or "").strip(),
                    "size": item.size or 0,
                    "note": str(item.note or "").strip(),
                    "ocr_text": str(item.ocr_text or "").strip(),
                    "summary": str(item.summary or "").strip(),
                    "document_type": str(item.document_type or "").strip(),
                    "document_type_label": str(item.document_type_label or "").strip(),
                    "scene_code": str(item.scene_code or "").strip(),
                    "scene_label": str(item.scene_label or "").strip(),
                    "avg_score": float(item.avg_score or 0.0),
                    "recognition_status": str(item.recognition_status or "").strip(),
                    "document_fields": AgentAssetRiskRuleSimulationMixin._normalize_document_fields(
                        item.document_fields
                    ),
                }
            )
        return normalized

    @staticmethod
    def _build_simulation_corpus(message: str, attachments: list[dict[str, Any]]) -> str:
        parts = [str(message or "").strip()]
        for item in attachments:
            parts.append(str(item.get("name") or "").strip())
            parts.append(str(item.get("note") or "").strip())
            parts.append(str(item.get("summary") or "").strip())
            parts.append(str(item.get("ocr_text") or "").strip())
            for field in list(item.get("document_fields") or []):
                if isinstance(field, dict):
                    parts.append(str(field.get("value") or "").strip())
        return "\n".join(part for part in parts if part)

    @staticmethod
    def _normalize_document_fields(fields: list[dict[str, Any]]) -> list[dict[str, Any]]:
        normalized: list[dict[str, Any]] = []
        for field in list(fields or [])[:30]:
            if not isinstance(field, dict):
                continue
            key = str(field.get("key") or "").strip()
            label = str(field.get("label") or "").strip()
            value = field.get("value")
            if key and label and AgentAssetRiskRuleSimulationMixin._has_meaningful_value(value):
                normalized.append({"key": key, "label": label, "value": value})
        return normalized

    def _find_attachment_field_value(
        self,
        field_key: str,
        label: str,
        attachments: list[dict[str, Any]],
    ) -> Any:
        short_key = field_key.removeprefix("attachment.")
        for attachment in attachments:
            derived_value = derive_attachment_field_value(short_key, attachment)
            if self._has_meaningful_value(derived_value):
                return derived_value
            if short_key == "ocr_text":
                value = attachment.get("ocr_text") or attachment.get("summary")
                if self._has_meaningful_value(value):
                    return value
            for field in list(attachment.get("document_fields") or []):
                if not isinstance(field, dict):
                    continue
                candidate_key = str(field.get("key") or "").strip().lower()
                candidate_label = str(field.get("label") or "").strip()
                if field_matches_simulation_key(
                    candidate_key, candidate_label, short_key, label
                ):
                    return field.get("value")
        return None

    def _extract_execution_field_keys(self, manifest: dict[str, Any]) -> list[str]:
        params = manifest.get("params") if isinstance(manifest.get("params"), dict) else {}
        template_key = str(manifest.get("template_key") or params.get("template_key") or "").strip()
        keys: list[str] = []
        if template_key == "field_compare_v1":
            conditions = (
                params.get("conditions") if isinstance(params.get("conditions"), list) else []
            )
            for group_name in (
                "attachment_city_fields",
                "reference_city_fields",
                "exception_fields",
            ):
                for key in self._read_string_list(params.get(group_name)):
                    if key not in keys:
                        keys.append(key)
            for condition in conditions:
                if not isinstance(condition, dict):
                    continue
                for group_name in ("left_group", "right_group", "exception_fields"):
                    for key in self._read_string_list(condition.get(group_name)):
                        if key not in keys:
                            keys.append(key)
                for side in ("left", "right"):
                    key = str(condition.get(side) or "").strip()
                    if key and key not in keys:
                        keys.append(key)
        elif template_key == "keyword_match_v1":
            for key in self._read_string_list(
                params.get("search_fields") or params.get("field_keys")
            ):
                if key not in keys:
                    keys.append(key)
        elif template_key == "field_required_v1":
            return []
        elif template_key == "composite_rule_v1":
            conditions = (
                params.get("conditions") if isinstance(params.get("conditions"), list) else []
            )
            for condition in conditions:
                if not isinstance(condition, dict):
                    continue
                for group_name in (
                    "fields",
                    "left_fields",
                    "right_fields",
                    "date_fields",
                    "range_start_fields",
                    "range_end_fields",
                ):
                    for key in self._read_string_list(condition.get(group_name)):
                        if key not in keys:
                            keys.append(key)
        return keys

    def _build_missing_fields(
        self,
        manifest: dict[str, Any],
        *,
        field_values: dict[str, Any],
        source_map: dict[str, str],
        required_keys: list[str],
    ) -> list[dict[str, Any]]:
        labels = {field["key"]: field["label"] for field in self._extract_manifest_fields(manifest)}
        params = manifest.get("params") if isinstance(manifest.get("params"), dict) else {}
        if self._is_city_consistency_manifest(manifest):
            declared_keys = self._read_string_list(
                params.get("search_fields") or params.get("field_keys")
            )
            candidate_keys = [*required_keys, *declared_keys]
            reference_keys = [
                key for key in candidate_keys if key in {"claim.location", "item.item_location"}
            ]
            attachment_keys = [
                key
                for key in candidate_keys
                if key in {"attachment.route_cities", "attachment.hotel_city"}
            ]
            missing_groups: list[dict[str, Any]] = []
            if not any(self._has_meaningful_value(field_values.get(key)) for key in reference_keys):
                missing_groups.append({"key": "claim.location", "label": labels.get("claim.location", "申报地点")})
            if not any(self._has_meaningful_value(field_values.get(key)) for key in attachment_keys):
                missing_groups.append({"key": "attachment.route_cities", "label": labels.get("attachment.route_cities", "行程城市")})
            return missing_groups
        if str(params.get("template_key") or manifest.get("template_key") or "") == "composite_rule_v1":
            return []
        missing: list[dict[str, Any]] = []
        for key in required_keys:
            value = field_values.get(key)
            if key not in source_map or not self._has_meaningful_value(value):
                missing.append({"key": key, "label": labels.get(key, key)})
        return missing

    @staticmethod
    def _is_city_consistency_manifest(manifest: dict[str, Any]) -> bool:
        params = manifest.get("params") if isinstance(manifest.get("params"), dict) else {}
        if str(params.get("semantic_type") or "").strip() in {
            "travel_city_consistency",
            "travel_route_city_consistency",
        }:
            return True
        field_keys = AgentAssetRiskRuleSimulationMixin._read_string_list(
            params.get("search_fields") or params.get("field_keys")
        )
        has_reference = any(key in {"claim.location", "item.item_location"} for key in field_keys)
        has_attachment_city = any(
            key in {"attachment.route_cities", "attachment.hotel_city"} for key in field_keys
        )
        if not (has_reference and has_attachment_city):
            return False
        text = "\n".join(
            str(params.get(key) or "")
            for key in ("natural_language", "condition_summary", "message_template")
        )
        return any(term in text for term in ("一致", "不一致", "匹配", "不符", "对应", "出现在"))

    def _resolve_simulation_block(
        self,
        manifest: dict[str, Any],
        *,
        message: str,
        attachments: list[dict[str, Any]],
        missing_fields: list[dict[str, Any]],
    ) -> dict[str, str] | None:
        has_attachment = bool(attachments)
        requires_attachment = self._rule_requires_attachment(manifest)
        has_recognition = any(
            self._has_meaningful_value(item.get("ocr_text"))
            or self._has_meaningful_value(item.get("summary"))
            or self._has_meaningful_value(item.get("document_fields"))
            for item in attachments
        )
        has_user_evidence = self._has_meaningful_user_message(message)
        if requires_attachment and not has_attachment:
            return {
                "stage": "needs_attachment",
                "summary": "当前规则要求上传附件，暂不能仅凭文字执行风险判断。",
                "reason": "请上传测试单据，并填写本次测试意图后再执行仿真。",
            }
        if requires_attachment and not has_user_evidence:
            return {
                "stage": "needs_test_intent",
                "summary": "当前规则要求附件和测试说明一起进入仿真判断。",
                "reason": "请补充本次测试意图或关键业务事实，再执行风险识别。",
            }
        if has_attachment and not has_recognition and not has_user_evidence:
            return {
                "stage": "needs_recognition",
                "summary": "单据尚未完成识别，暂不能执行风险规则。",
                "reason": "请先完成 OCR 识别，或在对话中补充票据城市、金额、发票号等关键信息。",
            }
        template_key = str(
            manifest.get("template_key") or (manifest.get("params") or {}).get("template_key") or ""
        ).strip()
        if template_key != "field_required_v1" and missing_fields:
            labels = "、".join(
                str(item.get("label") or item.get("key")) for item in missing_fields[:4]
            )
            return {
                "stage": "needs_field_confirmation",
                "summary": f"还缺少规则执行所需字段：{labels}，暂不能判断是否命中。",
                "reason": "请补充缺失字段，或上传可识别出这些字段的票据后再执行。",
            }
        if not has_attachment and not has_user_evidence:
            return {
                "stage": "needs_input",
                "summary": "请先描述测试单据或上传票据，再执行风险识别。",
                "reason": "当前没有可用于规则判断的业务事实。",
            }
        return None

    @staticmethod
    def _rule_requires_attachment(manifest: dict[str, Any]) -> bool:
        if bool(manifest.get("requires_attachment")):
            return True
        metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
        return bool(metadata.get("requires_attachment"))

    @staticmethod
    def _has_meaningful_user_message(message: str) -> bool:
        text = str(message or "").strip()
        if not text:
            return False
        generic_prompts = (
            "请识别我上传的临时单据是否命中这条风险规则",
            "请识别上传单据是否命中风险规则",
        )
        return not any(prompt in text for prompt in generic_prompts)

    @staticmethod
    def _build_recognized_fields(
        fields: list[dict[str, str]],
        values: dict[str, Any],
        source_map: dict[str, str],
    ) -> list[dict[str, Any]]:
        labels = {field["key"]: field["label"] for field in fields}
        return [
            {
                "key": key,
                "label": labels.get(key, key),
                "value": value,
                "source": source_map.get(key, ""),
            }
            for key, value in values.items()
            if source_map.get(key)
        ]

    @staticmethod
    def _build_ocr_raw_fields(attachments: list[dict[str, Any]]) -> list[dict[str, Any]]:
        rows: list[dict[str, Any]] = []
        for attachment_index, attachment in enumerate(attachments):
            attachment_name = str(attachment.get("name") or f"attachment-{attachment_index + 1}")
            for field_index, field in enumerate(list(attachment.get("document_fields") or [])):
                if not isinstance(field, dict):
                    continue
                value = field.get("value")
                if not AgentAssetRiskRuleSimulationMixin._has_meaningful_value(value):
                    continue
                key = str(field.get("key") or f"field_{field_index + 1}").strip()
                label = str(field.get("label") or key).strip()
                rows.append(
                    {
                        "key": key,
                        "label": label,
                        "value": value,
                        "source": "ocr",
                        "source_label": "OCR结构字段",
                        "attachment_name": attachment_name,
                    }
                )
            for key, label in (("summary", "单据摘要"), ("ocr_text", "OCR原文")):
                value = attachment.get(key)
                if AgentAssetRiskRuleSimulationMixin._has_meaningful_value(value):
                    rows.append(
                        {
                            "key": key,
                            "label": label,
                            "value": value,
                            "source": "ocr",
                            "source_label": "OCR文本",
                            "attachment_name": attachment_name,
                        }
                    )
        return rows[:80]

    @staticmethod
    def _build_hermes_normalized_fields(
        fields: list[dict[str, str]],
        values: dict[str, Any],
        source_map: dict[str, str],
    ) -> list[dict[str, Any]]:
        labels = {field["key"]: field["label"] for field in fields}
        rows: list[dict[str, Any]] = []
        for key, value in values.items():
            if not AgentAssetRiskRuleSimulationMixin._has_meaningful_value(value):
                continue
            source = source_map.get(key, "")
            rows.append(
                {
                    "key": key,
                    "label": labels.get(key, key),
                    "value": value,
                    "source": source,
                    "source_label": AgentAssetRiskRuleSimulationMixin._field_source_label(source),
                }
            )
        return rows

    @staticmethod
    def _build_executor_input_fields(
        fields: list[dict[str, str]],
        values: dict[str, Any],
        source_map: dict[str, str],
        required_keys: list[str],
    ) -> list[dict[str, Any]]:
        labels = {field["key"]: field["label"] for field in fields}
        required_set = set(required_keys or [])
        ordered_keys = [*required_keys]
        for field in fields:
            key = field["key"]
            if key not in ordered_keys and key in values:
                ordered_keys.append(key)
        rows: list[dict[str, Any]] = []
        for key in ordered_keys:
            value = values.get(key)
            if not AgentAssetRiskRuleSimulationMixin._has_meaningful_value(value):
                continue
            source = source_map.get(key, "")
            rows.append(
                {
                    "key": key,
                    "label": labels.get(key, key),
                    "value": value,
                    "source": source,
                    "source_label": AgentAssetRiskRuleSimulationMixin._field_source_label(source),
                    "required": key in required_set,
                }
            )
        return rows

    @staticmethod
    def _field_source_label(source: str) -> str:
        return {
            "manual": "用户输入",
            "ocr": "OCR结构字段",
            "inferred": "文本推断",
            "model_refined": "Hermes规范化",
        }.get(str(source or "").strip(), "未标注来源")

    @staticmethod
    def _build_recognition_summary(attachments: list[dict[str, Any]]) -> list[dict[str, Any]]:
        return [
            {
                "name": item.get("name") or "",
                "status": item.get("recognition_status")
                or (
                    "recognized"
                    if item.get("ocr_text") or item.get("document_fields")
                    else "pending"
                ),
                "document_type_label": item.get("document_type_label") or "",
                "scene_label": item.get("scene_label") or "",
                "summary": item.get("summary") or "",
                "field_count": len(list(item.get("document_fields") or [])),
                "avg_score": item.get("avg_score") or 0.0,
            }
            for item in attachments
        ]

    @staticmethod
    def _extract_city_mentions(text: str) -> list[str]:
        return extract_city_mentions(text)

    @staticmethod
    def _extract_amount(text: str) -> str:
        return extract_amount(text)

    @staticmethod
    def _extract_iso_date(text: str) -> str:
        return extract_iso_date(text)

    @staticmethod
    def _extract_invoice_no(text: str) -> str:
        return extract_invoice_no(text)

    @staticmethod
    def _infer_item_type(text: str) -> str:
        return infer_item_type(text)

    @staticmethod
    def _infer_goods_name(text: str) -> str:
        return infer_goods_name(text)

    @staticmethod
    def _looks_like_city_field(field_key: str) -> bool:
        return looks_like_city_field(field_key)

    @staticmethod
    def _has_meaningful_value(value: Any) -> bool:
        return has_meaningful_value(value)

    @staticmethod
    def _risk_severity_label(severity: str) -> str:
        return {
            "low": "低风险",
            "medium": "中风险",
            "high": "高风险",
            "none": "未命中",
        }.get(str(severity or "").strip().lower(), "风险")

    @staticmethod
    def _read_string_list(value: Any) -> list[str]:
        if not isinstance(value, list):
            return []
        return [str(item or "").strip() for item in value if str(item or "").strip()]
