"""FastAPI 应用实例：lifespan / 异常处理 / 中间件 / 路由注册 / 状态初始化。

由 server.py（薄入口）导入 app 并交给 uvicorn.run。
"""

from __future__ import annotations

import argparse
import asyncio
import logging
import os
import sys
import time
from contextlib import asynccontextmanager
from typing import AsyncIterator

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

from woc_bridge.config import _state, BridgeConfig
from woc_bridge.db.coordinator import _resolve_db_state_tuple, _auto_extract_with_lock
from woc_bridge.messaging import MessageStreamer
from woc_bridge.models import BridgeError, ErrorResponse
from woc_bridge.version import BRIDGE_VERSION
from woc_bridge.db import DbReader
from woc_bridge.messaging import SendQueue
from woc_bridge.messaging.ui_action_scheduler import UIActionScheduler
from woc_bridge.ui import QrCapture, XdotoolDriver

# 路由
from woc_bridge.routes import status as routes_status
from woc_bridge.routes import db as routes_db
from woc_bridge.routes import messages as routes_messages
from woc_bridge.routes import send as routes_send
from woc_bridge.routes import login as routes_login
from woc_bridge.routes import contacts as routes_contacts
from woc_bridge.routes import moments as routes_moments
from woc_bridge.routes import media as routes_media
from woc_bridge.routes import diagnostic as routes_diagnostic
from woc_bridge.routes import screenshot as routes_screenshot
from woc_bridge.routes import export as routes_export
from woc_bridge.routes import batch as routes_batch


# 模块级 logger
logger = logging.getLogger("woc-bridge")

# 为 woc-bridge logger 配置输出到 stderr 的 handler
if not logger.handlers:
    _handler = logging.StreamHandler(sys.stderr)
    _handler.setFormatter(
        logging.Formatter(
            "%(asctime)s [%(levelname)s] [%(trace_id)s] %(name)s: %(message)s"
        )
    )
    logger.addHandler(_handler)
    # P3：注入 TraceFilter 让日志带 trace_id
    from woc_bridge.ui.trace import TraceFilter
    _handler.addFilter(TraceFilter())
logger.setLevel(logging.INFO)
logger.propagate = False


