Scripts/Compare-FIDOKeyFiles.ps1

<#
.SYNOPSIS
    Compares two FIDO key JSON files and shows what changed between them.

.DESCRIPTION
    Takes two JSON files (old and current) and compares the FIDO keys to identify
    added, removed, and modified AAGUIDs. Can output in console format or as a
    professional changelog suitable for documentation.

.PARAMETER OldFile
    Path to the older JSON file to compare from.

.PARAMETER CurrentFile
    Path to the current/newer JSON file to compare to.
    Default is 'Assets/FidoKeys.json'.

.PARAMETER ShowDetails
    Display detailed change information for each AAGUID.

.PARAMETER AsChangeLog
    Format output as a professional changelog suitable for documentation.
    Automatically enables detailed output.

.PARAMETER OutputFile
    Save changelog output to a file. Only valid with -AsChangeLog.

.EXAMPLE
    Compare-FIDOKeyFiles -OldFile "backup/FidoKeys-2026-01-01.json"

.EXAMPLE
    Compare-FIDOKeyFiles -OldFile "backup/FidoKeys-2026-01-01.json" -ShowDetails

.EXAMPLE
    Compare-FIDOKeyFiles -OldFile "backup/FidoKeys-2026-01-01.json" -AsChangeLog -OutputFile "CHANGELOG.md"

.EXAMPLE
    # Compare with a specific backup
    Compare-FIDOKeyFiles -OldFile "Assets/FidoKeys.json.bak" -CurrentFile "Assets/FidoKeys.json"

.NOTES
    Author: GitHub Copilot
    Date: 2026-05-24
#>


