Private/Core/LogManager.ps1

function Write-DATLog {
    <#
    .SYNOPSIS
        Writes a log entry in CMTrace-compatible format with optional JSON structured output.
    .PARAMETER Message
        The message to log.
    .PARAMETER Severity
        1 = Information, 2 = Warning, 3 = Error.
    .PARAMETER Component
        The component name for the log entry.
    .PARAMETER LogFile
        Override the default log file name.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Message,

        [ValidateSet(1, 2, 3)]
        [int]$Severity = 1,

        [string]$Component = 'DriverAutomationTool',

        [string]$LogFile
    )

    if (-not $LogFile) {
        $LogFile = Join-Path $script:LogPath 'DriverAutomationTool.log'
    }

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

    # Build CMTrace-compatible timestamp
    $Now = Get-Date
    $UtcOffset = [System.TimeZone]::CurrentTimeZone.GetUtcOffset($Now)
    $OffsetMinutes = $UtcOffset.TotalMinutes
    $TimeStr = '{0}+{1}' -f $Now.ToString('HH:mm:ss.fff'), $OffsetMinutes

    $Identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $Context = if ($Identity) { $Identity.Name } else { $env:USERNAME }
    $Thread = [System.Threading.Thread]::CurrentThread.ManagedThreadId

    # CMTrace format
    $LogEntry = '<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="{4}" type="{5}" thread="{6}" file="">' -f `
        $Message, $TimeStr, $Now.ToString('MM-dd-yyyy'), $Component, $Context, $Severity, $Thread

    try {
        Add-Content -Path $LogFile -Value $LogEntry -ErrorAction Stop
    } catch {
        # If file is locked, write to alternate
        $AltLog = $LogFile -replace '\.log$', '_alt.log'
        Add-Content -Path $AltLog -Value $LogEntry -ErrorAction SilentlyContinue
    }

    # Also write to PowerShell streams
    switch ($Severity) {
        1 { Write-Verbose $Message }
        2 { Write-Warning $Message }
        3 { Write-Error $Message -ErrorAction Continue }
    }

    # Fire event for GUI subscribers
    if ($script:LogEventSubscribers) {
        $EventData = [PSCustomObject]@{
            Timestamp = $Now
            Message   = $Message
            Severity  = $Severity
            Component = $Component
        }
        foreach ($Subscriber in $script:LogEventSubscribers) {
            try { & $Subscriber $EventData } catch { }
        }
    }
}

function Write-DATJsonLog {
    <#
    .SYNOPSIS
        Writes a structured JSON log entry for SIEM integration.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Message,

        [ValidateSet('Information', 'Warning', 'Error')]
        [string]$Level = 'Information',

        [string]$Component = 'DriverAutomationTool',

        [hashtable]$Properties,

        [string]$LogFile
    )

    if (-not $LogFile) {
        $LogFile = Join-Path $script:LogPath 'DriverAutomationTool.jsonl'
    }

    $Entry = [ordered]@{
        timestamp = (Get-Date).ToUniversalTime().ToString('o')
        level     = $Level
        message   = $Message
        component = $Component
        host      = $env:COMPUTERNAME
        user      = $env:USERNAME
    }

    if ($Properties) {
        $Entry['properties'] = $Properties
    }

    $Json = $Entry | ConvertTo-Json -Compress
    Add-Content -Path $LogFile -Value $Json -ErrorAction SilentlyContinue
}

function Register-DATLogSubscriber {
    <#
    .SYNOPSIS
        Registers a scriptblock to receive log events (used by GUI for real-time log display).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [scriptblock]$Action
    )

    if (-not $script:LogEventSubscribers) {
        $script:LogEventSubscribers = [System.Collections.Generic.List[scriptblock]]::new()
    }
    $script:LogEventSubscribers.Add($Action)
}

function Unregister-DATLogSubscriber {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [scriptblock]$Action
    )

    if ($script:LogEventSubscribers) {
        $script:LogEventSubscribers.Remove($Action) | Out-Null
    }
}

function Write-DATJobSummary {
    <#
    .SYNOPSIS
        Appends a row to the job summary CSV for audit trail.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Manufacturer,

        [Parameter(Mandatory)]
        [string]$Model,

        [string]$Type = 'Drivers',

        [string]$Version,

        [string]$PackageID,

        [string]$Status = 'Success',

        [string]$DownloadUrl,

        [string]$Hash,

        [double]$DownloadTimeSec,

        [string]$SummaryFile
    )

    if (-not $SummaryFile) {
        $SummaryFile = Join-Path $script:LogPath ('JobSummary_{0}.csv' -f (Get-Date -Format 'yyyy-MM-dd'))
    }

    $Row = [PSCustomObject]@{
        Timestamp       = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
        Manufacturer    = $Manufacturer
        Model           = $Model
        Type            = $Type
        Version         = $Version
        PackageID       = $PackageID
        Status          = $Status
        DownloadUrl     = $DownloadUrl
        SHA256          = $Hash
        DownloadTimeSec = $DownloadTimeSec
    }

    $CsvExists = Test-Path $SummaryFile
    $Row | Export-Csv -Path $SummaryFile -Append -NoTypeInformation -Force
}

function Send-DATWebhookNotification {
    <#
    .SYNOPSIS
        Sends a notification to a Teams/Slack webhook URL.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$WebhookUrl,

        [Parameter(Mandatory)]
        [string]$Title,

        [Parameter(Mandatory)]
        [string]$Message,

        [ValidateSet('Success', 'Warning', 'Error')]
        [string]$Status = 'Success'
    )

    $ColorMap = @{
        'Success' = '00FF00'
        'Warning' = 'FFFF00'
        'Error'   = 'FF0000'
    }

    # Teams Adaptive Card format
    $Body = @{
        '@type'      = 'MessageCard'
        '@context'   = 'http://schema.org/extensions'
        themeColor   = $ColorMap[$Status]
        summary      = $Title
        sections     = @(
            @{
                activityTitle = $Title
                text          = $Message
                facts         = @(
                    @{ name = 'Status'; value = $Status }
                    @{ name = 'Time'; value = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') }
                    @{ name = 'Computer'; value = $env:COMPUTERNAME }
                )
            }
        )
    } | ConvertTo-Json -Depth 5

    try {
        Invoke-RestMethod -Uri $WebhookUrl -Method Post -Body $Body -ContentType 'application/json' -ErrorAction Stop
    } catch {
        Write-DATLog -Message "Failed to send webhook notification: $($_.Exception.Message)" -Severity 2
    }
}