"""AI failure analyzer for Playwright and Monocart dashboards.

Invokes playwright-monocart-helper via the code-puppy CLI to batch-analyze
all failing/flaky flows and return structured classifications.

Usage (standalone):
    python3 -m looper_dashboard.ai_analyzer
    python3 -m looper_dashboard.ai_analyzer --failures failures.json --output analysis.json
"""
import base64
import json
import re
import subprocess
import sys
from pathlib import Path

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

_VENV_BIN = Path.home() / ".code-puppy-venv" / "bin" / "code-puppy"
AGENT = "playwright-monocart-helper"
TIMEOUT = 240  # seconds
MAX_TITLES = 15  # max test titles per flow to keep token usage reasonable


# ---------------------------------------------------------------------------
# ANSI / OSC stripping
# ---------------------------------------------------------------------------

def _strip_ansi(text: str) -> str:
    """Remove ANSI/VT escape sequences (including OSC) from text."""
    text = re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', text)
    text = re.sub(r'\x1b\].*?(?:\x07|\x1b\\)', '', text, flags=re.DOTALL)
    text = re.sub(r'\x1b.', '', text)
    return text


def _extract_json_from_osc52(text: str) -> list | None:
    """Extract and decode a base64 payload from an OSC 52 clipboard sequence."""
    m = re.search(r']\s*52\s*;\s*c\s*;\s*([A-Za-z0-9+/=]+)', text)
    if not m:
        return None
    try:
        decoded = base64.b64decode(m.group(1)).decode("utf-8")
        return json.loads(decoded)
    except Exception:  # noqa: BLE001
        return None


# ---------------------------------------------------------------------------
# Prompt construction
# ---------------------------------------------------------------------------

def _build_prompt(failures: list[dict]) -> str:
    trimmed = [
        {
            **f,
            "failedTests": (f.get("failedTests") or [])[:MAX_TITLES],
            "flakyTests":  (f.get("flakyTests")  or [])[:MAX_TITLES],
        }
        for f in failures
    ]

    return f"""You are analyzing Playwright test failures from a CI/CD pipeline.

For each failing flow below, classify the root cause and provide actionable guidance.

Failures:
{json.dumps(trimmed, indent=2)}

Respond with ONLY a raw JSON array (no markdown fences, no explanation).
Each element must have exactly these fields:
  "flow"           - (string) the flowName from the input
  "job"            - (string) the job short name from the input
  "classification" - one of: "service_regression", "environment_issue", "test_issue", "flaky"
  "severity"       - one of: "high", "medium", "low"
  "summary"        - (string <=120 chars) what is failing and likely why, inferred from test names
  "recommendation" - (string <=200 chars) concrete next step to fix or investigate

Classification guide:
  service_regression  - feature tests failing; likely a code regression in the app
  environment_issue   - infra/network/auth failures or env-specific issues
  test_issue          - stale selectors, bad test data, or a test code bug
  flaky               - intermittent with no clear pattern

Start your response with [ and end with ].
"""


# ---------------------------------------------------------------------------
# JSON extraction
# ---------------------------------------------------------------------------

def _extract_json(text: str) -> list | None:
    """Robustly extract a JSON array from code-puppy output."""
    osc = _extract_json_from_osc52(text)
    if osc is not None:
        return osc

    stripped = _strip_ansi(text).strip()

    if stripped.startswith("["):
        try:
            return json.loads(stripped)
        except json.JSONDecodeError:
            pass

    m = re.search(r"```(?:json)?\s*(\[.*?\])\s*```", stripped, re.DOTALL)
    if m:
        try:
            return json.loads(m.group(1))
        except json.JSONDecodeError:
            pass

    m = re.search(r"(\[.*\])", stripped, re.DOTALL)
    if m:
        try:
            return json.loads(m.group(1))
        except json.JSONDecodeError:
            pass

    return None


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def analyze_failures(
    failures: list[dict],
    code_puppy_bin: str | None = None,
) -> list[dict]:
    """Invoke playwright-monocart-helper to classify each failing/flaky flow.

    Args:
        failures: List of failure dicts from save_failure_summary.
        code_puppy_bin: Override path to code-puppy binary.

    Returns:
        List of analysis dicts, or [] on any failure (never raises).
    """
    if not failures:
        return []

    bin_path = Path(code_puppy_bin or _VENV_BIN)
    if not bin_path.exists():
        print(f"Warning: code-puppy not found at {bin_path}", file=sys.stderr)
        return []

    prompt = _build_prompt(failures)
    n = len(failures)
    print(f"Invoking {AGENT} to analyze {n} failing flow{'s' if n != 1 else ''}...", file=sys.stderr)

    try:
        result = subprocess.run(
            [str(bin_path), "--prompt", prompt, "--agent", AGENT],
            capture_output=True,
            text=True,
            timeout=TIMEOUT,
        )
    except subprocess.TimeoutExpired:
        print(f"Warning: AI analysis timed out after {TIMEOUT}s.", file=sys.stderr)
        return []
    except Exception as exc:  # noqa: BLE001
        print(f"Warning: AI analysis subprocess error: {exc}", file=sys.stderr)
        return []

    raw = result.stdout or ""
    analysis = _extract_json(raw)

    if analysis is None and result.stderr:
        analysis = _extract_json(result.stderr)

    if analysis is None:
        print("Warning: Could not parse JSON from AI output.", file=sys.stderr)
        tail = (raw or result.stderr or "").strip().splitlines()
        for line in tail[-8:]:
            print(f"   | {line}", file=sys.stderr)
        return []

    print(f"AI analysis complete — {len(analysis)} flow{'s' if len(analysis) != 1 else ''} classified.", file=sys.stderr)
    return analysis


# ---------------------------------------------------------------------------
# CLI (standalone use)
# ---------------------------------------------------------------------------

def main() -> None:
    import argparse

    parser = argparse.ArgumentParser(
        description="Analyze Playwright/Monocart failures with playwright-monocart-helper AI",
    )
    parser.add_argument("--failures", default="failures.json", help="Path to failures JSON")
    parser.add_argument("--output", default="ai-analysis.json", help="Output path for analysis JSON")
    parser.add_argument("--code-puppy-bin", default=None, help=f"Override path to code-puppy (default: {_VENV_BIN})")
    args = parser.parse_args()

    failures_path = Path(args.failures)
    if not failures_path.exists():
        print(f"Failures file not found: {failures_path}", file=sys.stderr)
        sys.exit(1)

    failures = json.loads(failures_path.read_text())
    if not failures:
        print("No failures to analyze.", file=sys.stderr)
        sys.exit(0)

    analysis = analyze_failures(failures, code_puppy_bin=args.code_puppy_bin)
    if not analysis:
        sys.exit(1)

    output_path = Path(args.output)
    output_path.write_text(json.dumps(analysis, indent=2))
    print(f"Analysis saved to {output_path.resolve()}", file=sys.stderr)


if __name__ == "__main__":
    main()
