"""
Common validation functions for all tools.

SEC-003: Path traversal protection
SEC-008: Input validation standardization
REL-004: Retry logic with exponential backoff
"""

import random
import time
from functools import wraps
from pathlib import Path
from typing import Callable, Optional, Tuple, Type, TypeVar

T = TypeVar("T")


# REL-004: Retry configuration
DEFAULT_MAX_RETRIES = 3
DEFAULT_BACKOFF_BASE = 1.0  # Base delay in seconds
DEFAULT_BACKOFF_MAX = 30.0  # Maximum delay in seconds
DEFAULT_JITTER = 0.1  # Random jitter factor (0.1 = 10%)


def retry_with_backoff(
    max_retries: int = DEFAULT_MAX_RETRIES,
    backoff_base: float = DEFAULT_BACKOFF_BASE,
    backoff_max: float = DEFAULT_BACKOFF_MAX,
    jitter: float = DEFAULT_JITTER,
    exceptions: Tuple[Type[Exception], ...] = (Exception,),
) -> Callable:
    """
    Decorator for retry logic with exponential backoff.

    REL-004: Handles transient failures with automatic retry.

    Args:
        max_retries: Maximum number of retry attempts
        backoff_base: Base delay in seconds (doubles each retry)
        backoff_max: Maximum delay cap
        jitter: Random jitter factor to prevent thundering herd
        exceptions: Tuple of exception types to retry on

    Returns:
        Decorated function with retry logic

    Example:
        @retry_with_backoff(max_retries=3, exceptions=(IOError, OSError))
        def read_file(path):
            return path.read_text()
    """
    def decorator(func: Callable[..., T]) -> Callable[..., T]:
        @wraps(func)
        def wrapper(*args, **kwargs) -> T:
            last_exception = None

            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e

                    if attempt == max_retries:
                        # Final attempt failed, raise the exception
                        raise

                    # Calculate delay with exponential backoff
                    delay = min(backoff_base * (2 ** attempt), backoff_max)

                    # Add jitter to prevent thundering herd
                    if jitter > 0:
                        delay = delay * (1 + random.uniform(-jitter, jitter))

                    time.sleep(delay)

            # This should never be reached, but just in case
            if last_exception:
                raise last_exception

        return wrapper
    return decorator


async def retry_with_backoff_async(
    func: Callable,
    *args,
    max_retries: int = DEFAULT_MAX_RETRIES,
    backoff_base: float = DEFAULT_BACKOFF_BASE,
    backoff_max: float = DEFAULT_BACKOFF_MAX,
    jitter: float = DEFAULT_JITTER,
    exceptions: Tuple[Type[Exception], ...] = (Exception,),
    **kwargs,
) -> T:
    """
    Async function retry with exponential backoff.

    REL-004: Handles transient failures with automatic retry.

    Args:
        func: Async function to call
        *args: Positional arguments for func
        max_retries: Maximum number of retry attempts
        backoff_base: Base delay in seconds
        backoff_max: Maximum delay cap
        jitter: Random jitter factor
        exceptions: Tuple of exception types to retry on
        **kwargs: Keyword arguments for func

    Returns:
        Result from func

    Example:
        result = await retry_with_backoff_async(
            fetch_data,
            url,
            max_retries=3,
            exceptions=(ConnectionError,)
        )
    """
    import asyncio

    last_exception = None

    for attempt in range(max_retries + 1):
        try:
            return await func(*args, **kwargs)
        except exceptions as e:
            last_exception = e

            if attempt == max_retries:
                raise

            delay = min(backoff_base * (2 ** attempt), backoff_max)
            if jitter > 0:
                delay = delay * (1 + random.uniform(-jitter, jitter))

            await asyncio.sleep(delay)

    if last_exception:
        raise last_exception


# Maximum input lengths (SEC-008)
MAX_PATH_LENGTH = 4096
MAX_CONTENT_LENGTH = 10 * 1024 * 1024  # 10MB
MAX_PATTERN_LENGTH = 1024
MAX_COMMAND_LENGTH = 8192


class PathValidationError(PermissionError):
    """Raised when path validation fails due to security concerns."""
    pass


class InputValidationError(ValueError):
    """Raised when input validation fails."""
    pass


