from __future__ import annotations

from dataclasses import dataclass
from typing import Iterable

from playwright.async_api import Page


VISIBLE_FEEDBACK_SELECTORS = (
    "[role='alert']",
    ".d-message-notice",
    ".d-message-notice-content",
    ".d-message__content",
    ".d-modal",
    ".d-dialog",
    ".toast",
)


@dataclass(frozen=True)
class RednoteIssue:
    failure_type: str
    reason_code: str
    failure_reason: str
    next_action: str
    agent_action: str
    source: str
    retry_recommended: bool = False
    cli_code: str = "INTERNAL_ERROR"
    observed_text: str = ""

    def to_details(self) -> dict:
        return {
            "failure_type": self.failure_type,
            "failure_reason": self.failure_reason,
            "next_action": self.next_action,
            "retry_recommended": self.retry_recommended,
            "agent_action": self.agent_action,
        }


_ISSUE_RULES: tuple[dict, ...] = (
    {
        "markers": ("未绑定手机号",),
        "failure_type": "PHONE_NOT_BOUND",
        "reason_code": "PHONE_NOT_BOUND",
        "next_action": "请给账号绑定手机号",
        "agent_action": "REQUEST_USER_ACTION",
        "retry_recommended": False,
        "cli_code": "INTERNAL_ERROR",
    },
    {
        "markers": ("请扫码通过验证", "请通过验证", "验证码"),
        "failure_type": "VERIFICATION_REQUIRED",
        "reason_code": "VERIFICATION_REQUIRED",
        "next_action": "请使用已登录小红书 APP 扫码完成验证",
        "agent_action": "REQUEST_USER_ACTION",
        "retry_recommended": False,
        "cli_code": "RISK_CONTROL_TRIGGERED",
    },
    {
        "markers": ("薯队长遇到了点小麻烦",),
        "failure_type": "RISK_CONTROL",
        "reason_code": "CAPTAIN_POTATO",
        "next_action": "请暂停当前操作，稍后重试或联系开发者",
        "agent_action": "STOP",
        "retry_recommended": False,
        "cli_code": "RISK_CONTROL_TRIGGERED",
    },
    {
        "markers": ("IP存在风险", "安全限制"),
        "failure_type": "NETWORK_ENV_RISK",
        "reason_code": "IP_RISK",
        "next_action": "请切换可靠网络环境后重试",
        "agent_action": "STOP",
        "retry_recommended": False,
        "cli_code": "RISK_CONTROL_TRIGGERED",
    },
    {
        "markers": ("未连接到服务器，刷新一下试试",),
        "failure_type": "TRANSIENT_NETWORK",
        "reason_code": "SERVER_DISCONNECTED",
        "next_action": "请刷新页面后重试当前步骤",
        "agent_action": "RETRY_LATER",
        "retry_recommended": True,
        "cli_code": "INTERNAL_ERROR",
    },
    {
        "markers": ("当前笔记暂时无法浏览",),
        "failure_type": "CONTENT_UNAVAILABLE",
        "reason_code": "CONTENT_UNAVAILABLE",
        "next_action": "请跳过该内容或联系开发者确认可见性限制",
        "agent_action": "STOP",
        "retry_recommended": False,
        "cli_code": "INTERNAL_ERROR",
    },
    {
        "markers": ("你访问的页面不见了",),
        "failure_type": "PAGE_UNAVAILABLE",
        "reason_code": "PAGE_NOT_FOUND",
        "next_action": "请检查链接参数或联系开发者确认页面访问路径",
        "agent_action": "STOP",
        "retry_recommended": False,
        "cli_code": "INTERNAL_ERROR",
    },
    {
        "markers": ("Cannot read properties of undefined (reading 'status')",),
        "failure_type": "LOGIN_PAGE_BROKEN",
        "reason_code": "QR_WIDGET_SCRIPT_ERROR",
        "next_action": "请刷新页面后重试登录；若仍失败请联系开发者",
        "agent_action": "RETRY_LATER",
        "retry_recommended": True,
        "cli_code": "INTERNAL_ERROR",
    },
)


