Scripts/Sync-FidoKeysWithDocs.ps1

<#
.SYNOPSIS
Synchronizes local FidoKeys.json with the latest Microsoft Entra FIDO vendor table.

.DESCRIPTION
Fetches the live vendor table from Microsoft Entra docs, compares against local FidoKeys.json,
displays differences in terminal, and prompts user to approve/reject changes. Generates markdown
report of all changes and optionally updates FidoKeys.json with approved modifications.

.PARAMETER Owner
GitHub repository owner (default: MicrosoftDocs)

.PARAMETER Repo
GitHub repository name (default: entra-docs)

.PARAMETER Ref
Branch or commit SHA to fetch from (default: main)

.PARAMETER BaseRef
Older branch or commit SHA for doc-to-doc comparison (e.g., compare Dec 8 to main).
When provided, compares old doc vs new doc instead of new doc vs local FidoKeys.json

.PARAMETER FilePath
Path to vendor table in repo (default: docs/identity/authentication/concept-fido2-hardware-vendor.md)

.PARAMETER LocalPath
Path to local FidoKeys.json (default: Assets/FidoKeys.json)

.PARAMETER AutoApprove
Automatically approve all changes without prompting

.PARAMETER PreviewOnly
Show changes without prompting or updating

.PARAMETER OutputMarkdown
Custom output markdown path (default: auto-timestamped)

.EXAMPLE
./Sync-FidoKeysWithDocs.ps1
Shows differences interactively, prompts for each change

.EXAMPLE
./Sync-FidoKeysWithDocs.ps1 -PreviewOnly
Shows differences only; no prompts and no updates
#>


[CmdletBinding()]
param(
    [string]$Owner = 'MicrosoftDocs',
    [string]$Repo = 'entra-docs',
    [string]$Ref = 'main',
    [string]$BaseRef = '',
    [string]$FilePath = 'docs/identity/authentication/concept-fido2-hardware-vendor.md',
    [string]$LocalPath = 'Assets/FidoKeys.json',
    [switch]$AutoApprove,
    [switch]$PreviewOnly,
    [string]$OutputMarkdown = "FIDO_SYNC_$(Get-Date -Format 'yyyyMMdd_HHmmss').md"
)

function Get-DocFile {
    param(
        [string]$Owner,
        [string]$Repo,
        [string]$Ref,
        [string]$Path
    )
    $url = "https://raw.githubusercontent.com/$Owner/$Repo/$Ref/$Path"
    try {
        $resp = Invoke-WebRequest -Uri $url -UseBasicParsing -ErrorAction Stop
        return $resp.Content
    } catch {
        throw "Failed to fetch doc file from ${url}: $_"
    }
}

function ConvertFrom-DocTable {
    param([string]$content)
    $entries = @()
    $lines = $content -split "`r`n|`n|`r"
    $headerSeen = $false
    $lineIndex = 0
    $emitLimit = 5
    foreach ($line in $lines) {
        $lineIndex++
        if ([string]::IsNullOrWhiteSpace($line)) { continue }
        $cleanLine = ($line -replace '^\|\s*', '') -replace '\s*\|$', ''
        if (-not $headerSeen -and $cleanLine -match '^\s*Description\s*\|\s*AAGUID\s*\|\s*Bio\s*\|\s*USB\s*\|\s*NFC\s*\|\s*BLE\s*$') {
            $headerSeen = $true
            Write-Verbose "Header detected at line $lineIndex"
            continue
        }
        if ($headerSeen -and ($cleanLine -match '^##' -or $cleanLine -match '^Related content' -or $cleanLine -match '^For more')) {
            Write-Verbose "End of table detected at line $lineIndex"
            break
        }
        if ($headerSeen -and ($cleanLine -split '\|').Count -ge 6) {
            $parts = $cleanLine -split '\|' | ForEach-Object { $_.Trim() }
            if ($parts[0] -match '^-+$') { continue }
            $aaguid = $parts[1]
            if ($aaguid -notmatch '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$') { continue }
            $mapToken = {
                param([string]$t)
                if ($t -match '✅|&#x2705;|&#x2714;|&#10004;|✓|yes|true') { return '✅' }
                if ($t -match '❌|&#10060;|✗|no|false') { return '❌' }
                return '❌'
            }
            $entry = @{
                Description = $parts[0]
                AAGUID = $aaguid.ToLower()
                Bio = & $mapToken $parts[2]
                USB = & $mapToken $parts[3]
                NFC = & $mapToken $parts[4]
                BLE = & $mapToken $parts[5]
            }
            if ($entries.Count -lt $emitLimit) {
                Write-Verbose ("Parsed entry #{0}: {1} [{2}] Bio={3} USB={4} NFC={5} BLE={6}" -f ($entries.Count+1), $entry.Description, $entry.AAGUID, $entry.Bio, $entry.USB, $entry.NFC, $entry.BLE)
            }
            $entries += $entry
        }
    }
    Write-Verbose "Parsed $($entries.Count) entries from doc content"
    if ($entries.Count -eq 0) { Write-Verbose "No rows parsed; header may not have been detected" }
    return $entries
}

