Private/Functions/CacheHelpers.ps1

# Shared cache for project and group ID lookups
# Structure: @{ $siteUrl = @{ projects = @{ $path = $id }; groups = @{ $path = $id } } }
$script:GitlabCache = @{}

function Get-GitlabCachePath {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [string]
        $ResolvedSiteUrl
    )

    $CacheDir = Split-Path -Parent $global:GitlabConfigurationPath

    $SafeSiteName = $ResolvedSiteUrl -replace 'https?://', '' -replace '[^a-zA-Z0-9.-]', '_'

    $CachePath = Join-Path $CacheDir "cache-$SafeSiteName.yml"
    Write-Debug "GitlabCache: CachePath='$CachePath'"
    $CachePath
}

function Get-GitlabSiteCache {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]
        $ResolvedSiteUrl
    )

    if ($script:GitlabCache.ContainsKey($ResolvedSiteUrl)) {
        Write-Debug "GitlabCache: Using in-memory cache for '$ResolvedSiteUrl'"
        return $script:GitlabCache[$ResolvedSiteUrl]
    }

    $CachePath = Get-GitlabCachePath -ResolvedSiteUrl $ResolvedSiteUrl
    if (Test-Path $CachePath) {
        try {
            $DiskCache = Get-Content $CachePath -Raw | ConvertFrom-Yaml
            if ($DiskCache) {
                $script:GitlabCache[$ResolvedSiteUrl] = $DiskCache
                $ProjectCount = $DiskCache.projects ? $DiskCache.projects.Count : 0
                $GroupCount = $DiskCache.groups ? $DiskCache.groups.Count : 0
                Write-Debug "GitlabCache: Loaded $ProjectCount project(s) and $GroupCount group(s) from disk for '$ResolvedSiteUrl'"
                return $DiskCache
            }
        } catch {
            Write-Debug "GitlabCache: Error loading cache from disk: $_"
        }
    }

    Write-Debug "GitlabCache: Initializing empty cache for '$ResolvedSiteUrl'"
    $script:GitlabCache[$ResolvedSiteUrl] = @{ projects = @{}; groups = @{} }
    return $script:GitlabCache[$ResolvedSiteUrl]
}

function Save-GitlabSiteCache {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]
        $ResolvedSiteUrl
    )

    if (-not $script:GitlabCache.ContainsKey($ResolvedSiteUrl)) {
        Write-Debug "GitlabCache: Nothing to save for '$ResolvedSiteUrl'"
        return
    }

    $CachePath = Get-GitlabCachePath -ResolvedSiteUrl $ResolvedSiteUrl
    $CacheDir = Split-Path -Parent $CachePath

    if (-not (Test-Path $CacheDir)) {
        Write-Debug "GitlabCache: Creating cache directory '$CacheDir'"
        New-Item -ItemType Directory -Path $CacheDir -Force | Out-Null
    }

    try {
        $Cache = $script:GitlabCache[$ResolvedSiteUrl]
        $SortedCache = [ordered]@{}
        if ($Cache.projects) {
            $SortedCache.projects = [ordered]@{}
            $Cache.projects.GetEnumerator() | Sort-Object Key | ForEach-Object { $SortedCache.projects[$_.Key] = $_.Value }
        }
        if ($Cache.groups) {
            $SortedCache.groups = [ordered]@{}
            $Cache.groups.GetEnumerator() | Sort-Object Key | ForEach-Object { $SortedCache.groups[$_.Key] = $_.Value }
        }
        $SortedCache | ConvertTo-Yaml | Set-Content -Path $CachePath -Force
        Write-Debug "GitlabCache: Persisted cache to '$CachePath'"
    } catch {
        Write-Debug "GitlabCache: Error saving cache to disk: $_"
    }
}

function Resolve-LocalGroupPath {
    [CmdletBinding()]
    [OutputType([string])]
    param()

    $GitGroup = $(Get-LocalGitContext).Group
    if ($GitGroup) {
        Write-Debug "GitlabCache: Resolved '.' to '$GitGroup' from git context"
        return $GitGroup
    }

    $LocalPath = Get-Location | Select-Object -ExpandProperty Path
    $Parts = $LocalPath.Split([IO.Path]::DirectorySeparatorChar) | Where-Object { $_ }
    $Site = Resolve-GitlabSite
    $MaxDepth = [Math]::Min(3, $Parts.Count)

    for ($depth = 1; $depth -le $MaxDepth; $depth++) {
        $PossibleGroupName = ($Parts | Select-Object -Last $depth) -join '/'
        $CachedId = Get-GroupIdFromCache -GroupPath $PossibleGroupName -ResolvedSiteUrl $Site.Url
        if ($CachedId) {
            Write-Debug "GitlabCache: Resolved '.' to cached group '$PossibleGroupName'"
            return $PossibleGroupName
        }
        try {
            $Group = Invoke-GitlabApi GET "groups/$($PossibleGroupName | ConvertTo-UrlEncoded)" @{
                'with_projects' = 'false'
            } -SiteUrl $Site.Url
            if ($Group) {
                Set-GroupIdInCache -GroupPath $Group.FullPath -GroupId $Group.Id -ResolvedSiteUrl $Site.Url
                Write-Debug "GitlabCache: Resolved '.' to group '$($Group.FullPath)' (cached)"
                return $Group.FullPath
            }
        }
        catch {
            Write-Debug "Group lookup failed for '$PossibleGroupName': $_"
        }
        Write-Verbose "Didn't find a group named '$PossibleGroupName'"
    }

    throw "Could not infer group based on current directory ($(Get-Location))"
}

