# Copyright (C) 2026 GoodData Corporation

"""Error excerpt extraction helpers for validation tools.

Goal: provide a *small, actionable* excerpt on failure without returning full logs inline.
"""

from __future__ import annotations

import re

from aida_mcp.output.gradle_parser import extract_gradle_errors

_GRADLE_ANCHOR_PATTERNS: list[tuple[re.Pattern[str], int]] = [
    (re.compile(r"^FAILURE: Build failed with an exception"), 50),
    (re.compile(r"^BUILD FAILED"), 45),
    (re.compile(r"^> Task .* FAILED"), 40),
    (re.compile(r"^Caused by:"), 35),
    (re.compile(r"^e: .+\.kt:\d+:\d+"), 30),
    (re.compile(r"\.java:\d+:\s+error:"), 25),
]

_PYTHON_ANCHOR_PATTERNS: list[tuple[re.Pattern[str], int]] = [
    (re.compile(r"^Found \d+ error", re.IGNORECASE), 50),  # ruff summary
    (re.compile(r"^.+:\d+:\d+:\s+[A-Z]\d+"), 45),  # ruff locations
    (re.compile(r"^.+:\d+:\d+\s+-\s+error:", re.IGNORECASE), 45),  # pyright locations
    (re.compile(r"^=+ FAILURES =+$"), 40),  # pytest section
    (re.compile(r"^FAILED\s", re.IGNORECASE), 35),  # pytest summary
    (re.compile(r"^E\s{3,}"), 30),  # pytest error lines
    (re.compile(r"^Traceback \(most recent call last\):"), 25),
    (re.compile(r"\bERROR\b"), 10),
]

_GENERIC_ANCHOR_PATTERNS: list[tuple[re.Pattern[str], int]] = [
    (re.compile(r"\bERROR\b"), 10),
    (re.compile(r"\bFAILED\b"), 9),
    (re.compile(r"\bException\b"), 8),
]


def _limit_lines(text: str, *, max_lines: int) -> str:
    lines = [ln.rstrip("\n") for ln in text.splitlines()]
    if len(lines) <= max_lines:
        return "\n".join(lines).strip()
    kept = lines[:max_lines]
    kept.append(f"... ({len(lines) - max_lines} more lines)")
    return "\n".join(kept).strip()


def _window_around_line(
    lines: list[str],
    *,
    idx: int,
    before: int,
    after: int,
    max_lines: int,
) -> str:
    start = max(0, idx - before)
    end = min(len(lines), idx + after + 1)
    window = lines[start:end]
    if len(window) > max_lines:
        window = window[:max_lines] + [f"... ({len(lines) - (start + max_lines)} more lines)"]
    return "\n".join(window).strip()


def _best_anchor_excerpt(
    combined: str,
    *,
    anchor_patterns: list[tuple[re.Pattern[str], int]],
    before: int,
    after: int,
    max_lines: int,
) -> str | None:
    if not combined.strip():
        return None
    lines = [ln.rstrip("\n") for ln in combined.splitlines()]

    best_score = -1
    best_idx = -1
    for i, line in enumerate(lines):
        for pat, score in anchor_patterns:
            if pat.search(line) and score > best_score:
                best_score = score
                best_idx = i

    if best_idx >= 0:
        return _window_around_line(lines, idx=best_idx, before=before, after=after, max_lines=max_lines)

    return None


def extract_error_excerpt(
    *,
    kind: str,
    stdout: str,
    stderr: str,
    max_lines: int = 120,
) -> str | None:
    """Return a small excerpt that is likely to contain the actionable error.

    kind:
      - "gradle"
      - "python"
      - other values fall back to generic anchors
    """
    combined = (stdout or "") + "\n" + (stderr or "")

    if kind == "gradle":
        extracted = extract_gradle_errors(stdout, stderr)
        if extracted:
            return _limit_lines(extracted, max_lines=max_lines)
        return _best_anchor_excerpt(
            combined,
            anchor_patterns=_GRADLE_ANCHOR_PATTERNS,
            before=3,
            after=25,
            max_lines=max_lines,
        )

    if kind == "python":
        # Prefer pyright/ruff/pytest anchors; otherwise fall back to generic error-like lines.
        return _best_anchor_excerpt(
            combined,
            anchor_patterns=_PYTHON_ANCHOR_PATTERNS,
            before=2,
            after=35,
            max_lines=max_lines,
        )

    # Generic fallback
    excerpt = _best_anchor_excerpt(
        combined,
        anchor_patterns=_GENERIC_ANCHOR_PATTERNS,
        before=2,
        after=25,
        max_lines=max_lines,
    )
    if excerpt:
        return excerpt

    # Last lines fallback
    lines = [ln.rstrip("\n") for ln in combined.splitlines() if ln.strip()]
    if not lines:
        return None
    tail = lines[-min(max_lines, len(lines)) :]
    if len(lines) > len(tail):
        tail = [f"... ({len(lines) - len(tail)} lines omitted)"] + tail
    return "\n".join(tail).strip()
