Action.Logging.psm1

# ========================================
# Action.Logging Module
# ========================================
#
# Module Version : 2.4.0
# Date Created: 06-21-2025
# Last Revised: 08-31-2025
# Revision details: v2.4.0 - Minor documentation improvements and version consistency updates
# Created by : Gonzalo More
# Required files: None
# Required folders: None
#
# Licensed under the MIT License.
# See LICENSE file in the project root for full license information.
# ========================================

<#
.SYNOPSIS
    Action.Logging - Enhanced PowerShell Logging Module with async support.
 
.LINK
    For full documentation, see README.md
#>


# Enhanced logging structure with severity levels
enum LogLevel {
    ERROR = 1
    WARN = 2
    INFO = 3
    DEBUG = 4
    TRACE = 5
}

# Console color mapping for log levels
$script:LogLevelColors = @{
    [LogLevel]::ERROR = 'Red'
    [LogLevel]::WARN  = 'Yellow'
    [LogLevel]::INFO  = 'Green'
    [LogLevel]::DEBUG = 'Cyan'
    [LogLevel]::TRACE = 'Gray'
}

# Module-level variables for async logging
$script:LogQueue = [System.Collections.Concurrent.ConcurrentQueue[PSObject]]::new()
$script:LoggerRunspace = $null
$script:LoggerPowerShell = $null
$script:AsyncHandle = $null  # Store the async invocation handle
$script:CurrentLogFile = $null
$script:AsyncLoggingEnabled = $false
$script:ConsoleOutputEnabled = $true
$script:FileOutputEnabled = $true
$script:DefaultLogRetentionCount = 10
$script:MinLogLevel = [LogLevel]::INFO  # Minimum log level for filtering
$script:VerifiedPaths = @{}
$script:SessionFiles = @{}
$script:CustomScriptName = $null
$script:EffectiveScriptName = $null
$script:CustomLogPath = $null
$script:SyncHash = $null  # Synchronized hashtable for async logging

<#
.SYNOPSIS
    Internal function to initialize the effective script name for logging.
 
.DESCRIPTION
    Sets the $script:EffectiveScriptName variable based on custom name or auto-detection.
    Auto-detection is performed only once and the result is cached.
 
.NOTES
    This is an internal function not exported by the module.
#>

function Initialize-EffectiveScriptName {
    [CmdletBinding()]
    param()

    # If custom script name is set, use it
    if ($script:CustomScriptName) {
        $script:EffectiveScriptName = $script:CustomScriptName
        return
    }

    # Auto-detect the script name by walking the call stack
    try {
        $callStack = Get-PSCallStack
        $scriptName = $null

        # Walk through the call stack to find the first script that's not this module
        foreach ($frame in $callStack) {
            if ($frame.ScriptName -and
                $frame.ScriptName -ne $PSCommandPath -and
                -not $frame.ScriptName.EndsWith('Action.Logging.psm1')) {

                try {
                    $rawName = [System.IO.Path]::GetFileNameWithoutExtension($frame.ScriptName)
                    # Sanitize script name to remove invalid characters for filenames
                    $scriptName = $rawName -replace '[<>:"/\\|?*]', '_'
                    if ($scriptName -and $scriptName.Trim() -ne '') {
                        break
                    }
                }
                catch {
                    # Skip this frame if path processing fails
                    continue
                }
            }
        }

        # Use detected script name or fallback to module name
        if ($scriptName -and $scriptName.Trim() -ne '') {
            $script:EffectiveScriptName = $scriptName
        } else {
            $script:EffectiveScriptName = "Action.Logging"
        }
    }
    catch {
        # If call stack analysis fails entirely, use fallback
        Write-Warning "Failed to detect script name from call stack: $($_.Exception.Message)"
        $script:EffectiveScriptName = "Action.Logging"
    }
}

<#
.SYNOPSIS
    Internal function to format log messages consistently.
 
.DESCRIPTION
    Centralizes message formatting to ensure consistent timestamping and formatting
    across all logging functions. Makes future metadata additions easier.
 
.PARAMETER Message
    The message to format.
 
.PARAMETER Level
    The log level for the message.
 
.PARAMETER When
    Optional timestamp. Defaults to current time.
 
.OUTPUTS
    String. Formatted log entry with timestamp, level, and message.
 
.NOTES
    This is an internal function not exported by the module.
    Used by Write-EnhancedLog, Write-EnhancedLogAsync, and async worker.
#>

function Format-LogMessage {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Message,

        [Parameter(Mandatory = $true)]
        [LogLevel]$Level,

        [Parameter(Mandatory = $false)]
        [datetime]$When = (Get-Date)
    )

    return "$($When.ToString('yyyy-MM-dd HH:mm:ss')) [$Level] $Message"
}

<#
.SYNOPSIS
    Internal helper function to get the effective log path.
 
.DESCRIPTION
    Determines the log path based on custom settings or defaults.
    Used by Write-EnhancedLog and Start-AsyncLogger.
 
