scripts/internal/version-check.ps1

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$sharedGovernancePath = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) 'extensions\specrew-speckit\scripts\shared-governance.ps1'
if (-not (Test-Path -LiteralPath $sharedGovernancePath -PathType Leaf)) {
    throw "Missing shared governance helper '$sharedGovernancePath'."
}
. $sharedGovernancePath

function Get-SpecrewVersionConfigValue {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ProjectRoot,

        [Parameter(Mandatory = $true)]
        [string]$Key
    )

    $configPath = Join-Path (Resolve-ProjectPath -Path $ProjectRoot) '.specrew\config.yml'
    if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) {
        return $null
    }

    foreach ($line in Get-Content -LiteralPath $configPath -Encoding UTF8) {
        if ($line -match ('^\s*{0}:\s*"?(?<value>[^"#]+?)"?\s*$' -f [regex]::Escape($Key))) {
            return $Matches['value'].Trim()
        }
    }

    return $null
}

function Get-SpecrewInstalledVersion {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    # Step 1: Get-Module -ListAvailable. SilentlyContinue + try/catch because on Linux,
    # PSModulePath often contains directories with malformed modules or permission
    # issues; without SilentlyContinue, those produce non-terminating errors that
    # $ErrorActionPreference='Stop' (set at the top of this script) turns into
    # terminating exceptions, which silently fail the whole function via outer catch.
    try {
        $module = @(Get-Module -Name Specrew -ListAvailable -ErrorAction SilentlyContinue |
            Sort-Object Version -Descending |
            Select-Object -First 1)
        if ($module.Count -gt 0 -and $module[0].Version) {
            return $module[0].Version.ToString()
        }
    }
    catch {
        # Fall through to manifest check.
    }

    # Step 2: manifest path search. Always try the repo-root manifest (two parents
    # up from this script). Add ProjectRoot manifest only if Resolve-ProjectPath
    # succeeds (it normally does; defensive).
    $manifestCandidates = @(
        (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) 'Specrew.psd1')
    )

    try {
        $resolvedProjectRoot = Resolve-ProjectPath -Path $ProjectRoot
        $manifestCandidates += (Join-Path $resolvedProjectRoot 'Specrew.psd1')
    }
    catch {
        # ProjectRoot may be unresolvable; the repo-root manifest is still tried.
    }

    $manifestCandidates = @($manifestCandidates | Select-Object -Unique)

    foreach ($manifestPath in $manifestCandidates) {
        try {
            if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
                continue
            }

            $manifest = Import-PowerShellDataFile -LiteralPath $manifestPath
            if ($manifest -and $manifest.ContainsKey('ModuleVersion')) {
                return [string]$manifest.ModuleVersion
            }
        }
        catch {
            continue
        }
    }

    return $null
}

function ConvertTo-SpecrewSemanticVersion {
    param([AllowNull()][string]$Value)

    if ([string]::IsNullOrWhiteSpace($Value)) {
        return $null
    }

    $match = [regex]::Match($Value, '(?<version>\d+\.\d+\.\d+(?:\.\d+)?)')
    if (-not $match.Success) {
        return $null
    }

    try {
        return [version]$match.Groups['version'].Value
    }
    catch {
        return $null
    }
}

function Get-SpecrewSlashCommandMinVersion {
    return '0.24.0'
}

function Get-SpecrewSupportedVersionsPath {
    return Join-Path $PSScriptRoot 'supported-versions.yml'
}

function Get-SpecrewSupportedVersions {
    param(
        [string]$Path = (Get-SpecrewSupportedVersionsPath)
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        return $null
    }

    try {
        $result = [ordered]@{
            Schema  = $null
            Speckit = [ordered]@{ Min = $null; MaxTested = $null; Notes = '' }
            Squad   = [ordered]@{ Min = $null; MaxTested = $null; Notes = '' }
        }

        $currentSection = $null
        foreach ($line in (Get-Content -LiteralPath $Path -Encoding UTF8)) {
            if ($line -match '^\s*(?:#.*)?$') { continue }

            if ($line -match '^schema:\s*"?(?<value>[^"#\r\n]+?)"?\s*(?:#.*)?$') {
                $result.Schema = $Matches['value'].Trim()
                $currentSection = $null
                continue
            }

            if ($line -match '^(?<section>speckit|squad):\s*(?:#.*)?$') {
                $currentSection = $Matches['section']
                continue
            }

            if ($currentSection -and ($line -match '^\s+(?<key>min|max_tested|notes):\s*"?(?<value>[^"#\r\n]*?)"?\s*(?:#.*)?$')) {
                $key = $Matches['key']
                $value = $Matches['value'].Trim()

                $sectionMap = if ($currentSection -eq 'speckit') { $result.Speckit } else { $result.Squad }
                switch ($key) {
                    'min'        { $sectionMap.Min = $value }
                    'max_tested' { $sectionMap.MaxTested = $value }
                    'notes'      { $sectionMap.Notes = $value }
                }
            }
        }

        if ([string]::IsNullOrWhiteSpace($result.Speckit.Min) -or
            [string]::IsNullOrWhiteSpace($result.Speckit.MaxTested) -or
            [string]::IsNullOrWhiteSpace($result.Squad.Min) -or
            [string]::IsNullOrWhiteSpace($result.Squad.MaxTested)) {
            return $null
        }

        if (-not [string]::IsNullOrWhiteSpace($env:SPECREW_SUPPORTED_MAX_SPECKIT)) {
            $result.Speckit.MaxTested = $env:SPECREW_SUPPORTED_MAX_SPECKIT.Trim()
        }
        if (-not [string]::IsNullOrWhiteSpace($env:SPECREW_SUPPORTED_MAX_SQUAD)) {
            $result.Squad.MaxTested = $env:SPECREW_SUPPORTED_MAX_SQUAD.Trim()
        }

        return $result
    }
    catch {
        return $null
    }
}

