# Allow Orchestrator.ps1 to override the app list file paths while preserving legacy defaults.
param(
    [Parameter()]
    [string]$wingetAppsJsonFile = (Join-Path -Path $PSScriptRoot -ChildPath "WinGetWin32Apps.json"),
    [Parameter()]
    [string]$userAppsJsonFile = (Join-Path -Path (Split-Path -Parent $PSScriptRoot) -ChildPath "UserAppList.json")
)

function Get-AppsMediaRoot {
    $appsRoot = [string]$env:FFUAppsRoot
    if ([string]::IsNullOrWhiteSpace($appsRoot)) {
        return $null
    }

    return $appsRoot.Trim().TrimEnd('\')
}

function Expand-AppsMediaRootTokens {
    param(
        [AllowNull()]
        [string]$Value
    )

    if ($null -eq $Value) {
        return $null
    }

    $appsRoot = Get-AppsMediaRoot
    if ([string]::IsNullOrWhiteSpace($appsRoot)) {
        return $Value
    }

    $tokenPattern = '%FFUAppsRoot%|\$\{?env:FFUAppsRoot\}?|\{FFUAppsRoot\}'
    $regexOptions = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
    return [regex]::Replace($Value, $tokenPattern, [System.Text.RegularExpressions.MatchEvaluator] { param($match) $appsRoot }, $regexOptions)
}

function Resolve-LegacyAppsMediaPath {
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    if ($Path -notmatch '^(?i)d:\\') {
        return $Path
    }

    $appsRoot = Get-AppsMediaRoot
    if ([string]::IsNullOrWhiteSpace($appsRoot)) {
        return $Path
    }

    $relativePath = $Path.Substring(3)
    $candidatePath = Join-Path -Path $appsRoot -ChildPath $relativePath
    $literalExists = Test-Path -Path $Path
    $candidateExists = Test-Path -Path $candidatePath

    if ($literalExists) {
        if ($candidateExists) {
            Write-Warning "Both legacy Apps path '$Path' and Apps media path '$candidatePath' exist. Keeping literal path."
        }
        return $Path
    }

    if ($candidateExists) {
        Write-Host "Remapped legacy Apps path '$Path' to '$candidatePath'."
        return $candidatePath
    }

    return $Path
}

function Resolve-AppsMediaPathValue {
    param(
        [AllowNull()]
        [string]$Value
    )

    if ($null -eq $Value) {
        return $null
    }

    $resolvedValue = Expand-AppsMediaRootTokens -Value $Value
    if ($resolvedValue -match '^(?i)d:\\') {
        return Resolve-LegacyAppsMediaPath -Path $resolvedValue
    }

    $regexOptions = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
    $resolvedValue = [regex]::Replace($resolvedValue, '"(?<path>d:\\[^"\r\n]+)"', [System.Text.RegularExpressions.MatchEvaluator] {
        param($match)
        '"' + (Resolve-LegacyAppsMediaPath -Path $match.Groups['path'].Value) + '"'
    }, $regexOptions)
    $resolvedValue = [regex]::Replace($resolvedValue, '''(?<path>d:\\[^''\r\n]+)''', [System.Text.RegularExpressions.MatchEvaluator] {
        param($match)
        "'" + (Resolve-LegacyAppsMediaPath -Path $match.Groups['path'].Value) + "'"
    }, $regexOptions)
    $resolvedValue = [regex]::Replace($resolvedValue, '(?<![\w:"''])(?<path>d:\\[^\s"'']+)', [System.Text.RegularExpressions.MatchEvaluator] {
        param($match)
        Resolve-LegacyAppsMediaPath -Path $match.Groups['path'].Value
    }, $regexOptions)

    return $resolvedValue
}

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

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

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

        [Parameter()]
        [string[]]$AdditionalSuccessCodes,

        [Parameter()]
        [bool]$IgnoreNonZeroExitCodes = $false
    )

    $ErrorActionPreference = 'Stop'

    try {
        if ($PSCmdlet.ShouldProcess("Process [$($FilePath)]", "Run with args: [$($ArgumentList)]")) {
            # Use .NET Process class for proper stream handling
            $pinfo = New-Object System.Diagnostics.ProcessStartInfo
            $pinfo.FileName = $FilePath
            if ($ArgumentList) {
                $pinfo.Arguments = $ArgumentList -join ' '
            }
            $pinfo.RedirectStandardOutput = $true
            $pinfo.RedirectStandardError = $true
            $pinfo.UseShellExecute = $false
            $pinfo.CreateNoWindow = $true
            
            $p = New-Object System.Diagnostics.Process
            $p.StartInfo = $pinfo
            
            # Start the process
            $p.Start() | Out-Null
            
            # Read output and error streams
            $cmdOutput = $p.StandardOutput.ReadToEnd()
            $cmdError = $p.StandardError.ReadToEnd()
            
            if ($Wait) {
                $p.WaitForExit()
            }
            
            $exitCode = $p.ExitCode
            # An exit code of 0 is always a success
            if ($exitCode -ne 0) {
                # If IgnoreNonZeroExitCodes is true, treat any non-zero exit code as a success
                if ($IgnoreNonZeroExitCodes) {
                    Write-Host "Ignoring non-zero exit code $exitCode because IgnoreNonZeroExitCodes is set to true."
                }
                # Check if the non-zero exit code is in the list of additional success codes
                elseif ($null -eq $AdditionalSuccessCodes -or $exitCode -notin $AdditionalSuccessCodes) {
                    if ($cmdError) {
                        throw $cmdError.Trim()
                    }
                    if ($cmdOutput) {
                        throw $cmdOutput.Trim()
                    }
                    # If there's no output, throw a generic error with the exit code
                    if (-not $cmdError -and -not $cmdOutput) {
                        throw "Process exited with non-zero code: $exitCode"
                    }
                }
            }
            else {
                if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) {
                    # WriteLog $cmdOutput
                }
            }
            
            # Create a simple object with exit code for compatibility
            $result = [PSCustomObject]@{
                ExitCode = $exitCode
            }
            
            return $result
        }
    }
    catch {
        #$PSCmdlet.ThrowTerminatingError($_)
        # WriteLog $_
        # Write-Host "Script failed - $Logfile for more info"
        throw $_
    }
}

