Private/SKU/Get-SkuSimilarityScore.ps1

function Get-SkuSimilarityScore {
    <#
    .SYNOPSIS
        Scores how similar a candidate SKU is to a target SKU profile.
    .DESCRIPTION
        Weighted scoring across 8 dimensions: vCPU (20), memory (20), family (18),
        family version newness (12), architecture (10), premium IO (5), disk IOPS (8),
        data disk count (7). Max 100.
        Family version newness strongly rewards the latest SKU generations (_v7 > _v6 > _v5)
        to prioritize lifecycle upgrades to the newest available hardware.
    #>

    param(
        [Parameter(Mandatory)][hashtable]$Target,
        [Parameter(Mandatory)][hashtable]$Candidate,
        [hashtable]$FamilyInfo
    )

    $score = 0

    # vCPU closeness (20 points)
    if ($Target.vCPU -gt 0 -and $Candidate.vCPU -gt 0) {
        $maxCpu = [math]::Max($Target.vCPU, $Candidate.vCPU)
        $cpuScore = 1 - ([math]::Abs($Target.vCPU - $Candidate.vCPU) / $maxCpu)
        $score += [math]::Round($cpuScore * 20)
    }

    # Memory closeness (20 points)
    if ($Target.MemoryGB -gt 0 -and $Candidate.MemoryGB -gt 0) {
        $maxMem = [math]::Max($Target.MemoryGB, $Candidate.MemoryGB)
        $memScore = 1 - ([math]::Abs($Target.MemoryGB - $Candidate.MemoryGB) / $maxMem)
        $score += [math]::Round($memScore * 20)
    }

    # Family match (18 points) — exact = 18, same category = 13, same first letter = 9
    if ($Target.Family -eq $Candidate.Family) {
        $score += 18
    }
    else {
        $targetInfo = if ($FamilyInfo) { $FamilyInfo[$Target.Family] } else { $null }
        $candidateInfo = if ($FamilyInfo) { $FamilyInfo[$Candidate.Family] } else { $null }
        $targetCat = if ($targetInfo) { $targetInfo.Category } else { 'Unknown' }
        $candidateCat = if ($candidateInfo) { $candidateInfo.Category } else { 'Unknown' }
        if ($targetCat -ne 'Unknown' -and $targetCat -eq $candidateCat) {
            $score += 13
        }
        elseif ($Target.Family.Length -gt 0 -and $Candidate.Family.Length -gt 0 -and
            $Target.Family[0] -eq $Candidate.Family[0]) {
            $score += 9
        }
    }

    # Family version newness (12 points) — strongly rewards latest SKU generations
    $targetVer = if ($Target.FamilyVersion) { [int]$Target.FamilyVersion } else { 1 }
    $candidateVer = if ($Candidate.FamilyVersion) { [int]$Candidate.FamilyVersion } else { 1 }

    if ($Target.Family -eq $Candidate.Family) {
        if ($candidateVer -gt $targetVer) {
            # Upgrade: base 8 + bonus for how new the candidate is
            $verBonus = switch ($candidateVer) {
                { $_ -ge 7 } { 4; break }
                { $_ -ge 6 } { 3; break }
                { $_ -ge 5 } { 2; break }
                default      { 1 }
            }
            $score += [math]::Min(8 + $verBonus, 12)
        }
        elseif ($candidateVer -eq $targetVer) {
            $score += 5
        }
        else {
            $score += 1
        }
    }
    else {
        # Cross-family: graduated by candidate version
        $score += switch ($candidateVer) {
            { $_ -ge 7 } { 10; break }
            { $_ -ge 6 } { 9; break }
            { $_ -ge 5 } { 7; break }
            { $_ -ge 4 } { 5; break }
            { $_ -ge 3 } { 3; break }
            { $_ -ge 2 } { 1; break }
            default      { 0 }
        }
    }

    # Architecture match (10 points)
    if ($Target.Architecture -eq $Candidate.Architecture) {
        $score += 10
    }

    # Premium IO match (5 points) — if target needs premium, candidate must have it
    if ($Target.PremiumIO -eq $true -and $Candidate.PremiumIO -eq $true) {
        $score += 5
    }
    elseif ($Target.PremiumIO -ne $true) {
        $score += 5
    }

    # Disk IOPS closeness (8 points) — uncached disk IO throughput
    if ($Target.UncachedDiskIOPS -gt 0 -and $Candidate.UncachedDiskIOPS -gt 0) {
        $maxIOPS = [math]::Max($Target.UncachedDiskIOPS, $Candidate.UncachedDiskIOPS)
        $iopsScore = 1 - ([math]::Abs($Target.UncachedDiskIOPS - $Candidate.UncachedDiskIOPS) / $maxIOPS)
        $score += [math]::Round($iopsScore * 8)
    }
    elseif ($Target.UncachedDiskIOPS -le 0) {
        $score += 8
    }

    # Data disk count closeness (7 points)
    if ($Target.MaxDataDiskCount -gt 0 -and $Candidate.MaxDataDiskCount -gt 0) {
        $maxDisks = [math]::Max($Target.MaxDataDiskCount, $Candidate.MaxDataDiskCount)
        $diskScore = 1 - ([math]::Abs($Target.MaxDataDiskCount - $Candidate.MaxDataDiskCount) / $maxDisks)
        $score += [math]::Round($diskScore * 7)
    }
    elseif ($Target.MaxDataDiskCount -le 0) {
        $score += 7
    }

    return [math]::Min($score, 100)
}