src/Remote.ps1

# Remote.ps1 - fetch community catalog entries from a GitHub repo.
#
# Themes are contributed as one JSON file per catalog dir (like oh-my-posh /
# nerd-fonts). This lets a user browse and pull entries straight from a repo
# without cloning it: list what's there, then save the ones they want locally.

$script:DefaultRepo = 'livlign/posh-palette'

function Get-PoshPaletteRemoteCatalog {
    [CmdletBinding()]
    param(
        [ValidateSet('themes','schemes','palettes','prompts')] [string] $Kind = 'themes',
        [string] $Repo = $script:DefaultRepo,
        [string] $Branch = 'main',
        [int] $TimeoutSec = 10
    )
    $api = "https://api.github.com/repos/$Repo/contents/$Kind`?ref=$Branch"
    $headers = @{ 'User-Agent' = 'PoshPalette'; 'Accept' = 'application/vnd.github+json' }
    if ($env:GITHUB_TOKEN) { $headers['Authorization'] = "Bearer $env:GITHUB_TOKEN" }

    $items = Invoke-RestMethod -Uri $api -Headers $headers -TimeoutSec $TimeoutSec
    $items | Where-Object { $_.name -like '*.json' } | ForEach-Object {
        [pscustomobject]@{
            Id          = [IO.Path]::GetFileNameWithoutExtension($_.name)
            Kind        = $Kind
            Repo        = $Repo
            DownloadUrl = $_.download_url
        }
    }
}

# Download one entry into the local catalog dir (themes/, schemes/, ...).
function Save-PoshPaletteRemoteTheme {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)][string] $Id,
        [ValidateSet('themes','schemes','palettes','prompts')] [string] $Kind = 'themes',
        [string] $Repo = $script:DefaultRepo,
        [string] $Branch = 'main',
        [switch] $Force
    )
    $entry = Get-PoshPaletteRemoteCatalog -Kind $Kind -Repo $Repo -Branch $Branch |
        Where-Object { $_.Id -eq $Id } | Select-Object -First 1
    if (-not $entry) { throw "No '$Kind' entry '$Id' in $Repo." }

    $dest = Join-Path (Join-Path (Get-PoshPaletteDataRoot) $Kind) "$Id.json"
    if ((Test-Path $dest) -and -not $Force) { throw "$dest already exists. Use -Force to overwrite." }

    $headers = @{ 'User-Agent' = 'PoshPalette' }
    $body = Invoke-RestMethod -Uri $entry.DownloadUrl -Headers $headers
    $json = if ($body -is [string]) { $body } else { $body | ConvertTo-Json -Depth 32 }
    $null = ConvertFrom-Jsonc $json   # validate before writing
    Set-Content -Path $dest -Value $json -Encoding utf8
    Write-Host "Saved $Kind/$Id -> $dest" -ForegroundColor Green
    Get-Item $dest
}

# Download one remote entry into the user cache (~/.poshpalette/catalog/<kind>/),
# validate it, and return the parsed object. Used by the auto-refresh below.
function Save-PoshPaletteCacheEntry {
    param([Parameter(Mandatory)][string] $Kind, [Parameter(Mandatory)] $Entry, [int] $TimeoutSec = 6)
    $dir = Join-Path (Get-PoshPaletteCacheRoot) $Kind
    if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
    $headers = @{ 'User-Agent' = 'PoshPalette' }
    $body = Invoke-RestMethod -Uri $Entry.DownloadUrl -Headers $headers -TimeoutSec $TimeoutSec
    $json = if ($body -is [string]) { $body } else { $body | ConvertTo-Json -Depth 32 }
    $parsed = ConvertFrom-Jsonc $json   # validate before writing
    Set-Content -Path (Join-Path $dir "$($Entry.Id).json") -Value $json -Encoding utf8
    $parsed
}

