Private/Use-BlackCatCache.ps1

using namespace System.IO.Compression

function Set-BlackCatCache {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Key,
        
        [Parameter(Mandatory = $true)]
        [object]$Data,
        
        [Parameter(Mandatory = $false)]
        [int]$ExpirationMinutes = 30,

        [Parameter(Mandatory = $false)]
        [ValidateSet('MSGraph', 'AzBatch', 'General')]
        [string]$CacheType = 'General',

        [Parameter(Mandatory = $false)]
        [int]$MaxCacheSize = 100,

        [Parameter(Mandatory = $false)]
        [switch]$CompressData
    )
    
    # Initialize cache if it doesn't exist
    $currentCache = Initialize-CacheStore -CacheType $CacheType
    
    # Apply cache size limit (LRU eviction)
    if ($currentCache.Value.Count -ge $MaxCacheSize) {
        # Remove oldest entries until we're under the limit
        $sortedEntries = $currentCache.Value.GetEnumerator() | Sort-Object { $_.Value.Timestamp }
        $entriesToRemove = $sortedEntries | Select-Object -First ($currentCache.Value.Count - $MaxCacheSize + 1)
        
        foreach ($entry in $entriesToRemove) {
            $currentCache.Value.Remove($entry.Key)
            Write-Verbose "Removed old cache entry: $($entry.Key) from $CacheType cache (LRU cleanup)"
        }
    }
    
    # Prepare data for caching
    $dataToCache = $Data
    $isCompressed = $false
    
    # Apply compression if requested
    if ($CompressData -and $Data) {
        try {
            # Simple compression using .NET compression
            $jsonData = $Data | ConvertTo-Json -Depth 10 -Compress
            $bytes = [System.Text.Encoding]::UTF8.GetBytes($jsonData)
            
            if ($bytes.Length -gt 1KB) {  # Only compress if data is larger than 1KB
                $memoryStream = New-Object System.IO.MemoryStream
                $gzipStream = New-Object System.IO.Compression.GzipStream($memoryStream, [System.IO.Compression.CompressionMode]::Compress)
                $gzipStream.Write($bytes, 0, $bytes.Length)
                $gzipStream.Close()
                
                $compressedBytes = $memoryStream.ToArray()
                $memoryStream.Close()
                
                if ($compressedBytes.Length -lt $bytes.Length) {
                    $dataToCache = [Convert]::ToBase64String($compressedBytes)
                    $isCompressed = $true
                    Write-Verbose "Compressed cache data: $($bytes.Length) → $($compressedBytes.Length) bytes"
                }
            }
        }
        catch {
            Write-Verbose "Compression failed, storing uncompressed: $($_.Exception.Message)"
        }
    }
    
    $cacheEntry = @{
        Data = $dataToCache
        Timestamp = Get-Date
        ExpirationMinutes = $ExpirationMinutes
        IsCompressed = $isCompressed
        Size = if ($isCompressed) { [Convert]::FromBase64String($dataToCache).Length } else { 
            ($Data | ConvertTo-Json -Depth 10 -Compress | Measure-Object -Character).Characters 
        }
    }
    
    $currentCache.Value[$Key] = $cacheEntry
    $compressionNote = if ($isCompressed) { " (compressed)" } else { "" }
    Write-Verbose "Cached data for key: $Key in $CacheType cache (expires in $ExpirationMinutes minutes)$compressionNote"
}

