"""Validator for CLI command docstring examples.

This module provides automated validation to ensure that CLI docstring examples
respect the positional argument order defined by the Typer command signature.
When a docstring example drifts from the actual command signature, the published
documentation becomes misleading. This validator catches such drift early.
"""

from __future__ import annotations

import importlib.util
import inspect
import logging
import re
import shlex
from collections.abc import Callable
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)


class DocstringExampleValidator:
    """Validates CLI command docstring examples against Typer signatures."""

    def __init__(self) -> None:
        """Initialize the validator."""
        self.errors: list[str] = []

    def validate_command(
        self,
        func: Callable[..., Any],
        command_name: str,
    ) -> bool:
        """Validate a single CLI command's docstring examples.

        Args:
            func: The CLI command function to validate.
            command_name: The full command name (e.g., "agents plan rollback").

        Returns:
            True if validation passes, False if errors were found.
        """
        docstring = inspect.getdoc(func)
        if not docstring:
            return True

        # Extract Examples section
        examples_match = re.search(
            r"Examples:\s*\n((?:(?:\n|.)*?)(?=\n\n|\Z))",
            docstring,
            re.MULTILINE,
        )
        if not examples_match:
            return True

        examples_text = examples_match.group(1).strip()
        if not examples_text:
            return True

        # Parse Typer parameters to get positional args and options
        positional_args = self._extract_positional_args(func)
        option_flags = self._extract_option_flags(func)

        # Validate each example line
        example_lines = [
            line.strip()
            for line in examples_text.split("\n")
            if line.strip() and not line.strip().startswith("#")
        ]

        for example_line in example_lines:
            if not self._validate_example_line(
                example_line,
                command_name,
                positional_args,
                option_flags,
            ):
                return False

        return True

    def _extract_positional_args(self, func: Callable[..., Any]) -> list[str]:
        """Extract required positional argument names from function signature.

        Only arguments without a default value (or with a default of
        ``inspect.Parameter.empty``) are considered required.  Optional
        positional arguments (those with a default) are excluded so that
        examples that omit them are not incorrectly rejected.

        Args:
            func: The CLI command function.

        Returns:
            List of required positional argument names in order.
        """
        positional_args = []
        sig = inspect.signature(func)

        for param_name, param in sig.parameters.items():
            if param_name in ("self", "cls"):
                continue

            # Check if this is a Typer Argument (positional)
            if param.annotation != inspect.Parameter.empty:
                annotation_str = str(param.annotation)
                if (
                    "Argument" in annotation_str
                    and param.default is inspect.Parameter.empty
                ):
                    positional_args.append(param_name)

        return positional_args

    def _extract_option_flags(self, func: Callable[..., Any]) -> set[str]:
        """Extract option flag names from function signature.

        Args:
            func: The CLI command function.

        Returns:
            Set of option flag names (e.g., "--yes", "--format").
        """
        option_flags = set()
        sig = inspect.signature(func)

        for param_name, param in sig.parameters.items():
            if param_name in ("self", "cls"):
                continue

            # Check if this is a Typer Option
            if param.annotation != inspect.Parameter.empty:
                annotation_str = str(param.annotation)
                if "Option" in annotation_str:
                    # Try to extract flag names from the annotation
                    # Common patterns: --flag, -f, --flag-name
                    flag_match = re.search(
                        r'["\'](-{1,2}[a-z0-9-]+)["\']',
                        annotation_str,
                    )
                    if flag_match:
                        option_flags.add(flag_match.group(1))
                    # Also add common flag patterns
                    option_flags.add(f"--{param_name.replace('_', '-')}")

        return option_flags

    def _validate_example_line(
        self,
        example_line: str,
        command_name: str,
        positional_args: list[str],
        option_flags: set[str],
    ) -> bool:
        """Validate a single example line.

        Args:
            example_line: The example command line to validate.
            command_name: The full command name.
            positional_args: List of positional argument names.
            option_flags: Set of option flag names.

        Returns:
            True if the example is valid, False otherwise.
        """
        try:
            # Parse the example line with shlex
            tokens = shlex.split(example_line)
        except ValueError as e:
            self.errors.append(
                f"[{command_name}] Failed to parse example: {example_line}\n"
                f"  Error: {e}"
            )
            return False

        # Skip if it doesn't start with the command
        if not tokens or not tokens[0].startswith("agents"):
            return True

        # Determine how many tokens the command name occupies so we can
        # correctly skip the command prefix when extracting positional values.
        # For example, "agents plan rollback" occupies 3 tokens.
        command_token_count = len(shlex.split(command_name))

        # Find where positional arguments end and options begin.
        # Scan from after the command prefix; the first token that starts
        # with "-" marks the boundary.
        positional_end_idx = command_token_count
        suffix_tokens = tokens[command_token_count:]
        for i, token in enumerate(suffix_tokens, start=command_token_count):
            if token.startswith("-"):
                break
            positional_end_idx = i + 1

        # Extract positional values from the example, skipping the command prefix
        positional_values = tokens[command_token_count:positional_end_idx]

        # Check if positional arguments appear before any options.
        # Validation applies only when the command has known required positional args.
        if positional_args:
            required_positional_count = len(positional_args)

            # Find the first option flag in the tokens (after the command prefix)
            first_option_idx = None
            for i, token in enumerate(suffix_tokens, start=command_token_count):
                if token.startswith("-"):
                    first_option_idx = i
                    break

            # If there are options, verify that all required positional args
            # appear before the first option flag.
            if (
                first_option_idx is not None
                and len(positional_values) < required_positional_count
            ):
                self.errors.append(
                    f"[{command_name}] Example violates positional argument"
                    f" order:\n"
                    f"  {example_line}\n"
                    f"  Expected positional arguments {positional_args} "
                    f"to appear before options.\n"
                    f"  Found {len(positional_values)} positional values, "
                    f"expected at least {required_positional_count}."
                )
                return False

        return True

    def validate_all_commands(self, commands_dir: Path) -> bool:
        """Validate all CLI commands in a directory.

        Args:
            commands_dir: Path to the CLI commands directory.

        Returns:
            True if all validations pass, False if any errors were found.
        """
        all_valid = True

        # Import all command modules
        for command_file in sorted(commands_dir.glob("*.py")):
            if command_file.name.startswith("_"):
                continue

            module_name = command_file.stem
            try:
                spec = importlib.util.spec_from_file_location(
                    f"cleveragents.cli.commands.{module_name}",
                    command_file,
                )
                if spec and spec.loader:
                    module = importlib.util.module_from_spec(spec)
                    spec.loader.exec_module(module)

                    # Find all functions that look like CLI commands
                    for name, obj in inspect.getmembers(module):
                        if (
                            inspect.isfunction(obj)
                            and not name.startswith("_")
                            and hasattr(obj, "__doc__")
                        ):
                            # Try to validate the command
                            cmd_suffix = name.replace("_", " ")
                            command_name = f"agents {module_name} {cmd_suffix}"
                            if not self.validate_command(obj, command_name):
                                all_valid = False

            except Exception as e:
                # Log at DEBUG level so import failures are observable without
                # blocking validation of other modules.
                logger.debug("Skipping module %s: %s", module_name, e)

        return all_valid

    def get_errors(self) -> list[str]:
        """Get all validation errors found.

        Returns:
            List of error messages.
        """
        return self.errors

    def print_errors(self) -> None:
        """Print all validation errors to stdout."""
        if self.errors:
            print("CLI Docstring Example Validation Errors:")
            print("=" * 70)
            for error in self.errors:
                print(error)
                print()
