"""ActionExecutor — deterministic browser actions.

Every method performs exactly one user-visible action via Playwright and
returns a result dict describing what happened.  These are the building
blocks that the FlowRunner (Phase 4) and TestCaseInterpreter (Phase 6)
compose into test sequences.
"""

from __future__ import annotations

import logging
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Literal

from playwright.sync_api import Page, TimeoutError as PlaywrightTimeout

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

DEFAULT_TIMEOUT_MS = 10_000


@dataclass
class ActionResult:
    """Outcome of a single browser action."""

    action: str
    success: bool
    duration_ms: float
    details: dict[str, Any] = field(default_factory=dict)
    error: str | None = None
    screenshot_path: str | None = None


class ActionExecutor:
    """Execute deterministic browser actions on a Playwright Page."""

    def __init__(
        self,
        page: Page,
        screenshots_dir: str | Path = "logs/screenshots",
        timeout_ms: int = DEFAULT_TIMEOUT_MS,
        config: Any = None,
        auto_heal: bool = True,
    ) -> None:
        self.page = page
        self.screenshots_dir = Path(screenshots_dir)
        self.screenshots_dir.mkdir(parents=True, exist_ok=True)
        self.timeout_ms = timeout_ms
        self._step_counter = 0
        self._config = config
        self._auto_heal = auto_heal and config is not None
        self._healed: dict[str, str] = {}  # old_selector -> healed_selector

    # ── Helpers ────────────────────────────────────────

    def _timed(self, action: str, fn: Any, **details: Any) -> ActionResult:
        """Run *fn*, measure time, catch errors, auto-screenshot on failure."""
        self._step_counter += 1
        start = time.perf_counter()
        try:
            fn()
            elapsed = (time.perf_counter() - start) * 1000
            log.debug("[%s] %s OK (%.0fms)", self._step_counter, action, elapsed)
            return ActionResult(
                action=action, success=True, duration_ms=elapsed, details=details,
            )
        except PlaywrightTimeout as exc:
            elapsed = (time.perf_counter() - start) * 1000
            screenshot = self._failure_screenshot(action)
            log.warning("[%s] %s TIMEOUT (%.0fms)", self._step_counter, action, elapsed)
            return ActionResult(
                action=action,
                success=False,
                duration_ms=elapsed,
                details=details,
                error=str(exc),
                screenshot_path=screenshot,
            )
        except Exception as exc:
            elapsed = (time.perf_counter() - start) * 1000
            screenshot = self._failure_screenshot(action)
            log.error("[%s] %s FAILED: %s", self._step_counter, action, exc)
            return ActionResult(
                action=action,
                success=False,
                duration_ms=elapsed,
                details=details,
                error=str(exc),
                screenshot_path=screenshot,
            )

    def _failure_screenshot(self, action: str) -> str:
        """Take a screenshot on failure and return the path."""
        name = f"fail_{self._step_counter}_{action}.png"
        path = str(self.screenshots_dir / name)
        try:
            self.page.screenshot(path=path)
        except Exception:
            pass
        return path

    def _try_heal_selector(self, selector: str, description: str = "") -> str | None:
        """Attempt AI selector healing when an element isn't found."""
        if not self._auto_heal:
            return None

        if selector in self._healed:
            return self._healed[selector]

        try:
            from antiqa.agents.selector_healer import SelectorHealerAgent
            html = self.page.content()
            healer = SelectorHealerAgent(self._config)
            suggestions = healer.heal(selector, description, html)

            for s in suggestions:
                candidate = s.get("selector", "")
                if not candidate:
                    continue
                try:
                    el = self.page.query_selector(candidate)
                    if el:
                        log.info("Healed selector '%s' → '%s' (strategy: %s)",
                                 selector, candidate, s.get("strategy", "unknown"))
                        self._healed[selector] = candidate
                        return candidate
                except Exception:
                    continue
        except Exception as exc:
            log.debug("Selector healing unavailable: %s", exc)

        return None

    # ── Actions ────────────────────────────────────────

    def navigate(self, url: str, wait_until: str = "domcontentloaded") -> ActionResult:
        return self._timed(
            "navigate",
            lambda: self.page.goto(url, wait_until=wait_until, timeout=self.timeout_ms),
            url=url,
        )

    def click(self, selector: str, description: str = "") -> ActionResult:
        result = self._timed(
            "click",
            lambda: self.page.click(selector, timeout=self.timeout_ms),
            selector=selector,
        )
        if not result.success and self._auto_heal:
            healed = self._try_heal_selector(selector, description or f"clickable element: {selector}")
            if healed:
                result = self._timed(
                    "click",
                    lambda: self.page.click(healed, timeout=self.timeout_ms),
                    selector=healed, healed_from=selector,
                )
        return result

    def fill(self, selector: str, value: str, description: str = "") -> ActionResult:
        result = self._timed(
            "fill",
            lambda: self.page.fill(selector, value, timeout=self.timeout_ms),
            selector=selector,
            value=value,
        )
        if not result.success and self._auto_heal:
            healed = self._try_heal_selector(selector, description or f"input field: {selector}")
            if healed:
                result = self._timed(
                    "fill",
                    lambda: self.page.fill(healed, value, timeout=self.timeout_ms),
                    selector=healed, value=value, healed_from=selector,
                )
        return result

    def select(self, selector: str, value: str) -> ActionResult:
        return self._timed(
            "select",
            lambda: self.page.select_option(selector, value, timeout=self.timeout_ms),
            selector=selector,
            value=value,
        )

    def hover(self, selector: str) -> ActionResult:
        return self._timed(
            "hover",
            lambda: self.page.hover(selector, timeout=self.timeout_ms),
            selector=selector,
        )

    def press_key(self, key: str, selector: str | None = None) -> ActionResult:
        def _press() -> None:
            if selector:
                self.page.press(selector, key, timeout=self.timeout_ms)
            else:
                self.page.keyboard.press(key)

        return self._timed("press_key", _press, key=key, selector=selector)

    def scroll(self, direction: Literal["up", "down"] = "down", amount: int = 500) -> ActionResult:
        delta = amount if direction == "down" else -amount
        return self._timed(
            "scroll",
            lambda: self.page.mouse.wheel(0, delta),
            direction=direction,
            amount=amount,
        )

    def wait_for(
        self,
        selector: str | None = None,
        url: str | None = None,
        timeout_ms: int | None = None,
    ) -> ActionResult:
        """Wait for a selector to appear or the URL to contain a string."""
        t = timeout_ms or self.timeout_ms

        if selector:
            return self._timed(
                "wait_for",
                lambda: self.page.wait_for_selector(selector, timeout=t),
                selector=selector,
            )
        if url:
            return self._timed(
                "wait_for",
                lambda: self.page.wait_for_url(f"**{url}**", timeout=t),
                url=url,
            )
        return self._timed(
            "wait_for",
            lambda: self.page.wait_for_timeout(t),
            timeout_ms=t,
        )

    def screenshot(self, name: str | None = None, full_page: bool = False) -> ActionResult:
        """Capture a screenshot. Returns the file path in details."""
        if not name:
            name = f"step_{self._step_counter}"
        path = str(self.screenshots_dir / f"{name}.png")
        return self._timed(
            "screenshot",
            lambda: self.page.screenshot(path=path, full_page=full_page),
            path=path,
        )

    def get_text(self, selector: str) -> ActionResult:
        """Read the text content of an element."""
        text: str | None = None

        def _get() -> None:
            nonlocal text
            el = self.page.wait_for_selector(selector, timeout=self.timeout_ms)
            text = el.text_content() if el else None

        result = self._timed("get_text", _get, selector=selector)
        result.details["text"] = text
        return result

    def get_attribute(self, selector: str, attribute: str) -> ActionResult:
        """Read an attribute value from an element."""
        value: str | None = None

        def _get() -> None:
            nonlocal value
            el = self.page.wait_for_selector(selector, timeout=self.timeout_ms)
            value = el.get_attribute(attribute) if el else None

        result = self._timed("get_attribute", _get, selector=selector, attribute=attribute)
        result.details["value"] = value
        return result

    @property
    def current_url(self) -> str:
        return self.page.url
