"""
Tool Discovery - Auto-discover tool handlers in Python projects.

This module scans Python files for @tool decorators and ToolRegistry
usage patterns, then imports them to trigger registration.

Supports two patterns:
1. Decorator-based: @tool or @registry.tool decorators
2. Programmatic: registry.register_tool() calls

Usage:
    from cli.tool_discovery import discover_tools, ToolDiscoveryResult

    # Discover all tools in a package
    result = discover_tools(package="my_project")

    # Discover from a specific path
    result = discover_tools(path=Path("./src/my_project"))

    # Get the registry with all discovered handlers
    registry = result.registry
"""
from __future__ import annotations

import ast
import importlib.util
import logging
import sys
from dataclasses import dataclass, field
from importlib import import_module
from pathlib import Path
from typing import Optional, Set

logger = logging.getLogger(__name__)


@dataclass
class DiscoveredTool:
    """A tool discovered from a Python file."""

    name: str
    module: str
    source_file: Path
    projects: list[str] = field(default_factory=list)  # Project IDs this tool belongs to
    has_handler: bool = True

    def __repr__(self) -> str:
        return f"DiscoveredTool(name={self.name!r}, module={self.module!r}, projects={self.projects!r})"


@dataclass
class ToolDiscoveryResult:
    """Result of tool discovery."""

    tools: list[DiscoveredTool] = field(default_factory=list)
    modules_scanned: int = 0
    modules_with_tools: int = 0
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)

    @property
    def success(self) -> bool:
        return len(self.errors) == 0

    def __len__(self) -> int:
        return len(self.tools)


def find_project_root(start_path: Optional[Path] = None) -> Optional[Path]:
    """
    Find project root by looking for pyproject.toml or setup.py.

    Args:
        start_path: Starting directory (defaults to current working directory)

    Returns:
        Path to project root if found, None otherwise
    """
    if start_path is None:
        start_path = Path.cwd()

    current = start_path.resolve()

    # Search up to 10 levels
    for _ in range(10):
        if (current / "pyproject.toml").exists():
            return current
        if (current / "setup.py").exists():
            return current

        if current.parent == current:
            break
        current = current.parent

    return None


@dataclass
class ToolMetadata:
    """Metadata extracted from a @tool decorator."""
    name: str
    projects: list[str] = field(default_factory=list)


def _has_tool_decorator(filepath: Path) -> list[ToolMetadata]:
    """
    Check if file contains @tool or @registry.tool decorator using AST.

    Returns list of ToolMetadata with name and projects for each tool found.
    """
    tools: list[ToolMetadata] = []

    try:
        source = filepath.read_text(encoding="utf-8")
        tree = ast.parse(source)
    except (SyntaxError, UnicodeDecodeError) as e:
        logger.debug(f"Could not parse {filepath}: {e}")
        return []

    for node in ast.walk(tree):
        if isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
            for decorator in node.decorator_list:
                metadata = _extract_tool_metadata(decorator, node.name)
                if metadata:
                    tools.append(metadata)

    return tools


def _extract_tool_metadata(decorator: ast.expr, func_name: str) -> Optional[ToolMetadata]:
    """Extract tool name and projects from a tool decorator."""

    # @tool("name", ...) or @registry.tool("name", ...)
    if isinstance(decorator, ast.Call):
        func = decorator.func

        # Check if it's a tool decorator
        is_tool = False
        if isinstance(func, ast.Name) and func.id == "tool":
            is_tool = True
        elif isinstance(func, ast.Attribute) and func.attr == "tool":
            is_tool = True

        if not is_tool:
            return None

        # Extract name from first positional argument
        name = func_name  # Default to function name
        if decorator.args:
            first_arg = decorator.args[0]
            if isinstance(first_arg, ast.Constant) and isinstance(first_arg.value, str):
                name = first_arg.value

        # Extract projects from keyword arguments or positional args
        projects: list[str] = []

        # Check keyword arguments for projects=
        for keyword in decorator.keywords:
            if keyword.arg == "projects":
                projects = _extract_list_literal(keyword.value)
                break

        # If not found in kwargs, check if projects is 4th positional arg
        # (after name, description, schema)
        if not projects and len(decorator.args) >= 4:
            projects = _extract_list_literal(decorator.args[3])

        return ToolMetadata(name=name, projects=projects)

    # @tool (bare decorator without call - unlikely but handle it)
    if isinstance(decorator, ast.Name) and decorator.id == "tool":
        return ToolMetadata(name=func_name, projects=[])

    # @registry.tool (bare attribute without call)
    if isinstance(decorator, ast.Attribute) and decorator.attr == "tool":
        return ToolMetadata(name=func_name, projects=[])

    return None


