"""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 状态。
"""

from __future__ import annotations

import asyncio
import logging
import random
import time
from typing import Awaitable, Callable, Optional

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 = "微信"

# DB 校验参数
_DB_VERIFY_TIMEOUT_SEC = 3.0
_DB_VERIFY_INTERVAL_SEC = 0.2


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

    def __init__(self, actions, db_reader=None, session_cache=None) -> None:
        super().__init__(actions, db_reader)
        self.session_cache = session_cache
        # 基线消息的 local_id，与 before_msg_id（create_time）配合用于 DB 游标查询
        self._before_msg_local_id: int = 0

    async def run(self, ctx: FlowContext) -> FlowResult:
        """执行发送文本流程。"""
        started_at = time.monotonic()
        # 后台 task：获取 before_msg_id（与 _activate 并行）
        before_msg_task: Optional[asyncio.Task] = None

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

                if cache_hit:
                    # 跳过 click_search_box ~ open_session，但仍需激活窗口
                    # （30s 内用户可能切到其他应用，微信失去焦点）
                    before_msg_task = asyncio.create_task(
                        self._get_latest_create_time(ctx.to_wxid)
                    )
                    # 仅激活窗口 + 获取几何，跳过 Esc 清场（会话已打开）
                    await self.actions.backend.activate_window(_WECHAT_WINDOW_TITLE)
                    ctx.win_geom = await self.actions.backend.get_window_geometry(
                        _WECHAT_WINDOW_TITLE
                    )
                    await self._verify_session_still_open(ctx)
                    ctx.current_state = FlowState.SESSION_OPENED
                else:
                    # 完整流程
                    await self._activate(ctx)
                    # 并行启动 before_msg_id 获取
                    before_msg_task = asyncio.create_task(
                        self._get_latest_create_time(ctx.to_wxid)
                    )
                    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)
                # 等待 before_msg_task 完成（如果已启动）
                if before_msg_task is not None:
                    ct, lid = await before_msg_task
                    ctx.before_msg_id = ct
                    self._before_msg_local_id = lid
                await self._send(ctx)
                await self._cleanup(ctx)

                # 成功：更新会话缓存（用 to_wxid 做 key）
                if self.session_cache is not None and ctx.verified:
                    self.session_cache.set(ctx.to_wxid)

                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",
                ctx.current_state, 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()

            error_code = ""
            error_msg = str(exc)
            if isinstance(exc, FlowError):
                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()
            return FlowResult(
                success=False,
                error=str(exc),
                error_code="BRIDGE_INTERNAL_ERROR",
                state=FlowState.FAILED,
                duration_ms=(time.monotonic() - started_at) * 1000,
            )
        finally:
            # 兜底：确保 before_msg_task 被取消（CancelledError 路径也走这里）
            # Python 3.8+ 中 CancelledError 继承自 BaseException，
            # except Exception 无法捕获，finally 块保证清理执行
            if before_msg_task is not None and not before_msg_task.done():
                before_msg_task.cancel()
                try:
                    await before_msg_task
                except (asyncio.CancelledError, Exception):
                    pass

    # ------------------------------------------------------------------
    # Transitions
    # ------------------------------------------------------------------
    async def _activate(self, ctx: FlowContext) -> None:
        """激活窗口 + 获取几何 + Esc。"""
        await self.actions.backend.activate_window(_WECHAT_WINDOW_TITLE)
        ctx.win_geom = await self.actions.backend.get_window_geometry(
            _WECHAT_WINDOW_TITLE
        )
        await self.actions.backend.key_press("Escape")
        ctx.current_state = FlowState.WINDOW_ACTIVATED
        logger.info(
            "[flow:send_text] activated window geom=%dx%d",
            ctx.win_geom.width, ctx.win_geom.height,
        )

    async def _click_search_box(self, ctx: FlowContext) -> None:
        """点击搜索框。"""
        await self.actions.click_element("search_box", ctx.win_geom)
        ctx.current_state = FlowState.SEARCH_BOX_CLICKED
        # 简化：点击后立即可输入，等待 0.3s 让 UI 响应
        await asyncio.sleep(0.3)

    async def _type_query(self, ctx: FlowContext) -> None:
        """输入搜索查询（display_name）。"""
        # 清空可能的残留
        await self.actions.backend.key_press("BackSpace", repeat=30)
        await self.actions.backend.type_text(ctx.display_name)
        ctx.current_state = FlowState.QUERY_TYPED
        # 自适应等待搜索结果
        appeared = await self._wait_for_condition(
            lambda: self._check_search_result_appeared(ctx),
            timeout=3.0,
            interval=0.3,
        )
        if not appeared:
            logger.warning(
                "[flow:send_text] search result not appeared in 3s, continue"
            )

    async def _open_session(self, ctx: FlowContext) -> None:
        """点击搜索结果第一项打开会话。"""
        await self.actions.click_element("search_result_first", ctx.win_geom)
        ctx.current_state = FlowState.SESSION_OPENED
        # 自适应等待 input_box 出现
        appeared = await self._wait_for_condition(
            lambda: self._check_input_box_appeared(ctx),
            timeout=3.0,
            interval=0.3,
        )
        if not appeared:
            logger.warning(
                "[flow:send_text] input_box not appeared in 3s, continue"
            )

    async def _focus_input(self, ctx: FlowContext) -> None:
        """聚焦输入框。"""
        await self.actions.click_element("input_box", ctx.win_geom)
        ctx.current_state = FlowState.INPUT_FOCUSED

    async def _type_content(self, ctx: FlowContext) -> None:
        """输入消息内容。"""
        await self.actions.backend.type_text(ctx.content)
        ctx.current_state = FlowState.TEXT_TYPED

    async def _send(self, ctx: FlowContext) -> None:
        """点击发送按钮 + 校验。"""
        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)}"
        ctx.verified = await self._verify_sent(ctx)
        logger.info(
            "[flow:send_text] sent local_id=%s verified=%s",
            ctx.local_send_id, ctx.verified,
        )

    async def _cleanup(self, ctx: FlowContext) -> None:
        """清理：Esc。"""
        await self.actions.backend.key_press("Escape")
        ctx.current_state = FlowState.CLEANED_UP

    # ------------------------------------------------------------------
    # 校验
    # ------------------------------------------------------------------
    async def _verify_sent(self, ctx: FlowContext) -> bool:
        """发送结果校验：DB 校验优先，失败降级截图校验。"""
        if self.db_reader is None:
            logger.info("[flow:send_text] no db_reader, fall back to screenshot")
            return await self._verify_by_screenshot(ctx)
        # before_msg_id=0 表示无法建立基线（DB 不可用或会话无历史消息），
        # 直接降级截图校验，避免 cursor=0 查询返回所有历史消息导致假阳性
        if ctx.before_msg_id <= 0:
            logger.warning(
                "[flow:send_text] before_msg_id=%d, skip DB verify, fall back to screenshot",
                ctx.before_msg_id,
            )
            return await self._verify_by_screenshot(ctx)

        deadline = time.monotonic() + _DB_VERIFY_TIMEOUT_SEC
        attempts = 0
        while time.monotonic() < deadline:
            attempts += 1
            try:
                result = await asyncio.to_thread(
                    self.db_reader.get_messages_by_session,
                    ctx.to_wxid,
                    cursor=ctx.before_msg_id,
                    cursor_local_id=self._before_msg_local_id,
                    limit=3,
                    direction="after",
                    is_sender=None,
                )
                messages = result.get("messages", [])
                if messages:
                    # 防串号：校验是否有消息内容匹配发送的 content
                    # 注意：get_messages_by_session 按 talker 分表查询，talker 字段恒等于 to_wxid，
                    # 无法通过 talker 字段检测串号。改为内容匹配校验。
                    for msg in messages:
                        msg_content = msg.get("content", "")
                        # 微信消息内容可能包含发送者前缀（群消息），用 in 匹配
                        if ctx.content and ctx.content in msg_content:
                            logger.info(
                                "[flow:send_text] DB verify OK (attempt=%d, content matched, is_sender=%s)",
                                attempts, msg.get("is_sender"),
                            )
                            return True
                    # 有新消息但内容不匹配 → 可能串号或消息未到达
                    logger.warning(
                        "[flow:send_text] DB verify: %d new msgs but content not matched (attempt=%d)",
                        len(messages), attempts,
                    )
                    # 继续轮询，可能是 DB 写入延迟
            except Exception as exc:
                logger.debug(
                    "[flow:send_text] DB verify attempt=%d exception: %s",
                    attempts, exc,
                )
            await asyncio.sleep(_DB_VERIFY_INTERVAL_SEC)

        logger.warning(
            "[flow:send_text] DB verify timeout, fall back to screenshot"
        )
        return await self._verify_by_screenshot(ctx)

    async def _verify_by_screenshot(self, ctx: FlowContext) -> bool:
        """截图校验：等待 input_box_empty 模板出现。"""
        if ctx.win_geom is None:
            return False
        try:
            elem = await self.actions.wait_for(
                "input_box_empty", ctx.win_geom, timeout=3.0, interval=0.3
            )
            if elem is not None:
                logger.info("[flow:send_text] screenshot verify OK")
                return True
        except Exception as exc:
            logger.debug(
                "[flow:send_text] screenshot verify exception: %s", exc
            )
        logger.warning("[flow:send_text] screenshot verify failed")
        return False

    async def _verify_session_still_open(self, ctx: FlowContext) -> None:
        """会话缓存命中时校验会话仍打开（input_box 存在）。"""
        # 获取窗口几何（缓存命中时未走 _activate）
        ctx.win_geom = await self.actions.backend.get_window_geometry(
            _WECHAT_WINDOW_TITLE
        )
        try:
            elem = await self.actions.wait_for(
                "input_box", ctx.win_geom, timeout=2.0, interval=0.3
            )
            if elem is None:
                raise PermanentError(
                    "ELEMENT_NOT_FOUND",
                    "session cache hit but input_box not found",
                )
        except PermanentError:
            raise
        except Exception as exc:
            raise PermanentError(
                "ELEMENT_NOT_FOUND",
                f"session cache verify failed: {exc}",
            )

    # ------------------------------------------------------------------
    # 自适应轮询
    # ------------------------------------------------------------------
    async def _wait_for_condition(
        self,
        condition_fn: Callable[[], Awaitable[bool]],
        timeout: float,
        interval: float,
    ) -> bool:
        """自适应轮询：condition_fn 返回 True 时立即返回，超时返回 False。"""
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            try:
                if await condition_fn():
                    return True
            except Exception:
                pass
            await asyncio.sleep(interval)
        return False

    async def _check_search_result_appeared(self, ctx: FlowContext) -> bool:
        """检查搜索结果是否出现。"""
        try:
            elem = await self.actions.find_element(
                "search_result_first", ctx.win_geom
            )
            return elem is not None
        except Exception:
            return False

    async def _check_input_box_appeared(self, ctx: FlowContext) -> bool:
        """检查输入框是否出现。"""
        try:
            elem = await self.actions.find_element("input_box", ctx.win_geom)
            return elem is not None
        except Exception:
            return False

    # ------------------------------------------------------------------
    # DB 辅助
    # ------------------------------------------------------------------
    async def _get_latest_create_time(self, to_wxid: str) -> tuple[int, int]:
        """获取目标会话当前最新消息的 (create_time, local_id)。"""
        if self.db_reader is None:
            return (0, 0)
        try:
            result = await asyncio.to_thread(
                self.db_reader.get_messages_by_session,
                to_wxid,
                cursor=0,
                limit=1,
                direction="before",
                is_sender=None,
            )
            messages = result.get("messages", [])
            if messages:
                ct = int(messages[0].get("create_time", 0))
                lid = int(messages[0].get("local_id", 0))
                return (ct, lid)
        except Exception as exc:
            logger.debug(
                "[flow:send_text] get_latest_create_time exception: %s", exc
            )
        return (0, 0)
