#!/bin/bash

# ============================================================================
# Claude Code Statusline - Security Module
# ============================================================================
#
# This module contains all security-related functions including input
# sanitization, secure file operations, and validation routines.
#
# Error Suppression Patterns (Issue #108):
# - mkdir -p 2>/dev/null: Creating secure dirs (race condition safe)
# - chmod 700 2>/dev/null: Best-effort permissions (may fail on some FS)
# - rm -f 2>/dev/null: Cleanup lock/temp files (may not exist)
# - stat 2>/dev/null: Platform-specific stat (BSD vs GNU fallback)
# - kill -0 2>/dev/null: Check if PID exists (expected to fail for dead PIDs)
#
# Dependencies: core.sh
# ============================================================================

# Prevent multiple includes
[[ "${STATUSLINE_SECURITY_LOADED:-}" == "true" ]] && return 0
export STATUSLINE_SECURITY_LOADED=true

# Dependencies will be checked during initialization

# ============================================================================
# SECURITY CONSTANTS
# ============================================================================

# Maximum allowed path length
export MAX_PATH_LENGTH=1000

# Maximum iterations for path sanitization loops
export MAX_SANITIZATION_ITERATIONS=10

# Dangerous patterns for Python code validation
export DANGEROUS_PYTHON_PATTERNS=(
    "rm -rf" "rm -r" "rmdir" "unlink" "delete"
    "system(" "exec(" "eval(" "compile("
    "subprocess." "popen(" "call(" "run("
    "os.system" "os.popen" "os.execv" "os.spawn"
    "__import__" "importlib" "import subprocess"
    "urllib" "requests" "http" "socket" "ftp"
    "open(" "file(" "write(" "writelines("
    "shutil" "glob.glob" "pathlib" "tempfile"
    "; " "&&" "||" "|" ">" ">>" "<"
)

# ============================================================================
# PATH SANITIZATION FUNCTIONS
# ============================================================================