def _extract_list_literal(node: ast.expr) -> list[str]:
    """Extract a list of strings from an AST list literal."""
    result: list[str] = []

    if isinstance(node, ast.List):
        for elt in node.elts:
            if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
                result.append(elt.value)

    return result


def _has_tool_registry(filepath: Path) -> bool:
    """
    Check if file contains ToolRegistry usage using AST.

    Detects:
    - ToolRegistry imports
    - ToolRegistry(...) instantiation
    - registry.register_tool(...) calls

    Returns True if ToolRegistry pattern is found.
    """
    try:
        source = filepath.read_text(encoding="utf-8")
        tree = ast.parse(source)
    except (SyntaxError, UnicodeDecodeError) as e:
        logger.debug(f"Could not parse {filepath}: {e}")
        return False

    has_registry_import = False
    has_registry_instance = False
    has_register_tool_call = False

    for node in ast.walk(tree):
        # Check for ToolRegistry import
        if isinstance(node, ast.ImportFrom):
            if node.module and "tools" in node.module:
                for alias in node.names:
                    if alias.name == "ToolRegistry":
                        has_registry_import = True

        # Check for ToolRegistry(...) instantiation
        if isinstance(node, ast.Call):
            if isinstance(node.func, ast.Name) and node.func.id == "ToolRegistry":
                has_registry_instance = True
            # Check for registry.register_tool(...) calls
            if isinstance(node.func, ast.Attribute) and node.func.attr == "register_tool":
                has_register_tool_call = True

    return has_registry_import and (has_registry_instance or has_register_tool_call)


def _is_tool_decorator(node: ast.expr) -> bool:
    """Check if an AST node is a tool decorator."""
    # @tool(...)
    if isinstance(node, ast.Call):
        return _is_tool_decorator(node.func)

    # @tool
    if isinstance(node, ast.Name):
        return node.id == "tool"

    # @registry.tool or @something.tool
    if isinstance(node, ast.Attribute):
        return node.attr == "tool"

    return False


def _package_to_path(package: str) -> Optional[Path]:
    """Convert package name to filesystem path."""
    try:
        module = import_module(package)
        if hasattr(module, "__path__"):
            return Path(module.__path__[0])
        elif hasattr(module, "__file__") and module.__file__:
            return Path(module.__file__).parent
    except ImportError:
        pass

    # Fallback: assume it's a relative path
    potential_path = Path(package.replace(".", "/"))
    if potential_path.exists():
        return potential_path

    # Try src/ prefix
    src_path = Path("src") / potential_path
    if src_path.exists():
        return src_path

    return None


def _path_to_module(filepath: Path, root: Path) -> Optional[str]:
    """Convert filepath to importable module name."""
    try:
        # Make paths absolute for comparison
        filepath = filepath.resolve()
        root = root.resolve()

        # Get relative path
        relative = filepath.relative_to(root)

        # Remove .py and convert / to .
        module = str(relative.with_suffix("")).replace("/", ".").replace("\\", ".")

        # If root is inside a package, we need the full module path
        # Try to find the package root
        parent = root
        parts = []
        while parent != parent.parent:
            if (parent / "__init__.py").exists():
                parts.insert(0, parent.name)
                parent = parent.parent
            else:
                break

        if parts:
            return ".".join(parts) + "." + module

        return module
    except ValueError:
        return None


def _detect_source_root(project_root: Path) -> Path:
    """Detect the source root directory."""
    # Common patterns
    if (project_root / "src").is_dir():
        # Check for package inside src/
        for item in (project_root / "src").iterdir():
            if item.is_dir() and (item / "__init__.py").exists():
                return project_root / "src"
        return project_root / "src"

    # Look for top-level package
    for item in project_root.iterdir():
        if item.is_dir() and (item / "__init__.py").exists():
            if item.name not in ("tests", "test", "docs", "examples"):
                return project_root

    return project_root


