"""L4 SendTextFlow：发送文本消息状态机。

8 个 transition：activate → click_search_box → type_query → open_session →
focus_input → type_content → send → cleanup。

会话缓存命中时跳过前 4 步，仅做 _verify_session_still_open。
异常时调 _reset_to_idle 恢复 UI 状态。

发送结果校验：通过 SSEVerifyBus 注册等待，MessageStreamer 广播消息时
notify 匹配 talker + content。无 DB 轮询，无熔断器，无雪崩风险。
"""

from __future__ import annotations

import asyncio
import logging
import random
import time
from typing import Optional

from woc_bridge.ui.backends.base import WindowGeometry
from woc_bridge.ui.errors import FlowError, PermanentError
from woc_bridge.ui.flow import (
    Flow,
    FlowContext,
    FlowResult,
    FlowState,
    _FLOW_TIMEOUT_SEC,
)

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

# 微信窗口标题
_WECHAT_WINDOW_TITLE = "微信"


class SendTextFlow(Flow):
    """发送文本消息 Flow。"""

    def __init__(self, actions, db_reader=None, session_cache=None,
                 verify_bus=None) -> None:
        """初始化 SendTextFlow。

        Args:
            actions: L3 Actions 实例
            db_reader: 保留参数兼容性（不再用于 verify，仅供 base 类）
            session_cache: 会话缓存（SessionCache 实例）
            verify_bus: SSE 发送结果校验总线（SSEVerifyBus 实例）。
                        None 时 verified 永远为 False（不阻塞发送）。
        """
        super().__init__(actions, db_reader)
        self.session_cache = session_cache
        self.verify_bus = verify_bus

    async def run(self, ctx: FlowContext) -> FlowResult:
        """执行发送文本流程。"""
        started_at = time.monotonic()

        logger.info(
            "[flow:send_text] START to_wxid=%s display_name=%s content_len=%d client_request_id=%s",
            ctx.to_wxid, ctx.display_name, len(ctx.content), ctx.client_request_id,
        )
        try:
            async with asyncio.timeout(_FLOW_TIMEOUT_SEC):
                # 检查会话缓存（用 to_wxid 做 key，display_name 非唯一）
                cache_hit = False
                if self.session_cache is not None:
                    cache_hit = self.session_cache.get(ctx.to_wxid)
                    if cache_hit:
                        logger.info(
                            "[flow:send_text] session cache hit for %s",
                            ctx.to_wxid,
                        )

                if cache_hit:
                    # 跳过 click_search_box ~ open_session，但仍需激活窗口
                    # （30s 内用户可能切到其他应用，微信失去焦点）
                    t0 = time.perf_counter()
                    # 仅激活窗口 + 获取几何，跳过 Esc 清场（会话已打开）
                    await self.actions.backend.activate_window(_WECHAT_WINDOW_TITLE)
                    ctx.win_geom = self._ensure_valid_geometry(
                        await self.actions.backend.get_window_geometry(
                            _WECHAT_WINDOW_TITLE
                        )
                    )
                    # 校验会话仍打开；失败时回退到完整流程，不直接返回失败
                    try:
                        await self._verify_session_still_open(ctx)
                        ctx.current_state = FlowState.SESSION_OPENED
                        logger.info(
                            "[flow:send_text] cache_hit branch done geom=%dx%d (%.0fms)",
                            ctx.win_geom.width, ctx.win_geom.height,
                            (time.perf_counter() - t0) * 1000,
                        )
                    except PermanentError as verify_exc:
                        logger.info(
                            "[flow:send_text] cache_hit verify failed, fallback to full flow: %s (%.0fms)",
                            verify_exc, (time.perf_counter() - t0) * 1000,
                        )
                        # 失效缓存，走完整流程
                        if self.session_cache is not None:
                            self.session_cache.invalidate(ctx.to_wxid)
                        cache_hit = False

                if not cache_hit:
                    # 完整流程
                    await self._activate(ctx)
                    await self._click_search_box(ctx)
                    await self._type_query(ctx)
                    await self._open_session(ctx)

                await self._focus_input(ctx)
                await self._type_content(ctx)
                await self._send(ctx)
                await self._cleanup(ctx)

                # 成功：更新会话缓存（用 to_wxid 做 key）
                # SSE verify 通过才缓存，避免发错会话被缓存
                if self.session_cache is not None and ctx.verified:
                    self.session_cache.set(ctx.to_wxid)

                logger.info(
                    "[flow:send_text] DONE success=%s verified=%s local_id=%s state=%s (%.0fms)",
                    True, ctx.verified, ctx.local_send_id, ctx.current_state,
                    (time.monotonic() - started_at) * 1000,
                )

                return FlowResult(
                    success=True,
                    local_id=ctx.local_send_id,
                    verified=ctx.verified,
                    state=ctx.current_state,
                    image_confidence=ctx.image_confidence,
                    duration_ms=(time.monotonic() - started_at) * 1000,
                )

        except (FlowError, asyncio.TimeoutError) as exc:
            logger.warning(
                "[flow:send_text] failed at state=%s: %s: %s",
                ctx.current_state, type(exc).__name__, exc,
            )
            # 尝试恢复
            try:
                await self._reset_to_idle(ctx.win_geom)
            except Exception as reset_exc:
                logger.warning(
                    "[flow:send_text] reset_to_idle failed: %s", reset_exc
                )
            # 失败时仅失效当前联系人会话缓存
            if self.session_cache is not None:
                self.session_cache.invalidate(ctx.to_wxid)

            error_code = "SEND_FAILED"
            error_msg = str(exc)
            if isinstance(exc, FlowError) and exc.code:
                error_code = exc.code
            elif isinstance(exc, asyncio.TimeoutError):
                error_code = "SEND_TIMEOUT"

            return FlowResult(
                success=False,
                local_id=ctx.local_send_id,
                verified=False,
                error=error_msg,
                error_code=error_code,
                state=FlowState.FAILED,
                duration_ms=(time.monotonic() - started_at) * 1000,
            )
        except Exception as exc:
            logger.exception("[flow:send_text] unexpected exception")
            try:
                await self._reset_to_idle(ctx.win_geom)
            except Exception:
                pass
            if self.session_cache is not None:
                self.session_cache.invalidate(ctx.to_wxid)
            return FlowResult(
                success=False,
                error=str(exc),
                error_code="BRIDGE_INTERNAL_ERROR",
                state=FlowState.FAILED,
                duration_ms=(time.monotonic() - started_at) * 1000,
            )

    # ------------------------------------------------------------------
    # Transitions
    # ------------------------------------------------------------------
    async def _activate(self, ctx: FlowContext) -> None:
        """激活窗口 + Esc 清场（关闭残留搜索框/弹窗）。"""
        t0 = time.perf_counter()
        logger.info("[flow:send_text] step=activate start")
        await self.actions.backend.activate_window(_WECHAT_WINDOW_TITLE)
        ctx.win_geom = self._ensure_valid_geometry(
            await self.actions.backend.get_window_geometry(_WECHAT_WINDOW_TITLE)
        )
        # 点击 session_manager_icon 强制回到会话列表首页，清除搜索覆盖层
        await self.actions.click_element("session_manager_icon", ctx.win_geom)
        # Esc 关闭可能的弹窗/搜索框
        await self.actions.backend.key_press("Escape")
        await asyncio.sleep(0.3)
        ctx.current_state = FlowState.WINDOW_ACTIVATED
        logger.info(
            "[flow:send_text] activated window geom=%dx%d (%.0fms)",
            ctx.win_geom.width, ctx.win_geom.height,
            (time.perf_counter() - t0) * 1000,
        )

    async def _click_search_box(self, ctx: FlowContext) -> None:
        """点击搜索框。"""
        t0 = time.perf_counter()
        logger.info("[flow:send_text] step=search_box click start")
        await self.actions.click_element("search_box", ctx.win_geom)
        ctx.current_state = FlowState.SEARCH_BOX_CLICKED
        logger.info(
            "[flow:send_text] step=search_box click done (%.0fms)",
            (time.perf_counter() - t0) * 1000,
        )

    async def _type_query(self, ctx: FlowContext) -> None:
        """输入搜索查询（display_name）。"""
        t0 = time.perf_counter()
        logger.info(
            "[flow:send_text] step=type_query start display_name=%r",
            ctx.display_name,
        )
        # 清空可能的残留：Ctrl+A 全选 + BackSpace 删除，比 BackSpace×30 更可靠
        await self.actions.backend.key_press("ctrl+a")
        await self.actions.backend.key_press("BackSpace")
        await asyncio.sleep(0.2)
        await self.actions.backend.type_text(ctx.display_name)
        ctx.current_state = FlowState.QUERY_TYPED
        # 纯坐标模式下无法用 OpenCV 验证搜索结果是否渲染，
        # 改为固定等待 0.5s 让微信渲染搜索结果列表。
        await asyncio.sleep(0.5)
        logger.info(
            "[flow:send_text] step=type_query done (fixed wait 0.5s, %.0fms)",
            (time.perf_counter() - t0) * 1000,
        )

    async def _open_session(self, ctx: FlowContext) -> None:
        """按 Enter 打开搜索结果第一项（比点击坐标更可靠）。"""
        t0 = time.perf_counter()
        logger.info(
            "[flow:send_text] step=open_session press Enter to open first search result"
        )
        # 微信 4.x 搜索框输入 display_name 后，直接按 Enter 即可打开第一项搜索结果。
        # 这比 click search_result_first 的固定坐标更稳定，不受"搜索网络结果" /
        # "最近在搜" / 联系人结果排序变化的影响。
        await self.actions.backend.key_press("Return")
        ctx.current_state = FlowState.SESSION_OPENED
        # 等待会话切换完成、输入框自动聚焦。
        await asyncio.sleep(0.5)
        logger.info(
            "[flow:send_text] step=open_session done (fixed wait 0.5s, %.0fms)",
            (time.perf_counter() - t0) * 1000,
        )

    async def _focus_input(self, ctx: FlowContext) -> None:
        """聚焦输入框。"""
        t0 = time.perf_counter()
        logger.info("[flow:send_text] step=focus_input click input_box start")
        await self.actions.click_element("input_box", ctx.win_geom)
        ctx.current_state = FlowState.INPUT_FOCUSED
        await asyncio.sleep(0.1)
        logger.info(
            "[flow:send_text] step=focus_input done (%.0fms)",
            (time.perf_counter() - t0) * 1000,
        )

    async def _type_content(self, ctx: FlowContext) -> None:
        """输入消息内容。"""
        t0 = time.perf_counter()
        # 1. 清理首尾空白
        cleaned_content = ctx.content.strip()
        # 2. 把换行符（含 \r\n / \n / \r）替换为空格，避免 xdotool type 把 \n
        #    解析为 Return 键，导致消息在输入过程中被提前分段发送。
        #    微信输入框本身支持 Shift+Enter 换行，但 xdotool type 无法区分。
        normalized_content = cleaned_content.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
        # 3. 合并连续空格为一个，保持内容整洁
        normalized_content = " ".join(normalized_content.split())
        if normalized_content != ctx.content:
            logger.info(
                "[flow:send_text] step=type_content normalized: "
                "original_len=%d cleaned_len=%d",
                len(ctx.content), len(normalized_content),
            )
        ctx.content = normalized_content
        logger.info(
            "[flow:send_text] step=type_content start content_len=%d",
            len(ctx.content),
        )
        # 4. 输入前清空输入框，避免上次残留内容与本次内容混合
        await self.actions.backend.key_press("ctrl+a")
        await self.actions.backend.key_press("Delete")
        await asyncio.sleep(0.05)
        await self.actions.backend.type_text(ctx.content)
        ctx.current_state = FlowState.TEXT_TYPED
        logger.info(
            "[flow:send_text] step=type_content done (%.0fms)",
            (time.perf_counter() - t0) * 1000,
        )

    async def _send(self, ctx: FlowContext) -> None:
        """触发发送 + SSE 校验。

        纯坐标模式（WOC_DISABLE_OPENCV=1）下用 Enter 键发送，
        避免发送按钮坐标偏移导致点空。微信 4.x 默认 Enter 即发送。
        OpenCV 模式保留点击 send_button（坐标+模板双重保障）。

        关键：register 必须在 send 之前，否则存在竞态条件：
        - T0: 按 Enter 发送
        - T0+0.05: 微信写 DB
        - T0+0.05~T0+1.0: MessageStreamer 轮询 + 广播 + notify（无 waiter → 丢失）
        - T0+0.1: register（已晚于 notify）
        先注册 future，发送后等待 notify 匹配，超时（2.5s）未匹配则
        verified=False，不阻塞发送成功语义（success=True），调用方按需重试。
        """
        t0 = time.perf_counter()
        from woc_bridge.ui.actions import _DISABLE_OPENCV

        # 1. 先注册 SSE 校验等待，避免 send 后 broadcast 已发生的竞态
        verify_future = None
        if self.verify_bus is not None:
            verify_future = self.verify_bus.register(
                to_wxid=ctx.to_wxid,
                content=ctx.content,
            )

        # 2. 触发发送
        if _DISABLE_OPENCV:
            logger.info("[flow:send_text] step=send press Enter start")
            # 短暂等待输入框聚焦稳定，避免 type_text 后焦点未完全落位
            await asyncio.sleep(0.1)
            await self.actions.backend.key_press("Return")
            logger.info("[flow:send_text] step=send press Enter done")
        else:
            logger.info("[flow:send_text] step=send click send_button start")
            await self.actions.click_element("send_button", ctx.win_geom)
        ctx.current_state = FlowState.SENT
        ctx.local_send_id = f"local_{int(time.time())}_{random.randint(1000,9999)}"

        # 3. 等待 SSE 校验结果
        ctx.verified = await self._wait_verify(verify_future, ctx)
        logger.info(
            "[flow:send_text] step=send done local_id=%s verified=%s (%.0fms)",
            ctx.local_send_id, ctx.verified,
            (time.perf_counter() - t0) * 1000,
        )

    async def _cleanup(self, ctx: FlowContext) -> None:
        """清理：Esc。"""
        t0 = time.perf_counter()
        logger.info("[flow:send_text] step=cleanup Esc start")
        await self.actions.backend.key_press("Escape")
        await asyncio.sleep(0.2)
        ctx.current_state = FlowState.CLEANED_UP
        logger.info(
            "[flow:send_text] step=cleanup done (%.0fms)",
            (time.perf_counter() - t0) * 1000,
        )

    # ------------------------------------------------------------------
    # 校验：SSE 推送匹配
    # ------------------------------------------------------------------
    async def _wait_verify(self, future: Optional[asyncio.Future], ctx: FlowContext) -> bool:
        """等待 SSEVerifyBus 的 future 完成。

        future 由 _send 在触发发送前 register，确保 MessageStreamer
        广播消息时 notify 能匹配到 waiter。匹配到返回 True，超时返回 False。

        无 DB 轮询、无截图校验、无熔断器，避免雪崩。
        future 为 None 时（verify_bus 未注入）直接返回 False，不阻塞发送。
        """
        if future is None:
            logger.warning(
                "[flow:send_text] no verify_bus, skip SSE verify (verified=False)"
            )
            return False
        t0 = time.perf_counter()
        try:
            result = await self.verify_bus.wait(future)
            elapsed_ms = (time.perf_counter() - t0) * 1000
            if result:
                logger.info(
                    "[flow:send_text] SSE verify OK (talker=%s content matched, %.0fms)",
                    ctx.to_wxid, elapsed_ms,
                )
            else:
                logger.warning(
                    "[flow:send_text] SSE verify timeout (talker=%s no matching broadcast, %.0fms)",
                    ctx.to_wxid, elapsed_ms,
                )
            return result
        except Exception as exc:
            logger.warning(
                "[flow:send_text] SSE verify exception: %s (%.0fms)",
                exc, (time.perf_counter() - t0) * 1000,
            )
            return False

    async def _verify_session_still_open(self, ctx: FlowContext) -> None:
        """会话缓存命中时校验会话仍打开。

        纯坐标模式（WOC_DISABLE_OPENCV=1）下 wait_for("input_box") 纯算术
        返回，无法真正校验 UI 状态。此时改用 is_window_active 校验窗口焦点：
        若微信窗口已失焦（用户切到其他应用），cache 失效走完整流程。
        会话是否真的打开仍靠后续 SSE verify 兜底（content 不匹配则 invalidate）。

        OpenCV 模式下保留原逻辑（wait_for input_box 模板匹配）。

        异常处理：get_window_geometry 的 RuntimeError（窗口消失）也转为
        PermanentError，触发 cache_hit 回退到完整流程，避免冒泡为
        BRIDGE_INTERNAL_ERROR。
        """
        t0 = time.perf_counter()
        # 获取窗口几何（缓存命中时未走 _activate），并校验/窄化类型
        try:
            ctx.win_geom = self._ensure_valid_geometry(
                await self.actions.backend.get_window_geometry(_WECHAT_WINDOW_TITLE)
            )
        except (RuntimeError, PermanentError) as exc:
            logger.warning(
                "[flow:send_text] session_still_open FAIL: get_window_geometry failed: %s (%.0fms)",
                exc, (time.perf_counter() - t0) * 1000,
            )
            raise PermanentError(
                "WINDOW_NOT_FOUND",
                f"session cache verify failed: window not found: {exc}",
            )
        from woc_bridge.ui.actions import _DISABLE_OPENCV

        if _DISABLE_OPENCV:
            # 纯坐标模式：校验窗口焦点，避免命中已失焦的会话
            is_active = await self.actions.backend.is_window_active(
                _WECHAT_WINDOW_TITLE
            )
            elapsed_ms = (time.perf_counter() - t0) * 1000
            if not is_active:
                logger.warning(
                    "[flow:send_text] session_still_open FAIL: window not active (%.0fms)",
                    elapsed_ms,
                )
                raise PermanentError(
                    "ELEMENT_NOT_FOUND",
                    "session cache verify failed: window not active",
                )
            logger.info(
                "[flow:send_text] session_still_open OK (window active, %.0fms)",
                elapsed_ms,
            )
            return

        # OpenCV 模式：用 input_box 模板匹配校验
        try:
            elem = await self.actions.wait_for(
                "input_box", ctx.win_geom, timeout=2.0, interval=0.3
            )
            if elem is None:
                logger.warning(
                    "[flow:send_text] session_still_open FAIL: input_box not found (%.0fms)",
                    (time.perf_counter() - t0) * 1000,
                )
                raise PermanentError(
                    "ELEMENT_NOT_FOUND",
                    "session cache verify failed: input_box not found",
                )
            logger.info(
                "[flow:send_text] session_still_open OK (%.0fms)",
                (time.perf_counter() - t0) * 1000,
            )
        except PermanentError:
            raise
        except Exception as exc:
            logger.warning(
                "[flow:send_text] session_still_open exception: %s (%.0fms)",
                exc, (time.perf_counter() - t0) * 1000,
            )
            raise PermanentError(
                "ELEMENT_NOT_FOUND",
                f"session cache verify failed: {exc}",
            )
