from __future__ import annotations

from datetime import UTC, datetime
from typing import Any

from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.schemas.expense_application_preview import ExpenseApplicationPreviewDecisionCreate
from app.schemas.ontology import OntologyParseResult, OntologyPermission
from app.schemas.reimbursement import ExpenseApplicationPreviewActionPayload
from app.schemas.steward import (
    StewardActionExecuteRequest,
    StewardActionExecuteResponse,
    StewardTask,
)
from app.schemas.user_agent import UserAgentRequest
from app.services.attachment_association_jobs import AttachmentAssociationJobRunner
from app.services.expense_application_preview_workflow import ExpenseApplicationPreviewWorkflow
from app.services.expense_claims import ExpenseClaimService
from app.services.steward_intent_registry import (
    all_noop_actions,
    all_side_effect_actions,
    resolve_intent_by_action,
)
from app.services.user_agent import UserAgentService
from app.services.user_agent_application_dates import resolve_application_days_from_time_range

SUPPORTED_ACTIONS = {
    "fill_application_fields",
    "build_application_preview",
    "fill_reimbursement_fields",
    "build_reimbursement_preview",
    "validate_required_fields",
    "run_duplicate_precheck",
    "save_application_draft",
    "submit_application",
    "create_reimbursement_draft",
    "link_existing_application",
    "associate_attachments",
}
APPLICATION_SIDE_EFFECT_ACTIONS = {"save_application_draft", "submit_application", "run_duplicate_precheck"}
REIMBURSEMENT_SIDE_EFFECT_ACTIONS = {"create_reimbursement_draft", "link_existing_application", "associate_attachments"}
NOOP_ACTIONS = {
    "fill_application_fields",
    "fill_reimbursement_fields",
    "build_reimbursement_preview",
    "validate_required_fields",
}
TRANSPORT_MODE_LABELS = {
    "train": "火车",
    "rail": "火车",
    "high_speed_rail": "火车",
    "flight": "飞机",
    "airplane": "飞机",
    "plane": "飞机",
    "ship": "轮船",
    "boat": "轮船",
    "taxi": "出租车",
}
EXPENSE_TYPE_LABELS = {
    "travel": "差旅费",
    "transport": "交通费",
    "hotel": "住宿费",
    "meal": "业务招待费",
    "meeting": "会务费",
    "office": "办公用品费",
    "other": "其他费用",
}
APPLICATION_TYPE_LABELS = {
    "travel": "差旅费用申请",
    "travel_application": "差旅费用申请",
    "transport": "交通费用申请",
    "hotel": "住宿费用申请",
    "meeting": "会务费用申请",
    "office": "办公费用申请",
}


