"""Error analysis for detecting permission and authentication issues.

When a tool execution fails, this module analyzes the error to determine
if it was a permission/auth issue and identifies which API caused it.
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from enum import Enum


class ErrorCategory(Enum):
    """Categories of errors that can occur during tool execution."""
    PERMISSION = "permission"  # Auth/permission issues (401, 403)
    RATE_LIMIT = "rate_limit"  # Rate limiting (429)
    NETWORK = "network"  # Network/connection issues
    VALIDATION = "validation"  # Invalid input/parameters
    NOT_FOUND = "not_found"  # Resource not found (404)
    SERVER = "server"  # Server errors (500+)
    MISSING_DEPENDENCY = "missing_dependency"  # Missing Python module
    UNKNOWN = "unknown"


@dataclass
class ErrorAnalysis:
    """Result of analyzing an error."""
    category: ErrorCategory
    is_permission_error: bool
    api_name: str | None = None  # Identified API (e.g., "Stripe", "OpenAI")
    env_var_hint: str | None = None  # Suggested env var (e.g., "STRIPE_API_KEY")
    error_message: str = ""
    suggested_action: str | None = None
    missing_module: str | None = None  # For MISSING_DEPENDENCY errors
    pip_package: str | None = None  # Package name to install (may differ from module)


# Patterns for detecting permission/auth errors
PERMISSION_PATTERNS = [
    # HTTP status codes
    (r"\b401\b", "Unauthorized (401)"),
    (r"\b403\b", "Forbidden (403)"),
    (r"status.?code.*401", "Unauthorized (401)"),
    (r"status.?code.*403", "Forbidden (403)"),

    # Common auth error messages
    (r"invalid.*(api|access|auth).*(key|token|credential)", "Invalid credentials"),
    (r"(api|access|auth).*(key|token|credential).*invalid", "Invalid credentials"),
    (r"unauthorized", "Unauthorized"),
    (r"authentication\s+(failed|required|error)", "Authentication failed"),
    (r"access\s+denied", "Access denied"),
    (r"permission\s+denied", "Permission denied"),
    (r"forbidden", "Forbidden"),
    (r"insufficient.*permission", "Insufficient permissions"),
    (r"scope.*required", "Missing scope/permission"),
    (r"missing.*(api|auth).*(key|token)", "Missing API key"),
    (r"no.*(api|auth).*(key|token)", "Missing API key"),
    (r"expired.*(token|key|credential)", "Expired credentials"),
    (r"token.*expired", "Expired token"),
]

# Patterns for detecting rate limiting
RATE_LIMIT_PATTERNS = [
    (r"\b429\b", "Too Many Requests (429)"),
    (r"rate.?limit", "Rate limited"),
    (r"too\s+many\s+requests", "Too many requests"),
    (r"quota\s+exceeded", "Quota exceeded"),
    (r"throttl", "Throttled"),
]

# Known API patterns for identification
API_PATTERNS = {
    "OpenAI": {
        "patterns": [r"openai", r"api\.openai\.com", r"gpt-", r"davinci", r"text-embedding"],
        "env_var": "OPENAI_API_KEY",
    },
    "Anthropic": {
        "patterns": [r"anthropic", r"api\.anthropic\.com", r"claude"],
        "env_var": "ANTHROPIC_API_KEY",
    },
    "Stripe": {
        "patterns": [r"stripe", r"api\.stripe\.com", r"sk_live_", r"sk_test_", r"payment"],
        "env_var": "STRIPE_SECRET_KEY",
    },
    "GitHub": {
        "patterns": [r"github", r"api\.github\.com", r"ghp_", r"github_pat_"],
        "env_var": "GITHUB_TOKEN",
    },
    "AWS": {
        "patterns": [r"aws", r"amazonaws\.com", r"s3\.", r"dynamodb", r"lambda"],
        "env_var": "AWS_ACCESS_KEY_ID",
    },
    "Google Cloud": {
        "patterns": [r"googleapis\.com", r"google\s+cloud", r"gcloud"],
        "env_var": "GOOGLE_APPLICATION_CREDENTIALS",
    },
    "Twilio": {
        "patterns": [r"twilio", r"api\.twilio\.com"],
        "env_var": "TWILIO_AUTH_TOKEN",
    },
    "SendGrid": {
        "patterns": [r"sendgrid", r"api\.sendgrid\.com"],
        "env_var": "SENDGRID_API_KEY",
    },
    "Slack": {
        "patterns": [r"slack\.com/api", r"slack.?api", r"xoxb-", r"xoxp-"],
        "env_var": "SLACK_API_TOKEN",
    },
    "HuggingFace": {
        "patterns": [r"huggingface", r"hf_", r"api-inference\.huggingface"],
        "env_var": "HF_TOKEN",
    },
}

# Module name to pip package name mapping (when they differ)
MODULE_TO_PACKAGE = {
    "cv2": "opencv-python",
    "PIL": "Pillow",
    "sklearn": "scikit-learn",
    "yaml": "pyyaml",
    "bs4": "beautifulsoup4",
    "dotenv": "python-dotenv",
    "dateutil": "python-dateutil",
    "jose": "python-jose",
    "magic": "python-magic",
    "Crypto": "pycryptodome",
    "serial": "pyserial",
    "usb": "pyusb",
    "gi": "PyGObject",
}


def extract_missing_module(error: str, stderr: str = "") -> tuple[str | None, str | None]:
    """
    Extract the missing module name from a ModuleNotFoundError or ImportError.

    Returns:
        Tuple of (module_name, pip_package_name) or (None, None) if not found
    """
    combined = f"{error} {stderr}"

    # Pattern for ModuleNotFoundError: No module named 'xxx'
    match = re.search(r"No module named ['\"]([^'\"]+)['\"]", combined)
    if match:
        module = match.group(1).split(".")[0]  # Get top-level module
        package = MODULE_TO_PACKAGE.get(module, module)
        return module, package

    # Pattern for ImportError: cannot import name 'xxx' from 'yyy'
    match = re.search(r"cannot import name ['\"]([^'\"]+)['\"] from ['\"]([^'\"]+)['\"]", combined)
    if match:
        module = match.group(2).split(".")[0]
        package = MODULE_TO_PACKAGE.get(module, module)
        return module, package

    # Pattern for ModuleNotFoundError without quotes
    match = re.search(r"ModuleNotFoundError:\s*(\w+)", combined)
    if match:
        module = match.group(1)
        package = MODULE_TO_PACKAGE.get(module, module)
        return module, package

    return None, None


def is_missing_dependency_error(error: str, stderr: str = "") -> bool:
    """Check if an error is a missing module/dependency error."""
    combined = f"{error} {stderr}".lower()
    patterns = [
        r"no module named",
        r"modulenotfounderror",
        r"importerror.*cannot import",
        r"importerror.*no module",
    ]
    return any(re.search(p, combined, re.IGNORECASE) for p in patterns)


def is_permission_error(error: str, stderr: str = "") -> bool:
    """
    Check if an error is a permission/authentication error.

    Args:
        error: The error message
        stderr: Optional stderr output

    Returns:
        True if this appears to be a permission error
    """
    combined = f"{error} {stderr}".lower()

    for pattern, _ in PERMISSION_PATTERNS:
        if re.search(pattern, combined, re.IGNORECASE):
            return True

    return False


def is_rate_limit_error(error: str, stderr: str = "") -> bool:
    """
    Check if an error is a rate limit error.

    Args:
        error: The error message
        stderr: Optional stderr output

    Returns:
        True if this appears to be a rate limit error
    """
    combined = f"{error} {stderr}".lower()

    for pattern, _ in RATE_LIMIT_PATTERNS:
        if re.search(pattern, combined, re.IGNORECASE):
            return True

    return False


def identify_api(error: str, stderr: str = "", tool_env_vars: list[str] | None = None) -> tuple[str | None, str | None]:
    """
    Try to identify which API caused an error.

    Args:
        error: The error message
        stderr: Optional stderr output
        tool_env_vars: Optional list of env vars the tool uses (hints)

    Returns:
        Tuple of (api_name, env_var) or (None, None) if unknown
    """
    combined = f"{error} {stderr}".lower()

    # First check if tool's env vars hint at the API
    if tool_env_vars:
        for api_name, info in API_PATTERNS.items():
            if info["env_var"] in tool_env_vars:
                return api_name, info["env_var"]

    # Then scan error text for API patterns
    for api_name, info in API_PATTERNS.items():
        for pattern in info["patterns"]:
            if re.search(pattern, combined, re.IGNORECASE):
                return api_name, info["env_var"]

    return None, None


def analyze_error(
    error: str,
    stderr: str = "",
    tool_env_vars: list[str] | None = None,
) -> ErrorAnalysis:
    """
    Analyze an error to categorize it and extract useful information.

    Args:
        error: The error message
        stderr: Optional stderr output
        tool_env_vars: Optional list of env vars the tool uses

    Returns:
        ErrorAnalysis with categorization and suggestions
    """
    combined = f"{error} {stderr}"

    # Check for permission error
    if is_permission_error(error, stderr):
        api_name, env_var = identify_api(error, stderr, tool_env_vars)

        suggestion = "Check that your API key is valid and has the required permissions."
        if api_name:
            suggestion = f"Check your {api_name} API key ({env_var}) is valid and has required scopes."

        return ErrorAnalysis(
            category=ErrorCategory.PERMISSION,
            is_permission_error=True,
            api_name=api_name,
            env_var_hint=env_var,
            error_message=error,
            suggested_action=suggestion,
        )

    # Check for rate limit
    if is_rate_limit_error(error, stderr):
        api_name, env_var = identify_api(error, stderr, tool_env_vars)

        return ErrorAnalysis(
            category=ErrorCategory.RATE_LIMIT,
            is_permission_error=False,
            api_name=api_name,
            env_var_hint=env_var,
            error_message=error,
            suggested_action="Wait and retry, or upgrade your API plan for higher limits.",
        )

    # Check for 404
    if re.search(r"\b404\b|not\s+found", combined, re.IGNORECASE):
        return ErrorAnalysis(
            category=ErrorCategory.NOT_FOUND,
            is_permission_error=False,
            error_message=error,
            suggested_action="Check the resource URL or identifier is correct.",
        )

    # Check for 5xx
    if re.search(r"\b50[0-9]\b|server\s+error|internal\s+error", combined, re.IGNORECASE):
        api_name, env_var = identify_api(error, stderr, tool_env_vars)

        return ErrorAnalysis(
            category=ErrorCategory.SERVER,
            is_permission_error=False,
            api_name=api_name,
            error_message=error,
            suggested_action="This appears to be a server-side issue. Try again later.",
        )

    # Check for network issues
    if re.search(r"connection\s+(refused|reset|timeout)|network|dns|host\s+not\s+found", combined, re.IGNORECASE):
        return ErrorAnalysis(
            category=ErrorCategory.NETWORK,
            is_permission_error=False,
            error_message=error,
            suggested_action="Check your network connection and try again.",
        )

    # Check for missing dependency
    if is_missing_dependency_error(error, stderr):
        module, package = extract_missing_module(error, stderr)
        return ErrorAnalysis(
            category=ErrorCategory.MISSING_DEPENDENCY,
            is_permission_error=False,
            error_message=error,
            missing_module=module,
            pip_package=package,
            suggested_action=f"Install missing package: pip install {package}" if package else "Install the missing Python package.",
        )

    # Unknown error
    return ErrorAnalysis(
        category=ErrorCategory.UNKNOWN,
        is_permission_error=False,
        error_message=error,
    )