function Get-BlackCatCache {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Key,

        [Parameter(Mandatory = $false)]
        [ValidateSet('MSGraph', 'AzBatch', 'General')]
        [string]$CacheType = 'General'
    )
    
    # Get cache variable
    $currentCache = Get-CacheStore -CacheType $CacheType
    
    # Return null if cache doesn't exist
    if (-not $currentCache -or -not $currentCache.Value) {
        return $null
    }
    
    # Return null if key doesn't exist
    if (-not $currentCache.Value.ContainsKey($Key)) {
        return $null
    }
    
    $cacheEntry = $currentCache.Value[$Key]
    
    # Check if cache entry has expired
    if (Test-CacheEntryExpired -CacheEntry $cacheEntry) {
        $age = (Get-Date) - $cacheEntry.Timestamp
        Write-Verbose "Cache entry for key '$Key' in $CacheType cache has expired (age: $($age.TotalMinutes) minutes)"
        $currentCache.Value.Remove($Key)
        return $null
    }
    
    # Decompress data if it was compressed
    $resultData = $cacheEntry.Data
    if ($cacheEntry.IsCompressed) {
        try {
            $compressedBytes = [Convert]::FromBase64String($cacheEntry.Data)
            $memoryStream = New-Object System.IO.MemoryStream(,$compressedBytes)
            $gzipStream = New-Object System.IO.Compression.GzipStream($memoryStream, [System.IO.Compression.CompressionMode]::Decompress)
            $streamReader = New-Object System.IO.StreamReader($gzipStream)
            $jsonData = $streamReader.ReadToEnd()
            $streamReader.Close()
            $gzipStream.Close()
            $memoryStream.Close()
            
            $resultData = $jsonData | ConvertFrom-Json
            Write-Verbose "Decompressed cache data for key: $Key"
        }
        catch {
            Write-Verbose "Failed to decompress cache data for key: $Key - $($_.Exception.Message)"
            $currentCache.Value.Remove($Key)
            return $null
        }
    }
    
    $age = (Get-Date) - $cacheEntry.Timestamp
    Write-Verbose "Retrieved cached data for key: $Key from $CacheType cache (age: $($age.TotalMinutes) minutes)"
    return $resultData
}

function Clear-BlackCatCacheInternal {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]$Key,
        
        [Parameter(Mandatory = $false)]
        [switch]$All,

        [Parameter(Mandatory = $false)]
        [ValidateSet('MSGraph', 'AzBatch', 'General')]
        [string]$CacheType = 'General'
    )
    
    # Get cache variable
    $currentCache = Get-CacheStore -CacheType $CacheType
    
    if (-not $currentCache -or -not $currentCache.Value) {
        Write-Verbose "No $CacheType cache to clear"
        return
    }
    
    if ($All) {
        $currentCache.Value.Clear()
        Write-Verbose "Cleared all $CacheType cache entries"
    }
    elseif ($Key) {
        if ($currentCache.Value.ContainsKey($Key)) {
            $currentCache.Value.Remove($Key)
            Write-Verbose "Cleared $CacheType cache entry for key: $Key"
        }
        else {
            Write-Verbose "$CacheType cache key '$Key' not found"
        }
    }
    else {
        Write-Warning "Specify either -Key or -All parameter"
    }
}

function Get-BlackCatCacheStatsInternal {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [ValidateSet('MSGraph', 'AzBatch', 'General')]
        [string]$CacheType = 'General'
    )
    
    # Get cache variable
    $currentCache = Get-CacheStore -CacheType $CacheType
    
    if (-not $currentCache -or -not $currentCache.Value) {
        Write-Host "No $CacheType cache initialized" -ForegroundColor Yellow
        return
    }
    
    $totalEntries = $currentCache.Value.Count
    $expiredEntries = 0
    $validEntries = 0
    
    foreach ($key in $currentCache.Value.Keys) {
        $cacheEntry = $currentCache.Value[$key]
        
        if (Test-CacheEntryExpired -CacheEntry $cacheEntry) {
            $expiredEntries++
        }
        else {
            $validEntries++
        }
    }
    
    $stats = [PSCustomObject]@{
        CacheType = $CacheType
        TotalEntries = $totalEntries
        ValidEntries = $validEntries
        ExpiredEntries = $expiredEntries
        CacheKeys = $currentCache.Value.Keys | Sort-Object
    }
    
    return $stats
}

function ConvertTo-CacheKey {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$BaseIdentifier,
        
        [Parameter(Mandatory = $false)]
        [hashtable]$Parameters = @{}
    )
    
    # Create a consistent cache key from the base identifier and parameters
    $keyParts = @($BaseIdentifier)
    
    # Add sorted parameters to ensure consistent key generation
    if ($Parameters.Count -gt 0) {
        $sortedParams = $Parameters.GetEnumerator() | Sort-Object Name
        foreach ($param in $sortedParams) {
            $keyParts += "$($param.Name)=$($param.Value)"
        }
    }
    
    $cacheKey = ($keyParts -join "|").ToLower()
    return $cacheKey
}

