src/GitOperations.ps1

# Git Operations - Core Git functionality for tag management

function Invoke-GitValidation {
    <#
    .SYNOPSIS
        Validates that a directory contains a valid Git repository with necessary prerequisites.
     
    .DESCRIPTION
        Comprehensive validation of Git repository state including:
        - Git CLI availability
        - Valid Git repository structure
        - At least one commit exists
        - Working directory is clean (optional)
     
    .PARAMETER RepositoryPath
        Path to the Git repository to validate
     
    .PARAMETER RequireCleanWorkingDirectory
        Requires that there are no uncommitted changes
     
    .OUTPUTS
        [PSCustomObject] Validation result with IsValid and ErrorMessage properties
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$RepositoryPath,
        
        [Parameter()]
        [switch]$RequireCleanWorkingDirectory
    )
    
    $result = [PSCustomObject]@{
        IsValid = $false
        ErrorMessage = ""
        Warnings = @()
    }
    
    try {
        Push-Location $RepositoryPath
        Write-SafeInfoLog "Starting Git repository validation" -Context "RepositoryPath: $RepositoryPath"
        
        # Check if git command is available
        git --version 2>$null | Out-Null
        if ($LASTEXITCODE -ne 0) {
            $errorMsg = "Git CLI is not available in PATH. Please install Git."
            Write-SafeErrorLog $errorMsg
            $result.ErrorMessage = $errorMsg
            return $result
        }
        
        Write-SafeDebugLog "Git CLI is available"
        
        # Check if we're in a Git repository
        git rev-parse --git-dir 2>$null | Out-Null
        if ($LASTEXITCODE -ne 0) {
            $errorMsg = "Directory is not a Git repository."
            Write-SafeErrorLog $errorMsg -Context "Path: $RepositoryPath"
            $result.ErrorMessage = $errorMsg
            return $result
        }
        
        Write-SafeDebugLog "Git repository structure validated"
        
        # Check if there's at least one commit
        git rev-parse HEAD 2>$null | Out-Null
        if ($LASTEXITCODE -ne 0) {
            $errorMsg = "Repository has no commits. At least one commit is required."
            Write-SafeErrorLog $errorMsg
            $result.ErrorMessage = $errorMsg
            return $result
        }
        
        Write-SafeDebugLog "Repository has commits available"
        
        # Check working directory status if required
        if ($RequireCleanWorkingDirectory) {
            $status = git status --porcelain 2>$null
            if ($status) {
                $errorMsg = "Working directory has uncommitted changes. Commit or stash changes before creating tags."
                Write-SafeWarningLog $errorMsg -Context "UncommittedFiles: $($status -join ', ')"
                $result.ErrorMessage = $errorMsg
                return $result
            }
            Write-SafeDebugLog "Working directory is clean"
        }
        
        $result.IsValid = $true
        Write-SafeInfoLog "Git repository validation completed successfully"
        return $result
        
    }
    catch {
        $errorMsg = "Git validation failed: $($_.Exception.Message)"
        Write-SafeErrorLog $errorMsg -Context "RepositoryPath: $RepositoryPath"
        $result.ErrorMessage = $errorMsg
        return $result
    }
    finally {
        Pop-Location
    }
}