.NOTES
    - Internal function, not exported
    - Returns the effective log path
#>

function Get-EffectiveLogPath {
    [CmdletBinding()]
    [OutputType([string])]
    param()

    if ($script:CustomLogPath) {
        # Validate custom path format
        try {
            $validatedPath = [System.IO.Path]::GetFullPath($script:CustomLogPath)
            return $validatedPath
        }
        catch {
            Write-Warning "Invalid custom log path '$($script:CustomLogPath)': $($_.Exception.Message). Using default path."
            # Fall through to default path logic
        }
    }

    # Ensure effective script name is initialized
    if (-not $script:EffectiveScriptName) {
        Initialize-EffectiveScriptName
    }

    # Sanitize script name for safe path construction
    $safeName = $script:EffectiveScriptName -replace '[<>:"/\\|?*]', '_'
    if (-not $safeName -or $safeName.Trim() -eq '') {
        $safeName = "Action.Logging"
    }

    # Construct and validate default path
    try {
        $defaultPath = Join-Path -Path "C:\temp\logs" -ChildPath $safeName
        $validatedPath = [System.IO.Path]::GetFullPath($defaultPath)
        return $validatedPath
    }
    catch {
        Write-Warning "Failed to construct log path: $($_.Exception.Message). Using fallback path."
        return "C:\temp\logs\Action.Logging"
    }
}

<#
.SYNOPSIS
    Internal helper function to initialize log path and session file.
 
.DESCRIPTION
    Consolidates the common logic for path verification, directory creation,
    session file initialization, and log cleanup. Used by Write-EnhancedLog,
    Start-AsyncLogger, and Set-LogPath.
 
.PARAMETER Path
    The log directory path to initialize.
 
.NOTES
    - Internal function, not exported
    - Returns the full path to the session log file
    - Only performs operations if FileOutputEnabled is true
#>

function Initialize-LogPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Path
    )

    # Only perform operations if file output is enabled
    if (-not $script:FileOutputEnabled) {
        return $null
    }

    # Validate path format and length (Windows max path length is typically 260 characters)
    try {
        $fullPath = [System.IO.Path]::GetFullPath($Path)
        if ($fullPath.Length -gt 240) {  # Leave room for filename
            Write-Warning "Log path is too long (${fullPath.Length} characters). Maximum recommended is 240 characters to allow for filenames."
            return $null
        }
    }
    catch {
        Write-Warning "Invalid log path format '$Path': $($_.Exception.Message)"
        return $null
    }

    # Check if path has already been verified this session
    if (-not $script:VerifiedPaths.ContainsKey($fullPath)) {
        # Create directory if it doesn't exist
        if (-not (Test-Path $fullPath)) {
            try {
                $createdDir = New-Item -ItemType Directory -Path $fullPath -Force
                if (-not $createdDir -or -not (Test-Path $fullPath)) {
                    Write-Warning "Failed to verify log directory creation: '$fullPath'"
                    return $null
                }
            }
            catch {
                Write-Warning "Failed to create log directory '$fullPath': $($_.Exception.Message)"
                return $null
            }
        }

        # Test write permissions
        try {
            $testFile = Join-Path -Path $fullPath -ChildPath "write_test_$(Get-Date -Format 'yyyyMMddHHmmss').tmp"
            "test" | Out-File -FilePath $testFile -Force
            Remove-Item -Path $testFile -Force -ErrorAction SilentlyContinue
        }
        catch {
            Write-Warning "No write permission to log directory '$fullPath': $($_.Exception.Message)"
            return $null
        }

        $script:VerifiedPaths[$fullPath] = $true

        # Initialize effective script name if not already done
        if (-not $script:EffectiveScriptName) {
            Initialize-EffectiveScriptName
        }

        # Create session file name with script name prefix
        if ($script:EffectiveScriptName -eq "Action.Logging") {
            $logFileName = "ActionLogging_$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
        } else {
            $logFileName = "$($script:EffectiveScriptName)_$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
        }

        # Validate final file path length
        $sessionFile = Join-Path -Path $fullPath -ChildPath $logFileName
        if ($sessionFile.Length -gt 260) {
            Write-Warning "Generated log file path exceeds Windows limits (${sessionFile.Length} characters): '$sessionFile'"
            return $null
        }

        # Store the full path to the session file
        $script:SessionFiles[$fullPath] = $sessionFile

        # Clean up old logs with error handling
        try {
            Clear-OldLog -LogPath $fullPath
        }
        catch {
            Write-Warning "Failed to clean old logs in '$fullPath': $($_.Exception.Message)"
            # Continue anyway - this shouldn't prevent logging
        }
    }

    # Return the session file path for this log directory
    return $script:SessionFiles[$fullPath]
}

<#
.SYNOPSIS
    Writes enhanced log entries with severity levels and color coding.
 
.DESCRIPTION
    Creates timestamped log entries with severity levels, saves to log files,
    and displays color-coded output to the console. Automatically creates
    log directories if they don't exist.
 
