"""Helper for detecting and suggesting missing dependencies.

This module analyzes test output and exceptions to identify missing
dependencies and provide helpful installation suggestions.
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Dict, List, Optional, Set

# Common package name mappings (import name -> pip package name)
PACKAGE_NAME_MAPPINGS: Dict[str, str] = {
    "cv2": "opencv-python",
    "PIL": "Pillow",
    "sklearn": "scikit-learn",
    "yaml": "pyyaml",
    "bs4": "beautifulsoup4",
    "dateutil": "python-dateutil",
    "dotenv": "python-dotenv",
    "jwt": "pyjwt",
    "faker": "Faker",
    "redis": "redis",
    "psycopg2": "psycopg2-binary",
    "mysql": "mysql-connector-python",
    "pymongo": "pymongo",
    "boto3": "boto3",
    "googleapiclient": "google-api-python-client",
    "flask": "Flask",
    "django": "Django",
    "fastapi": "fastapi",
    "aiohttp": "aiohttp",
    "httpx": "httpx",
    "requests": "requests",
    "pytest": "pytest",
    "trio": "trio",
    "anyio": "anyio",
    "simplejson": "simplejson",
    "ujson": "ujson",
    "orjson": "orjson",
    "msgpack": "msgpack",
    "lxml": "lxml",
    "numpy": "numpy",
    "pandas": "pandas",
    "scipy": "scipy",
    "tensorflow": "tensorflow",
    "torch": "torch",
    "transformers": "transformers",
    "celery": "celery",
    "dramatiq": "dramatiq",
    "pika": "pika",
    "kombu": "kombu",
    "sqlalchemy": "sqlalchemy",
    "alembic": "alembic",
    "asyncpg": "asyncpg",
    "aiopg": "aiopg",
    "aiomysql": "aiomysql",
    "motor": "motor",
    "beanie": "beanie",
    "odmantic": "odmantic",
    "pydantic": "pydantic",
    "attrs": "attrs",
    "marshmallow": "marshmallow",
    "cerberus": "cerberus",
    "voluptuous": "voluptuous",
    "click": "click",
    "typer": "typer",
    "rich": "rich",
    "colorama": "colorama",
    "tqdm": "tqdm",
    "loguru": "loguru",
    "structlog": "structlog",
    "sentry_sdk": "sentry-sdk",
    "opentelemetry": "opentelemetry-api",
    "prometheus_client": "prometheus-client",
    "grpc": "grpcio",
    "grpcio": "grpcio",
    "protobuf": "protobuf",
    "arrow": "arrow",
    "pendulum": "pendulum",
    "freezegun": "freezegun",
    "faker": "Faker",
    "factory_boy": "factory-boy",
    "hypothesis": "hypothesis",
    "pytest_asyncio": "pytest-asyncio",
    "pytest_mock": "pytest-mock",
    "pytest_cov": "pytest-cov",
    "responses": "responses",
    "httpretty": "httpretty",
    "respx": "respx",
    "aioresponses": "aioresponses",
    "fakeredis": "fakeredis",
    "moto": "moto",
    "localstack": "localstack",
}


@dataclass
class MissingDependency:
    """A missing dependency detected from test output."""

    module_name: str
    package_name: str
    import_line: Optional[str] = None
    file_path: Optional[str] = None

    def install_command(self) -> str:
        """Get the pip install command for this dependency."""
        return f"pip install {self.package_name}"


def extract_missing_modules(output: str) -> List[MissingDependency]:
    """Extract missing module names from test output.

    Args:
        output: Test output (stdout + stderr)

    Returns:
        List of missing dependencies detected
    """
    missing: List[MissingDependency] = []
    seen_modules: Set[str] = set()

    # Pattern 1: ModuleNotFoundError: No module named 'xyz'
    pattern1 = re.compile(
        r"ModuleNotFoundError:\s*No module named ['\"]([^'\"]+)['\"]",
        re.IGNORECASE
    )

    # Pattern 2: ImportError: No module named xyz
    pattern2 = re.compile(
        r"ImportError:\s*No module named ['\"]?([a-zA-Z0-9_]+)['\"]?",
        re.IGNORECASE
    )

    # Pattern 3: E   ModuleNotFoundError: No module named 'xyz' (pytest format)
    pattern3 = re.compile(
        r"^E\s+ModuleNotFoundError:\s*No module named ['\"]([^'\"]+)['\"]",
        re.MULTILINE
    )

    # Pattern 4: from xyz import ... (in traceback context)
    pattern4 = re.compile(
        r"(?:from|import)\s+([a-zA-Z_][a-zA-Z0-9_]*)",
        re.IGNORECASE
    )

    for pattern in [pattern1, pattern2, pattern3]:
        for match in pattern.finditer(output):
            module_name = match.group(1)
            # Get the root module name (e.g., "foo" from "foo.bar.baz")
            root_module = module_name.split(".")[0]

            if root_module not in seen_modules:
                seen_modules.add(root_module)
                package_name = PACKAGE_NAME_MAPPINGS.get(root_module, root_module)
                missing.append(MissingDependency(
                    module_name=module_name,
                    package_name=package_name,
                ))

    # Pattern 4: from xyz import ... (in traceback context)
    # Only match modules that are in our known package mappings
    for match in pattern4.finditer(output):
        module_name = match.group(1)
        root_module = module_name.split(".")[0]
        if root_module not in seen_modules and root_module in PACKAGE_NAME_MAPPINGS:
            seen_modules.add(root_module)
            package_name = PACKAGE_NAME_MAPPINGS[root_module]
            missing.append(MissingDependency(
                module_name=module_name,
                package_name=package_name,
            ))

    return missing


def suggest_dependencies(output: str) -> Optional[str]:
    """Generate a helpful message about missing dependencies.

    Args:
        output: Test output (stdout + stderr)

    Returns:
        Suggestion message, or None if no missing dependencies
    """
    missing = extract_missing_modules(output)

    if not missing:
        return None

    lines = ["\n⚠️  Missing Dependencies Detected"]
    lines.append("-" * 40)

    for dep in missing:
        lines.append(f"  • {dep.module_name}")
        if dep.module_name != dep.package_name:
            lines.append(f"    Package: {dep.package_name}")

    lines.append("")
    lines.append("Install with:")

    if len(missing) == 1:
        lines.append(f"  {missing[0].install_command()}")
    else:
        packages = " ".join(dep.package_name for dep in missing)
        lines.append(f"  pip install {packages}")

    return "\n".join(lines)


def check_common_test_dependencies(test_file: str) -> List[str]:
    """Suggest common test dependencies based on test file content.

    Args:
        test_file: Content of the test file

    Returns:
        List of suggested packages to install
    """
    suggestions = []

    # Check for async tests
    if "@pytest.mark.asyncio" in test_file or "async def test_" in test_file:
        suggestions.append("pytest-asyncio")

    # Check for mocking
    if "from unittest.mock import" in test_file or "import mock" in test_file:
        pass  # Built-in
    elif "@mock" in test_file or "mocker." in test_file:
        suggestions.append("pytest-mock")

    # Check for coverage
    if "--cov" in test_file or "pytest-cov" in test_file:
        suggestions.append("pytest-cov")

    # Check for HTTP mocking
    if "responses." in test_file or "@responses.activate" in test_file:
        suggestions.append("responses")
    elif "httpretty." in test_file:
        suggestions.append("httpretty")
    elif "respx." in test_file:
        suggestions.append("respx")

    # Check for async HTTP mocking
    if "aioresponses" in test_file:
        suggestions.append("aioresponses")

    # Check for time mocking
    if "freeze_time" in test_file or "freezegun" in test_file:
        suggestions.append("freezegun")

    # Check for factory
    if "factory." in test_file or "factory_boy" in test_file:
        suggestions.append("factory-boy")

    # Check for faker
    if "from faker import" in test_file or "Faker()" in test_file:
        suggestions.append("Faker")

    # Check for hypothesis
    if "@given" in test_file and "hypothesis" in test_file:
        suggestions.append("hypothesis")

    # Check for fakeredis
    if "fakeredis" in test_file or "FakeRedis" in test_file:
        suggestions.append("fakeredis")

    return suggestions
