src/SemanticVersionUtilities.ps1

# Semantic Version Utilities - Parsing, validation, and comparison functions

function ConvertTo-SemanticVersionObject {
    <#
    .SYNOPSIS
        Converts a Git tag name to a structured semantic version object.
     
    .DESCRIPTION
        Parses a Git tag name and extracts semantic version components including
        major, minor, patch, pre-release identifiers, and build metadata.
         
        Supports multiple formats:
        - v1.2.3, 1.2.3 (standard versions)
        - v1.2.3-alpha.1 (pre-release)
        - v1.2.3+build.123 (build metadata)
        - v1.2.3-alpha.1+build.123 (combined)
     
    .PARAMETER TagName
        Git tag name to parse
     
    .OUTPUTS
        [PSCustomObject] Structured version object with Tag, Version, IsPreRelease, and metadata properties
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$TagName
    )
    
    try {
        Write-SafeDebugLog "Parsing semantic version tag" -Context "TagName: $TagName"
        
        # Extract version string (remove 'v' prefix if present)
        $versionString = if ($TagName.StartsWith('v')) {
            $TagName.Substring(1)
        } else {
            $TagName
        }
        
        Write-SafeDebugLog "Extracted version string" -Context "Original: $TagName`nVersionString: $versionString"
        
        # Parse using PowerShell's semantic version class
        $semVer = [System.Management.Automation.SemanticVersion]::new($versionString)
        
        # Determine pre-release information
        $isPreRelease = -not [string]::IsNullOrEmpty($semVer.PreReleaseLabel)
        $preReleaseLabel = if ($isPreRelease) {
            # Extract just the label part (alpha, beta, rc, etc.)
            if ($semVer.PreReleaseLabel -match '^([a-zA-Z]+)') {
                $matches[1]
            } else {
                $semVer.PreReleaseLabel
            }
        } else {
            $null
        }
        
        $parsedVersion = [PSCustomObject]@{
            Tag = $TagName
            Version = $semVer
            IsPreRelease = $isPreRelease
            PreReleaseLabel = $preReleaseLabel
            Major = $semVer.Major
            Minor = $semVer.Minor
            Patch = $semVer.Patch
            BuildLabel = $semVer.BuildLabel
        }
        
        Write-SafeDebugLog "Successfully parsed semantic version" -Context "Tag: $TagName`nMajor: $($semVer.Major)`nMinor: $($semVer.Minor)`nPatch: $($semVer.Patch)`nIsPreRelease: $isPreRelease"
        
        return $parsedVersion
        
    }
    catch {
        $warningMsg = "Failed to parse semantic version from tag '$TagName': $($_.Exception.Message)"
        Write-SafeWarningLog $warningMsg
        return $null
    }
}