function Format-MsiArguments {
    <#
    .SYNOPSIS
        Ensures MSI file paths in msiexec arguments are properly quoted.
    .DESCRIPTION
        Detects /i arguments followed by an unquoted path ending in .msi
        and wraps the path in double quotes to handle paths with spaces.
    #>
    param(
        [Parameter(Mandatory)]
        [string]$CommandLine,

        [Parameter(Mandatory)]
        [string]$Arguments
    )

    # Only process if the command is msiexec
    if ($CommandLine -notmatch '^msiexec(\.exe)?$') {
        return $Arguments
    }

    # Regex pattern explanation:
    # (?i)            - Case-insensitive matching
    # (/i)\s+         - Match /i followed by whitespace
    # (?!")           - Negative lookahead: not already quoted
    # (.+?\.msi)      - Capture path ending in .msi (lazy match to stop at first .msi)
    # (?=\s+/|\s*$)   - Followed by another switch or end of string
    
    # Pattern to match /i followed by an unquoted MSI path
    $pattern = '(?i)(/i)\s+(?!")(.+?\.msi)(?=\s+/|\s*$)'
    
    if ($Arguments -match $pattern) {
        $originalArgs = $Arguments
        # Replace with quoted path
        $Arguments = $Arguments -replace $pattern, '$1 "$2"'
        Write-Host "Detected unquoted MSI path in msiexec arguments. Adjusted arguments:"
        Write-Host "Original: $originalArgs"
        Write-Host "Modified: $Arguments"
    }

    return $Arguments
}

