Private/OEM/LenovoAdapter.ps1

# Lenovo OEM Adapter
# Handles Lenovo Catalog v2 XML for driver packs and BIOS updates.

function Update-LenovoCatalogCache {
    <#
    .SYNOPSIS
        Downloads and caches the Lenovo driver catalog.
    .PARAMETER ForceRefresh
        Forces re-download even if cache is valid.
    .PARAMETER CacheTTLHours
        Cache time-to-live in hours. Default: 24.
    #>

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

    $Sources = Get-DATOEMSources
    $LenovoSources = $Sources.lenovo

    # Driver Catalog v2
    $DriverCacheKey = 'Lenovo_CatalogV2.xml'
    $Cached = if (-not $ForceRefresh) { Get-DATCachedItem -Key $DriverCacheKey -MaxAgeHours $CacheTTLHours } else { $null }

    if (-not $Cached) {
        Write-DATLog -Message "Downloading Lenovo driver catalog (catalogv2.xml)" -Severity 1
        $TempDir = Get-DATTempPath -Prefix 'LenovoDriverCat'
        try {
            $Url = $LenovoSources.driverCatalog

            # catalogv2.xml may be a direct XML or a .cab - handle both
            $FileName = Split-Path $Url -Leaf
            $DownloadPath = Join-Path $TempDir $FileName

            Invoke-DATDownload -Url $Url -DestinationPath $DownloadPath

            if ($FileName -like '*.cab') {
                # Expand cabinet
                $ExtractedFiles = Expand-DATCabinet -CabPath $DownloadPath -DestinationPath $TempDir -Filter '*.xml'
                $XmlFile = $ExtractedFiles | Where-Object { $_ -like '*.xml' } | Select-Object -First 1
            } else {
                $XmlFile = $DownloadPath
            }

            if ($XmlFile -and (Test-Path $XmlFile)) {
                Set-DATCachedItem -Key $DriverCacheKey -SourcePath $XmlFile -SourceUrl $Url
                Write-DATLog -Message "Lenovo driver catalog cached successfully" -Severity 1
            } else {
                throw "Failed to obtain Lenovo catalog XML"
            }
        } finally {
            Remove-DATTempPath -Path $TempDir
        }
    }
}

function Get-LenovoModelList {
    <#
    .SYNOPSIS
        Returns all Lenovo models available in the catalog.
    .OUTPUTS
        Array of PSCustomObjects with Model, MachineType, and supported OS.
    #>

    [CmdletBinding()]
    param()

    $CatalogPath = Get-DATCachedItem -Key 'Lenovo_CatalogV2.xml'
    if (-not $CatalogPath) {
        Update-LenovoCatalogCache
        $CatalogPath = Get-DATCachedItem -Key 'Lenovo_CatalogV2.xml'
    }

    if (-not $CatalogPath) {
        throw "Lenovo catalog not available. Check network connectivity."
    }

    $Xml = Read-DATXml -Path $CatalogPath
    $Models = [System.Collections.Generic.List[PSCustomObject]]::new()
    $Seen = [System.Collections.Generic.HashSet[string]]::new()

    # Lenovo catalogv2.xml structure: ModelList > Model elements with @name attribute
    $Products = $Xml.SelectNodes('//Model')
    if (-not $Products -or $Products.Count -eq 0) {
        # Fallback: try legacy element names in case catalog format changes
        $Products = $Xml.SelectNodes('//Product')
    }
    if (-not $Products -or $Products.Count -eq 0) {
        $Products = $Xml.SelectNodes('//ModelType')
    }

    foreach ($Product in $Products) {
        $ModelName = $Product.name
        if (-not $ModelName) { $ModelName = $Product.Model }
        if (-not $ModelName) { continue }

        $MachineTypes = @()
        # Machine types can be in Types/Type or directly as attribute
        $TypeNodes = $Product.SelectNodes('.//Type')
        if ($TypeNodes -and $TypeNodes.Count -gt 0) {
            $MachineTypes = @($TypeNodes | ForEach-Object { $_.InnerText.Trim() } | Where-Object { $_ })
        } elseif ($Product.Types) {
            $MachineTypes = @($Product.Types.Split(',') | ForEach-Object { $_.Trim() })
        }

        $Key = $ModelName
        if (-not $Seen.Contains($Key)) {
            $Seen.Add($Key) | Out-Null
            $Models.Add([PSCustomObject]@{
                Manufacturer = 'Lenovo'
                Model        = $ModelName
                MachineType  = ($MachineTypes -join ';')
                Platform     = ''
            })
        }
    }

    return ($Models | Sort-Object Model)
}