function Test-TargetVersionValidity {
    <#
    .SYNOPSIS
        Validates a target version against existing tags and semantic versioning rules.
     
    .DESCRIPTION
        Comprehensive validation of a target version including:
        - Semantic version format compliance
        - Comparison with existing versions
        - Conflict detection
        - Version progression logic
     
    .PARAMETER TargetVersion
        The target version to validate
     
    .PARAMETER ExistingTags
        Array of existing semantic version tags
     
    .PARAMETER Force
        Skip certain validations when forcing creation
     
    .OUTPUTS
        [PSCustomObject] Validation result with IsValid, ErrorMessage, and Warnings properties
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$TargetVersion,
        
        [Parameter()]
        [PSCustomObject[]]$ExistingTags = @(),
        
        [Parameter()]
        [switch]$Force
    )
    
    $result = [PSCustomObject]@{
        IsValid = $false
        ErrorMessage = ""
        Warnings = @()
    }
    
    try {
        Write-SafeInfoLog "Starting target version validation" -Context "TargetVersion: $TargetVersion`nExistingTagCount: $($ExistingTags.Count)`nForce: $Force"
        
        # Parse target version
        $targetObj = ConvertTo-SemanticVersionObject -TagName $TargetVersion
        if (-not $targetObj) {
            $errorMsg = "Target version '$TargetVersion' is not a valid semantic version."
            Write-SafeErrorLog $errorMsg
            $result.ErrorMessage = $errorMsg
            return $result
        }
        
        Write-SafeDebugLog "Target version parsed successfully" -Context "ParsedVersion: $($targetObj.Version)"
        
        # Check if version already exists
        $existingTag = $ExistingTags | Where-Object { $_.Tag -eq $TargetVersion }
        if ($existingTag -and -not $Force) {
            $result.ErrorMessage = "Version '$TargetVersion' already exists. Use -Force to overwrite."
            return $result
        }
        
        if ($ExistingTags.Count -gt 0) {
            # Get latest existing version
            $latestExisting = $ExistingTags | Sort-Object { $_.Version } -Descending | Select-Object -First 1
            
            # Check if target version is progression forward
            if ($targetObj.Version -le $latestExisting.Version) {
                if ($Force) {
                    $result.Warnings += "Target version '$TargetVersion' is not newer than latest existing version '$($latestExisting.Tag)'. Proceeding due to -Force."
                } else {
                    $result.ErrorMessage = "Target version '$TargetVersion' must be newer than latest existing version '$($latestExisting.Tag)'."
                    return $result
                }
            }
            
            # Check for large version jumps (potential typos)
            $majorJump = $targetObj.Major - $latestExisting.Major
            $minorJump = $targetObj.Minor - $latestExisting.Minor
            
            if ($majorJump -gt 1) {
                $result.Warnings += "Large major version jump detected ($($latestExisting.Major) → $($targetObj.Major)). Verify this is intentional."
            } elseif ($majorJump -eq 0 -and $minorJump -gt 5) {
                $result.Warnings += "Large minor version jump detected ($($latestExisting.Minor) → $($targetObj.Minor)). Verify this is intentional."
            }
        }
        
        $result.IsValid = $true
        return $result
        
    }
    catch {
        $result.ErrorMessage = "Version validation failed: $($_.Exception.Message)"
        return $result
    }
}

function Get-SmartTagStrategy {
    <#
    .SYNOPSIS
        Calculates the smart tag strategy for a target version release.
     
    .DESCRIPTION
        Determines which smart tags to create, update, or preserve based on the target version
        and existing tag structure. Implements the core logic for moving vs. static tags.
         
        Key Logic:
        - Patch updates: Current major/minor smart tags move with the new version
        - Minor updates: Major smart tag moves, previous minor smart tag becomes static
        - Major updates: All previous smart tags become static, new smart tags created
     
    .PARAMETER TargetVersion
        The version being released
     
    .PARAMETER ExistingTags
        Current semantic version tags in the repository
     
    .OUTPUTS
        [PSCustomObject] Strategy object with arrays of tags to create, update, and preserve
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$TargetVersion,
        
        [Parameter()]
        [PSCustomObject[]]$ExistingTags = @()
    )
    
    $strategy = [PSCustomObject]@{
        SmartTagsToCreate = @()
        MovingTagsToUpdate = @()
        TagsToBecomeStatic = @()
    }
    
    try {
        $targetObj = ConvertTo-SemanticVersionObject -TagName $TargetVersion
        if (-not $targetObj) {
            throw "Invalid target version: $TargetVersion"
        }
        
        # Always update 'latest' tag
        $strategy.MovingTagsToUpdate += [PSCustomObject]@{
            Name = "latest"
            NewTarget = $TargetVersion
        }
        
        if ($ExistingTags.Count -eq 0) {
            # First release - create all smart tags
            $strategy.SmartTagsToCreate += [PSCustomObject]@{
                Name = "v$($targetObj.Major)"
                Target = $TargetVersion
            }
            
            if ($targetObj.Minor -gt 0 -or $targetObj.Patch -gt 0) {
                $strategy.SmartTagsToCreate += [PSCustomObject]@{
                    Name = "v$($targetObj.Major).$($targetObj.Minor)"
                    Target = $TargetVersion
                }
            }
        } else {
            # Get latest version for comparison
            $latestExisting = $ExistingTags | Sort-Object { $_.Version } -Descending | Select-Object -First 1
            
            $majorChange = $targetObj.Major -ne $latestExisting.Major
            $minorChange = $targetObj.Minor -ne $latestExisting.Minor
            
            if ($majorChange) {
                # Major version change - preserve old smart tags, create new ones
                $oldMajorTags = $ExistingTags | Where-Object { 
                    $_.Tag -match "^v$($latestExisting.Major)(\.\d+)?$" 
                }
                $strategy.TagsToBecomeStatic += $oldMajorTags
                
                # Create new major smart tags
                $strategy.SmartTagsToCreate += [PSCustomObject]@{
                    Name = "v$($targetObj.Major)"
                    Target = $TargetVersion
                }
                
                if ($targetObj.Minor -gt 0 -or $targetObj.Patch -gt 0) {
                    $strategy.SmartTagsToCreate += [PSCustomObject]@{
                        Name = "v$($targetObj.Major).$($targetObj.Minor)"
                        Target = $TargetVersion
                    }
                }
                
            } elseif ($minorChange) {
                # Minor version change - major tag moves, old minor becomes static
                $majorTag = "v$($targetObj.Major)"
                $oldMinorTag = "v$($latestExisting.Major).$($latestExisting.Minor)"
                
                # Major tag moves
                $strategy.SmartTagsToCreate += [PSCustomObject]@{
                    Name = $majorTag
                    Target = $TargetVersion
                }
                
                # Old minor tag becomes static
                $existingMinorTag = $ExistingTags | Where-Object { $_.Tag -eq $oldMinorTag }
                if ($existingMinorTag) {
                    $strategy.TagsToBecomeStatic += $existingMinorTag
                }
                
                # Create new minor tag
                $strategy.SmartTagsToCreate += [PSCustomObject]@{
                    Name = "v$($targetObj.Major).$($targetObj.Minor)"
                    Target = $TargetVersion
                }
                
            } else {
                # Patch version change - both major and minor tags move
                $majorTag = "v$($targetObj.Major)"
                $minorTag = "v$($targetObj.Major).$($targetObj.Minor)"
                
                $strategy.SmartTagsToCreate += [PSCustomObject]@{
                    Name = $majorTag
                    Target = $TargetVersion
                }
                
                $strategy.SmartTagsToCreate += [PSCustomObject]@{
                    Name = $minorTag
                    Target = $TargetVersion
                }
            }
        }
        
        return $strategy
        
    }
    catch {
        Write-Error "Failed to calculate smart tag strategy: $($_.Exception.Message)"
        throw
    }
}

