<#
.SYNOPSIS
    Provides core, shared functions for logging, process execution, and resilient file transfers used across the FFU project.
.DESCRIPTION
    This module is a central component of the FFU project, offering a set of robust, reusable functions. 
    It includes a centralized logging mechanism (WriteLog), a wrapper for running external processes with error handling (Invoke-Process), 
    a retry-aware BITS transfer function for reliable downloads (Start-BitsTransferWithRetry), and a progress reporting helper. 
    This module is designed to be imported by other scripts and modules within the project to ensure consistent behavior for common tasks.
#>
# Script-scoped variable for the log file path
$script:CommonCoreLogFilePath = $null
# Mutex for log file access
$script:commonCoreLogMutexName = "Global\FFUCommonCoreLogMutex" # Unique name
$script:commonCoreLogMutex = New-Object System.Threading.Mutex($false, $script:commonCoreLogMutexName)
$script:BitsTransferPriority = 'Normal'
if (-not [string]::IsNullOrWhiteSpace($env:FFU_BITS_PRIORITY)) {
    $script:BitsTransferPriority = $env:FFU_BITS_PRIORITY
}

# Function to set the log file path for this module
function Set-CommonCoreLogPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )
    $script:CommonCoreLogFilePath = $Path
    if (-not [string]::IsNullOrWhiteSpace($script:CommonCoreLogFilePath)) {
        # This initial WriteLog confirms the path is set and the logger is working.
        WriteLog "CommonCoreLogPath set to: $script:CommonCoreLogFilePath"
    }
    else {
        # This Write-Warning will appear on console if path is bad, but won't go to log file yet.
        Write-Warning "Set-CommonCoreLogPath called with an empty or null path."
    }
}
        
function Set-BitsTransferPriority {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Foreground', 'High', 'Normal', 'Low')]
        [string]$Priority
    )
    $script:BitsTransferPriority = $Priority
    try {
        Set-Item -Path Env:FFU_BITS_PRIORITY -Value $Priority -ErrorAction Stop
    }
    catch {
        WriteLog "Failed to set FFU_BITS_PRIORITY environment variable: $($_.Exception.Message)"
    }
    WriteLog "BITS transfer priority set to $Priority."
}
        
# Centralized WriteLog function
function WriteLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$LogText
    )

    # Check if the log file path has been set
    if ([string]::IsNullOrWhiteSpace($script:CommonCoreLogFilePath)) {
        Write-Warning "CommonCoreLogFilePath not set. Message: $LogText"
        return
    }

    $logEntry = "$((Get-Date).ToString()) $LogText"
    $streamWriter = $null

    try {
        $script:commonCoreLogMutex.WaitOne() | Out-Null
        # Ensure directory exists before writing
        $logDir = Split-Path -Path $script:CommonCoreLogFilePath -Parent
        if (-not (Test-Path -Path $logDir -PathType Container)) {
            New-Item -Path $logDir -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
        }
        $streamWriter = New-Object System.IO.StreamWriter($script:CommonCoreLogFilePath, $true, [System.Text.Encoding]::UTF8)
        $streamWriter.WriteLine($logEntry)

        Write-Verbose $LogText
    }
    catch {
        # Use Write-Host for console visibility as Write-Warning might also try to log
        Write-Host "WARNING: Error writing to log file '$($script:CommonCoreLogFilePath)': $($_.Exception.Message)" -ForegroundColor Yellow
    }
    finally {
        if ($null -ne $streamWriter) {
            $streamWriter.Dispose()
        }
        $script:commonCoreLogMutex.ReleaseMutex()
    }
}