.PARAMETER Message
    The message to log.
 
.PARAMETER Level
    The severity level of the log entry. Default is INFO.
 
.EXAMPLE
    Write-EnhancedLog -Message "Script started" -Level INFO
 
.EXAMPLE
    Write-EnhancedLog -Message "Critical error occurred" -Level ERROR
 
.EXAMPLE
    Write-EnhancedLog -Message "Debug information" -Level DEBUG
#>

function Write-EnhancedLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Message,

        [Parameter(Mandatory = $false)]
        [LogLevel]$Level = [LogLevel]::INFO
    )

    # Filter messages below minimum log level
    if ($Level -gt $script:MinLogLevel) { 
        return 
    }

    # Get the effective log path
    $LogPath = Get-EffectiveLogPath

    # Create formatted log entry using centralized formatter
    $logEntry = Format-LogMessage -Message $Message -Level $Level

    try {
        # Only handle file operations if file output is enabled
        if ($script:FileOutputEnabled) {
            # Initialize log path if needed and get session file
            $currentLogFile = Initialize-LogPath -Path $LogPath
            $script:CurrentLogFile = $currentLogFile

            # Write to log file if we have a valid session file
            if ($currentLogFile) {
                Add-Content -Path $currentLogFile -Value $logEntry -Encoding UTF8 -ErrorAction Stop
            }
        }

        # Output to console with color coding if enabled
        if ($script:ConsoleOutputEnabled) {
            $color = $script:LogLevelColors[$Level]
            if ($color) {
                Write-Host $logEntry -ForegroundColor $color
            } else {
                Write-Host $logEntry
            }
        }
    }
    catch {
        # Only show file write errors if file output was expected
        if ($script:FileOutputEnabled -and $script:ConsoleOutputEnabled) {
            Write-Warning "Failed to write to log file: $($_.Exception.Message)"
            # Fallback to console output only
            Write-Host "$logEntry" -ForegroundColor White
        }
    }
}

<#
.SYNOPSIS
    Cleans up old log files, keeping only the most recent ones.
 
.DESCRIPTION
    Removes old log files from the specified path, keeping only the last
    10 log files to manage disk space. Files are sorted by last write time.
 
.PARAMETER LogPath
    The path containing log files to clean up.
 
.PARAMETER KeepCount
    Number of recent log files to keep. Default is 10.
 
.EXAMPLE
    Clear-OldLog -LogPath "C:\temp\logs\MyScript"
 
.EXAMPLE
    Clear-OldLog -LogPath "C:\temp\logs\MyScript" -KeepCount 5
#>

function Clear-OldLog {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [string]$LogPath,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 100)]
        [int]$KeepCount = $script:DefaultLogRetentionCount
    )

    try {
        if (Test-Path $LogPath) {
            $logFiles = Get-ChildItem -Path $LogPath -Filter "*.log" |
                        Sort-Object LastWriteTime -Descending

            if ($logFiles.Count -gt $KeepCount) {
                $filesToDelete = $logFiles[$KeepCount..($logFiles.Count - 1)]
                foreach ($file in $filesToDelete) {
                    if ($PSCmdlet.ShouldProcess($file.FullName, 'Remove old log file')) {
                        Remove-Item -Path $file.FullName -Force -ErrorAction Stop
                    }
                }
                Write-Verbose "Cleaned up $($filesToDelete.Count) old log files from $LogPath"
            }
        }
    }
    catch {
        Write-Verbose "Failed to clean up old logs in '${LogPath}': $($_.Exception.Message)"
    }
}

<#
.SYNOPSIS
    Starts an asynchronous logging background worker for high-performance logging.
 
.DESCRIPTION
    Initializes a background PowerShell runspace that handles log writing asynchronously.
    This provides better performance for high-volume logging scenarios by queuing
    log entries and writing them in batches to reduce I/O overhead. Uses the path set
    by Set-LogPath or defaults to C:\temp\logs\[ScriptName].
 
.EXAMPLE
    Start-AsyncLogger
    Starts async logger using the configured or default log path.
 
.EXAMPLE
    Set-LogPath -Path "D:\MyApp\Logs"
    Start-AsyncLogger
    Starts async logger using the custom log path.
 
.NOTES
    - Only one async logger can run at a time per PowerShell session
    - Use Write-EnhancedLogAsync to write to the async logger
    - Use Stop-AsyncLogger to properly shutdown the background worker
    - Automatically creates log directories if they don't exist
#>

