Public/New-JuribaAppRApplication.ps1

function New-JuribaAppRApplication {
    <#
      .SYNOPSIS
      Creates a new application in Juriba App Readiness.
      .DESCRIPTION
      Creates a new application record in App Readiness from a previously uploaded
      setup file. The application is created asynchronously. Use
      Get-JuribaAppRApplicationCreationState or Watch-JuribaAppRApplicationCreation
      to monitor progress.

      This cmdlet reads Default Settings from the instance to resolve:
        - VM groups for repackaging and testing
        - Output format bitmask (which package types to produce)

      It also calls the server-side metadata extraction API to detect the
      application name, manufacturer, and version from the uploaded binary.

      It also calls the command suggestion API to detect the best install
      and uninstall command lines for the uploaded binary.

      Typical workflow:
        1. Send-JuribaAppRSetupFile to upload the installer
        2. New-JuribaAppRApplication to start processing
        3. Watch-JuribaAppRApplicationStatus to poll until complete
      .PARAMETER Instance
      The URL of the App Readiness instance. Not required if connected via Connect-JuribaAppR.
      .PARAMETER APIKey
      The API key for authentication. Not required if connected via Connect-JuribaAppR.
      .PARAMETER Uuid
      The upload UUID returned by Send-JuribaAppRSetupFile. This identifies the
      uploaded file on the server.
      .PARAMETER FileName
      The original file name of the uploaded setup file. Returned by Send-JuribaAppRSetupFile.
      .PARAMETER FileSize
      The size in bytes of the uploaded file. Returned by Send-JuribaAppRSetupFile.
      .PARAMETER TotalChunks
      The number of chunks the file was split into during upload. Returned by Send-JuribaAppRSetupFile.
      .PARAMETER Name
      Optional override name for the application. If not specified, the server-side
      metadata extraction API is called, falling back to the file name.
      .PARAMETER CommandLine
      Optional override install command line. If not specified, the command suggestion
      API is called to auto-detect the best install command.
      .PARAMETER Manufacturer
      Optional manufacturer/vendor name override.
      .PARAMETER Version
      Optional application version override.
      .PARAMETER RunImmediately
      When specified, immediately begins automated packaging after creation.
      This is the default behavior for most automation scenarios.
      .PARAMETER VMGroupId
      Optional. The VM group to use for packaging. If not specified, reads from
      the instance's Default Settings.
      .PARAMETER VMGroupForTestingId
      Optional. The VM group to use for smoke testing. If not specified, reads from
      the instance's Default Settings.
      .PARAMETER PrePackaged
      When specified, indicates the uploaded file is already in a final package
      format (e.g. an MSI or IntuneWin that does not need repackaging).
      .PARAMETER SkipMetadataExtraction
      When specified, skips the server-side metadata extraction API call.
      Use this if you are providing Name, Manufacturer, and Version explicitly.
      .PARAMETER SkipCommandSuggestion
      When specified, skips the command suggestion API call.
      Use this if you are providing CommandLine explicitly.
      .EXAMPLE
      $upload = Send-JuribaAppRSetupFile -FilePath "C:\Installers\Firefox-Setup.exe"
      New-JuribaAppRApplication -Uuid $upload.Uuid -FileName $upload.FileName `
          -FileSize $upload.FileSize -TotalChunks $upload.TotalChunks -RunImmediately
      Uploads a file and creates an application using default settings and auto-detection.
      .EXAMPLE
      $upload = Send-JuribaAppRSetupFile -FilePath "C:\Installers\App.exe"
      New-JuribaAppRApplication -Uuid $upload.Uuid -FileName $upload.FileName `
          -FileSize $upload.FileSize -TotalChunks $upload.TotalChunks `
          -Name "My App 2.0" -Manufacturer "Contoso" -Version "2.0" -RunImmediately
      Creates an application with explicit metadata overrides.
      .EXAMPLE
      $upload = Send-JuribaAppRSetupFile -FilePath "C:\Packages\App.msi"
      $app = New-JuribaAppRApplication -Uuid $upload.Uuid -FileName $upload.FileName `
          -FileSize $upload.FileSize -TotalChunks $upload.TotalChunks -PrePackaged
      Creates a pre-packaged application (no repackaging needed).
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $false)]
        [string]$Instance,

        [Parameter(Mandatory = $false)]
        [string]$APIKey,

        [Parameter(Mandatory = $true)]
        [string]$Uuid,

        [Parameter(Mandatory = $true)]
        [string]$FileName,

        [Parameter(Mandatory = $true)]
        [long]$FileSize,

        [Parameter(Mandatory = $true)]
        [int]$TotalChunks,

        [Parameter(Mandatory = $false)]
        [string]$Name,

        [Parameter(Mandatory = $false)]
        [string]$CommandLine,

        [Parameter(Mandatory = $false)]
        [string]$Manufacturer,

        [Parameter(Mandatory = $false)]
        [string]$Version,

        [Parameter(Mandatory = $false)]
        [switch]$RunImmediately,

        [Parameter(Mandatory = $false)]
        [int]$VMGroupId,

        [Parameter(Mandatory = $false)]
        [int]$VMGroupForTestingId,

        [Parameter(Mandatory = $false)]
        [switch]$PrePackaged,

        [Parameter(Mandatory = $false)]
        [switch]$SkipMetadataExtraction,

        [Parameter(Mandatory = $false)]
        [switch]$SkipCommandSuggestion
    )

    $conn = Get-JuribaAppRConnection -Instance $Instance -APIKey $APIKey

    # ── Step 1: Read Default Settings for VM groups and output format bitmask ──
    # This matches the UI's GET /api/default-settings call before creating an app.
    $defaults = $null
    try {
        $defaults = Get-JuribaAppRDefaultSetting -Instance $conn.Instance -APIKey $conn.APIKey
        Write-Verbose "Default Settings: VMGroup=$($defaults.VMGroupForRepackaging), TestGroup=$($defaults.VMGroupForTesting), OutputBitmask=$($defaults.OutputFormatBitmask)"
    }
    catch {
        Write-Warning "Could not read Default Settings: $($_.Exception.Message). Using fallback values."
    }

    # Resolve VM groups: explicit parameter > default settings
    $resolvedVMGroupId = if ($VMGroupId) { $VMGroupId }
                         elseif ($defaults -and $defaults.VMGroupForRepackaging) { $defaults.VMGroupForRepackaging }
                         else { 0 }

    $resolvedVMGroupForTestingId = if ($VMGroupForTestingId) { $VMGroupForTestingId }
                                   elseif ($defaults -and $defaults.VMGroupForTesting) { $defaults.VMGroupForTesting }
                                   else { 0 }

    # Resolve output format bitmask from default settings
    $outputBitmask = if ($defaults -and $defaults.OutputFormatBitmask) { $defaults.OutputFormatBitmask }
                     else { 0 }

    # ── Step 2: Server-side metadata extraction ──
    # Matches the UI's PUT /api/apm/application/setupFile/getMetadata/{uuid}
    $serverMeta = $null
    if (-not $SkipMetadataExtraction -and (-not $Name -or -not $Manufacturer -or -not $Version)) {
        try {
            Write-Verbose "Extracting metadata from server for upload $Uuid..."
            $serverMeta = Invoke-JuribaAppRRestMethod -Instance $conn.Instance -APIKey $conn.APIKey `
                -Uri "api/apm/application/setupFile/getMetadata/$Uuid" -Method PUT
            Write-Verbose "Server metadata: Name=$($serverMeta.applicationName), Mfg=$($serverMeta.applicationManufacturer), Ver=$($serverMeta.applicationVersion)"
        }
        catch {
            Write-Warning "Server-side metadata extraction failed: $($_.Exception.Message)"
        }
    }

    # Resolve name/manufacturer/version: explicit param > server metadata > fallback
    $appName = if ($Name)         { $Name }
               elseif ($serverMeta -and $serverMeta.applicationName) { $serverMeta.applicationName }
               else { [System.IO.Path]::GetFileNameWithoutExtension($FileName) }

    $appMfg  = if ($Manufacturer) { $Manufacturer }
               elseif ($serverMeta -and $serverMeta.applicationManufacturer) { $serverMeta.applicationManufacturer }
               else { "Unknown" }

    $appVer  = if ($Version)      { $Version }
               elseif ($serverMeta -and $serverMeta.applicationVersion) { $serverMeta.applicationVersion }
               else { "1.0" }

    # ── Step 3: Command suggestion API ──
    # Matches the UI's POST /api/application/temp/commands/suggestion
    $suggestedCmd = $null
    if (-not $SkipCommandSuggestion -and -not $CommandLine) {
        try {
            Write-Verbose "Getting command suggestions for $FileName..."
            $suggestedCmd = Get-JuribaAppRCommandSuggestion `
                -Instance $conn.Instance -APIKey $conn.APIKey `
                -UploadId $Uuid -FileName $FileName `
                -Name $appName -Manufacturer $appMfg -Version $appVer
            if ($suggestedCmd) {
                Write-Verbose "Suggested command: $($suggestedCmd | ConvertTo-Json -Compress)"
            }
        }
        catch {
            Write-Warning "Command suggestion failed: $($_.Exception.Message)"
        }
    }

    # Parse command suggestions — the API returns a commands array with source priority:
    # source 3 = Juriba KB, source 1 = programmatic, source 2 = AI
    # Within each source, prefer: lastResult > 0 (previously successful), then highest
    # successRate, then first in array (API returns best-ranked first).
    $suggestedInstallCmd = $null
    if ($suggestedCmd) {
        $cmds = $null
        if ($suggestedCmd.PSObject.Properties['commands']) {
            $cmds = $suggestedCmd.commands
        }
        if ($cmds -and $cmds.Count -gt 0) {
            $installCmds = @($cmds | Where-Object { $_.type -eq 1 })
            Write-Verbose "Install command candidates: $($installCmds.Count)"
            foreach ($c in $installCmds) {
                $srcLabel = switch ($c.source) { 3 { 'JuribaKB' } 1 { 'Programmatic' } 2 { 'AI' } default { "Unknown($($c.source))" } }
                Write-Verbose " [$srcLabel] $($c.command) (success=$($c.successRate) failure=$($c.failureRate) lastResult=$($c.lastResult))"
            }

            # Source priority: Juriba KB (3), programmatic (1), AI (2)
            foreach ($sourcePriority in @(3, 1, 2)) {
                $candidates = @($installCmds | Where-Object { $_.source -eq $sourcePriority })
                if ($candidates.Count -gt 0) {
                    # Within a source: prefer last-used successful (lastResult > 0),
                    # then highest successRate, then first (API's own ranking)
                    $pick = $candidates |
                        Sort-Object -Property @{ Expression = { if ($_.lastResult -gt 0) { 0 } else { 1 } } },
                                              @{ Expression = { $_.successRate }; Descending = $true } |
                        Select-Object -First 1
                    $suggestedInstallCmd = $pick.command
                    $srcLabel = switch ($sourcePriority) { 3 { 'JuribaKB' } 1 { 'Programmatic' } 2 { 'AI' } }
                    Write-Verbose "Selected install command [$srcLabel]: $suggestedInstallCmd"
                    break
                }
            }
        }
        else {
            Write-Verbose "Command suggestion response had no commands array"
        }
    }

    # Resolve install command: explicit param > suggestion API > empty
    # Uninstall is intentionally left blank — the product handles it during packaging
    $installCmd = if ($CommandLine) { $CommandLine }
                  elseif ($suggestedInstallCmd) { $suggestedInstallCmd }
                  else { "" }

    # ── Step 4: Build the request body ──

    # applicationInfo — matches the UI's AddApplicationViewModel
    $applicationInfo = @{
        appVer                 = $appVer
        manufacturer           = $appMfg
        name                   = $appName
        pkgVer                 = "1.0"
        siteCode               = "GLOBAL"
        isDiscovery            = $false
        preReqIds              = @()
        upgradeAppIds          = @()
        existingAppId          = -1
        fullPreReqInfo         = @()
        source                 = 0
        sourceFileName         = $FileName
        sendUda                = $true
        operatingSystemType    = 0
        isAutomatedRepackaging = $false
    }

    # Add install command line if we have one
    if ($installCmd) {
        $applicationInfo['cmdLine'] = $installCmd
    }
    # Explicitly set uninstall to empty — without this, the server copies
    # the install command to uninstall, which hangs the VM
    $applicationInfo['uninstall'] = ""

    # uploadChunkModel — tells the server which uploaded chunks to use
    $uploadChunkModel = @{
        dzIdentifier  = $Uuid
        fileName      = $FileName
        expectedBytes = $FileSize
        totalChunks   = $TotalChunks
    }

    # packageTypeMatrixModel — determines repackaging vs wrapping workflow
    # from=0: repackage from source (VM required, for MSI/MSIX output)
    # from=4: wrap only (no VM, for IntuneWin/PSADT only)
    $needsRepackaging = ($outputBitmask -band 1) -or ($outputBitmask -band 4) -or ($outputBitmask -band 8)  # MSI, MSIX, MSIX App Attach
    $matrixFrom = if ($needsRepackaging) { 0 } else { 4 }
    Write-Verbose "Output bitmask=$outputBitmask, needsRepackaging=$needsRepackaging, matrixFrom=$matrixFrom"

    $packageTypeMatrixModel = @{
        from = $matrixFrom
        to   = $outputBitmask
    }

    # Main request body (AddApplicationParentViewModel)
    $body = @{
        applicationId          = -1
        applicationInfo        = $applicationInfo
        preReqs                = @()
        fullPreReqInfo         = @()
        upgradeAppIds          = @()
        setAsMainSource        = $true
        uploadChunkModel       = $uploadChunkModel
        packageTypeMatrixModel = $packageTypeMatrixModel
        runImmediately         = [bool]$RunImmediately
        runEvAsAnotherUser     = $null
    }

    # VM groups — only needed for repackaging, use -1 for wrapping-only
    if ($needsRepackaging -and $resolvedVMGroupId -gt 0) {
        $body['vmGroupId'] = $resolvedVMGroupId
    }
    elseif (-not $needsRepackaging) {
        $body['vmGroupId'] = -1
    }
    if ($resolvedVMGroupForTestingId -gt 0) {
        $body['vmGroupForTestingId'] = $resolvedVMGroupForTestingId
    }

    $Target = if ($appName -ne [System.IO.Path]::GetFileNameWithoutExtension($FileName)) { $appName } else { $FileName }

    if ($PSCmdlet.ShouldProcess($Target, "Create Application")) {
        if ($PrePackaged) {
            $uri = "api/apm/application/prePackaged/async"
        }
        else {
            $uri = "api/apm/application/async"
        }

        Write-Verbose "Submitting create request to $uri..."
        Write-Verbose "Body: $($body | ConvertTo-Json -Depth 5 -Compress)"

        $result = Invoke-JuribaAppRRestMethod -Instance $conn.Instance -APIKey $conn.APIKey `
            -Uri $uri -Method POST -Body $body

        # Return a useful object with the UUID for tracking
        if ($result) {
            $result | Add-Member -NotePropertyName 'Uuid' -NotePropertyValue $Uuid -Force -PassThru
        }
        else {
            # API returned success but empty body — return tracking object
            [PSCustomObject]@{
                Uuid     = $Uuid
                FileName = $FileName
                Status   = 'Submitted'
            }
        }
    }
}