"""PydanticAI Agent 模块

架构：单 Agent + 多 Tool 模式
- Agent 负责对话逻辑和决策
- Tool 负责具体能力：看图/查客户/转接
- AI 自主决定何时调用哪个工具，时序自然，不需要外部协调
"""
import os
import glob
import asyncio
import random
import hashlib
import re
import json
import logging
from pathlib import Path
from typing import Optional, Dict, List, Any, Tuple
from datetime import datetime
from pydantic import BaseModel, Field, model_validator
from pydantic_ai import Agent, RunContext
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
from dotenv import load_dotenv
from utils.metrics_tracker import emit as metrics_emit
from utils.observability import emit_activity
from core.quote_state_machine import QuoteStateMachine
from services.risk_service import RiskService
from core.agent_pre_rules import AgentPreRuleService
from core.order_helpers import parse_order_info, order_instruction as build_order_instruction
from core.collection_intent_helpers import (
    append_requirement,
    build_collect_ack,
    build_collect_progress_reply,
    build_collect_remind,
    build_find_image_clarify_reply,
    build_not_understood_reply,
    classify_short_customer_text,
    is_batch_finish_intent,
    is_batch_finish_signal,
    is_cross_image_composite_intent,
    is_find_image_not_edit_conflict,
    is_related_image_followup_intent,
    is_result_followup_query,
    needs_clarification_in_collecting,
)
from core.agent_prompts import (
    build_after_sale_prompt,
    build_natural_reply_prompt,
    build_order_prompt,
    build_pricing_prompt,
    build_processing_prompt,
    build_risk_prompt,
    build_similar_prompt,
    build_system_prompt,
)
from core.risk_text_helpers import is_map_inquiry, is_political_inquiry
from core.context_helpers import (
    calc_avg_complexity,
    get_conversation_context,
    get_customer_profile_context,
    get_intent_emotion_hint,
    get_refusal_context_hint,
)
from core.batch_quote_helpers import (
    assess_batch_risk,
    build_batch_pricing_plan,
    build_batch_quote_reply,
    calc_requirement_surcharge,
    prepare_batch_intake,
)
from core.prompt_builder import build_prompt as build_agent_prompt, split_customer_text
from core.image_workflow_router import handle_image_workflow as route_image_workflow
from core.message_orchestrator import process_incoming_message
from core.conversation_state_store import (
    get_conversation_state as load_conversation_state,
    mark_quote_ready as state_mark_quote_ready,
    refresh_quote_phase as state_refresh_quote_phase,
    should_defer_batch_quote as state_should_defer_batch_quote,
    sync_pending_quote_state as state_sync_pending_quote_state,
    restore_pending_quote_state as state_restore_pending_quote_state,
    cleanup_inactive as state_cleanup_inactive,
)

load_dotenv()

from services.service_tuhui_upload import upload_to_tuhui
from core.workflow_router import get_workflow_router

# ========== 企业微信通知 ==========
_WECHAT_WEBHOOK = os.getenv("WECHAT_WEBHOOK", "")
logger = logging.getLogger("cs_agent")


async def _notify_wechat(content: str, tag: str = "通知"):
    """发送企业微信 markdown 通知，任何异常都发"""
    if not _WECHAT_WEBHOOK:
        logger.info("[%s] 未配置 WECHAT_WEBHOOK，跳过推送", tag)
        return
    try:
        import httpx
        async with httpx.AsyncClient(timeout=10) as client:
            resp = await client.post(_WECHAT_WEBHOOK, json={
                "msgtype": "markdown",
                "markdown": {"content": content}
            })
            data = resp.json()
            if data.get("errcode") == 0:
                logger.info("[%s] 企业微信推送成功", tag)
            else:
                logger.warning("[%s] 企业微信推送失败: %s", tag, data)
    except Exception as e:
        logger.exception("[%s] 企业微信发送异常: %s", tag, e)


async def _notify_wechat_overdue():
    """API 欠费时发企业微信通知"""
    await _notify_wechat(
        "⚠️ **火山引擎 API 欠费**，客服AI已停止响应，请立即充值！\n"
        "地址：https://console.volcengine.com/ark"
    )


# ========== 转接常量 ==========
TRANSFER_MESSAGE = "话术|[转移会话],分组20252916034,无原因"
CASE_LIBRARY_LINK = "https://www.yuque.com/zuowei-dfvpq/kge0in/mynala0g35b8cec5"
TAOBAO_REPLY_TAILS = ("嗯", "哦", "好的", "嗯咯", "嗯啦")


def _is_ack_like_customer_text(text: str) -> bool:
    """客户是否为确认型短句（好的/嗯/收到/ok 等）。"""
    s = (text or "").strip().lower()
    if not s:
        return False
    s = s.rstrip("。.!！？~～")
    ack_set = {
        "好", "好的", "嗯", "嗯嗯", "收到", "知道了", "明白了",
        "ok", "okay", "行", "可以", "好嘞", "好的呢",
    }
    return s in ack_set