function Install-Applications {
    param(
        [Parameter(Mandatory)]
        [array]$apps
    )

    if ($apps.Count -eq 0) {
        Write-Host "No applications to install from this source."
        return
    }

    Write-Host "Total apps to install from this source: $($apps.Count)"

    # Sort all apps by priority
    $sortedApps = $apps | Sort-Object -Property Priority

    # Install each app
    foreach ($app in $sortedApps) {
        # Check if required properties exist
        if (-not $app.PSObject.Properties['Name'] -or -not $app.PSObject.Properties['CommandLine'] -or -not $app.PSObject.Properties['Arguments']) {
            Write-Warning "Skipping app due to missing required properties (Name, CommandLine, Arguments): $($app | ConvertTo-Json -Depth 1 -Compress)"
            continue
        }

        Write-Host "Installing $($app.Name)..."
        
        # Wait until no MSIExec installation is running
        while ($true) {
            try {
                # Try to open the MSIExec global mutex
                $Mutex = [System.Threading.Mutex]::OpenExisting("Global\_MSIExecute")
                # Dispose releases the handle from our script only.
                $Mutex.Dispose()
                Write-Host "Another MSIExec installer is running. Waiting for 5 seconds before rechecking..."
                Start-Sleep -Seconds 5
            }
            catch [System.Threading.WaitHandleCannotBeOpenedException] {
                # If we can't open the mutex, it means no MSIExec installation is running
                break
            }
            catch {
                # Handle other potential errors when checking the mutex
                Write-Warning "Error checking MSIExec mutex: $_. Proceeding with caution."
                break 
            }
        }

        # Check for 'PAUSE' command
        if ($app.CommandLine -eq 'PAUSE') {
            Write-Host "Pausing script as requested by '$($app.Name)'. Press Enter to continue..."
            $null = Read-Host
            continue
        }
        
        try {
            $commandLineToRun = Resolve-AppsMediaPathValue -Value $app.CommandLine

            # Normalize arguments: treat null/empty/whitespace as no arguments
            $argumentsToPass = $null
            if ($null -ne $app.Arguments) {
                if ($app.Arguments -is [array]) {
                    $trimmed = $app.Arguments | ForEach-Object { ($_ | ForEach-Object { if ($_ -ne $null) { Resolve-AppsMediaPathValue -Value $_.ToString().Trim() } else { $_ } }) } | Where-Object { $_ -and (-not [string]::IsNullOrWhiteSpace($_)) }
                    if ($trimmed.Count -gt 0) {
                        $argumentsToPass = $trimmed
                    }
                }
                else {
                    $single = Resolve-AppsMediaPathValue -Value $app.Arguments.ToString().Trim()
                    if (-not [string]::IsNullOrWhiteSpace($single)) {
                        $argumentsToPass = @($single)
                    }
                }
            }

            # Check for and parse AdditionalExitCodes
            $additionalSuccessCodes = @()
            if ($app.PSObject.Properties['AdditionalExitCodes'] -and -not [string]::IsNullOrWhiteSpace($app.AdditionalExitCodes)) {
                $additionalSuccessCodes = $app.AdditionalExitCodes -split ',' | ForEach-Object { $_.Trim() }
                Write-Host "Additional success exit codes for $($app.Name): $($additionalSuccessCodes -join ', ')"
            }

            # Check for IgnoreNonZeroExitCodes
            $ignoreNonZeroExitCodes = $false
            if ($app.PSObject.Properties['IgnoreNonZeroExitCodes'] -and $app.IgnoreNonZeroExitCodes -is [bool]) {
                $ignoreNonZeroExitCodes = $app.IgnoreNonZeroExitCodes
            }

            # Auto-quote MSI paths if using msiexec and path contains spaces but no quotes
            if ($null -ne $argumentsToPass -and $argumentsToPass.Count -gt 0) {
                $joinedArgs = $argumentsToPass -join ' '
                $formattedArgs = Format-MsiArguments -CommandLine $commandLineToRun -Arguments $joinedArgs
                if ($formattedArgs -ne $joinedArgs) {
                    $argumentsToPass = @($formattedArgs)
                }
            }

            if ($null -eq $argumentsToPass -or $argumentsToPass.Count -eq 0) {
                Write-Host "Running command: $commandLineToRun (no arguments)"
                $result = Invoke-Process -FilePath $commandLineToRun -AdditionalSuccessCodes $additionalSuccessCodes -IgnoreNonZeroExitCodes $ignoreNonZeroExitCodes
            }
            else {
                Write-Host "Running command: $commandLineToRun $($argumentsToPass -join ' ')"
                $result = Invoke-Process -FilePath $commandLineToRun -ArgumentList $argumentsToPass -AdditionalSuccessCodes $additionalSuccessCodes -IgnoreNonZeroExitCodes $ignoreNonZeroExitCodes
            }
            Write-Host "$($app.Name) exited with exit code: $($result.ExitCode)`r`n"
        }
        catch {
            Write-Error "Error occurred while installing $($app.Name): $_"
            Read-Host "An error occurred, and the script cannot continue. Press Enter to exit."
            throw $_
        }
    }
}

