"""Structured error trace parsing for the LangGraph agent.

Parses stack traces from Python, Node.js, and generic formats into
structured frames with file:line references and auto-context.

This is a critical debugging effectiveness tool identified by the
effectiveness audit — reduces debug turns by 30-40% by eliminating
manual stack trace parsing.
"""

from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Optional


# ---------------------------------------------------------------------------
# Python traceback parser
# ---------------------------------------------------------------------------

_PY_FRAME_RE = re.compile(
    r'File "([^"]+)", line (\d+), in (\S+)'
)
_PY_EXCEPTION_RE = re.compile(
    r'^(\w+(?:\.\w+)*(?:Error|Exception|Warning|Interrupt|Exit|Failure))\s*:\s*(.*)',
    re.MULTILINE,
)
_PY_EXCEPTION_BARE_RE = re.compile(
    r'^(\w+(?:\.\w+)*(?:Error|Exception|Warning|Interrupt|Exit|Failure))\s*$',
    re.MULTILINE,
)


def parse_python_traceback(text: str) -> Optional[dict]:
    """Parse a Python traceback into structured frames.

    Returns dict with: exception_type, message, frames[], root_frame
    or None if no traceback found.
    """
    frames = []
    for match in _PY_FRAME_RE.finditer(text):
        filepath, line_str, func = match.groups()
        frames.append({
            "file": filepath,
            "line": int(line_str),
            "function": func,
            "code": "",
            "is_user_code": not _is_stdlib_path(filepath),
        })

    if not frames:
        return None

    # Try to extract code lines (they appear after each File line)
    lines = text.splitlines()
    for i, line in enumerate(lines):
        match = _PY_FRAME_RE.search(line)
        if match and i + 1 < len(lines):
            code_line = lines[i + 1].strip()
            if code_line and not code_line.startswith("File "):
                # Find the matching frame
                filepath = match.group(1)
                line_num = int(match.group(2))
                for frame in frames:
                    if frame["file"] == filepath and frame["line"] == line_num:
                        frame["code"] = code_line
                        break

    # Extract exception type and message
    exc_match = _PY_EXCEPTION_RE.search(text)
    if exc_match:
        exc_type = exc_match.group(1)
        exc_msg = exc_match.group(2).strip()
    else:
        bare_match = _PY_EXCEPTION_BARE_RE.search(text)
        if bare_match:
            exc_type = bare_match.group(1)
            exc_msg = ""
        else:
            exc_type = "Unknown"
            exc_msg = ""

    # Root frame = innermost user code frame, or last frame
    root_frame = None
    for frame in reversed(frames):
        if frame["is_user_code"]:
            root_frame = frame
            break
    if root_frame is None and frames:
        root_frame = frames[-1]

    return {
        "exception_type": exc_type,
        "message": exc_msg,
        "frames": frames,
        "root_frame": root_frame,
    }


def _is_stdlib_path(path: str) -> bool:
    """Check if a file path is from the standard library or site-packages."""
    indicators = [
        "/lib/python", "/site-packages/", "/dist-packages/",
        "<frozen ", "<string>", "<module>",
        "\\lib\\python", "\\site-packages\\",
    ]
    return any(ind in path for ind in indicators)


# ---------------------------------------------------------------------------
# Node.js error parser
# ---------------------------------------------------------------------------

_NODE_FRAME_RE = re.compile(
    r'at\s+(?:(.+?)\s+)?\(?((?:[A-Za-z]:)?[^\s():]+):(\d+):\d+\)?'
)
_NODE_ERROR_RE = re.compile(
    r'^(\w+(?:Error|Exception|TypeError|RangeError|ReferenceError))\s*:\s*(.*)',
    re.MULTILINE,
)


def parse_node_traceback(text: str) -> Optional[dict]:
    """Parse a Node.js stack trace into structured frames."""
    frames = []
    for match in _NODE_FRAME_RE.finditer(text):
        func = match.group(1) or "<anonymous>"
        filepath = match.group(2)
        line_num = int(match.group(3))
        frames.append({
            "file": filepath,
            "line": line_num,
            "function": func,
            "code": "",
            "is_user_code": not _is_node_internal(filepath),
        })

    if not frames:
        return None

    exc_match = _NODE_ERROR_RE.search(text)
    if exc_match:
        exc_type = exc_match.group(1)
        exc_msg = exc_match.group(2).strip()
    else:
        exc_type = "Error"
        exc_msg = ""

    root_frame = None
    for frame in frames:
        if frame["is_user_code"]:
            root_frame = frame
            break
    if root_frame is None and frames:
        root_frame = frames[0]

    return {
        "exception_type": exc_type,
        "message": exc_msg,
        "frames": frames,
        "root_frame": root_frame,
    }


