"""Heuristic web-act failure taxonomy for tool output and salvage handoffs."""

from __future__ import annotations

import re
from typing import Any

_MAX_BLOCKERS = 4

_BLOCKERS_LINE_RE = re.compile(r"^blockers:\s*(.+)$", re.MULTILINE | re.IGNORECASE)

_WEB_ACT_BLOCKER_SLUGS = frozenset({
    "playwright_timeout",
    "selector_not_found",
    "playwright_browsers_missing",
    "host_not_allowed",
    "missing_fresh_run",
    "invalid_script_path",
    "web_act_policy_blocked",
})


def infer_web_act_blockers(
    *,
    stdout: str = "",
    stderr: str = "",
    exit_code: int | None = None,
    fresh_run: bool = False,
    tool_message: str = "",
    validation_detail: str = "",
) -> list[str]:
    """Return stable blocker slugs (max 4) from harness/tool output."""
    hay = "\n".join(
        [
            str(tool_message or ""),
            str(stderr or ""),
            str(stdout or ""),
            str(validation_detail or ""),
        ]
    )
    lower = hay.lower()
    found: list[str] = []

    def _add(slug: str) -> None:
        if slug and slug not in found and len(found) < _MAX_BLOCKERS:
            found.append(slug)

    if "[policy blocked]" in lower or "web-act scripts execute arbitrary python" in lower:
        _add("web_act_policy_blocked")

    if (
        "executable doesn't exist" in lower
        or "playwright_browsers_ready" in lower
        or "browser binaries are missing" in lower
        or ("playwright install" in lower and "missing" in lower)
    ):
        _add("playwright_browsers_missing")

    if "host not in web-act allowlist" in lower or "host not in allowlist" in lower:
        _add("host_not_allowed")

    if (
        ("script not found" in lower and "nested web_runs" in lower)
        or re.search(r"web_runs/[^/\s]+/web_runs/", hay.replace("\\", "/"))
    ):
        _add("invalid_script_path")

    if re.search(r"timeouterror|timeout .* exceeded", lower):
        _add("playwright_timeout")
    elif re.search(
        r"waiting for selector|no node found|strict mode violation|selector.*not found",
        lower,
    ):
        _add("selector_not_found")

    if fresh_run:
        if exit_code not in (None, 0):
            _add("missing_fresh_run")
        elif "result.json missing" in lower or "result.json could not be parsed" in lower:
            _add("missing_fresh_run")
        elif "result.json ok=false" in lower:
            _add("missing_fresh_run")
        elif "answer is empty" in lower or "result.json ok=true but answer is empty" in lower:
            _add("missing_fresh_run")
        elif "result_ok: false" in lower and "fresh_run: true" in lower:
            _add("missing_fresh_run")
        elif str(validation_detail or "").strip():
            _add("missing_fresh_run")

    if exit_code not in (None, 0) and not found:
        if "timed_out: true" in lower:
            _add("playwright_timeout")
        else:
            _add("playwright_timeout" if "timeout" in lower else "missing_fresh_run")

    return found


def format_blockers_line(blockers: list[str]) -> str:
    slugs = [str(s).strip() for s in blockers if str(s).strip()][: _MAX_BLOCKERS]
    if not slugs:
        return ""
    return "blockers: " + ", ".join(slugs)


def parse_blockers_from_tool_message(content: str) -> list[str]:
    """Parse ``blockers: slug1, slug2`` from ``run_web_script`` tool output."""
    text = str(content or "")
    m = _BLOCKERS_LINE_RE.search(text)
    if not m:
        return []
    raw = m.group(1).strip()
    return [s.strip() for s in raw.split(",") if s.strip()][: _MAX_BLOCKERS]


def merge_web_act_blockers(*sources: Any) -> list[str]:
    """Dedupe blockers from infer output, tool lines, and metadata lists."""
    out: list[str] = []
    for src in sources:
        if isinstance(src, str):
            items = parse_blockers_from_tool_message(src) or (
                [s.strip() for s in src.split(",") if s.strip()] if "," in src else ([src] if src.strip() else [])
            )
        elif isinstance(src, list):
            items = [str(x).strip() for x in src if str(x).strip()]
        else:
            continue
        for slug in items:
            if slug and slug not in out and len(out) < _MAX_BLOCKERS:
                out.append(slug)
    return out


def primary_web_act_blocker_slug(blockers: list[str]) -> str:
    """First web-act taxonomy slug in *blockers* (ignores structural loop slugs)."""
    for slug in blockers or []:
        s = str(slug or "").strip()
        if s in _WEB_ACT_BLOCKER_SLUGS:
            return s
    return ""


def web_act_blocker_next_action_hint(
    blockers: list[str],
    *,
    task_metadata: dict[str, Any] | None = None,
) -> str:
    """Tailored salvage hint for a primary web-act blocker slug."""
    primary = primary_web_act_blocker_slug(blockers) or (blockers or [""])[0]
    mapping = {
        "playwright_timeout": (
            "Increase waits and narrow selectors in the Playwright script, then "
            "run_web_script with fresh_run=True."
        ),
        "selector_not_found": (
            "Inspect the page with a shorter probe script, fix CSS selectors, then "
            "run_web_script with fresh_run=True."
        ),
        "playwright_browsers_missing": (
            "Install Playwright browser binaries from Settings → Advanced, then retry "
            "run_web_script."
        ),
        "host_not_allowed": (
            "Add the target host to [web_act].allowed_hosts or task metadata allowed_hosts, "
            "then use safe_goto() in the script."
        ),
        "missing_fresh_run": (
            "Fix result.json (non-empty answer) and run run_web_script(..., fresh_run=True)."
        ),
        "invalid_script_path": (
            "Use write_web_act_file with a path relative to the web-act scope root "
            "(e.g. explore.py), not a nested web_runs/… path."
        ),
        "web_act_policy_blocked": (
            "Enable web automation in Settings → Advanced and ensure the task is a "
            "kanban web_act worker, then retry."
        ),
    }
    if primary in mapping:
        return mapping[primary]
    _ = task_metadata
    return "Fix the Playwright script and re-run run_web_script with fresh_run=True."