function Get-LocalDatabase {
    <# Get and parse local FidoKeys.json #>
    param([string]$Path)
    if (-not (Test-Path $Path)) {
        throw "Local FidoKeys.json not found at: $Path"
    }
    
    try {
        $json = Get-Content -Path $Path -Raw | ConvertFrom-Json
        return $json.keys
    } catch {
        throw "Failed to parse FidoKeys.json: $_"
    }
}

function Compare-Databases {
    <# Compare doc entries against local data by AAGUID #>
    param($docEntries, $localEntries)
    
    $localMap = @{}
    foreach ($entry in $localEntries) {
        if ($entry.AAGUID) {
            $localMap[$entry.AAGUID.ToLower()] = $entry
        }
    }
    
    $added = @()
    $modified = @()
    $removed = @()
    
    # Find added and modified
    foreach ($doc in $docEntries) {
        if ($localMap.ContainsKey($doc.AAGUID)) {
            $local = $localMap[$doc.AAGUID]
            if ($doc.Bio -ne $local.Bio -or $doc.USB -ne $local.USB -or $doc.NFC -ne $local.NFC -or $doc.BLE -ne $local.BLE) {
                $modified += @{
                    AAGUID = $doc.AAGUID
                    Description = $doc.Description
                    OldData = $local
                    NewData = $doc
                }
            }
        } else {
            $added += $doc
        }
    }
    
    # Find removed
    $docAaguidSet = @{}
    foreach ($doc in $docEntries) {
        $docAaguidSet[$doc.AAGUID] = $true
    }
    foreach ($local in $localEntries) {
        if ($local.AAGUID -and -not $docAaguidSet.ContainsKey($local.AAGUID.ToLower())) {
            $removed += $local
        }
    }
    
    return @{
        Added = $added
        Modified = $modified
        Removed = $removed
    }
}