# ---------------------------------------------------------------------------
# lifespan
# ---------------------------------------------------------------------------
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
    """应用生命周期。

    兼容两种启动方式：
    1. `python3 server.py`：__main__ 已 _init_state，lifespan 直接用
    2. `uvicorn server:app`：__main__ 未执行，lifespan 内兜底初始化

    启动 send_queue，yield，停止 send_queue。
    """
    # 兜底初始化：兼容 uvicorn server:app 启动方式
    if _state.xdotool is None:
        _init_state_from_env()
    _state.start_time = time.monotonic()
    if _state.send_queue is not None:
        await _state.send_queue.start()
    if _state.message_streamer is not None:
        await _state.message_streamer.start()
    # 异步预热 display_name 缓存，降低高并发首批请求的 DB 压力
    try:
        from woc_bridge.routes.send import warm_display_name_cache
        asyncio.create_task(warm_display_name_cache(_state.db_reader))
    except Exception as e:
        logger.warning("[lifespan] warm_display_name_cache 启动失败: %s", e)
    # P3：启动 HA 模块（watchdog + resource_reaper）
    if _state.watchdog is not None:
        try:
            await _state.watchdog.start()
        except Exception as e:
            logger.warning("[lifespan] watchdog start failed: %s", e)
    if _state.resource_reaper is not None:
        try:
            await _state.resource_reaper.start()
        except Exception as e:
            logger.warning("[lifespan] resource_reaper start failed: %s", e)
    # 启动时全量清场：上次崩溃可能残留脏状态（搜索框打开/输入框有内容）
    # 失败仅告警不阻塞启动（spec Requirement: 启动全量清场）
    if _state.xdotool is not None:
        try:
            cleanup_ok = await _state.xdotool._full_cleanup_on_startup()
            if cleanup_ok:
                logger.info("[lifespan] 启动清场完成")
            else:
                logger.warning("[lifespan] 启动清场 3 次失败，可能需要人工 VNC 接入")
        except Exception as e:
            logger.warning("[lifespan] 启动清场异常（不阻塞启动）: %s", e)
    # 启动登录守卫（在 friend_watcher 之前，提供登录状态检测基础）
    if _state.login_guard is not None:
        try:
            await _state.login_guard.start()
        except Exception as e:
            logger.warning("[lifespan] login_guard.start() 失败: %s", e)
    # 启动好友申请监听器（仅当 auto_accept 开启时）
    # 放在 message_streamer 之后，保持"基础设施先于业务"顺序
    if _state.friend_watcher is not None and _state.config.auto_accept_enabled:
        try:
            # 必须先 set_enabled(True) 再 start()：FriendRequestWatcher 构造时
            # _enabled_event 初始为 cleared，_watch_loop 会阻塞在 wait() 上不轮询
            _state.friend_watcher.set_enabled(True)
            await _state.friend_watcher.start()
        except Exception as e:
            logger.warning("[lifespan] friend_watcher.start() 失败: %s", e)
    try:
        yield
    finally:
        # 先停 friend_watcher（业务），再停 login_guard，最后停基础设施
        if _state.friend_watcher is not None:
            try:
                await asyncio.wait_for(
                    _state.friend_watcher.stop(), timeout=5.0
                )
            except Exception:
                # Exception 已涵盖 TimeoutError，无需单独列
                logger.warning("[lifespan] friend_watcher.stop() 超时或异常，强制继续")
        # 停止登录守卫（friend_watcher 之后，LIFO）
        if _state.login_guard is not None:
            try:
                await asyncio.wait_for(_state.login_guard.stop(), timeout=5.0)
            except (asyncio.TimeoutError, Exception):
                logger.warning("[lifespan] login_guard.stop() 超时或异常，强制继续")
        # 停止消息流推送器（带超时，防止 worker 卡住阻塞关闭）
        if _state.message_streamer is not None:
            try:
                await asyncio.wait_for(_state.message_streamer.stop(), timeout=5.0)
            except (asyncio.TimeoutError, Exception):
                logger.warning("[lifespan] message_streamer.stop() 超时或异常，强制继续")
        # 清理 SSE 发送结果校验总线：resolve 所有 pending 等待为 False
        if _state.verify_bus is not None:
            try:
                _state.verify_bus.clear()
            except Exception:
                logger.warning("[lifespan] verify_bus.clear() 异常，强制继续")
        # 停止群发后台协程（LIFO：先于 send_queue 停止，避免新任务继续入队）
        if _state.batch_worker is not None:
            try:
                await asyncio.wait_for(_state.batch_worker.stop(), timeout=5.0)
            except (asyncio.TimeoutError, Exception):
                logger.warning("[lifespan] batch_worker.stop() 超时或异常，强制继续")
        # 停止发送队列（带超时）
        if _state.send_queue is not None:
            try:
                await asyncio.wait_for(_state.send_queue.stop(), timeout=5.0)
            except (asyncio.TimeoutError, Exception):
                logger.warning("[lifespan] send_queue.stop() 超时或异常，强制继续")
        # P3：停止 HA 模块
        if _state.watchdog is not None:
            try:
                await asyncio.wait_for(_state.watchdog.stop(), timeout=5.0)
            except (asyncio.TimeoutError, Exception):
                logger.warning("[lifespan] watchdog.stop() 超时或异常，强制继续")
        if _state.resource_reaper is not None:
            try:
                await asyncio.wait_for(_state.resource_reaper.stop(), timeout=5.0)
            except (asyncio.TimeoutError, Exception):
                logger.warning("[lifespan] resource_reaper.stop() 超时或异常，强制继续")
        # 取消所有运行中的导出任务（Task 21），避免残留 asyncio.Task
        try:
            from woc_bridge.routes.export import cancel_all_export_tasks_on_shutdown
            await asyncio.wait_for(
                cancel_all_export_tasks_on_shutdown(), timeout=5.0
            )
        except (asyncio.TimeoutError, Exception):
            logger.warning("[lifespan] export tasks cancel 超时或异常，强制继续")


def _init_state_from_env() -> None:
    """从环境变量初始化全局状态（uvicorn 启动兜底路径）。

    构造与 `python3 server.py` 等价的 BridgeConfig：命令行参数取默认值，
    其余字段由 BridgeConfig.from_args_and_env 从环境变量读取。
    """
    args = argparse.Namespace(
        listen=os.environ.get("WOC_BRIDGE_LISTEN", "0.0.0.0:8088"),
        display=os.environ.get("DISPLAY", ":1") or ":1",
        wechat_db=os.environ.get("WOC_WECHAT_DB", "/config"),
    )
    cfg = BridgeConfig.from_args_and_env(args)
    _init_state(cfg)