function Find-LenovoMachineType {
    <#
    .SYNOPSIS
        Resolves a Lenovo model friendly name to its machine type code(s).
    .PARAMETER Model
        The Lenovo model name (e.g., 'ThinkPad T14 Gen 4').
    .OUTPUTS
        Array of machine type strings, or $null if not found.
    #>

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

    $AllModels = Get-LenovoModelList
    $Match = $AllModels | Where-Object {
        $_.Model -eq $Model -or
        $_.Model -like "*$Model*"
    } | Select-Object -First 1

    if ($Match -and $Match.MachineType) {
        return $Match.MachineType.Split(';') | ForEach-Object { $_.Trim() } | Where-Object { $_ }
    }

    Write-DATLog -Message "Could not resolve Lenovo machine type for model: $Model" -Severity 2
    return $null
}

function Get-LenovoDriverPack {
    <#
    .SYNOPSIS
        Finds the latest Lenovo driver pack for a specific model and OS.
    .PARAMETER Model
        The Lenovo model name (e.g., 'ThinkPad T14 Gen 4').
    .PARAMETER MachineType
        Optional machine type code. If not provided, will be looked up.
    .PARAMETER OperatingSystem
        Target OS (e.g., 'Windows 11 24H2').
    .OUTPUTS
        PSCustomObject with Url, Version, FileName, or $null if not found.
    #>

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

        [string]$MachineType,

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

    $CatalogPath = Get-DATCachedItem -Key 'Lenovo_CatalogV2.xml'
    if (-not $CatalogPath) {
        Update-LenovoCatalogCache
        $CatalogPath = Get-DATCachedItem -Key 'Lenovo_CatalogV2.xml'
    }

    $Xml = Read-DATXml -Path $CatalogPath

    # Resolve machine type if not provided
    if (-not $MachineType) {
        $MachineTypes = Find-LenovoMachineType -Model $Model
        if (-not $MachineTypes) {
            Write-DATLog -Message "Cannot find Lenovo driver pack: machine type unknown for $Model" -Severity 2
            return $null
        }
        $MachineType = $MachineTypes | Select-Object -First 1
    }

    # Determine OS matching criteria
    $OsPattern = ConvertTo-LenovoOSPattern -OperatingSystem $OperatingSystem

    # Extract build version for version-specific matching (e.g., "23H2" from "Windows 11 23H2")
    $BuildVersion = $null
    if ($OperatingSystem -match 'Windows 1[01]\s+(\d{2}H\d)') {
        $BuildVersion = $Matches[1]
    }

    # Search catalog for matching driver pack
    # Lenovo catalogv2.xml: ModelList > Model > SCCM elements with os/version/date attributes
    $DriverPack = $null
    $Products = $Xml.SelectNodes('//Model')
    if (-not $Products -or $Products.Count -eq 0) {
        $Products = $Xml.SelectNodes('//Product')
    }

    foreach ($Product in $Products) {
        # Check machine type match
        $ProductTypes = @()
        $TypeNodes = $Product.SelectNodes('.//Type')
        if ($TypeNodes) {
            $ProductTypes = @($TypeNodes | ForEach-Object { $_.InnerText.Trim() })
        }

        $TypeMatch = $ProductTypes | Where-Object { $_ -eq $MachineType }
        if (-not $TypeMatch) { continue }

        # Look for SCCM driver packages within this model
        $DriverPackNodes = @($Product.SelectNodes('.//SCCM'))

        # Two-tier tracking: prefer version-specific match, fall back to any OS match
        $BestVersionMatch = $null
        $BestVersionDate = [datetime]::MinValue
        $BestFallbackMatch = $null
        $BestFallbackDate = [datetime]::MinValue

        foreach ($Pack in $DriverPackNodes) {
            if (-not $Pack) { continue }

            $PackOS = $Pack.os
            if (-not $PackOS) { continue }

            if ($PackOS -match $OsPattern) {
                $PackUrl = $Pack.InnerText.Trim()
                if (-not $PackUrl) { continue }

                $PackDate = [datetime]::MinValue
                if ($Pack.date) {
                    try { $PackDate = [datetime]::Parse($Pack.date) } catch { }
                }

                # Check if this pack has a matching version attribute
                $PackVersion = $Pack.version
                if ($BuildVersion -and $PackVersion -and $PackVersion -match $BuildVersion) {
                    if ($PackDate -ge $BestVersionDate) {
                        $BestVersionDate = $PackDate
                        $BestVersionMatch = $Pack
                    }
                }

                # Always track as potential fallback (any OS match)
                if ($PackDate -ge $BestFallbackDate) {
                    $BestFallbackDate = $PackDate
                    $BestFallbackMatch = $Pack
                }
            }
        }

        # Prefer version-specific match; fall back to any OS match
        $BestPack = if ($BestVersionMatch) { $BestVersionMatch } else { $BestFallbackMatch }

        if ($BestVersionMatch) {
            Write-DATLog -Message "Found version-specific Lenovo driver pack match (version: $BuildVersion)" -Severity 1
        } elseif ($BestFallbackMatch -and $BuildVersion) {
            Write-DATLog -Message "No version-specific match for $BuildVersion; using best available driver pack" -Severity 2
        }

        if ($BestPack) {
            $PackUrl = $BestPack.InnerText.Trim()

            # Get ALL machine types for this model so the package description
            # includes every variant (e.g., "20U7;20U8") for TS script matching
            $AllTypes = Find-LenovoMachineType -Model $Model
            $AllMachineTypesStr = if ($AllTypes) { $AllTypes -join ';' } else { $MachineType }

            $DriverPack = [PSCustomObject]@{
                Manufacturer    = 'Lenovo'
                Model           = $Model
                MachineType     = $MachineType
                AllMachineTypes = $AllMachineTypesStr
                OS              = $OperatingSystem
                Architecture    = 'x64'
                Version         = $BestPack.version
                ReleaseDate     = $BestPack.date
                Url             = $PackUrl
                FileName        = Split-Path $PackUrl -Leaf
            }
            break
        }
    }

    if (-not $DriverPack) {
        Write-DATLog -Message "No Lenovo driver pack found for $Model ($MachineType) / $OperatingSystem" -Severity 2
        return $null
    }

    Write-DATLog -Message "Found Lenovo driver pack: $($DriverPack.FileName) for $Model ($MachineType)" -Severity 1
    return $DriverPack
}

