# =============================================================================
# 企微IT智能服务台 — 坐席管理 API
# =============================================================================
# 说明：坐席端的管理接口，包括：
#   1. POST /api/agents/login — 坐席登录（用户名密码，返回JWT token）
#   2. GET /api/agents/me — 获取当前坐席信息
#   3. PUT /api/agents/me/status — 更新坐席状态（online/busy/offline）
#   4. GET /api/agents — 获取坐席列表（用于转接选择）
# 坐席认证使用 JWT，token 存 Redis（TTL 8小时）
# =============================================================================

import base64
import io
import json
import logging
import secrets
from datetime import datetime
from typing import Optional
from uuid import UUID

import pyotp
import qrcode
import redis.asyncio as aioredis
import bcrypt  # P1 修复: 直接使用 bcrypt 库替代 passlib
from fastapi import APIRouter, Depends, Header, Query, Request
from pydantic import BaseModel, Field
from slowapi import Limiter
from slowapi.util import get_remote_address
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.config import settings
from app.database import get_db
from app.dependencies import get_current_user, require_role
from app.models.agent import Agent
from app.schemas.agent import AgentLogin, AgentResponse, AgentStatusUpdate
from app.services.wecom_service import WecomService
from app.utils.response import AppException, ERR_UNAUTHORIZED, success_response
from app.utils.error_codes import ErrorCode

# 速率限制器实例（与 main.py 共享同一配置）
# 移除 env_file=None 参数：slowapi 0.1.9 不支持该参数
# python-dotenv 已在应用启动时处理 .env 文件
limiter = Limiter(key_func=get_remote_address)

logger = logging.getLogger(__name__)

# 创建路由器
router = APIRouter()

# JWT 简化版：使用随机 token 存 Redis，TTL 8 小时
# 为什么不用标准 JWT：第一步简化实现，token 存 Redis 更容易实现登出和状态管理
TOKEN_TTL_SECONDS = 8 * 60 * 60  # 8小时


def _get_redis() -> aioredis.Redis:
    """获取 Redis 客户端。"""
    return settings.create_redis_client()


# --------------------------------------------------------------------------
# 坐席认证依赖
# --------------------------------------------------------------------------
async def get_current_agent(
    authorization: Optional[str] = Header(None, alias="Authorization"),
    db: AsyncSession = Depends(get_db),
) -> Agent:
    """从请求头中提取坐席身份（认证依赖）。

    支持两种 Token 格式：
    1. 统一格式：user:token:{token} → JSON 包含 employee_id 和 roles
    2. 旧格式：agent:token:{token} → 直接存储 user_id

    Args:
        authorization: 请求头中的 Authorization 字段（格式：Bearer token）
        db: 数据库会话

    Returns:
        Agent: 当前坐席对象

    Raises:
        AppException: 未授权（token 缺失、无效或过期）
    """
    if not authorization:
        raise ERR_UNAUTHORIZED

    # 提取 token（支持 "Bearer xxx" 格式）
    token = authorization.replace("Bearer ", "") if authorization.startswith("Bearer ") else authorization

    if not token:
        raise ERR_UNAUTHORIZED

    # 从 Redis 查找坐席ID
    redis_client = _get_redis()
    try:
        # 1. 尝试统一格式（新）
        unified_data = await redis_client.get(f"user:token:{token}")
        if unified_data:
            try:
                user_info = json.loads(unified_data)
                agent_user_id = user_info.get("employee_id")
                if agent_user_id:
                    # 从数据库查找坐席
                    stmt = select(Agent).where(Agent.user_id == agent_user_id)
                    result = await db.execute(stmt)
                    agent = result.scalars().first()
                    if agent:
                        return agent
            except json.JSONDecodeError:
                logger.warning(f"统一 Token 数据解析失败: {token[:10]}...")

        # 2. 尝试旧格式（兼容）
        agent_user_id = await redis_client.get(f"agent:token:{token}")
        if not agent_user_id:
            raise ERR_UNAUTHORIZED

        # 从数据库查找坐席
        # agent_user_id 可能是 bytes（Redis 返回）或 str
        uid = agent_user_id.decode("utf-8") if isinstance(agent_user_id, bytes) else agent_user_id
        stmt = select(Agent).where(Agent.user_id == uid)
        result = await db.execute(stmt)
        agent = result.scalars().first()

        if not agent:
            raise ERR_UNAUTHORIZED

        return agent

    except AppException:
        # 业务异常直接抛出（如 ERR_UNAUTHORIZED）
        raise
    except Exception as e:
        # Redis 连接失败等底层异常
        logger.error(f"Redis 读取失败: {e}")
        raise ERR_UNAUTHORIZED
    finally:
        try:
            await redis_client.close()
        except Exception:
            pass


