# =============================================================================
# 企微IT智能服务台 — 管理后台业务逻辑层
# =============================================================================
# 说明：管理后台的核心业务逻辑，包括：
#   1. 仪表盘数据聚合
#   2. 配置项分组读取与更新（含变更日志）
#   3. 坐席 CRUD 管理
#   4. 外部系统集成配置
#   5. 快速回复审核
#   6. 分配模式管理
#   7. 会话监控
#   8. 全局搜索
# =============================================================================

import json
import logging
from datetime import datetime, date, timedelta
from typing import Any, Dict, List, Optional

from sqlalchemy import func, select, or_, and_, case, literal_column
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.agent import Agent
from app.models.config_change_log import ConfigChangeLog
from app.models.conversation import Conversation
from app.models.message import Message
from app.models.quick_reply_template import QuickReplyTemplate
from app.models.system_config import SystemConfig
from app.schemas.admin import (
    AdminAgentResponse,
    AdminQuickReplyResponse,
    AssignmentModeItem,
    AssignmentModeResponse,
    ConfigGroupResponse,
    ConfigHistoryItem,
    ConfigItemResponse,
    DashboardOverviewResponse,
    IntegrationConfig,
    IntegrationHealthItem,
    IntegrationResponse,
    MonitorSessionsResponse,
    SearchItem,
    SessionItem,
    SessionStats,
    SystemAlertItem,
)
from app.utils.response import AppException, ERR_NOT_FOUND, ERR_PARAMS

logger = logging.getLogger(__name__)


# --------------------------------------------------------------------------
# 配置分组映射（前缀 → 分组名称）
# --------------------------------------------------------------------------
CONFIG_GROUP_MAP: Dict[str, str] = {
    "ai_": "AI 对话引擎",
    "emergency_": "应急模式",
    "assign_": "消息分配",
    "polling_": "轮询配置",
    "emotion_": "情绪检测",
    "integration_": "外部集成",
    "queue_": "排队策略",
    "satisfaction_": "满意度评价",
    "invite_": "邀请功能",
    "notification_": "通知推送",
    "security_": "安全策略",
}


# --------------------------------------------------------------------------
# 集成系统定义（硬编码，阶段一不增加表）
# --------------------------------------------------------------------------
INTEGRATION_DEFINITIONS = [
    {
        "id": "dify",
        "name": "Dify AI",
        "key_prefix": "integration_dify_",
        "configurable": True,
        "config_type": "url_key",          # api_url + api_key
    },
    {
        "id": "ragflow",
        "name": "RAGFlow",
        "key_prefix": "integration_ragflow_",
        "configurable": True,
        "config_type": "url_key",          # api_url + api_key
    },
    {
        "id": "huorong",
        "name": "火绒安全",
        "key_prefix": "integration_huorong_",
        "configurable": True,
        "config_type": "access_key",      # access_key_id + access_key_secret + base_url
    },
    {
        "id": "lianruan",
        "name": "联软LV7000",
        "key_prefix": "integration_lianruan_",
        "configurable": True,
        "config_type": "account_password",  # api_account + api_password + base_url + validate_key
    },
    {
        "id": "data_platform",
        "name": "数据平台",
        "key_prefix": None,
        "configurable": False,
        "config_type": None,
    },
    {
        "id": "beisen",
        "name": "北森 eHR",
        "key_prefix": None,
        "configurable": False,
        "config_type": None,
    },
]


# --------------------------------------------------------------------------
# 分配模式定义（硬编码，阶段一仅手动接单可用）
# --------------------------------------------------------------------------
ASSIGNMENT_MODES = [
    {"id": "manual", "name": "手动接单", "locked": False, "unlock_at": ""},
    {"id": "round_robin", "name": "轮询分配", "locked": True, "unlock_at": "阶段二"},
    {"id": "least_active", "name": "最少活跃优先", "locked": True, "unlock_at": "阶段二"},
    {"id": "weighted", "name": "加权比例分配", "locked": True, "unlock_at": "阶段三"},
    {"id": "skill_match", "name": "技能匹配分配", "locked": True, "unlock_at": "阶段三"},
    {"id": "priority_queue", "name": "优先队列", "locked": True, "unlock_at": "阶段三"},
]


# ==========================================================================
# 仪表盘
# ==========================================================================