def _is_meaningless_short_text(text: str) -> bool:
    """识别无意义短句：仅需简短承接，不进入复杂流程。"""
    s = (text or "").strip().lower().rstrip("。.!！？~～")
    if not s:
        return False
    meaningless = {
        "好", "好的", "嗯", "嗯嗯", "哦", "哦哦", "收到", "知道了", "明白了",
        "ok", "okay", "行", "可以", "好嘞", "好的呢", "在吗", "有人吗", "在不在",
    }
    return s in meaningless


# ========== 数据模型 ==========


class CustomerMessage(BaseModel):
    """客户消息模型"""
    msg_id: str
    acc_id: str
    msg: str
    from_id: str
    from_name: str
    cy_id: str
    acc_type: str
    msg_type: int
    cy_name: str
    goods_name: Optional[str] = None
    goods_order: Optional[str] = None


class ConversationState(BaseModel):
    """对话状态"""
    customer_id: str
    stage: str = "售前"  # 售前/售后
    last_price: Optional[int] = None  # 最后报价
    last_min_price: Optional[int] = None  # 最近图片的最低价
    last_order_id: Optional[str] = None  # 订单号
    order_status: Optional[str] = None  # 订单状态
    discount_count: int = 0  # 让价次数
    image_count: int = 0  # 图片数量
    pending_image_urls: List[str] = Field(default_factory=list)  # 待统一报价图片
    pending_requirements: List[str] = Field(default_factory=list)  # 待统一报价需求
    quote_phase: str = "idle"  # idle/collecting/ready_to_quote/waiting_result
    quote_ready_turns: int = 0  # ready_to_quote 阶段还需等待的消息轮次
    last_update: str = ""
    last_reply_at: Optional[datetime] = None   # 最后一次回复客户的时间


class AgentDeps(BaseModel):
    """Agent 依赖项 - 用于传递上下文"""
    msg_id: str
    acc_id: str
    from_id: str
    platform: str


class AgentResponse(BaseModel):
    """Agent 回复模型"""
    reply: str
    should_reply: bool = True
    need_transfer: bool = False  # 是否需要转人工
    transfer_msg: str = ""  # 转接消息

    @model_validator(mode="after")
    def _ensure_reply_tail(self):
        # 统一在 process_message 中按客户输入决定是否补口语尾词
        return self


def _get_shop_type(acc_id: str = "", goods_name: str = "") -> str:
    """根据 acc_id 或 goods_name 判断店铺类型，返回 gemini_api / find_image / default"""
    try:
        from config.config import CONFIG_DIR
        import json
        cfg_path = CONFIG_DIR / "shop_prompts.json"
        if not cfg_path.exists():
            return "find_image"
        with open(cfg_path, "r", encoding="utf-8") as f:
            cfg = json.load(f)
        shops = cfg.get("shops", {})
        goods_kw = cfg.get("goods_keywords", {})
        type_hints = cfg.get("type_hints", {})
        # 优先按 acc_id
        if acc_id and acc_id in shops:
            return shops[acc_id].get("type", "find_image")
        # 按商品名关键词
        goods_lower = (goods_name or "").lower()
        for kw, stype in goods_kw.items():
            if kw in goods_lower:
                return stype
    except Exception:
        pass
    return "find_image"


def _get_shop_persona(acc_id: str = "", goods_name: str = "") -> str:
    """按店铺返回人设描述，优先级：shops.persona > type_personas > default_persona。"""
    default_persona = "淘宝老店主，说话自然，像真人微信聊天，不官腔、不背模板。"
    try:
        from config.config import CONFIG_DIR
        import json

        cfg_path = CONFIG_DIR / "shop_prompts.json"
        if not cfg_path.exists():
            return default_persona
        with open(cfg_path, "r", encoding="utf-8") as f:
            cfg = json.load(f)

        shops = cfg.get("shops", {})
        if acc_id and acc_id in shops:
            persona = str(shops[acc_id].get("persona", "")).strip()
            if persona:
                return persona

        shop_type = _get_shop_type(acc_id, goods_name)
        type_personas = cfg.get("type_personas", {})
        persona = str(type_personas.get(shop_type, "")).strip()
        if persona:
            return persona

        cfg_default = str(cfg.get("default_persona", "")).strip()
        return cfg_default or default_persona
    except Exception:
        return default_persona


def load_skill_map(skills_dir: str = "skills") -> Dict[str, str]:
    """按技能目录名加载 SKILL.md，返回 {skill_name: content}。"""
    skill_map: Dict[str, str] = {}
    skill_files = glob.glob(os.path.join(skills_dir, "**/SKILL.md"), recursive=True)
    for skill_file in skill_files:
        try:
            content = Path(skill_file).read_text(encoding="utf-8")
            skill_name = Path(skill_file).parent.name.strip().lower()
            if not skill_name:
                continue
            if skill_name in skill_map:
                skill_map[skill_name] += "\n\n" + content
            else:
                skill_map[skill_name] = content
        except Exception as e:
            logger.warning("读取技能文件失败: %s | err=%s", skill_file, e)
    return skill_map