# Enhanced path sanitization (addresses security concern from original line 617)
sanitize_path_secure() {
    local path="$1"

    # Validate input
    if [[ -z "$path" ]]; then
        echo ""
        return 0
    fi

    # Check path length (prevent excessively long paths)
    if [[ ${#path} -gt $MAX_PATH_LENGTH ]]; then
        handle_warning "Path too long (${#path} chars, max $MAX_PATH_LENGTH)" "sanitize_path_secure"
        # Return truncated path instead of failing
        path="${path:0:$MAX_PATH_LENGTH}"
    fi

    # Security-first sanitization: remove path traversal sequences FIRST
    local sanitized="$path"

    # Iteratively remove path traversal patterns until none remain
    # This prevents bypass attempts like ....// -> ../
    local prev_sanitized=""
    local iteration_count=0

    while [[ "$sanitized" != "$prev_sanitized" ]] && [[ $iteration_count -lt $MAX_SANITIZATION_ITERATIONS ]]; do
        prev_sanitized="$sanitized"

        # Remove various path traversal patterns - using safe parameter expansion
        sanitized="${sanitized//..\/}"    # Remove ../
        sanitized="${sanitized//.\/}"     # Remove ./  
        sanitized="${sanitized//\/\///}"  # Remove double slashes -> single slash

        iteration_count=$((iteration_count + 1))
    done

    # Final cleanup: remove any remaining .. sequences completely
    sanitized="${sanitized//../removed-dotdot}"

    # Remove any remaining suspicious patterns using safe parameter expansion
    sanitized="${sanitized//..}"       # Remove any remaining ..
    sanitized="${sanitized//\~}"       # Remove ~
    sanitized="${sanitized//\$}"       # Remove $

    # Replace slashes with hyphens using safe parameter expansion  
    sanitized="${sanitized//\//-}"

    # Remove potentially dangerous characters, keep only safe ones (no dots for cache key compatibility)
    # Using printf to avoid echo vulnerabilities with tr
    sanitized=$(printf '%s' "$sanitized" | /usr/bin/tr -cd '[:alnum:]-_')

    # Ensure result is not empty
    if [[ -z "$sanitized" ]]; then
        sanitized="unknown-path"
    fi

    echo "$sanitized"
}

# Sanitize string for use as bash variable name
# Bash variable names can only contain: letters, numbers, underscores
# Must start with letter or underscore
sanitize_variable_name() {
    local input="$1"

    # Validate input
    if [[ -z "$input" ]]; then
        echo "var_unknown"
        return 0
    fi

    # Replace dots with underscores (main issue from #51)
    local sanitized="${input//./_}"

    # Replace hyphens with underscores
    sanitized="${sanitized//-/_}"

    # Remove any characters that aren't alphanumeric or underscore
    sanitized=$(printf '%s' "$sanitized" | /usr/bin/tr -cd '[:alnum:]_')

    # Ensure it doesn't start with a number (bash requirement)
    if [[ "$sanitized" =~ ^[0-9] ]]; then
        sanitized="var_${sanitized}"
    fi

    # Enforce maximum length to prevent excessively long variable names
    if [[ ${#sanitized} -gt 64 ]]; then
        sanitized="${sanitized:0:64}_truncated"
    fi

    # Ensure result is not empty
    if [[ -z "$sanitized" ]]; then
        sanitized="var_unknown"
    fi

    # Log sanitization events when input was modified (debug mode)
    if [[ "$input" != "$sanitized" ]] && [[ "${STATUSLINE_DEBUG:-false}" == "true" ]]; then
        [[ "${STATUSLINE_CORE_LOADED:-}" == "true" ]] && debug_log "Username sanitized: '$input' → '$sanitized'" "INFO"
    fi

    echo "$sanitized"
}

# ============================================================================
# SECURE FILE OPERATIONS
# ============================================================================

# Secure cache file creation with file locking to prevent race conditions
create_secure_cache_file() {
    local cache_file="$1"
    local content="$2"
    local lock_file="${cache_file}.lock"
    local max_retries=5
    local retry_count=0

    # Check required parameters
    if [[ -z "$cache_file" || -z "$content" ]]; then
        handle_error "create_secure_cache_file requires cache_file and content parameters" 1 "create_secure_cache_file"
        return 1
    fi

    # Create cache directory if it doesn't exist
    local cache_dir
    cache_dir=$(dirname "$cache_file")
    if [[ ! -d "$cache_dir" ]]; then
        mkdir -p "$cache_dir" 2>/dev/null
        chmod 700 "$cache_dir" 2>/dev/null
    fi

    # Acquire exclusive file lock using atomic noclobber (Issue #138)
    # Uses retry with random backoff instead of sleep polling
    while [[ $retry_count -lt $max_retries ]]; do
        # Clean up stale lock before attempting
        if [[ -f "$lock_file" ]]; then
            local lock_age=$(($(date +%s) - $(stat -f %m "$lock_file" 2>/dev/null || stat -c %Y "$lock_file" 2>/dev/null || echo 0)))
            [[ $lock_age -gt 30 ]] && rm -f "$lock_file" 2>/dev/null
        fi

        # Try atomic lock acquisition
        if (set -C; echo "$$:$(date +%s)" >"$lock_file") 2>/dev/null; then
            break  # Lock acquired
        fi

        retry_count=$((retry_count + 1))
        # Random backoff to prevent thundering herd
        sleep "0.$((RANDOM % 3 + 1))"
    done

    if [[ $retry_count -ge $max_retries ]]; then
        handle_warning "Failed to acquire lock after $max_retries attempts, proceeding without lock: $cache_file" "create_secure_cache_file"
    fi

    # Create file with content atomically
    local write_status=1
    {
        # Use temporary file for atomic write
        local temp_file="${cache_file}.tmp.$$"

        # Write content to temporary file
        # Note: stderr suppressed - write_status captures success/failure
        echo "$content" >"$temp_file" 2>/dev/null
        write_status=$?

        if [[ $write_status -eq 0 && -f "$temp_file" ]]; then
            # Set secure permissions before moving
            # Note: chmod stderr suppressed - non-fatal, file still usable
            chmod 644 "$temp_file" 2>/dev/null

            # Atomic move to final location
            # Note: mv stderr suppressed - write_status captures success/failure
            mv "$temp_file" "$cache_file" 2>/dev/null
            write_status=$?
        else
            handle_error "Failed to write to temporary cache file: $temp_file" 1 "create_secure_cache_file"
            # Note: rm stderr suppressed - file may not exist
            rm -f "$temp_file" 2>/dev/null
        fi

        # Clean up temporary file if move failed
        # Note: rm stderr suppressed - file may already be removed
        [[ -f "$temp_file" ]] && rm -f "$temp_file" 2>/dev/null

        # Release lock
        # Note: rm stderr suppressed - lock may not exist if acquisition failed
        rm -f "$lock_file" 2>/dev/null

        # Verify final result
        if [[ $write_status -eq 0 && -f "$cache_file" ]]; then
            # Verify permissions were set correctly
            local perms
            perms=$(get_file_permissions "$cache_file")
            if [[ -n "$perms" && "$perms" != "644" ]]; then
                handle_warning "Cache file has unexpected permissions: $perms (expected: 644)" "create_secure_cache_file"
                # Try to fix permissions
                chmod 644 "$cache_file" 2>/dev/null
            fi
            return 0
        else
            handle_error "Failed to create secure cache file: $cache_file" 1 "create_secure_cache_file"
            return 1
        fi
    }
}

# ============================================================================
# INPUT VALIDATION FUNCTIONS
# ============================================================================

# ANSI color code validation for custom themes
validate_ansi_color() {
    local color_value="$1"
    local color_name="${2:-unknown}"

    # Check if color value is provided
    if [[ -z "$color_value" ]]; then
        return 1 # Empty color
    fi

    # Valid ANSI color patterns:
    # 1. Basic ANSI: \\033[30-37m, \\033[90-97m
    # 2. 256-color: \\033[38;5;0-255m, \\033[48;5;0-255m
    # 3. RGB: \\033[38;2;r;g;bm, \\033[48;2;r;g;bm
    # 4. Reset and formatting: \\033[0-9m

    local valid_patterns=(
        # Basic ANSI colors (30-37 for foreground, 40-47 for background)
        '^\\033\[[39][0-7]m$'
        # Bright ANSI colors (90-97 for bright foreground, 100-107 for bright background)
        '^\\033\[1[0-9][0-7]m$'
        # 256-color format (38;5;n for foreground, 48;5;n for background)
        '^\\033\[38;5;[0-9]{1,3}m$'
        '^\\033\[48;5;[0-9]{1,3}m$'
        # RGB format (38;2;r;g;b for foreground, 48;2;r;g;b for background)
        '^\\033\[38;2;[0-9]{1,3};[0-9]{1,3};[0-9]{1,3}m$'
        '^\\033\[48;2;[0-9]{1,3};[0-9]{1,3};[0-9]{1,3}m$'
        # Text formatting codes (0-9, some multi-digit)
        '^\\033\[[0-9]{1,2}m$'
    )

    # Check against valid patterns
    local is_valid=false
    for pattern in "${valid_patterns[@]}"; do
        if [[ "$color_value" =~ $pattern ]]; then
            is_valid=true
            break
        fi
    done

    if [[ "$is_valid" != "true" ]]; then
        handle_warning "Invalid ANSI color code for '$color_name': $color_value" "validate_ansi_color"
        debug_log "Valid formats: \\033[31m, \\033[38;5;208m, \\033[38;2;255;100;50m" "INFO"
        return 2 # Invalid format
    fi

    # Additional validation for 256-color values (0-255 range)
    local color_256_pattern='\\033\[(38|48);5;([0-9]+)m'
    if [[ "$color_value" =~ $color_256_pattern ]]; then
        local color_num="${BASH_REMATCH[2]}"
        if [[ "$color_num" -gt 255 ]]; then
            handle_warning "256-color code out of range for '$color_name': $color_num (max: 255)" "validate_ansi_color"
            return 3 # Out of range
        fi
    fi

    # Additional validation for RGB values (0-255 range for each component)
    local rgb_pattern='\\033\[(38|48);2;([0-9]+);([0-9]+);([0-9]+)m'
    if [[ "$color_value" =~ $rgb_pattern ]]; then
        local r="${BASH_REMATCH[2]}"
        local g="${BASH_REMATCH[3]}"
        local b="${BASH_REMATCH[4]}"

        if [[ "$r" -gt 255 || "$g" -gt 255 || "$b" -gt 255 ]]; then
            handle_warning "RGB color values out of range for '$color_name': R=$r G=$g B=$b (max: 255)" "validate_ansi_color"
            return 4 # RGB out of range
        fi
    fi

    return 0 # Valid color code
}

# Enhanced Python execution with better error handling
execute_python_safely() {
    local python_code="$1"
    local fallback_value="$2"

    # Check if Python is available
    if ! command_exists python3; then
        debug_log "Python3 not available, using fallback" "WARN"
        echo "$fallback_value"
        return 0
    fi

    # Validate input (comprehensive injection prevention)
    for pattern in "${DANGEROUS_PYTHON_PATTERNS[@]}"; do
        if [[ "$python_code" == *"$pattern"* ]]; then
            handle_error "Potentially dangerous Python code detected: $pattern" 1 "execute_python_safely"
            echo "$fallback_value"
            return 1
        fi
    done

    # Execute with timeout if available
    local result
    if command_exists timeout; then
        result=$(timeout 5s python3 -c "$python_code" 2>/dev/null)
    elif command_exists gtimeout; then
        result=$(gtimeout 5s python3 -c "$python_code" 2>/dev/null)
    else
        result=$(python3 -c "$python_code" 2>/dev/null)
    fi

    # Return result or fallback
    if [[ $? -eq 0 && -n "$result" ]]; then
        echo "$result"
    else
        echo "$fallback_value"
    fi
}

# Enhanced MCP server name parsing (addresses lines 374, 396-398 security concern)
parse_mcp_server_name_secure() {
    local line="$1"

    # Improved regex pattern that's more restrictive and secure
    # Only allow ASCII alphanumeric, underscore, and hyphen
    # Must start and end with alphanumeric
    if [[ "$line" =~ ^([a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]|[a-zA-Z0-9]): ]]; then
        local server_name="${BASH_REMATCH[1]}"

        # Additional validation: check length and character set
        if [[ ${#server_name} -gt 100 ]]; then
            handle_error "MCP server name too long: ${server_name:0:20}..." 1 "parse_mcp_server_name_secure"
            return 1
        fi

        # Ensure no dangerous characters slipped through
        if [[ "$server_name" =~ [^a-zA-Z0-9_-] ]]; then
            handle_error "Invalid characters in MCP server name: $server_name" 1 "parse_mcp_server_name_secure"
            return 1
        fi

        echo "$server_name"
        return 0
    fi

    return 1
}

# ============================================================================
# TIMEOUT VALIDATION
# ============================================================================

# Parse timeout string to numeric seconds for validation
# Supports formats: "10s", "2m", "30" (defaults to seconds)
parse_timeout_to_seconds() {
    local timeout_str="$1"
    local numeric_value
    local unit

    # Handle empty or null values
    [[ -z "$timeout_str" || "$timeout_str" == "null" ]] && return 1

    # Extract numeric value and unit
    if [[ "$timeout_str" =~ ^([0-9]+)([sm]?)$ ]]; then
        numeric_value="${BASH_REMATCH[1]}"
        unit="${BASH_REMATCH[2]}"

        # Convert to seconds based on unit
        case "$unit" in
        "m") echo $((numeric_value * 60)) ;;
        "s" | "") echo "$numeric_value" ;; # Default to seconds
        *) return 1 ;;                     # Invalid unit
        esac
        return 0
    else
        return 1 # Invalid format
    fi
}

# ============================================================================
# PLATFORM-AWARE FILE STAT HELPERS
# ============================================================================

# Get file modification time (cross-platform, BSD/GNU stat compatible)
# Returns: Unix timestamp (seconds since epoch) or 0 on failure
# Note: stderr suppressed on stat commands because BSD/GNU syntaxes differ;
# one will always fail depending on platform, which is expected behavior
get_file_mtime() {
    local file="$1"
    local mtime

    [[ ! -f "$file" ]] && echo "0" && return 1

    # Try BSD stat first (macOS), then GNU stat (Linux), then fallback
    if mtime=$(stat -f %m "$file" 2>/dev/null); then
        echo "$mtime"
        return 0
    elif mtime=$(stat -c %Y "$file" 2>/dev/null); then
        echo "$mtime"
        return 0
    else
        echo "0"
        return 1
    fi
}

# Get file permissions (cross-platform, BSD/GNU stat compatible)
# Returns: Octal permissions (e.g., "644") or empty string on failure
get_file_permissions() {
    local file="$1"
    local perms

    [[ ! -f "$file" ]] && return 1

    # Try BSD stat first (macOS), then GNU stat (Linux)
    if perms=$(stat -f %A "$file" 2>/dev/null); then
        echo "$perms"
        return 0
    elif perms=$(stat -c %a "$file" 2>/dev/null); then
        echo "$perms"
        return 0
    else
        return 1
    fi
}

# Get file size in bytes (cross-platform, BSD/GNU stat compatible)
# Returns: File size in bytes or 0 on failure
get_file_size() {
    local file="$1"
    local size

    [[ ! -f "$file" ]] && echo "0" && return 1

    # Try BSD stat first (macOS), then GNU stat (Linux)
    if size=$(stat -f %z "$file" 2>/dev/null); then
        echo "$size"
        return 0
    elif size=$(stat -c %s "$file" 2>/dev/null); then
        echo "$size"
        return 0
    else
        echo "0"
        return 1
    fi
}

# ============================================================================
# CACHE VALIDATION
# ============================================================================

# Check if cache file is fresh (not expired)
is_cache_fresh() {
    local cache_file="$1"
    local cache_duration="${2:-30}" # Default 30 seconds

    if [[ -f "$cache_file" ]]; then
        local file_mtime=$(get_file_mtime "$cache_file")
        local cache_age=$(($(get_timestamp) - file_mtime))
        [[ $cache_age -lt $cache_duration ]]
    else
        return 1
    fi
}

# Remove stale lock files from dead processes
cleanup_stale_locks() {
    local lock_file="$1"
    local max_age="${2:-120}" # Default 2 minutes

    if [[ -f "$lock_file" ]]; then
        local lock_content=$(cat "$lock_file" 2>/dev/null)
        local lock_mtime=$(get_file_mtime "$lock_file")
        local lock_age=$(($(get_timestamp) - lock_mtime))

        # Extract PID from lock file format: "instance:PID:timestamp" or "PID:timestamp:instance"
        local lock_pid
        if [[ "$lock_content" =~ :([0-9]+): ]]; then
            lock_pid="${BASH_REMATCH[1]}"  # PID is in the middle
        elif [[ "$lock_content" =~ ^([0-9]+): ]]; then
            lock_pid="${BASH_REMATCH[1]}"  # PID is at the start
        else
            lock_pid="$lock_content"       # Fallback: assume entire content is PID
        fi

        # Remove lock if process is dead OR lock is older than max_age
        if [[ -n "$lock_pid" ]] && (! kill -0 "$lock_pid" 2>/dev/null || [[ $lock_age -gt $max_age ]]); then
            rm -f "$lock_file" 2>/dev/null
            debug_log "Removed stale lock file: $lock_file (PID: $lock_pid, age: ${lock_age}s)" "INFO"
        fi
    fi
}

# ============================================================================
# XDG-COMPLIANT SECURE DIRECTORY FUNCTIONS (Issue #110)
# ============================================================================

# Get XDG-compliant runtime directory for session markers and locks
# Priority: XDG_RUNTIME_DIR -> CACHE_BASE_DIR -> fallback with user isolation
get_secure_runtime_dir() {
    local runtime_dir=""
    local user_id="${USER:-$(id -u 2>/dev/null || echo 'unknown')}"

    # Priority 1: XDG_RUNTIME_DIR (most secure, per-user, tmpfs)
    if [[ -n "${XDG_RUNTIME_DIR:-}" ]] && [[ -d "${XDG_RUNTIME_DIR}" ]] && [[ -w "${XDG_RUNTIME_DIR}" ]]; then
        runtime_dir="${XDG_RUNTIME_DIR}/claude-code-statusline"
    # Priority 2: /run/user/$UID (Linux standard)
    elif [[ -d "/run/user/$(id -u 2>/dev/null)" ]] && [[ -w "/run/user/$(id -u 2>/dev/null)" ]]; then
        runtime_dir="/run/user/$(id -u)/claude-code-statusline"
    # Priority 3: Use cache directory (already XDG-compliant)
    elif [[ -n "${CACHE_BASE_DIR:-}" ]]; then
        runtime_dir="${CACHE_BASE_DIR}/runtime"
    # Priority 4: Secure fallback with user isolation
    else
        runtime_dir="${TMPDIR:-/tmp}/.claude_statusline_runtime_${user_id}"
    fi

    # Create directory with secure permissions
    if [[ ! -d "$runtime_dir" ]]; then
        mkdir -p "$runtime_dir" 2>/dev/null && chmod 700 "$runtime_dir" 2>/dev/null
    fi

    echo "$runtime_dir"
}

# Get secure temporary directory for truly temporary files
# Priority: TMPDIR -> /tmp (with user isolation for sensitive files)
get_secure_temp_dir() {
    local temp_dir="${TMPDIR:-/tmp}"
    local user_id="${USER:-$(id -u 2>/dev/null || echo 'unknown')}"

    # For statusline-specific temp files, use isolated subdirectory
    local isolated_temp="${temp_dir}/.claude_statusline_${user_id}"

    # Create directory with secure permissions
    if [[ ! -d "$isolated_temp" ]]; then
        mkdir -p "$isolated_temp" 2>/dev/null && chmod 700 "$isolated_temp" 2>/dev/null
    fi

    echo "$isolated_temp"
}

# Get session marker path (for cost tracking instance isolation)
get_session_marker_path() {
    local instance_id="${1:-${CLAUDE_INSTANCE_ID:-${PPID:-$$}}}"
    local runtime_dir
    runtime_dir=$(get_secure_runtime_dir)
    echo "${runtime_dir}/session_${instance_id}"
}

# Cleanup session markers in secure runtime directory
cleanup_runtime_session_markers() {
    local runtime_dir
    runtime_dir=$(get_secure_runtime_dir)

    # Remove old markers (older than 24 hours)
    find "$runtime_dir" -name "session_*" -mtime +1 -delete 2>/dev/null || true

    # Remove orphaned markers (where the parent process no longer exists)
    local marker
    for marker in "$runtime_dir"/session_*; do
        [[ -f "$marker" ]] || continue

        local marker_pid="${marker##*_}"
        if [[ "$marker_pid" =~ ^[0-9]+$ ]] && ! kill -0 "$marker_pid" 2>/dev/null; then
            rm -f "$marker" 2>/dev/null
            debug_log "Removed orphaned session marker for dead process: $marker_pid" "INFO"
        fi
    done
}

# ============================================================================
# MODULE INITIALIZATION
# ============================================================================

# Initialize the security module
init_security_module() {
    debug_log "Security module initialized" "INFO"
    return 0
}

# Initialize the module (skip during testing to allow sourcing without side effects)
if [[ "${STATUSLINE_TESTING:-}" != "true" ]]; then
    init_security_module
fi

# Export security functions
export -f sanitize_path_secure sanitize_variable_name create_secure_cache_file validate_ansi_color get_file_mtime get_file_permissions get_file_size
export -f execute_python_safely parse_mcp_server_name_secure
export -f parse_timeout_to_seconds is_cache_fresh cleanup_stale_locks
export -f get_secure_runtime_dir get_secure_temp_dir get_session_marker_path cleanup_runtime_session_markers