from __future__ import annotations

import json
import re
import uuid
from datetime import date
from typing import Any

from app.schemas.steward import (
    StewardAttachmentGroup,
    StewardAttachmentInput,
    StewardCandidateFlow,
    StewardPendingFlowConfirmation,
    StewardPlanRequest,
    StewardPlanResponse,
    StewardTask,
    StewardThinkingEvent,
)
from app.services.ontology_field_registry import normalize_ontology_form_values
from app.services.steward_constants import BUSINESS_CANONICAL_FIELDS
from app.services.steward_intent_agent import StewardIntentAgentResult
from app.services.steward_intent_registry import (
    all_flow_ids,
    field_allowlist_for,
    get_intent,
    resolve_task_type_for_flow,
)


class StewardModelPlanBuilder:
    """把模型 function calling 返回值转换为小财管家的服务端计划。"""

    def __init__(self, planner: Any) -> None:
        self.planner = planner

    def build(
        self,
        intent_result: StewardIntentAgentResult,
        *,
        request: StewardPlanRequest,
        base_date: date,
    ) -> StewardPlanResponse | None:
        pending_flow_confirmation = self._build_pending_flow_confirmation(
            intent_result.payload,
            request=request,
            base_date=base_date,
        )
        tasks = self._build_tasks_from_model_payload(intent_result.payload, request, base_date)
        if not tasks and pending_flow_confirmation.status == "pending":
            return self._build_pending_flow_plan(
                pending_flow_confirmation,
                intent_result,
                request=request,
            )
        if not tasks:
            return None

        attachment_groups = self._build_attachment_groups_from_model_payload(
            intent_result.payload,
            request.attachments,
            tasks,
        )
        if request.attachments and not attachment_groups:
            attachment_groups = self.planner._build_attachment_groups(request.attachments, tasks)
        confirmation_groups = self.planner._build_confirmation_actions(tasks, attachment_groups)
        thinking_events = self._build_llm_thinking_events(
            intent_result.payload,
            tasks=tasks,
            attachment_groups=attachment_groups,
            attachments=request.attachments,
        )

        return StewardPlanResponse(
            plan_id=f"steward_plan_{uuid.uuid4().hex[:12]}",
            plan_status="needs_confirmation" if confirmation_groups else "ready_to_delegate",
            planning_source="llm_function_call",
            next_action="confirm_task" if confirmation_groups else "delegate_task",
            summary=self.planner._build_summary(tasks, attachment_groups),
            thinking_events=thinking_events,
            tasks=tasks,
            attachment_groups=attachment_groups,
            confirmation_groups=confirmation_groups,
            pending_flow_confirmation=pending_flow_confirmation,
            candidate_flows=pending_flow_confirmation.candidate_flows,
            model_call_traces=intent_result.model_call_traces,
        )

    def _build_pending_flow_plan(
        self,
        pending_flow_confirmation: StewardPendingFlowConfirmation,
        intent_result: StewardIntentAgentResult,
        *,
        request: StewardPlanRequest,
    ) -> StewardPlanResponse:
        return StewardPlanResponse(
            plan_id=f"steward_plan_{uuid.uuid4().hex[:12]}",
            plan_status="needs_flow_confirmation",
            planning_source="llm_function_call",
            next_action="confirm_flow",
            summary=self._build_pending_flow_summary(pending_flow_confirmation),
            thinking_events=self._build_pending_flow_thinking_events(intent_result.payload, request),
            pending_flow_confirmation=pending_flow_confirmation,
            candidate_flows=pending_flow_confirmation.candidate_flows,
            model_call_traces=intent_result.model_call_traces,
        )

    def _build_tasks_from_model_payload(
        self,
        payload: dict[str, Any],
        request: StewardPlanRequest,
        base_date: date,
    ) -> list[StewardTask]:
        raw_tasks = payload.get("tasks")
        # 兼容模型把 tasks 输出成 JSON 字符串(而非数组)的情况:
        # 某些供应商在流式输出时会把整个数组序列化为字符串,需要先反序列化。
        if isinstance(raw_tasks, str):
            raw_tasks = raw_tasks.strip()
            if not raw_tasks:
                return []
            try:
                parsed_tasks = json.loads(raw_tasks)
            except (TypeError, ValueError):
                # JSON 可能被截断或语法不完整,尝试从残缺字符串里抢救 task_type 等关键字段,
                # 避免模型偶发的格式抖动导致正确意图被整体丢弃。
                parsed_tasks = self._salvage_tasks_from_fragment(raw_tasks)
            raw_tasks = parsed_tasks if isinstance(parsed_tasks, list) else []
        if not isinstance(raw_tasks, list):
            return []

        tasks: list[StewardTask] = []
        for raw_task in raw_tasks:
            if not isinstance(raw_task, dict):
                continue
            task_type = str(raw_task.get("task_type") or "").strip()
            intent_descriptor = get_intent(task_type)
            if intent_descriptor is None:
                # 兼容模型把 flow_id(如 travel_application)误填为 task_type 的情况:
                # 通过 flow_id → task_type 映射还原,避免正确意图被丢弃。
                mapped_task_type = resolve_task_type_for_flow(task_type)
                if mapped_task_type is not None:
                    task_type = mapped_task_type
                    intent_descriptor = get_intent(task_type)
            if intent_descriptor is None:
                continue

            task_index = len(tasks) + 1
            fields = self._sanitize_model_ontology_fields(
                raw_task.get("ontology_fields"),
                request=request,
                base_date=base_date,
                task_type=task_type,
            )
            supplement_segment = " ".join(
                [
                    str(raw_task.get("title") or ""),
                    str(raw_task.get("summary") or ""),
                ]
            )
            supplement_fields = self.planner._extract_ontology_fields(
                supplement_segment,
                task_type,
                base_date,
                request,
            )
            for key, value in supplement_fields.items():
                fields.setdefault(key, value)

            assigned_agent = intent_descriptor.assigned_agent
            task_id = self._build_task_id(task_type, task_index)
            title_prefix = intent_descriptor.label
            title = self.planner._clean_text(raw_task.get("title")) or self.planner._build_task_title(
                title_prefix,
                fields,
                task_index,
            )
            summary = self.planner._clean_text(raw_task.get("summary")) or self.planner._build_task_summary(
                supplement_segment,
                fields,
            )
            missing_fields = self._sanitize_model_missing_fields(
                raw_task.get("missing_fields"),
                task_type=task_type,
                fields=fields,
            )
            requested_action = self._sanitize_requested_action(
                raw_task.get("requested_action"),
                fallback_text=request.message,
            )
            tasks.append(
                StewardTask(
                    task_id=task_id,
                    task_type=task_type,  # type: ignore[arg-type]
                    assigned_agent=assigned_agent,  # type: ignore[arg-type]
                    title=title,
                    summary=summary,
                    status="needs_confirmation",
                    confidence=self._resolve_model_confidence(
                        raw_task.get("confidence"),
                        segment=supplement_segment,
                        fields=fields,
                        task_type=task_type,
                    ),
                    requested_action=requested_action,  # type: ignore[arg-type]
                    ontology_fields=fields,
                    missing_fields=missing_fields,
                    confirmation_required=True,
                )
            )

        return tasks

    @staticmethod
    def _salvage_tasks_from_fragment(fragment: str) -> list[dict[str, Any]]:
        """从残缺的 JSON 字符串里抢救 task 结构,避免模型格式抖动导致意图被整体丢弃。

        模型偶发把 tasks 输出成被截断或语法不完整的 JSON 字符串,直接 json.loads 会失败。
        这里用正则提取 task_type、requested_action 和 ontology_fields 等关键字段,
        构造最小可用的 task dict 列表。
        """
        if not fragment:
            return []
        task_type_match = re.search(r'"task_type"\s*:\s*"([^"]+)"', fragment)
        if not task_type_match:
            return []
        task_type = task_type_match.group(1).strip()
        requested_action = "preview"
        action_match = re.search(r'"requested_action"\s*:\s*"([^"]+)"', fragment)
        if action_match:
            requested_action = action_match.group(1).strip()

        # 提取 ontology_fields 对象内的键值对
        ontology_fields: dict[str, str] = {}
        ontology_block = re.search(
            r'"ontology_fields"\s*:\s*\{([^}]*)\}',
            fragment,
        )
        if ontology_block:
            for key_match in re.finditer(
                r'"([^"]+)"\s*:\s*"([^"]*)"',
                ontology_block.group(1),
            ):
                ontology_fields[key_match.group(1)] = key_match.group(2)

        title_match = re.search(r'"title"\s*:\s*"([^"]+)"', fragment)
        summary_match = re.search(r'"summary"\s*:\s*"([^"]+)"', fragment)
        return [{
            "task_type": task_type,
            "title": title_match.group(1) if title_match else "",
            "summary": summary_match.group(1) if summary_match else "",
            "confidence": 0.8,
            "requested_action": requested_action,
            "ontology_fields": ontology_fields,
            "missing_fields": [],
        }]

    def _build_pending_flow_confirmation(
        self,
        payload: dict[str, Any],
        *,
        request: StewardPlanRequest,
        base_date: date,
    ) -> StewardPendingFlowConfirmation:
        raw_pending = payload.get("pending_flow_confirmation")
        raw_candidates = payload.get("candidate_flows")
        if isinstance(raw_pending, dict):
            raw_candidates = raw_pending.get("candidate_flows", raw_candidates)
            status = self.planner._clean_text(raw_pending.get("status")) or "pending"
            source_message = self.planner._clean_text(raw_pending.get("source_message")) or request.message
            reason = self.planner._clean_text(raw_pending.get("reason"))
        else:
            status = "pending" if isinstance(raw_candidates, list) and raw_candidates else "none"
            source_message = request.message
            reason = ""
        candidates = self._build_candidate_flows(raw_candidates, request=request, base_date=base_date)
        if status != "pending" or not candidates:
            return StewardPendingFlowConfirmation()
        return StewardPendingFlowConfirmation(
            status="pending",
            source_message=source_message,
            reason=reason or "当前话术同时可能进入申请或报销流程，需要先请用户确认。",
            candidate_flows=candidates,
        )

    def _build_candidate_flows(
        self,
        raw_candidates: Any,
        *,
        request: StewardPlanRequest,
        base_date: date,
    ) -> list[StewardCandidateFlow]:
        if not isinstance(raw_candidates, list):
            return []
        candidates: list[StewardCandidateFlow] = []
        for raw_candidate in raw_candidates:
            if not isinstance(raw_candidate, dict):
                continue
            flow_id = self.planner._clean_text(raw_candidate.get("flow_id"))
            if flow_id not in set(all_flow_ids()):
                continue
            task_type = resolve_task_type_for_flow(flow_id) or "expense_application"
            fields = self._sanitize_model_ontology_fields(
                raw_candidate.get("ontology_fields"),
                request=request,
                base_date=base_date,
                task_type=task_type,
            )
            if not fields:
                fields = self.planner._extract_ontology_fields(
                    request.message,
                    task_type,
                    base_date,
                    request,
                )
            missing_fields = self._sanitize_model_missing_fields(
                raw_candidate.get("missing_fields"),
                task_type=task_type,
                fields=fields,
            )
            label = self.planner._clean_text(raw_candidate.get("label")) or (
                "补办出差申请" if flow_id == "travel_application" else "发起费用报销"
            )
            candidates.append(
                StewardCandidateFlow(
                    flow_id=flow_id,  # type: ignore[arg-type]
                    label=label,
                    confidence=self._clamp_confidence(raw_candidate.get("confidence"), default=0.5),
                    reason=self.planner._clean_text(raw_candidate.get("reason")),
                    ontology_fields=fields,
                    missing_fields=missing_fields,
                )
            )
        return candidates[:2]

    def _build_pending_flow_thinking_events(
        self,
        payload: dict[str, Any],
        request: StewardPlanRequest,
    ) -> list[StewardThinkingEvent]:
        events = [
            StewardThinkingEvent(
                event_id="intent_agent_function_call",
                stage="llm_function_call",
                title="识别财务事项",
                content="我识别到这句话包含出差事项，但还需要确认您是要进入申请流程还是报销流程。",
            )
        ]
        raw_events = payload.get("thinking_events")
        if isinstance(raw_events, list):
            for raw_event in raw_events[:4]:
                if not isinstance(raw_event, dict):
                    continue
                title = self.planner._clean_text(raw_event.get("title"))
                content = self.planner._clean_text(raw_event.get("content"))
                if not title or not content:
                    continue
                events.append(
                    StewardThinkingEvent(
                        event_id=f"intent_agent_model_{len(events):03d}",
                        stage=self.planner._clean_text(raw_event.get("stage")) or "flow_confirmation",
                        title=title,
                        content=content,
                    )
                )
        if len(events) == 1:
            events.append(
                StewardThinkingEvent(
                    event_id="intent_agent_pending_flow",
                    stage="flow_confirmation",
                    title="等待确认流程方向",
                    content=f"当前输入“{request.message}”缺少明确的动作词，需要先由您选择是补办出差申请，还是发起费用报销。",
                )
            )
        return events

    @staticmethod
    def _build_pending_flow_summary(pending_flow_confirmation: StewardPendingFlowConfirmation) -> str:
        candidate_labels = [item.label for item in pending_flow_confirmation.candidate_flows if item.label]
        if len(candidate_labels) >= 2:
            return (
                f"我识别到这是一次财务事项，但还不能确定您要做的是"
                f"**{candidate_labels[0]}**还是**{candidate_labels[1]}**。请先选择一个方向。"
            )
        return "我识别到这是一次财务事项，但还需要先确认具体流程方向。"

    @staticmethod
    def _build_task_id(task_type: str, index: int) -> str:
        """根据 task_type 生成稳定的任务 ID 前缀。"""
        prefix_map = {
            "expense_application": "app",
            "reimbursement": "reim",
            "query_travel_standard": "query",
        }
        prefix = prefix_map.get(task_type, re.sub(r"[^a-z0-9]+", "_", task_type.lower()).strip("_") or "task")
        return f"task_{prefix}_{index:03d}"

    def _sanitize_model_ontology_fields(
        self,
        raw_fields: Any,
        *,
        request: StewardPlanRequest,
        base_date: date,
        task_type: str = "",
    ) -> dict[str, str]:
        allowlist = field_allowlist_for(task_type, fallback=BUSINESS_CANONICAL_FIELDS)
        normalized_context = normalize_ontology_form_values(request.context_json.get("review_form_values"))
        fields: dict[str, str] = {
            key: value
            for key, value in normalized_context.items()
            if key in allowlist and str(value or "").strip()
        }
        if not isinstance(raw_fields, dict):
            return fields

        normalized_model_fields = normalize_ontology_form_values(raw_fields)
        for key, value in normalized_model_fields.items():
            if key not in allowlist:
                continue
            normalized_value = self._normalize_model_field_value(key, value, base_date)
            if normalized_value:
                fields[key] = normalized_value
        if request.attachments and not fields.get("attachments"):
            fields["attachments"] = "、".join(item.name for item in request.attachments if item.name)
        return {key: value for key, value in fields.items() if key in allowlist and value}

    def _build_attachment_groups_from_model_payload(
        self,
        payload: dict[str, Any],
        attachments: list[StewardAttachmentInput],
        tasks: list[StewardTask],
    ) -> list[StewardAttachmentGroup]:
        raw_groups = payload.get("attachment_groups")
        if not isinstance(raw_groups, list) or not attachments:
            return []

        uploaded_names = {item.name for item in attachments if item.name}
        groups: list[StewardAttachmentGroup] = []
        for raw_group in raw_groups:
            if not isinstance(raw_group, dict):
                continue
            attachment_names = self._filter_uploaded_attachment_names(
                raw_group.get("attachment_names"),
                uploaded_names,
            )
            excluded_names = self._filter_uploaded_attachment_names(
                raw_group.get("excluded_attachment_names"),
                uploaded_names,
            )
            if not attachment_names and not excluded_names:
                continue

            scene = self.planner._clean_text(raw_group.get("scene")) or "other"
            groups.append(
                StewardAttachmentGroup(
                    group_id=f"ag_{self._slug_scene(scene)}_{len(groups) + 1:03d}",
                    target_task_id=self._resolve_model_group_target_task_id(raw_group, tasks),
                    scene=scene,
                    scene_label=self.planner._clean_text(raw_group.get("scene_label")) or "待确认费用",
                    attachment_names=attachment_names,
                    excluded_attachment_names=excluded_names,
                    confidence=self._clamp_confidence(raw_group.get("confidence"), default=0.68),
                    rationale=(
                        self.planner._clean_text(raw_group.get("rationale"))
                        or "模型根据附件线索生成归集建议。"
                    ),
                    confirmation_required=True,
                )
            )

        return groups

    def _build_llm_thinking_events(
        self,
        payload: dict[str, Any],
        *,
        tasks: list[StewardTask],
        attachment_groups: list[StewardAttachmentGroup],
        attachments: list[StewardAttachmentInput],
    ) -> list[StewardThinkingEvent]:
        events = [
            StewardThinkingEvent(
                event_id="intent_agent_function_call",
                stage="llm_function_call",
                title="拆解财务事项",
                content=(
                    "我正在把这句话拆成可执行的财务事项，并检查每一项应该进入申请流程还是报销流程。"
                ),
            )
        ]
        raw_events = payload.get("thinking_events")
        if isinstance(raw_events, list):
            for raw_event in raw_events[:4]:
                if not isinstance(raw_event, dict):
                    continue
                title = self.planner._clean_text(raw_event.get("title"))
                content = self.planner._clean_text(raw_event.get("content"))
                if not title or not content:
                    continue
                events.append(
                    StewardThinkingEvent(
                        event_id=f"intent_agent_model_{len(events):03d}",
                        stage=self.planner._clean_text(raw_event.get("stage")) or "model_summary",
                        title=title,
                        content=content,
                    )
                )
        if len(events) == 1:
            events.extend(self.planner._build_thinking_events(tasks, attachment_groups, attachments)[1:])
        else:
            gap_event = self.planner._build_business_gap_thinking_event(tasks)
            if gap_event:
                events.append(gap_event)
        return events

    def _sanitize_model_missing_fields(
        self,
        raw_missing_fields: Any,
        *,
        task_type: str,
        fields: dict[str, str],
    ) -> list[str]:
        allowlist = field_allowlist_for(task_type, fallback=BUSINESS_CANONICAL_FIELDS)
        missing_fields: list[str] = []
        if isinstance(raw_missing_fields, list):
            for item in raw_missing_fields:
                key = str(item or "").strip()
                if key in allowlist and key not in missing_fields and not fields.get(key):
                    missing_fields.append(key)
        # 查询类意图没有强制的必填字段,跳过 planner 的必填推断
        intent_descriptor = get_intent(task_type)
        if intent_descriptor is not None and not intent_descriptor.flow_id:
            return missing_fields
        for key in self.planner._resolve_missing_fields(task_type, fields):
            if key not in missing_fields:
                missing_fields.append(key)
        return missing_fields

    def _sanitize_requested_action(self, raw_action: Any, *, fallback_text: str = "") -> str:
        action = self.planner._clean_text(raw_action)
        if action in {"preview", "save_draft", "submit"}:
            return action
        compact = self.planner._clean_text(fallback_text).replace(" ", "")
        if any(keyword in compact for keyword in ("直接提交", "提交申请", "确认提交", "提交审批")):
            return "submit"
        if any(keyword in compact for keyword in ("保存草稿", "存草稿", "先保存")):
            return "save_draft"
        return "preview"

    def _resolve_model_confidence(
        self,
        value: Any,
        *,
        segment: str,
        fields: dict[str, str],
        task_type: str,
    ) -> float:
        return self._clamp_confidence(
            value,
            default=self.planner._resolve_task_confidence(segment, fields, task_type),
        )

    def _normalize_model_field_value(self, key: str, value: Any, base_date: date) -> str:
        cleaned = self.planner._clean_text(value)
        if not cleaned:
            return ""
        if key == "time_range":
            if re.search(r"(?:至|到|~|～|—|--|-).*\d", cleaned):
                return cleaned
            return self.planner._extract_time_range(cleaned, base_date) or cleaned
        if key == "expense_type":
            return self._normalize_expense_type_value(cleaned)
        if key == "transport_mode":
            return self._normalize_transport_mode_value(cleaned)
        return cleaned

    @staticmethod
    def _normalize_expense_type_value(value: str) -> str:
        normalized = str(value or "").strip().lower()
        if normalized in {"travel", "travel_application", "差旅", "差旅费", "出差"}:
            return "travel"
        if normalized in {"transport", "traffic", "交通", "交通费", "打车", "出租车"}:
            return "transport"
        if normalized in {"entertainment", "meal", "招待", "招待费", "接待", "接待费", "餐饮", "业务招待", "业务招待费"}:
            return "entertainment"
        if normalized in {"office", "办公", "办公用品"}:
            return "office"
        return normalized

    @staticmethod
    def _normalize_transport_mode_value(value: str) -> str:
        normalized = str(value or "").strip().lower()
        if normalized in {"train", "高铁", "动车", "火车"}:
            return "train"
        if normalized in {"flight", "air", "飞机", "机票", "航班"}:
            return "flight"
        if normalized in {"taxi", "出租车", "的士", "网约车", "打车"}:
            return "taxi"
        if normalized in {"subway", "地铁"}:
            return "subway"
        return normalized

    @staticmethod
    def _filter_uploaded_attachment_names(raw_names: Any, uploaded_names: set[str]) -> list[str]:
        if not isinstance(raw_names, list):
            return []
        names: list[str] = []
        for raw_name in raw_names:
            name = str(raw_name or "").strip()
            if name in uploaded_names and name not in names:
                names.append(name)
        return names

    @staticmethod
    def _resolve_model_group_target_task_id(raw_group: dict[str, Any], tasks: list[StewardTask]) -> str | None:
        try:
            target_index = int(raw_group.get("target_task_index") or 0)
        except (TypeError, ValueError):
            target_index = 0
        if target_index > 0 and target_index <= len(tasks):
            return tasks[target_index - 1].task_id

        target_task_id = str(raw_group.get("target_task_id") or "").strip()
        if target_task_id and any(task.task_id == target_task_id for task in tasks):
            return target_task_id
        return None

    @staticmethod
    def _slug_scene(value: str) -> str:
        normalized = re.sub(r"[^a-zA-Z0-9_]+", "_", str(value or "").strip().lower()).strip("_")
        return normalized or "other"

    @staticmethod
    def _clamp_confidence(value: Any, *, default: float) -> float:
        try:
            parsed = float(value)
        except (TypeError, ValueError):
            parsed = default
        return round(min(1.0, max(0.0, parsed)), 2)