function Resolve-GitlabProjectId {
    [CmdletBinding()]
    [OutputType([int])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]
        $ProjectId
    )

    $OriginalInput = $ProjectId
    $ProjectId = $ProjectId.TrimEnd('/')
    $Site = Resolve-GitlabSite
    Write-Debug "GitlabCache: Resolve-GitlabProjectId '$OriginalInput' for site '$($Site.Url)'"

    if ($ProjectId -match '^\d+$') {
        $NumericId = [int]$ProjectId
        if (Test-ProjectIdInCache -ProjectId $NumericId -ResolvedSiteUrl $Site.Url) {
            Write-Debug "GitlabCache: Numeric ID $NumericId is cached (known valid)"
            return $NumericId
        }
        Write-Debug "GitlabCache: Numeric ID $NumericId not cached, validating via API"
        $Project = Get-GitlabProject -ProjectId $NumericId -SiteUrl $Site.Url
        Set-ProjectIdInCache -ProjectPath $Project.PathWithNamespace -ProjectId $Project.Id -ResolvedSiteUrl $Site.Url
        return $Project.Id
    }

    if ($ProjectId -eq '.') {
        $ProjectId = $(Get-LocalGitContext).Project
        if (-not $ProjectId) {
            throw "Could not infer project based on current directory ($(Get-Location))"
        }
        Write-Debug "GitlabCache: Resolved '.' to '$ProjectId'"
        if ($ProjectId -match '^\d+$') {
            return [int]$ProjectId
        }
    }

    $CachedId = Get-ProjectIdFromCache -ProjectPath $ProjectId -ResolvedSiteUrl $Site.Url
    if ($CachedId) {
        Write-Debug "GitlabCache: Cache hit '$ProjectId' -> $CachedId"
        return $CachedId
    }

    Write-Debug "GitlabCache: Cache miss for '$ProjectId', calling API"
    $Project = Get-GitlabProject -ProjectId $ProjectId -SiteUrl $Site.Url
    Set-ProjectIdInCache -ProjectPath $ProjectId -ProjectId $Project.Id -ResolvedSiteUrl $Site.Url
    Write-Debug "GitlabCache: Resolved '$ProjectId' -> $($Project.Id) (cached)"
    return $Project.Id
}

