Private/Helpers.ps1

# Built-in standard profiles and helpers for CodeCompass.GitHub

#region Standard Profiles

$script:Standards = @{
    core    = @{
        branch_protection = @{
            require                 = $true
            min_reviewers           = 1
            dismiss_stale_reviews   = $true
            require_status_checks   = $true
            require_up_to_date      = $true
            enforce_admins          = $false
            restrict_force_pushes   = $true
            restrict_deletions      = $true
            require_linear_history  = $false
            require_signed_commits  = $false
        }
        security          = @{
            dependabot_security  = $true
            dependabot_versions  = $true
            secret_scanning      = $true
            push_protection      = $true
            vulnerability_alerts = $true
        }
        settings          = @{
            require_description      = $true
            require_topics           = $true
            issues_enabled           = $true
            default_branch_name      = 'main'
            delete_branch_on_merge   = $true
            allow_squash_merge       = $true
            wiki_disabled            = $true
        }
        collaboration     = @{
            max_stale_branch_days = 90
            max_open_prs          = 10
        }
    }
    active  = @{
        branch_protection = @{
            require                 = $true
            min_reviewers           = 1
            dismiss_stale_reviews   = $false
            require_status_checks   = $false
            require_up_to_date      = $false
            enforce_admins          = $false
            restrict_force_pushes   = $true
            restrict_deletions      = $true
            require_linear_history  = $false
            require_signed_commits  = $false
        }
        security          = @{
            dependabot_security  = $true
            dependabot_versions  = $false
            secret_scanning      = $true
            push_protection      = $false
            vulnerability_alerts = $true
        }
        settings          = @{
            require_description      = $true
            require_topics           = $true
            issues_enabled           = $true
            default_branch_name      = $null  # Don't enforce
            delete_branch_on_merge   = $true
            allow_squash_merge       = $false # Don't enforce
            wiki_disabled            = $false # Don't enforce
        }
        collaboration     = @{
            max_stale_branch_days = 90
            max_open_prs          = 10
        }
    }
    minimal = @{
        branch_protection = @{
            require                 = $false
            min_reviewers           = 0
            dismiss_stale_reviews   = $false
            require_status_checks   = $false
            require_up_to_date      = $false
            enforce_admins          = $false
            restrict_force_pushes   = $false
            restrict_deletions      = $false
            require_linear_history  = $false
            require_signed_commits  = $false
        }
        security          = @{
            dependabot_security  = $false
            dependabot_versions  = $false
            secret_scanning      = $false
            push_protection      = $false
            vulnerability_alerts = $true
        }
        settings          = @{
            require_description      = $true
            require_topics           = $false
            issues_enabled           = $false
            default_branch_name      = $null
            delete_branch_on_merge   = $false
            allow_squash_merge       = $false
            wiki_disabled            = $false
        }
        collaboration     = @{
            max_stale_branch_days = $null
            max_open_prs          = $null
        }
    }
}

#endregion

#region API Helpers

function Resolve-CCToken {
    <#
    .SYNOPSIS
        Resolves a GitHub token from parameter, environment, or gh CLI.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter()]
        [string]$Token
    )

    if ($Token) { return $Token }
    if ($env:GITHUB_TOKEN) { return $env:GITHUB_TOKEN }
    if ($env:GH_TOKEN) { return $env:GH_TOKEN }

    # Try gh CLI
    try {
        $ghToken = gh auth token 2>$null
        if ($LASTEXITCODE -eq 0 -and $ghToken) {
            return $ghToken.Trim()
        }
    }
    catch { }

    return $null
}

function Invoke-CCGitHubApi {
    <#
    .SYNOPSIS
        Makes an authenticated GitHub API call with error handling.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Endpoint,

        [Parameter(Mandatory)]
        [string]$Token,

        [Parameter()]
        [ValidateSet('Get', 'Put', 'Post', 'Patch', 'Delete')]
        [string]$Method = 'Get',

        [Parameter()]
        [object]$Body,

        [Parameter()]
        [switch]$AllowNotFound
    )

    $uri = if ($Endpoint -match '^https://') { $Endpoint } else { "https://api.github.com/$Endpoint" }

    $headers = @{
        'Authorization'        = "Bearer $Token"
        'Accept'               = 'application/vnd.github+json'
        'X-GitHub-Api-Version' = '2022-11-28'
    }

    $params = @{
        Uri     = $uri
        Headers = $headers
        Method  = $Method
    }

    if ($Body) {
        $params['Body'] = ($Body | ConvertTo-Json -Depth 10)
        $params['ContentType'] = 'application/json'
    }

    try {
        return Invoke-RestMethod @params
    }
    catch {
        $statusCode = $_.Exception.Response.StatusCode.value__
        if ($AllowNotFound -and $statusCode -eq 404) {
            return $null
        }
        if ($statusCode -eq 403 -and $_.Exception.Response.Headers['X-RateLimit-Remaining'] -eq '0') {
            $reset = $_.Exception.Response.Headers['X-RateLimit-Reset']
            $resetTime = [DateTimeOffset]::FromUnixTimeSeconds([int]$reset).LocalDateTime
            Write-Warning "GitHub API rate limit exceeded. Resets at $resetTime"
        }
        throw
    }
}