function Start-AsyncLogger {
    [CmdletBinding(SupportsShouldProcess)]
    param()

    # Don't start if already running
    if ($script:AsyncLoggingEnabled) {
        Write-Warning "Async logging is already running"
        return
    }

    if ($PSCmdlet.ShouldProcess("Async Logger", "Start background logging")) {

    # Initialize effective script name if not already set
    if (-not $script:EffectiveScriptName) {
        Initialize-EffectiveScriptName
    }

    # Get the effective log path
    $LogPath = Get-EffectiveLogPath

    # Initialize log path and get session file
    $script:CurrentLogFile = Initialize-LogPath -Path $LogPath

    # Create runspace for background logging
    $script:LoggerRunspace = [RunspaceFactory]::CreateRunspace()
    # Only set STA on Windows (not needed for logging anyway)
    if ($IsWindows -or $PSVersionTable.PSVersion.Major -le 5) {
        $script:LoggerRunspace.ApartmentState = [System.Threading.ApartmentState]::STA
    }
    $script:LoggerRunspace.ThreadOptions = "ReuseThread"
    $script:LoggerRunspace.Open()

    # Create synchronized hashtable for shared state
    $syncHash = [hashtable]::Synchronized(@{
        LogQueue = $script:LogQueue
        CurrentLogFile = $script:CurrentLogFile
        StopRequested = $false
        ConsoleOutputEnabled = $script:ConsoleOutputEnabled
        FileOutputEnabled = $script:FileOutputEnabled
        LogLevelColors = $script:LogLevelColors
        LastError = $null
        ErrorCount = 0
        HasErrors = $false
    })

    # Share synchronized hashtable with runspace
    $script:LoggerRunspace.SessionStateProxy.SetVariable("syncHash", $syncHash)

    # Create PowerShell instance for background work
    $script:LoggerPowerShell = [PowerShell]::Create()
    $script:LoggerPowerShell.Runspace = $script:LoggerRunspace

    # Define background worker script
    $workerScript = {
        $logBuffer = [System.Collections.Generic.List[string]]::new()
        $lastFlush = [DateTime]::Now
        $flushInterval = [TimeSpan]::FromMilliseconds(500)

        # Use color mapping from SyncHash
        $levelColors = $syncHash.LogLevelColors

        # Continue processing until stop is requested AND queue is empty
        while (-not $syncHash.StopRequested -or $syncHash.LogQueue.Count -gt 0) {
            $hasMessages = $false
            $message = $null

            # Collect messages from queue
            while ($syncHash.LogQueue.TryDequeue([ref]$message)) {
                $hasMessages = $true
                $logBuffer.Add($message.Entry)

                # Console output based on level if enabled
                if ($syncHash.ConsoleOutputEnabled) {
                    $color = $levelColors[[LogLevel]$message.Level]
                    if ($color) {
                        Write-Host $message.Entry -ForegroundColor $color
                    } else {
                        Write-Host $message.Entry
                    }
                }
            }

            # Flush buffer to file periodically or when buffer is large (only if file output is enabled)
            if ($syncHash.FileOutputEnabled -and $syncHash.CurrentLogFile) {
                $shouldFlush = $logBuffer.Count -gt 0 -and (
                    $logBuffer.Count -ge 50 -or
                    ([DateTime]::Now - $lastFlush) -gt $flushInterval
                )

                if ($shouldFlush) {
                    try {
                        Add-Content -Path $syncHash.CurrentLogFile -Value $logBuffer -Encoding UTF8
                        $logBuffer.Clear()
                        $lastFlush = [DateTime]::Now
                    }
                    catch {
                        # Store error information in sync hash for main thread visibility
                        $syncHash.LastError = $_.Exception.Message
                        $syncHash.ErrorCount++
                        $syncHash.HasErrors = $true
                        Write-Warning "Async logger failed to write: $_"
                    }
                }
            } else {
                # Clear buffer when file output is disabled to prevent memory buildup
                $logBuffer.Clear()
            }

            # Sleep briefly if no messages
            if (-not $hasMessages) {
                Start-Sleep -Milliseconds 50
            }
        }

        # Final flush on shutdown (only if file output is enabled)
        if ($syncHash.FileOutputEnabled -and $syncHash.CurrentLogFile -and $logBuffer.Count -gt 0) {
            try {
                Add-Content -Path $syncHash.CurrentLogFile -Value $logBuffer -Encoding UTF8
            }
            catch {
                # Store error information in sync hash for main thread visibility
                $syncHash.LastError = $_.Exception.Message
                $syncHash.ErrorCount++
                $syncHash.HasErrors = $true
                Write-Warning "Final flush failed: $_"
            }
        }
    }

    # Start background worker
    $null = $script:LoggerPowerShell.AddScript($workerScript)
    $script:AsyncHandle = $script:LoggerPowerShell.BeginInvoke()  # Store the handle for proper shutdown

    # Store sync hash reference
    $script:SyncHash = $syncHash

    $script:AsyncLoggingEnabled = $true
    Write-Verbose "Async logging started for $LogPath"
    }
}

<#
.SYNOPSIS
    Stops the asynchronous logging background worker gracefully.
 
.DESCRIPTION
    Properly shuts down the background PowerShell runspace used for async logging.
    Ensures all queued log entries are written before stopping and cleans up
    all resources including runspaces and PowerShell instances.
 