function Show-Changes {
    <# Display changes in terminal with colors #>
    param($comparison)
    
    Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
    Write-Host "FIDO KEYS SYNC SUMMARY" -ForegroundColor Cyan
    Write-Host ("=" * 80) -ForegroundColor Cyan
    
    Write-Host "`n✅ ADDED ($($comparison.Added.Count)):" -ForegroundColor Green
    if ($comparison.Added.Count -eq 0) {
        Write-Host " (none)" -ForegroundColor DarkGray
    } else {
        foreach ($item in $comparison.Added) {
            Write-Host " • $($item.Description) [$($item.AAGUID)]" -ForegroundColor Green
            Write-Host " Bio: $($item.Bio) USB: $($item.USB) NFC: $($item.NFC) BLE: $($item.BLE)" -ForegroundColor DarkGreen
        }
    }
    
    Write-Host "`n⚠️ MODIFIED ($($comparison.Modified.Count)):" -ForegroundColor Yellow
    if ($comparison.Modified.Count -eq 0) {
        Write-Host " (none)" -ForegroundColor DarkGray
    } else {
        foreach ($item in $comparison.Modified) {
            Write-Host " • $($item.Description) [$($item.AAGUID)]" -ForegroundColor Yellow
            $changes = @()
            if ($item.OldData.Bio -ne $item.NewData.Bio) { $changes += "Bio: $($item.OldData.Bio) → $($item.NewData.Bio)" }
            if ($item.OldData.USB -ne $item.NewData.USB) { $changes += "USB: $($item.OldData.USB) → $($item.NewData.USB)" }
            if ($item.OldData.NFC -ne $item.NewData.NFC) { $changes += "NFC: $($item.OldData.NFC) → $($item.NewData.NFC)" }
            if ($item.OldData.BLE -ne $item.NewData.BLE) { $changes += "BLE: $($item.OldData.BLE) → $($item.NewData.BLE)" }
            foreach ($change in $changes) {
                Write-Host " $change" -ForegroundColor DarkYellow
            }
        }
    }
    
    Write-Host "`n❌ REMOVED ($($comparison.Removed.Count)):" -ForegroundColor Red
    if ($comparison.Removed.Count -eq 0) {
        Write-Host " (none)" -ForegroundColor DarkGray
    } else {
        foreach ($item in $comparison.Removed) {
            Write-Host " • $($item.Description) [$($item.AAGUID)]" -ForegroundColor Red
            Write-Host " Bio: $($item.Bio) USB: $($item.USB) NFC: $($item.NFC) BLE: $($item.BLE)" -ForegroundColor DarkRed
        }
    }
    
    Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
}

function Get-UserApproval {
    <# Interactive prompt for each change category #>
    param($comparison)
    
    $approvedChanges = @{
        AddedApproved = [System.Collections.ArrayList]@()
        ModifiedApproved = [System.Collections.ArrayList]@()
        RemovedApproved = [System.Collections.ArrayList]@()
        AddedRejected = [System.Collections.ArrayList]@()
        ModifiedRejected = [System.Collections.ArrayList]@()
        RemovedRejected = [System.Collections.ArrayList]@()
    }
    
    # Added
    if ($comparison.Added.Count -gt 0) {
        Write-Host "`n📌 ADDING $($comparison.Added.Count) NEW KEY(S):" -ForegroundColor Green
        foreach ($item in $comparison.Added) {
            $prompt = Read-Host " Add '$($item.Description)'? (y/n/skip)"
            if ($prompt -eq 'y') {
                [void]$approvedChanges.AddedApproved.Add($item)
            } else {
                [void]$approvedChanges.AddedRejected.Add($item)
            }
        }
    }
    
    # Modified
    if ($comparison.Modified.Count -gt 0) {
        Write-Host "`n🔄 MODIFYING $($comparison.Modified.Count) KEY(S):" -ForegroundColor Yellow
        foreach ($item in $comparison.Modified) {
            Write-Host " '$($item.Description)' [$($item.AAGUID)]" -ForegroundColor Yellow
            $changes = @()
            if ($item.OldData.Bio -ne $item.NewData.Bio) { $changes += "Bio: $($item.OldData.Bio) → $($item.NewData.Bio)" }
            if ($item.OldData.USB -ne $item.NewData.USB) { $changes += "USB: $($item.OldData.USB) → $($item.NewData.USB)" }
            if ($item.OldData.NFC -ne $item.NewData.NFC) { $changes += "NFC: $($item.OldData.NFC) → $($item.NewData.NFC)" }
            if ($item.OldData.BLE -ne $item.NewData.BLE) { $changes += "BLE: $($item.OldData.BLE) → $($item.NewData.BLE)" }
            foreach ($change in $changes) {
                Write-Host " $change" -ForegroundColor DarkYellow
            }
            $prompt = Read-Host " Apply changes? (y/n/skip)"
            if ($prompt -eq 'y') {
                [void]$approvedChanges.ModifiedApproved.Add($item)
            } else {
                [void]$approvedChanges.ModifiedRejected.Add($item)
            }
        }
    }
    
    # Removed
    if ($comparison.Removed.Count -gt 0) {
        Write-Host "`n🗑️ REMOVING $($comparison.Removed.Count) KEY(S):" -ForegroundColor Red
        foreach ($item in $comparison.Removed) {
            $prompt = Read-Host " Remove '$($item.Description)'? (y/n/skip)"
            if ($prompt -eq 'y') {
                [void]$approvedChanges.RemovedApproved.Add($item)
            } else {
                [void]$approvedChanges.RemovedRejected.Add($item)
            }
        }
    }
    
    return $approvedChanges
}