function Compare-SemanticVersions {
    <#
    .SYNOPSIS
        Compares two semantic versions and returns the relationship.
     
    .DESCRIPTION
        Performs semantic version comparison following semver.org rules.
        Handles pre-release versions, build metadata, and edge cases.
     
    .PARAMETER Version1
        First version to compare
     
    .PARAMETER Version2
        Second version to compare
     
    .OUTPUTS
        [int] -1 if Version1 < Version2, 0 if equal, 1 if Version1 > Version2
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Version1,
        
        [Parameter(Mandatory)]
        [string]$Version2
    )
    
    try {
        $v1Obj = ConvertTo-SemanticVersionObject -TagName $Version1
        $v2Obj = ConvertTo-SemanticVersionObject -TagName $Version2
        
        if (-not $v1Obj -or -not $v2Obj) {
            throw "Invalid semantic version format in comparison."
        }
        
        return $v1Obj.Version.CompareTo($v2Obj.Version)
        
    }
    catch {
        Write-Error "Failed to compare semantic versions '$Version1' and '$Version2': $($_.Exception.Message)"
        throw
    }
}

function Test-IsValidSemanticVersion {
    <#
    .SYNOPSIS
        Tests if a string represents a valid semantic version.
     
    .DESCRIPTION
        Validates semantic version format according to semver.org specification.
        Supports v-prefixed versions, pre-release labels, and build metadata.
     
    .PARAMETER Version
        Version string to validate
     
    .PARAMETER AllowVPrefix
        Allow 'v' prefix (default: true)
     
    .PARAMETER AllowPreRelease
        Allow pre-release versions (default: true)
     
    .OUTPUTS
        [bool] True if version is valid, false otherwise
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Version,
        
        [Parameter()]
        [bool]$AllowVPrefix = $true,
        
        [Parameter()]
        [bool]$AllowPreRelease = $true
    )
    
    try {
        # Check v-prefix restriction
        if (-not $AllowVPrefix -and $Version.StartsWith('v')) {
            return $false
        }
        
        $versionObj = ConvertTo-SemanticVersionObject -TagName $Version
        if (-not $versionObj) {
            return $false
        }
        
        # Check pre-release restriction
        if (-not $AllowPreRelease -and $versionObj.IsPreRelease) {
            return $false
        }
        
        return $true
        
    }
    catch {
        return $false
    }
}