function Get-ExistingSemanticTags {
    <#
    .SYNOPSIS
        Retrieves all existing semantic version tags from a Git repository.
     
    .DESCRIPTION
        Scans the repository for all tags matching semantic version patterns
        and returns them as structured objects for further processing.
     
    .PARAMETER RepositoryPath
        Path to the Git repository
     
    .OUTPUTS
        [PSCustomObject[]] Array of tag objects with Name, Version, and metadata
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$RepositoryPath
    )
    
    try {
        Push-Location $RepositoryPath
        Write-SafeInfoLog "Retrieving existing semantic version tags" -Context "RepositoryPath: $RepositoryPath"
        
        $allTags = git tag -l 2>$null
        if ($LASTEXITCODE -ne 0 -or -not $allTags) {
            Write-SafeInfoLog "No tags found in repository"
            return @()
        }
        
        Write-SafeDebugLog "Retrieved all tags from repository" -Context "TotalTagCount: $($allTags.Count)"
        
        $semanticPattern = '^v?\d+\.\d+\.\d+(-[a-zA-Z0-9\-\.]+)?(\+[a-zA-Z0-9\-\.]+)?$'
        $semanticTags = $allTags | Where-Object { $_ -match $semanticPattern }
        
        Write-SafeInfoLog "Filtered semantic version tags" -Context "SemanticTagCount: $($semanticTags.Count)`nTotalTagCount: $($allTags.Count)"
        
        # Parse and filter out null values (failed parsing)
        $tagObjects = $semanticTags | ForEach-Object {
            $parsed = ConvertTo-SemanticVersionObject -TagName $_
            if ($parsed) {
                Write-SafeDebugLog "Successfully parsed semantic tag" -Context "Tag: $_`nVersion: $($parsed.Version)"
                $parsed
            }
            # Implicitly: if $parsed is $null, nothing is added to the array
        } | Where-Object { $_ -ne $null }  # Extra safety: filter out any nulls
        
        $sortedTags = $tagObjects | Sort-Object { $_.Version } -Descending
        Write-SafeInfoLog "Semantic tags retrieved and sorted successfully" -Context "FinalCount: $($sortedTags.Count)"
        
        return $sortedTags
        
    }
    catch {
        $errorMsg = "Failed to get existing semantic tags: $($_.Exception.Message)"
        Write-SafeErrorLog $errorMsg -Context "RepositoryPath: $RepositoryPath"
        return @()
    }
    finally {
        Pop-Location
    }
}

function New-GitTag {
    <#
    .SYNOPSIS
        Creates a new Git tag with proper error handling and validation.
     
    .DESCRIPTION
        Creates a Git tag with comprehensive error handling, conflict detection,
        and support for both lightweight and annotated tags.
     
    .PARAMETER TagName
        Name of the tag to create
     
    .PARAMETER CommitSha
        Commit SHA to tag (defaults to HEAD)
     
    .PARAMETER TargetRef
        Alternative way to specify target (e.g., another tag name)
     
    .PARAMETER Message
        Optional tag message (creates annotated tag)
     
    .PARAMETER Force
        Force creation, overwriting existing tags
     
    .PARAMETER RepositoryPath
        Path to Git repository
     
    .OUTPUTS
        [PSCustomObject] Result with Success, ErrorMessage, and TagName properties
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$TagName,
        
        [Parameter()]
        [string]$CommitSha,
        
        [Parameter()]
        [string]$TargetRef,
        
        [Parameter()]
        [string]$Message,
        
        [Parameter()]
        [switch]$Force,
        
        [Parameter()]
        [string]$RepositoryPath = (Get-Location).Path
    )
    
    $result = [PSCustomObject]@{
        Success = $false
        ErrorMessage = ""
        TagName = $TagName
    }
    
    try {
        Push-Location $RepositoryPath
        
        # Determine target reference
        $target = if ($TargetRef) { $TargetRef } elseif ($CommitSha) { $CommitSha } else { "HEAD" }
        
        # Check if tag already exists
        $existingTag = git tag -l $TagName 2>$null
        if ($existingTag -and -not $Force) {
            $result.ErrorMessage = "Tag '$TagName' already exists. Use -Force to overwrite."
            return $result
        }
        
        # Delete existing tag if force is specified
        if ($existingTag -and $Force) {
            git tag -d $TagName 2>$null | Out-Null
            if ($LASTEXITCODE -ne 0) {
                $result.ErrorMessage = "Failed to delete existing tag '$TagName'."
                return $result
            }
        }
        
        # Create the tag
        if ($Message) {
            # Annotated tag
            git tag -a $TagName -m $Message $target 2>$null
        } else {
            # Lightweight tag
            git tag $TagName $target 2>$null
        }
        
        if ($LASTEXITCODE -eq 0) {
            $result.Success = $true
        } else {
            $result.ErrorMessage = "Git tag creation failed for '$TagName'."
        }
        
        return $result
        
    }
    catch {
        $result.ErrorMessage = "Exception creating tag '$TagName': $($_.Exception.Message)"
        return $result
    }
    finally {
        Pop-Location
    }
}