def discover_tools(
    package: Optional[str] = None,
    path: Optional[Path] = None,
    exclude: Optional[list[str]] = None,
) -> ToolDiscoveryResult:
    """
    Auto-discover tool handlers in a Python project.

    Scans Python files for @tool decorators using AST analysis,
    then imports the modules to trigger registration.

    Args:
        package: Package name to scan (e.g., "my_project")
        path: Directory path to scan (alternative to package)
        exclude: Patterns to exclude (default: tests, __pycache__, etc.)

    Returns:
        ToolDiscoveryResult with discovered tools and any errors
    """
    result = ToolDiscoveryResult()
    exclude = exclude or [
        "**/__pycache__/**",
        "**/test_*",
        "**/*_test.py",
        "**/tests/**",
        "**/test/**",
        "**/.venv/**",
        "**/venv/**",
        "**/node_modules/**",
    ]

    # Determine scan root
    if path:
        scan_root = Path(path).resolve()
    elif package:
        scan_root = _package_to_path(package)
        if not scan_root:
            result.errors.append(f"Could not find package: {package}")
            return result
    else:
        # Auto-detect: find project root and scan
        project_root = find_project_root()
        if not project_root:
            result.errors.append("Could not find project root (no pyproject.toml or setup.py)")
            return result
        scan_root = _detect_source_root(project_root)

    if not scan_root.exists():
        result.errors.append(f"Scan path does not exist: {scan_root}")
        return result

    logger.info(f"Scanning for tools in: {scan_root}")

    # Find all Python files
    py_files = list(scan_root.glob("**/*.py"))

    # Filter exclusions
    filtered_files = []
    for py_file in py_files:
        excluded = False
        for exc in exclude:
            if py_file.match(exc):
                excluded = True
                break
        if not excluded:
            filtered_files.append(py_file)

    logger.info(f"Found {len(filtered_files)} Python files to scan")

    # Track modules with tools
    modules_with_tools: Set[Path] = set()
    modules_with_registry: Set[Path] = set()
    discovered_tools: dict[str, DiscoveredTool] = {}

    # First pass: AST scan to find files with @tool decorators or ToolRegistry
    for py_file in filtered_files:
        result.modules_scanned += 1

        # Check for @tool decorators
        tool_metadata_list = _has_tool_decorator(py_file)
        if tool_metadata_list:
            modules_with_tools.add(py_file)
            for metadata in tool_metadata_list:
                if metadata.name not in discovered_tools:
                    discovered_tools[metadata.name] = DiscoveredTool(
                        name=metadata.name,
                        module=str(py_file),
                        source_file=py_file,
                        projects=metadata.projects,
                        has_handler=True,
                    )

        # Check for ToolRegistry usage (register_tool pattern)
        if _has_tool_registry(py_file):
            modules_with_registry.add(py_file)

    result.modules_with_tools = len(modules_with_tools) + len(modules_with_registry)
    logger.info(f"Found {len(modules_with_tools)} modules with @tool decorators")
    logger.info(f"Found {len(modules_with_registry)} modules with ToolRegistry")

    # Second pass: Import modules to trigger registration
    # First, ensure the scan root is in sys.path
    scan_root_str = str(scan_root.parent if (scan_root / "__init__.py").exists() else scan_root)
    if scan_root_str not in sys.path:
        sys.path.insert(0, scan_root_str)

    # Import ToolRegistry modules first so registry is populated before decorator modules
    all_modules_to_import = list(modules_with_registry) + list(modules_with_tools)

    for py_file in all_modules_to_import:
        module_name = _path_to_module(py_file, scan_root)
        if not module_name:
            # Try simpler approach
            rel = py_file.relative_to(scan_root)
            module_name = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")

        logger.debug(f"Importing module: {module_name} from {py_file}")

        try:
            # Try direct import first
            try:
                import_module(module_name)
            except ImportError:
                # Fallback: load from file path
                spec = importlib.util.spec_from_file_location(module_name, py_file)
                if spec and spec.loader:
                    module = importlib.util.module_from_spec(spec)
                    sys.modules[module_name] = module
                    spec.loader.exec_module(module)

            # Update discovered tools with module info
            for name, tool in discovered_tools.items():
                if tool.source_file == py_file:
                    tool.module = module_name

        except Exception as e:
            result.warnings.append(f"Could not import {module_name}: {e}")
            logger.warning(f"Could not import {module_name}: {e}")

    result.tools = list(discovered_tools.values())
    return result


def discover_tools_for_registry(
    registry: "ToolRegistry",  # noqa: F821
    package: Optional[str] = None,
    path: Optional[Path] = None,
) -> ToolDiscoveryResult:
    """
    Discover tools and verify they're registered in the given registry.

    This is a convenience function that discovers tools and cross-references
    them with an existing registry.

    Args:
        registry: The ToolRegistry to check against
        package: Package name to scan
        path: Directory path to scan

    Returns:
        ToolDiscoveryResult with discovered tools
    """
    result = discover_tools(package=package, path=path)

    # Update has_handler based on registry
    for tool in result.tools:
        reg_tool = registry.get_tool(tool.name)
        if reg_tool:
            tool.has_handler = reg_tool.has_handler
        else:
            tool.has_handler = False
            result.warnings.append(
                f"Tool '{tool.name}' found in code but not in registry"
            )

    return result