async def get_dashboard_overview(db: AsyncSession) -> DashboardOverviewResponse:
    """获取仪表盘统计数据。

    聚合查询在线坐席数、今日会话数、待审核数、集成健康状态等。

    Args:
        db: 数据库会话

    Returns:
        DashboardOverviewResponse: 仪表盘统计数据
    """
    # 在线坐席数
    online_count_result = await db.execute(
        select(func.count(Agent.id)).where(Agent.status == "online")
    )
    online_agents = online_count_result.scalar() or 0

    # 今日会话数（今天创建的所有会话）
    today_start = datetime.combine(date.today(), datetime.min.time())
    today_conv_result = await db.execute(
        select(func.count(Conversation.id)).where(
            Conversation.created_at >= today_start
        )
    )
    today_conversations = today_conv_result.scalar() or 0

    # 待审核快速回复数
    pending_result = await db.execute(
        select(func.count(QuickReplyTemplate.id)).where(
            QuickReplyTemplate.status == "pending_review"
        )
    )
    pending_reviews = pending_result.scalar() or 0

    # 系统告警 — 阶段一仅基于待审核快速回复生成告警，后续阶段接入更多告警源
    system_alerts: List[SystemAlertItem] = []
    if pending_reviews > 0:
        # 查询待审核模板，用于告警详情
        pending_templates_result = await db.execute(
            select(QuickReplyTemplate)
            .where(QuickReplyTemplate.status == "pending_review")
            .order_by(QuickReplyTemplate.updated_at.desc())
            .limit(5)  # 最多展示5条告警
        )
        pending_templates = list(pending_templates_result.scalars().all())

        for t in pending_templates:
            system_alerts.append(
                SystemAlertItem(
                    type="quick_reply_pending",
                    content=f"快速回复待审核：{t.content[:50]}{'...' if len(t.content) > 50 else ''}",
                    submitter=t.submitted_by or None,
                    time=t.updated_at.isoformat() if t.updated_at else "",
                    severity="warning",
                )
            )

    # 集成系统健康状态（通用检查：按config_type判断连接状态）
    integrations_health: List[IntegrationHealthItem] = []
    for integ_def in INTEGRATION_DEFINITIONS:
        if integ_def["configurable"] and integ_def["key_prefix"]:
            prefix = integ_def["key_prefix"]
            config_type = integ_def.get("config_type", "url_key")
            status = "disconnected"

            if config_type == "url_key":
                # Dify/RAGFlow: 检查 api_url + api_key
                au = await _get_config_value(db, f"{prefix}api_url")
                ak = await _get_config_value(db, f"{prefix}api_key")
                if au and ak:
                    status = "connected"
                elif au:
                    status = "partial"

            elif config_type == "access_key":
                # 火绒: 检查 access_key_id + access_key_secret + base_url
                aki = await _get_config_value(db, f"{prefix}access_key_id")
                aks = await _get_config_value(db, f"{prefix}access_key_secret")
                bu = await _get_config_value(db, f"{prefix}base_url")
                if aki and aks and bu:
                    status = "connected"
                elif bu:
                    status = "partial"

            elif config_type == "account_password":
                # 联软: 检查 api_account + api_password + base_url
                aa = await _get_config_value(db, f"{prefix}api_account")
                ap = await _get_config_value(db, f"{prefix}api_password")
                bu = await _get_config_value(db, f"{prefix}base_url")
                if aa and ap and bu:
                    status = "connected"
                elif bu:
                    status = "partial"

            integrations_health.append(
                IntegrationHealthItem(system=integ_def["name"], status=status)
            )
        else:
            integrations_health.append(
                IntegrationHealthItem(system=integ_def["name"], status="disconnected")
            )

    # 平均响应时间（首次人工回复距首条员工消息的时间差）
    avg_response_time_str = "—"
    try:
        # 取今日已结单或服务中的会话，计算平均首次响应时间
        conv_ids_result = await db.execute(
            select(Conversation.id).where(
                Conversation.created_at >= today_start,
                Conversation.assigned_agent_id.isnot(None),
            )
        )
        conv_ids = [row[0] for row in conv_ids_result.all()]

        if conv_ids:
            response_times = []
            for cid in conv_ids[:50]:  # 最多统计50个会话，避免性能问题
                # 找该会话首条员工消息
                first_emp_msg = await db.execute(
                    select(Message.created_at).where(
                        Message.conversation_id == cid,
                        Message.sender_type == "employee",
                    ).order_by(Message.created_at.asc()).limit(1)
                )
                first_emp_time = first_emp_msg.scalar()

                # 找该会话首条坐席/AI回复
                first_reply = await db.execute(
                    select(Message.created_at).where(
                        Message.conversation_id == cid,
                        Message.sender_type.in_(["agent", "ai"]),
                    ).order_by(Message.created_at.asc()).limit(1)
                )
                first_reply_time = first_reply.scalar()

                if first_emp_time and first_reply_time:
                    delta = (first_reply_time - first_emp_time).total_seconds()
                    if 0 < delta < 3600:  # 合理范围内（1小时内）
                        response_times.append(delta)

            if response_times:
                avg_seconds = sum(response_times) / len(response_times)
                if avg_seconds < 60:
                    avg_response_time_str = f"{avg_seconds:.0f}秒"
                else:
                    avg_response_time_str = f"{avg_seconds / 60:.1f}分钟"
    except Exception as e:
        logger.warning(f"计算平均响应时间失败: {e}")

    # AI 命中率（有AI实质性回复的会话占比）
    ai_hit_rate_str = "—"
    try:
        total_conv_result = await db.execute(
            select(func.count(Conversation.id)).where(
                Conversation.created_at >= today_start
            )
        )
        total_conv = total_conv_result.scalar() or 0

        if total_conv > 0:
            ai_conv_result = await db.execute(
                select(func.count(Conversation.id)).where(
                    Conversation.created_at >= today_start,
                    Conversation.ai_substantive_reply_count > 0,
                )
            )
            ai_conv = ai_conv_result.scalar() or 0
            ai_hit_rate_str = f"{(ai_conv / total_conv) * 100:.0f}%"
    except Exception as e:
        logger.warning(f"计算AI命中率失败: {e}")

    return DashboardOverviewResponse(
        online_agents=online_agents,
        today_conversations=today_conversations,
        avg_response_time=avg_response_time_str,
        ai_hit_rate=ai_hit_rate_str,
        pending_reviews=pending_reviews,
        system_alerts=system_alerts,
        integrations_health=integrations_health,
    )


# ==========================================================================
# 配置管理
# ==========================================================================

async def get_config_groups(db: AsyncSession) -> List[ConfigGroupResponse]:
    """获取全部配置项（按功能分组）。

    从 system_configs 表读取所有配置，按前缀分组返回。

    Args:
        db: 数据库会话

    Returns:
        List[ConfigGroupResponse]: 配置分组列表
    """
    # 查询所有非 integration_ 前缀的配置项
    result = await db.execute(
        select(SystemConfig).order_by(SystemConfig.config_key)
    )
    all_configs = list(result.scalars().all())

    # 按 key 前缀分组（排除 integration_ 前缀）
    groups_dict: Dict[str, List[ConfigItemResponse]] = {}
    other_items: List[ConfigItemResponse] = []

    for cfg in all_configs:
        # 跳过 integration_ 前缀的配置（在集成管理中单独展示）
        if cfg.config_key.startswith("integration_"):
            continue

        # 推断值类型
        value_type = _infer_value_type(cfg.config_value)

        item = ConfigItemResponse(
            key=cfg.config_key,
            value=cfg.config_value,
            description=cfg.description or "",
            value_type=value_type,
        )

        # 查找匹配的前缀分组
        matched = False
        for prefix, group_name in CONFIG_GROUP_MAP.items():
            if cfg.config_key.startswith(prefix):
                if group_name not in groups_dict:
                    groups_dict[group_name] = []
                groups_dict[group_name].append(item)
                matched = True
                break

        if not matched:
            other_items.append(item)

    # 构建分组响应
    groups: List[ConfigGroupResponse] = []
    for prefix, group_name in CONFIG_GROUP_MAP.items():
        if group_name in groups_dict:
            groups.append(
                ConfigGroupResponse(
                    name=group_name,
                    key_prefix=prefix,
                    items=groups_dict[group_name],
                )
            )

    # 未匹配前缀的配置项放入"其他"分组
    if other_items:
        groups.append(
            ConfigGroupResponse(
                name="其他配置",
                key_prefix="",
                items=other_items,
            )
        )

    return groups