def _is_node_internal(path: str) -> bool:
    """Check if a file path is from node_modules or Node internals."""
    indicators = [
        "node_modules/", "node:internal/", "node:events",
        "node:fs", "node:http", "node:net", "node:stream",
        "internal/modules/", "<anonymous>",
    ]
    return any(ind in path for ind in indicators)


# ---------------------------------------------------------------------------
# Generic error parser
# ---------------------------------------------------------------------------

_GENERIC_FILE_LINE_RE = re.compile(
    r'(?:^|\s)([\w./\\-]+\.\w+)[:\s]+(?:line\s+)?(\d+)',
    re.MULTILINE | re.IGNORECASE,
)


def parse_generic_error(text: str) -> Optional[dict]:
    """Parse any error text for file:line references."""
    frames = []
    seen = set()
    for match in _GENERIC_FILE_LINE_RE.finditer(text):
        filepath = match.group(1)
        line_num = int(match.group(2))
        key = f"{filepath}:{line_num}"
        if key in seen:
            continue
        seen.add(key)
        frames.append({
            "file": filepath,
            "line": line_num,
            "function": "",
            "code": "",
            "is_user_code": True,
        })

    if not frames:
        return None

    # Try to extract an error message from common patterns
    error_patterns = [
        re.compile(r'(?:error|Error|ERROR)[:\s]+(.*)', re.MULTILINE),
        re.compile(r'(?:failed|FAILED|Failed)[:\s]+(.*)', re.MULTILINE),
    ]
    exc_msg = ""
    for pat in error_patterns:
        m = pat.search(text)
        if m:
            exc_msg = m.group(1).strip()[:200]
            break

    return {
        "exception_type": "Error",
        "message": exc_msg,
        "frames": frames,
        "root_frame": frames[0] if frames else None,
    }


# ---------------------------------------------------------------------------
# Auto-context: read source lines around failure point
# ---------------------------------------------------------------------------

def add_source_context(
    result: dict,
    project_path: str,
    context_lines: int = 10,
) -> dict:
    """Add source code context around the root frame's failure point.

    Reads the file at root_frame.file and includes lines around root_frame.line.
    """
    root = result.get("root_frame")
    if not root or not root.get("file") or not root.get("line"):
        return result

    filepath = root["file"]
    line_num = root["line"]

    # Resolve relative paths
    full_path = Path(filepath)
    if not full_path.is_absolute():
        full_path = Path(project_path) / filepath
    if not full_path.exists():
        return result

    try:
        source_lines = full_path.read_text(encoding="utf-8", errors="replace").splitlines()
        start = max(0, line_num - context_lines - 1)
        end = min(len(source_lines), line_num + context_lines)
        context = []
        for i in range(start, end):
            marker = " >> " if i == line_num - 1 else "    "
            context.append(f"{marker}{i+1:4d} | {source_lines[i]}")
        result["source_context"] = "\n".join(context)
    except Exception:
        pass

    return result


# ---------------------------------------------------------------------------
# Main entry point
# ---------------------------------------------------------------------------

def parse_error(
    error_text: str,
    language: str = "auto",
    project_path: str = ".",
) -> str:
    """Parse an error trace and return structured JSON.

    Tries language-specific parser first, falls back to generic.

    Args:
        error_text: The error/stack trace text.
        language: "python", "node", "auto" (tries both).
        project_path: Project root for resolving relative file paths.

    Returns:
        JSON string with structured error information.
    """
    result = None

    if language in ("python", "auto"):
        result = parse_python_traceback(error_text)

    if result is None and language in ("node", "auto"):
        result = parse_node_traceback(error_text)

    if result is None:
        result = parse_generic_error(error_text)

    if result is None:
        return json.dumps({
            "exception_type": "Unknown",
            "message": error_text[:500],
            "frames": [],
            "root_frame": None,
        }, indent=2)

    # Add source context around the root frame
    result = add_source_context(result, project_path)

    return json.dumps(result, indent=2)