def validate_path_within_workspace(
    resolved_path: Path,
    workspace_root: Path,
    allow_external: bool = False
) -> None:
    """
    Validate that a resolved path is within the workspace directory.

    SEC-003: Path traversal protection - ensures paths don't escape workspace
    after symlink resolution.

    Args:
        resolved_path: The fully resolved path (after resolve())
        workspace_root: The workspace root directory
        allow_external: If True, allows paths outside workspace (requires permission)

    Raises:
        PathValidationError: If path escapes workspace and allow_external is False
    """
    resolved_workspace = workspace_root.resolve()

    try:
        # Check if resolved_path is relative to workspace
        resolved_path.relative_to(resolved_workspace)
    except ValueError:
        if not allow_external:
            raise PathValidationError(
                f"Path traversal detected: '{resolved_path}' is outside workspace "
                f"'{resolved_workspace}'. Access to paths outside the workspace "
                f"is not allowed for security reasons."
            )


def validate_path_no_traversal(path_str: str) -> None:
    """
    Validate that a path string doesn't contain obvious traversal patterns.

    SEC-003: Early detection of path traversal attempts.

    Args:
        path_str: The path string to validate

    Raises:
        PathValidationError: If path contains suspicious patterns
    """
    # Check for null bytes (can be used to bypass checks)
    if '\x00' in path_str:
        raise PathValidationError(
            "Path contains null byte, which is not allowed for security reasons."
        )

    # Normalize path and check for excessive parent references
    normalized = Path(path_str)
    parts = normalized.parts

    # Count parent directory references
    parent_refs = sum(1 for part in parts if part == '..')

    # If there are more parent refs than other parts, this is suspicious
    non_parent_parts = len(parts) - parent_refs
    if parent_refs > non_parent_parts + 2:  # Allow some reasonable traversal
        raise PathValidationError(
            f"Path '{path_str}' contains excessive parent directory references. "
            f"This may indicate a path traversal attack."
        )


def validate_string_length(
    value: str,
    max_length: int,
    field_name: str
) -> None:
    """
    Validate string length is within acceptable bounds.

    SEC-008: Input validation standardization.

    Args:
        value: The string value to validate
        max_length: Maximum allowed length
        field_name: Name of the field for error messages

    Raises:
        InputValidationError: If string exceeds max length
    """
    if len(value) > max_length:
        raise InputValidationError(
            f"{field_name} exceeds maximum length of {max_length} characters "
            f"(got {len(value)} characters)."
        )


def validate_path_input(path_str: str) -> None:
    """
    Validate a path input string.

    SEC-008: Input validation standardization.

    Args:
        path_str: The path string to validate

    Raises:
        InputValidationError: If path is invalid
        PathValidationError: If path contains traversal attempts
    """
    if not isinstance(path_str, str):
        raise InputValidationError(f"path must be a string, got {type(path_str).__name__}")

    if not path_str.strip():
        raise InputValidationError("path cannot be empty")

    validate_string_length(path_str, MAX_PATH_LENGTH, "path")
    validate_path_no_traversal(path_str)


def validate_content_input(content: str) -> None:
    """
    Validate content input string.

    SEC-008: Input validation standardization.

    Args:
        content: The content string to validate

    Raises:
        InputValidationError: If content is invalid
    """
    if not isinstance(content, str):
        raise InputValidationError(f"content must be a string, got {type(content).__name__}")

    validate_string_length(content, MAX_CONTENT_LENGTH, "content")


def validate_pattern_input(pattern: str) -> None:
    """
    Validate a pattern input string (glob or regex).

    SEC-008: Input validation standardization.

    Args:
        pattern: The pattern string to validate

    Raises:
        InputValidationError: If pattern is invalid
    """
    if not isinstance(pattern, str):
        raise InputValidationError(f"pattern must be a string, got {type(pattern).__name__}")

    if not pattern.strip():
        raise InputValidationError("pattern cannot be empty")

    validate_string_length(pattern, MAX_PATTERN_LENGTH, "pattern")


def validate_command_input(command: str) -> None:
    """
    Validate a command input string.

    SEC-008: Input validation standardization.

    Args:
        command: The command string to validate

    Raises:
        InputValidationError: If command is invalid
    """
    if not isinstance(command, str):
        raise InputValidationError(f"command must be a string, got {type(command).__name__}")

    if not command.strip():
        raise InputValidationError("command cannot be empty")

    validate_string_length(command, MAX_COMMAND_LENGTH, "command")


def sanitize_path_for_display(path: Path, workspace_root: Path) -> str:
    """
    Sanitize a path for safe display in error messages.

    Prevents leaking full system paths in error messages.

    Args:
        path: The path to sanitize
        workspace_root: The workspace root directory

    Returns:
        A sanitized path string safe for display
    """
    try:
        return str(path.relative_to(workspace_root))
    except ValueError:
        # Path is outside workspace, show only the filename
        return f"<external>/{path.name}"