app = FastAPI(title="woc-bridge", version=BRIDGE_VERSION, lifespan=lifespan)


# ---------------------------------------------------------------------------
# 全局异常处理
# ---------------------------------------------------------------------------
@app.exception_handler(BridgeError)
async def bridge_error_handler(request: Request, exc: BridgeError) -> JSONResponse:
    """捕获 BridgeError，返回统一错误结构。

    对 RATE_LIMITED 错误额外设置 Retry-After 响应头，供客户端退避。
    """
    headers: dict[str, str] | None = None
    if exc.code == "RATE_LIMITED" and exc.details:
        retry_after = exc.details.get("retry_after")
        if isinstance(retry_after, int):
            headers = {"Retry-After": str(retry_after)}
    return JSONResponse(
        status_code=exc.http_status,
        content=ErrorResponse(
            success=False,
            error={
                "code": exc.code,
                "message": exc.message,
                "details": exc.details,
            },
        ).model_dump(),
        headers=headers,
    )


@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
    """捕获其他异常，返回 BRIDGE_INTERNAL_ERROR。

    日志里记完整异常信息便于排障；响应只回通用 message，不泄露内部细节。
    """
    logger.exception("unhandled exception on %s %s", request.method, request.url.path)
    return JSONResponse(
        status_code=500,
        content=ErrorResponse(
            success=False,
            error={
                "code": "BRIDGE_INTERNAL_ERROR",
                "message": "bridge internal error",
                "details": None,
            },
        ).model_dump(),
    )


# ---------------------------------------------------------------------------
# 请求/响应日志中间件
# ---------------------------------------------------------------------------
_SLOW_REQUEST_MS = 2000
_QUIET_ENTRY_PATHS: set[str] = set()


@app.middleware("http")
async def request_logging_middleware(request: Request, call_next):
    """统一 HTTP 请求/响应日志。

    入口：INFO 级别，打印 method + path + query（仅非 quiet 路径）
    出口：INFO 级别，打印 method + path + status_code + 耗时ms
        - 4xx/5xx 升级为 WARNING/ERROR
        - 超过 _SLOW_REQUEST_MS 升级为 WARNING（带 ⚠️ 标记）
        - 响应体含 error.code 时额外打印错误码与 message
    """
    method = request.method
    path = request.url.path
    query = str(request.url.query) if request.url.query else ""

    # 入口日志
    if path not in _QUIET_ENTRY_PATHS:
        if query:
            logger.info("→ %s %s?%s", method, path, query)
        else:
            logger.info("→ %s %s", method, path)

    start = time.perf_counter()

    try:
        response = await call_next(request)
    except Exception as exc:
        # 未被异常处理器捕获的极端情况
        elapsed_ms = (time.perf_counter() - start) * 1000
        logger.error(
            "← %s %s → EXCEPTION %s %.1fms: %s",
            method, path, type(exc).__name__, elapsed_ms, exc,
        )
        raise

    elapsed_ms = (time.perf_counter() - start) * 1000

    # 出口日志级别按状态码分级
    status = response.status_code
    if status >= 500:
        log_level = logging.ERROR
    elif status >= 400:
        log_level = logging.WARNING
    else:
        log_level = logging.INFO

    # 慢请求升级为 WARNING
    slow_marker = ""
    if elapsed_ms > _SLOW_REQUEST_MS and log_level == logging.INFO:
        log_level = logging.WARNING
        slow_marker = " ⚠️ slow"

    logger.log(
        log_level,
        "← %s %s → %d %.1fms%s",
        method, path, status, elapsed_ms, slow_marker,
    )

    return response


# ---------------------------------------------------------------------------
# 路由注册
# ---------------------------------------------------------------------------
app.include_router(routes_status.router)
app.include_router(routes_db.router)
app.include_router(routes_messages.router)
app.include_router(routes_send.router)
app.include_router(routes_login.router)
app.include_router(routes_contacts.router)
app.include_router(routes_moments.router)
app.include_router(routes_media.router)
app.include_router(routes_diagnostic.router)
app.include_router(routes_screenshot.router)
app.include_router(routes_export.router)
app.include_router(routes_batch.router)

