from __future__ import annotations

import re
from datetime import UTC, datetime, timedelta
from decimal import Decimal, InvalidOperation

from sqlalchemy import select

from app.api.deps import CurrentUserContext
from app.models.financial_record import ExpenseClaim
from app.schemas.user_agent import (
    UserAgentDraftPayload,
    UserAgentRequest,
    UserAgentResponse,
    UserAgentSuggestedAction,
)
from app.services.expense_claim_access_policy import ExpenseClaimAccessPolicy
from app.services.document_numbering import (
    build_document_number,
    generate_unique_expense_claim_no,
)
from app.services.user_agent_application_locations import normalize_application_location

APPLICATION_CONTEXT_VALUES = {
    "application",
    "documents_application",
    "expense_application",
    "pre_approval",
    "preapproval",
}
APPLICATION_BASE_FIELDS = ("time", "location", "reason")
APPLICATION_TRANSPORT_OPTIONS = ("飞机", "火车", "轮船")
APPLICATION_TRANSPORT_KEYWORDS = {
    "飞机": ("飞机", "机票", "航班", "乘机", "坐飞机"),
    "火车": ("火车", "高铁", "动车", "铁路", "列车"),
    "轮船": ("轮船", "船", "客轮", "邮轮", "坐船"),
}
APPLICATION_REASON_VERBS = (
    "支撑",
    "支持",
    "部署",
    "上线",
    "实施",
    "驻场",
    "拜访",
    "验收",
    "会议",
    "采购",
    "培训",
    "协助",
    "处理",
    "办理",
    "参加",
    "进行",
)
APPLICATION_SUBMIT_KEYWORDS = (
    "确认提交",
    "确认申请",
    "提交审核",
    "确认无误提交",
    "直接提交",
)
APPLICATION_SHORT_CONFIRMATIONS = {"提交", "确认", "是", "好的", "可以", "没问题"}


