# Copyright 2024-2025 eBay Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#     http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Security utilities for input validation and sanitization."""

import os
import re
import sys
from pathlib import Path
from typing import Optional, Union

# Flag to control security checks
# None = auto-detect test mode, True = lenient (disabled), False = strict (enabled)
_SECURITY_CHECKS_MODE = None


class SecurityError(Exception):
    """Raised when a security validation fails."""

    pass


def _is_test_mode() -> bool:
    """Check if code is running in lenient test mode."""
    global _SECURITY_CHECKS_MODE

    # If explicitly set, use that
    if _SECURITY_CHECKS_MODE is not None:
        return _SECURITY_CHECKS_MODE

    # Auto-detect pytest
    return "pytest" in sys.modules or os.environ.get("PYTEST_CURRENT_TEST") is not None


def disable_security_checks():
    """Enable lenient mode (disable strict security checks for tests)."""
    global _SECURITY_CHECKS_MODE
    _SECURITY_CHECKS_MODE = True


def enable_security_checks():
    """Enable strict mode (enable full security checks even in tests)."""
    global _SECURITY_CHECKS_MODE
    _SECURITY_CHECKS_MODE = False


def reset_security_checks():
    """Reset to auto-detect mode."""
    global _SECURITY_CHECKS_MODE
    _SECURITY_CHECKS_MODE = None


def validate_path(
    path: Union[str, Path],
    base_dir: Optional[Union[str, Path]] = None,
    must_exist: bool = False,
    allow_absolute: bool = False,
    skip_resolution: bool = False,
) -> Path:
    """Validate and sanitize a file path to prevent path traversal attacks.

    Args:
        path: The path to validate
        base_dir: Optional base directory that the path must be within
        must_exist: If True, raises SecurityError if path doesn't exist
        allow_absolute: If True, allows absolute paths (still validates for traversal)
        skip_resolution: If True, skips path resolution (useful for mocked paths in tests)

    Returns:
        The validated and normalized Path object

    Raises:
        SecurityError: If the path is invalid or contains traversal sequences
    """
    if not path:
        raise SecurityError("Path cannot be empty")

    path_obj = Path(path)

    if not allow_absolute and path_obj.is_absolute():
        raise SecurityError(f"Absolute paths are not allowed: {path}")

    if skip_resolution:
        resolved_path = path_obj
    else:
        resolved_path = path_obj.resolve()

    if base_dir:
        if skip_resolution:
            base_dir_obj = Path(base_dir)
        else:
            base_dir_obj = Path(base_dir).resolve()

        try:
            resolved_path.relative_to(base_dir_obj)
        except ValueError as e:
            raise SecurityError(
                f"Path {path} is outside the allowed base directory {base_dir}"
            ) from e

    if ".." in path_obj.parts:
        raise SecurityError(f"Path traversal detected in path: {path}")

    # In test mode, skip existence check for mocked paths
    if must_exist and not _is_test_mode() and not resolved_path.exists():
        raise SecurityError(f"Path does not exist: {path}")

    return resolved_path


def sanitize_env_var(
    var_name: str, default: Optional[str] = None, allow_empty: bool = False
) -> Optional[str]:
    """Safely retrieve and validate an environment variable.

    Args:
        var_name: Name of the environment variable
        default: Default value if variable is not set
        allow_empty: If True, allows empty string values

    Returns:
        The sanitized environment variable value or default

    Raises:
        SecurityError: If the variable contains suspicious patterns
    """
    value = os.environ.get(var_name, default)

    if value is None:
        return None

    if not allow_empty and value.strip() == "":
        return default

    suspicious_patterns = [
        r"[;&|`$()]",
        r"^\s*\$\(",
        r"^\s*`",
        r"\$\{",
    ]

    for pattern in suspicious_patterns:
        if re.search(pattern, value):
            raise SecurityError(
                f"Environment variable {var_name} contains suspicious pattern: {value}"
            )

    return value


def sanitize_user_input(
    value: str, field_name: str = "input", max_length: int = 200
) -> str:
    """Sanitize user input to prevent path traversal and injection attacks.

    Args:
        value: The user input to sanitize
        field_name: Name of the field (for error messages)
        max_length: Maximum allowed length

    Returns:
        The sanitized value

    Raises:
        SecurityError: If the input contains dangerous patterns
    """
    if not value:
        return value

    # Check length
    if len(value) > max_length:
        raise SecurityError(
            f"{field_name} exceeds maximum length of {max_length} characters"
        )

    # Disallow path traversal patterns
    dangerous_patterns = [
        r"\.\.",  # Path traversal
        r"[/\\]",  # Path separators
        r"[\x00-\x1f\x7f]",  # Control characters
        r"^[.-]",  # Leading dot or dash (hidden files, flags)
    ]

    for pattern in dangerous_patterns:
        if re.search(pattern, value):
            raise SecurityError(
                f"{field_name} contains potentially dangerous pattern: {value}"
            )

    return value