# P3：Prometheus /metrics 端点
try:
    from prometheus_client import make_asgi_app
    app.mount("/metrics", make_asgi_app())
    logger.info("[init] /metrics endpoint mounted")
except ImportError:
    logger.warning("[init] prometheus_client not available, /metrics disabled")


# ---------------------------------------------------------------------------
# 状态初始化
# ---------------------------------------------------------------------------
def _detect_display_resolution(display: str) -> tuple[int, int]:
    """通过 xdotool getdisplaygeometry 检测真实分辨率。

    失败时回退到 1920x1080（保守默认，兼容旧 profile）。
    """
    import subprocess
    try:
        result = subprocess.run(
            ["xdotool", "getdisplaygeometry"],
            capture_output=True, text=True, timeout=3.0,
            env={**os.environ, "DISPLAY": display},
        )
        if result.returncode == 0:
            parts = result.stdout.strip().split()
            if len(parts) == 2:
                w, h = int(parts[0]), int(parts[1])
                if w > 0 and h > 0:
                    return (w, h)
    except Exception as e:
        logger.warning("[app] 检测分辨率失败，回退 1920x1080: %s", e)
    return (1920, 1080)


def _init_state(cfg: BridgeConfig) -> None:
    """根据 BridgeConfig 初始化全局状态（运行期实例）。"""
    _state.config = cfg
    _state.xdotool = XdotoolDriver(display=cfg.display)
    # DbReader 共享 AppState 中的 key_cache（单一密钥来源）
    _state.db_reader = DbReader(db_root=cfg.wechat_db, key_cache=_state.key_cache)
    _state.qr_capture = QrCapture(display=cfg.display)

    _state.send_queue = UIActionScheduler(
        send_delay_ms=cfg.send_delay_ms,
        max_calls_per_sec=cfg.max_calls_per_sec,
        max_queue_size=cfg.max_queue_size,
    )

    # SSE 发送结果校验总线：send_text 发送后注册等待，
    # MessageStreamer 广播消息时 notify 匹配 talker + content。
    # 替代 DB 轮询校验，避免熔断器雪崩。
    from woc_bridge.messaging.verify_bus import SSEVerifyBus
    _state.verify_bus = SSEVerifyBus(default_timeout_sec=2.5)

    # 消息流推送器：内部轮询 DB + SSE 广播（lifespan 中 start/stop）
    # 注入 verify_bus：广播 messages 事件时 notify 匹配 pending 的发送等待
    _state.message_streamer = MessageStreamer(
        db_reader=_state.db_reader,
        resolve_db_state=_resolve_db_state_tuple,
        extract_key=lambda: _auto_extract_with_lock(force=True),
        verify_bus=_state.verify_bus,
    )

    # 读取 WOC_DB_KEY 环境变量，注入 DbReader
    # 用 set_default_key 而非 set_key：保留 woc-keys.json 已持久化的多 salt 映射，
    # env key 仅作为默认兜底，首次查询时按 salt 验证后入库
    # db_reader.set_default_key 已转发到 key_cache.set_default_key，无需重复调用
    if cfg.db_key:
        _state.db_reader.set_default_key(cfg.db_key)
        logger.info("从 WOC_DB_KEY 环境变量加载默认 DB 密钥（未验证，首次查询时验证）")

    # P3：UI 自动化新架构组件（ui_backend != "legacy" 时构造完整能力；
    # legacy 模式仍构造最小 LocatorRegistry + Actions，使 ContactDriver 能使用
    # YAML profile 的几何兜底，改善好友申请通过等硬编码坐标准确性）。
    if cfg.ui_backend != "legacy":
        try:
            from woc_bridge.ui.backends import XdotoolBackend, OpenCVBackend
            from woc_bridge.ui.locators.registry import LocatorRegistry
            from woc_bridge.ui.actions import Actions
            from woc_bridge.ui.idem_cache import IdemCache
            from woc_bridge.ui.orchestrator import FlowOrchestrator
            from woc_bridge.ui.flows.send_file import SendFileFlow
            from woc_bridge.ui.capabilities import WeChatCapabilities
            from woc_bridge.ui.watchdog import WeChatWatchdog
            from woc_bridge.ui.resource_reaper import ResourceReaper

            # 构造后端
            _state.xdotool_backend = XdotoolBackend(display=cfg.display)
            template_dir = os.path.join(
                os.path.dirname(__file__), "ui", "profiles", "templates", "wechat_4.0", "light"
            )
            _state.opencv_backend = OpenCVBackend(template_dir=template_dir)

            # 构造 Locator + Actions
            profile_dir = os.path.join(os.path.dirname(__file__), "ui", "profiles")
            _state.locator = LocatorRegistry(profile_dir=profile_dir, opencv=_state.opencv_backend)

            # P0 修复：检测真实分辨率而非硬编码 1920x1080。
            # VNC 默认 1280x720，硬编码 1920x1080 会导致 1280x720 profile 永不加载，
            # 几何兜底 offset 偏差（如 verify_button 横向偏移 27px 可能点到按钮外）。
            real_resolution = _detect_display_resolution(cfg.display)
            _state.locator.load("4.0", real_resolution)
            logger.info(
                "[app] UI profile 加载: wechat_4.0 %s", real_resolution,
            )

            _state.actions = Actions(backend=_state.xdotool_backend, locator=_state.locator)

            # P0 修复：将 actions 注入 ContactDriver 与 MessageDriver，启用 OpenCV/
            # LocatorRegistry 优先策略。门面构造子 driver 时 actions 尚未构造，
            # 导致 _click_element_or_fallback / _open_session_by_name 永远走硬编码坐标。
            if _state.xdotool is not None and hasattr(_state.xdotool, 'set_actions'):
                _state.xdotool.set_actions(_state.actions)
            else:
                if _state.xdotool is not None and hasattr(_state.xdotool, '_contact'):
                    _state.xdotool._contact._actions = _state.actions
                if _state.xdotool is not None and hasattr(_state.xdotool, '_message'):
                    _state.xdotool._message._actions = _state.actions

            # 构造 SendFileFlow（注入 actions + xdotool + db_reader）
            # SendFileFlow 依赖 XdotoolDriver（用于 xdotool type 路径输入文件选择器），
            # orchestrator 未持有 XdotoolDriver 引用，必须在此处构造后注入
            send_file_flow = SendFileFlow(
                actions=_state.actions,
                xdotool=_state.xdotool,
                db_reader=_state.db_reader,
            )

            # 构造幂等缓存 + 编排器
            _state.idem_cache = IdemCache(ttl=300, max_size=1000)
            _state.orchestrator = FlowOrchestrator(
                send_queue=_state.send_queue,
                idem_cache=_state.idem_cache,
                actions=_state.actions,
                db_reader=_state.db_reader,
                config=cfg,
                send_file_flow=send_file_flow,
                verify_bus=_state.verify_bus,
            )

            # 构造 Capabilities
            _state.capabilities = WeChatCapabilities(
                orchestrator=_state.orchestrator,
                xdotool_driver=_state.xdotool,  # 保留旧驱动供 P1 兼容
            )

            # 构造群发后台协程（Task 13-16）
            # 注入 orchestrator（幂等/熔断/会话缓存）+ send_queue（串行化）+
            # db_reader（nickname 查询用于 {nickname} 个性化）+ config（风控参数）
            from woc_bridge.messaging.batch_worker import BatchSendWorker
            _state.batch_worker = BatchSendWorker(
                orchestrator=_state.orchestrator,
                send_queue=_state.send_queue,
                db_reader=_state.db_reader,
                config=cfg,
            )

            # 构造 HA 模块
            _state.watchdog = WeChatWatchdog(
                backend=_state.xdotool_backend,
                interval=10.0,
                scheduler=_state.send_queue,
            )
            _state.resource_reaper = ResourceReaper(
                debug_screenshot_dir="/tmp/woc_debug",
                max_age_hours=24,
                max_total_mb=100,
            )

            logger.info("[init] UI 新架构组件构造完成 (ui_backend=%s)", cfg.ui_backend)
        except Exception as exc:
            logger.error("[init] UI 新架构构造失败，回退 legacy: %s", exc)
            # 构造失败不阻塞启动，回退到 legacy
            # 清理已构造的部分组件，避免半初始化对象残留导致后续访问异常
            _state.orchestrator = None
            _state.capabilities = None
            _state.actions = None
            _state.locator = None
            _state.idem_cache = None
            _state.xdotool_backend = None
            _state.opencv_backend = None
            _state.watchdog = None
            _state.resource_reaper = None
            _state.batch_worker = None
    else:
        # legacy 模式：至少加载 YAML profile 并构造无 OpenCV 的 Actions，
        # 让 ContactDriver.accept_friend_request 使用 profile 几何兜底而非硬编码坐标。
        try:
            from woc_bridge.ui.backends import XdotoolBackend
            from woc_bridge.ui.locators.registry import LocatorRegistry
            from woc_bridge.ui.actions import Actions

            _state.xdotool_backend = XdotoolBackend(display=cfg.display)
            profile_dir = os.path.join(os.path.dirname(__file__), "ui", "profiles")
            _state.locator = LocatorRegistry(profile_dir=profile_dir, opencv=None)
            real_resolution = _detect_display_resolution(cfg.display)
            _state.locator.load("4.0", real_resolution)
            _state.actions = Actions(backend=_state.xdotool_backend, locator=_state.locator)
            if _state.xdotool is not None and hasattr(_state.xdotool, 'set_actions'):
                _state.xdotool.set_actions(_state.actions)
            else:
                if _state.xdotool is not None and hasattr(_state.xdotool, '_contact'):
                    _state.xdotool._contact._actions = _state.actions
                if _state.xdotool is not None and hasattr(_state.xdotool, '_message'):
                    _state.xdotool._message._actions = _state.actions
            logger.info(
                "[init] legacy 模式已加载 UI profile 几何兜底: wechat_4.0 %s",
                real_resolution,
            )
        except Exception as exc:
            logger.warning("[init] legacy 模式加载 UI profile 失败: %s", exc)
            _state.actions = None
            _state.locator = None
            _state.xdotool_backend = None

    # 好友自动通过组件（不依赖 P3 UI 架构，独立构造）
    try:
        from woc_bridge.models import AcceptRuleConfig, AcceptRuleEngine
        from woc_bridge.messaging.friend_watcher import FriendRequestWatcher
        from woc_bridge.messaging.login_guard import LoginGuard
        from woc_bridge.ui.circuit_breaker import CircuitBreaker
        from woc_bridge.ui.idem_cache import IdemCache

        accept_config = AcceptRuleConfig(
            enabled=cfg.auto_accept_enabled,
            accept_all=cfg.auto_accept_all,
            whitelist_wxids=cfg.auto_accept_whitelist_wxids,
            whitelist_nicknames=cfg.auto_accept_whitelist_nicknames,
            keywords=cfg.auto_accept_keywords,
            blacklist_wxids=cfg.auto_accept_blacklist_wxids,
            blacklist_nicknames=cfg.auto_accept_blacklist_nicknames,
            allow_scenes=cfg.auto_accept_allow_scenes,
        )
        _state.rule_engine = AcceptRuleEngine(accept_config)

        # 熔断器（failure_threshold=5, recovery_timeout=60s，比 send_text 的 30s 更长）
        _state.accept_breaker = CircuitBreaker(
            "accept_verify", failure_threshold=5, recovery_timeout=60.0,
        )

        # 幂等缓存：独立构造，避免 ui_backend=legacy 时 _state.idem_cache 为 None
        # （好友自动通过不依赖 P3 UI 架构，自持 IdemCache 实例）
        accept_idem_cache = IdemCache(ttl=300, max_size=1000)

        # 登录守卫：后台检测微信登录状态，供 friend_watcher 做登录前提保护
        # 未登录时 watcher 跳过轮询（不推进游标），登录后自然从上次位置继续
        _state.login_guard = LoginGuard(
            xdotool_driver=_state.xdotool,
            check_interval=5.0,
        )

        # 好友申请监听器（注入 7 个依赖，含 login_guard）
        _state.friend_watcher = FriendRequestWatcher(
            db_reader=_state.db_reader,
            rule_engine=_state.rule_engine,
            send_queue=_state.send_queue,
            idem_cache=accept_idem_cache,
            xdotool_driver=_state.xdotool,
            breaker=_state.accept_breaker,
            poll_interval=cfg.auto_accept_poll_interval,
            login_guard=_state.login_guard,
        )

        logger.info("[init] 好友自动通过组件构造完成 (enabled=%s)", cfg.auto_accept_enabled)
    except Exception as exc:
        logger.error("[init] 好友自动通过组件构造失败: %s", exc)
        # 构造失败不阻塞启动，相关 API 会返回 BRIDGE_INTERNAL_ERROR
        _state.rule_engine = None
        _state.friend_watcher = None
        _state.accept_breaker = None
        _state.login_guard = None