function Invoke-Process {
    [CmdletBinding(SupportsShouldProcess)]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$FilePath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string[]]$ArgumentList,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [bool]$Wait = $true
    )

    $ErrorActionPreference = 'Stop'

    try {
        $stdOutTempFile = "$env:TEMP\$((New-Guid).Guid)"
        $stdErrTempFile = "$env:TEMP\$((New-Guid).Guid)"

        $startProcessParams = @{
            FilePath               = $FilePath
            ArgumentList           = $ArgumentList
            RedirectStandardError  = $stdErrTempFile
            RedirectStandardOutput = $stdOutTempFile
            Wait                   = $($Wait);
            PassThru               = $true;
            NoNewWindow            = $true;
        }
        if ($PSCmdlet.ShouldProcess("Process [$($FilePath)]", "Run with args: [$($ArgumentList)]")) {
            $cmd = Start-Process @startProcessParams
            $cmdOutput = Get-Content -Path $stdOutTempFile -Raw
            $cmdError = Get-Content -Path $stdErrTempFile -Raw
            if ($cmd.ExitCode -ne 0 -and $wait -eq $true) {
                if ($cmdError) {
                    throw $cmdError.Trim()
                }
                if ($cmdOutput) {
                    throw $cmdOutput.Trim()
                }
            }
            else {
                if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) {
                    WriteLog $cmdOutput
                }
            }
        }
    }
    catch {
        #$PSCmdlet.ThrowTerminatingError($_)
        WriteLog $_
        # Write-Host "Script failed - $Logfile for more info"
        throw $_

    }
    finally {
        Remove-Item -Path $stdOutTempFile, $stdErrTempFile -Force -ErrorAction Ignore
    }
    return $cmd
}

function Get-RunManifestPathForDownloadTarget {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Destination
    )

    try {
        $currentPath = Split-Path -Path $Destination -Parent
        if ([string]::IsNullOrWhiteSpace($currentPath)) { return $null }

        while ($currentPath) {
            $manifestPath = Join-Path -Path $currentPath -ChildPath '.session\currentRun.json'
            if (Test-Path -LiteralPath $manifestPath -PathType Leaf) {
                return $manifestPath
            }

            $parentPath = Split-Path -Path $currentPath -Parent
            if ([string]::IsNullOrWhiteSpace($parentPath) -or $parentPath -eq $currentPath) {
                break
            }
            $currentPath = $parentPath
        }
    }
    catch {
        WriteLog "Get-RunManifestPathForDownloadTarget failed for '$Destination': $($_.Exception.Message)"
    }

    return $null
}

function Register-CurrentRunDownloadTarget {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Destination
    )

    if ([string]::IsNullOrWhiteSpace($Destination)) { return }

    $manifestPath = Get-RunManifestPathForDownloadTarget -Destination $Destination
    if ([string]::IsNullOrWhiteSpace($manifestPath)) { return }

    $mutexName = 'Global\FFUCurrentRunDownloadTargetsMutex'
    $mutex = New-Object System.Threading.Mutex($false, $mutexName)

    try {
        $null = $mutex.WaitOne()

        $manifest = Get-Content -LiteralPath $manifestPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
        if ($null -eq $manifest) { return }

        if ($null -eq $manifest.PSObject.Properties['DownloadTargets']) {
            Add-Member -InputObject $manifest -MemberType NoteProperty -Name DownloadTargets -Value @()
        }

        $downloadTargets = @($manifest.DownloadTargets)
        if ($Destination -notin $downloadTargets) {
            $downloadTargets += $Destination
            $manifest.DownloadTargets = $downloadTargets
            $manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
            WriteLog "Registered current-run download target: $Destination"
        }
    }
    catch {
        WriteLog "Register-CurrentRunDownloadTarget failed for '$Destination': $($_.Exception.Message)"
    }
    finally {
        try { $mutex.ReleaseMutex() | Out-Null } catch {}
        $mutex.Dispose()
    }
}

