from __future__ import annotations

import re
from dataclasses import dataclass


FLAG_RE = re.compile(r"(?<![\w-])(--[a-zA-Z0-9][\w-]*|-[a-zA-Z0-9])(?![\w-])")


@dataclass(slots=True)
class ParsedFailure:
    error_type: str
    hallucinated_flag: str | None = None
    missing_command: str | None = None
    raw_error: str = ""


def command_name(command: str) -> str:
    parts = command.strip().split()
    return parts[0] if parts else ""


def extract_cmd_name(command_text: str) -> str:
    """Extract the meaningful tool name from a command, piercing bash -c wrappers."""
    known = ["git", "npm", "npx", "pnpm", "yarn", "pip", "pip3", "rm", "docker", "kubectl", "python", "python3"]
    for tool in known:
        if re.search(rf"\b{tool}\b", command_text):
            return tool
    skip = {"bash", "sh", "zsh", "fish", "hallucinate", "exec", "-c", "-lc", "--"}
    for word in command_text.split():
        if word not in skip and not word.startswith("-"):
            return word
    return ""


def extract_hallucinated_flag(stderr_text: str) -> str | None:
    patterns = [
        r"(?:unrecognized|unknown|unsupported|illegal)\s+(?:option|flag)\s+['\"]?(--?[A-Za-z0-9][\w-]*)['\"]?",
        r"no such option:\s*(--?[A-Za-z0-9][\w-]*)",
        r"invalid option\s*--\s*['\"]?([A-Za-z0-9])['\"]?",
    ]
    for pattern in patterns:
        match = re.search(pattern, stderr_text, re.IGNORECASE)
        if match:
            token = match.group(1)
            if token and len(token) == 1:
                return f"-{token}"
            return token

    all_flags = FLAG_RE.findall(stderr_text)
    return all_flags[0] if all_flags else None


def extract_missing_command(stderr_text: str, command_text: str) -> str | None:
    match = re.search(r"^\s*([^\s:]+):\s+command not found\b", stderr_text, re.IGNORECASE | re.MULTILINE)
    if match:
        return match.group(1)
    first = command_name(command_text)
    return first or None


def classify_failure(command: str, stderr_text: str, exit_code: int) -> ParsedFailure:
    stderr = stderr_text or ""
    stderr_low = stderr.lower()

    if exit_code == 0:
        return ParsedFailure(error_type="ok", raw_error=stderr)

    # Permission
    if "permission denied" in stderr_low:
        return ParsedFailure(error_type="permission_denied", raw_error=stderr)

    # Disk full
    if "no space left on device" in stderr_low:
        return ParsedFailure(error_type="no_space", raw_error=stderr)

    # Port in use
    if "eaddrinuse" in stderr_low or "address already in use" in stderr_low:
        return ParsedFailure(error_type="port_in_use", raw_error=stderr)

    # Network errors
    if any(p in stderr_low for p in (
        "could not resolve host", "connection refused", "econnrefused",
        "network is unreachable", "no route to host", "ssl error",
        "certificate verify failed", "name or service not known",
    )):
        return ParsedFailure(error_type="network_error", raw_error=stderr)

    # Git: dirty working tree
    if any(p in stderr_low for p in (
        "please commit your changes", "you have unstaged changes",
        "your local changes to the following", "cannot pull with rebase",
        "would be overwritten by merge",
    )):
        return ParsedFailure(error_type="git_dirty", raw_error=stderr)

    # Git: merge conflict
    if "conflict" in stderr_low and any(p in stderr_low for p in (
        "automatic merge failed", "merge conflict", "fix conflicts",
    )):
        return ParsedFailure(error_type="git_conflict", raw_error=stderr)

    # Hallucinated flag — must come before git_branch_missing because git's
    # usage text contains "pathspec" which would otherwise win
    hallucinated_flag = extract_hallucinated_flag(stderr)
    if hallucinated_flag and any(
        token in stderr_low
        for token in (
            "unrecognized option", "unknown option", "unknown flag",
            "invalid option", "no such option", "illegal option",
            "unsupported option", "unsupported flag",
        )
    ):
        return ParsedFailure(
            error_type="invalid_flag",
            hallucinated_flag=hallucinated_flag,
            raw_error=stderr,
        )

    # Git: missing branch/ref
    if any(p in stderr_low for p in (
        "not something we can merge", "did not match any file",
        "unknown revision or path", "no such branch",
        "pathspec", "not a valid object name",
    )):
        return ParsedFailure(error_type="git_branch_missing", raw_error=stderr)

    # npm missing script
    if "missing script:" in stderr_low or "npm error missing script" in stderr_low:
        return ParsedFailure(error_type="npm_missing_script", raw_error=stderr)

    # Command not found — exit code 127 is universal in bash; also match
    # Windows cmd.exe English + localized variants
    _not_found_patterns = (
        "command not found",
        "not recognized as an internal or external command",  # Windows English
        "不是内部或外部命令",   # Windows Simplified Chinese
        "不是外部或内部命令",
        "찾을 수 없습니다",     # Windows Korean
        "n'est pas reconnu",    # Windows French
        "no se reconoce",       # Windows Spanish
    )
    if exit_code == 127 or any(p in stderr_low for p in _not_found_patterns):
        return ParsedFailure(
            error_type="missing_package",
            missing_command=extract_missing_command(stderr, command),
            raw_error=stderr,
        )

    # File not found (after command-not-found check)
    if "no such file or directory" in stderr_low:
        return ParsedFailure(error_type="file_not_found", raw_error=stderr)

    # Syntax error
    if any(p in stderr_low for p in (
        "syntax error near unexpected", "unexpected token",
        "syntax error:", "parse error",
    )):
        return ParsedFailure(error_type="syntax_error", raw_error=stderr)

    return ParsedFailure(error_type="generic_error", raw_error=stderr)