def _infer_value_type(value: str) -> str:
    """推断配置值的类型。

    Args:
        value: 配置值字符串

    Returns:
        str: 值类型标识
    """
    if value.lower() in ("true", "false"):
        return "boolean"
    try:
        float(value)
        return "number"
    except (ValueError, TypeError):
        pass
    try:
        parsed = json.loads(value)
        if isinstance(parsed, list):
            return "json_array"
        if isinstance(parsed, dict):
            return "json_object"
    except (json.JSONDecodeError, TypeError):
        pass
    return "string"


async def update_config(
    db: AsyncSession,
    key: str,
    value: str,
    agent_id: str,
) -> Dict[str, Any]:
    """更新单个配置项，并记录变更日志。

    Args:
        db: 数据库会话
        key: 配置键
        value: 新的配置值
        agent_id: 操作人 agent_id

    Returns:
        Dict: 包含 key, old_value, new_value, changed_at

    Raises:
        AppException: 配置项不存在
    """
    # 查找配置项
    result = await db.execute(
        select(SystemConfig).where(SystemConfig.config_key == key)
    )
    config = result.scalars().first()

    if not config:
        raise AppException(1003, f"配置项不存在: {key}")

    old_value = config.config_value

    # 写入变更日志
    change_log = ConfigChangeLog(
        config_key=key,
        old_value=old_value,
        new_value=value,
        changed_by=agent_id,
    )
    db.add(change_log)

    # 更新配置值
    config.config_value = value
    config.updated_at = datetime.now()
    db.add(config)

    logger.info(f"配置更新: key={key}, old={old_value}, new={value}, by={agent_id}")

    return {
        "key": key,
        "old_value": old_value,
        "new_value": value,
        "changed_at": datetime.now().isoformat(),
    }


async def get_config_history(
    db: AsyncSession,
    key: str,
    limit: int = 20,
) -> List[ConfigHistoryItem]:
    """获取指定配置项的变更历史。

    Args:
        db: 数据库会话
        key: 配置键
        limit: 返回条数上限

    Returns:
        List[ConfigHistoryItem]: 变更历史列表
    """
    result = await db.execute(
        select(ConfigChangeLog)
        .where(ConfigChangeLog.config_key == key)
        .order_by(ConfigChangeLog.changed_at.desc())
        .limit(limit)
    )
    logs = list(result.scalars().all())

    # 批量查询操作人姓名
    agent_ids = list({log.changed_by for log in logs})
    agent_names = {}
    if agent_ids:
        agents_result = await db.execute(
            select(Agent.id, Agent.name).where(Agent.id.in_(agent_ids))
        )
        agent_names = {row[0]: row[1] for row in agents_result.all()}

    items = []
    for log in logs:
        items.append(
            ConfigHistoryItem(
                id=log.id,
                config_key=log.config_key,
                old_value=log.old_value,
                new_value=log.new_value,
                changed_by=log.changed_by,
                changed_by_name=agent_names.get(log.changed_by, ""),
                changed_at=log.changed_at,
            )
        )

    return items


# ==========================================================================
# 坐席管理
# ==========================================================================

async def list_admin_agents(
    db: AsyncSession,
    status: Optional[str] = None,
) -> List[AdminAgentResponse]:
    """获取坐席列表（管理视图，含角色/技能标签/今日结单数）。

    Args:
        db: 数据库会话
        status: 按状态筛选（可选）

    Returns:
        List[AdminAgentResponse]: 坐席列表
    """
    stmt = select(Agent).order_by(Agent.name)
    if status:
        stmt = stmt.where(Agent.status == status)

    result = await db.execute(stmt)
    agents = list(result.scalars().all())

    # 批量查询今日结单数
    today_start = datetime.combine(date.today(), datetime.min.time())
    agent_ids = [a.id for a in agents]
    today_resolved_map: Dict[str, int] = {}

    if agent_ids:
        resolved_result = await db.execute(
            select(
                Conversation.assigned_agent_id,
                func.count(Conversation.id),
            )
            .where(
                Conversation.assigned_agent_id.in_(agent_ids),
                Conversation.status == "resolved",
                Conversation.updated_at >= today_start,
            )
            .group_by(Conversation.assigned_agent_id)
        )
        today_resolved_map = dict(resolved_result.all())

    items = []
    for a in agents:
        resp = AdminAgentResponse(
            id=a.id,
            user_id=a.user_id,
            name=a.name,
            status=a.status,
            role=a.role,
            skill_tags=a.skill_tags or [],
            current_load=a.current_load,
            max_load=a.max_load,
            today_resolved=today_resolved_map.get(a.id, 0),
            created_at=a.created_at,
            updated_at=a.updated_at,
        )
        items.append(resp)

    return items


async def create_agent(
    db: AsyncSession,
    user_id: str,
    name: str,
    role: str = "agent",
    skill_tags: Optional[List[str]] = None,
    max_load: int = 5,
) -> AdminAgentResponse:
    """创建坐席。

    Args:
        db: 数据库会话
        user_id: 企微用户ID
        name: 坐席姓名
        role: 角色（仅允许 admin / agent）
        skill_tags: 技能标签列表
        max_load: 最大同时服务数

    Returns:
        AdminAgentResponse: 创建的坐席信息

    Raises:
        AppException: user_id 已存在 或 role 值非法
    """
    # 校验 role 白名单，防止非法角色值入库
    if role not in ("admin", "agent"):
        raise AppException(1001, f"角色值非法: {role}，仅允许 admin 或 agent")

    # 检查 user_id 是否已存在
    existing = await db.execute(
        select(Agent).where(Agent.user_id == user_id)
    )
    if existing.scalars().first():
        raise AppException(1001, f"坐席 user_id 已存在: {user_id}")

    agent = Agent(
        user_id=user_id,
        name=name,
        role=role,
        skill_tags=skill_tags or [],
        max_load=max_load,
        status="offline",
        current_load=0,
    )
    db.add(agent)
    await db.flush()

    logger.info(f"创建坐席: user_id={user_id}, name={name}, role={role}")

    return AdminAgentResponse(
        id=agent.id,
        user_id=agent.user_id,
        name=agent.name,
        status=agent.status,
        role=agent.role,
        skill_tags=agent.skill_tags or [],
        current_load=agent.current_load,
        max_load=agent.max_load,
        today_resolved=0,
        created_at=agent.created_at,
        updated_at=agent.updated_at,
    )


