"""HealthMonitor — passive listener that captures runtime issues during tests.

Attaches to a Playwright Page and silently captures:
  - Console errors/warnings (console.error, console.warn)
  - Uncaught JS exceptions (page crashes, unhandled promise rejections)
  - Failed network requests (4xx/5xx API responses, failed fetches)
  - Page crash events
  - Performance metrics (DOM size, memory usage via JS heap)

All issues are available after test execution for reporting.
"""

from __future__ import annotations

import logging
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Any

from playwright.sync_api import ConsoleMessage, Error as PlaywrightError, Page, Request, Response

log = logging.getLogger("antiqa.browser")


class IssueSeverity(str, Enum):
    CRITICAL = "critical"
    WARNING = "warning"
    INFO = "info"


class IssueCategory(str, Enum):
    CONSOLE_ERROR = "console_error"
    JS_EXCEPTION = "js_exception"
    API_FAILURE = "api_failure"
    NETWORK_ERROR = "network_error"
    PAGE_CRASH = "page_crash"
    PERFORMANCE = "performance"
    SECURITY = "security"


@dataclass
class HealthIssue:
    """A single runtime issue detected during test execution."""
    category: IssueCategory
    severity: IssueSeverity
    message: str
    url: str = ""
    timestamp: float = 0.0
    details: dict[str, Any] = field(default_factory=dict)
    screenshot_path: str | None = None

    def to_dict(self) -> dict[str, Any]:
        return {
            "category": self.category.value,
            "severity": self.severity.value,
            "message": self.message,
            "url": self.url,
            "details": self.details,
            "screenshot_path": self.screenshot_path,
        }


@dataclass
class PerformanceSnapshot:
    """Point-in-time performance metrics."""
    timestamp: float = 0.0
    js_heap_used_mb: float = 0.0
    js_heap_total_mb: float = 0.0
    dom_nodes: int = 0
    url: str = ""