function Push-GitTags {
    <#
    .SYNOPSIS
        Pushes Git tags to remote repository with error handling.
     
    .DESCRIPTION
        Safely pushes tags to remote repository with comprehensive error handling
        and support for force pushing when necessary.
     
    .PARAMETER RepositoryPath
        Path to Git repository
     
    .PARAMETER TagNames
        Specific tags to push (defaults to all tags)
     
    .PARAMETER Force
        Force push tags (overwrite remote tags)
     
    .OUTPUTS
        [PSCustomObject] Result with Success and ErrorMessage properties
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$RepositoryPath = (Get-Location).Path,
        
        [Parameter()]
        [string[]]$TagNames,
        
        [Parameter()]
        [switch]$Force
    )
    
    $result = [PSCustomObject]@{
        Success = $false
        ErrorMessage = ""
    }
    
    try {
        Push-Location $RepositoryPath
        
        # Check if remote exists
        $remotes = git remote 2>$null
        if ($LASTEXITCODE -ne 0 -or -not $remotes) {
            $result.ErrorMessage = "No remote repository configured."
            return $result
        }
        
        if ($TagNames) {
            # Push specific tags
            foreach ($tag in $TagNames) {
                $pushArgs = @('push')
                if ($Force) { $pushArgs += '--force' }
                $pushArgs += @('origin', $tag)
                
                $gitError = & git @pushArgs 2>&1
                if ($LASTEXITCODE -ne 0) {
                    $result.ErrorMessage = "Failed to push tag '$tag' to remote. Git error: $gitError"
                    return $result
                }
            }
        } else {
            # Push all tags
            $pushArgs = @('push')
            if ($Force) { $pushArgs += '--force' }
            $pushArgs += @('origin', '--tags')
            
            $gitError = & git @pushArgs 2>&1
            if ($LASTEXITCODE -ne 0) {
                $result.ErrorMessage = "Failed to push tags to remote. Git error: $gitError"
                return $result
            }
        }
        
        $result.Success = $true
        return $result
        
    }
    catch {
        $result.ErrorMessage = "Exception pushing tags: $($_.Exception.Message)"
        return $result
    }
    finally {
        Pop-Location
    }
}

function Get-GitTagDetails {
    <#
    .SYNOPSIS
        Gets detailed information about a specific Git tag.
     
    .DESCRIPTION
        Retrieves comprehensive information about a Git tag including
        commit SHA, creation date, author, and tag message.
     
    .PARAMETER TagName
        Name of the tag to inspect
     
    .PARAMETER RepositoryPath
        Path to Git repository
     
    .OUTPUTS
        [PSCustomObject] Tag details including CommitSha, CreatedDate, Message, AuthorName, AuthorEmail
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$TagName,
        
        [Parameter()]
        [string]$RepositoryPath = (Get-Location).Path
    )
    
    try {
        Push-Location $RepositoryPath
        
        # Get commit SHA
        $commitSha = git rev-list -n 1 $TagName 2>$null
        if ($LASTEXITCODE -ne 0) {
            throw "Tag '$TagName' not found."
        }
        
        # Get tag object info (works for both lightweight and annotated tags)
        $tagInfo = git show --format="%H|%ai|%an|%ae|%s" -s $TagName 2>$null
        if ($LASTEXITCODE -eq 0 -and $tagInfo) {
            $parts = $tagInfo -split '\|'
            $createdDate = [DateTime]::Parse($parts[1])
            $authorName = $parts[2]
            $authorEmail = $parts[3]
            $message = $parts[4]
        } else {
            # Fallback for lightweight tags
            $commitInfo = git show --format="%H|%ai|%an|%ae|%s" -s $commitSha 2>$null
            $parts = $commitInfo -split '\|'
            $createdDate = [DateTime]::Parse($parts[1])
            $authorName = $parts[2]
            $authorEmail = $parts[3]
            $message = ""
        }
        
        return [PSCustomObject]@{
            TagName = $TagName
            CommitSha = $commitSha.Substring(0, 8)  # Short SHA
            CreatedDate = $createdDate
            Message = $message
            AuthorName = $authorName
            AuthorEmail = $authorEmail
        }
        
    }
    catch {
        Write-Error "Failed to get tag details for '$TagName': $($_.Exception.Message)"
        return [PSCustomObject]@{
            TagName = $TagName
            CommitSha = "unknown"
            CreatedDate = [DateTime]::MinValue
            Message = ""
            AuthorName = "unknown"
            AuthorEmail = "unknown"
        }
    }
    finally {
        Pop-Location
    }
}