# --------------------------------------------------------------------------
# POST /api/agents/login — 坐席登录
# --------------------------------------------------------------------------
@router.post("/agents/login")
@limiter.limit("10/minute")  # 登录接口限流：每IP每分钟最多10次，防暴力破解
async def agent_login(
    request: Request,
    body: AgentLogin,
    db: AsyncSession = Depends(get_db),
):
    """坐席登录。

    第一步使用简单的用户名密码登录。
    登录成功后生成 token 存入 Redis（TTL 8小时）。

    流程：
    1. 查找坐席记录（按 user_id），不存在则自动创建
    2. 生成随机 token
    3. token 存 Redis（key: agent:token:{token}, value: user_id）
    4. 更新坐席状态为 online
    5. 返回坐席信息和 token

    Args:
        body: 登录请求体（包含 user_id 和 name）
        db: 数据库会话

    Returns:
        Dict: 统一响应格式，包含坐席信息和 token
    """
    try:
        # 0. 企微通讯录身份验证（防止任意 user_id 冒充坐席）
        #    调用企微API校验 user_id 是否存在于通讯录中
        #    安全策略：
        #    - 企微验证通过 → 正常登录，用企微真实姓名覆盖前端传入值
        #    - 企微验证失败（用户不存在） → 拒绝登录
        #    - 企微API不可达（网络故障） → 仅允许已注册坐席降级登录，新注册必须验证
        wecom_verified = False
        try:
            redis_client_verify = _get_redis()
            try:
                wecom_service = WecomService(redis_client_verify)
                user_info = await wecom_service.get_user_info(body.user_id)
                # 验证通过：用户存在于企微通讯录
                wecom_verified = True
                # 用企微返回的真实姓名覆盖前端传入的姓名（防止冒用他人身份）
                real_name = user_info.get("name", "")
                if real_name:
                    body.name = real_name
                logger.info(f"坐席企微身份验证通过: user_id={body.user_id}, name={real_name}")
            finally:
                try:
                    await redis_client_verify.close()
                except Exception:
                    pass
                try:
                    await wecom_service.close()
                except Exception:
                    pass
        except Exception as wecom_err:
            # 企微API不可达时：仅允许已注册坐席降级登录，新注册必须验证
            # 原因：网络故障不应阻断已注册坐席工作，但不能让未验证用户注册新账号
            logger.warning(
                f"企微通讯录验证失败: user_id={body.user_id}, "
                f"error={wecom_err}"
            )
            # 检查是否为已注册坐席（数据库已有记录才允许降级登录）
            check_stmt = select(Agent).where(Agent.user_id == body.user_id)
            check_result = await db.execute(check_stmt)
            existing_agent = check_result.scalars().first()
            if not existing_agent:
                # 新坐席注册必须通过企微验证，防止任意 user_id 冒充
                raise AppException(
                    ErrorCode.AUTH_TOKEN_INVALID,
                    "企微通讯录验证失败，新坐席注册需要企微身份验证。请稍后重试或联系管理员。"
                )
            logger.warning(
                f"企微API不可达，已注册坐席降级放行: user_id={body.user_id}"
            )
            # P0 修复: 降级放行时，如果 agent 已设置密码则必须验证本地密码
            if existing_agent:
                if existing_agent.password_hash is None:
                    # 已注册坐席但未设置密码，要求先设置密码
                    raise AppException(
                        ErrorCode.AUTH_PASSWORD_REQUIRED,
                        "首次登录请先设置密码。管理后台 → 坐席管理 → 设置本地密码"
                    )
                if not body.password:
                    raise AppException(ErrorCode.AUTH_PASSWORD_WRONG, "请输入本地密码")
                if not bcrypt.checkpw(body.password.encode('utf-8'), existing_agent.password_hash.encode('utf-8')):
                    raise AppException(ErrorCode.AUTH_PASSWORD_WRONG, "本地密码错误")

        # 1. 查找或创建坐席记录
        stmt = select(Agent).where(Agent.user_id == body.user_id)
        result = await db.execute(stmt)
        agent = result.scalars().first()

        if not agent:
            # 首次登录，创建坐席记录
            agent = Agent(
                user_id=body.user_id,
                name=body.name,
                status="online",
                current_load=0,
                max_load=5,
            )
            db.add(agent)
            await db.flush()
            logger.info(f"新坐席注册: user_id={body.user_id}, name={body.name}")
        else:
            # 更新坐席名称（可能改名了）
            agent.name = body.name
            agent.status = "online"
            agent.updated_at = datetime.now()
            db.add(agent)
            await db.flush()
            logger.info(f"坐席登录: user_id={body.user_id}, name={body.name}")

        # 2. OTP 二次验证（admin 角色且已绑定 OTP）
        if agent.role == "admin" and agent.otp_enabled == 1:
            if not body.otp_code:
                # 需要 OTP 验证，返回 require_otp 标记
                return success_response(data={
                    "require_otp": True,
                    "message": "请输入OTP动态码",
                    "user_id": agent.user_id,
                    "name": agent.name,
                })
            else:
                # 验证 OTP 码
                totp = pyotp.TOTP(agent.otp_secret)
                if not totp.verify(body.otp_code, valid_window=1):
                    raise AppException(1006, "OTP验证码错误，请重新输入")

        # 3. 生成随机 token（使用统一格式）
        from app.services.token_service import TokenService
        from app.dependencies import get_redis

        # 使用共享 Redis 连接（从连接池获取，不要手动关闭）
        redis_client = await get_redis()
        token_service = TokenService(redis_client)

        # 查询用户角色
        from app.services.role_mapping_service import RoleMappingService
        role_service = RoleMappingService(db)
        roles = await role_service.get_user_roles(body.user_id)

        # 创建统一格式的 Token
        token = await token_service.create_token(
            employee_id=body.user_id,
            name=body.name,
            roles=roles,
            login_source="agent",
        )

        # 5. 返回坐席信息和 token
        agent_data = AgentResponse.model_validate(agent).model_dump()
        agent_data["token"] = token

        return success_response(data=agent_data)

    except AppException:
        # 业务异常直接抛出
        raise
    except Exception as e:
        # 未预期的异常：记录日志，返回友好错误
        logger.error(f"登录异常: {e}", exc_info=True)
        raise AppException(1005, f"登录失败: {str(e)}")