async def update_agent(
    db: AsyncSession,
    agent_id: str,
    role: Optional[str] = None,
    skill_tags: Optional[List[str]] = None,
    max_load: Optional[int] = None,
) -> Dict[str, Any]:
    """更新坐席信息（角色/技能标签/负载上限）。

    Args:
        db: 数据库会话
        agent_id: 坐席ID
        role: 角色（可选）
        skill_tags: 技能标签列表（可选）
        max_load: 最大同时服务数（可选）

    Returns:
        Dict: 更新后的坐席关键字段

    Raises:
        AppException: 坐席不存在
    """
    result = await db.execute(
        select(Agent).where(Agent.id == agent_id)
    )
    agent = result.scalars().first()

    if not agent:
        raise AppException(3004, "坐席不存在")

    # 校验 role 白名单，防止非法角色值入库
    if role is not None and role not in ("admin", "agent"):
        raise AppException(1001, f"角色值非法: {role}，仅允许 admin 或 agent")

    if role is not None:
        agent.role = role
    if skill_tags is not None:
        agent.skill_tags = skill_tags
    if max_load is not None:
        agent.max_load = max_load

    agent.updated_at = datetime.now()
    db.add(agent)

    logger.info(f"更新坐席: id={agent_id}, role={role}, skill_tags={skill_tags}, max_load={max_load}")

    return {
        "id": agent.id,
        "role": agent.role,
        "skill_tags": agent.skill_tags or [],
        "max_load": agent.max_load,
    }


async def delete_agent(db: AsyncSession, agent_id: str) -> None:
    """移除坐席。

    Args:
        db: 数据库会话
        agent_id: 坐席ID

    Raises:
        AppException: 坐席不存在
    """
    result = await db.execute(
        select(Agent).where(Agent.id == agent_id)
    )
    agent = result.scalars().first()

    if not agent:
        raise AppException(3004, "坐席不存在")

    await db.delete(agent)
    logger.info(f"移除坐席: id={agent_id}, user_id={agent.user_id}")


# ==========================================================================
# 集成配置管理
# ==========================================================================

async def get_integrations(db: AsyncSession) -> List[IntegrationResponse]:
    """获取集成系统列表及配置状态。

    从 system_configs 表读取 integration_ 前缀的配置。

    Args:
        db: 数据库会话

    Returns:
        List[IntegrationResponse]: 集成系统列表
    """
    # 查询所有 integration_ 前缀的配置
    result = await db.execute(
        select(SystemConfig).where(
            SystemConfig.config_key.startswith("integration_")
        )
    )
    integ_configs = list(result.scalars().all())

    # 构建 {prefix: {api_url: ..., api_key: ...}} 映射
    config_map: Dict[str, Dict[str, str]] = {}
    for cfg in integ_configs:
        # integration_dify_api_url → 前缀 integration_dify_
        # 找到对应的 key_prefix
        for integ_def in INTEGRATION_DEFINITIONS:
            prefix = integ_def.get("key_prefix")
            if prefix and cfg.config_key.startswith(prefix):
                if prefix not in config_map:
                    config_map[prefix] = {}
                # 去掉前缀得到子键名（如 api_url, api_key）
                sub_key = cfg.config_key[len(prefix):]
                config_map[prefix][sub_key] = cfg.config_value
                break

    items: List[IntegrationResponse] = []
    for integ_def in INTEGRATION_DEFINITIONS:
        if integ_def["configurable"]:
            prefix = integ_def["key_prefix"]
            config_type = integ_def.get("config_type", "url_key")
            cfg_data = config_map.get(prefix, {})

            if config_type == "url_key":
                # Dify / RAGFlow 模式：api_url + api_key
                api_url = cfg_data.get("api_url", "")
                api_key = cfg_data.get("api_key", "")
                if api_url and api_key:
                    status = "connected"
                elif api_url:
                    status = "partial"
                else:
                    status = "disconnected"
                items.append(
                    IntegrationResponse(
                        id=integ_def["id"],
                        name=integ_def["name"],
                        status=status,
                        configurable=True,
                        config_type="url_key",
                        config=IntegrationConfig(
                            api_url=api_url,
                            api_key_set=bool(api_key),
                        ),
                    )
                )

            elif config_type == "access_key":
                # 火绒模式：access_key_id + access_key_secret + base_url
                access_key_id = cfg_data.get("access_key_id", "")
                access_key_secret = cfg_data.get("access_key_secret", "")
                base_url = cfg_data.get("base_url", "")
                if access_key_id and access_key_secret and base_url:
                    status = "connected"
                elif base_url:
                    status = "partial"
                else:
                    status = "disconnected"
                items.append(
                    IntegrationResponse(
                        id=integ_def["id"],
                        name=integ_def["name"],
                        status=status,
                        configurable=True,
                        config_type="access_key",
                        config=IntegrationConfig(
                            # url_key 模式字段（火绒不需要，但前端卡片复用展示）
                            api_url=base_url,
                            api_key_set=bool(access_key_id),
                            # access_key 模式专属字段
                            access_key_id_set=bool(access_key_id),
                            access_key_secret_set=bool(access_key_secret),
                            base_url=base_url or None,
                        ),
                    )
                )

            elif config_type == "account_password":
                # 联软模式：api_account + api_password + base_url + validate_key
                api_account = cfg_data.get("api_account", "")
                api_password = cfg_data.get("api_password", "")
                base_url = cfg_data.get("base_url", "")
                if api_account and api_password and base_url:
                    status = "connected"
                elif base_url:
                    status = "partial"
                else:
                    status = "disconnected"
                items.append(
                    IntegrationResponse(
                        id=integ_def["id"],
                        name=integ_def["name"],
                        status=status,
                        configurable=True,
                        config_type="account_password",
                        config=IntegrationConfig(
                            # 复用字段（前端展示用）
                            api_url=base_url,
                            api_key_set=bool(api_account),
                            # account_password 模式专属字段
                            base_url=base_url or None,
                            api_account_set=bool(api_account),
                            api_password_set=bool(api_password),
                        ),
                    )
                )

        else:
            items.append(
                IntegrationResponse(
                    id=integ_def["id"],
                    name=integ_def["name"],
                    status="disconnected",
                    configurable=False,
                    config=None,
                )
            )

    return items