class StewardActionExecutor:
    """执行 LangGraph 规划出的确定性白名单动作。"""

    def __init__(self, db: Session) -> None:
        self.db = db

    def execute(
        self,
        request: StewardActionExecuteRequest,
        current_user: CurrentUserContext,
    ) -> StewardActionExecuteResponse:
        action_type = self._normalize_action_type(request.action_type)
        trace = [self._trace("received", action_type=action_type, plan_id=request.plan_id)]
        supported = SUPPORTED_ACTIONS | all_side_effect_actions() | all_noop_actions()
        if action_type not in supported:
            return self._blocked(
                action_type,
                f"不支持的小财管家动作：{action_type or '空动作'}。",
                trace=[*trace, self._trace("blocked", reason="unsupported_action")],
            )

        task = request.task
        # 申请预览已经接入服务端决策签发，不得再被注册表中的历史 NOOP 声明吞掉。
        noop_actions = (NOOP_ACTIONS | all_noop_actions()) - {"build_application_preview"}
        if task is None and action_type not in noop_actions:
            return self._blocked(
                action_type,
                "动作缺少任务快照，无法安全执行。",
                trace=[*trace, self._trace("blocked", reason="missing_task")],
            )

        blocked_reasons = self._resolve_task_blockers(task)
        if blocked_reasons:
            return self._blocked(
                action_type,
                "当前任务仍有必填信息缺失，已停止执行动作。",
                blocked_reasons=blocked_reasons,
                trace=[*trace, self._trace("blocked", reason="missing_fields")],
            )

        if action_type in noop_actions:
            return StewardActionExecuteResponse(
                action_type=action_type,
                status="succeeded",
                message="动作已确认，无需调用外部业务服务。",
                result_payload={
                    "task_id": task.task_id if task is not None else "",
                    "action_type": action_type,
                },
                trace=[*trace, self._trace("completed", mode="noop")],
            )

        # 优先走注册表:查到 action 所属意图的 executor 即委托执行
        intent = resolve_intent_by_action(action_type)
        if intent is not None and intent.executor is not None:
            return intent.executor(self, request, current_user, trace)

        # 兼容回退:注册表未命中时按旧逻辑分发
        if action_type == "build_application_preview":
            return self._issue_application_preview(request, current_user, trace)
        if action_type == "run_duplicate_precheck":
            return self._run_duplicate_precheck(request, current_user, trace)
        if action_type in APPLICATION_SIDE_EFFECT_ACTIONS:
            return self._execute_application_action(request, current_user, action_type, trace)
        if action_type in REIMBURSEMENT_SIDE_EFFECT_ACTIONS:
            return self._execute_reimbursement_action(request, current_user, action_type, trace)
        if action_type == "associate_attachments":
            return self._execute_associate_attachments_action(request, current_user, trace)

        return self._blocked(
            action_type,
            f"动作 {action_type} 暂未接入执行器。",
            trace=[*trace, self._trace("blocked", reason="unwired_action")],
        )

    def _dispatch_application_action(
        self,
        request: StewardActionExecuteRequest,
        current_user: CurrentUserContext,
        trace: list[dict[str, Any]],
    ) -> StewardActionExecuteResponse:
        """registry 入口:分发申请类副作用动作。"""
        action_type = self._normalize_action_type(request.action_type)
        if action_type == "build_application_preview":
            return self._issue_application_preview(request, current_user, trace)
        if action_type == "run_duplicate_precheck":
            return self._run_duplicate_precheck(request, current_user, trace)
        return self._execute_application_action(request, current_user, action_type, trace)

    def _issue_application_preview(
        self,
        request: StewardActionExecuteRequest,
        current_user: CurrentUserContext,
        trace: list[dict[str, Any]],
    ) -> StewardActionExecuteResponse:
        request_id = self._resolve_required_client_trace_id(request)
        if not request_id:
            return self._blocked(
                "build_application_preview",
                "生成申请核对表需要稳定的 client_trace_id。",
                blocked_reasons=["missing_client_trace_id"],
                trace=[*trace, self._trace("blocked", reason="missing_client_trace_id")],
            )

        try:
            issued = ExpenseApplicationPreviewWorkflow(self.db).issue(
                ExpenseApplicationPreviewDecisionCreate(
                    message=self._resolve_message(request),
                    conversation_id=request.conversation_id,
                    request_id=request_id,
                ),
                current_user,
            )
        except ValueError as exc:
            return self._failed("build_application_preview", str(exc), trace)

        result_payload = issued.model_dump(mode="json")
        return StewardActionExecuteResponse(
            action_type="build_application_preview",
            status="succeeded",
            message="申请核对表已生成，请确认后再保存或提交。",
            result_payload=result_payload,
            trace=[
                *trace,
                self._trace(
                    "completed",
                    service="ExpenseApplicationPreviewWorkflow",
                    decision_id=issued.decision_id,
                ),
            ],
        )

    def _dispatch_reimbursement_action(
        self,
        request: StewardActionExecuteRequest,
        current_user: CurrentUserContext,
        trace: list[dict[str, Any]],
    ) -> StewardActionExecuteResponse:
        """registry 入口:分发报销类副作用动作。"""
        action_type = self._normalize_action_type(request.action_type)
        if action_type == "associate_attachments":
            return self._execute_associate_attachments_action(request, current_user, trace)
        return self._execute_reimbursement_action(request, current_user, action_type, trace)

    def _run_duplicate_precheck(
        self,
        request: StewardActionExecuteRequest,
        current_user: CurrentUserContext,
        trace: list[dict[str, Any]],
    ) -> StewardActionExecuteResponse:
        payload = self._build_application_user_agent_request(
            request,
            current_user,
            action_type="submit_application",
            force_submit_message=False,
        )
        service = UserAgentService(self.db)
        facts = service._resolve_expense_application_facts(payload)
        duplicate = service._find_duplicate_expense_application_record(payload, facts)
        if duplicate is not None:
            result_payload = {
                "status": "blocked",
                "blocking": True,
                "duplicate_claim_id": str(duplicate.id or ""),
                "duplicate_claim_no": str(duplicate.claim_no or ""),
                "duplicate_stage": str(duplicate.approval_stage or ""),
            }
            return StewardActionExecuteResponse(
                action_type="run_duplicate_precheck",
                status="blocked",
                message="检测到同一申请人、同一申请类型、同一时间段已有申请单，已停止直接提交。",
                blocked_reasons=["duplicate_application"],
                result_payload=result_payload,
                trace=[*trace, self._trace("blocked", reason="duplicate_application")],
            )
        result_payload = {"status": "ok", "blocking": False}
        return StewardActionExecuteResponse(
            action_type="run_duplicate_precheck",
            status="succeeded",
            message="未发现重复或冲突申请，可以继续提交。",
            result_payload=result_payload,
            trace=[*trace, self._trace("completed", mode="duplicate_precheck")],
        )

    def _execute_application_action(
        self,
        request: StewardActionExecuteRequest,
        current_user: CurrentUserContext,
        action_type: str,
        trace: list[dict[str, Any]],
    ) -> StewardActionExecuteResponse:
        if action_type == "submit_application" and not request.confirmed:
            return StewardActionExecuteResponse(
                action_type=action_type,
                status="needs_confirmation",
                requires_confirmation=True,
                message="提交申请前需要用户确认。请确认申请核对表无误后再提交。",
                trace=[*trace, self._trace("needs_confirmation")],
            )
        if action_type == "submit_application":
            precheck_blocker = self._resolve_submit_precheck_blocker(request.context_json)
            if precheck_blocker:
                return self._blocked(
                    action_type,
                    precheck_blocker,
                    blocked_reasons=["precheck_not_passed"],
                    trace=[*trace, self._trace("blocked", reason="precheck_not_passed")],
                )

        decision_id = str(request.decision_id or "").strip()
        if not decision_id:
            return self._blocked(
                action_type,
                "保存或提交申请必须携带服务端签发的 decision_id。",
                blocked_reasons=["missing_decision_id"],
                trace=[*trace, self._trace("blocked", reason="missing_decision_id")],
            )
        request_id = self._resolve_required_client_trace_id(request)
        if not request_id:
            return self._blocked(
                action_type,
                "保存或提交申请需要稳定的 client_trace_id。",
                blocked_reasons=["missing_client_trace_id"],
                trace=[*trace, self._trace("blocked", reason="missing_client_trace_id")],
            )

        resolved_action = "save_draft" if action_type == "save_application_draft" else "submit"
        message = self._resolve_message(request)
        if resolved_action == "submit" and "确认提交" not in message and "直接提交" not in message:
            message = "\n".join([message, "确认提交"]).strip()
        if resolved_action == "save_draft" and "保存草稿" not in message:
            message = "\n".join([message, "保存草稿"]).strip()
        payload = ExpenseApplicationPreviewActionPayload(
            source="steward_action",
            user_id=current_user.username,
            conversation_id=request.conversation_id,
            action_type=resolved_action,
            decision_id=decision_id,
            request_id=request_id,
            message=message,
            context_json=self._build_application_context_json(
                request,
                current_user,
                action_type,
            ),
        )
        try:
            workflow_response = ExpenseApplicationPreviewWorkflow(self.db).execute(
                payload,
                current_user,
            )
        except ValueError as exc:
            return self._failed(action_type, str(exc), trace)

        result_payload = workflow_response.result.model_dump(mode="json")
        draft_payload = result_payload.get("draft_payload")
        status = "succeeded" if draft_payload is not None else "blocked"
        blocked_reasons = [] if draft_payload is not None else ["application_not_persisted"]
        return StewardActionExecuteResponse(
            action_type=action_type,
            status=status,
            message=workflow_response.result.message,
            blocked_reasons=blocked_reasons,
            result_payload=result_payload,
            trace=[
                *trace,
                self._trace(
                    "completed",
                    service="ExpenseApplicationPreviewWorkflow",
                    decision_id=decision_id,
                    renewed_decision_id=workflow_response.result.decision_id or "",
                ),
            ],
        )

    def _execute_reimbursement_action(
        self,
        request: StewardActionExecuteRequest,
        current_user: CurrentUserContext,
        action_type: str,
        trace: list[dict[str, Any]],
    ) -> StewardActionExecuteResponse:
        context_json = self._build_reimbursement_context_json(request, current_user)
        run_id = self._build_run_id(request, action_type)
        ontology = OntologyParseResult(
            scenario="expense",
            intent="draft",
            permission=OntologyPermission(
                level="draft_write",
                allowed=True,
                reason="小财管家白名单动作创建报销草稿。",
            ),
            confidence=1.0,
            run_id=run_id,
        )
        try:
            result = ExpenseClaimService(self.db).save_or_submit_from_ontology(
                run_id=run_id,
                user_id=current_user.username,
                message=self._resolve_message(request),
                ontology=ontology,
                context_json=context_json,
            )
        except ValueError as exc:
            return self._failed(action_type, str(exc), trace)

        persisted = str(result.get("claim_id") or "").strip() and str(result.get("status") or "").strip() == "draft"
        return StewardActionExecuteResponse(
            action_type=action_type,
            status="succeeded" if persisted else "blocked",
            message=str(result.get("message") or "报销草稿已生成。").strip(),
            blocked_reasons=[] if persisted else ["reimbursement_not_persisted"],
            result_payload=result,
            trace=[*trace, self._trace("completed", service="ExpenseClaimService")],
        )

    def _execute_associate_attachments_action(
        self,
        request: StewardActionExecuteRequest,
        current_user: CurrentUserContext,
        trace: list[dict[str, Any]],
    ) -> StewardActionExecuteResponse:
        receipt_ids = self._resolve_receipt_ids(request)
        if not receipt_ids:
            return self._blocked(
                "associate_attachments",
                "关联附件前需要先提供票据夹 receipt_ids。",
                blocked_reasons=["missing_receipt_ids"],
                trace=[*trace, self._trace("blocked", reason="missing_receipt_ids")],
            )
        try:
            result = AttachmentAssociationJobRunner(self.db).run(
                receipt_ids=receipt_ids,
                current_user=current_user,
            )
        except ValueError as exc:
            return self._blocked(
                "associate_attachments",
                str(exc),
                blocked_reasons=["attachment_association_blocked"],
                trace=[*trace, self._trace("blocked", reason="attachment_association_blocked")],
            )
        except Exception as exc:
            return self._failed("associate_attachments", str(exc), trace)

        return StewardActionExecuteResponse(
            action_type="associate_attachments",
            status="succeeded",
            message=str(
                result.get("message")
                or f"已自动关联到 {result.get('claim_no') or '报销草稿'}。"
            ).strip(),
            result_payload={
                **dict(result or {}),
                "receipt_ids": receipt_ids,
            },
            trace=[*trace, self._trace("completed", service="AttachmentAssociationJobRunner")],
        )

    def _build_application_user_agent_request(
        self,
        request: StewardActionExecuteRequest,
        current_user: CurrentUserContext,
        *,
        action_type: str,
        force_submit_message: bool,
    ) -> UserAgentRequest:
        run_id = self._build_run_id(request, action_type)
        context_json = self._build_application_context_json(request, current_user, action_type)
        message = self._resolve_message(request)
        if force_submit_message and "确认提交" not in message and "直接提交" not in message:
            message = "\n".join([message, "确认提交"]).strip()
        ontology = OntologyParseResult(
            scenario="expense",
            intent="operate",
            permission=OntologyPermission(
                level="approval_required",
                allowed=True,
                reason="小财管家白名单动作执行申请操作。",
            ),
            confidence=1.0,
            run_id=run_id,
        )
        return UserAgentRequest(
            run_id=run_id,
            user_id=current_user.username,
            message=message,
            ontology=ontology,
            context_json=context_json,
            tool_payload={},
            selected_capability_codes=[],
            degraded=False,
            requires_confirmation=False,
        )

    def _build_application_context_json(
        self,
        request: StewardActionExecuteRequest,
        current_user: CurrentUserContext,
        action_type: str,
    ) -> dict[str, Any]:
        fields = self._resolve_ontology_fields(request.task)
        preview_fields = {
            "applicationType": self._resolve_application_type_label(fields.get("expense_type")),
            "time": str(fields.get("time_range") or ""),
            "location": str(fields.get("location") or ""),
            "reason": str(fields.get("reason") or ""),
            "days": self._resolve_days_text(fields.get("time_range")),
            "transportMode": self._resolve_transport_label(fields.get("transport_mode")),
            "amount": str(fields.get("amount") or ""),
            "applicant": current_user.name,
            "department": current_user.department_name,
            "position": current_user.position,
            "grade": current_user.grade,
            "managerName": current_user.manager_name,
        }
        context_json = {
            **dict(request.context_json or {}),
            "session_type": "application",
            "entry_source": "steward_action_executor",
            "document_type": "expense_application",
            "application_stage": "expense_application",
            "tenant_id": current_user.tenant_id,
            "role_codes": current_user.role_codes,
            "is_admin": current_user.is_admin,
            "username": current_user.username,
            "name": current_user.name,
            "department_name": current_user.department_name,
            "position": current_user.position,
            "grade": current_user.grade,
            "employee_no": current_user.employee_no,
            "manager_name": current_user.manager_name,
            "application_preview": {"fields": preview_fields},
        }
        if action_type == "save_application_draft":
            context_json["application_action"] = "save_draft"
            context_json["application_save_mode"] = True
        return context_json

    def _build_reimbursement_context_json(
        self,
        request: StewardActionExecuteRequest,
        current_user: CurrentUserContext,
    ) -> dict[str, Any]:
        fields = self._resolve_ontology_fields(request.task)
        review_form_values = {
            "expense_type": self._resolve_expense_type_label(fields.get("expense_type")),
            "time_range": str(fields.get("time_range") or ""),
            "occurred_date": str(fields.get("time_range") or ""),
            "location": str(fields.get("location") or ""),
            "reason": str(fields.get("reason") or ""),
            "amount": str(fields.get("amount") or ""),
            "transport_mode": self._resolve_transport_label(fields.get("transport_mode")),
            "attachments": str(fields.get("attachments") or ""),
        }
        return {
            **dict(request.context_json or {}),
            "session_type": "expense",
            "entry_source": "steward_action_executor",
            "review_action": "save_draft",
            "tenant_id": current_user.tenant_id,
            "review_form_values": review_form_values,
            "user_input_text": self._resolve_message(request),
            "role_codes": current_user.role_codes,
            "is_admin": current_user.is_admin,
            "username": current_user.username,
            "name": current_user.name,
            "department_name": current_user.department_name,
            "position": current_user.position,
            "grade": current_user.grade,
            "employee_no": current_user.employee_no,
            "manager_name": current_user.manager_name,
        }

    @staticmethod
    def _resolve_receipt_ids(request: StewardActionExecuteRequest) -> list[str]:
        context_json = dict(request.context_json or {})
        raw_values = context_json.get("receipt_ids") or context_json.get("receiptIds") or []
        if isinstance(raw_values, str):
            raw_values = [item.strip() for item in raw_values.split(",")]
        if not isinstance(raw_values, list):
            raw_values = []
        receipt_ids = [
            str(item or "").strip()
            for item in raw_values
            if str(item or "").strip()
        ]
        if receipt_ids:
            return list(dict.fromkeys(receipt_ids))
        fields = StewardActionExecutor._resolve_ontology_fields(request.task)
        raw_receipt = str(fields.get("receipt_ids") or fields.get("receiptIds") or "").strip()
        if not raw_receipt:
            return []
        return list(dict.fromkeys(item.strip() for item in raw_receipt.split(",") if item.strip()))

    @staticmethod
    def _resolve_submit_precheck_blocker(context_json: dict[str, Any]) -> str:
        precheck = context_json.get("precheck_result") or context_json.get("precheckResult")
        if not isinstance(precheck, dict):
            return "提交申请前需要先完成重复/冲突预检查。"
        if bool(precheck.get("blocking")):
            return str(precheck.get("summary") or "重复/冲突预检查未通过，已停止提交。").strip()
        status = str(precheck.get("status") or "").strip().lower()
        if status not in {"ok", "passed", "succeeded"}:
            return "重复/冲突预检查未通过，已停止提交。"
        return ""

    @staticmethod
    def _resolve_task_blockers(task: StewardTask | None) -> list[str]:
        if task is None:
            return []
        return [
            str(field or "").strip()
            for field in list(task.missing_fields or [])
            if str(field or "").strip()
        ]

    @staticmethod
    def _resolve_ontology_fields(task: StewardTask | None) -> dict[str, str]:
        if task is None or not isinstance(task.ontology_fields, dict):
            return {}
        return {
            str(key or "").strip(): str(value or "").strip()
            for key, value in task.ontology_fields.items()
            if str(key or "").strip() and str(value or "").strip()
        }

    @staticmethod
    def _resolve_message(request: StewardActionExecuteRequest) -> str:
        message = str(request.message or "").strip()
        if message:
            return message
        if request.task is not None:
            return str(request.task.summary or request.task.title or "").strip()
        return "小财管家动作执行"

    @staticmethod
    def _resolve_transport_label(value: Any) -> str:
        text = str(value or "").strip()
        return TRANSPORT_MODE_LABELS.get(text.lower(), text)

    @staticmethod
    def _resolve_application_type_label(value: Any) -> str:
        text = str(value or "").strip()
        return APPLICATION_TYPE_LABELS.get(text.lower(), text or "费用申请")

    @staticmethod
    def _resolve_expense_type_label(value: Any) -> str:
        text = str(value or "").strip()
        return EXPENSE_TYPE_LABELS.get(text.lower(), text or "其他费用")

    @staticmethod
    def _resolve_days_text(value: Any) -> str:
        days = resolve_application_days_from_time_range(str(value or ""))
        return f"{days}天" if days else ""

    @staticmethod
    def _normalize_action_type(value: str) -> str:
        return str(value or "").strip()

    @staticmethod
    def _build_run_id(request: StewardActionExecuteRequest, action_type: str) -> str:
        trace_id = str(request.client_trace_id or "").strip()
        if trace_id:
            return f"steward-action:{trace_id}"
        task_id = str(request.task.task_id if request.task is not None else "").strip()
        suffix = task_id or datetime.now(UTC).strftime("%Y%m%d%H%M%S%f")
        return f"steward-action:{action_type}:{suffix}"

    @staticmethod
    def _resolve_required_client_trace_id(request: StewardActionExecuteRequest) -> str:
        return str(request.client_trace_id or "").strip()

    @staticmethod
    def _trace(stage: str, **extra: Any) -> dict[str, Any]:
        return {
            "stage": stage,
            "at": datetime.now(UTC).isoformat(),
            **extra,
        }

    @staticmethod
    def _blocked(
        action_type: str,
        message: str,
        *,
        blocked_reasons: list[str] | None = None,
        trace: list[dict[str, Any]] | None = None,
    ) -> StewardActionExecuteResponse:
        return StewardActionExecuteResponse(
            action_type=action_type,
            status="blocked",
            execution_source="rule_fallback",
            fallback_used=True,
            message=message,
            blocked_reasons=blocked_reasons or [message],
            trace=trace or [],
        )

    def _failed(
        self,
        action_type: str,
        message: str,
        trace: list[dict[str, Any]],
    ) -> StewardActionExecuteResponse:
        return StewardActionExecuteResponse(
            action_type=action_type,
            status="failed",
            message=message or "动作执行失败。",
            blocked_reasons=[message] if message else [],
            trace=[*trace, self._trace("failed", reason=message)],
        )