# --------------------------------------------------------------------------
# GET /api/agents/me — 获取当前坐席信息
# --------------------------------------------------------------------------
@router.get("/agents/me")
async def get_agent_me(
    agent: Agent = Depends(get_current_agent),
):
    """获取当前坐席信息。

    需要在请求头中携带有效的 token。

    Args:
        agent: 当前坐席（通过认证依赖注入）

    Returns:
        Dict: 统一响应格式，包含坐席信息
    """
    agent_data = AgentResponse.model_validate(agent).model_dump()
    return success_response(data=agent_data)


# --------------------------------------------------------------------------
# PUT /api/agents/me/status — 更新坐席状态
# --------------------------------------------------------------------------
@router.put("/agents/me/status")
async def update_agent_status(
    body: AgentStatusUpdate,
    agent: Agent = Depends(get_current_agent),
    db: AsyncSession = Depends(get_db),
):
    """更新坐席状态。

    坐席可以切换为 online/busy/offline。
    - online: 在线，可以接收新会话
    - busy: 忙碌，不接收新会话但继续处理已有的
    - offline: 离线，不接收任何会话

    Args:
        body: 状态更新请求体
        agent: 当前坐席
        db: 数据库会话

    Returns:
        Dict: 统一响应格式，包含更新后的坐席信息
    """
    agent.status = body.status
    agent.updated_at = datetime.now()
    db.add(agent)
    await db.flush()

    logger.info(f"坐席状态更新: agent={agent.user_id}, status={body.status}")

    agent_data = AgentResponse.model_validate(agent).model_dump()
    return success_response(data=agent_data)