class HealthMonitor:
    """Passively monitor a Playwright page for runtime issues.

    Usage::

        monitor = HealthMonitor(page, screenshots_dir="logs/screenshots")
        monitor.start()
        # ... run test steps ...
        monitor.take_snapshot()  # optional perf check
        monitor.stop()

        for issue in monitor.issues:
            print(issue.category, issue.severity, issue.screenshot_path)
    """

    MIXED_CONTENT_PATTERNS = ("mixed content", "insecure", "blocked loading")
    CORS_PATTERNS = ("cors", "cross-origin", "access-control-allow-origin")

    def __init__(self, page: Page, screenshots_dir: str = "logs/screenshots") -> None:
        self.page = page
        self.issues: list[HealthIssue] = []
        self.snapshots: list[PerformanceSnapshot] = []
        self._listening = False
        self._start_time = 0.0
        self._screenshot_counter = 0

        from pathlib import Path
        self._screenshots_dir = Path(screenshots_dir)
        self._screenshots_dir.mkdir(parents=True, exist_ok=True)

    def start(self) -> None:
        """Begin monitoring the page."""
        if self._listening:
            return
        self._start_time = time.time()
        self.page.on("console", self._on_console)
        self.page.on("pageerror", self._on_pageerror)
        self.page.on("crash", self._on_crash)
        self.page.on("response", self._on_response)
        self.page.on("requestfailed", self._on_request_failed)
        self._listening = True
        log.debug("HealthMonitor started")

    def stop(self) -> None:
        """Stop monitoring and detach listeners."""
        if not self._listening:
            return
        try:
            self.page.remove_listener("console", self._on_console)
            self.page.remove_listener("pageerror", self._on_pageerror)
            self.page.remove_listener("crash", self._on_crash)
            self.page.remove_listener("response", self._on_response)
            self.page.remove_listener("requestfailed", self._on_request_failed)
        except Exception:
            pass
        self._listening = False
        log.debug("HealthMonitor stopped (%d issues captured)", len(self.issues))

    def clear(self) -> None:
        self.issues.clear()
        self.snapshots.clear()

    # ── Performance snapshots ──────────────────────────

    def take_snapshot(self) -> PerformanceSnapshot | None:
        """Capture current JS heap and DOM metrics."""
        try:
            metrics = self.page.evaluate("""() => {
                const result = {};
                if (window.performance && performance.memory) {
                    result.jsHeapUsed = performance.memory.usedJSHeapSize;
                    result.jsHeapTotal = performance.memory.totalJSHeapSize;
                }
                result.domNodes = document.querySelectorAll('*').length;
                return result;
            }""")

            snap = PerformanceSnapshot(
                timestamp=time.time(),
                js_heap_used_mb=metrics.get("jsHeapUsed", 0) / (1024 * 1024),
                js_heap_total_mb=metrics.get("jsHeapTotal", 0) / (1024 * 1024),
                dom_nodes=metrics.get("domNodes", 0),
                url=self.page.url,
            )
            self.snapshots.append(snap)

            # Flag large DOM
            if snap.dom_nodes > 1500:
                self._add_issue(
                    IssueCategory.PERFORMANCE,
                    IssueSeverity.WARNING,
                    f"Large DOM: {snap.dom_nodes} nodes (>1500 threshold)",
                    details={"dom_nodes": snap.dom_nodes},
                )

            # Flag potential memory leak (heap > 50MB)
            if snap.js_heap_used_mb > 50:
                self._add_issue(
                    IssueCategory.PERFORMANCE,
                    IssueSeverity.WARNING,
                    f"High JS heap: {snap.js_heap_used_mb:.1f}MB used",
                    details={"heap_used_mb": round(snap.js_heap_used_mb, 2)},
                )

            # Flag growing memory across snapshots
            if len(self.snapshots) >= 3:
                heaps = [s.js_heap_used_mb for s in self.snapshots[-3:] if s.js_heap_used_mb > 0]
                if len(heaps) == 3 and heaps[2] > heaps[0] * 1.5:
                    self._add_issue(
                        IssueCategory.PERFORMANCE,
                        IssueSeverity.WARNING,
                        f"Possible memory leak: heap grew from {heaps[0]:.1f}MB to {heaps[2]:.1f}MB across 3 snapshots",
                        details={"heap_history_mb": [round(h, 2) for h in heaps]},
                    )

            return snap
        except Exception as exc:
            log.debug("Performance snapshot failed: %s", exc)
            return None

    # ── Queries ────────────────────────────────────────

    @property
    def critical_issues(self) -> list[HealthIssue]:
        return [i for i in self.issues if i.severity == IssueSeverity.CRITICAL]

    @property
    def warnings(self) -> list[HealthIssue]:
        return [i for i in self.issues if i.severity == IssueSeverity.WARNING]

    @property
    def has_critical(self) -> bool:
        return any(i.severity == IssueSeverity.CRITICAL for i in self.issues)

    @property
    def console_errors(self) -> list[HealthIssue]:
        return [i for i in self.issues if i.category == IssueCategory.CONSOLE_ERROR]

    @property
    def js_exceptions(self) -> list[HealthIssue]:
        return [i for i in self.issues if i.category == IssueCategory.JS_EXCEPTION]

    @property
    def api_failures(self) -> list[HealthIssue]:
        return [i for i in self.issues if i.category == IssueCategory.API_FAILURE]

    @property
    def network_errors(self) -> list[HealthIssue]:
        return [i for i in self.issues if i.category == IssueCategory.NETWORK_ERROR]

    def summary(self) -> dict[str, Any]:
        by_cat: dict[str, int] = {}
        for issue in self.issues:
            by_cat[issue.category.value] = by_cat.get(issue.category.value, 0) + 1
        return {
            "total_issues": len(self.issues),
            "critical": len(self.critical_issues),
            "warnings": len(self.warnings),
            "by_category": by_cat,
            "snapshots": len(self.snapshots),
        }

    def summary_for_ai(self) -> str:
        """Return a text summary for LLM bug report context."""
        if not self.issues:
            return "No runtime health issues detected."
        lines = [f"Runtime health issues detected: {len(self.issues)}"]
        for issue in self.issues:
            lines.append(f"  [{issue.severity.value}] {issue.category.value}: {issue.message}")
            if issue.url:
                lines.append(f"    URL: {issue.url}")
        return "\n".join(lines)

    # ── Internal handlers ──────────────────────────────

    def _on_console(self, msg: ConsoleMessage) -> None:
        msg_type = msg.type
        text = msg.text

        if msg_type == "error":
            severity = IssueSeverity.CRITICAL
            category = IssueCategory.CONSOLE_ERROR

            # Check for security issues
            lower = text.lower()
            if any(p in lower for p in self.MIXED_CONTENT_PATTERNS):
                category = IssueCategory.SECURITY
            elif any(p in lower for p in self.CORS_PATTERNS):
                category = IssueCategory.SECURITY

            self._add_issue(category, severity, text)

        elif msg_type == "warning":
            # Only capture meaningful warnings, skip noise
            lower = text.lower()
            if any(kw in lower for kw in ("deprecated", "warning", "failed", "error", "refused")):
                self._add_issue(
                    IssueCategory.CONSOLE_ERROR,
                    IssueSeverity.WARNING,
                    text,
                )

    def _on_pageerror(self, error: PlaywrightError) -> None:
        self._add_issue(
            IssueCategory.JS_EXCEPTION,
            IssueSeverity.CRITICAL,
            f"Uncaught exception: {error.message}",
            details={"stack": error.message},
        )

    def _on_crash(self, page: Page) -> None:
        self._add_issue(
            IssueCategory.PAGE_CRASH,
            IssueSeverity.CRITICAL,
            f"Page crashed: {page.url}",
        )

    def _on_response(self, response: Response) -> None:
        status = response.status
        if status >= 500:
            self._add_issue(
                IssueCategory.API_FAILURE,
                IssueSeverity.CRITICAL,
                f"Server error {status}: {response.request.method} {response.url}",
                details={"status": status, "method": response.request.method},
            )
        elif status >= 400:
            # 401/403 might be expected — treat as warnings
            severity = IssueSeverity.WARNING if status in (401, 403, 404) else IssueSeverity.CRITICAL
            self._add_issue(
                IssueCategory.API_FAILURE,
                severity,
                f"Client error {status}: {response.request.method} {response.url}",
                details={"status": status, "method": response.request.method},
            )

    def _on_request_failed(self, request: Request) -> None:
        failure = request.failure
        msg = failure if failure else "Unknown failure"
        self._add_issue(
            IssueCategory.NETWORK_ERROR,
            IssueSeverity.CRITICAL,
            f"Request failed: {request.method} {request.url} — {msg}",
            details={"method": request.method, "failure": msg},
        )

    def _add_issue(
        self,
        category: IssueCategory,
        severity: IssueSeverity,
        message: str,
        details: dict[str, Any] | None = None,
    ) -> None:
        # Deduplicate exact messages
        for existing in self.issues:
            if existing.message == message and existing.category == category:
                return

        try:
            url = self.page.url
        except Exception:
            url = ""

        # Auto-screenshot for critical issues
        screenshot_path = None
        if severity == IssueSeverity.CRITICAL:
            screenshot_path = self._take_screenshot(category)

        self.issues.append(HealthIssue(
            category=category,
            severity=severity,
            message=message,
            url=url,
            timestamp=time.time(),
            details=details or {},
            screenshot_path=screenshot_path,
        ))

    def _take_screenshot(self, category: IssueCategory) -> str | None:
        """Capture a screenshot as proof of a health issue."""
        self._screenshot_counter += 1
        name = f"health_{self._screenshot_counter}_{category.value}.png"
        path = str(self._screenshots_dir / name)
        try:
            self.page.screenshot(path=path)
            log.debug("Health screenshot: %s", path)
            return path
        except Exception:
            return None