def _normalize_text(text: str) -> str:
    return (text or "").strip()


def _candidate_lines(text: str) -> list[str]:
    normalized = _normalize_text(text)
    if not normalized:
        return []
    return [line.strip() for line in normalized.splitlines() if line.strip()] or [normalized]


def _match_rule(text: str, source: str) -> RednoteIssue | None:
    lines = _candidate_lines(text)
    for rule in _ISSUE_RULES:
        for marker in rule["markers"]:
            for line in lines:
                if marker in line:
                    return RednoteIssue(
                        failure_type=rule["failure_type"],
                        reason_code=rule["reason_code"],
                        failure_reason=line,
                        next_action=rule["next_action"],
                        agent_action=rule["agent_action"],
                        source=source,
                        retry_recommended=rule["retry_recommended"],
                        cli_code=rule["cli_code"],
                        observed_text=line,
                    )
    return None


def classify_issue_from_text(text: str, *, source: str = "page") -> RednoteIssue | None:
    return _match_rule(text, source)


def detect_issue_from_texts(texts: Iterable[str], *, source: str = "page") -> RednoteIssue | None:
    for text in texts:
        issue = classify_issue_from_text(text, source=source)
        if issue is not None:
            return issue
    return None


def build_unknown_issue(
    *,
    source: str = "unknown",
    observed_text: str = "",
    failure_reason: str = "未知原因",
    next_action: str = "请联系开发者",
    agent_action: str = "STOP",
    retry_recommended: bool = False,
) -> RednoteIssue:
    return RednoteIssue(
        failure_type="UNKNOWN",
        reason_code="UNKNOWN",
        failure_reason=failure_reason,
        next_action=next_action,
        agent_action=agent_action,
        source=source,
        retry_recommended=retry_recommended,
        cli_code="INTERNAL_ERROR",
        observed_text=_normalize_text(observed_text),
    )


async def collect_visible_feedback_texts(page: Page) -> list[str]:
    texts = await page.evaluate(
        """
        (selectors) => {
          const isVisible = (el) => {
            if (!el) return false;
            const style = window.getComputedStyle(el);
            if (!style || style.visibility === 'hidden' || style.display === 'none') return false;
            const rect = el.getBoundingClientRect();
            return rect.width > 0 && rect.height > 0;
          };

          const nodes = [];
          for (const selector of selectors) {
            for (const el of document.querySelectorAll(selector)) {
              if (isVisible(el)) nodes.push(el);
            }
          }

          for (const el of document.querySelectorAll('[class*="message"], [class*="toast"], [class*="modal"], [class*="dialog"]')) {
            if (isVisible(el)) nodes.push(el);
          }

          const uniqueTexts = [];
          const seen = new Set();
          for (const el of nodes) {
            const text = (el.innerText || el.textContent || '').trim();
            if (!text || seen.has(text)) continue;
            seen.add(text);
            uniqueTexts.push(text);
          }
          return uniqueTexts;
        }
        """,
        list(VISIBLE_FEEDBACK_SELECTORS),
    )
    return texts if isinstance(texts, list) else []


async def detect_issue_from_page(
    page: Page,
    *,
    body_text: str = "",
    include_body: bool = True,
) -> RednoteIssue | None:
    visible_texts = await collect_visible_feedback_texts(page)
    issue = detect_issue_from_texts(visible_texts, source="toast")
    if issue is not None:
        return issue

    if not include_body:
        return None

    effective_body_text = _normalize_text(body_text)
    if not effective_body_text:
        body = page.locator("body").first
        if await body.count() == 0:
            return None
        effective_body_text = _normalize_text(await body.inner_text() or "")
    if not effective_body_text:
        return None
    return classify_issue_from_text(effective_body_text, source="page")