.EXAMPLE
    Stop-AsyncLogger
    Stops the currently running async logger.
 
.NOTES
    - Waits up to 5 seconds for remaining log entries to be processed
    - Properly disposes of PowerShell runspace and instances
    - Safe to call even if async logger is not running
    - Should always be called before script termination if async logging was used
#>

function Stop-AsyncLogger {
    [CmdletBinding(SupportsShouldProcess)]
    param()

    if (-not $script:AsyncLoggingEnabled) {
        return
    }

    if ($PSCmdlet.ShouldProcess("Async Logger", "Stop background logging")) {

    Write-Verbose "Stopping async logger..."

    # Signal stop
    $script:SyncHash.StopRequested = $true

    # Wait for queue to empty (max 5 seconds)
    $timeout = [DateTime]::Now.AddSeconds(5)
    while ($script:SyncHash.LogQueue.Count -gt 0 -and [DateTime]::Now -lt $timeout) {
        Start-Sleep -Milliseconds 100
    }

    # Wait for the async operation to complete with timeout
    if ($script:AsyncHandle -and $script:LoggerPowerShell) {
        $waitTimeout = 3000  # 3 seconds
        $completed = $script:AsyncHandle.AsyncWaitHandle.WaitOne($waitTimeout)

        if ($completed) {
            # Properly end the invocation to get any results/errors
            try {
                $null = $script:LoggerPowerShell.EndInvoke($script:AsyncHandle)
            }
            catch {
                Write-Verbose "Error ending async invocation: $($_.Exception.Message)"
            }
        } else {
            # Force stop if wait timed out
            Write-Verbose "Async logger did not stop gracefully, forcing stop"
            if ($script:LoggerPowerShell) {
                $script:LoggerPowerShell.Stop()
            }
        }
    }

    # Clean up resources
    if ($script:LoggerPowerShell) {
        $script:LoggerPowerShell.Dispose()
        $script:LoggerPowerShell = $null
    }

    if ($script:LoggerRunspace) {
        $script:LoggerRunspace.Close()
        $script:LoggerRunspace.Dispose()
        $script:LoggerRunspace = $null
    }

    # Clear the handle
    $script:AsyncHandle = $null
    $script:SyncHash = $null

    $script:AsyncLoggingEnabled = $false
    Write-Verbose "Async logger stopped"
    }
}

<#
.SYNOPSIS
    Writes log entries asynchronously using the background logging worker.
 
.DESCRIPTION
    Queues log messages for asynchronous processing by the background worker.
    Provides high-performance logging by avoiding blocking I/O operations.
    Falls back to synchronous logging if async logger is not running.
 
.PARAMETER Message
    The message to log. This parameter is mandatory.
 
.PARAMETER Level
    The severity level of the log entry. Default is INFO.
    Valid values: ERROR, WARN, INFO, DEBUG, TRACE
 
.EXAMPLE
    Write-EnhancedLogAsync -Message "Process completed successfully"
    Writes an INFO level message asynchronously.
 
.EXAMPLE
    Write-EnhancedLogAsync -Message "Critical error occurred" -Level ERROR
    Writes an ERROR level message asynchronously.
 
.NOTES
    - Requires Start-AsyncLogger to be called first
    - Falls back to Write-EnhancedLog if async logger not running
    - Messages are queued and written in batches for performance
    - Use Stop-AsyncLogger to ensure all messages are written before exit
#>

function Write-EnhancedLogAsync {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Message,

        [Parameter(Mandatory = $false)]
        [LogLevel]$Level = [LogLevel]::INFO
    )

    # Filter messages below minimum log level
    if ($Level -gt $script:MinLogLevel) { 
        return 
    }

    # Fall back to sync logging if async not enabled
    if (-not $script:AsyncLoggingEnabled) {
        Write-EnhancedLog -Message $Message -Level $Level
        return
    }

    # Check for async logger errors and warn user if logging is failing
    if ($script:SyncHash -and $script:SyncHash.HasErrors) {
        Write-Warning "CRITICAL: Async logging is experiencing failures! Last error: $($script:SyncHash.LastError). Total errors: $($script:SyncHash.ErrorCount). Consider switching to synchronous logging or check log file permissions."
        # Reset the error flag after warning (prevents spam but still alerts on new errors)
        $script:SyncHash.HasErrors = $false
    }

    # Create formatted log entry using centralized formatter
    $logEntry = Format-LogMessage -Message $Message -Level $Level

    # Queue the message
    $logMessage = [PSCustomObject]@{
        Entry = $logEntry
        Level = $Level
    }

    $script:LogQueue.Enqueue($logMessage)
}

<#
.SYNOPSIS
    Controls whether log entries are displayed in the console.
 
.DESCRIPTION
    Enables or disables console output for all logging functions in the module.
    When disabled, log entries are still written to files but not displayed
    in the console. Affects both synchronous and asynchronous logging.
 