# Function to download a file using BITS with retry and error handling
function Start-BitsTransferWithRetry {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Source,
        [Parameter(Mandatory = $true)]
        [string]$Destination,
        [int]$Retries = 3,
        [ValidateSet('Foreground','High','Normal','Low')]
        [string]$Priority
    )

    if ([string]::IsNullOrWhiteSpace($Priority)) {
        if (-not [string]::IsNullOrWhiteSpace($env:FFU_BITS_PRIORITY)) {
            $Priority = $env:FFU_BITS_PRIORITY
        }
        elseif (-not [string]::IsNullOrWhiteSpace($script:BitsTransferPriority)) {
            $Priority = $script:BitsTransferPriority
        }
        else {
            $Priority = 'Normal'
        }
    }

    # Register destination so cancel cleanup can remove this run's downloaded files
    # even when file timestamps are inherited from the source.
    Register-CurrentRunDownloadTarget -Destination $Destination

    $attempt = 0
    $lastError = $null
    $notLoggedOnHResult = [int]0x800704dd
    $fallbackTriggered = $false

    while ($attempt -lt $Retries -and -not $fallbackTriggered) {
        $OriginalVerbosePreference = $VerbosePreference
        $OriginalProgressPreference = $ProgressPreference
        try {
            $VerbosePreference = 'SilentlyContinue'
            $ProgressPreference = 'SilentlyContinue'

            Start-BitsTransfer -Source $Source -Destination $Destination -Priority $Priority -ErrorAction Stop
            
            $ProgressPreference = $OriginalProgressPreference
            $VerbosePreference = $OriginalVerbosePreference
            WriteLog "Successfully transferred $Source to $Destination."
            return
        }
        catch {
            $lastError = $_
            $attempt++
            $errorMessage = $lastError.Exception.Message
            WriteLog "Attempt $attempt of $Retries failed to download $Source. Error: $errorMessage."
            $hResult = $null
            if ($null -ne $lastError.Exception) {
                $hResult = $lastError.Exception.HResult
            }
            $needsHttpFallback = $false
            if ($hResult -eq $notLoggedOnHResult) {
                $needsHttpFallback = $true
            }
            elseif ($errorMessage -match '0x800704DD' -or $errorMessage -match 'not.*logged on to the network') {
                $needsHttpFallback = $true
            }
            if ($needsHttpFallback) {
                WriteLog "BITS cannot download $Source because the current session is not logged on to the network. Falling back to Invoke-WebRequest."
                $fallbackTriggered = $true
                break
            }
            Start-Sleep -Seconds (1 * $attempt)
        }
        finally {
            if (Get-Variable -Name 'OriginalProgressPreference' -ErrorAction SilentlyContinue) {
                $ProgressPreference = $OriginalProgressPreference
            }
            if (Get-Variable -Name 'OriginalVerbosePreference' -ErrorAction SilentlyContinue) {
                $VerbosePreference = $OriginalVerbosePreference
            }
        }
    }

    if ($fallbackTriggered) {
        $remainingAttempts = $Retries - $attempt
        if ($remainingAttempts -lt 1) {
            $remainingAttempts = 1
        }
        $httpAttempt = 0
        while ($httpAttempt -lt $remainingAttempts) {
            $httpAttempt++
            $OriginalVerbosePreference = $VerbosePreference
            $OriginalProgressPreference = $ProgressPreference
            try {
                $VerbosePreference = 'SilentlyContinue'
                $ProgressPreference = 'SilentlyContinue'
                Invoke-WebRequest -Uri $Source -OutFile $Destination -ErrorAction Stop
                $ProgressPreference = $OriginalProgressPreference
                $VerbosePreference = $OriginalVerbosePreference
                WriteLog "Successfully transferred $Source to $Destination via HTTP fallback."
                return
            }
            catch {
                $lastError = $_
                WriteLog "HTTP fallback attempt $httpAttempt of $remainingAttempts failed to download $Source. Error: $($lastError.Exception.Message)."
                Start-Sleep -Seconds (1 * $httpAttempt)
            }
            finally {
                if (Get-Variable -Name 'OriginalProgressPreference' -ErrorAction SilentlyContinue) {
                    $ProgressPreference = $OriginalProgressPreference
                }
                if (Get-Variable -Name 'OriginalVerbosePreference' -ErrorAction SilentlyContinue) {
                    $VerbosePreference = $OriginalVerbosePreference
                }
            }
        }
    }

    WriteLog "Failed to download $Source after $Retries attempts. Last Error: $($lastError.Exception.Message)"
    throw $lastError
}
    
function Set-Progress {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [int]$Percentage,
        [Parameter(Mandatory)]
        [string]$Message
    )
    WriteLog "[PROGRESS] $Percentage | $Message"
}
    
function ConvertTo-SafeName {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name
    )
    # Replace invalid Windows filename characters (<>:"/\|?* and control chars) with a dash
    $sanitized = $Name -replace '[<>:\"/\\|?*\x00-\x1F]', '-'
    # Collapse multiple consecutive dashes
    $sanitized = $sanitized -replace '-{2,}', '-'
    # Trim leading/trailing spaces, periods, and dashes
    $sanitized = $sanitized.Trim(' ', '.', '-')
    if ([string]::IsNullOrWhiteSpace($sanitized)) {
        $sanitized = 'Unnamed'
    }
    return $sanitized
}

Export-ModuleMember -Function *