async def update_integration(
    db: AsyncSession,
    integration_id: str,
    # url_key 模式（Dify / RAGFlow）
    api_url: str = "",
    api_key: str = "",
    # access_key 模式（火绒安全）
    access_key_id: str = "",
    access_key_secret: str = "",
    base_url: str = "",
    # account_password 模式（联软LV7000）
    api_account: str = "",
    api_password: str = "",
    validate_key: str = "",
    agent_id: str = "",
) -> IntegrationResponse:
    """更新集成系统配置。

    支持三种模式：
    - url_key 模式（Dify / RAGFlow）：传入 api_url + api_key
    - access_key 模式（火绒安全）：传入 access_key_id + access_key_secret + base_url
    - account_password 模式（联软LV7000）：传入 api_account + api_password + base_url + validate_key

    Args:
        db: 数据库会话
        integration_id: 集成系统ID（如 dify/ragflow/huorong/lianruan）
        api_url: API 地址（url_key 模式）
        api_key: API Key（url_key 模式）
        access_key_id: AccessKey ID（access_key 模式）
        access_key_secret: AccessKey Secret（access_key 模式）
        base_url: 内网 Base URL（access_key/account_password 模式）
        api_account: API账号（account_password 模式）
        api_password: API密码（account_password 模式）
        validate_key: 验证密钥（account_password 模式，可选）
        agent_id: 操作人 agent_id

    Returns:
        IntegrationResponse: 更新后的集成系统信息

    Raises:
        AppException: 集成系统不存在或不可配置
    """
    # 查找集成定义
    integ_def = None
    for d in INTEGRATION_DEFINITIONS:
        if d["id"] == integration_id:
            integ_def = d
            break

    if not integ_def:
        raise AppException(1003, f"集成系统不存在: {integration_id}")

    if not integ_def["configurable"]:
        raise AppException(1001, f"集成系统不可配置: {integ_def['name']}")

    config_type = integ_def.get("config_type", "url_key")
    prefix = integ_def["key_prefix"]

    # ---------- url_key 模式（Dify / RAGFlow）----------
    if config_type == "url_key":
        await _upsert_system_config(db, f"{prefix}api_url", api_url, f"{integ_def['name']} API 地址", agent_id)
        await _upsert_system_config(db, f"{prefix}api_key", api_key, f"{integ_def['name']} API Key", agent_id)

        if api_url and api_key:
            status = "connected"
        elif api_url:
            status = "partial"
        else:
            status = "disconnected"

        return IntegrationResponse(
            id=integration_id,
            name=integ_def["name"],
            status=status,
            configurable=True,
            config_type="url_key",
            config=IntegrationConfig(
                api_url=api_url,
                api_key_set=bool(api_key),
            ),
        )

    # ---------- access_key 模式（火绒安全）----------
    elif config_type == "access_key":
        await _upsert_system_config(db, f"{prefix}access_key_id", access_key_id, f"{integ_def['name']} AccessKey ID", agent_id)
        await _upsert_system_config(db, f"{prefix}access_key_secret", access_key_secret, f"{integ_def['name']} AccessKey Secret", agent_id)
        await _upsert_system_config(db, f"{prefix}base_url", base_url, f"{integ_def['name']} Base URL", agent_id)

        if access_key_id and access_key_secret and base_url:
            status = "connected"
        elif base_url:
            status = "partial"
        else:
            status = "disconnected"

        return IntegrationResponse(
            id=integration_id,
            name=integ_def["name"],
            status=status,
            configurable=True,
            config_type="access_key",
            config=IntegrationConfig(
                # 前端复用字段（url_key 模式展示用）
                api_url=base_url,
                api_key_set=bool(access_key_id),
                # access_key 模式专属字段
                access_key_id_set=bool(access_key_id),
                access_key_secret_set=bool(access_key_secret),
                base_url=base_url or None,
            ),
        )

    # ---------- account_password 模式（联软LV7000）----------
    elif config_type == "account_password":
        await _upsert_system_config(db, f"{prefix}api_account", api_account, f"{integ_def['name']} API账号", agent_id)
        await _upsert_system_config(db, f"{prefix}api_password", api_password, f"{integ_def['name']} API密码", agent_id)
        await _upsert_system_config(db, f"{prefix}base_url", base_url, f"{integ_def['name']} Base URL", agent_id)
        await _upsert_system_config(db, f"{prefix}validate_key", validate_key, f"{integ_def['name']} 验证密钥", agent_id)

        if api_account and api_password and base_url:
            status = "connected"
        elif base_url:
            status = "partial"
        else:
            status = "disconnected"

        return IntegrationResponse(
            id=integration_id,
            name=integ_def["name"],
            status=status,
            configurable=True,
            config_type="account_password",
            config=IntegrationConfig(
                api_url=base_url,
                api_key_set=bool(api_account),
                base_url=base_url or None,
                api_account_set=bool(api_account),
                api_password_set=bool(api_password),
            ),
        )

    raise AppException(1001, f"未知配置类型: {config_type}")


async def _get_config_value(db: AsyncSession, key: str) -> str:
    """快速读取单个配置值。

    Args:
        db: 数据库会话
        key: 配置键

    Returns:
        str: 配置值，不存在返回空字符串
    """
    result = await db.execute(
        select(SystemConfig.config_value).where(
            SystemConfig.config_key == key
        )
    )
    row = result.scalar()
    return row if row else ""


