Private/OEM/SurfaceAdapter.ps1

# Microsoft Surface OEM Adapter
# Handles Surface driver/firmware MSI packs via the Microsoft Download Center.
# Surface does not provide a machine-readable catalog like Dell or Lenovo,
# so models and Download Center IDs are maintained in OEMSources.json.

function Update-SurfaceCatalogCache {
    <#
    .SYNOPSIS
        Validates the Surface model catalog from OEMSources.json.
    .DESCRIPTION
        Unlike Dell/Lenovo, Surface has no downloadable catalog file.
        The model-to-download-ID mapping lives in OEMSources.json.
        This function simply loads and validates that the mapping is present.
    .PARAMETER ForceRefresh
        Not used for Surface (kept for adapter interface consistency).
    .PARAMETER CacheTTLHours
        Not used for Surface (kept for adapter interface consistency).
    #>

    [CmdletBinding()]
    param(
        [switch]$ForceRefresh,
        [int]$CacheTTLHours = 24
    )

    $Sources = Get-DATOEMSources
    if (-not $Sources.surface -or -not $Sources.surface.models) {
        throw "Surface configuration missing from OEMSources.json. Ensure a 'surface.models' section exists."
    }

    $ModelCount = $Sources.surface.models.Count
    Write-DATLog -Message "Surface catalog loaded: $ModelCount model(s) configured in OEMSources.json" -Severity 1
}

function Get-SurfaceModelList {
    <#
    .SYNOPSIS
        Returns all Microsoft Surface models configured in OEMSources.json.
    .OUTPUTS
        Array of PSCustomObjects with Manufacturer, Model, and DownloadID.
    #>

    [CmdletBinding()]
    param()

    $Sources = Get-DATOEMSources
    if (-not $Sources.surface -or -not $Sources.surface.models) {
        throw "Surface model list not configured in OEMSources.json"
    }

    $Models = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($ModelName in $Sources.surface.models.Keys) {
        $ModelInfo = $Sources.surface.models[$ModelName]
        $DownloadID = if ($ModelInfo -is [hashtable] -or $ModelInfo -is [PSCustomObject]) { $ModelInfo.id } else { $ModelInfo }

        $Models.Add([PSCustomObject]@{
            Manufacturer = 'Microsoft'
            Model        = $ModelName
            DownloadID   = $DownloadID
            Platform     = ''
        })
    }

    return ($Models | Sort-Object Model)
}