function Invoke-CacheableOperation {
    <#
    .SYNOPSIS
        Executes a cacheable operation with unified cache handling logic.
     
    .DESCRIPTION
        This helper function encapsulates the common pattern of cache retrieval,
        operation execution, and cache storage used by both MSGraph and AzBatch functions.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$CacheKey,
        
        [Parameter(Mandatory = $true)]
        [ValidateSet('MSGraph', 'AzBatch', 'General')]
        [string]$CacheType,
        
        [Parameter(Mandatory = $true)]
        [scriptblock]$Operation,
        
        [Parameter(Mandatory = $false)]
        [bool]$SkipCache = $false,
        
        [Parameter(Mandatory = $false)]
        [int]$CacheExpirationMinutes = 30,
        
        [Parameter(Mandatory = $false)]
        [int]$MaxCacheSize = 100,
        
        [Parameter(Mandatory = $false)]
        [bool]$CompressCache = $false,
        
        [Parameter(Mandatory = $false)]
        [string]$OperationName = "operation"
    )
    
    # Check cache first (unless skipping cache)
    if (-not $SkipCache) {
        try {
            $cachedResult = Get-BlackCatCache -Key $CacheKey -CacheType $CacheType
            if ($null -ne $cachedResult) {
                Write-Verbose "Retrieved result from cache for $OperationName"
                return $cachedResult
            }
        }
        catch {
            Write-Verbose "Error retrieving from cache: $($_.Exception.Message). Proceeding with fresh $OperationName."
        }
    }
    
    # Execute the operation
    try {
        $result = & $Operation
        
        # Cache the result (unless skipping cache or result is null)
        if (-not $SkipCache -and $null -ne $result) {
            try {
                Set-BlackCatCache -Key $CacheKey -Data $result -ExpirationMinutes $CacheExpirationMinutes -CacheType $CacheType -MaxCacheSize $MaxCacheSize -CompressData:$CompressCache
                Write-Verbose "Cached result for $OperationName (expires in $CacheExpirationMinutes minutes)"
            }
            catch {
                Write-Verbose "Failed to cache result for $OperationName - $($_.Exception.Message)"
            }
        }
        
        return $result
    }
    catch {
        Write-Verbose "Error executing $OperationName : $($_.Exception.Message)"
        throw
    }
}

function Initialize-CacheStore {
    <#
    .SYNOPSIS
        Initializes a cache store if it doesn't exist.
     
    .DESCRIPTION
        This helper function consolidates the cache initialization logic
        used across multiple cache functions.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet('MSGraph', 'AzBatch', 'General')]
        [string]$CacheType
    )
    
    $currentCache = Get-Variable -Name "${CacheType}Cache" -Scope Script -ErrorAction SilentlyContinue
    
    if (-not $currentCache) {
        New-Variable -Name "${CacheType}Cache" -Value @{} -Scope Script -Force
        $currentCache = Get-Variable -Name "${CacheType}Cache" -Scope Script
    }
    
    return $currentCache
}

function Get-CacheStore {
    <#
    .SYNOPSIS
        Gets a cache store for the specified cache type.
     
    .DESCRIPTION
        This helper function consolidates the cache store retrieval logic
        used across multiple cache functions.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet('MSGraph', 'AzBatch', 'General')]
        [string]$CacheType
    )
    
    return Get-Variable -Name "${CacheType}Cache" -Scope Script -ErrorAction SilentlyContinue
}

function Get-CacheParameters {
    <#
    .SYNOPSIS
        Extracts and validates cache parameters from function calls.
     
    .DESCRIPTION
        This helper function standardizes cache parameter extraction
        for use with the Invoke-CacheableOperation function.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [bool]$SkipCache = $false,
        
        [Parameter(Mandatory = $false)]
        [int]$CacheExpirationMinutes = 30,
        
        [Parameter(Mandatory = $false)]
        [int]$MaxCacheSize = 100,
        
        [Parameter(Mandatory = $false)]
        [bool]$CompressCache = $false
    )
    
    return @{
        SkipCache = $SkipCache
        CacheExpirationMinutes = $CacheExpirationMinutes
        MaxCacheSize = $MaxCacheSize
        CompressCache = $CompressCache
    }
}

function Test-CacheEntryExpired {
    <#
    .SYNOPSIS
        Tests if a cache entry has expired.
     
    .DESCRIPTION
        This helper function consolidates cache expiration logic
        used across multiple cache functions.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [hashtable]$CacheEntry
    )
    
    $age = (Get-Date) - $CacheEntry.Timestamp
    return ($age.TotalMinutes -gt $CacheEntry.ExpirationMinutes)
}