async def _upsert_system_config(
    db: AsyncSession,
    key: str,
    value: str,
    description: str,
    agent_id: str,
) -> None:
    """插入或更新 system_configs 记录，并记录变更日志。

    Args:
        db: 数据库会话
        key: 配置键
        value: 配置值
        description: 配置说明
        agent_id: 操作人
    """
    result = await db.execute(
        select(SystemConfig).where(SystemConfig.config_key == key)
    )
    config = result.scalars().first()

    if config:
        old_value = config.config_value
        config.config_value = value
        config.updated_at = datetime.now()
        db.add(config)

        # 记录变更日志
        change_log = ConfigChangeLog(
            config_key=key,
            old_value=old_value,
            new_value=value,
            changed_by=agent_id,
        )
        db.add(change_log)
    else:
        new_config = SystemConfig(
            config_key=key,
            config_value=value,
            description=description,
        )
        db.add(new_config)

        # 记录新增日志
        change_log = ConfigChangeLog(
            config_key=key,
            old_value="",
            new_value=value,
            changed_by=agent_id,
        )
        db.add(change_log)


# ==========================================================================
# 快速回复审核
# ==========================================================================

async def list_pending_quick_replies(
    db: AsyncSession,
    category: Optional[str] = None,
) -> List[AdminQuickReplyResponse]:
    """获取待审核快速回复模板列表。

    Args:
        db: 数据库会话
        category: 按分类筛选（可选）

    Returns:
        List[AdminQuickReplyResponse]: 待审核模板列表
    """
    stmt = (
        select(QuickReplyTemplate)
        .where(QuickReplyTemplate.status == "pending_review")
        .order_by(QuickReplyTemplate.updated_at.desc())
    )
    if category:
        stmt = stmt.where(QuickReplyTemplate.category == category)

    result = await db.execute(stmt)
    templates = list(result.scalars().all())

    # 批量查询提交人姓名
    submitted_by_ids = list({t.submitted_by for t in templates if t.submitted_by})
    agent_names: Dict[str, str] = {}
    if submitted_by_ids:
        agents_result = await db.execute(
            select(Agent.id, Agent.name).where(Agent.id.in_(submitted_by_ids))
        )
        agent_names = dict(agents_result.all())

    items = []
    for t in templates:
        items.append(
            AdminQuickReplyResponse(
                id=t.id,
                category=t.category,
                title=t.title,
                content=t.content,
                variables=t.variables or [],
                status=t.status,
                version=t.version,
                submitted_by=t.submitted_by,
                submitted_by_name=agent_names.get(t.submitted_by or "", ""),
                sort_order=t.sort_order,
                created_at=t.created_at,
                updated_at=t.updated_at,
            )
        )

    return items


async def review_quick_reply(
    db: AsyncSession,
    template_id: str,
    action: str,
    reason: str,
    agent_id: str,
) -> Dict[str, Any]:
    """审核快速回复模板（通过/驳回）。

    Args:
        db: 数据库会话
        template_id: 模板ID
        action: 审核动作（approve/reject）
        reason: 审核原因
        agent_id: 审核人 agent_id

    Returns:
        Dict: 包含 id, status, version

    Raises:
        AppException: 模板不存在或动作非法
    """
    if action not in ("approve", "reject"):
        raise AppException(1001, "审核动作只能是 approve 或 reject")

    result = await db.execute(
        select(QuickReplyTemplate).where(QuickReplyTemplate.id == template_id)
    )
    template = result.scalars().first()

    if not template:
        raise ERR_NOT_FOUND

    # 校验模板状态：仅允许审核 pending_review 状态的模板
    if template.status != "pending_review":
        raise AppException(
            1001,
            f"当前模板状态为 {template.status}，仅 pending_review 状态可审核"
        )

    if action == "approve":
        template.status = "approved"
        template.version = (template.version or 1) + 1
    else:
        template.status = "rejected"

    template.updated_at = datetime.now()
    db.add(template)

    logger.info(
        f"快速回复审核: id={template_id}, action={action}, "
        f"by={agent_id}, reason={reason}"
    )

    return {
        "id": template.id,
        "status": template.status,
        "version": template.version,
    }


# ==========================================================================
# 分配模式
# ==========================================================================

async def get_assignment_mode(db: AsyncSession) -> AssignmentModeResponse:
    """获取当前分配模式。

    从 system_configs 表读取 assign_mode 配置。

    Args:
        db: 数据库会话

    Returns:
        AssignmentModeResponse: 分配模式信息
    """
    # 读取当前分配模式
    result = await db.execute(
        select(SystemConfig).where(SystemConfig.config_key == "assign_mode")
    )
    config = result.scalars().first()
    current_mode = config.config_value if config else "manual"

    # 构建模式列表
    modes = [
        AssignmentModeItem(
            id=m["id"],
            name=m["name"],
            enabled=(m["id"] == current_mode),
            locked=m["locked"],
            unlock_at=m["unlock_at"],
        )
        for m in ASSIGNMENT_MODES
    ]

    return AssignmentModeResponse(
        current_mode=current_mode,
        modes=modes,
    )


async def update_assignment_mode(
    db: AsyncSession,
    mode: str,
    agent_id: str,
) -> AssignmentModeResponse:
    """切换分配模式（阶段一仅允许手动接单）。

    Args:
        db: 数据库会话
        mode: 分配模式ID
        agent_id: 操作人 agent_id

    Returns:
        AssignmentModeResponse: 更新后的分配模式信息

    Raises:
        AppException: 模式不存在或已锁定
    """
    # 验证模式是否合法
    mode_def = None
    for m in ASSIGNMENT_MODES:
        if m["id"] == mode:
            mode_def = m
            break

    if not mode_def:
        raise AppException(1001, f"无效的分配模式: {mode}")

    if mode_def["locked"]:
        raise AppException(1001, f"分配模式已锁定，将在{mode_def['unlock_at']}解锁")

    # 更新或创建 assign_mode 配置
    await _upsert_system_config(db, "assign_mode", mode, "消息分配模式", agent_id)

    logger.info(f"切换分配模式: mode={mode}, by={agent_id}")

    return await get_assignment_mode(db)


# ==========================================================================
# 会话监控
# ==========================================================================