function Get-ProjectIdFromCache {
    [CmdletBinding()]
    [OutputType([int])]
    param(
        [Parameter(Mandatory)]
        [string]
        $ProjectPath,

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

    $Cache = Get-GitlabSiteCache -ResolvedSiteUrl $ResolvedSiteUrl

    if ($Cache.projects -and $Cache.projects.ContainsKey($ProjectPath)) {
        Write-Debug "GitlabCache: Get '$ProjectPath' -> $($Cache.projects[$ProjectPath])"
        return $Cache.projects[$ProjectPath]
    }

    Write-Debug "GitlabCache: Get '$ProjectPath' -> (not found)"
    return $null
}

function Test-ProjectIdInCache {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [int]
        $ProjectId,

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

    $Cache = Get-GitlabSiteCache -ResolvedSiteUrl $ResolvedSiteUrl

    if ($Cache.projects) {
        $Found = $Cache.projects.Values -contains $ProjectId
        Write-Debug "GitlabCache: Test ID $ProjectId -> $Found"
        return $Found
    }

    Write-Debug "GitlabCache: Test ID $ProjectId -> False (no projects in cache)"
    return $false
}

function Set-ProjectIdInCache {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Internal cache helper, not user-facing state change')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]
        $ProjectPath,

        [Parameter(Mandatory)]
        [int]
        $ProjectId,

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

    $Cache = Get-GitlabSiteCache -ResolvedSiteUrl $ResolvedSiteUrl

    if (-not $Cache.projects) {
        $Cache.projects = @{}
    }

    if ($Cache.projects[$ProjectPath] -eq $ProjectId) {
        Write-Debug "GitlabCache: '$ProjectPath' -> $ProjectId (unchanged, skip save)"
        return
    }

    $Cache.projects[$ProjectPath] = $ProjectId
    Write-Debug "GitlabCache: Set '$ProjectPath' -> $ProjectId"

    Save-GitlabSiteCache -ResolvedSiteUrl $ResolvedSiteUrl
}

function Resolve-GitlabGroupId {
    [CmdletBinding()]
    [OutputType([int])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]
        $GroupId
    )

    $OriginalInput = $GroupId
    $GroupId = $GroupId.TrimEnd('/')
    $Site = Resolve-GitlabSite
    Write-Debug "GitlabCache: Resolve-GitlabGroupId '$OriginalInput' for site '$($Site.Url)'"

    if ($GroupId -match '^\d+$') {
        $NumericId = [int]$GroupId
        if (Test-GroupIdInCache -GroupId $NumericId -ResolvedSiteUrl $Site.Url) {
            Write-Debug "GitlabCache: Numeric group ID $NumericId is cached (known valid)"
            return $NumericId
        }
        Write-Debug "GitlabCache: Numeric group ID $NumericId not cached, validating via API"
        $Group = Get-GitlabGroup -GroupId $NumericId -SiteUrl $Site.Url
        Set-GroupIdInCache -GroupPath $Group.FullPath -GroupId $Group.Id -ResolvedSiteUrl $Site.Url
        return $Group.Id
    }

    if ($GroupId -eq '.') {
        $GroupId = Resolve-LocalGroupPath
        if ($GroupId -match '^\d+$') {
            return [int]$GroupId
        }
    }

    $CachedId = Get-GroupIdFromCache -GroupPath $GroupId -ResolvedSiteUrl $Site.Url
    if ($CachedId) {
        Write-Debug "GitlabCache: Cache hit group '$GroupId' -> $CachedId"
        return $CachedId
    }

    Write-Debug "GitlabCache: Cache miss for group '$GroupId', calling API"
    $Group = Get-GitlabGroup -GroupId $GroupId -SiteUrl $Site.Url
    Set-GroupIdInCache -GroupPath $GroupId -GroupId $Group.Id -ResolvedSiteUrl $Site.Url
    Write-Debug "GitlabCache: Resolved group '$GroupId' -> $($Group.Id) (cached)"
    return $Group.Id
}

function Get-GroupIdFromCache {
    [CmdletBinding()]
    [OutputType([int])]
    param(
        [Parameter(Mandatory)]
        [string]
        $GroupPath,

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

    $Cache = Get-GitlabSiteCache -ResolvedSiteUrl $ResolvedSiteUrl

    if ($Cache.groups -and $Cache.groups.ContainsKey($GroupPath)) {
        Write-Debug "GitlabCache: Get group '$GroupPath' -> $($Cache.groups[$GroupPath])"
        return $Cache.groups[$GroupPath]
    }

    Write-Debug "GitlabCache: Get group '$GroupPath' -> (not found)"
    return $null
}

function Test-GroupIdInCache {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [int]
        $GroupId,

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

    $Cache = Get-GitlabSiteCache -ResolvedSiteUrl $ResolvedSiteUrl

    if ($Cache.groups) {
        $Found = $Cache.groups.Values -contains $GroupId
        Write-Debug "GitlabCache: Test group ID $GroupId -> $Found"
        return $Found
    }

    Write-Debug "GitlabCache: Test group ID $GroupId -> False (no groups in cache)"
    return $false
}

function Set-GroupIdInCache {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Internal cache helper, not user-facing state change')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]
        $GroupPath,

        [Parameter(Mandatory)]
        [int]
        $GroupId,

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

    $Cache = Get-GitlabSiteCache -ResolvedSiteUrl $ResolvedSiteUrl

    if (-not $Cache.groups) {
        $Cache.groups = @{}
    }

    if ($Cache.groups[$GroupPath] -eq $GroupId) {
        Write-Debug "GitlabCache: group '$GroupPath' -> $GroupId (unchanged, skip save)"
        return
    }

    $Cache.groups[$GroupPath] = $GroupId
    Write-Debug "GitlabCache: Set group '$GroupPath' -> $GroupId"

    Save-GitlabSiteCache -ResolvedSiteUrl $ResolvedSiteUrl
}

function Save-ProjectToCache {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Internal cache helper, not user-facing state change')]
    [CmdletBinding()]
    [OutputType('Gitlab.Project')]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [PSObject]
        $Project
    )

    begin {
        $Site = Resolve-GitlabSite
    }

    process {
        Set-ProjectIdInCache -ProjectPath $Project.PathWithNamespace -ProjectId $Project.Id -ResolvedSiteUrl $Site.Url
        $Project
    }
}

function Save-GroupToCache {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Internal cache helper, not user-facing state change')]
    [CmdletBinding()]
    [OutputType('Gitlab.Group')]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [PSObject]
        $Group
    )

    begin {
        $Site = Resolve-GitlabSite
    }

    process {
        Set-GroupIdInCache -GroupPath $Group.FullPath -GroupId $Group.Id -ResolvedSiteUrl $Site.Url
        $Group
    }
}