Function Compare-FIDOKeyFiles {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({Test-Path $_})]
        [string]$OldFile,
        
        [Parameter()]
        [ValidateScript({Test-Path $_})]
        [string]$CurrentFile = "Assets/FidoKeys.json",
        
        [Parameter()]
        [switch]$ShowDetails,
        
        [Parameter()]
        [switch]$AsChangeLog,
        
        [Parameter()]
        [string]$OutputFile
    )

    $ErrorActionPreference = 'Stop'
    
    # Load JSON files
    Write-Host "`n📂 Loading JSON files..." -ForegroundColor Cyan
    
    try {
        $oldData = Get-Content -Raw -Path $OldFile | ConvertFrom-Json
        $currentData = Get-Content -Raw -Path $CurrentFile | ConvertFrom-Json
        
        $oldEntries = $oldData.keys
        $currentEntries = $currentData.keys
        
        Write-Host " Old file: $(Split-Path -Leaf $OldFile) - $($oldEntries.Count) AAGUIDs" -ForegroundColor Gray
        Write-Host " Current file: $(Split-Path -Leaf $CurrentFile) - $($currentEntries.Count) AAGUIDs" -ForegroundColor Gray
        
        # Get timestamps from metadata if available
        $oldDate = if ($oldData.metadata.databaseLastUpdated) {
            [DateTime]::Parse($oldData.metadata.databaseLastUpdated)
        } else {
            (Get-Item $OldFile).LastWriteTime
        }
        
        $currentDate = if ($currentData.metadata.databaseLastUpdated) {
            [DateTime]::Parse($currentData.metadata.databaseLastUpdated)
        } else {
            (Get-Item $CurrentFile).LastWriteTime
        }
        
    }
    catch {
        Write-Error "Failed to load JSON files: $_"
        return
    }
    
    # Index entries by AAGUID
    $oldByAAGUID = @{}
    foreach ($entry in $oldEntries) {
        $oldByAAGUID[$entry.AAGUID] = $entry
    }
    
    $currentByAAGUID = @{}
    foreach ($entry in $currentEntries) {
        $currentByAAGUID[$entry.AAGUID] = $entry
    }
    
    # Find differences
    Write-Host "🔍 Analyzing differences...`n" -ForegroundColor Cyan
    
    # Find added AAGUIDs
    $added = $currentEntries | Where-Object { $_.AAGUID -notin $oldByAAGUID.Keys }
    
    # Find removed AAGUIDs
    $removed = $oldEntries | Where-Object { $_.AAGUID -notin $currentByAAGUID.Keys }
    
    # Find modified AAGUIDs (present in both but with different properties)
    $modified = @()
    foreach ($entry in $currentEntries) {
        if ($oldByAAGUID.ContainsKey($entry.AAGUID)) {
            $oldEntry = $oldByAAGUID[$entry.AAGUID]
            $changes = @()
            foreach ($prop in @('Description', 'Vendor', 'Bio', 'USB', 'NFC', 'BLE', 'ValidVendor')) {
                if ($entry.$prop -ne $oldEntry.$prop) {
                    $changes += "$prop`: '$($oldEntry.$prop)' → '$($entry.$prop)'"
                }
            }
            if ($changes.Count -gt 0) {
                $modified += [PSCustomObject]@{
                    AAGUID = $entry.AAGUID
                    Description = $entry.Description
                    Vendor = $entry.Vendor
                    Changes = $changes
                }
            }
        }
    }
    
    $totalChanges = $added.Count + $removed.Count + $modified.Count
    
    # Prepare output
    $outputLines = New-Object System.Collections.ArrayList
    
    if ($AsChangeLog) {
        # Professional changelog format matching CHANGELOG.md pattern
        
        # Helper function to convert Yes/No to emoji
        function ConvertTo-Emoji {
            param($value)
            if ($value -eq 'Yes' -or $value -eq '✅') { return '✅' }
            else { return '❌' }
        }
        
        $outputLines.Add("# Changelog") | Out-Null
        $outputLines.Add("") | Out-Null
        $outputLines.Add("## Comparison Results") | Out-Null
        $outputLines.Add("") | Out-Null
        
        if ($totalChanges -eq 0) {
            $outputLines.Add("No changes detected between the two files.") | Out-Null
            $outputLines.Add("") | Out-Null
            $outputLines.Add("**Files Compared:**") | Out-Null
            $outputLines.Add("- Old: $(Split-Path -Leaf $OldFile) ($($oldDate.ToString('yyyy-MM-dd')))") | Out-Null
            $outputLines.Add("- Current: $(Split-Path -Leaf $CurrentFile) ($($currentDate.ToString('yyyy-MM-dd')))") | Out-Null
            $outputLines.Add("") | Out-Null
        } else {
            # Build summary description
            $summaryParts = @()
            if ($added.Count -gt 0) { $summaryParts += "$($added.Count) new authenticator$($added.Count -gt 1 ? 's' : '') added" }
            if ($modified.Count -gt 0) { $summaryParts += "$($modified.Count) authenticator$($modified.Count -gt 1 ? 's' : '') updated" }
            if ($removed.Count -gt 0) { $summaryParts += "$($removed.Count) authenticator$($removed.Count -gt 1 ? 's' : '') removed" }
            
            $outputLines.Add(($summaryParts -join ', ') + " between $(Split-Path -Leaf $OldFile) and $(Split-Path -Leaf $CurrentFile)!") | Out-Null
            $outputLines.Add("") | Out-Null
            
            # Summary bullets
            if ($added.Count -gt 0) {
                $outputLines.Add("- **$($added.Count) new authenticator$($added.Count -gt 1 ? 's have' : ' has')** been added to the supported vendors list") | Out-Null
            }
            if ($modified.Count -gt 0) {
                $outputLines.Add("- **$($modified.Count) authenticator$($modified.Count -gt 1 ? 's have' : ' has')** been updated with new capability information") | Out-Null
            }
            if ($removed.Count -gt 0) {
                $outputLines.Add("- **$($removed.Count) authenticator$($removed.Count -gt 1 ? 's have' : ' has')** been removed from the supported vendors list") | Out-Null
            }
            $outputLines.Add("") | Out-Null
            
            # Added authenticators
            if ($added.Count -gt 0) {
                $outputLines.Add("## ✅ New Authenticators ($($added.Count))") | Out-Null
                $outputLines.Add("") | Out-Null
                $outputLines.Add("The following authenticators are now supported:") | Out-Null
                $outputLines.Add("") | Out-Null
                
                foreach ($item in $added) {
                    $outputLines.Add("### $($item.Description)") | Out-Null
                    $outputLines.Add("") | Out-Null
                    $outputLines.Add("**AAGUID:** ``$($item.AAGUID)``") | Out-Null
                    $outputLines.Add("") | Out-Null
                    $outputLines.Add("**Supported Interfaces:**") | Out-Null
                    $outputLines.Add("") | Out-Null
                    $outputLines.Add("| Interface | Supported |") | Out-Null
                    $outputLines.Add("|-----------|-----------|") | Out-Null
                    $outputLines.Add("| Biometric | $(ConvertTo-Emoji $item.Bio) |") | Out-Null
                    $outputLines.Add("| USB | $(ConvertTo-Emoji $item.USB) |") | Out-Null
                    $outputLines.Add("| NFC | $(ConvertTo-Emoji $item.NFC) |") | Out-Null
                    $outputLines.Add("| BLE | $(ConvertTo-Emoji $item.BLE) |") | Out-Null
                    $outputLines.Add("") | Out-Null
                }
            }
            
            # Modified authenticators
            if ($modified.Count -gt 0) {
                $outputLines.Add("## ⚠️ Updated Authenticators ($($modified.Count))") | Out-Null
                $outputLines.Add("") | Out-Null
                $outputLines.Add("The following authenticators have been updated with new capability information:") | Out-Null
                $outputLines.Add("") | Out-Null
                
                foreach ($item in $modified) {
                    $outputLines.Add("### $($item.Description)") | Out-Null
                    $outputLines.Add("") | Out-Null
                    $outputLines.Add("**AAGUID:** ``$($item.AAGUID)``") | Out-Null
                    $outputLines.Add("") | Out-Null
                    $outputLines.Add("**Changes:**") | Out-Null
                    $outputLines.Add("") | Out-Null
                    
                    # Parse changes and format with emojis
                    foreach ($change in $item.Changes) {
                        # Extract property and values (format: "Property: 'oldval' → 'newval'")
                        if ($change -match "^([^:]+):\s*'([^']*)'[^']*'([^']*)'") {
                            $prop = $matches[1]
                            $oldVal = $matches[2]
                            $newVal = $matches[3]
                            
                            # Convert values to emojis if they're Bio/USB/NFC/BLE properties
                            if ($prop -in @('Bio', 'USB', 'NFC', 'BLE')) {
                                $oldEmoji = ConvertTo-Emoji $oldVal
                                $newEmoji = ConvertTo-Emoji $newVal
                                $outputLines.Add("- $prop`: $oldEmoji → $newEmoji") | Out-Null
                            } else {
                                # For other properties (Description, Vendor, ValidVendor), show as-is
                                $outputLines.Add("- $change") | Out-Null
                            }
                        } else {
                            $outputLines.Add("- $change") | Out-Null
                        }
                    }
                    $outputLines.Add("") | Out-Null
                }
            }
            
            # Removed authenticators
            if ($removed.Count -gt 0) {
                $outputLines.Add("## ❌ Removed Authenticators ($($removed.Count))") | Out-Null
                $outputLines.Add("") | Out-Null
                $outputLines.Add("The following authenticators have been removed:") | Out-Null
                $outputLines.Add("") | Out-Null
                
                foreach ($item in $removed) {
                    $outputLines.Add("### $($item.Description)") | Out-Null
                    $outputLines.Add("") | Out-Null
                    $outputLines.Add("**AAGUID:** ``$($item.AAGUID)``") | Out-Null
                    $outputLines.Add("") | Out-Null
                }
            }
        }
        
        # Output to file or console
        if ($OutputFile) {
            $outputLines | Out-File -FilePath $OutputFile -Encoding utf8
            Write-Host "✅ Changelog saved to: $OutputFile" -ForegroundColor Green
        } else {
            $outputLines | ForEach-Object { Write-Output $_ }
        }
        
    } else {
        # Console format with colors
        Write-Host ("=" * 100) -ForegroundColor Cyan
        Write-Host "📊 COMPARISON RESULTS" -ForegroundColor Cyan
        Write-Host ("=" * 100) -ForegroundColor Cyan
        Write-Host ""
        
        if ($totalChanges -eq 0) {
            Write-Host "✅ No changes detected between the two files." -ForegroundColor Green
        } else {
            Write-Host "📈 Summary:" -ForegroundColor Magenta
            Write-Host " • Added: $($added.Count)" -ForegroundColor Green
            Write-Host " • Removed: $($removed.Count)" -ForegroundColor Red
            Write-Host " • Modified: $($modified.Count)" -ForegroundColor Yellow
            Write-Host " • Total: $totalChanges changes" -ForegroundColor White
            
            if ($ShowDetails -or $AsChangeLog) {
                if ($added.Count -gt 0) {
                    Write-Host "`n➕ ADDED ($($added.Count)):" -ForegroundColor Green
                    foreach ($item in $added) {
                        Write-Host " • $($item.Description)" -ForegroundColor White
                        Write-Host " AAGUID: $($item.AAGUID)" -ForegroundColor Gray
                        Write-Host " Vendor: $($item.Vendor)" -ForegroundColor Gray
                        Write-Host " Bio: $($item.Bio) | USB: $($item.USB) | NFC: $($item.NFC) | BLE: $($item.BLE)" -ForegroundColor Gray
                    }
                }
                
                if ($removed.Count -gt 0) {
                    Write-Host "`n➖ REMOVED ($($removed.Count)):" -ForegroundColor Red
                    foreach ($item in $removed) {
                        Write-Host " • $($item.Description)" -ForegroundColor White
                        Write-Host " AAGUID: $($item.AAGUID)" -ForegroundColor Gray
                        Write-Host " Vendor: $($item.Vendor)" -ForegroundColor Gray
                    }
                }
                
                if ($modified.Count -gt 0) {
                    Write-Host "`n🔄 MODIFIED ($($modified.Count)):" -ForegroundColor Yellow
                    foreach ($item in $modified) {
                        Write-Host " • $($item.Description)" -ForegroundColor White
                        Write-Host " AAGUID: $($item.AAGUID)" -ForegroundColor Gray
                        Write-Host " Vendor: $($item.Vendor)" -ForegroundColor Gray
                        foreach ($change in $item.Changes) {
                            Write-Host " - $change" -ForegroundColor Cyan
                        }
                    }
                }
            }
        }
        
        Write-Host "`n" + ("=" * 100) -ForegroundColor Cyan
        Write-Host "📈 STATISTICS" -ForegroundColor Cyan
        Write-Host ("=" * 100) -ForegroundColor Cyan
        Write-Host "Old file AAGUIDs: $($oldEntries.Count)" -ForegroundColor White
        Write-Host "Current file AAGUIDs: $($currentEntries.Count)" -ForegroundColor White
        Write-Host "Net change: $($currentEntries.Count - $oldEntries.Count)" -ForegroundColor White
        Write-Host "Old file date: $($oldDate.ToString('yyyy-MM-dd HH:mm:ss'))" -ForegroundColor White
        Write-Host "Current file date: $($currentDate.ToString('yyyy-MM-dd HH:mm:ss'))" -ForegroundColor White
        Write-Host ""
    }
}