# --------------------------------------------------------------------------
# GET /api/agents — 获取坐席列表（需要 agent 或 admin 角色）
# --------------------------------------------------------------------------
@router.get("/agents")
@require_role("agent", "admin")
async def list_agents(
    status: Optional[str] = Query(None, description="按状态过滤: online/busy/offline"),
    db: AsyncSession = Depends(get_db),
):
    """获取坐席列表。

    用于转接选择时展示可用的坐席列表。

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

    Returns:
        Dict: 统一响应格式，包含坐席列表
    """
    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())

    items = [AgentResponse.model_validate(a).model_dump() for a in agents]
    return success_response(data={"items": items})


# --------------------------------------------------------------------------
# OTP 绑定接口
# --------------------------------------------------------------------------
@router.post("/agents/otp-bind")
async def bind_agent_otp(
    agent: Agent = Depends(get_current_agent),
    db: AsyncSession = Depends(get_db),
):
    """为当前坐席生成 OTP 密钥和二维码。

    生成 TOTP 密钥，生成 otpauth:// URI 用于扫码绑定 Google Authenticator。
    返回二维码（base64编码）和密钥，供用户手动输入备用。

    Returns:
        Dict: 二维码图片(base64)和密钥
    """
    try:
        # 检查是否已绑定
        if agent.otp_secret:
            # 已绑定，返回现有密钥的二维码
            totp = pyotp.TOTP(agent.otp_secret)
        else:
            # 生成新密钥
            secret = pyotp.random_base32()
            agent.otp_secret = secret
            # otp_enabled 保持 0，等待首次验证后启用
            db.add(agent)
            await db.flush()
            totp = pyotp.TOTP(secret)

        # 生成 otpauth:// URI
        otpauth_uri = totp.provisioning_uri(
            name=f"IT支持服务:{agent.name}",
            issuer_name="IT支持服务",
        )

        # 生成二维码图片
        qr = qrcode.make(otpauth_uri)
        buffer = io.BytesIO()
        qr.save(buffer, format="PNG")
        qr_base64 = base64.b64encode(buffer.getvalue()).decode()

        logger.info(f"OTP绑定: agent={agent.user_id}, secret={agent.otp_secret[:4]}...")

        return success_response(data={
            "qr_code": f"data:image/png;base64,{qr_base64}",
            "secret": agent.otp_secret,
        })

    except AppException:
        raise
    except Exception as e:
        logger.error(f"OTP绑定异常: {e}", exc_info=True)
        raise AppException(1007, f"OTP绑定失败: {str(e)}")