function Get-SpecrewVersionStatus {
    param(
        [AllowNull()][string]$Current,
        [AllowNull()][string]$Min,
        [AllowNull()][string]$MaxTested
    )

    if ([string]::IsNullOrWhiteSpace($Current)) {
        return 'not-installed'
    }

    $currentVersion = ConvertTo-SpecrewSemanticVersion -Value $Current
    if ($null -eq $currentVersion) {
        return 'unknown'
    }

    $minVersion = ConvertTo-SpecrewSemanticVersion -Value $Min
    $maxTestedVersion = ConvertTo-SpecrewSemanticVersion -Value $MaxTested

    if (($null -ne $minVersion) -and ($currentVersion -lt $minVersion)) {
        return 'behind-supported'
    }

    if ($null -ne $maxTestedVersion) {
        if ($currentVersion -eq $maxTestedVersion) { return 'current' }
        if ($currentVersion -lt $maxTestedVersion) { return 'update-available-supported' }
        if ($currentVersion -gt $maxTestedVersion) { return 'ahead-of-supported' }
    }

    return 'unknown'
}

function Get-SpecrewVersionCheckCachePath {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    return Join-Path (Resolve-ProjectPath -Path $ProjectRoot) '.specrew\version-check-cache.json'
}

function Test-SpecrewSkipUpdateCheck {
    param([bool]$SkipUpdateCheck)

    if ($SkipUpdateCheck) {
        return $true
    }

    return ($env:SPECREW_SKIP_UPDATE_CHECK -eq '1')
}

function Get-SpecrewVersionCheckCacheState {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    $cachePath = Get-SpecrewVersionCheckCachePath -ProjectRoot $ProjectRoot
    if (-not (Test-Path -LiteralPath $cachePath -PathType Leaf)) {
        return $null
    }

    try {
        # F-023: Use -AsHashtable for StrictMode compatibility; hashtable indexer tolerates missing fields
        $cache = Get-Content -LiteralPath $cachePath -Raw -Encoding UTF8 | ConvertFrom-Json -AsHashtable -Depth 6
        
        # F-023: Legacy schema handling - missing 'schema' field implies v0
        $schema = $cache['schema']
        if (-not $schema) {
            Write-Debug "schema-implied-v0 for $cachePath"
            # v0 behavior: all cache fields are optional
        }
        # v1+ behavior: same as v0 for this cache (no behavioral divergence yet)
        
        return $cache
    }
    catch {
        return $null
    }
}

function Set-SpecrewVersionCheckCacheState {
    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [Parameter(Mandatory = $true)][string]$LatestVersion,
        [Parameter(Mandatory = $true)][string]$CheckedAt,
        [Parameter(Mandatory = $true)][string]$CacheValidUntil,
        [Parameter(Mandatory = $true)][string]$Source
    )

    $payload = [ordered]@{
        schema            = 'v1'
        latest_version    = $LatestVersion
        checked_at        = $CheckedAt
        cache_valid_until = $CacheValidUntil
        source            = $Source
    } | ConvertTo-Json -Depth 6

    Write-Utf8FileAtomic -Path (Get-SpecrewVersionCheckCachePath -ProjectRoot $ProjectRoot) -Content ($payload + [Environment]::NewLine)
}

function Test-SpecrewVersionCacheValid {
    param([AllowNull()][object]$CacheState)

    if ($null -eq $CacheState -or [string]::IsNullOrWhiteSpace([string]$CacheState.cache_valid_until)) {
        return $false
    }

    try {
        $cacheValidUntil = [datetime]::Parse([string]$CacheState.cache_valid_until, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal)
        return ($cacheValidUntil -gt (Get-Date).ToUniversalTime())
    }
    catch {
        return $false
    }
}