function Resolve-CCRepository {
    <#
    .SYNOPSIS
        Resolves repository owner/name from parameter or git remote.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter()]
        [string]$Repository,

        [Parameter()]
        [string]$Path = $PWD.Path
    )

    if ($Repository) { return $Repository }

    try {
        Push-Location $Path
        $remote = git remote get-url origin 2>$null
        Pop-Location

        if ($remote -match 'github\.com[:/](.+?)(?:\.git)?$') {
            return $Matches[1]
        }
    }
    catch { }

    return $null
}

function Get-CCStandardConfig {
    <#
    .SYNOPSIS
        Loads and merges standard configuration with per-repo overrides.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)]
        [string]$Standard,

        [Parameter()]
        [string]$ConfigPath
    )

    if (-not $script:Standards.ContainsKey($Standard)) {
        throw "Unknown standard '$Standard'. Available: $($script:Standards.Keys -join ', ')"
    }

    $config = $script:Standards[$Standard]

    # Load per-repo override if available
    if ($ConfigPath -and (Test-Path $ConfigPath)) {
        try {
            # Support YAML-like or JSON config
            $content = Get-Content $ConfigPath -Raw
            if ($ConfigPath -match '\.json$') {
                $override = $content | ConvertFrom-Json -AsHashtable
            }
            else {
                # Simple YAML parser for our config subset
                $override = ConvertFrom-CCYaml -Content $content
            }

            if ($override -and $override['github']) {
                $config = Merge-CCConfig -Base $config -Override $override['github']
            }
        }
        catch {
            Write-Warning "Failed to load config override from $ConfigPath : $_"
        }
    }

    return $config
}

function Merge-CCConfig {
    <#
    .SYNOPSIS
        Deep merges two config hashtables (override wins).
    #>

    param(
        [hashtable]$Base,
        [hashtable]$Override
    )

    $result = @{}
    foreach ($key in $Base.Keys) {
        if ($Base[$key] -is [hashtable] -and $Override.ContainsKey($key) -and $Override[$key] -is [hashtable]) {
            $result[$key] = Merge-CCConfig -Base $Base[$key] -Override $Override[$key]
        }
        elseif ($Override.ContainsKey($key)) {
            $result[$key] = $Override[$key]
        }
        else {
            $result[$key] = $Base[$key]
        }
    }
    # Add keys from override that aren't in base
    foreach ($key in $Override.Keys) {
        if (-not $result.ContainsKey($key)) {
            $result[$key] = $Override[$key]
        }
    }
    return $result
}

function ConvertFrom-CCYaml {
    <#
    .SYNOPSIS
        Minimal YAML parser for CodeCompass config files (key: value, nested with indentation).
    #>

    param([string]$Content)

    $result = @{}
    $stack = @(@{ Indent = -1; Dict = $result })

    foreach ($line in $Content -split "`n") {
        if ($line -match '^\s*#' -or $line -match '^\s*$') { continue }
        if ($line -match '^(\s*)(\S+):\s*$') {
            # Section header (no value)
            $indent = $Matches[1].Length
            $key = $Matches[2]
            $newDict = @{}

            while ($stack.Count -gt 1 -and $stack[-1].Indent -ge $indent) {
                $stack = $stack[0..($stack.Count - 2)]
            }
            $stack[-1].Dict[$key] = $newDict
            $stack += @{ Indent = $indent; Dict = $newDict }
        }
        elseif ($line -match '^(\s*)(\S+):\s+(.+)$') {
            # Key: value
            $indent = $Matches[1].Length
            $key = $Matches[2]
            $rawValue = $Matches[3].Trim()

            while ($stack.Count -gt 1 -and $stack[-1].Indent -ge $indent) {
                $stack = $stack[0..($stack.Count - 2)]
            }

            # Type coercion
            $value = switch -Regex ($rawValue) {
                '^true$' { $true }
                '^false$' { $false }
                '^\d+$' { [int]$rawValue }
                '^null$' { $null }
                default { $rawValue.Trim('"', "'") }
            }
            $stack[-1].Dict[$key] = $value
        }
    }

    return $result
}

function New-CCCheckResult {
    <#
    .SYNOPSIS
        Creates a standardized check result object.
    #>

    param(
        [string]$CheckId,
        [string]$Category,
        [string]$Item,
        [ValidateSet('Pass', 'Fail', 'Warning', 'Skipped')]
        [string]$Status,
        [ValidateSet('Error', 'Warning', 'Info')]
        [string]$Severity = 'Error',
        [string]$Message,
        [bool]$FixAvailable = $false,
        [string]$FixId,
        [object]$Current,
        [object]$Expected
    )

    [PSCustomObject]@{
        CheckId      = $CheckId
        Category     = $Category
        Item         = $Item
        Status       = $Status
        Severity     = $Severity
        Message      = $Message
        FixAvailable = $FixAvailable
        FixId        = $FixId
        Current      = $Current
        Expected     = $Expected
    }
}

#endregion