# Copyright (C) 2026 GoodData Corporation

"""Built-in reusable processors shipped with aida-mcp."""

from __future__ import annotations

from collections.abc import Callable
from typing import Any, cast

from aida_mcp.output.error_excerpt import extract_error_excerpt
from aida_mcp.output.gradle_parser import extract_gradle_errors
from aida_mcp.output.test_summary import create_test_failure_summary, extract_test_summary
from aida_mcp.tools.validate.models import ExecutionResult, StepResult

BuiltinProcessor = Callable[[ExecutionResult, dict[str, object]], StepResult]

_TS_RUSH_NOISE_SUBSTRINGS = (
    "rush multi-project build tool",
    "project(s) succeeded",
    "project(s) failed",
    "project(s) are up to date",
    "to inspect, run",
    "to view detailed timing",
)

_TS_RUSH_NOISE_PREFIXES = (
    "done in ",
    "watching for changes",
    "starting ",
)


def _int_cfg(cfg: dict[str, object], key: str, default: int) -> int:
    v = cfg.get(key)
    return v if isinstance(v, int) and v >= 0 else default


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 _last_meaningful_line(text: str) -> str | None:
    lines = [ln.strip() for ln in text.splitlines()]
    if not lines:
        return None

    fallback: str | None = None
    for line in reversed(lines):
        if not line:
            continue
        if fallback is None:
            fallback = line

        low = line.lower()
        if any(token in low for token in _TS_RUSH_NOISE_SUBSTRINGS):
            continue
        if any(low.startswith(prefix) for prefix in _TS_RUSH_NOISE_PREFIXES):
            continue
        return line

    return fallback


def passthrough(execution: ExecutionResult, _cfg: dict[str, object]) -> StepResult:
    """Minimal processor: treat exit code as success and return combined output."""
    text = execution.stdout.strip() or execution.stderr.strip()
    if not text:
        text = f"(no output) exit_code={execution.exit_code}"
    log_paths = [str(execution.log_path)] if execution.log_path is not None else []
    return StepResult(
        success=execution.exit_code == 0,
        text=text,
        error_count=(0 if execution.exit_code == 0 else 1),
        log_paths=log_paths,
        meta={"exit_code": execution.exit_code},
    )


def external_json(execution: ExecutionResult, _cfg: dict[str, object]) -> StepResult:
    """Parse JSON emitted by our external-runner tools.

    Expected payload shape (best-effort):
    - success: bool
    - text: str
    - error_count: int (optional)
    - did_autofix / modified_files (optional)
    """
    raw = (execution.stdout or "").strip() or (execution.stderr or "").strip()
    if not raw:
        return passthrough(execution, _cfg)

    # External runners may stream log/progress lines and print the final JSON as the last line.
    # Be robust: try full text first, then fall back to the last non-empty line.
    try:
        import orjson

        loaded: Any
        try:
            loaded = orjson.loads(raw)
        except Exception:
            lines = [ln.strip() for ln in raw.splitlines() if ln.strip()]
            if not lines:
                return passthrough(execution, _cfg)
            loaded = orjson.loads(lines[-1])
    except Exception:
        # Not JSON (or truncated) - fall back to raw passthrough.
        return passthrough(execution, _cfg)

    if not isinstance(loaded, dict):
        return passthrough(execution, _cfg)

    payload = cast(dict[str, object], loaded)
    success = payload.get("success") is True
    text_obj = payload.get("text")
    text = text_obj if isinstance(text_obj, str) and text_obj.strip() else raw

    error_count_obj = payload.get("error_count")
    error_count: int | None = error_count_obj if isinstance(error_count_obj, int) else (0 if success else 1)

    did_autofix_obj = payload.get("did_autofix")
    did_autofix = did_autofix_obj if isinstance(did_autofix_obj, bool) else None

    modified_files_obj = payload.get("modified_files")
    modified_files: list[str] | None = None
    if isinstance(modified_files_obj, list):
        modified_files_any = cast(list[object], modified_files_obj)
        if all(isinstance(item, str) for item in modified_files_any):
            modified_files = cast(list[str], modified_files_any)

    tool_obj = payload.get("tool")
    tool = tool_obj if isinstance(tool_obj, str) and tool_obj else "external_json"

    log_paths = [str(execution.log_path)] if execution.log_path is not None else []
    return StepResult(
        success=success and execution.exit_code == 0,
        text=text,
        error_count=error_count,
        log_paths=log_paths,
        meta={"tool": tool, "exit_code": execution.exit_code},
        did_autofix=did_autofix,
        modified_files=modified_files,
    )