function Invoke-SpecrewPSGalleryLatestVersionQuery {
    param([int]$TimeoutSeconds = 10)

    if (-not [string]::IsNullOrWhiteSpace($env:SPECREW_PSGALLERY_LATEST_VERSION)) {
        return [pscustomobject]@{
            LatestVersion = $env:SPECREW_PSGALLERY_LATEST_VERSION
            Source        = 'override'
        }
    }

    if ($env:SPECREW_PSGALLERY_FORCE_FAILURE -eq '1') {
        throw 'Simulated PSGallery failure.'
    }

    $job = Start-Job -ScriptBlock {
        $module = Find-Module -Name Specrew -Repository PSGallery -ErrorAction Stop
        [pscustomobject]@{
            LatestVersion = [string]$module.Version
            Source        = 'psgallery'
        }
    }

    try {
        if (-not (Wait-Job -Job $job -Timeout $TimeoutSeconds)) {
            Stop-Job -Job $job -ErrorAction SilentlyContinue | Out-Null
            throw ("Timed out after {0} seconds while querying PSGallery." -f $TimeoutSeconds)
        }

        $result = Receive-Job -Job $job -ErrorAction Stop
        if ($null -eq $result -or [string]::IsNullOrWhiteSpace([string]$result.LatestVersion)) {
            throw 'PSGallery query returned no version.'
        }

        return $result
    }
    finally {
        Remove-Job -Job $job -Force -ErrorAction SilentlyContinue | Out-Null
    }
}

function Get-PSGalleryLatestVersion {
    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [switch]$ForceRefresh,
        [bool]$SkipCheck
    )

    if (Test-SpecrewSkipUpdateCheck -SkipUpdateCheck $SkipCheck) {
        return [pscustomobject]@{
            Skipped       = $true
            LatestVersion = $null
            Source        = 'skipped'
        }
    }

    $cacheState = Get-SpecrewVersionCheckCacheState -ProjectRoot $ProjectRoot
    if (-not $ForceRefresh -and (Test-SpecrewVersionCacheValid -CacheState $cacheState) -and -not [string]::IsNullOrWhiteSpace([string]$cacheState.latest_version)) {
        return [pscustomobject]@{
            Skipped       = $false
            LatestVersion = [string]$cacheState.latest_version
            Source        = 'cache'
            CheckedAt     = [string]$cacheState.checked_at
            CacheValidUntil = [string]$cacheState.cache_valid_until
        }
    }

    try {
        $queryResult = Invoke-SpecrewPSGalleryLatestVersionQuery
        $checkedAt = (Get-Date).ToUniversalTime().ToString('o')
        $cacheValidUntil = (Get-Date).ToUniversalTime().AddHours(24).ToString('o')
        Set-SpecrewVersionCheckCacheState `
            -ProjectRoot $ProjectRoot `
            -LatestVersion ([string]$queryResult.LatestVersion) `
            -CheckedAt $checkedAt `
            -CacheValidUntil $cacheValidUntil `
            -Source ([string]$queryResult.Source)

        return [pscustomobject]@{
            Skipped         = $false
            LatestVersion   = [string]$queryResult.LatestVersion
            Source          = [string]$queryResult.Source
            CheckedAt       = $checkedAt
            CacheValidUntil = $cacheValidUntil
        }
    }
    catch {
        Write-Verbose ("PSGallery latest-version query failed: {0}" -f $_.Exception.Message)
        return $null
    }
}

function Get-PSGalleryUpdateWarning {
    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [bool]$SkipCheck
    )

    if (Test-SpecrewSkipUpdateCheck -SkipUpdateCheck $SkipCheck) {
        return $null
    }

    $installedVersionText = Get-SpecrewInstalledVersion -ProjectRoot $ProjectRoot
    $installedVersion = ConvertTo-SpecrewSemanticVersion -Value $installedVersionText
    if ($null -eq $installedVersion) {
        return $null
    }

    $latestState = Get-PSGalleryLatestVersion -ProjectRoot $ProjectRoot -SkipCheck:$SkipCheck
    if ($null -eq $latestState -or [string]::IsNullOrWhiteSpace([string]$latestState.LatestVersion)) {
        return $null
    }

    $latestVersion = ConvertTo-SpecrewSemanticVersion -Value ([string]$latestState.LatestVersion)
    if ($null -eq $latestVersion -or $latestVersion -le $installedVersion) {
        return $null
    }

    return "Newer version available: $($latestVersion.ToString()) (current: $($installedVersion.ToString())). To update: Update-Module Specrew"
}