# Initialize empty arrays for apps from each source
$wingetApps = @()
$userApps = @()

# Read the WinGetWin32Apps.json file if it exists
if (Test-Path -Path $wingetAppsJsonFile) {
    Write-Host "Processing WinGetWin32Apps.json..."
    try {
        $wingetContent = Get-Content -Path $wingetAppsJsonFile -Raw -ErrorAction Stop | ConvertFrom-Json
        if ($wingetContent -is [array]) {
            $wingetApps = $wingetContent
            Write-Host "Found $(($wingetApps | Measure-Object).Count) WinGet Win32 apps."
        }
        elseif ($wingetContent) {
            $wingetApps = @($wingetContent) # Ensure it's an array
            Write-Host "Found 1 WinGet Win32 app."
        }
        else {
            Write-Host "WinGetWin32Apps.json is empty or invalid."
        }
    }
    catch {
        Write-Error "Failed to read or parse WinGetWin32Apps.json file: $_"
    }
}
else {
    Write-Host "WinGetWin32Apps.json file not found. Skipping."
}

# Install WinGet apps if any were found
if ($wingetApps.Count -gt 0) {
    Install-Applications -apps $wingetApps
}

# Read the configured BYO app list file if it exists
if (Test-Path -Path $userAppsJsonFile) {
    Write-Host "Processing $(Split-Path -Path $userAppsJsonFile -Leaf)..."
    try {
        $userContent = Get-Content -Path $userAppsJsonFile -Raw -ErrorAction Stop | ConvertFrom-Json
        if ($userContent -is [array]) {
            $userApps = $userContent
            Write-Host "Found $(($userApps | Measure-Object).Count) user-defined apps."
        }
        elseif ($userContent) {
            $userApps = @($userContent)
            Write-Host "Found 1 user-defined app."
        }
        else {
            Write-Host "$(Split-Path -Path $userAppsJsonFile -Leaf) is empty or invalid."
        }
    }
    catch {
        Write-Error "Failed to read or parse BYO app list file '$userAppsJsonFile': $_"
    }
}
else {
    Write-Host "BYO app list file not found at $userAppsJsonFile. Skipping."
}

# Install User apps if any were found
if ($userApps.Count -gt 0) {
    Install-Applications -apps $userApps
}

# Check if any apps were installed at all
if ($wingetApps.Count -eq 0 -and $userApps.Count -eq 0) {
    Write-Host "No Win32 apps found in either WinGetWin32Apps.json or UserAppList.json. Exiting."
    exit 0
}

Write-Host "All Win32 app installations attempted."