def pytest(execution: ExecutionResult, _cfg: dict[str, object]) -> StepResult:
    """Processor for pytest-like output (reusable across repos)."""
    stdout = execution.stdout
    stderr = execution.stderr

    if execution.exit_code == 0:
        summary = extract_test_summary(stdout, stderr) or "📊 Test Summary: all tests passed"
        text = summary
        error_count = 0
    else:
        max_failed = _int_cfg(_cfg, "max_failed_tests", 10)
        text = create_test_failure_summary(stdout, stderr, max_failed_tests=max_failed).strip() or (
            stdout.strip() or stderr.strip()
        )
        if not text:
            text = f"pytest failed (exit_code={execution.exit_code})"
        error_count = 1

    log_paths = [str(execution.log_path)] if execution.log_path is not None else []
    return StepResult(
        success=execution.exit_code == 0,
        text=text,
        error_count=error_count,
        log_paths=log_paths,
        meta={"exit_code": execution.exit_code},
    )


def gradle(execution: ExecutionResult, _cfg: dict[str, object]) -> StepResult:
    """Processor for Gradle output (reusable across repos)."""
    stdout = execution.stdout
    stderr = execution.stderr
    combined = (stdout or "") + "\n" + (stderr or "")

    if execution.exit_code == 0:
        test_summary = extract_test_summary(stdout, stderr)
        if test_summary:
            text = test_summary
        elif "BUILD SUCCESSFUL" in combined or "UP-TO-DATE" in combined:
            text = "🏗️ Gradle: BUILD SUCCESSFUL"
        else:
            text = stdout.strip() or stderr.strip() or f"(no output) exit_code={execution.exit_code}"
        error_count = 0
    else:
        max_lines = _int_cfg(_cfg, "max_excerpt_lines", 120)
        max_sections = _int_cfg(_cfg, "max_error_sections", 5)
        extracted = extract_gradle_errors(stdout, stderr, max_sections=max_sections)
        excerpt = extracted or extract_error_excerpt(kind="gradle", stdout=stdout, stderr=stderr, max_lines=max_lines)
        if isinstance(excerpt, str) and excerpt.strip():
            text = _limit_lines(excerpt, max_lines=max_lines)
        else:
            text = "🏗️ Gradle: BUILD FAILED"
        error_count = 1

    log_paths = [str(execution.log_path)] if execution.log_path is not None else []
    return StepResult(
        success=execution.exit_code == 0,
        text=text,
        error_count=error_count,
        log_paths=log_paths,
        meta={"exit_code": execution.exit_code},
    )


def typescript_rush_tail(execution: ExecutionResult, _cfg: dict[str, object]) -> StepResult:
    """Minimal TypeScript/Rush summary using the last meaningful output line."""
    combined = ((execution.stdout or "").strip() + "\n" + (execution.stderr or "").strip()).strip()
    last_line = _last_meaningful_line(combined) if combined else None
    if execution.exit_code == 0:
        text = last_line or "🧩 TypeScript/Rush: completed successfully"
        error_count = 0
    else:
        text = last_line or "🧩 TypeScript/Rush: failed"
        error_count = 1

    log_paths = [str(execution.log_path)] if execution.log_path is not None else []
    return StepResult(
        success=execution.exit_code == 0,
        text=text,
        error_count=error_count,
        log_paths=log_paths,
        meta={"exit_code": execution.exit_code},
    )


BUILTIN_PROCESSORS: dict[str, BuiltinProcessor] = {
    "passthrough": passthrough,
    "external_json": external_json,
    "pytest": pytest,
    "gradle": gradle,
    "typescript_rush_tail": typescript_rush_tail,
}
