from __future__ import annotations

import hashlib
import re
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from urllib.parse import urlsplit

from playwright.sync_api import Page, Response


class E2EBugDetected(AssertionError):
    pass


@dataclass(frozen=True)
class NetworkIssue:
    status: int
    method: str
    url: str
    resource_type: str
    request_body: str = ""
    response_body: str = ""

    @property
    def signature(self) -> str:
        parsed = urlsplit(self.url)
        return f"{self.status}:{self.method}:{parsed.path}"


class BugReporter:
    def __init__(self, page: Page, case_id: str, output_dir: str = "bugs"):
        self.case_id = case_id
        self.output_dir = Path(output_dir)
        self.network_issues: list[NetworkIssue] = []
        self.page = page
        self.page.on("response", self._collect_bad_response)

    def bind_page(self, page: Page) -> None:
        if page is self.page:
            return
        self.page = page
        self.page.on("response", self._collect_bad_response)

    def _collect_bad_response(self, response: Response) -> None:
        request = response.request
        if request.resource_type not in {"xhr", "fetch"}:
            return
        if response.status < 400:
            return
        self.network_issues.append(
            NetworkIssue(
                status=response.status,
                method=request.method,
                url=response.url,
                resource_type=request.resource_type,
                request_body=self.safe_request_body(request),
                response_body=self.safe_response_body(response),
            )
        )

    def safe_request_body(self, request) -> str:
        try:
            return (request.post_data or "")[:3000]
        except Exception:
            return ""

    def safe_response_body(self, response: Response) -> str:
        try:
            return response.text()[:3000]
        except Exception as exc:
            return f"<读取响应体失败: {exc}>"

    def record_if_errors(self, *, account: str, location: str) -> Path | None:
        frontend_errors = self.frontend_errors()
        if not self.network_issues and not frontend_errors:
            return None

        report_path = self.write_bug_report(
            account=account,
            location=location,
            frontend_errors=frontend_errors,
            network_issues=list(self.network_issues),
        )
        self.network_issues.clear()
        print(f"[E2E bug] Recorded non-blocking issue: {report_path}")
        return report_path

    def assert_no_errors(self, *, account: str, location: str) -> None:
        report_path = self.record_if_errors(account=account, location=location)
        if report_path:
            raise E2EBugDetected(f"E2E detected blocking page/API error at {location}. Bug report: {report_path}")

    def clear(self) -> None:
        self.network_issues.clear()

    def frontend_errors(self) -> list[str]:
        text = self.page.locator("body").inner_text(timeout=3000)
        literal_patterns = [
            "上传失败",
            "上传异常",
            "提交失败",
            "保存失败",
            "操作失败",
            "请求失败",
            "服务异常",
            "系统异常",
            "网络异常",
            "接口异常",
            "报错",
            "错误",
        ]
        regex_patterns = {
            "404": r"(?<!\d)(?:HTTP\s*)?404(?!\d)|状态码\s*404|404\s*(?:错误|异常|Not Found)",
            "500": r"(?<!\d)(?:HTTP\s*)?500(?!\d)|状态码\s*500|500\s*(?:错误|异常|Internal Server Error)",
        }
        errors = [pattern for pattern in literal_patterns if re.search(re.escape(pattern), text)]
        errors.extend(label for label, pattern in regex_patterns.items() if re.search(pattern, text, re.IGNORECASE))
        return errors

    def write_bug_report(
        self,
        *,
        account: str,
        location: str,
        frontend_errors: list[str],
        network_issues: list[NetworkIssue],
    ) -> Path:
        self.output_dir.mkdir(parents=True, exist_ok=True)
        fingerprint = self.fingerprint(location, frontend_errors, network_issues)
        report_path = self.output_dir / f"BUG-{self.case_id}-{fingerprint}.md"
        screenshot_path = self.output_dir / f"BUG-{self.case_id}-{fingerprint}.png"

        self.page.screenshot(path=str(screenshot_path), full_page=True)

        previous_count = self.previous_occurrence_count(report_path)
        occurrence_count = previous_count + 1
        now = datetime.now().isoformat(timespec="seconds")
        network_lines = [
            "\n".join(
                [
                    f"- `{issue.status}` `{issue.method}` `{issue.resource_type}` {issue.url}",
                    f"  - Request Body: `{issue.request_body or '<empty>'}`",
                    f"  - Response Body: `{issue.response_body or '<empty>'}`",
                ]
            )
            for issue in network_issues
        ]
        frontend_lines = [f"- {item}" for item in frontend_errors]

        report = "\n".join(
            [
                f"# E2E Bug - {self.case_id}",
                "",
                f"- 用例: `{self.case_id}`",
                f"- 登录账号/手机号: `{account}`",
                f"- 报错地点: `{location}`",
                f"- 页面地址: `{self.page.url}`",
                f"- 截图: `{screenshot_path.name}`",
                f"- 缺陷指纹: `{fingerprint}`",
                f"- 首次/最近记录时间: `{now}`",
                f"- 出现次数: `{occurrence_count}`",
                "",
                "## 前端页面错误",
                *(frontend_lines or ["- 未检测到明确前端错误文案"]),
                "",
                "## 后台接口错误",
                *(network_lines or ["- 未检测到 4xx/5xx XHR/fetch 响应"]),
            ]
        )
        report_path.write_text(report, encoding="utf-8")
        return report_path.resolve()

    def fingerprint(
        self,
        location: str,
        frontend_errors: list[str],
        network_issues: list[NetworkIssue],
    ) -> str:
        parts = [
            self.case_id,
            "|".join(sorted(frontend_errors)),
            "|".join(sorted(issue.signature for issue in network_issues)),
        ]
        return hashlib.sha1("\n".join(parts).encode("utf-8")).hexdigest()[:12]

    def previous_occurrence_count(self, report_path: Path) -> int:
        if not report_path.exists():
            return 0
        content = report_path.read_text(encoding="utf-8")
        match = re.search(r"出现次数: `(\d+)`", content)
        return int(match.group(1)) if match else 0