async def get_monitor_sessions(
    db: AsyncSession,
    status: Optional[str] = None,
) -> MonitorSessionsResponse:
    """获取实时会话列表（Demo预览）。

    Args:
        db: 数据库会话
        status: 按状态筛选（可选，默认非 resolved）

    Returns:
        MonitorSessionsResponse: 会话监控数据
    """
    today_start = datetime.combine(date.today(), datetime.min.time())

    # 统计数据
    in_progress_result = await db.execute(
        select(func.count(Conversation.id)).where(
            Conversation.status == "serving"
        )
    )
    in_progress = in_progress_result.scalar() or 0

    queued_result = await db.execute(
        select(func.count(Conversation.id)).where(
            Conversation.status == "queued"
        )
    )
    queued = queued_result.scalar() or 0

    resolved_today_result = await db.execute(
        select(func.count(Conversation.id)).where(
            Conversation.status == "resolved",
            Conversation.updated_at >= today_start,
        )
    )
    resolved_today = resolved_today_result.scalar() or 0

    stats = SessionStats(
        in_progress=in_progress,
        queued=queued,
        resolved_today=resolved_today,
        alerts=0,
    )

    # 会话列表（默认查询非 resolved 的会话）
    stmt = select(Conversation).order_by(Conversation.created_at.desc())
    if status:
        stmt = stmt.where(Conversation.status == status)
    else:
        stmt = stmt.where(Conversation.status != "resolved")

    result = await db.execute(stmt.limit(50))
    conversations = list(result.scalars().all())

    # 批量查询坐席姓名
    agent_ids = list({c.assigned_agent_id for c in conversations if c.assigned_agent_id})
    agent_names: Dict[str, str] = {}
    if agent_ids:
        agents_result = await db.execute(
            select(Agent.id, Agent.name).where(Agent.id.in_(agent_ids))
        )
        agent_names = dict(agents_result.all())

    items = []
    for c in conversations:
        items.append(
            SessionItem(
                id=c.id,
                employee_name=c.employee_name,
                status=c.status,
                assigned_agent_name=agent_names.get(c.assigned_agent_id or "", ""),
                urgency_score=c.urgency_score,
                created_at=c.created_at,
                last_message_summary=c.last_message_summary,
            )
        )

    return MonitorSessionsResponse(stats=stats, items=items)


# ==========================================================================
# 全局搜索
# ==========================================================================

async def global_search(
    db: AsyncSession,
    query: str,
) -> List[SearchItem]:
    """全局搜索配置项、坐席、快速回复。

    按类型优先级排序：配置项 > 坐席 > 快速回复，同类型按名称排序。

    Args:
        db: 数据库会话
        query: 搜索关键词

    Returns:
        List[SearchItem]: 搜索结果列表
    """
    items: List[SearchItem] = []
    keyword = f"%{query}%"

    # 搜索配置项
    config_result = await db.execute(
        select(SystemConfig).where(
            or_(
                SystemConfig.config_key.ilike(keyword),
                SystemConfig.description.ilike(keyword),
            )
        ).limit(10)
    )
    for cfg in config_result.scalars().all():
        items.append(
            SearchItem(
                type="config",
                id=cfg.config_key,
                name=cfg.description or cfg.config_key,
                route="/admin/configs",
            )
        )

    # 搜索坐席
    agent_result = await db.execute(
        select(Agent).where(
            or_(
                Agent.name.ilike(keyword),
                Agent.user_id.ilike(keyword),
            )
        ).limit(10)
    )
    for a in agent_result.scalars().all():
        items.append(
            SearchItem(
                type="agent",
                id=a.id,
                name=a.name,
                route="/admin/agents",
            )
        )

    # 搜索快速回复
    qr_result = await db.execute(
        select(QuickReplyTemplate).where(
            or_(
                QuickReplyTemplate.title.ilike(keyword),
                QuickReplyTemplate.content.ilike(keyword),
                QuickReplyTemplate.category.ilike(keyword),
            )
        ).limit(10)
    )
    for qr in qr_result.scalars().all():
        items.append(
            SearchItem(
                type="quick_reply",
                id=qr.id,
                name=qr.title,
                route="/admin/quick-replies",
            )
        )

    return items


# ==========================================================================
# P2: 会话审计
# ==========================================================================

async def list_audit_conversations(
    db: AsyncSession,
    status: Optional[str] = None,
    agent_id: Optional[str] = None,
    keyword: Optional[str] = None,
    date_from: Optional[str] = None,
    date_to: Optional[str] = None,
    page: int = 1,
    page_size: int = 20,
) -> Dict[str, Any]:
    """获取会话审计列表（支持分页+多条件筛选）。"""
    stmt = select(Conversation)
    count_stmt = select(func.count(Conversation.id))

    filters = []
    if status:
        filters.append(Conversation.status == status)
    if agent_id:
        filters.append(Conversation.assigned_agent_id == agent_id)
    if keyword:
        like_pattern = f"%{keyword}%"
        filters.append(
            or_(
                Conversation.employee_name.ilike(like_pattern),
                Conversation.last_message_summary.ilike(like_pattern),
            )
        )
    if date_from:
        try:
            dt_from = datetime.strptime(date_from, "%Y-%m-%d")
            filters.append(Conversation.created_at >= dt_from)
        except ValueError:
            pass
    if date_to:
        try:
            dt_to = datetime.strptime(date_to, "%Y-%m-%d") + timedelta(days=1)
            filters.append(Conversation.created_at < dt_to)
        except ValueError:
            pass

    if filters:
        stmt = stmt.where(and_(*filters))
        count_stmt = count_stmt.where(and_(*filters))

    total_result = await db.execute(count_stmt)
    total = total_result.scalar() or 0

    offset = (page - 1) * page_size
    stmt = stmt.order_by(Conversation.created_at.desc()).offset(offset).limit(page_size)
    result = await db.execute(stmt)
    conversations = list(result.scalars().all())

    agent_ids = list({c.assigned_agent_id for c in conversations if c.assigned_agent_id})
    agent_names: Dict[str, str] = {}
    if agent_ids:
        agents_result = await db.execute(
            select(Agent.id, Agent.name).where(Agent.id.in_(agent_ids))
        )
        agent_names = dict(agents_result.all())

    items = []
    for c in conversations:
        items.append({
            "id": c.id,
            "employee_name": c.employee_name or "",
            "department": c.department or "",
            "status": c.status,
            "assigned_agent_name": agent_names.get(c.assigned_agent_id or "", ""),
            "urgency_score": c.urgency_score or 1,
            "created_at": c.created_at.isoformat() if c.created_at else "",
            "updated_at": c.updated_at.isoformat() if c.updated_at else "",
            "last_message_summary": c.last_message_summary or "",
        })

    return {"items": items, "total": total, "page": page, "page_size": page_size}