function Get-LenovoBIOSUpdate {
    <#
    .SYNOPSIS
        Finds the latest Lenovo BIOS update for a specific model.
    .PARAMETER Model
        The Lenovo model name.
    .PARAMETER MachineType
        Machine type code. If not provided, will be looked up.
    .PARAMETER OperatingSystem
        Target OS (needed for Lenovo's OS-specific BIOS XML URLs).
    .OUTPUTS
        PSCustomObject with Url, Version, ReleaseDate, FileName, or $null if not found.
    #>

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

        [string]$MachineType,

        [string]$OperatingSystem = 'Windows 11'
    )

    $Sources = Get-DATOEMSources

    # Resolve machine type if not provided
    if (-not $MachineType) {
        $MachineTypes = Find-LenovoMachineType -Model $Model
        if (-not $MachineTypes) {
            Write-DATLog -Message "Cannot find BIOS update: machine type unknown for Lenovo $Model" -Severity 2
            return $null
        }
        $MachineType = $MachineTypes | Select-Object -First 1
    }

    # Determine Windows version for BIOS XML URL
    $WinVersion = if ($OperatingSystem -match 'Windows 11') { '11' } else { '10' }
    $BiosXmlUrl = '{0}{1}_Win{2}.xml' -f $Sources.lenovo.biosBase, $MachineType, $WinVersion

    Write-DATLog -Message "Checking Lenovo BIOS catalog: $BiosXmlUrl" -Severity 1

    # Download BIOS XML for this machine type
    $TempDir = Get-DATTempPath -Prefix 'LenovoBios'
    try {
        $XmlPath = Join-Path $TempDir 'bios.xml'

        try {
            Invoke-DATDownload -Url $BiosXmlUrl -DestinationPath $XmlPath -MaxRetries 2
        } catch {
            Write-DATLog -Message "Lenovo BIOS XML not available for machine type $MachineType`: $($_.Exception.Message)" -Severity 2
            return $null
        }

        $Xml = Read-DATXml -Path $XmlPath

        # Parse BIOS packages (Lenovo XML can contain firmware, drivers, etc. alongside BIOS)
        $AllPackages = $Xml.SelectNodes('//Package')

        # Filter to BIOS packages only
        $BiosPackages = @()
        if ($AllPackages -and $AllPackages.Count -gt 0) {
            $BiosPackages = @($AllPackages | Where-Object {
                $_.Category -match 'BIOS' -or $_.Title -match 'BIOS' -or $_.Name -match 'BIOS'
            })
        }

        # If category filter yielded nothing, fall back to all packages (some XMLs may not have Category)
        if ($BiosPackages.Count -eq 0 -and $AllPackages -and $AllPackages.Count -gt 0) {
            Write-DATLog -Message "No BIOS-categorized packages found; using all packages for Lenovo $Model ($MachineType)" -Severity 2
            $BiosPackages = @($AllPackages)
        }

        if ($BiosPackages.Count -eq 0) {
            Write-DATLog -Message "No BIOS packages found for Lenovo $Model ($MachineType)" -Severity 2
            return $null
        }

        # Get latest by version/date
        $Latest = $BiosPackages | Sort-Object {
            if ($_.ReleaseDate) { [datetime]::Parse($_.ReleaseDate) } else { [datetime]::MinValue }
        } -Descending | Select-Object -First 1

        $DownloadUrl = $Latest.URL
        if (-not $DownloadUrl) { $DownloadUrl = $Latest.Location }

        if (-not $DownloadUrl) {
            Write-DATLog -Message "BIOS package found but no download URL for Lenovo $Model" -Severity 2
            return $null
        }

        # Get ALL machine types for this model so the BIOS package description
        # includes every variant for TS script matching (consistent with driver packages)
        $AllTypes = Find-LenovoMachineType -Model $Model
        $AllMachineTypesStr = if ($AllTypes) { $AllTypes -join ';' } else { $MachineType }

        $Result = [PSCustomObject]@{
            Manufacturer    = 'Lenovo'
            Model           = $Model
            MachineType     = $MachineType
            AllMachineTypes = $AllMachineTypesStr
            Type            = 'BIOS'
            Version         = $Latest.version
            ReleaseDate     = $Latest.ReleaseDate
            Url             = $DownloadUrl
            FileName        = Split-Path $DownloadUrl -Leaf
        }

        Write-DATLog -Message "Found Lenovo BIOS update: v$($Result.Version) ($($Result.ReleaseDate)) for $Model" -Severity 1
        return $Result
    } finally {
        Remove-DATTempPath -Path $TempDir
    }
}