@router.post("/agents/otp-verify")
async def verify_agent_otp(
    body: AgentLogin,  # 复用 AgentLogin，otp_code 为必填
    db: AsyncSession = Depends(get_db),
):
    """验证并启用 OTP。

    用户输入 OTP 码验证成功后，启用 OTP。
    首次验证成功后 otp_enabled 设为 1。

    Args:
        body.otp_code: 用户输入的 OTP 码（必填）

    Returns:
        Dict: 验证结果
    """
    try:
        # 查找坐席
        stmt = select(Agent).where(Agent.user_id == body.user_id)
        result = await db.execute(stmt)
        agent = result.scalars().first()

        if not agent or not agent.otp_secret:
            raise AppException(1008, "请先绑定OTP")

        # 验证 OTP 码
        totp = pyotp.TOTP(agent.otp_secret)
        if not totp.verify(body.otp_code, valid_window=1):
            raise AppException(1006, "OTP验证码错误")

        # 验证成功，启用 OTP
        agent.otp_enabled = 1
        agent.updated_at = datetime.now()
        db.add(agent)
        await db.flush()

        logger.info(f"OTP验证成功并启用: agent={agent.user_id}")

        return success_response(data={
            "otp_enabled": True,
            "message": "OTP验证成功，已启用",
        })

    except AppException:
        raise
    except Exception as e:
        logger.error(f"OTP验证异常: {e}", exc_info=True)
        raise AppException(1009, f"OTP验证失败: {str(e)}")


@router.post("/agents/otp-unbind")
async def unbind_agent_otp(
    agent: Agent = Depends(get_current_agent),
    db: AsyncSession = Depends(get_db),
):
    """解绑 OTP。

    解绑后 otp_secret 和 otp_enabled 都清空。
    需要管理员操作。

    Returns:
        Dict: 解绑结果
    """
    try:
        agent.otp_secret = None
        agent.otp_enabled = 0
        agent.updated_at = datetime.now()
        db.add(agent)
        await db.flush()

        logger.info(f"OTP解绑: agent={agent.user_id}")

        return success_response(data={"message": "OTP已解绑"})

    except AppException:
        raise
    except Exception as e:
        logger.error(f"OTP解绑异常: {e}", exc_info=True)
        raise AppException(1010, f"OTP解绑失败: {str(e)}")


# --------------------------------------------------------------------------
# 本地密码管理接口（P0-#5）
# --------------------------------------------------------------------------
class AgentPasswordUpdate(BaseModel):
    """坐席修改密码请求 Schema"""
    old_password: Optional[str] = Field(None, description="旧密码（验证用）")
    new_password: str = Field(..., min_length=6, max_length=128, description="新密码")


@router.post("/agents/password")
async def update_agent_password(
    body: AgentPasswordUpdate,
    agent: Agent = Depends(get_current_agent),
    db: AsyncSession = Depends(get_db),
):
    """修改坐席本地密码。

    可选功能，允许坐席设置本地密码作为备用认证方式。
    企微验证失败时，可使用本地密码认证。

    P0-#5 新增端点。

    Args:
        body.new_password: 新密码（6-128位）

    Returns:
        Dict: 修改结果
    """
    try:
        # 如果已有旧密码，验证旧密码
        if agent.password_hash:
            if not body.old_password:
                # 2026-06-15 修复: 改用专用 ErrorCode,避免与登录 1012 冲突
                raise AppException(ErrorCode.AUTH_OLD_PASSWORD_REQUIRED, "请输入旧密码")
            if not bcrypt.checkpw(body.old_password.encode('utf-8'), agent.password_hash.encode('utf-8')):
                # 2026-06-15 修复: 改用专用 ErrorCode
                raise AppException(ErrorCode.AUTH_OLD_PASSWORD_WRONG, "旧密码错误")

        # 设置新密码
        agent.password_hash = bcrypt.hashpw(body.new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
        agent.updated_at = datetime.now()
        db.add(agent)
        await db.flush()

        logger.info(f"本地密码已更新: agent={agent.user_id}")

        return success_response(data={"message": "密码已更新"})

    except AppException:
        raise
    except Exception as e:
        logger.error(f"密码更新异常: {e}", exc_info=True)
        raise AppException(1014, f"密码更新失败: {str(e)}")
