"""好友申请监听器：轮询 fmessage 系统消息，解析后触发自动通过。

独立后台协程，不侵入 MessageStreamer 架构。复用其 mtime 感知 +
复合游标 (create_time, local_id) 增量检测模式，但独立游标与频率控制。

auto_accept 关闭时挂起在 asyncio.Event 上，不轮询 DB。
DB_ENCRYPTED 时 _init_cursor 返回 False，每轮重试初始化。
"""

from __future__ import annotations

import asyncio
import logging
import time
from dataclasses import dataclass
from typing import Optional

from woc_bridge.models import AcceptDecision, BridgeError
from woc_bridge.models.contact import FriendRequestInfo
from woc_bridge.messaging.friend_parser import parse_friend_request

logger = logging.getLogger("woc-bridge")


class FriendRequestWatcher:
    """好友申请监听器：轮询 fmessage 系统消息，解析后触发自动通过。"""

    def __init__(
        self,
        db_reader,          # DbReader（避免类型注解循环 import）
        rule_engine,        # AcceptRuleEngine
        send_queue,         # SendQueue
        idem_cache,         # IdemCache
        xdotool_driver,     # XdotoolDriver
        breaker,            # CircuitBreaker
        poll_interval: float = 3.0,
        cursor_create_time: int = 0,
        cursor_local_id: int = 0,
        login_guard=None,   # LoginGuard（登录守卫，None 时跳过登录检查）
    ) -> None:
        self._db_reader = db_reader
        self._rule_engine = rule_engine
        self._send_queue = send_queue
        self._idem_cache = idem_cache
        self._xdotool = xdotool_driver
        self._breaker = breaker
        self._poll_interval = poll_interval
        self._login_guard = login_guard
        # 复合游标 (create_time, local_id)，与 get_friend_requests_since 返回值对齐
        self._cursor_create_time: int = cursor_create_time
        self._cursor_local_id: int = cursor_local_id
        self._last_db_mtime: float = 0.0
        self._last_wal_mtime: float = 0.0
        # contact.db 独立 mtime：好友申请可能只写入 contact.db 的 ticket_info，
        # 不触发 message_0.db 变化，必须单独感知
        self._last_contact_db_mtime: float = 0.0
        self._last_contact_wal_mtime: float = 0.0
        # ticket_info 增量游标：避免每次全量读取历史已通过/残留 ticket
        self._ticket_cursor_local_id: int = 0
        self._ticket_cursor_inited: bool = False
        self._watcher: Optional[asyncio.Task] = None
        self._enabled_event: asyncio.Event = asyncio.Event()  # auto_accept 开关事件
        self._cursor_inited: bool = False
        # 上轮满载标志：为 True 时跳过 mtime 一级过滤，避免批量积压（>50 条）时
        # 剩余批次因 DB mtime 未变被卡住
        self._has_more_pending: bool = False
        self._has_more_ticket_pending: bool = False

        # 状态统计（供 /api/friends/auto_accept/status 查询）
        self._processed_count: int = 0
        self._accepted_count: int = 0
        self._rejected_count: int = 0
        self._last_processed_time: Optional[int] = None
        # 短期去重：同一个 wxid 在 N 秒内不重复入队执行 UI 通过，
        # 避免已通过但 verify 校验超时/失败时被反复处理导致乱点
        self._recent_wxids: dict[str, float] = {}
        self._recent_wxid_ttl: float = 60.0

    @property
    def is_running(self) -> bool:
        return self._watcher is not None and not self._watcher.done()

    @property
    def is_enabled(self) -> bool:
        return self._enabled_event.is_set()

    @property
    def processed_count(self) -> int:
        return self._processed_count

    @property
    def accepted_count(self) -> int:
        return self._accepted_count

    @property
    def rejected_count(self) -> int:
        return self._rejected_count

    @property
    def last_processed_time(self) -> Optional[int]:
        return self._last_processed_time

    @property
    def cursor_create_time(self) -> int:
        return self._cursor_create_time

    @property
    def cursor_local_id(self) -> int:
        return self._cursor_local_id

    async def start(self) -> None:
        if self._watcher is None or self._watcher.done():
            self._watcher = asyncio.create_task(self._watch_loop())
            logger.info("FriendRequestWatcher 已启动")

    async def stop(self) -> None:
        if self._watcher is not None and not self._watcher.done():
            self._watcher.cancel()
            try:
                await self._watcher
            except asyncio.CancelledError:
                pass
        self._watcher = None

    def set_enabled(self, enabled: bool) -> None:
        if enabled:
            # 开启时重置游标，避免批量处理积压申请
            self._cursor_inited = False
            self._ticket_cursor_inited = False
            self._has_more_pending = False
            self._has_more_ticket_pending = False
            self._enabled_event.set()
        else:
            self._enabled_event.clear()
        logger.info("FriendRequestWatcher enabled=%s", enabled)

    async def _init_cursor(self) -> bool:
        """对齐游标到当前 fmessage 表最大 (create_time, local_id)。

        Returns:
            True 表示已对齐（或 DB 不可读但标记为已初始化，避免无限重试）
            False 表示异常，下一轮重试
        """
        try:
            max_cursor = await asyncio.to_thread(
                self._db_reader.get_max_create_time_for_talker, "fmessage"
            )
            if max_cursor is None:
                # DB 不可读（_ensure_decrypted 抛 BridgeError 已被吞掉返回 None）
                # 不标记 _cursor_inited，下一轮重试
                logger.warning("FriendRequestWatcher 游标初始化: DB 不可读，将在下轮重试")
                return False
            if isinstance(max_cursor, tuple) and max_cursor[0] > 0:
                self._cursor_create_time = max_cursor[0]
                self._cursor_local_id = max_cursor[1]
                logger.info(
                    "FriendRequestWatcher 游标对齐到 (create_time=%d, local_id=%d)（跳过历史申请）",
                    self._cursor_create_time, self._cursor_local_id,
                )
            # 无论 max_cursor 是否为 (0, 0)（空表），都标记为已初始化，避免空表时无限重试
            self._cursor_inited = True
            return True
        except Exception as e:
            logger.warning("FriendRequestWatcher 游标初始化失败: %s", e)
            return False

    async def _init_ticket_cursor(self) -> bool:
        """初始化 ticket_info 增量游标到当前最大 id，跳过历史数据。

        Returns:
            True 表示已初始化；None 时按 0 处理并标记成功。
        """
        try:
            max_id = await asyncio.to_thread(
                self._db_reader.get_max_ticket_info_id
            )
            if max_id is None:
                logger.warning(
                    "FriendRequestWatcher ticket 游标初始化: DB 不可读，将在下轮重试"
                )
                return False
            self._ticket_cursor_local_id = max_id
            self._ticket_cursor_inited = True
            logger.info(
                "FriendRequestWatcher ticket 游标对齐到 max_id=%d（跳过历史 ticket）",
                self._ticket_cursor_local_id,
            )
            return True
        except Exception as e:
            logger.warning("FriendRequestWatcher ticket 游标初始化失败: %s", e)
            return False

    async def _poll_once(self) -> None:
        # 1. mtime 感知（一级过滤，避免空 SQL）
        #    必须同时感知 message_0.db 与 contact.db：微信 4.x Linux 好友申请可能
        #    只写入 contact.db 的 ticket_info，不触发 message_0.db 变化。
        #    上轮满载时跳过过滤（仍有未读批次），否则会因 DB mtime 未变卡住剩余申请。
        msg_changed = False
        contact_changed = False

        mtimes = await asyncio.to_thread(
            self._db_reader.get_db_mtime, "message/message_0.db"
        )
        if mtimes is None:
            logger.debug("FriendRequestWatcher _poll_once: message_0.db mtime 不可读")
        else:
            db_mtime, wal_mtime = mtimes
            if (
                self._has_more_pending
                or db_mtime != self._last_db_mtime
                or wal_mtime != self._last_wal_mtime
            ):
                msg_changed = True
            self._last_db_mtime = db_mtime
            self._last_wal_mtime = wal_mtime

        mtimes_contact = await asyncio.to_thread(
            self._db_reader.get_db_mtime, "contact/contact.db"
        )
        if mtimes_contact is None:
            logger.debug("FriendRequestWatcher _poll_once: contact.db mtime 不可读")
        else:
            cdb_mtime, cwal_mtime = mtimes_contact
            if (
                self._has_more_ticket_pending
                or cdb_mtime != self._last_contact_db_mtime
                or cwal_mtime != self._last_contact_wal_mtime
            ):
                contact_changed = True
            self._last_contact_db_mtime = cdb_mtime
            self._last_contact_wal_mtime = cwal_mtime

        logger.debug(
            "FriendRequestWatcher _poll_once: 开始轮询 "
            "cursor=(%d,%d) msg_changed=%s contact_changed=%s "
            "last_msg_mtime=(%.3f,%.3f) last_contact_mtime=(%.3f,%.3f)",
            self._cursor_create_time, self._cursor_local_id,
            msg_changed, contact_changed,
            self._last_db_mtime, self._last_wal_mtime,
            self._last_contact_db_mtime, self._last_contact_wal_mtime,
        )
        if not msg_changed and not contact_changed:
            logger.debug(
                "FriendRequestWatcher _poll_once: message_0.db/contact.db mtime 均未变化，跳过本次轮询"
            )
            return

        # 2. 查询 fmessage 分片表增量消息（复合游标）
        result = await asyncio.to_thread(
            self._db_reader.get_friend_requests_since,
            self._cursor_create_time,
            self._cursor_local_id,
            50,  # limit
        )
        if result is None:
            # DB 不可读（_ensure_decrypted 抛 BridgeError）
            logger.warning(
                "FriendRequestWatcher _poll_once: get_friend_requests_since 返回 None（DB 不可读）"
            )
            return

        raw_items: list[dict] = list(result["requests"])

        # 2.1 fallback：微信 4.x Linux 好友申请可能仅存于 contact.db 的 ticket_info，
        #     fmessage 表可能为空。额外读取 ticket_info，通过 since_id 游标跳过历史数据。
        ticket_items = await asyncio.to_thread(
            self._db_reader.get_pending_requests_from_ticket_info,
            50,
            self._ticket_cursor_local_id,
        )
        if ticket_items:
            logger.info(
                "FriendRequestWatcher _poll_once: 从 ticket_info 读到 %d 条新申请 "
                "(since_id=%d)",
                len(ticket_items), self._ticket_cursor_local_id,
            )
            raw_items.extend(ticket_items)
            # 推进 ticket_info 游标到本批最大 id
            max_ticket_id = max(item["local_id"] for item in ticket_items)
            if max_ticket_id > self._ticket_cursor_local_id:
                self._ticket_cursor_local_id = max_ticket_id
        self._has_more_ticket_pending = len(ticket_items) >= 50

        logger.info(
            "FriendRequestWatcher _poll_once: 本轮检测到 %d 条原始申请 "
            "(cursor=%d,%d → next=%d,%d)",
            len(raw_items),
            self._cursor_create_time, self._cursor_local_id,
            result["next_create_time"], result["next_local_id"],
        )

        # 3. 解析 XML + 逐条处理
        parsed_count = 0
        for item in raw_items:
            info = parse_friend_request(
                item["content"], item["create_time"], item["local_id"]
            )
            if info is None:
                # 非 verifyUser 类型或解析失败，记录日志便于排查
                logger.debug(
                    "FriendRequestWatcher: 解析失败或非 verifyUser，content=%s",
                    repr(item["content"][:200]) if item.get("content") else "",
                )
                continue
            parsed_count += 1
            logger.info(
                "FriendRequestWatcher: 解析到申请 wxid=%s nickname=%s scene=%s",
                info.stranger_wxid, info.nickname, info.scene,
            )
            await self._handle_request(info)

        # 4. 推进复合游标（仅由 fmessage 结果决定；ticket_info 无时间戳，不参与游标）
        self._cursor_create_time = result["next_create_time"]
        self._cursor_local_id = result["next_local_id"]

        # 5. 满载标志：仅由 fmessage 本批是否达 limit 决定
        self._has_more_pending = len(result["requests"]) >= 50
        logger.debug(
            "FriendRequestWatcher _poll_once: 解析成功 %d/%d 条，满载=%s",
            parsed_count, len(raw_items), self._has_more_pending,
        )

    async def _watch_loop(self) -> None:
        while True:
            try:
                # auto_accept 关闭时挂起，避免空轮询
                await self._enabled_event.wait()

                # 登录守卫：微信未登录时跳过轮询，不推进游标
                # 登录后自然从上次游标位置重新读取，DB 中积累的申请不丢失
                if self._login_guard is not None and not self._login_guard.is_logged_in:
                    logger.info(
                        "FriendRequestWatcher: 微信未登录 (state=%s)，暂停轮询等待登录恢复",
                        self._login_guard.current_state,
                    )
                    # 等待登录恢复，带超时避免紧密循环
                    # 超时后 continue 回到 while 顶部重新检查状态
                    await self._login_guard.wait_for_login(
                        timeout=self._poll_interval
                    )
                    continue

                # 游标未初始化时先对齐（DB 恢复可读后自动补齐）
                # DB_ENCRYPTED 时 _init_cursor 返回 False，下一轮仍会重试
                if not self._cursor_inited:
                    await self._init_cursor()
                    if not self._cursor_inited:
                        # DB 仍不可读，本轮跳过 _poll_once
                        await asyncio.sleep(self._poll_interval)
                        continue

                # fmessage 游标初始化成功后，再初始化 ticket_info 游标
                if not self._ticket_cursor_inited:
                    await self._init_ticket_cursor()
                    if not self._ticket_cursor_inited:
                        await asyncio.sleep(self._poll_interval)
                        continue

                await asyncio.sleep(self._poll_interval)
                await self._poll_once()
            except asyncio.CancelledError:
                raise
            except Exception as e:
                logger.exception("FriendRequestWatcher 异常: %s", e)
                await asyncio.sleep(5.0)

    async def _verify_with_retry(self, stranger_wxid: str) -> bool:
        """UI 操作完成后轮询 DB 校验好友是否已通过。

        先等待 1.5s 让微信刷盘，之后每隔 1s 查询一次，最多 10s。
        期间任一次校验成功即返回 True；DB 异常视为未通过并继续重试。
        """
        await asyncio.sleep(1.5)
        deadline = time.monotonic() + 10.0
        attempt = 0
        while time.monotonic() < deadline:
            attempt += 1
            try:
                verified = await asyncio.to_thread(
                    self._db_reader.verify_friend_accepted, stranger_wxid
                )
                logger.info(
                    "FriendRequestWatcher: verify 尝试 #%d (wxid=%s) → verified=%s",
                    attempt, stranger_wxid, verified,
                )
                if verified:
                    return True
            except Exception as exc:
                logger.warning(
                    "FriendRequestWatcher: verify_friend_accepted 异常 #%d (wxid=%s): %s",
                    attempt, stranger_wxid, exc,
                )
            await asyncio.sleep(1.0)
        logger.warning(
            "FriendRequestWatcher: verify 超时 (wxid=%s, 尝试 %d 次)",
            stranger_wxid, attempt,
        )
        return False

    async def _handle_request(self, req: FriendRequestInfo) -> None:
        self._processed_count += 1
        self._last_processed_time = req.create_time

        # 步骤 1：收到申请，打印原始信息
        logger.info(
            "[自动通过][步骤1/8] 收到好友申请 wxid=%s nickname=%s scene=%s verify=%s",
            req.stranger_wxid,
            req.nickname,
            req.scene,
            repr(req.verify_message[:80]) if req.verify_message else "",
        )

        # 步骤 2：规则引擎决策
        logger.info("[自动通过][步骤2/8] 进入规则引擎评估 wxid=%s", req.stranger_wxid)
        decision = await self._rule_engine.evaluate(req)
        if decision == AcceptDecision.REJECT:
            self._rejected_count += 1
            logger.info(
                "[自动通过][步骤2/8] 决策结果=REJECT（拒绝），处理结束 wxid=%s",
                req.stranger_wxid,
            )
            return
        if decision != AcceptDecision.ACCEPT:
            logger.info(
                "[自动通过][步骤2/8] 决策结果=SKIP（跳过），处理结束 wxid=%s，"
                "原因：未命中任何通过规则（accept_all=false、无白名单/关键词匹配）",
                req.stranger_wxid,
            )
            return
        logger.info(
            "[自动通过][步骤2/8] 决策结果=ACCEPT（允许通过） wxid=%s",
            req.stranger_wxid,
        )

        # 步骤 2.5：前置 DB 检查，避免已经通过的好友被重复执行 UI
        logger.info("[自动通过][步骤2.5/8] 前置校验是否已是好友 wxid=%s", req.stranger_wxid)
        try:
            already_accepted = await asyncio.to_thread(
                self._db_reader.verify_friend_accepted, req.stranger_wxid
            )
        except Exception as exc:
            logger.warning(
                "[自动通过][步骤2.5/8] verify_friend_accepted 异常，按未通过继续 wxid=%s: %s",
                req.stranger_wxid, exc,
            )
            already_accepted = False
        if already_accepted:
            self._idem_cache.set(
                "friend_accept", req.stranger_wxid, "",
                {"success": True, "verified": True, "pre_check": True}, "",
            )
            self._accepted_count += 1
            logger.info(
                "[自动通过][步骤2.5/8] 已是好友，跳过 UI 操作并写入幂等缓存 wxid=%s",
                req.stranger_wxid,
            )
            return
        logger.info("[自动通过][步骤2.5/8] 尚未通过，继续执行 UI wxid=%s", req.stranger_wxid)

        # 步骤 3：幂等去重（TTL=300s，5 分钟内不重复处理同一申请人）
        logger.info("[自动通过][步骤3/8] 检查幂等缓存 wxid=%s", req.stranger_wxid)
        cached = self._idem_cache.get(
            "friend_accept", req.stranger_wxid, "", ""
        )
        if cached is not None:
            logger.info(
                "[自动通过][步骤3/8] 幂等命中，跳过 wxid=%s (cached=%s)",
                req.stranger_wxid, cached,
            )
            return

        # 步骤 3.5：短期去重，避免 verify 超时/失败后在短时间内重复入队乱点
        now = time.monotonic()
        last_at = self._recent_wxids.get(req.stranger_wxid)
        if last_at is not None and now - last_at < self._recent_wxid_ttl:
            logger.info(
                "[自动通过][步骤3.5/8] %.0fs 内已处理过，跳过 wxid=%s",
                self._recent_wxid_ttl, req.stranger_wxid,
            )
            return
        logger.info("[自动通过][步骤3/8] 幂等未命中，继续处理 wxid=%s", req.stranger_wxid)

        # 步骤 4：熔断检查
        logger.info("[自动通过][步骤4/8] 检查熔断器 wxid=%s", req.stranger_wxid)
        breaker_allowed = self._breaker.allow()
        breaker_state = self._breaker.state.value
        logger.info(
            "[自动通过][步骤4/8] 熔断器状态 allow=%s state=%s wxid=%s",
            breaker_allowed, breaker_state, req.stranger_wxid,
        )
        if not breaker_allowed:
            logger.warning(
                "[自动通过][步骤4/8] 熔断器 OPEN，跳过 wxid=%s",
                req.stranger_wxid,
            )
            return

        # 步骤 5/6：入队执行 UI 通过
        logger.info(
            "[自动通过][步骤5/8] 准备入队执行 UI 通过 wxid=%s",
            req.stranger_wxid,
        )
        # 记录本次处理时间，用于短期去重（即使后续 verify 失败也不立即重试）
        self._recent_wxids[req.stranger_wxid] = time.monotonic()
        try:
            result = await self._send_queue.enqueue(
                lambda req=req: self._xdotool.accept_friend_request(
                    stranger_wxid=req.stranger_wxid,
                    nickname=req.nickname,
                ),
                delay_ms=None,  # 使用 send_queue 默认间隔
            )

            logger.info(
                "[自动通过][步骤6/8] UI 操作完成 result=%s，开始 DB 校验 wxid=%s",
                result, req.stranger_wxid,
            )

            # 步骤 7：等待微信 DB WAL 刷盘后校验
            verified = await self._verify_with_retry(req.stranger_wxid)

            # 步骤 8：结果处理
            if verified:
                # 只有真正通过并验证成功才写幂等缓存，避免坐标不准等临时失败
                # 导致该申请人 5 分钟内无法再次处理
                self._idem_cache.set(
                    "friend_accept", req.stranger_wxid, "",
                    {"success": True, "verified": True}, "",
                )
                self._breaker.record_success()
                self._accepted_count += 1
                logger.info(
                    "[自动通过][步骤8/8] 通过并验证成功 wxid=%s nickname=%s",
                    req.stranger_wxid, req.nickname,
                )
            else:
                # UI 操作完成但 DB 校验未通过（可能有延迟），不写入幂等缓存，
                # 下轮仍可尝试；仅记录熔断器失败。
                self._breaker.record_failure()
                logger.warning(
                    "[自动通过][步骤8/8] UI 操作完成但 DB 校验未通过，"
                    "不写入幂等缓存，允许下轮重试 wxid=%s",
                    req.stranger_wxid,
                )
        except BridgeError as e:
            # 透传 BridgeError 错误码（RATE_LIMITED / SEND_FAILED / WINDOW_NOT_FOUND 等）
            self._breaker.record_failure()
            logger.error(
                "[自动通过][步骤6/8] BridgeError code=%s: %s wxid=%s",
                getattr(e, "code", "UNKNOWN"), e, req.stranger_wxid,
            )
        except Exception as e:
            self._breaker.record_failure()
            logger.error(
                "[自动通过][步骤6/8] 异常: %s wxid=%s",
                e, req.stranger_wxid,
            )