function ConvertTo-LenovoOSPattern {
    <#
    .SYNOPSIS
        Converts a friendly OS name to a regex pattern for Lenovo catalog matching.
    .DESCRIPTION
        Lenovo catalogv2.xml uses os="win10" or os="win11" on SCCM elements.
        Returns a regex pattern that matches these values.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$OperatingSystem
    )

    if ($OperatingSystem -match 'Windows 11') {
        return 'win11'
    } elseif ($OperatingSystem -match 'Windows 10') {
        return 'win10'
    }

    # Fallback: match any Windows
    return 'win1[01]'
}

function Test-LenovoCatalogConnectivity {
    <#
    .SYNOPSIS
        Tests connectivity to Lenovo catalog endpoints.
    .OUTPUTS
        PSCustomObject with endpoint status results.
    #>

    [CmdletBinding()]
    param()

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

    foreach ($Endpoint in @(
        @{ Name = 'DriverCatalog'; Url = $Sources.lenovo.driverCatalog }
        @{ Name = 'BIOSBase'; Url = $Sources.lenovo.biosBase }
    )) {
        $Reachable = Test-DATUrlReachable -Url $Endpoint.Url
        $Results.Add([PSCustomObject]@{
            Manufacturer = 'Lenovo'
            Endpoint     = $Endpoint.Name
            Url          = $Endpoint.Url
            Reachable    = $Reachable
        })

        $SeverityLevel = if ($Reachable) { 1 } else { 3 }
        $StatusText = if ($Reachable) { 'OK' } else { 'UNREACHABLE' }
        Write-DATLog -Message "Lenovo $($Endpoint.Name): $StatusText ($($Endpoint.Url))" -Severity $SeverityLevel
    }

    return $Results
}