.PARAMETER Enabled
    Boolean value to enable (True) or disable (False) console output.
    This parameter is mandatory.
 
.EXAMPLE
    Set-LoggingConsoleOutput -Enabled $false
    Disables console output for all logging functions.
 
.EXAMPLE
    Set-LoggingConsoleOutput -Enabled $true
    Enables console output for all logging functions.
 
.NOTES
    - Affects both Write-EnhancedLog and Write-EnhancedLogAsync functions
    - Changes are applied immediately to running async logger if active
    - Log files are still created regardless of console output setting
    - Useful for silent automation scenarios or reducing console noise
#>

function Set-LoggingConsoleOutput {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [bool]$Enabled
    )

    if ($PSCmdlet.ShouldProcess("Console Output", "Set logging console output to $Enabled")) {
        $script:ConsoleOutputEnabled = $Enabled

    # Update async logger if running
    if ($script:AsyncLoggingEnabled -and $script:SyncHash) {
        $script:SyncHash.ConsoleOutputEnabled = $Enabled
    }

        $status = if ($Enabled) { "enabled" } else { "disabled" }
        Write-Verbose "Console output $status for logging module"
    }
}

<#
.SYNOPSIS
    Controls whether log entries are written to files or only displayed in console.
 
.DESCRIPTION
    The Set-LoggingFileOutput function enables or disables file logging for the Action.Logging module.
    When disabled, all logging operations will only output to the console with color coding,
    without creating any log files or directories. This is useful for environments where file
    persistence is not needed or desired, such as CI/CD pipelines or containerized applications.
 
.PARAMETER Enabled
    Boolean value to enable (True) or disable (False) file output.
    This parameter is mandatory.
 
.EXAMPLE
    Set-LoggingFileOutput -Enabled $false
    Disables file output for all logging functions - console only mode.
 
.EXAMPLE
    Set-LoggingFileOutput -Enabled $true
    Enables file output for all logging functions (default behavior).
 
.NOTES
    - Affects both Write-EnhancedLog and Write-EnhancedLogAsync functions
    - Changes are applied immediately to running async logger if active
    - Console output remains active regardless of this setting
    - When disabled, no directories are created and no files are written
    - Useful for reducing I/O overhead when file persistence isn't required
#>

function Set-LoggingFileOutput {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [bool]$Enabled
    )

    if ($PSCmdlet.ShouldProcess("File Output", "Set logging file output to $Enabled")) {
        $script:FileOutputEnabled = $Enabled

        # Update async logger if running
        if ($script:AsyncLoggingEnabled -and $script:SyncHash) {
            $script:SyncHash.FileOutputEnabled = $Enabled
        }

        $status = if ($Enabled) { "enabled" } else { "disabled" }
        Write-Verbose "File output $status for logging module"
    }
}

<#
.SYNOPSIS
    Sets the default log retention count for automatic cleanup.
 
.DESCRIPTION
    Configures how many log files to keep when automatic cleanup occurs.
    This setting applies to all logging functions in the module.
 
.PARAMETER Count
    Number of log files to keep. Must be between 1 and 100. Default is 10.
 
.EXAMPLE
    Set-LogRetention -Count 25
    Sets log retention to keep 25 most recent log files.
 
.EXAMPLE
    Set-LogRetention -Count 1
    Sets minimal log retention to keep only 1 most recent log file.
#>

function Set-LogRetention {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 100)]
        [int]$Count = 10
    )

    if ($PSCmdlet.ShouldProcess("Log Retention", "Set default log retention count to $Count")) {
        $script:DefaultLogRetentionCount = $Count
        Write-Verbose "Log retention count set to $Count files"
    }
}

<#
.SYNOPSIS
    Sets the minimum log level for filtering log messages.
 
.DESCRIPTION
    Configures the minimum severity level for log messages. Messages below this level
    will be filtered out and not processed, improving performance and reducing noise.
    This affects both synchronous and asynchronous logging functions.
 
.PARAMETER Level
    The minimum log level. Valid values: ERROR, WARN, INFO, DEBUG, TRACE.
    Default is INFO.
 
.EXAMPLE
    Set-MinLogLevel -Level WARN
    Sets the minimum log level to WARN. DEBUG and TRACE messages will be filtered out.
 
.EXAMPLE
    Set-MinLogLevel -Level ERROR
    Sets the minimum log level to ERROR. Only ERROR messages will be processed.
 
.NOTES
    - Affects both Write-EnhancedLog and Write-EnhancedLogAsync functions
    - Lower severity messages are completely filtered out (not processed at all)
    - Changes take effect immediately for all subsequent log operations
    - Useful for reducing log volume in production environments
#>

function Set-MinLogLevel {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [LogLevel]$Level
    )

    if ($PSCmdlet.ShouldProcess("Minimum Log Level", "Set minimum log level to $Level")) {
        $script:MinLogLevel = $Level
        Write-Verbose "Minimum log level set to $Level"
    }
}


<#
.SYNOPSIS
    Sets a custom script name for log file naming.
 