# Auto-refresh the catalog from GitHub: pull any themes (and the scheme / palette
# / prompt files they reference) that aren't already available locally, into the
# user cache so they appear in the picker. Safe to call on every launch:
# * throttled to once / 24h unless -Force,
# * disabled entirely by $env:POSHPALETTE_NO_AUTOUPDATE (unless -Force),
# * every network call is time-boxed and the whole thing is best-effort, so an
# offline or slow GitHub never blocks startup — it just uses what's cached.
# Returns the number of new themes added.
function Update-PoshPaletteCatalog {
    [CmdletBinding()]
    param(
        [switch] $Force,
        [string] $Repo = $script:DefaultRepo,
        [string] $Branch = 'main',
        [int] $TimeoutSec = 5
    )
    if ($env:POSHPALETTE_NO_AUTOUPDATE -and -not $Force) { return 0 }

    $cacheRoot = Get-PoshPaletteCacheRoot
    $stamp     = Join-Path $cacheRoot '.last-refresh'

    if (-not $Force -and (Test-Path $stamp)) {
        $last = try { [datetime]((Get-Content $stamp -Raw).Trim()) } catch { $null }
        if ($last -and ((Get-Date) - $last).TotalHours -lt 24) { return 0 }   # refreshed recently
    }

    $added = 0
    try {
        if (-not (Test-Path $cacheRoot)) { New-Item -ItemType Directory -Path $cacheRoot -Force | Out-Null }

        # Same pass also caches the latest published version, so the version check
        # rides the same once/24h cadence as the theme fetch (read back cheaply by
        # Get-PoshPaletteUpdateAvailable for the menu).
        try {
            $latest = Get-PoshPaletteLatestVersion -TimeoutSec $TimeoutSec
            if ($latest) { Set-Content -Path (Join-Path $cacheRoot '.latest-version') -Value $latest -Encoding utf8 }
        } catch { Write-Verbose "PoshPalette version check skipped: $_" }

        $haveThemes = @(Get-PoshPaletteThemes | ForEach-Object Id)
        $remote     = @(Get-PoshPaletteRemoteCatalog -Kind themes -Repo $Repo -Branch $Branch -TimeoutSec $TimeoutSec)
        $newThemes  = @($remote | Where-Object { $_.Id -notin $haveThemes })

        if ($newThemes.Count -gt 0) {
            # Indexes for resolving each new theme's referenced layers.
            $remIdx = @{}
            $have   = @{}
            foreach ($k in 'schemes', 'palettes', 'prompts') {
                $remIdx[$k] = @(Get-PoshPaletteRemoteCatalog -Kind $k -Repo $Repo -Branch $Branch -TimeoutSec $TimeoutSec)
                $have[$k]   = @(Get-PoshPaletteCatalog -Kind $k | ForEach-Object Id)
            }
            $themesDir = Join-Path (Get-PoshPaletteCacheRoot) 'themes'
            if (-not (Test-Path $themesDir)) { New-Item -ItemType Directory -Path $themesDir -Force | Out-Null }
            $headers = @{ 'User-Agent' = 'PoshPalette' }
            foreach ($t in $newThemes) {
                # Peek the theme to learn its referenced layers before committing it.
                $raw  = Invoke-RestMethod -Uri $t.DownloadUrl -Headers $headers -TimeoutSec $TimeoutSec
                $json = if ($raw -is [string]) { $raw } else { $raw | ConvertTo-Json -Depth 32 }
                $theme = ConvertFrom-Jsonc $json   # validate
                if (-not $theme.id) { continue }
                # Cache any missing scheme / palette / prompt it depends on first.
                foreach ($dep in @(
                        @{ k = 'schemes';  id = $theme.scheme },
                        @{ k = 'palettes'; id = $theme.palette },
                        @{ k = 'prompts';  id = $theme.prompt })) {
                    if (-not $dep.id -or ($dep.id -in $have[$dep.k])) { continue }   # already have it
                    $e = $remIdx[$dep.k] | Where-Object { $_.Id -eq $dep.id } | Select-Object -First 1
                    if ($e) { $null = Save-PoshPaletteCacheEntry -Kind $dep.k -Entry $e -TimeoutSec $TimeoutSec; $have[$dep.k] += $dep.id }
                }
                # Commit the theme LAST, so it never appears without its layers.
                Set-Content -Path (Join-Path $themesDir "$($t.Id).json") -Value $json -Encoding utf8
                $added++
            }
        }

        if (-not (Test-Path $cacheRoot)) { New-Item -ItemType Directory -Path $cacheRoot -Force | Out-Null }
        Set-Content -Path $stamp -Value ((Get-Date).ToString('o')) -Encoding utf8
    } catch {
        Write-Verbose "PoshPalette catalog refresh skipped: $_"   # offline / slow / rate-limited: non-fatal
    }
    $added
}

# --- Version check ------------------------------------------------------------

# The latest PoshPalette version published to the PowerShell Gallery, as a string,
# or $null if it can't be determined. Uses the Gallery's OData feed via a
# time-boxed Invoke-WebRequest and regex (no PowerShellGet round-trip, so it can't
# hang), then takes the highest version found.
function Get-PoshPaletteLatestVersion {
    param([int] $TimeoutSec = 5)
    $url = "https://www.powershellgallery.com/api/v2/FindPackagesById()?id='PoshPalette'"
    $resp = Invoke-WebRequest -Uri $url -Headers @{ 'User-Agent' = 'PoshPalette' } -TimeoutSec $TimeoutSec
    $content = [string]$resp.Content
    $versions = [regex]::Matches($content, '<d:Version>([0-9][0-9.]*)</d:Version>') |
        ForEach-Object { try { [version]$_.Groups[1].Value } catch { $null } } |
        Where-Object { $_ }
    if (-not $versions) { return $null }
    ($versions | Sort-Object -Descending | Select-Object -First 1).ToString()
}

# The version of this loaded module (from its manifest).
function Get-PoshPaletteInstalledVersion {
    $m = $ExecutionContext.SessionState.Module
    if ($m -and $m.Version) { return $m.Version }
    (Get-Module PoshPalette | Sort-Object Version -Descending | Select-Object -First 1).Version
}

# If the cached latest published version is newer than the installed one, return
# it as a string; otherwise $null. Reads the cache written by the daily refresh,
# so it's instant and works offline.
function Get-PoshPaletteUpdateAvailable {
    $f = Join-Path (Get-PoshPaletteCacheRoot) '.latest-version'
    if (-not (Test-Path $f)) { return $null }
    $latest = try { [version]((Get-Content $f -Raw).Trim()) } catch { $null }
    $cur    = try { [version](Get-PoshPaletteInstalledVersion) } catch { $null }
    if ($latest -and $cur -and ($latest -gt $cur)) { return $latest.ToString() }
    $null
}