function Write-SyncMarkdown {
    <# Generate markdown report #>
    param($comparison, $approvedChanges, [string]$output)
    
    $sb = [System.Text.StringBuilder]::new()
    [void]$sb.AppendLine("# FIDO Keys Sync Report")
    [void]$sb.AppendLine()
    [void]$sb.AppendLine("Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
    [void]$sb.AppendLine()
    
    # Summary
    [void]$sb.AppendLine("## Summary")
    [void]$sb.AppendLine()
    [void]$sb.AppendLine("| Category | Found | Approved | Rejected |")
    [void]$sb.AppendLine("|----------|-------|----------|----------|")
    [void]$sb.AppendLine("| Added | $($comparison.Added.Count) | $($approvedChanges.AddedApproved.Count) | $($approvedChanges.AddedRejected.Count) |")
    [void]$sb.AppendLine("| Modified | $($comparison.Modified.Count) | $($approvedChanges.ModifiedApproved.Count) | $($approvedChanges.ModifiedRejected.Count) |")
    [void]$sb.AppendLine("| Removed | $($comparison.Removed.Count) | $($approvedChanges.RemovedApproved.Count) | $($approvedChanges.RemovedRejected.Count) |")
    [void]$sb.AppendLine()
    
    # Added
    [void]$sb.AppendLine("## ✅ Added Keys ($($comparison.Added.Count))")
    [void]$sb.AppendLine()
    if ($comparison.Added.Count -eq 0) {
        [void]$sb.AppendLine("No keys were added.")
    } else {
        [void]$sb.AppendLine("| Status | Description | AAGUID | Bio | USB | NFC | BLE |")
        [void]$sb.AppendLine("|--------|-------------|--------|-----|-----|-----|-----|")
        foreach ($item in $comparison.Added) {
            $status = if ($approvedChanges.AddedApproved -contains $item) { "✅" } else { "❌" }
            [void]$sb.AppendLine("| $status | $($item.Description) | $($item.AAGUID) | $($item.Bio) | $($item.USB) | $($item.NFC) | $($item.BLE) |")
        }
    }
    [void]$sb.AppendLine()
    
    # Modified
    [void]$sb.AppendLine("## ⚠️ Modified Keys ($($comparison.Modified.Count))")
    [void]$sb.AppendLine()
    if ($comparison.Modified.Count -eq 0) {
        [void]$sb.AppendLine("No keys were modified.")
    } else {
        foreach ($item in ($comparison.Modified | Sort-Object AAGUID)) {
            $status = if ($approvedChanges.ModifiedApproved -contains $item) { "✅" } else { "❌" }
            [void]$sb.AppendLine("### $status $($item.Description)")
            [void]$sb.AppendLine("- **AAGUID:** $($item.AAGUID)")
            if ($item.OldData.Bio -ne $item.NewData.Bio) {
                [void]$sb.AppendLine("- **Bio:** $($item.OldData.Bio) → $($item.NewData.Bio)")
            }
            if ($item.OldData.USB -ne $item.NewData.USB) {
                [void]$sb.AppendLine("- **USB:** $($item.OldData.USB) → $($item.NewData.USB)")
            }
            if ($item.OldData.NFC -ne $item.NewData.NFC) {
                [void]$sb.AppendLine("- **NFC:** $($item.OldData.NFC) → $($item.NewData.NFC)")
            }
            if ($item.OldData.BLE -ne $item.NewData.BLE) {
                [void]$sb.AppendLine("- **BLE:** $($item.OldData.BLE) → $($item.NewData.BLE)")
            }
            [void]$sb.AppendLine()
        }
    }
    
    # Removed
    [void]$sb.AppendLine("## ❌ Removed Keys ($($comparison.Removed.Count))")
    [void]$sb.AppendLine()
    if ($comparison.Removed.Count -eq 0) {
        [void]$sb.AppendLine("No keys were removed.")
    } else {
        [void]$sb.AppendLine("| Status | Description | AAGUID | Bio | USB | NFC | BLE |")
        [void]$sb.AppendLine("|--------|-------------|--------|-----|-----|-----|-----|")
        foreach ($item in $comparison.Removed) {
            $status = if ($approvedChanges.RemovedApproved -contains $item) { "✅" } else { "❌" }
            [void]$sb.AppendLine("| $status | $($item.Description) | $($item.AAGUID) | $($item.Bio) | $($item.USB) | $($item.NFC) | $($item.BLE) |")
        }
    }
    [void]$sb.AppendLine()
    
    Set-Content -Path $output -Value $sb.ToString() -Encoding UTF8
}

function Write-PublicDocComparisonMarkdown {
    <# Generate a professional public-facing markdown report for doc-to-doc comparisons #>
    param($comparison, [string]$baseRef, [string]$newRef, [string]$output)
    
    $sb = [System.Text.StringBuilder]::new()
    
    # Professional header
    [void]$sb.AppendLine("# FIDO2 Hardware Vendor Table Update Report")
    [void]$sb.AppendLine()
    [void]$sb.AppendLine("**Report Generated:** $(Get-Date -Format 'MMMM d, yyyy HH:mm:ss UTC')")
    [void]$sb.AppendLine()
    [void]$sb.AppendLine("## Comparison Details")
    [void]$sb.AppendLine()
    [void]$sb.AppendLine("| Metric | Value |")
    [void]$sb.AppendLine("|--------|-------|")
    [void]$sb.AppendLine("| Base Version | $baseRef |")
    [void]$sb.AppendLine("| New Version | $newRef |")
    [void]$sb.AppendLine("| New Entries | $($comparison.Added.Count) |")
    [void]$sb.AppendLine("| Updated Entries | $($comparison.Modified.Count) |")
    [void]$sb.AppendLine("| Removed Entries | $($comparison.Removed.Count) |")
    [void]$sb.AppendLine()
    
    # Executive Summary
    [void]$sb.AppendLine("## Summary")
    [void]$sb.AppendLine()
    if ($comparison.Added.Count -eq 0 -and $comparison.Modified.Count -eq 0 -and $comparison.Removed.Count -eq 0) {
        [void]$sb.AppendLine("No changes detected between versions.")
    } else {
        [void]$sb.AppendLine("The Microsoft Entra FIDO2 hardware vendor table has been updated with the following changes:")
        [void]$sb.AppendLine()
        if ($comparison.Added.Count -gt 0) {
            [void]$sb.AppendLine("- **$($comparison.Added.Count) new authenticators** have been added to the supported vendors list")
        }
        if ($comparison.Modified.Count -gt 0) {
            [void]$sb.AppendLine("- **$($comparison.Modified.Count) authenticators** have been updated with new capability information")
        }
        if ($comparison.Removed.Count -gt 0) {
            [void]$sb.AppendLine("- **$($comparison.Removed.Count) authenticators** have been removed from the supported vendors list")
        }
    }
    [void]$sb.AppendLine()
    
    # Added entries
    if ($comparison.Added.Count -gt 0) {
        [void]$sb.AppendLine("## ✅ New Authenticators ($($comparison.Added.Count))")
        [void]$sb.AppendLine()
        [void]$sb.AppendLine("The following authenticators are now supported:")
        [void]$sb.AppendLine()
        foreach ($item in ($comparison.Added | Sort-Object Description)) {
            [void]$sb.AppendLine("### $($item.Description)")
            [void]$sb.AppendLine()
            [void]$sb.AppendLine("**AAGUID:** ``$($item.AAGUID)``")
            [void]$sb.AppendLine()
            [void]$sb.AppendLine("**Supported Interfaces:**")
            [void]$sb.AppendLine()
            [void]$sb.AppendLine("| Interface | Supported |")
            [void]$sb.AppendLine("|-----------|-----------|")
            [void]$sb.AppendLine("| Biometric | $($item.Bio) |")
            [void]$sb.AppendLine("| USB | $($item.USB) |")
            [void]$sb.AppendLine("| NFC | $($item.NFC) |")
            [void]$sb.AppendLine("| BLE | $($item.BLE) |")
            [void]$sb.AppendLine()
        }
    }
    
    # Modified entries
    if ($comparison.Modified.Count -gt 0) {
        [void]$sb.AppendLine("## ⚠️ Updated Authenticators ($($comparison.Modified.Count))")
        [void]$sb.AppendLine()
        [void]$sb.AppendLine("The following authenticators have been updated with new capability information:")
        [void]$sb.AppendLine()
        foreach ($item in ($comparison.Modified | Sort-Object Description)) {
            [void]$sb.AppendLine("### $($item.Description)")
            [void]$sb.AppendLine()
            [void]$sb.AppendLine("**AAGUID:** ``$($item.AAGUID)``")
            [void]$sb.AppendLine()
            [void]$sb.AppendLine("**Changes:**")
            [void]$sb.AppendLine()
            $hasChanges = $false
            if ($item.OldData.Bio -ne $item.NewData.Bio) {
                [void]$sb.AppendLine("- Biometric: $($item.OldData.Bio) → $($item.NewData.Bio)")
                $hasChanges = $true
            }
            if ($item.OldData.USB -ne $item.NewData.USB) {
                [void]$sb.AppendLine("- USB: $($item.OldData.USB) → $($item.NewData.USB)")
                $hasChanges = $true
            }
            if ($item.OldData.NFC -ne $item.NewData.NFC) {
                [void]$sb.AppendLine("- NFC: $($item.OldData.NFC) → $($item.NewData.NFC)")
                $hasChanges = $true
            }
            if ($item.OldData.BLE -ne $item.NewData.BLE) {
                [void]$sb.AppendLine("- BLE: $($item.OldData.BLE) → $($item.NewData.BLE)")
                $hasChanges = $true
            }
            if (-not $hasChanges) {
                [void]$sb.AppendLine("No capability changes recorded.")
            }
            [void]$sb.AppendLine()
        }
    }
    
    # Removed entries (if any)
    if ($comparison.Removed.Count -gt 0) {
        [void]$sb.AppendLine("## ❌ Removed Authenticators ($($comparison.Removed.Count))")
        [void]$sb.AppendLine()
        [void]$sb.AppendLine("The following authenticators are no longer in the supported vendors list:")
        [void]$sb.AppendLine()
        foreach ($item in ($comparison.Removed | Sort-Object Description)) {
            [void]$sb.AppendLine("- **$($item.Description)** (``$($item.AAGUID)``)")
        }
        [void]$sb.AppendLine()
    }
    
    # Footer
    [void]$sb.AppendLine("---")
    [void]$sb.AppendLine()
    [void]$sb.AppendLine("*This report was automatically generated by comparing Microsoft Entra FIDO2 vendor table versions.*")
    
    Set-Content -Path $output -Value $sb.ToString() -Encoding UTF8
}

function Update-LocalDatabase {
    <# Apply approved changes to FidoKeys.json #>
    param($localData, $approvedChanges, [string]$path)
    
    # Backup original
    $backup = "$path.backup.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
    Copy-Item -Path $path -Destination $backup
    Write-Host "Backup created: $backup" -ForegroundColor Cyan
    
    # Load full JSON to preserve metadata
    $json = Get-Content -Path $path -Raw | ConvertFrom-Json
    
    # Build AAGUID map of local entries with indices
    $localMap = @{}
    $keysToDelete = [System.Collections.ArrayList]::new()
    
    for ($i = 0; $i -lt $json.keys.Count; $i++) {
        if ($json.keys[$i].AAGUID) {
            $localMap[$json.keys[$i].AAGUID.ToLower()] = $i
        }
    }
    
    # Mark removals (delete from highest index to lowest to preserve indices)
    foreach ($item in $approvedChanges.RemovedApproved) {
        $aaguidLower = $item.AAGUID.ToLower()
        if ($localMap.ContainsKey($aaguidLower)) {
            $idx = $localMap[$aaguidLower]
            [void]$keysToDelete.Add($idx)
        }
    }
    
    # Sort indices in descending order and remove
    $indicesToDelete = @($keysToDelete | Sort-Object -Descending)
    foreach ($idx in $indicesToDelete) {
        if ($idx -ge 0 -and $idx -lt $json.keys.Count) {
            $json.keys = @($json.keys[0..($idx-1)] + $json.keys[($idx+1)..($json.keys.Count-1)])
        }
    }
    
    # Rebuild map after deletions
    $localMap.Clear()
    for ($i = 0; $i -lt $json.keys.Count; $i++) {
        if ($json.keys[$i].AAGUID) {
            $localMap[$json.keys[$i].AAGUID.ToLower()] = $i
        }
    }
    
    # Apply modifications
    foreach ($item in $approvedChanges.ModifiedApproved) {
        $idx = $localMap[$item.AAGUID.ToLower()]
        if ($null -ne $idx) {
            $json.keys[$idx].Bio = $item.NewData.Bio
            $json.keys[$idx].USB = $item.NewData.USB
            $json.keys[$idx].NFC = $item.NewData.NFC
            $json.keys[$idx].BLE = $item.NewData.BLE
        }
    }
    
    # Apply additions
    foreach ($item in $approvedChanges.AddedApproved) {
        $newEntry = @{
            Vendor = "Unknown"
            Description = $item.Description
            AAGUID = $item.AAGUID
            Bio = $item.Bio
            USB = $item.USB
            NFC = $item.NFC
            BLE = $item.BLE
            Version = "Unknown"
            ValidVendor = "Unknown"
            metadataStatement = @{}
            statusReports = @()
            timeOfLastStatusChange = (Get-Date -Format 'yyyy-MM-dd')
        }
        $json.keys += @($newEntry)
    }
    
    # Update metadata
    $json.metadata.databaseLastUpdated = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    
    # Write updated JSON
    $json | ConvertTo-Json -Depth 10 | Set-Content -Path $path -Encoding UTF8
    Write-Host "FidoKeys.json updated successfully" -ForegroundColor Green
}

# ===== Main Execution =====

try {
    # Determine comparison mode
    $isDocToDocMode = -not [string]::IsNullOrWhiteSpace($BaseRef)
    
    if ($isDocToDocMode) {
        Write-Host "Comparing docs: $BaseRef vs $Ref" -ForegroundColor Cyan
        Write-Host "Fetching older doc from $BaseRef..." -ForegroundColor Cyan
        $baseDocContent = Get-DocFile -Owner $Owner -Repo $Repo -Ref $BaseRef -Path $FilePath
        Write-Host "Parsing older doc..." -ForegroundColor Cyan
        $baseDocEntries = ConvertFrom-DocTable -content $baseDocContent
        Write-Host "Found $($baseDocEntries.Count) entries in base doc" -ForegroundColor Green
        
        Write-Host "Fetching newer doc from $Ref..." -ForegroundColor Cyan
        $newDocContent = Get-DocFile -Owner $Owner -Repo $Repo -Ref $Ref -Path $FilePath
        Write-Host "Parsing newer doc..." -ForegroundColor Cyan
        $newDocEntries = ConvertFrom-DocTable -content $newDocContent
        Write-Host "Found $($newDocEntries.Count) entries in new doc" -ForegroundColor Green
        
        Write-Host "Comparing docs..." -ForegroundColor Cyan
        $comparison = Compare-Databases -docEntries $newDocEntries -localEntries $baseDocEntries
    } else {
        Write-Host "Fetching vendor table from Microsoft Entra docs (ref: $Ref)..." -ForegroundColor Cyan
        $docContent = Get-DocFile -Owner $Owner -Repo $Repo -Ref $Ref -Path $FilePath
        
        Write-Host "Parsing vendor table..." -ForegroundColor Cyan
        $docEntries = ConvertFrom-DocTable -content $docContent
        Write-Host "Found $($docEntries.Count) entries in docs" -ForegroundColor Green
        
        if ($docEntries.Count -eq 0) {
            Write-Host "WARNING: No entries found in docs. This may indicate a parsing issue." -ForegroundColor Yellow
            Write-Host "First 500 chars of content:" -ForegroundColor Yellow
            Write-Host ($docContent.Substring(0, [Math]::Min(500, $docContent.Length))) -ForegroundColor Yellow
        }
        
        Write-Host "Loading local FidoKeys.json..." -ForegroundColor Cyan
        $localEntries = Get-LocalDatabase -Path $LocalPath
        Write-Host "Found $($localEntries.Count) entries locally" -ForegroundColor Green
        
        Write-Host "Comparing databases..." -ForegroundColor Cyan
        $comparison = Compare-Databases -docEntries $docEntries -localEntries $localEntries
    }
    
    # Show changes
    Show-Changes -comparison $comparison
    
    # Process changes
    if ($isDocToDocMode -or $PreviewOnly) {
        Write-Host "`n⏸️ Preview mode: No changes will be applied." -ForegroundColor Yellow
        $approvedChanges = @{
            AddedApproved = [System.Collections.ArrayList]@()
            ModifiedApproved = [System.Collections.ArrayList]@()
            RemovedApproved = [System.Collections.ArrayList]@()
            AddedRejected = $comparison.Added
            ModifiedRejected = $comparison.Modified
            RemovedRejected = $comparison.Removed
        }
    } elseif ($AutoApprove) {
        Write-Host "`n✅ Auto-approve mode: Accepting all changes." -ForegroundColor Green
        $approvedChanges = @{
            AddedApproved = [System.Collections.ArrayList]@($comparison.Added)
            ModifiedApproved = [System.Collections.ArrayList]@($comparison.Modified)
            RemovedApproved = [System.Collections.ArrayList]@($comparison.Removed)
            AddedRejected = [System.Collections.ArrayList]@()
            ModifiedRejected = [System.Collections.ArrayList]@()
            RemovedRejected = [System.Collections.ArrayList]@()
        }
    } else {
        Write-Host "`n🔍 Interactive mode: Prompting for each change." -ForegroundColor Yellow
        $approvedChanges = Get-UserApproval -comparison $comparison
    }
    
    # Write markdown report
    Write-Host "`nGenerating sync report..." -ForegroundColor Cyan
    if ($isDocToDocMode) {
        Write-PublicDocComparisonMarkdown -comparison $comparison -baseRef $BaseRef -newRef $Ref -output $OutputMarkdown
    } else {
        Write-SyncMarkdown -comparison $comparison -approvedChanges $approvedChanges -output $OutputMarkdown
    }
    Write-Host "Report written to: $OutputMarkdown" -ForegroundColor Green
    
    # Update local database if changes approved
    $totalApproved = $approvedChanges.AddedApproved.Count + $approvedChanges.ModifiedApproved.Count + $approvedChanges.RemovedApproved.Count
    if ($totalApproved -gt 0 -and -not $PreviewOnly) {
        Write-Host "`nApplying $totalApproved approved changes..." -ForegroundColor Cyan
        Update-LocalDatabase -localData $localEntries -approvedChanges $approvedChanges -path $LocalPath
    } else {
        Write-Host "`nNo changes to apply." -ForegroundColor Yellow
    }
    
    Write-Host "`n✨ Sync complete!" -ForegroundColor Green
    
} catch {
    Write-Host "Error: $_" -ForegroundColor Red
    exit 1
}