<#
.SYNOPSIS
    Retrieves all tags from the repository that follow semantic versioning.
 
.DESCRIPTION
    This function fetches all tags from the specified Git repository, then filters them to return only those
    that conform to semantic versioning standards (e.g., v1.2.3, 2.0.0-alpha).
 
.PARAMETER RepositoryPath
    The path to the Git repository. Defaults to the current directory.
 
.EXAMPLE
    Get-SemanticVersionTags
    Returns all semantic version tags from the current repository.
 
.OUTPUTS
    [string[]] An array of tags that are valid semantic versions.
#>

function Get-SemanticVersionTags {
    param(
        [string]$RepositoryPath = (Get-Location).Path
    )
    
    Write-SafeDebugLog -Message "Getting all semantic version tags" -Additional @{ "RepositoryPath" = $RepositoryPath }
    
    try {
        Push-Location $RepositoryPath
        
        # Get all tags from repository
        $allTags = git tag -l 2>$null
        if ($LASTEXITCODE -ne 0 -or -not $allTags) {
            Write-SafeInfoLog -Message "No tags found in repository"
            return @()
        }
        
        # Filter for semantic version tags only (exclude smart tags like v0, v0.1, latest)
        $semanticTags = $allTags | Where-Object {
            # Must have full semantic version format (v0.0.0 with optional pre-release/build)
            $_ -match '^v?\d+\.\d+\.\d+' -and
            # Exclude smart tags (v0, v0.1) and moving tags (latest)
            $_ -notmatch '^(latest|v\d+|v\d+\.\d+)$'
        } | ForEach-Object {
            # Additional validation with Test-IsValidSemanticVersion
            if (Test-IsValidSemanticVersion -Version $_) {
                $_
            }
        } | Where-Object { $_ }  # Filter out any null values
        
        Write-SafeInfoLog -Message "Found $($semanticTags.Count) semantic version tags" -Additional @{ "TotalTags" = $allTags.Count }
        
        return $semanticTags
    }
    finally {
        Pop-Location
    }
}

<#
.SYNOPSIS
    Finds the latest (highest) semantic version tag in the repository.
 
.DESCRIPTION
    This function retrieves all semantic version tags and sorts them to find the highest, most recent version.
    It correctly handles pre-release and final versions to determine the latest tag.
 
.PARAMETER RepositoryPath
    The path to the Git repository. Defaults to the current directory.
 
.EXAMPLE
    Get-LatestSemanticTag
    Returns the latest semantic version tag (e.g., "v1.5.2").
 
.OUTPUTS
    [string] The latest semantic version tag found.
#>

function Get-LatestSemanticTag {
    param(
        [string]$RepositoryPath = (Get-Location).Path
    )
    
    Write-SafeDebugLog -Message "Getting the latest semantic tag" -Additional @{ "RepositoryPath" = $RepositoryPath }
    
    $semanticTags = Get-SemanticVersionTags -RepositoryPath $RepositoryPath
    
    if ($null -eq $semanticTags -or $semanticTags.Count -eq 0) {
        Write-SafeWarningLog -Message "No semantic version tags found in the repository."
        return $null
    }
    
    # Sort tags using semantic version comparison logic
    $sortedTags = $semanticTags | Sort-Object -Property @{ Expression = { [System.Version]($_.TrimStart('v')) } } -Descending
    
    $latestTag = $sortedTags[0]
    
    Write-SafeInfoLog -Message "Latest semantic tag found" -Additional @{ "LatestTag" = $latestTag }
    
    return $latestTag
}