src/core/Logging.ps1

#Requires -Version 5.1

# Global logging configuration
$script:LoggingConfig = @{
    LogDir = 'data/logs'
    LogLevel = 'INFO'
    Format = 'json'
    RetentionDays = 30
}

function Initialize-Logging {
    <#
    .SYNOPSIS
    Configures and initializes the global logging system.

    .DESCRIPTION
    Sets up logging configuration (directory, level, format), creates log directory if missing,
    and removes old log files exceeding retention period.

    .PARAMETER LogDir
    Directory where log files are stored. Defaults to 'data/logs'.

    .PARAMETER LogLevel
    Minimum log level (DEBUG, INFO, WARN, ERROR). Defaults to 'INFO'.

    .PARAMETER Format
    Log file format. Currently 'json' for structured logging. Defaults to 'json'.

    .PARAMETER RetentionDays
    Number of days to retain log files. Older files are deleted. Defaults to 30.

    .EXAMPLE
    Initialize-Logging -LogDir 'data/logs' -LogLevel 'INFO' -RetentionDays 30

    .NOTES
    Call this once at script startup before using Write-Log.
    #>

    [CmdletBinding()]
    param(
        [ValidateNotNullOrEmpty()][string]$LogDir = 'data/logs',
        [ValidateSet('DEBUG', 'INFO', 'WARN', 'ERROR')][string]$LogLevel = 'INFO',
        [ValidateNotNullOrEmpty()][string]$Format = 'json',
        [ValidateRange(1, 365)][int]$RetentionDays = 30
    )

    $script:LoggingConfig.LogDir = $LogDir
    $script:LoggingConfig.LogLevel = $LogLevel
    $script:LoggingConfig.Format = $Format
    $script:LoggingConfig.RetentionDays = $RetentionDays

    if (-not (Test-Path $LogDir)) {
        New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
    }

    Remove-OldLog -LogDir $LogDir -RetentionDays $RetentionDays
}

function Write-Log {
    <#
    .SYNOPSIS
    Logs a message to console and file with optional context and exception details.

    .DESCRIPTION
    Writes structured log entries to both console and persistent JSON log file.
    Console output is colored based on level (ERROR=red, WARN=yellow).
    Context hashtable and Exception object are included in file output for debugging.

    .PARAMETER Level
    Log level (DEBUG, INFO, WARN, ERROR). Defaults to 'INFO'.

    .PARAMETER Message
    The main log message.

    .PARAMETER Context
    Optional hashtable with structured context (e.g., @{ monitor_id = 'abc'; attempt = 3 }).

    .PARAMETER Exception
    Optional exception object to include stack trace and line number.

    .EXAMPLE
    Write-Log -Level INFO -Message "Monitor started" -Context @{ monitor = 'shooting-store' }

    .EXAMPLE
    Write-Log -Level ERROR -Message "Failed to fetch courses" -Exception $_

    .NOTES
    Requires Initialize-Logging to be called first.
    #>

    [CmdletBinding()]
    param(
        [ValidateSet('DEBUG', 'INFO', 'WARN', 'ERROR')][string]$Level = 'INFO',
        [ValidateNotNullOrEmpty()][string]$Message,
        [hashtable]$Context,
        [object]$Exception
    )

    # Format console output
    $color = $null
    if ($Level -eq 'ERROR') { $color = 'Red' }
    elseif ($Level -eq 'WARN') { $color = 'Yellow' }

    $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    $logMsg = "[$timestamp] $Level`: $Message"

    if ($Exception) {
        $logMsg += " | Exception: $($Exception.Message)"
        if ($Exception.InvocationInfo) { $logMsg += " (Line: $($Exception.InvocationInfo.ScriptLineNumber))" }
    }

    if ($color) { Write-Information $logMsg -InformationAction Continue }
    else { Write-Output $logMsg }

    # Write to file
    if ($script:LoggingConfig.Format -eq 'json') {
        Write-LogToFile-JSON -Level $Level -Message $Message -Context $Context -Exception $Exception
    }
}

function Write-LogToFile-JSON {
    param(
        [string]$Level,
        [string]$Message,
        [hashtable]$Context,
        [object]$Exception
    )

    $logDir = $script:LoggingConfig.LogDir
    $logFile = Join-Path $logDir "watcher-$(Get-Date -Format 'yyyy-MM-dd').log"

    $logEntry = @{
        timestamp = [datetime]::Now.ToString('o')
        level = $Level
        message = $Message
    }

    if ($Context) { $logEntry.context = $Context }

    if ($Exception) {
        $logEntry.exception = @{
            message = $Exception.Message
            type = $Exception.GetType().FullName
            line = if ($Exception.InvocationInfo) { $Exception.InvocationInfo.ScriptLineNumber } else { $null }
        }
    }

    try {
        $json = $logEntry | ConvertTo-Json -Compress -Depth 5
        Add-Content -Path $logFile -Value $json -Encoding UTF8 -ErrorAction Stop
    }
    catch {
        Write-Error "Failed to write log: $_"
    }
}

function Remove-OldLog {
    <#
    .SYNOPSIS
    Deletes log files older than retention period.

    .DESCRIPTION
    Removes watcher-*.log files from LogDir that exceed the RetentionDays threshold.
    Respects -WhatIf and -Confirm for safe operation.

    .PARAMETER LogDir
    Directory containing log files to clean.

    .PARAMETER RetentionDays
    Number of days to retain. Files older than this are deleted.

    .EXAMPLE
    Remove-OldLog -LogDir 'data/logs' -RetentionDays 30

    .EXAMPLE
    Remove-OldLog -LogDir 'data/logs' -RetentionDays 30 -WhatIf
    # Shows what would be deleted without deleting

    .NOTES
    Supports -WhatIf and -Confirm for safety. Silently continues on file access errors.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [string]$LogDir,
        [int]$RetentionDays
    )

    if (-not (Test-Path $LogDir)) { return }

    $cutoffDate = (Get-Date).AddDays(-$RetentionDays)
    Get-ChildItem $LogDir -Filter "watcher-*.log" -ErrorAction SilentlyContinue |
        Where-Object { $_.LastWriteTime -lt $cutoffDate } |
        Remove-Item -Force -ErrorAction SilentlyContinue -WhatIf:$WhatIfPreference -Confirm:$ConfirmPreference
}