function Get-SurfaceDriverPack {
    <#
    .SYNOPSIS
        Finds the latest Surface driver/firmware MSI for a specific model and OS.
    .DESCRIPTION
        Scrapes the Microsoft Download Center confirmation page for the given model's
        Download Center ID, parses available MSI files, and returns the best match
        for the requested OS and build number.
    .PARAMETER Model
        The Surface model name (e.g., 'Surface Pro 9').
    .PARAMETER OperatingSystem
        Target OS (e.g., 'Windows 11 24H2').
    .PARAMETER Architecture
        Target architecture. Default: 'x64'.
    .OUTPUTS
        PSCustomObject with Url, Version, FileName, or $null if not found.
    #>

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

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

        [string]$Architecture = 'x64'
    )

    $Sources = Get-DATOEMSources
    if (-not $Sources.surface -or -not $Sources.surface.models) {
        Write-DATLog -Message "Surface not configured in OEMSources.json" -Severity 3
        return $null
    }

    # Look up Download Center ID for this model
    $ModelInfo = $Sources.surface.models.$Model
    if (-not $ModelInfo) {
        # Try fuzzy match
        $FuzzyMatch = $Sources.surface.models.Keys | Where-Object { $Model -like "*$_*" -or $_ -like "*$Model*" } | Select-Object -First 1
        if ($FuzzyMatch) {
            $ModelInfo = $Sources.surface.models[$FuzzyMatch]
            $Model = $FuzzyMatch
            Write-DATLog -Message "Fuzzy-matched Surface model to: $Model" -Severity 1
        }
    }

    if (-not $ModelInfo) {
        Write-DATLog -Message "Surface model '$Model' not found in OEMSources.json. Available: $($Sources.surface.models.Keys -join ', ')" -Severity 2
        return $null
    }

    $DownloadID = if ($ModelInfo -is [hashtable] -or $ModelInfo -is [PSCustomObject]) { $ModelInfo.id } else { $ModelInfo }

    # Resolve target Windows build number from OS name
    $TargetBuild = $null
    if ($Sources.windowsBuilds) {
        $BuildVersion = $Sources.windowsBuilds.$OperatingSystem
        if ($BuildVersion -and $BuildVersion -match '(\d{5})$') {
            $TargetBuild = $Matches[1]
        }
    }

    # Determine Win10 vs Win11 from OS name
    $WinTag = if ($OperatingSystem -match 'Windows 11') { 'Win11' }
              elseif ($OperatingSystem -match 'Windows 10') { 'Win10' }
              else { 'Win11' }

    # Cache key for the scraped download page (avoid re-scraping within TTL)
    $CacheKey = "Surface_DownloadPage_$DownloadID"
    $CachedPage = Get-DATCachedItem -Key $CacheKey -MaxAgeHours 24

    $PageContent = $null
    if ($CachedPage) {
        $PageContent = Get-Content -Path $CachedPage -Raw -ErrorAction SilentlyContinue
    }

    if (-not $PageContent) {
        # Scrape the Download Center page to find direct MSI download links.
        # Use Invoke-WebRequest with browser-like headers instead of Invoke-DATDownload
        # (which is designed for file transfers and uses BITS/raw sockets).
        # Try confirmation.aspx first (direct download links), fall back to details.aspx.
        $BaseUrl = $Sources.surface.downloadCenterBase.TrimEnd('/')

        # Ensure TLS 1.2 for microsoft.com
        if ([System.Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12') {
            [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12
        }

        $WebParams = @{
            UseBasicParsing = $true
            TimeoutSec      = 60
            Headers         = @{
                'Accept'          = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
                'Accept-Language' = 'en-US,en;q=0.5'
            }
            UserAgent       = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
            ErrorAction     = 'Stop'
        }

        $UrlsToTry = @(
            "$BaseUrl/confirmation.aspx?id=$DownloadID"
            "$BaseUrl/details.aspx?id=$DownloadID"
        )

        foreach ($PageUrl in $UrlsToTry) {
            Write-DATLog -Message "Querying Microsoft Download Center for $Model (ID: $DownloadID): $PageUrl" -Severity 1
            try {
                $Response = Invoke-WebRequest -Uri $PageUrl @WebParams
                if ($Response.StatusCode -eq 200 -and $Response.Content) {
                    $PageContent = $Response.Content
                    Write-DATLog -Message "Download Center page retrieved ($([math]::Round($PageContent.Length / 1KB, 1)) KB)" -Severity 1

                    # Cache the successful page content
                    $TempDir = Get-DATTempPath -Prefix 'SurfaceDownload'
                    try {
                        $PagePath = Join-Path $TempDir 'download_page.html'
                        $PageContent | Set-Content -Path $PagePath -Encoding UTF8
                        Set-DATCachedItem -Key $CacheKey -SourcePath $PagePath -SourceUrl $PageUrl
                    } finally {
                        Remove-DATTempPath -Path $TempDir
                    }
                    break
                }
            } catch {
                Write-DATLog -Message "Download Center query failed for $PageUrl`: $($_.Exception.Message)" -Severity 2
            }
        }

        if (-not $PageContent) {
            Write-DATLog -Message "Failed to query Microsoft Download Center for $Model (ID: $DownloadID) - all URLs returned errors" -Severity 3
            return $null
        }
    }

    if (-not $PageContent) {
        Write-DATLog -Message "Empty response from Download Center for $Model (ID: $DownloadID)" -Severity 3
        return $null
    }

    # Parse all MSI download URLs from the page
    # Pattern: https://download.microsoft.com/download/GUID-path/filename.msi
    $MsiUrls = [regex]::Matches($PageContent, 'https://download\.microsoft\.com/download/[^"''>\s]+\.msi') |
        ForEach-Object { $_.Value } | Select-Object -Unique

    if (-not $MsiUrls -or $MsiUrls.Count -eq 0) {
        Write-DATLog -Message "No MSI download links found on Download Center page for $Model (ID: $DownloadID)" -Severity 2
        return $null
    }

    Write-DATLog -Message "Found $($MsiUrls.Count) MSI download(s) for $Model" -Severity 1

    # Score and select the best MSI match for the requested OS/build
    # MSI naming: SurfacePro10forBusiness_Win11_22631_26.013.37121.0.msi
    $BestUrl = $null
    $BestScore = -1
    $BestFileName = $null
    $BestVersion = $null
    $BestBuild = $null

    foreach ($Url in $MsiUrls) {
        $FileName = Split-Path $Url -Leaf
        $Score = 0

        # Must match Win10/Win11
        if ($FileName -match $WinTag) {
            $Score += 10
        } else {
            continue  # Wrong OS family, skip entirely
        }

        # Extract build number from filename
        if ($FileName -match "${WinTag}_(\d{5})_") {
            $FileBuild = $Matches[1]

            if ($TargetBuild -and $FileBuild -eq $TargetBuild) {
                $Score += 100  # Exact build match
            } elseif ($TargetBuild -and [int]$FileBuild -le [int]$TargetBuild) {
                # Closest build that doesn't exceed target (Microsoft's guidance)
                $Score += 50 + (1.0 / ([int]$TargetBuild - [int]$FileBuild + 1))
            } else {
                $Score += 1  # Build is higher than target, still usable
            }
        }

        # Extract version from filename
        $Version = $null
        if ($FileName -match '_(\d+\.\d+\.\d+\.\d+)\.msi$') {
            $Version = $Matches[1]
        }

        if ($Score -gt $BestScore) {
            $BestScore = $Score
            $BestUrl = $Url
            $BestFileName = $FileName
            $BestVersion = $Version
            $BestBuild = $FileBuild
        }
    }

    if (-not $BestUrl) {
        Write-DATLog -Message "No matching $WinTag MSI found for $Model among $($MsiUrls.Count) available downloads" -Severity 2
        return $null
    }

    $Result = [PSCustomObject]@{
        Manufacturer = 'Microsoft'
        Model        = $Model
        OS           = $OperatingSystem
        Architecture = $Architecture
        Version      = $BestVersion
        ReleaseDate  = $null
        Url          = $BestUrl
        FileName     = $BestFileName
        DownloadID   = $DownloadID
        BuildNumber  = $BestBuild
    }

    Write-DATLog -Message "Selected Surface driver pack: $BestFileName (v$BestVersion, build $BestBuild) for $Model" -Severity 1
    return $Result
}

function Get-SurfaceBIOSUpdate {
    <#
    .SYNOPSIS
        Surface firmware is bundled with the driver MSI - no separate BIOS update.
    .DESCRIPTION
        Unlike Dell/Lenovo, Microsoft Surface firmware updates are included in the
        same MSI as driver updates. This function returns $null and logs accordingly.
    .PARAMETER Model
        The Surface model name.
    .PARAMETER OperatingSystem
        Target OS.
    .OUTPUTS
        Always returns $null (firmware is in the driver pack).
    #>

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

        [string]$OperatingSystem = 'Windows 11'
    )

    Write-DATLog -Message "Surface firmware for $Model is included in the driver MSI pack - no separate BIOS update needed" -Severity 1
    return $null
}

function Test-SurfaceCatalogConnectivity {
    <#
    .SYNOPSIS
        Tests connectivity to the Microsoft Download Center.
    .OUTPUTS
        PSCustomObject with endpoint status results.
    #>

    [CmdletBinding()]
    param()

    $Sources = Get-DATOEMSources
    $Results = [System.Collections.Generic.List[PSCustomObject]]::new()

    $DownloadCenterUrl = "$($Sources.surface.downloadCenterBase)details.aspx?id=105947"

    $Reachable = Test-DATUrlReachable -Url $DownloadCenterUrl
    $Results.Add([PSCustomObject]@{
        Manufacturer = 'Microsoft'
        Endpoint     = 'DownloadCenter'
        Url          = $DownloadCenterUrl
        Reachable    = $Reachable
    })

    $SeverityLevel = if ($Reachable) { 1 } else { 3 }
    $StatusText = if ($Reachable) { 'OK' } else { 'UNREACHABLE' }
    Write-DATLog -Message "Microsoft DownloadCenter: $StatusText ($DownloadCenterUrl)" -Severity $SeverityLevel

    return $Results
}