class UserAgentApplicationMixin:
    @staticmethod
    def _is_expense_application_request(payload: UserAgentRequest) -> bool:
        context_json = payload.context_json or {}
        context_values = {
            str(context_json.get("session_type") or "").strip(),
            str(context_json.get("entry_source") or "").strip(),
            str(context_json.get("document_type") or "").strip(),
            str(context_json.get("application_stage") or "").strip(),
        }
        conversation_state = context_json.get("conversation_state")
        if isinstance(conversation_state, dict):
            context_values.update(
                {
                    str(conversation_state.get("session_type") or "").strip(),
                    str(conversation_state.get("entry_source") or "").strip(),
                    str(conversation_state.get("document_type") or "").strip(),
                    str(conversation_state.get("application_stage") or "").strip(),
                }
            )
        if context_values & APPLICATION_CONTEXT_VALUES:
            return True

        history = context_json.get("conversation_history")
        if not isinstance(history, list):
            return False
        compact_message = re.sub(r"\s+", "", str(payload.message or ""))
        looks_like_submit = (
            any(keyword in compact_message for keyword in APPLICATION_SUBMIT_KEYWORDS)
            or compact_message in APPLICATION_SHORT_CONFIRMATIONS
        )
        if not looks_like_submit:
            return False
        return any(
            isinstance(item, dict)
            and str(item.get("role") or "").strip() == "assistant"
            and (
                "#application-submit" in str(item.get("content") or "")
                or ("费用申请" in str(item.get("content") or "") and "确认" in str(item.get("content") or ""))
            )
            for item in history[-6:]
        )

    def _build_expense_application_response(
        self,
        payload: UserAgentRequest,
        *,
        risk_flags: list[str],
    ) -> UserAgentResponse:
        facts = self._resolve_expense_application_facts(payload)
        step = self._resolve_expense_application_step(payload, facts)
        application_claim = None
        if step == "submitted":
            application_claim = self._create_expense_application_record(payload, facts)
            facts["application_no"] = application_claim.claim_no
            facts["application_claim_id"] = application_claim.id
            facts["manager_name"] = self._resolve_application_manager_name(payload, application_claim)
        return UserAgentResponse(
            answer=self._build_expense_application_answer(payload, facts=facts, step=step),
            citations=[],
            suggested_actions=self._build_expense_application_actions(step, facts),
            query_payload=None,
            draft_payload=self._build_submitted_application_payload(application_claim, facts),
            review_payload=None,
            risk_flags=risk_flags,
            requires_confirmation=step == "preview",
        )

    def _build_expense_application_answer(
        self,
        payload: UserAgentRequest,
        *,
        facts: dict[str, str],
        step: str,
    ) -> str:
        recognized_table = self._build_application_summary_table(facts, include_empty=False)

        if step == "ask_missing":
            missing_fields = self._resolve_application_missing_fields(facts)
            missing_text = "、".join(
                self._display_application_slot_label(item)
                for item in missing_fields
            )
            return "\n\n".join(
                [
                    "我已按「费用申请 / 事前审批」来处理这条内容。",
                    "已识别信息：\n" + recognized_table,
                    f"当前还需要补充：{missing_text}。",
                    "请一次性补齐上述字段，我会继续生成模拟申请结果并让你确认是否提交。",
                ]
            )

        if step == "submitted":
            application_no = str(facts.get("application_no") or "").strip() or self._build_application_claim_no(payload, facts)
            manager_name = str(facts.get("manager_name") or "").strip() or "直属领导"
            return "\n\n".join(
                [
                    f"当前操作已完成，单据已经推送给 {manager_name} 进行审核，请耐心等待。",
                    f"申请单号：{application_no}",
                    "申请信息：\n" + self._build_application_summary_table(facts),
                    f"当前状态：{manager_name}审核中。",
                    "费用预估：预计费用已随申请提交，等待领导审核确认。",
                ]
            )

        return "\n\n".join(
            [
                "这是模拟的费用申请结果，请核对：",
                self._build_application_summary_table(facts),
                "请核对上述信息无误，确认无误后 [确认](#application-submit) 提交至审批流程。",
            ]
        )

    def _resolve_expense_application_facts(self, payload: UserAgentRequest) -> dict[str, str]:
        facts = {
            "time": "",
            "location": "",
            "reason": "",
            "days": "",
            "transport_mode": "",
            "amount": "",
            "application_type": "",
            "grade": "",
            "lodging_daily_cap": "",
            "subsidy_daily_cap": "",
            "transport_policy": "",
            "policy_estimate": "",
            "matched_city": "",
            "rule_name": "",
            "rule_version": "",
        }
        for message, is_current in self._iter_application_user_messages(payload):
            partial = {
                "time": self._resolve_application_time(payload, message=message) if is_current else self._resolve_application_time_from_text(message),
                "location": self._resolve_application_location(payload, message=message, use_entities=is_current),
                "reason": self._resolve_application_reason(message),
                "days": self._resolve_application_days(message),
                "transport_mode": self._resolve_application_transport_mode(message),
                "amount": self._resolve_application_amount(payload, message=message) if is_current else self._resolve_application_amount_from_text(message),
                "application_type": self._resolve_application_type_from_text(message),
            }
            for key, value in partial.items():
                if value:
                    facts[key] = value

        for key, value in self._resolve_application_preview_facts(payload.context_json or {}).items():
            if value:
                facts[key] = value

        if not facts["application_type"]:
            facts["application_type"] = self._infer_application_type(facts)
        facts["time"] = self._expand_application_time_with_days(
            facts.get("time", ""),
            facts.get("days", ""),
        )
        return facts

    @staticmethod
    def _resolve_application_preview_facts(context_json: dict[str, object]) -> dict[str, str]:
        preview = context_json.get("application_preview")
        if not isinstance(preview, dict):
            return {}
        fields = preview.get("fields")
        if not isinstance(fields, dict):
            return {}

        def pick(*keys: str) -> str:
            for key in keys:
                value = str(fields.get(key) or "").strip()
                if value:
                    return value
            return ""

        return {
            "application_type": pick("applicationType", "application_type"),
            "time": pick("time", "timeRange", "time_range"),
            "location": pick("location"),
            "reason": pick("reason"),
            "days": pick("days"),
            "transport_mode": pick("transportMode", "transport_mode"),
            "amount": pick("amount"),
            "grade": pick("grade"),
            "lodging_daily_cap": pick("lodgingDailyCap", "lodging_daily_cap"),
            "subsidy_daily_cap": pick("subsidyDailyCap", "subsidy_daily_cap"),
            "transport_policy": pick("transportPolicy", "transport_policy"),
            "policy_estimate": pick("policyEstimate", "policy_estimate"),
            "matched_city": pick("matchedCity", "matched_city"),
            "rule_name": pick("ruleName", "rule_name"),
            "rule_version": pick("ruleVersion", "rule_version"),
        }

    def _resolve_expense_application_step(
        self,
        payload: UserAgentRequest,
        facts: dict[str, str],
    ) -> str:
        if self._resolve_application_missing_base_fields(facts):
            return "ask_missing"
        if self._resolve_application_missing_followup_fields(facts):
            return "ask_missing"
        if self._is_application_submit_confirmation(payload):
            return "submitted"
        return "preview"

    @staticmethod
    def _iter_application_user_messages(payload: UserAgentRequest) -> list[tuple[str, bool]]:
        messages: list[tuple[str, bool]] = []
        history = (payload.context_json or {}).get("conversation_history")
        if isinstance(history, list):
            for item in history:
                if not isinstance(item, dict):
                    continue
                if str(item.get("role") or "").strip() != "user":
                    continue
                content = str(item.get("content") or "").strip()
                if content:
                    messages.append((content, False))
        current_message = str(payload.message or "").strip()
        if current_message:
            messages.append((current_message, True))
        return messages

    @staticmethod
    def _resolve_application_missing_base_fields(facts: dict[str, str]) -> list[str]:
        return [field for field in APPLICATION_BASE_FIELDS if not str(facts.get(field) or "").strip()]

    @staticmethod
    def _resolve_application_missing_followup_fields(facts: dict[str, str]) -> list[str]:
        return [
            field
            for field in ("transport_mode", "amount")
            if not str(facts.get(field) or "").strip()
        ]

    def _resolve_application_missing_fields(self, facts: dict[str, str]) -> list[str]:
        return [
            *self._resolve_application_missing_base_fields(facts),
            *self._resolve_application_missing_followup_fields(facts),
        ]

    @staticmethod
    def _resolve_application_time(payload: UserAgentRequest, *, message: str | None = None) -> str:
        if message and UserAgentApplicationMixin._resolve_application_time_from_text(message):
            return UserAgentApplicationMixin._resolve_application_time_from_text(message)

        context_time = UserAgentApplicationMixin._resolve_application_time_from_context(payload.context_json or {})
        if context_time:
            return context_time

        time_range = payload.ontology.time_range
        if time_range.start_date and time_range.end_date:
            return (
                time_range.start_date
                if time_range.start_date == time_range.end_date
                else f"{time_range.start_date} 至 {time_range.end_date}"
            )
        return str(time_range.raw or "").strip()

    @staticmethod
    def _resolve_application_time_from_text(message: str) -> str:
        labeled = UserAgentApplicationMixin._resolve_application_labeled_value(
            message,
            ("发生时间", "业务发生时间", "申请时间", "时间"),
        )
        if labeled:
            return labeled
        match = re.search(
            r"(?P<date>20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?)",
            str(message or ""),
        )
        return match.group("date").rstrip("日") if match else ""

    @staticmethod
    def _resolve_application_time_from_context(context_json: dict[str, object]) -> str:
        business_time_context = context_json.get("business_time_context")
        if not isinstance(business_time_context, dict):
            return ""
        start_date = str(business_time_context.get("start_date") or "").strip()
        end_date = str(business_time_context.get("end_date") or start_date).strip()
        display_value = str(business_time_context.get("display_value") or "").strip()
        if start_date and end_date:
            return start_date if start_date == end_date else f"{start_date} 至 {end_date}"
        return display_value

    @staticmethod
    def _resolve_application_labeled_value(message: str, labels: tuple[str, ...]) -> str:
        label_pattern = "|".join(re.escape(label) for label in labels)
        match = re.search(
            rf"(?:{label_pattern})[:：]\s*(?P<value>[^\n，。；;]+)",
            str(message or ""),
        )
        return match.group("value").strip() if match else ""

    def _resolve_application_location(
        self,
        payload: UserAgentRequest,
        *,
        message: str,
        use_entities: bool,
    ) -> str:
        labeled = self._resolve_application_labeled_value(message, ("地点", "业务地点", "发生地点"))
        if labeled:
            return normalize_application_location(labeled)

        if use_entities:
            entity_value = next(
                (
                    str(item.normalized_value or item.value or "").strip()
                    for item in payload.ontology.entities
                    if item.type == "location"
                    and str(item.normalized_value or item.value or "").strip()
                ),
                "",
            )
            if entity_value:
                return normalize_application_location(entity_value)

        return self._resolve_application_location_from_text(message)

    @staticmethod
    def _resolve_application_location_from_text(message: str) -> str:
        compact = re.sub(r"\s+", "", str(message or ""))
        if not compact:
            return ""

        for pattern in (
            r"(?:出差|去|到|赴|前往)(?P<target>[\u4e00-\u9fa5]{1,24})",
            r"(?P<target>[\u4e00-\u9fa5]{1,12})(?:出差|驻场)",
        ):
            match = re.search(pattern, compact)
            if not match:
                continue
            target = str(match.group("target") or "").strip()
            location = normalize_application_location(target)
            if location:
                return location
        return ""

    @staticmethod
    def _resolve_application_days(message: str) -> str:
        labeled = UserAgentApplicationMixin._resolve_application_labeled_value(
            message,
            ("天数", "出差天数", "申请天数"),
        )
        if labeled:
            return labeled if labeled.endswith("天") else f"{labeled}天"
        match = re.search(r"(?P<days>\d+|[一二两三四五六七八九十]{1,3})\s*天", str(message or ""))
        return f"{match.group('days')}天" if match else ""

    @staticmethod
    def _resolve_application_reason(message: str) -> str:
        labeled = UserAgentApplicationMixin._resolve_application_labeled_value(
            message,
            ("事由", "申请事由", "出差事由", "原因", "用途"),
        )
        if labeled:
            return labeled

        text = str(message or "").strip()
        if not text:
            return ""

        candidates: list[str] = []
        for segment in re.split(r"[\n，。；;]+", text):
            candidate = UserAgentApplicationMixin._cleanup_application_reason_candidate(segment)
            if candidate:
                candidates.append(candidate)

        if not candidates:
            return ""
        return max(candidates, key=len)

    @staticmethod
    def _cleanup_application_reason_candidate(segment: str) -> str:
        text = str(segment or "").strip()
        if not text:
            return ""

        text = re.sub(
            r"^(?:发生时间|业务发生时间|申请时间|时间|地点|业务地点|发生地点|天数|出差天数|申请天数|出行方式|交通方式|交通工具|用户预估费用|预估费用|预计总费用|预计费用|预计金额|申请金额|预算|金额)[:：]\s*",
            "",
            text,
        )
        if re.fullmatch(r"20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?", text):
            return ""
        if re.fullmatch(r"(?P<days>\d+|[一二两三四五六七八九十]{1,3})\s*天", text):
            return ""
        if re.fullmatch(r"(?:¥|￥)?\s*\d+(?:\.\d+)?\s*(?:元|块|万元)?", text):
            return ""
        if "时间" in text and re.search(r"20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}", text):
            return ""
        if re.fullmatch(r"(?:去|到|前往)?[\u4e00-\u9fa5]{1,8}出差(?P<days>\d+|[一二两三四五六七八九十]{1,3})?天?", text):
            return ""

        text = re.sub(r"^.*?(?:出差|前往|去|到|赴)[\u4e00-\u9fa5]{1,8}(?:出差)?(?P<days>\d+|[一二两三四五六七八九十]{1,3})?天?[，,\s]*", "", text)
        text = re.sub(r"^(?:出差|申请|费用申请|业务|本次|去|到|前往)\s*", "", text)
        text = text.strip(" ：:，,。；;")
        if not text:
            return ""
        if re.fullmatch(r"[\u4e00-\u9fa5]{1,8}", text) and not any(keyword in text for keyword in APPLICATION_REASON_VERBS):
            return ""
        return text

    @staticmethod
    def _expand_application_time_with_days(time_text: str, days_text: str) -> str:
        normalized_time = str(time_text or "").strip()
        if not normalized_time or re.search(r"\s*(?:至|到|~|-{2,}|—)\s*", normalized_time):
            return normalized_time

        days = UserAgentApplicationMixin._resolve_application_days_count(days_text)
        if not days:
            return normalized_time

        match = re.search(
            r"(?P<date>20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?)",
            normalized_time,
        )
        if not match:
            return normalized_time

        parsed_start = UserAgentApplicationMixin._parse_application_date(match.group("date"))
        if parsed_start is None:
            return normalized_time

        end_date = parsed_start + timedelta(days=days)
        return f"{parsed_start:%Y-%m-%d} 至 {end_date:%Y-%m-%d}"

    @staticmethod
    def _resolve_application_days_count(days_text: str) -> int:
        text = str(days_text or "").strip()
        if not text:
            return 0
        digit_match = re.search(r"\d+", text)
        if digit_match:
            return max(0, int(digit_match.group(0)))

        chinese_match = re.search(r"[一二两三四五六七八九十]{1,3}", text)
        if not chinese_match:
            return 0
        return UserAgentApplicationMixin._parse_chinese_number(chinese_match.group(0))

    @staticmethod
    def _parse_chinese_number(value: str) -> int:
        digits = {
            "一": 1,
            "二": 2,
            "两": 2,
            "三": 3,
            "四": 4,
            "五": 5,
            "六": 6,
            "七": 7,
            "八": 8,
            "九": 9,
        }
        text = str(value or "").strip()
        if not text:
            return 0
        if text == "十":
            return 10
        if "十" in text:
            left, _, right = text.partition("十")
            tens = digits.get(left, 1) if left else 1
            ones = digits.get(right, 0) if right else 0
            return tens * 10 + ones
        return digits.get(text, 0)

    @staticmethod
    def _parse_application_date(value: str) -> datetime | None:
        normalized = str(value or "").strip().rstrip("日").replace("年", "-").replace("月", "-")
        normalized = normalized.replace("/", "-").replace(".", "-")
        parts = [part for part in normalized.split("-") if part]
        if len(parts) != 3:
            return None
        try:
            year, month, day = (int(part) for part in parts)
            return datetime(year, month, day)
        except ValueError:
            return None

    def _resolve_application_amount(
        self,
        payload: UserAgentRequest,
        *,
        message: str | None = None,
    ) -> str:
        entity_amount = next(
            (
                str(item.normalized_value or item.value or "").strip()
                for item in payload.ontology.entities
                if item.type == "amount"
                and str(item.normalized_value or item.value or "").strip()
            ),
            "",
        )
        if entity_amount:
            return entity_amount if entity_amount.endswith("元") else f"{entity_amount}元"
        return self._resolve_application_amount_from_text(message or payload.message)

    @staticmethod
    def _resolve_application_amount_from_text(message: str) -> str:
        labeled = UserAgentApplicationMixin._resolve_application_labeled_value(
            message,
            ("用户预估费用", "预估费用", "预计总费用", "预计费用", "预计金额", "申请金额", "预算", "费用", "金额"),
        )
        if labeled:
            return UserAgentApplicationMixin._normalize_application_amount(labeled)
        match = re.search(
            r"(?P<amount>\d+(?:\.\d+)?\s*万?\s*(?:元|块|人民币))",
            str(message or ""),
        )
        return UserAgentApplicationMixin._normalize_application_amount(match.group("amount")) if match else ""

    @staticmethod
    def _normalize_application_amount(value: str) -> str:
        normalized = str(value or "").strip()
        if not normalized:
            return ""
        normalized = re.sub(r"\s+", "", normalized)
        if normalized.endswith(("元", "块")) or "人民币" in normalized:
            return normalized.replace("块", "元").replace("人民币", "")
        return f"{normalized}元"

    @staticmethod
    def _resolve_application_transport_mode(message: str) -> str:
        compact_message = re.sub(r"\s+", "", str(message or ""))
        for transport, keywords in APPLICATION_TRANSPORT_KEYWORDS.items():
            if any(keyword in compact_message for keyword in keywords):
                return transport
        labeled = UserAgentApplicationMixin._resolve_application_labeled_value(
            message,
            ("出行方式", "交通方式", "交通工具", "出行工具"),
        )
        if labeled:
            for transport, keywords in APPLICATION_TRANSPORT_KEYWORDS.items():
                if transport in labeled or any(keyword in labeled for keyword in keywords):
                    return transport
            return labeled
        return ""

    @staticmethod
    def _resolve_application_type_from_text(message: str) -> str:
        return UserAgentApplicationMixin._resolve_application_labeled_value(
            message,
            ("申请类型", "费用类型"),
        )

    @staticmethod
    def _resolve_application_missing_slots(payload: UserAgentRequest) -> list[str]:
        return [
            str(item or "").strip()
            for item in payload.ontology.missing_slots
            if str(item or "").strip()
        ]

    @staticmethod
    def _display_application_slot_label(slot: str) -> str:
        return {
            "expense_type": "申请类型",
            "amount": "用户预估费用",
            "time_range": "发生时间",
            "time": "发生时间",
            "location": "地点",
            "reason": "申请事由",
            "days": "天数",
            "transport_mode": "出行方式",
            "attachments": "申请材料/附件",
            "customer_name": "业务对象",
            "participants": "参与人员",
        }.get(str(slot or "").strip(), str(slot or "").strip())

    def _build_expense_application_actions(
        self,
        step: str,
        facts: dict[str, str],
    ) -> list[UserAgentSuggestedAction]:
        if step == "ask_missing":
            missing_fields = self._resolve_application_missing_fields(facts)
            return [
                UserAgentSuggestedAction(
                    label="一次性补充申请信息",
                    action_type="prefill_composer",
                    description="在输入框预填所有待补充字段，填写后一次提交。",
                    payload={
                        "application_fields": missing_fields,
                        "prompt_prefill": self._build_application_prefill_template(missing_fields),
                        "missing_fields": missing_fields,
                    },
                )
            ]
        if step == "preview":
            return []
        if step == "submitted":
            return []
        return []

    @staticmethod
    def _resolve_application_prefill_config(field: str) -> tuple[str, str]:
        config = {
            "time": ("补充发生时间", "申请时间段："),
            "location": ("补充地点", "地点："),
            "reason": ("补充申请事由", "事由："),
            "days": ("补充天数", "天数："),
            "transport_mode": ("补充出行方式", "出行方式："),
            "amount": ("补充预估费用", "用户预估费用："),
        }
        return config.get(field, ("补充申请信息", ""))

    @classmethod
    def _build_application_prefill_template(cls, fields: list[str]) -> str:
        lines = [
            prefill
            for field in fields
            for _, prefill in [cls._resolve_application_prefill_config(field)]
            if prefill
        ]
        return "\n".join(lines)

    @classmethod
    def _build_application_prefill_action(cls, field: str) -> UserAgentSuggestedAction:
        label, prefill = cls._resolve_application_prefill_config(field)
        return UserAgentSuggestedAction(
            label=label,
            action_type="prefill_composer",
            description=f"在输入框预填“{prefill}”，用户补充后再提交。",
            payload={
                "application_field": field,
                "prompt_prefill": prefill,
                "missing_fields": [field],
            },
        )

    @staticmethod
    def _infer_application_type(facts: dict[str, str]) -> str:
        text = " ".join(str(facts.get(key) or "") for key in ("reason", "transport_mode", "days"))
        if "采购" in text:
            return "采购费用申请"
        if "会议" in text or "会务" in text:
            return "会务费用申请"
        return "差旅费用申请"

    @staticmethod
    def _build_application_summary(facts: dict[str, str]) -> str:
        return "\n".join(
            f"{label}：{value or '待补充'}"
            for label, value in (
                ("申请类型", facts.get("application_type", "")),
                ("发生时间", facts.get("time", "")),
                ("地点", facts.get("location", "")),
                ("事由", facts.get("reason", "")),
                ("天数", facts.get("days", "")),
                ("出行方式", facts.get("transport_mode", "")),
                ("职级", facts.get("grade", "")),
                ("住宿上限/天", facts.get("lodging_daily_cap", "")),
                ("补贴标准/天", facts.get("subsidy_daily_cap", "")),
                ("交通费用口径", facts.get("transport_policy", "")),
                ("规则测算参考", facts.get("policy_estimate", "")),
                ("用户预估费用", facts.get("amount", "")),
            )
        )

    @staticmethod
    def _build_application_summary_table(
        facts: dict[str, str],
        *,
        include_empty: bool = True,
    ) -> str:
        rows = [
            ("申请类型", facts.get("application_type", "")),
            ("发生时间", facts.get("time", "")),
            ("地点", facts.get("location", "")),
            ("事由", facts.get("reason", "")),
            ("天数", facts.get("days", "")),
            ("出行方式", facts.get("transport_mode", "")),
            ("职级", facts.get("grade", "")),
            ("住宿上限/天", facts.get("lodging_daily_cap", "")),
            ("补贴标准/天", facts.get("subsidy_daily_cap", "")),
            ("交通费用口径", facts.get("transport_policy", "")),
            ("规则测算参考", facts.get("policy_estimate", "")),
            ("用户预估费用", facts.get("amount", "")),
        ]
        visible_rows = rows if include_empty else [(label, value) for label, value in rows if str(value or "").strip()]
        if not visible_rows:
            visible_rows = [("申请描述", "已收到，正在按费用申请上下文继续整理")]
        lines = ["| 字段 | 内容 |", "| --- | --- |"]
        lines.extend(f"| {label} | {value or '待补充'} |" for label, value in visible_rows)
        return "\n".join(lines)

    def _create_expense_application_record(
        self,
        payload: UserAgentRequest,
        facts: dict[str, str],
    ) -> ExpenseClaim:
        claim_no = self._build_application_claim_no(payload, facts)
        existing = self.db.scalar(
            select(ExpenseClaim)
            .where(ExpenseClaim.claim_no == claim_no)
            .limit(1)
        )
        if existing is not None:
            return existing

        current_user = self._build_application_current_user(payload)
        access_policy = ExpenseClaimAccessPolicy(self.db)
        employee = access_policy.resolve_current_employee(current_user)
        department_name = str(current_user.department_name or "").strip() or "待补充"
        department_id = None
        employee_id = None
        employee_name = str(current_user.username or current_user.name or payload.user_id or "anonymous").strip()

        if employee is not None:
            employee_id = employee.id
            employee_name = str(employee.name or employee.employee_no or employee.email or employee_name).strip()
            department_id = employee.organization_unit_id
            if employee.organization_unit is not None and employee.organization_unit.name:
                department_name = str(employee.organization_unit.name).strip()

        claim = ExpenseClaim(
            claim_no=claim_no,
            employee_id=employee_id,
            employee_name=employee_name,
            department_id=department_id,
            department_name=department_name,
            project_code=None,
            expense_type=self._resolve_application_expense_type_code(facts),
            reason=str(facts.get("reason") or "费用申请").strip() or "费用申请",
            location=str(facts.get("location") or "待补充").strip() or "待补充",
            amount=self._parse_application_amount_to_decimal(facts.get("amount", "")),
            currency="CNY",
            invoice_count=0,
            occurred_at=self._parse_application_occurred_at(facts.get("time", "")),
            submitted_at=datetime.now(UTC),
            status="submitted",
            approval_stage="直属领导审批",
            risk_flags_json=[self._build_application_detail_flag(facts)],
        )
        self.db.add(claim)
        self.db.commit()
        self.db.refresh(claim)
        return claim

    @staticmethod
    def _build_application_detail_flag(facts: dict[str, str]) -> dict[str, object]:
        return {
            "source": "application_detail",
            "severity": "info",
            "label": "申请详情",
            "application_detail": {
                "application_type": str(facts.get("application_type") or "").strip(),
                "time": str(facts.get("time") or "").strip(),
                "location": str(facts.get("location") or "").strip(),
                "reason": str(facts.get("reason") or "").strip(),
                "days": str(facts.get("days") or "").strip(),
                "transport_mode": str(facts.get("transport_mode") or "").strip(),
                "amount": str(facts.get("amount") or "").strip(),
                "grade": str(facts.get("grade") or "").strip(),
                "lodging_daily_cap": str(facts.get("lodging_daily_cap") or "").strip(),
                "subsidy_daily_cap": str(facts.get("subsidy_daily_cap") or "").strip(),
                "transport_policy": str(facts.get("transport_policy") or "").strip(),
                "policy_estimate": str(facts.get("policy_estimate") or "").strip(),
                "matched_city": str(facts.get("matched_city") or "").strip(),
                "rule_name": str(facts.get("rule_name") or "").strip(),
                "rule_version": str(facts.get("rule_version") or "").strip(),
            },
        }

    def _resolve_application_manager_name(
        self,
        payload: UserAgentRequest,
        claim: ExpenseClaim | None = None,
    ) -> str:
        if claim is not None:
            manager_name = ExpenseClaimAccessPolicy.resolve_claim_manager_name(claim)
            if manager_name and not ExpenseClaimAccessPolicy.is_missing_value(manager_name):
                return manager_name

        context_json = payload.context_json or {}
        for key in ("manager_name", "managerName", "direct_manager_name", "directManagerName"):
            value = str(context_json.get(key) or "").strip()
            if value and not ExpenseClaimAccessPolicy.is_missing_value(value):
                return value
        return ""

    @staticmethod
    def _build_application_current_user(payload: UserAgentRequest) -> CurrentUserContext:
        context_json = payload.context_json or {}
        raw_role_codes = context_json.get("role_codes")
        if isinstance(raw_role_codes, list):
            role_codes = [str(item).strip() for item in raw_role_codes if str(item).strip()]
        else:
            role_codes = [item.strip() for item in str(raw_role_codes or "").split(",") if item.strip()]
        username = str(
            payload.user_id
            or context_json.get("username")
            or context_json.get("user_id")
            or context_json.get("employee_no")
            or context_json.get("name")
            or "anonymous"
        ).strip()
        name = str(context_json.get("name") or context_json.get("user_name") or username).strip()
        return CurrentUserContext(
            username=username or name or "anonymous",
            name=name or username or "anonymous",
            role_codes=role_codes,
            is_admin=bool(context_json.get("is_admin")),
            department_name=str(
                context_json.get("department_name")
                or context_json.get("department")
                or context_json.get("departmentName")
                or ""
            ).strip(),
        )

    @staticmethod
    def _resolve_application_expense_type_code(facts: dict[str, str]) -> str:
        application_type = str(facts.get("application_type") or "").strip()
        if "差旅" in application_type:
            return "travel_application"
        if "采购" in application_type:
            return "purchase_application"
        if "会务" in application_type or "会议" in application_type:
            return "meeting_application"
        return "expense_application"

    @staticmethod
    def _parse_application_amount_to_decimal(amount_text: str) -> Decimal:
        normalized = str(amount_text or "").replace(",", "").replace("，", "").strip()
        match = re.search(r"\d+(?:\.\d+)?", normalized)
        if not match:
            return Decimal("0.00")
        try:
            return Decimal(match.group(0)).quantize(Decimal("0.01"))
        except (InvalidOperation, ValueError):
            return Decimal("0.00")

    @staticmethod
    def _parse_application_occurred_at(time_text: str) -> datetime:
        normalized = str(time_text or "")
        match = re.search(r"(20\d{2})[-/.年](\d{1,2})[-/.月](\d{1,2})", normalized)
        if match:
            year, month, day = (int(part) for part in match.groups())
            return datetime(year, month, day, tzinfo=UTC)
        return datetime.now(UTC)

    def _build_submitted_application_payload(
        self,
        claim: ExpenseClaim | None,
        facts: dict[str, str],
    ) -> UserAgentDraftPayload | None:
        if claim is None:
            return None
        return UserAgentDraftPayload(
            draft_type="expense_application",
            title=str(facts.get("application_type") or "费用申请").strip() or "费用申请",
            body=self._build_application_summary(facts),
            confirmation_required=False,
            claim_id=claim.id,
            claim_no=claim.claim_no,
            status=claim.status,
            approval_stage=claim.approval_stage,
        )

    def _is_application_submit_confirmation(self, payload: UserAgentRequest) -> bool:
        compact_message = re.sub(r"\s+", "", str(payload.message or ""))
        if any(keyword in compact_message for keyword in APPLICATION_SUBMIT_KEYWORDS):
            return True
        if compact_message not in APPLICATION_SHORT_CONFIRMATIONS:
            return False
        history = (payload.context_json or {}).get("conversation_history")
        if not isinstance(history, list):
            return False
        return any(
            isinstance(item, dict)
            and str(item.get("role") or "").strip() == "assistant"
            and (
                "是否确认提交" in str(item.get("content") or "")
                or "当前状态：待确认提交" in str(item.get("content") or "")
                or "#application-submit" in str(item.get("content") or "")
                or "确认无误后" in str(item.get("content") or "")
            )
            for item in history[-4:]
        )

    def _build_simulated_application_no(
        self,
        payload: UserAgentRequest,
        facts: dict[str, str],
    ) -> str:
        return self._build_simulated_application_no_from_facts(
            facts,
            fallback_seed=str(payload.run_id or ""),
        )

    @staticmethod
    def _build_simulated_application_no_from_facts(
        facts: dict[str, str],
        *,
        fallback_seed: str = "",
    ) -> str:
        return build_document_number("application", timestamp=datetime.now(UTC))

    def _build_application_claim_no(
        self,
        payload: UserAgentRequest,
        facts: dict[str, str],
    ) -> str:
        return generate_unique_expense_claim_no(
            self.db,
            "application",
            timestamp=datetime.now(UTC),
        )