.DESCRIPTION
    Configures a custom script name that will be used for all subsequent logging
    operations instead of auto-detecting the script name from the call stack.
    Call this function once at the beginning of your script to use a custom name.
 
.PARAMETER ScriptName
    The custom script name to use for log file naming. If not specified or set
    to null/empty, the module will revert to auto-detection.
 
.EXAMPLE
    Set-LogScriptName -ScriptName "MyCustomProcess"
    Sets the script name to "MyCustomProcess" for all logging operations.
 
.EXAMPLE
    Set-LogScriptName -ScriptName $null
    Clears the custom script name and reverts to auto-detection.
 
.NOTES
    - Call this once at the beginning of your script
    - Affects all logging functions: Write-EnhancedLog, Write-EnhancedLogAsync, Start-AsyncLogger
    - The custom name persists for the entire PowerShell session or until changed
#>

function Set-LogScriptName {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $false)]
        [AllowNull()]
        [AllowEmptyString()]
        [string]$ScriptName
    )

    if ($PSCmdlet.ShouldProcess("Log Script Name", "Set custom script name to '$ScriptName'")) {
        if ([string]::IsNullOrWhiteSpace($ScriptName)) {
            $script:CustomScriptName = $null
            # Clear effective name to force re-detection on next use
            $script:EffectiveScriptName = $null
            # Clear session caches to force new log files with new name
            $script:SessionFiles = @{}
            $script:VerifiedPaths = @{}
            # Update async logger target file if running
            if ($script:AsyncLoggingEnabled -and $script:SyncHash) {
                $newLogPath = Get-EffectiveLogPath
                $script:SyncHash.CurrentLogFile = Initialize-LogPath -Path $newLogPath
                Write-Verbose "Updated async logger target file to: $($script:SyncHash.CurrentLogFile)"
            }
            Write-Verbose "Custom script name cleared - reverting to auto-detection"
        } else {
            # Validate script name for invalid filename characters
            $invalidChars = [IO.Path]::GetInvalidFileNameChars()
            $hasInvalidChars = $ScriptName.IndexOfAny($invalidChars) -ge 0
            if ($hasInvalidChars) {
                $invalidCharsList = ($invalidChars | ForEach-Object { if ($_ -eq "`0") { "null" } else { "'$_'" } }) -join ", "
                throw "Invalid script name '$ScriptName'. Script names cannot contain these characters: $invalidCharsList"
            }

            # Validate script name is not too long (Windows filename limit minus timestamp and extension)
            if ($ScriptName.Length -gt 200) {
                throw "Script name '$ScriptName' is too long (maximum 200 characters)"
            }

            $script:CustomScriptName = $ScriptName
            # Set the effective name immediately
            $script:EffectiveScriptName = $ScriptName
            # Clear session caches to force new log files with new name
            $script:SessionFiles = @{}
            $script:VerifiedPaths = @{}
            # Update async logger target file if running
            if ($script:AsyncLoggingEnabled -and $script:SyncHash) {
                $newLogPath = Get-EffectiveLogPath
                $script:SyncHash.CurrentLogFile = Initialize-LogPath -Path $newLogPath
                Write-Verbose "Updated async logger target file to: $($script:SyncHash.CurrentLogFile)"
            }
            Write-Verbose "Custom script name set to: $ScriptName"
        }
    }
}

<#
.SYNOPSIS
    Sets a custom log path for all logging operations.
 
.DESCRIPTION
    Configures a custom log directory path that will be used for all subsequent logging
    operations instead of the default path based on script name. Call this function once
    at the beginning of your script to use a custom log location.
 
.PARAMETER Path
    The custom log directory path. If not specified or set to null/empty, the module
    will revert to the default path (C:\temp\logs\[ScriptName]).
 
.EXAMPLE
    Set-LogPath -Path "D:\MyApp\Logs"
    Sets the log path to "D:\MyApp\Logs" for all logging operations.
 
.EXAMPLE
    Set-LogPath -Path $null
    Clears the custom log path and reverts to default location.
 
.NOTES
    - Call this once at the beginning of your script
    - Affects all logging functions: Write-EnhancedLog, Write-EnhancedLogAsync, Start-AsyncLogger
    - The custom path persists for the entire PowerShell session or until changed
    - Directory is created if it doesn't exist
#>