async def get_audit_conversation_detail(
    db: AsyncSession,
    conversation_id: str,
) -> Optional[Dict[str, Any]]:
    """获取会话审计详情（含消息列表）。"""
    result = await db.execute(
        select(Conversation).where(Conversation.id == conversation_id)
    )
    conv = result.scalars().first()
    if not conv:
        return None

    msgs_result = await db.execute(
        select(Message)
        .where(Message.conversation_id == conversation_id)
        .order_by(Message.created_at.asc())
        .limit(200)
    )
    messages = list(msgs_result.scalars().all())

    agent_name = ""
    if conv.assigned_agent_id:
        agent_result = await db.execute(
            select(Agent.name).where(Agent.id == conv.assigned_agent_id)
        )
        agent_name = agent_result.scalar() or ""

    return {
        "id": conv.id,
        "employee_name": conv.employee_name or "",
        "employee_id": conv.employee_id or "",
        "department": conv.department or "",
        "position": conv.position or "",
        "status": conv.status,
        "assigned_agent_name": agent_name,
        "urgency_score": conv.urgency_score or 1,
        "tags": conv.tags or [],
        "created_at": conv.created_at.isoformat() if conv.created_at else "",
        "updated_at": conv.updated_at.isoformat() if conv.updated_at else "",
        "last_message_summary": conv.last_message_summary or "",
        "messages": [
            {
                "id": m.id,
                "sender_type": m.sender_type,
                "sender_name": m.sender_name or "",
                "content": m.content or "",
                "msg_type": m.msg_type or "text",
                "created_at": m.created_at.isoformat() if m.created_at else "",
            }
            for m in messages
        ],
    }


# ==========================================================================
# P2: 坐席绩效统计
# ==========================================================================

async def get_agent_performance(
    db: AsyncSession,
    date_from: Optional[str] = None,
    date_to: Optional[str] = None,
) -> List[Dict[str, Any]]:
    """获取坐席绩效统计。"""
    today = date.today()
    if date_from:
        try:
            dt_from = datetime.strptime(date_from, "%Y-%m-%d")
        except ValueError:
            dt_from = datetime(today.year, today.month, 1)
    else:
        dt_from = datetime(today.year, today.month, 1)

    if date_to:
        try:
            dt_to = datetime.strptime(date_to, "%Y-%m-%d") + timedelta(days=1)
        except ValueError:
            dt_to = datetime.now()
    else:
        dt_to = datetime.now()

    agents_result = await db.execute(select(Agent).order_by(Agent.name))
    agents = list(agents_result.scalars().all())
    if not agents:
        return []

    agent_ids = [a.id for a in agents]

    total_conv_result = await db.execute(
        select(Conversation.assigned_agent_id, func.count(Conversation.id))
        .where(
            Conversation.assigned_agent_id.in_(agent_ids),
            Conversation.created_at >= dt_from,
            Conversation.created_at < dt_to,
        )
        .group_by(Conversation.assigned_agent_id)
    )
    total_conv_map = dict(total_conv_result.all())

    resolved_result = await db.execute(
        select(Conversation.assigned_agent_id, func.count(Conversation.id))
        .where(
            Conversation.assigned_agent_id.in_(agent_ids),
            Conversation.status == "resolved",
            Conversation.created_at >= dt_from,
            Conversation.created_at < dt_to,
        )
        .group_by(Conversation.assigned_agent_id)
    )
    resolved_map = dict(resolved_result.all())

    today_start = datetime.combine(today, datetime.min.time())
    today_conv_result = await db.execute(
        select(Conversation.assigned_agent_id, func.count(Conversation.id))
        .where(
            Conversation.assigned_agent_id.in_(agent_ids),
            Conversation.created_at >= today_start,
        )
        .group_by(Conversation.assigned_agent_id)
    )
    today_conv_map = dict(today_conv_result.all())

    items = []
    for a in agents:
        total = total_conv_map.get(a.id, 0)
        resolved = resolved_map.get(a.id, 0)
        today_count = today_conv_map.get(a.id, 0)
        resolution_rate = f"{(resolved / total * 100):.0f}%" if total > 0 else "—"

        items.append({
            "id": a.id,
            "user_id": a.user_id,
            "name": a.name,
            "status": a.status,
            "role": a.role,
            "skill_tags": a.skill_tags or [],
            "current_load": a.current_load or 0,
            "max_load": a.max_load or 5,
            "total_conversations": total,
            "resolved_conversations": resolved,
            "today_conversations": today_count,
            "resolution_rate": resolution_rate,
        })

    return items


# ==========================================================================
# P2: 系统日志
# ==========================================================================

async def get_system_logs(
    db: AsyncSession,
    page: int = 1,
    page_size: int = 50,
) -> Dict[str, Any]:
    """获取系统日志（配置变更日志）。"""
    count_result = await db.execute(select(func.count(ConfigChangeLog.id)))
    total = count_result.scalar() or 0

    offset = (page - 1) * page_size
    result = await db.execute(
        select(ConfigChangeLog)
        .order_by(ConfigChangeLog.changed_at.desc())
        .offset(offset)
        .limit(page_size)
    )
    logs = list(result.scalars().all())

    agent_ids = list({log.changed_by for log in logs if log.changed_by})
    agent_names: Dict[str, str] = {}
    if agent_ids:
        agents_result = await db.execute(
            select(Agent.id, Agent.name).where(Agent.id.in_(agent_ids))
        )
        agent_names = dict(agents_result.all())

    items = []
    for log in logs:
        items.append({
            "id": log.id,
            "log_type": "config_change",
            "config_key": log.config_key,
            "old_value": log.old_value or "",
            "new_value": log.new_value or "",
            "changed_by": log.changed_by or "",
            "changed_by_name": agent_names.get(log.changed_by or "", ""),
            "changed_at": log.changed_at.isoformat() if log.changed_at else "",
        })

    return {"items": items, "total": total, "page": page, "page_size": page_size}