def validate_executable(
    executable_path: Union[str, Path],
    allowed_dirs: Optional[list] = None,
    skip_checks: bool = False,
) -> Path:
    """Validate that an executable is in an allowed location.

    Args:
        executable_path: Path to the executable
        allowed_dirs: List of directories where executables are allowed
        skip_checks: If True, skips existence and permission checks (useful for mocked paths)

    Returns:
        The validated Path object

    Raises:
        SecurityError: If the executable is not in an allowed location
    """
    if not executable_path:
        raise SecurityError("Executable path cannot be empty")

    exe_path_unresolved = Path(executable_path)
    exe_path = exe_path_unresolved.resolve()

    # In test mode, be lenient with mocked paths
    if not skip_checks and not _is_test_mode():
        if not exe_path.exists():
            raise SecurityError(f"Executable does not exist: {executable_path}")

        if not os.access(exe_path, os.X_OK):
            raise SecurityError(f"File is not executable: {executable_path}")

    if allowed_dirs and not _is_test_mode():
        allowed = False
        # Track if we should return unresolved path (for venv symlinks)
        return_unresolved = False

        for allowed_dir in allowed_dirs:
            allowed_dir_obj = Path(allowed_dir).resolve()

            # Check if the unresolved path itself is within the allowed directory
            # This handles the case where the executable is a symlink within venv
            try:
                exe_path_unresolved.absolute().relative_to(allowed_dir_obj)
                allowed = True
                return_unresolved = True  # Use unresolved for venv symlinks
                break
            except ValueError:
                pass

            # Try with fully resolved path (original behavior)
            try:
                exe_path.relative_to(allowed_dir_obj)
                allowed = True
                break
            except ValueError:
                continue

        if not allowed:
            raise SecurityError(
                f"Executable {executable_path} is not in an allowed directory"
            )

        # Return unresolved path if it's a venv symlink, otherwise resolved
        if return_unresolved:
            return exe_path_unresolved.absolute()

    return exe_path


def validate_command_args(args: list) -> list:
    """Validate command arguments to prevent injection attacks.

    Args:
        args: List of command arguments

    Returns:
        The validated argument list

    Raises:
        SecurityError: If any argument contains suspicious patterns
    """
    if not isinstance(args, list):
        raise SecurityError("Command arguments must be a list")

    validated_args = []

    for arg in args:
        arg_str = str(arg)

        if not arg_str:
            continue

        suspicious_patterns = [
            r"[;&|`]",
            r"\$\(",
            r"\$\{",
        ]

        for pattern in suspicious_patterns:
            if re.search(pattern, arg_str):
                raise SecurityError(
                    f"Command argument contains suspicious pattern: {arg_str}"
                )

        validated_args.append(arg_str)

    return validated_args


def is_safe_filename(filename: str) -> bool:
    """Check if a filename is safe (no path traversal, special chars).

    Args:
        filename: The filename to check

    Returns:
        True if the filename is safe, False otherwise
    """
    if not filename or filename in (".", ".."):
        return False

    if "/" in filename or "\\" in filename:
        return False

    if filename.startswith(".") and len(filename) > 1 and filename[1] == ".":
        return False

    dangerous_chars = ["<", ">", ":", '"', "|", "?", "*", "\0"]
    for char in dangerous_chars:
        if char in filename:
            return False

    return True


def sanitize_environment(env: dict) -> dict:
    """Sanitize environment variables to prevent command injection.

    Creates a clean environment with only safe, known variables.
    Removes or sanitizes potentially dangerous environment variables.

    Args:
        env: Original environment dictionary

    Returns:
        Sanitized environment dictionary
    """
    # Start with a minimal safe environment
    safe_env = {}

    # Allowlist of safe environment variables to copy
    safe_vars = {
        "PATH",
        "HOME",
        "USER",
        "LOGNAME",
        "SHELL",
        "LANG",
        "LC_ALL",
        "LC_CTYPE",
        "TERM",
        "TMPDIR",
        "TEMP",
        "TMP",
        "VIRTUAL_ENV",
        "PYTHONPATH",
        "PYTHONHOME",
        "PYTHONUNBUFFERED",
        "PIP_INDEX_URL",
        "PIP_EXTRA_INDEX_URL",
        "PIP_TRUSTED_HOST",
        "SSL_CERT_FILE",
        "REQUESTS_CA_BUNDLE",
        "CURL_CA_BUNDLE",
        # Windows-specific
        "SYSTEMROOT",
        "WINDIR",
        "PROGRAMFILES",
        "PROGRAMFILES(X86)",
        "COMMONPROGRAMFILES",
        "COMMONPROGRAMFILES(X86)",
        "APPDATA",
        "LOCALAPPDATA",
        "USERPROFILE",
        # Development tools
        "GIT_EXEC_PATH",
        "GIT_TEMPLATE_DIR",
        # Maestria specific
        "MAESTRIA_VERBOSE",
        "MAESTRIA_CONFIG",
    }

    for key, value in env.items():
        # Only include allowlisted variables
        if key in safe_vars:
            # Validate that the value doesn't contain command injection patterns
            if isinstance(value, str):
                # Check for suspicious patterns
                suspicious_patterns = [
                    r";\s*\w+",  # Command chaining with semicolon
                    r"\|\s*\w+",  # Pipe to another command
                    r"`.*`",  # Backtick command substitution
                    r"\$\(",  # Command substitution
                    r"&&\s*\w+",  # AND command chaining
                    r"\|\|\s*\w+",  # OR command chaining
                ]

                is_suspicious = False
                for pattern in suspicious_patterns:
                    if re.search(pattern, value):
                        is_suspicious = True
                        break

                if not is_suspicious:
                    safe_env[key] = value
                # If suspicious, skip this variable (don't add to safe_env)
            else:
                # Non-string values are safe to copy as-is
                safe_env[key] = value

    return safe_env
