"""
Terminal stream watcher for goCLI.

Reads the terminal output in real time and detects error signatures.
Each error category is defined as a pattern with a human-readable label.
The watcher is intentionally separated from the fix logic so both can
be extended independently.
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Optional


@dataclass(frozen=True)
class ErrorMatch:
    """Represents a detected error in terminal output."""

    category: str          # e.g. "python-missing-module"
    matched_text: str      # the raw lines that matched
    description: str       # short human-readable label


# Each pattern is a tuple of (compiled regex, category, description).
# The regex is tested against every new chunk of terminal output.
_PATTERNS: list[tuple[re.Pattern[str], str, str]] = [
    # Python ----------------------------------------------------------
    (
        re.compile(
            r"ModuleNotFoundError:\s+No module named ['\"]?(\S+)['\"]?",
            re.MULTILINE,
        ),
        "python-missing-module",
        "Python missing module",
    ),
    (
        re.compile(
            r"ImportError:\s+cannot import name ['\"]?(\S+)['\"]?",
            re.MULTILINE,
        ),
        "python-import-error",
        "Python import error",
    ),
    (
        re.compile(r"SyntaxError:", re.MULTILINE),
        "python-syntax-error",
        "Python syntax error",
    ),
    (
        re.compile(
            r"FileNotFoundError:\s+\[Errno 2\]",
            re.MULTILINE,
        ),
        "python-file-not-found",
        "Python file not found",
    ),

    # Node.js ----------------------------------------------------------
    (
        re.compile(
            r"Error:\s+Cannot find module ['\"](\S+)['\"]",
            re.MULTILINE,
        ),
        "node-missing-module",
        "Node.js missing package",
    ),
    (
        re.compile(
            r"ERR!\s+missing script:",
            re.MULTILINE,
        ),
        "npm-missing-script",
        "npm missing script",
    ),

    # Permission denied ------------------------------------------------
    (
        re.compile(
            r"Permission denied",
            re.MULTILINE | re.IGNORECASE,
        ),
        "permission-denied",
        "Permission denied",
    ),

    # Command not found ------------------------------------------------
    (
        re.compile(
            r"(command not found|not recognized as.*command)",
            re.MULTILINE | re.IGNORECASE,
        ),
        "command-not-found",
        "Command not found",
    ),

    # Port in use ------------------------------------------------------
    (
        re.compile(
            r"(address already in use|EADDRINUSE|port.*already in use)",
            re.MULTILINE | re.IGNORECASE,
        ),
        "port-in-use",
        "Port already in use",
    ),

    # Docker -----------------------------------------------------------
    (
        re.compile(
            r"(docker:.*Error|Cannot connect to the Docker daemon"
            r"|Error response from daemon)",
            re.MULTILINE | re.IGNORECASE,
        ),
        "docker-error",
        "Docker error",
    ),

    # Git --------------------------------------------------------------
    (
        re.compile(
            r"(fatal:.*|error:.*merge conflict|"
            r"error:.*failed to push|"
            r"error:.*not a git repository)",
            re.MULTILINE | re.IGNORECASE,
        ),
        "git-error",
        "Git error",
    ),

    # Rust / Cargo -----------------------------------------------------
    (
        re.compile(r"error\[E\d+\]:", re.MULTILINE),
        "rust-compiler-error",
        "Rust compiler error",
    ),

    # Generic catch-all  (kept last so specific patterns win) ----------
    (
        re.compile(
            r"^(Traceback \(most recent call last\):)",
            re.MULTILINE,
        ),
        "python-traceback",
        "Python traceback",
    ),
]


def detect_error(output: str) -> Optional[ErrorMatch]:
    """Scan *output* for known error signatures.

    Returns the first :class:`ErrorMatch` found, or ``None``.
    """
    for pattern, category, description in _PATTERNS:
        match = pattern.search(output)
        if match:
            # Grab up to 10 lines around the match for context.
            start = max(0, output.rfind("\n", 0, match.start()) + 1)
            # Walk forward to find end of relevant block.
            end = match.end()
            for _ in range(8):
                next_nl = output.find("\n", end + 1)
                if next_nl == -1:
                    end = len(output)
                    break
                end = next_nl
            matched_text = output[start:end].strip()

            return ErrorMatch(
                category=category,
                matched_text=matched_text,
                description=description,
            )

    return None