function Set-LogPath {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $false)]
        [AllowNull()]
        [AllowEmptyString()]
        [string]$Path
    )

    if ($PSCmdlet.ShouldProcess("Log Path", "Set custom log path to '$Path'")) {
        if ([string]::IsNullOrWhiteSpace($Path)) {
            # Store old path before clearing
            $oldPath = $script:CustomLogPath
            $script:CustomLogPath = $null
            # Clear verified paths cache for the old custom path
            if ($oldPath -and $script:VerifiedPaths.ContainsKey($oldPath)) {
                $script:VerifiedPaths.Remove($oldPath)
                $script:SessionFiles.Remove($oldPath)
            }
            # Update async logger target file if running
            if ($script:AsyncLoggingEnabled -and $script:SyncHash) {
                $newLogPath = Get-EffectiveLogPath
                $script:SyncHash.CurrentLogFile = Initialize-LogPath -Path $newLogPath
                Write-Verbose "Updated async logger target file to: $($script:SyncHash.CurrentLogFile)"
            }
            Write-Verbose "Custom log path cleared - reverting to default location"
        } else {
            # Validate path for invalid path characters
            $invalidChars = [IO.Path]::GetInvalidPathChars()
            $hasInvalidChars = $Path.IndexOfAny($invalidChars) -ge 0
            if ($hasInvalidChars) {
                $invalidCharsList = ($invalidChars | ForEach-Object { if ($_ -eq "`0") { "null" } else { "'$_'" } }) -join ", "
                throw "Invalid log path '$Path'. Paths cannot contain these characters: $invalidCharsList"
            }

            # Convert to absolute path for consistency
            try {
                $Path = [IO.Path]::GetFullPath($Path)
            }
            catch {
                throw "Invalid log path '$Path': $($_.Exception.Message)"
            }

            $script:CustomLogPath = $Path

            # Initialize the log path (handles file output check internally)
            Initialize-LogPath -Path $Path | Out-Null

            # Update async logger target file if running
            if ($script:AsyncLoggingEnabled -and $script:SyncHash) {
                $script:SyncHash.CurrentLogFile = Initialize-LogPath -Path $Path
                Write-Verbose "Updated async logger target file to: $($script:SyncHash.CurrentLogFile)"
            }

            Write-Verbose "Custom log path set to: $Path"
        }
    }
}

<#
.SYNOPSIS
    Gets the current status and configuration of the Action.Logging module.
 
.DESCRIPTION
    Returns comprehensive diagnostic information about the current state of the
    Action.Logging module, including async logger status, configuration settings,
    file paths, and error information.
 
.OUTPUTS
    Hashtable. Contains the following keys:
    - AsyncRunning: Boolean indicating if async logger is active
    - ConsoleEnabled: Boolean indicating if console output is enabled
    - FileEnabled: Boolean indicating if file output is enabled
    - CurrentLogFile: Path to current log file (if file output enabled)
    - LogPath: Current log directory path
    - CustomScriptName: Custom script name if set, otherwise null
    - EffectiveScriptName: The script name currently being used for logs
    - RetentionCount: Number of log files to retain during cleanup
    - QueueLength: Number of log messages currently queued for async processing
    - LastError: Last error from async logging (if any)
    - ErrorCount: Total number of async logging errors
    - HasErrors: Boolean indicating if there are unresolved async errors
 
.EXAMPLE
    $status = Get-LoggingStatus
    Write-Host "Async logging: $($status.AsyncRunning)"
    Write-Host "Current log file: $($status.CurrentLogFile)"
 
.EXAMPLE
    $status = Get-LoggingStatus
    if ($status.HasErrors) {
        Write-Warning "Logging errors detected: $($status.ErrorCount)"
        Write-Warning "Last error: $($status.LastError)"
    }
 
.NOTES
    This function is useful for troubleshooting logging issues and monitoring
    the health of the logging system, especially in automated scenarios.
#>

function Get-LoggingStatus {
    [CmdletBinding()]
    param()

    # Initialize the effective script name if not already done
    if (-not $script:EffectiveScriptName) {
        Initialize-EffectiveScriptName
    }

    # Get current log path
    $currentLogPath = Get-EffectiveLogPath

    # Build status hashtable
    $status = @{
        AsyncRunning = $script:AsyncLoggingEnabled
        ConsoleEnabled = $script:ConsoleOutputEnabled
        FileEnabled = $script:FileOutputEnabled
        CurrentLogFile = $script:CurrentLogFile
        LogPath = $currentLogPath
        CustomScriptName = $script:CustomScriptName
        EffectiveScriptName = $script:EffectiveScriptName
        RetentionCount = $script:DefaultLogRetentionCount
        QueueLength = $script:LogQueue.Count
        LastError = $null
        ErrorCount = 0
        HasErrors = $false
    }

    # Add async error information if available
    if ($script:SyncHash) {
        $status.LastError = $script:SyncHash.LastError
        $status.ErrorCount = $script:SyncHash.ErrorCount
        $status.HasErrors = $script:SyncHash.HasErrors
    }

    return $status
}


# Export module functions and enum
Export-ModuleMember -Function Write-EnhancedLog, Clear-OldLog, Start-AsyncLogger, Stop-AsyncLogger, Write-EnhancedLogAsync, Set-LoggingConsoleOutput, Set-LoggingFileOutput, Set-LogRetention, Set-MinLogLevel, Set-LogScriptName, Set-LogPath, Get-LoggingStatus

# The LogLevel enum is already available as [LogLevel] after module import
# No need to inject it as a global variable