class CustomerServiceAgent:
    """客服 Agent - 支持 SKILL.md + 工作流"""
    C_RESET = "\033[0m"
    C_PROMPT = "\033[96m"      # cyan
    C_THINK = "\033[95m"       # magenta
    C_TOOL = "\033[93m"        # yellow
    C_REPLY = "\033[92m"       # green
    C_MUTED = "\033[90m"       # gray
    _DEFAULT_EVOLUTION_CANDIDATE = Path("config") / "evolution_candidate.json"

    @staticmethod
    def _activity_log(event: str, **kwargs):
        emit_activity(
            logger,
            event=event,
            trace_id=str(kwargs.pop("trace_id", "")),
            customer_id=str(kwargs.pop("customer_id", "")),
            result=str(kwargs.pop("result", "ok")),
            **kwargs,
        )

    def __init__(self, skills_dir: str = "skills"):
        self.api_key = os.getenv("OPENAI_API_KEY")
        self.base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
        self.model_name = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
        self.reply_persona = os.getenv("AI_REPLY_PERSONA", "淘宝老店主，直爽利落，口语自然")
        self.dynamic_collection_replies = os.getenv("AI_DYNAMIC_COLLECTION_REPLIES", "true").strip().lower() in {"1", "true", "yes", "on"}
        self.rewrite_all_replies = os.getenv("AI_REWRITE_ALL_REPLIES", "true").strip().lower() in {"1", "true", "yes", "on"}
        try:
            self.batch_quote_delay_turns = max(0, int(os.getenv("BATCH_QUOTE_DELAY_TURNS", "1")))
        except Exception:
            self.batch_quote_delay_turns = 1
        self.quote_state_machine = QuoteStateMachine(delay_turns=self.batch_quote_delay_turns)
        self.risk_service = RiskService()
        self.pre_rule_service = AgentPreRuleService(self, self.risk_service)

        if not self.api_key:
            raise ValueError("请设置 OPENAI_API_KEY 环境变量")

        # 对话状态管理
        self.conversations: Dict[str, ConversationState] = {}
        self.ConversationStateClass = ConversationState
        # 多轮对话历史（PydanticAI ModelMessage 列表，按客户ID存储）
        self.message_histories: Dict[str, list] = {}
        self.evolution_candidate = self._load_evolution_candidate()

        # 加载技能并按角色拆分，避免所有 Agent 吃同一份大杂烩提示词
        self.skill_map = load_skill_map(skills_dir)
        self.skill_style = self._compose_skill_content(["style-skill", "owner-style"])
        self.skill_pre_sales = self._compose_skill_content(["pre-sales-skill"])
        self.skill_pricing = self._compose_skill_content(["pricing-skill"])
        self.skill_after_sale = self._compose_skill_content(["after-sales-skill"])
        self.skill_risk = self._compose_skill_content(["risk-skill"])

        # 创建 OpenAI 模型
        model = OpenAIChatModel(
            model_name=self.model_name,
            provider=OpenAIProvider(
                api_key=self.api_key,
                base_url=self.base_url
            )
        )

        try:
            from config.config import MIN_PRICE_FLOOR

            min_price_floor = MIN_PRICE_FLOOR
        except Exception:
            min_price_floor = 15

        self.agent = Agent(
            model=model,
            deps_type=AgentDeps,
            system_prompt=build_system_prompt(self.reply_persona, self.skill_pre_sales, self.skill_style),
        )
        self.agent_after_sale = Agent(
            model=model,
            deps_type=AgentDeps,
            system_prompt=build_after_sale_prompt(self.skill_after_sale, self.skill_style),
        )
        self.agent_pricing = Agent(
            model=model,
            deps_type=AgentDeps,
            system_prompt=build_pricing_prompt(
                min_price_floor=min_price_floor,
                case_library_link=CASE_LIBRARY_LINK,
                skill_pricing=self.skill_pricing,
                skill_style=self.skill_style,
            ),
        )
        self.agent_processing = Agent(
            model=model,
            deps_type=AgentDeps,
            system_prompt=build_processing_prompt(self.skill_after_sale, self.skill_style),
        )
        self.agent_similar = Agent(
            model=model,
            deps_type=AgentDeps,
            system_prompt=build_similar_prompt(self.skill_pre_sales, self.skill_style),
        )
        self.agent_natural_reply = Agent(
            model=model,
            deps_type=AgentDeps,
            system_prompt=build_natural_reply_prompt(self.reply_persona, self.skill_style),
        )
        # 工作流程路由器
        self.workflow_router = get_workflow_router()

        self.agent_order = Agent(
            model=model,
            deps_type=AgentDeps,
            system_prompt=build_order_prompt(self.skill_after_sale, self.skill_style),
        )
        self.agent_risk = Agent(
            model=model,
            deps_type=AgentDeps,
            system_prompt=build_risk_prompt(self.skill_risk, self.skill_style),
        )

        # 注册工具
        self._register_tools()

    def _compose_skill_content(self, names: List[str]) -> str:
        """按技能名拼接技能文本，找不到则跳过。"""
        parts: List[str] = []
        for name in names:
            key = (name or "").strip().lower()
            if key and key in self.skill_map:
                parts.append(self.skill_map[key])
        return "\n\n".join(parts)

    def _load_evolution_candidate(self) -> Dict[str, Any]:
        """读取自我进化候选配置（灰度策略），读取失败时返回空。"""
        try:
            path = Path(os.getenv("EVOLUTION_CANDIDATE_PATH", str(self._DEFAULT_EVOLUTION_CANDIDATE)))
            if not path.exists():
                return {}
            data = json.loads(path.read_text(encoding="utf-8"))
            if not isinstance(data, dict):
                return {}
            return data
        except Exception:
            return {}

    def _evolution_gray_percent(self) -> int:
        """灰度比例，默认 5%。"""
        try:
            env_pct = os.getenv("EVOLUTION_GRAY_PERCENT", "").strip()
            if env_pct:
                pct = int(float(env_pct))
            else:
                pct = int(((self.evolution_candidate or {}).get("gray_percent", 5)))
            return max(0, min(100, pct))
        except Exception:
            return 5

    def _evolution_enabled_for_customer(self, customer_id: str) -> bool:
        """按客户哈希稳定灰度命中，命中后启用候选策略。"""
        cand = self.evolution_candidate or {}
        if str(cand.get("status", "")).strip() != "ready_for_gray_5_percent":
            return False
        if not customer_id:
            return False
        pct = self._evolution_gray_percent()
        if pct <= 0:
            return False
        digest = hashlib.md5(customer_id.encode("utf-8")).hexdigest()
        bucket = int(digest[:8], 16) % 100
        hit = bucket < pct
        if hit:
            metrics_emit("evolution_gray_hit", customer_id=customer_id, percent=pct, version=str(cand.get("version", "")))
        return hit

    def _evolution_has_proposal(self, proposal_id: str) -> bool:
        cand = self.evolution_candidate or {}
        for p in cand.get("proposals", []) or []:
            if str((p or {}).get("id", "")).strip() == proposal_id:
                return True
        return False

    @staticmethod
    def _is_service_risk_inquiry(text: str) -> bool:
        """识别退款/投诉等服务风险场景。"""
        s = (text or "").strip().lower()
        if not s:
            return False
        kw = ("退款", "退货", "投诉", "差评", "举报", "欺骗", "骗人", "起诉", "法院", "生气", "不满意")
        return any(k in s for k in kw)

    @staticmethod
    def _log_block(title: str, content: str):
        """统一的控制台分层日志输出。"""
        logger.info("[%s]\n%s\n--------------------", title, content)

    @staticmethod
    def _normalize_reply_text(text: Optional[str]) -> str:
        """清洗模型输出，避免把占位词直接发给客户。"""
        if text is None:
            return ""
        cleaned = str(text).strip()
        if cleaned.lower() in {"无", "none", "null", "n/a"}:
            return ""
        return cleaned

    @staticmethod
    def _colloquialize_reply(text: str) -> str:
        """把常见机械表达柔化为更口语的客服话术。"""
        t = (text or "").strip()
        if not t:
            return t
        repl = {
            "确认我就安排": "你点头我就开做",
            "可以的话我马上安排": "可以我就马上给你做",
            "我这边马上安排": "我马上安排",
            "立刻统一报价": "马上给你报价",
            "统一报价": "一起给你报价",
            "您": "你",
            "请您": "你",
            "可选：A": "可选：",
            "流程完成": "已经安排好了",
        }
        for k, v in repl.items():
            t = t.replace(k, v)
        return t

    async def _render_collection_reply_with_ai(
        self,
        *,
        message: CustomerMessage,
        state: ConversationState,
        scene: str,
        intent_hint: str,
        fallback: str,
    ) -> str:
        """
        收图阶段回复默认走 AI 改写，失败时回退到固定模板。
        """
        first_image_ack = "收到，我先看一下哈，稍等哈。"
        if scene == "collect_ack" and len(state.pending_image_urls) == 1:
            fallback = first_image_ack
        if not self.dynamic_collection_replies:
            return fallback
        try:
            deps = AgentDeps(
                msg_id=message.msg_id,
                acc_id=message.acc_id,
                from_id=message.from_id,
                platform=message.acc_type,
            )
            history = self.message_histories.get(message.from_id, [])
            pending_req = "；".join((state.pending_requirements or [])[-4:]) or "无"
            persona = _get_shop_persona(message.acc_id or "", message.goods_name or "")
            user_prompt = (
                "请按下面意图生成给客户的自然回复。\n"
                f"场景: {scene}\n"
                f"店铺人设: {persona}\n"
                f"回复意图: {intent_hint}\n"
                f"客户原话: {message.msg}\n"
                f"当前已收图片数: {len(state.pending_image_urls)}\n"
                f"当前需求摘要: {pending_req}\n"
                "输出要求: 不超过2句话，像真人店主聊天；避免复用固定模板句。"
            )
            result = await self.agent_natural_reply.run(user_prompt, deps=deps, message_history=history)
            self.message_histories[message.from_id] = result.all_messages()[-30:]
            text = self._colloquialize_reply(self._normalize_reply_text(result.output))
            if not text:
                return fallback
            transfer_keywords = ("TRANSFER_REQUESTED", "[转移会话]", "转移会话")
            if any(k in text for k in transfer_keywords):
                return fallback
            return text
        except Exception:
            return fallback

    async def _rewrite_reply_with_ai(
        self,
        *,
        message: CustomerMessage,
        state: ConversationState,
        reply: str,
        scene: str = "final_reply",
    ) -> str:
        """
        对最终回复做 AI 润色，统一口吻。失败时返回原文。
        """
        text = (reply or "").strip()
        if not text or not self.rewrite_all_replies:
            return text
        transfer_keywords = ("TRANSFER_REQUESTED", "[转移会话]", "转移会话")
        if any(k in text for k in transfer_keywords):
            return text
        try:
            deps = AgentDeps(
                msg_id=message.msg_id,
                acc_id=message.acc_id,
                from_id=message.from_id,
                platform=message.acc_type,
            )
            history = self.message_histories.get(message.from_id, [])
            pending_req = "；".join((state.pending_requirements or [])[-4:]) or "无"
            persona = _get_shop_persona(message.acc_id or "", message.goods_name or "")
            prompt = (
                "请把下面这句客服回复润色成更自然的微信聊天口吻，语义必须保持一致。\n"
                f"场景: {scene}\n"
                f"店铺人设: {persona}\n"
                f"客户原话: {message.msg}\n"
                f"当前已收图: {len(state.pending_image_urls)}张\n"
                f"当前需求摘要: {pending_req}\n"
                f"原回复: {text}\n"
                "要求: 不要新增承诺/价格/流程；不超过2句话；只输出润色后的最终回复。"
            )
            result = await self.agent_natural_reply.run(prompt, deps=deps, message_history=history)
            self.message_histories[message.from_id] = result.all_messages()[-30:]
            polished = self._colloquialize_reply(self._normalize_reply_text(result.output))
            if not polished:
                return text
            if any(k in polished for k in transfer_keywords):
                return text
            return polished
        except Exception:
            return text

    def _register_tools(self):
        """注册所有 Tool，让 Agent 可以主动调用"""
        from core.agent_tools import register_tools
        register_tools(self)

    # 对话状态超过多少小时后重置（避免昨天的售后状态影响今天）
    CONVERSATION_TIMEOUT_HOURS = 12

    def _get_conversation_state(self, customer_id: str) -> ConversationState:
        return load_conversation_state(self, customer_id)

    def _cleanup_inactive(self, now: datetime):
        state_cleanup_inactive(self.conversations, self.message_histories, now)

    def _sync_pending_quote_state(self, customer_id: str, state: ConversationState):
        state_sync_pending_quote_state(self, customer_id, state)

    def _restore_pending_quote_state(self, customer_id: str, state: ConversationState):
        state_restore_pending_quote_state(customer_id, state)

    @staticmethod
    def _refresh_quote_phase(state: ConversationState, phase_hint: str = ""):
        state_refresh_quote_phase(state, phase_hint=phase_hint)

    def _should_defer_batch_quote(self, state: ConversationState, mark_ready: bool = False) -> bool:
        return state_should_defer_batch_quote(self, state, mark_ready=mark_ready)

    def _mark_quote_ready(self, state: ConversationState):
        state_mark_quote_ready(self, state)

    def _build_reject_message(self, reason: str = "") -> str:
        templates = [
            "这类图文字内容太密了，我们这边不接这单哈，建议精简后再发我看看。",
            "这种密集文字/宣传栏类图片暂时做不了，抱歉啦，换一版简化内容我可以继续帮你看。",
            "这张文字信息太多，处理风险高，我们先不接，您可以先筛重点文字再发我。",
        ]
        msg = random.choice(templates)
        if reason:
            msg += f"（{reason}）"
        return msg

    def _is_batch_quote_enabled(self, customer_id: str, acc_id: str) -> bool:
        """灰度开关：按店铺白名单 + 客户哈希百分比控制新策略是否生效。"""
        try:
            from config.config import (
                FEATURE_BATCH_QUOTE_ENABLED,
                FEATURE_BATCH_QUOTE_PERCENT,
                FEATURE_BATCH_QUOTE_SHOPS,
            )
            if not FEATURE_BATCH_QUOTE_ENABLED:
                return False
            pct = max(0, min(100, int(FEATURE_BATCH_QUOTE_PERCENT)))
            if pct == 0:
                return False
            shops = [s.strip() for s in (FEATURE_BATCH_QUOTE_SHOPS or "").split(",") if s.strip()]
            if shops and (acc_id or "") not in shops:
                return False
            if pct >= 100:
                return True
            h = int(hashlib.md5((customer_id or "").encode("utf-8")).hexdigest()[:8], 16) % 100
            return h < pct
        except Exception:
            return True

    def _detect_stage(self, message: str) -> str:
        """检测售前/售后"""
        # 系统订单通知不属于售后，单独处理
        if "系统订单信息" in message:
            return "订单通知"

        after_sale_keywords = ["已下单", "已付款", "催一下", "发文件", "要修改", "不满意", "退款", "退货"]
        for keyword in after_sale_keywords:
            if keyword in message:
                return "售后"
        return "售前"

    _is_political_inquiry = staticmethod(is_political_inquiry)
    _is_map_inquiry = staticmethod(is_map_inquiry)
    _get_shop_type = staticmethod(_get_shop_type)
    _get_shop_persona = staticmethod(_get_shop_persona)
    _notify_wechat = staticmethod(_notify_wechat)
    _notify_wechat_overdue = staticmethod(_notify_wechat_overdue)

    _calc_avg_complexity = staticmethod(calc_avg_complexity)
    _get_conversation_context = staticmethod(get_conversation_context)
    _get_intent_emotion_hint = staticmethod(get_intent_emotion_hint)

    def _get_customer_profile_context(self, customer_id: str) -> str:
        return get_customer_profile_context(self, customer_id)

    def _get_refusal_context_hint(self, customer_id: str, current_msg: str, profile_context: str) -> str:
        return get_refusal_context_hint(self, customer_id, current_msg, profile_context)

    # 简单打招呼类消息（在近期已回复后无需再回）
    _COOLDOWN_PATTERNS = [
        "你好", "您好", "在吗", "在么", "在不在", "有人吗",
        "嗯", "嗯嗯", "好", "好的", "好哒", "ok", "OK", "okay",
        "谢谢", "谢谢你", "感谢", "收到", "知道了", "明白了",
    ]
    _COOLDOWN_SECONDS = 5 * 60   # 5 分钟内不重复回复纯打招呼

    def _in_cooldown(self, state: ConversationState, msg: str) -> bool:
        """最近刚回复过 + 消息是纯打招呼 → True 静默"""
        if not state.last_reply_at:
            return False
        elapsed = (datetime.now() - state.last_reply_at).total_seconds()
        if elapsed > self._COOLDOWN_SECONDS:
            return False
        clean = msg.strip().rstrip("！!？?。.～~")
        return clean in self._COOLDOWN_PATTERNS

    def _should_handle_as_meaningless_short_text(self, state: ConversationState, msg: str) -> bool:
        """
        无意义短句仅在“非业务处理中”生效，避免误拦截真实推进消息。
        例如：已在收图/待报价阶段时，客户发“好的/在吗”不应直接 ping。
        """
        customer_text, _ = self._split_customer_text(msg or "")
        text = (customer_text or "").strip()
        if not _is_meaningless_short_text(text):
            return False
        if self._extract_image_urls(text):
            return False
        if (getattr(state, "pending_image_urls", None) or []):
            return False
        if getattr(state, "quote_phase", "idle") in {"collecting", "ready_to_quote", "waiting_result"}:
            return False
        return True

    async def build_auto_quote_reply(self, state: ConversationState, message: CustomerMessage) -> AgentResponse:
        """
        自动报价内部入口：不走 process_message，避免伪造客户语句污染上下文。
        """
        quote_res = await self._quote_pending_images(state, message)
        reply_text = self._colloquialize_reply(quote_res.get("reply", ""))
        reply_text = await self._rewrite_reply_with_ai(
            message=message,
            state=state,
            reply=reply_text,
            scene="batch_quote_reply",
        )
        need_transfer = bool(quote_res.get("need_transfer"))
        state.last_reply_at = datetime.now()
        return AgentResponse(
            reply=reply_text,
            should_reply=not need_transfer,
            need_transfer=need_transfer,
            transfer_msg=TRANSFER_MESSAGE if need_transfer else "",
        )

    async def process_message(self, message: CustomerMessage) -> AgentResponse:
        """处理客户消息并生成回复。"""
        return await process_incoming_message(self, message)

    async def _check_order_amount(self, customer_id: str, order: dict, acc_id: str):
        """核查订单实付金额是否与报价一致，异常时企业微信预警"""
        try:
            import re
            from db.customer_db import db
            profile = db.get_customer(customer_id)
            quoted = profile.last_price  # 上次报价（元）
            if not quoted:
                return

            # 从订单解析实付金额
            raw_amount = order.get("amount", "")
            m = re.search(r'[\d.]+', str(raw_amount))
            if not m:
                return
            paid = float(m.group())

            logger.info("[Agent] 订单金额核查：报价 %s元 vs 实付 %s元（客户 %s）", quoted, paid, customer_id)

            # 实付金额明显低于报价（低于报价的 60%）才预警
            if paid < quoted * 0.6:
                msg = (
                    f"⚠️ **订单金额异常**\n"
                    f"店铺：{acc_id}\n"
                    f"客户：{customer_id}（{profile.name or ''}）\n"
                    f"报价：{quoted}元\n"
                    f"实付：{paid}元\n"
                    f"差额：{quoted - paid:.1f}元 — 请人工核查"
                )
                logger.warning("[Agent] %s", msg)
                await self._notify_wechat(msg)
        except Exception as e:
            logger.exception("[Agent] 订单金额核查失败: %s", e)

    def _extract_image_url(self, msg: str) -> str:
        """从消息中提取图片URL，兼容纯URL和 text#*#url 两种格式"""
        urls = self._extract_image_urls(msg)
        return urls[0] if urls else ""

    def _extract_image_urls(self, msg: str) -> List[str]:
        """提取消息中的所有图片URL（去重保序）。"""
        import re
        if not msg:
            return []
        image_exts = (".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp")
        image_hosts = ("alicdn.com", "imgextra", "taobao.com", "jd.com", "pinduoduo.com", "suning.com")
        candidates = re.findall(r'https?://[^\s#]+', msg)
        urls: List[str] = []
        for u in candidates:
            low = u.lower()
            if any(ext in low for ext in image_exts) or any(h in low for h in image_hosts):
                if u not in urls:
                    urls.append(u)
        return urls

    def _strip_urls_from_text(self, msg: str) -> str:
        """去掉 URL 后的纯文本，用于提取额外需求。"""
        import re
        if not msg:
            return ""
        text = re.sub(r'https?://\S+', ' ', msg)
        text = text.replace("#*#", " ").strip()
        text = re.sub(r'\s+', ' ', text)
        return text.strip("，,。.!！？?；;：: ")

    _is_batch_finish_signal = staticmethod(is_batch_finish_signal)
    _is_batch_finish_intent = staticmethod(is_batch_finish_intent)
    _is_cross_image_composite_intent = staticmethod(is_cross_image_composite_intent)
    _is_related_image_followup_intent = staticmethod(is_related_image_followup_intent)
    _is_result_followup_query = staticmethod(is_result_followup_query)
    _classify_short_customer_text = staticmethod(classify_short_customer_text)
    _build_collect_ack = staticmethod(build_collect_ack)
    _build_collect_progress_reply = staticmethod(build_collect_progress_reply)
    _build_collect_remind = staticmethod(build_collect_remind)
    _is_find_image_not_edit_conflict = staticmethod(is_find_image_not_edit_conflict)
    _needs_clarification_in_collecting = staticmethod(needs_clarification_in_collecting)
    _build_find_image_clarify_reply = staticmethod(build_find_image_clarify_reply)
    _build_not_understood_reply = staticmethod(build_not_understood_reply)
    _append_requirement = staticmethod(append_requirement)

    async def _run_batch_feasibility(self, urls: List[str], concurrency: int) -> List[Tuple[str, Dict[str, Any]]]:
        """Stage 2: 可做性分析（逐图）。"""
        from image.image_analyzer import image_analyzer

        sem = asyncio.Semaphore(max(1, concurrency))

        async def _analyze_one(url: str):
            async with sem:
                try:
                    r = await image_analyzer.analyze(url)
                except Exception:
                    r = {
                        "complexity": "normal",
                        "reason": "识别异常，按常规估价",
                        "price_min": 15,
                        "price_max": 25,
                        "price_suggest": 20,
                        "success": False,
                    }
                return url, r

        return list(await asyncio.gather(*[_analyze_one(u) for u in urls]))

    async def _sync_batch_analysis_to_workflow(self, results: List[Tuple[str, Dict[str, Any]]], message: CustomerMessage) -> None:
        for url, r in results:
            try:
                from core.workflow import workflow
                await workflow.image_analysis_result(
                    customer_id=message.from_id,
                    image_url=url,
                    complexity=r.get("complexity", "normal"),
                    acc_id=message.acc_id,
                    acc_type=message.acc_type,
                    gemini_prompt=r.get("gemini_prompt", ""),
                    aspect_ratio=r.get("aspect_ratio", "1:1"),
                    perspective=r.get("perspective", "no"),
                    proc_type=r.get("proc_type", ""),
                    subject=r.get("subject", ""),
                    quality=r.get("quality", ""),
                )
            except Exception as e:
                logger.exception("[Agent] Workflow 批量任务创建失败: %s", e)

    _calc_requirement_surcharge = staticmethod(calc_requirement_surcharge)
    _build_batch_quote_reply = staticmethod(build_batch_quote_reply)
    _prepare_batch_intake = staticmethod(prepare_batch_intake)
    _assess_batch_risk = staticmethod(assess_batch_risk)
    _build_batch_pricing_plan = staticmethod(build_batch_pricing_plan)

    async def _try_batch_auto_process(
        self,
        results: List[Tuple[str, Dict[str, Any]]],
        message: CustomerMessage,
        req_fee: Dict[str, Any],
    ) -> Dict[str, Any]:
        """Stage 4-A: 自动处理+图绘链接。失败时回退到需求澄清。"""
        links = []
        try:
            from image.image_processor import image_processor
            from utils.image_queue import run_with_queue
            for idx, (url, r) in enumerate(results, 1):
                req_parts = [f"complexity:{r.get('complexity', 'normal')}"]
                if r.get("gemini_prompt"):
                    req_parts.append(f"prompt:{r.get('gemini_prompt')}")
                if r.get("aspect_ratio"):
                    req_parts.append(f"ratio:{r.get('aspect_ratio')}")
                if r.get("perspective") and r.get("perspective") != "no":
                    req_parts.append(f"perspective:{r.get('perspective')}")
                if r.get("proc_type"):
                    req_parts.append(f"proc_type:{r.get('proc_type')}")
                if r.get("subject"):
                    req_parts.append(f"subject:{r.get('subject')}")
                if r.get("quality"):
                    req_parts.append(f"quality:{r.get('quality')}")

                process_res = await run_with_queue(image_processor.process_image(
                    url,
                    "enhance",
                    requirements="|".join(req_parts),
                    gemini_prompt=r.get("gemini_prompt", ""),
                    aspect_ratio=r.get("aspect_ratio", "1:1"),
                    perspective=r.get("perspective", "no"),
                    proc_type=r.get("proc_type", ""),
                    subject=r.get("subject", ""),
                    quality=r.get("quality", ""),
                ))
                if not process_res.get("success"):
                    raise RuntimeError(process_res.get("message", "图片处理失败"))

                ok, link, _ = await upload_to_tuhui(
                    process_res["result_path"],
                    title=f"客户{message.from_id[-4:]}-图片{idx}",
                    description="AI自动处理结果",
                    price=max(10, int(r.get("price_suggest", 20) or 20) + int(req_fee.get("extra", 0) or 0) // max(1, len(results))),
                )
                if not ok:
                    raise RuntimeError(str(link))
                links.append(link)
        except Exception as e:
            logger.exception("[Agent] 找图自动处理失败，回退需求澄清: %s", e)
            return {
                "reply": "这种可以做类似款。你先说下具体需求：要几张、是否改字、尺寸比例、交付格式（单图/打包链接），我按需求给你直接做。",
                "need_transfer": False,
            }
        lines = ["找到了，链接如下："]
        for i, link in enumerate(links, 1):
            lines.append(f"链接{i}：{link}")
        return {"reply": "\n".join(lines), "need_transfer": False}

    def _finalize_batch_state(self, state: ConversationState, customer_id: str, final_price: int = 0):
        if final_price > 0:
            state.last_price = final_price
            try:
                from db.customer_db import db
                db.update_last_price(customer_id, final_price)
            except Exception:
                pass
        state.pending_image_urls.clear()
        state.pending_requirements.clear()
        self._refresh_quote_phase(state, "idle")
        self._sync_pending_quote_state(customer_id, state)

    async def _quote_pending_images(self, state: ConversationState, message: CustomerMessage) -> Dict[str, Any]:
        """
        统一报价主流程（分层）：
        1) Intake 收集
        2) Feasibility 可做性
        3) Pricing 报价
        4) Router 自动处理/报价/转人工
        """
        intake = self._prepare_batch_intake(state)
        if not intake.get("ok", False):
            return {"reply": intake.get("reply", ""), "need_transfer": bool(intake.get("need_transfer", False))}

        urls = intake["urls"]
        requirements = intake["requirements"]
        analyze_concurrency = int(intake["analyze_concurrency"])

        results = await self._run_batch_feasibility(urls=urls, concurrency=analyze_concurrency)
        await self._sync_batch_analysis_to_workflow(results=results, message=message)

        risk = self._assess_batch_risk(results)
        unsafe = risk["unsafe"]
        dense_text_reject = risk["dense_text_reject"]
        if unsafe:
            self._finalize_batch_state(state, message.from_id, final_price=0)
            if dense_text_reject and len(dense_text_reject) == len(unsafe):
                return {"reply": self._build_reject_message("文字密集类图片暂不接单"), "need_transfer": False}
            return {
                "reply": f"这批里{'、'.join(unsafe)}处理风险较高，我先帮你转人工设计师跟进会更稳妥。",
                "need_transfer": True,
            }

        pricing = self._build_batch_pricing_plan(results=results, requirements=requirements)
        total_suggest = int(pricing["total_suggest"])
        bundle_price = int(pricing["bundle_price"])
        req_fee = pricing["req_fee"]

        intent_text = (message.msg or "") + " " + " ".join(requirements[-5:])
        workflow_type, _ = self.workflow_router.detect_workflow(intent_text)
        if workflow_type == "find_image":
            route_res = await self._try_batch_auto_process(
                results=results,
                message=message,
                req_fee=req_fee,
            )
            self._finalize_batch_state(state, message.from_id, final_price=bundle_price)
            return route_res

        reply_text = self._build_batch_quote_reply(
            results=results,
            total_suggest=total_suggest,
            bundle_price=bundle_price,
            req_fee=req_fee,
        )
        self._finalize_batch_state(state, message.from_id, final_price=bundle_price)
        return {"reply": reply_text, "need_transfer": False}

    _split_customer_text = staticmethod(split_customer_text)

    def _build_prompt(self, message: CustomerMessage, state: ConversationState) -> str:
        return build_agent_prompt(
            message=message,
            state=state,
            extract_image_url=self._extract_image_url,
            shop_type_resolver=_get_shop_type,
            shop_persona_resolver=_get_shop_persona,
            parse_order_info=parse_order_info,
            build_order_instruction=build_order_instruction,
        )

    async def _handle_image_workflow(self, message: str, data: dict, image_urls: list) -> bool:
        return await route_image_workflow(
            workflow_router=self.workflow_router,
            message=message,
            data=data,
            image_urls=image_urls,
        )


async def test_agent():
    """测试 Agent"""
    agent = CustomerServiceAgent(skills_dir="skills")

    test_msg = CustomerMessage(
        msg_id="123",
        acc_id="test_account",
        msg="这张图可以做吗？",
        from_id="customer001",
        from_name="张三",
        cy_id="customer001",
        acc_type="AliWorkbench",
        msg_type=0,
        cy_name="张三",
        goods_name="专业找图代找高清图片",
        goods_order=""
    )

    response = await agent.process_message(test_msg)
    logger.info("回复内容: %s", response.reply)


if __name__ == "__main__":
    import asyncio
    asyncio.run(test_agent())
