Src/Private/Helpers.ps1

function Test-UserPrincipalName {
    <#
    .SYNOPSIS
    Validates that a string is a properly formatted User Principal Name.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory)]
        [string]$UserPrincipalName
    )
    return ($UserPrincipalName -match '^[^@\s]+@[^@\s]+\.[^@\s]+$')
}

function Write-TranscriptLog {
    <#
    .SYNOPSIS
    Writes a structured log entry to the host and optionally to a transcript file.
    Level values: DEBUG | INFO | SUCCESS | WARNING | ERROR
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string]$Message,

        [Parameter(Position = 1)]
        [ValidateSet('DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR')]
        [string]$Level = 'INFO',

        [Parameter(Position = 2)]
        [string]$Category = 'GENERAL'
    )

    $Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    $Entry     = "[$Timestamp] [$Level] [$Category] $Message"

    $null = switch ($Level) {
        'DEBUG'   { Write-PScriboMessage -Message $Entry  }
        'INFO'    { Write-Host " $Entry" }
        'SUCCESS' { Write-Host " $Entry" -ForegroundColor Green }
        'WARNING' { Write-PScriboMessage -IsWarning -Message $Entry }
        'ERROR'   { Write-Host " $Entry" -ForegroundColor Red }
    }

    if ($script:TranscriptLogPath) {
        try {
            Add-Content -Path $script:TranscriptLogPath -Value $Entry -ErrorAction SilentlyContinue | Out-Null
        } catch { }
    }
}

function Invoke-WithRetry {
    <#
    .SYNOPSIS
    Executes a ScriptBlock with configurable retry logic and exponential back-off.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [scriptblock]$ScriptBlock,

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

        [int]$MaxAttempts = 3,

        [int]$DelaySeconds = 5
    )

    $Attempt = 0
    $CurrentDelay = $DelaySeconds

    while ($Attempt -lt $MaxAttempts) {
        $Attempt++
        try {
            Write-TranscriptLog "Attempt ${Attempt}/${MaxAttempts}: $OperationName" 'DEBUG' 'RETRY'
            $null = (& $ScriptBlock)
            Write-TranscriptLog "Attempt ${Attempt}/${MaxAttempts} succeeded: $OperationName" 'DEBUG' 'RETRY'
            return
        } catch {
            if ($Attempt -lt $MaxAttempts) {
                Write-TranscriptLog "Attempt ${Attempt}/${MaxAttempts} failed for '$OperationName'. Retrying in ${CurrentDelay}s... Error: $($_.Exception.Message)" 'WARNING' 'RETRY'
                Start-Sleep -Seconds $CurrentDelay
                $CurrentDelay = $CurrentDelay * 2
            } else {
                Write-TranscriptLog "All ${MaxAttempts} attempts failed for '$OperationName'. Error: $($_.Exception.Message)" 'ERROR' 'RETRY'
                throw
            }
        }
    }
}

function Show-AbrDebugExecutionTime {
    <#
    .SYNOPSIS
    Tracks and displays execution time for report sections.
    #>

    [CmdletBinding()]
    param (
        [switch]$Start,
        [switch]$End,
        [string]$TitleMessage
    )

    if ($Start) {
        $script:SectionTimers[$TitleMessage] = [System.Diagnostics.Stopwatch]::StartNew()
        Write-TranscriptLog "Starting section: $TitleMessage" 'DEBUG' 'TIMER'
    }

    if ($End) {
        if ($script:SectionTimers -and $script:SectionTimers.ContainsKey($TitleMessage)) {
            $script:SectionTimers[$TitleMessage].Stop()
            $Elapsed = $script:SectionTimers[$TitleMessage].Elapsed.TotalSeconds
            Write-TranscriptLog "Completed section: $TitleMessage (${Elapsed}s)" 'DEBUG' 'TIMER'
            $null = $script:SectionTimers.Remove($TitleMessage)
        }
    }
}

function ConvertTo-HashToYN {
    <#
    .SYNOPSIS
    Converts boolean values in a hashtable/ordered-dictionary to Yes/No strings.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.IDictionary]$Hash
    )

    process {
        $Result = [ordered]@{}
        foreach ($Key in $Hash.Keys) {
            $Value = $Hash[$Key]
            if ($Value -is [bool]) {
                $Result[$Key] = if ($Value) { 'Yes' } else { 'No' }
            } elseif ($null -eq $Value -or $Value -eq '') {
                $Result[$Key] = '--'
            } else {
                $Result[$Key] = $Value
            }
        }
        return $Result
    }
}

function Export-ExoToExcel {
    <#
    .SYNOPSIS
    Exports one or more arrays of PSCustomObjects to a multi-sheet Excel workbook.
    .PARAMETER Sheets
    Ordered hashtable of SheetName => array of PSCustomObjects.
    .PARAMETER Path
    Full file path for the output .xlsx file.
    .PARAMETER TenantId
    Tenant name used in the title row of each sheet.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [System.Collections.Specialized.OrderedDictionary]$Sheets,

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

        [string]$TenantId = 'Unknown Tenant'
    )

    if (-not (Get-Module -ListAvailable -Name ImportExcel -ErrorAction SilentlyContinue)) {
        Write-Warning "ImportExcel module is not installed. Run 'Install-Module -Name ImportExcel -Force'."
        Write-Warning "Falling back to CSV export..."

        foreach ($SheetName in $Sheets.Keys) {
            $CsvPath = $Path -replace '\.xlsx$', "_$($SheetName -replace '\s','_').csv"
            $Sheets[$SheetName] | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8
            Write-Host " - CSV exported: $CsvPath" -ForegroundColor Yellow
        }
        return
    }

    Import-Module ImportExcel -ErrorAction SilentlyContinue

    $FirstSheet = $true
    foreach ($SheetName in $Sheets.Keys) {
        $Data = $Sheets[$SheetName]
        if (-not $Data -or @($Data).Count -eq 0) { continue }

        $ExcelParams = @{
            Path          = $Path
            WorksheetName = $SheetName
            AutoSize      = $true
            AutoFilter    = $true
            FreezeTopRow  = $true
            BoldTopRow    = $true
            PassThru      = $false
        }

        if ($FirstSheet) {
            $ExcelParams['ClearSheet'] = $true
        }

        try {
            $Data | Export-Excel @ExcelParams
            $FirstSheet = $false
            Write-Host " - Sheet exported: $SheetName ($(@($Data).Count) rows)" -ForegroundColor Cyan
        } catch {
            Write-Warning " - Failed to export sheet '$SheetName': $($_.Exception.Message)"
        }
    }

    if (Test-Path $Path) {
        Write-Host " - Excel workbook saved: $Path" -ForegroundColor Green
    }
}

function Write-AbrDebugLog {
    <#
    .SYNOPSIS
    Writes a structured debug log entry (module-scoped wrapper).
    #>

    param (
        [string]$Message,
        [ValidateSet('INFO', 'WARN', 'ERROR', 'DEBUG', 'SUCCESS')][string]$Level = 'INFO',
        [string]$Section = 'GENERAL'
    )
    $Entry = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff')] [$Level] [$Section] $Message"
    if ($script:DebugLogEnabled) {
        $null = $script:DebugLogEntries.Add($Entry)
    }
    $null = switch ($Level) {
        'ERROR'   { Write-Host " [DBG-ERR] $Entry" -ForegroundColor Red }
        'WARN'    { Write-Host " [DBG-WRN] $Entry" -ForegroundColor Yellow }
        'SUCCESS' { if ($script:DebugLogEnabled) { Write-Host " [DBG] $Entry" -ForegroundColor Green }; $null }
        default   { if ($script:DebugLogEnabled) { Write-Host " [DBG] $Entry" -ForegroundColor DarkGray }; $null }
    }
}

function Write-ExoError {
    param([string]$Section, [string]$Message)
    $Entry = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [ERROR] [$Section] $Message"
    $null = $script:ErrorLog.Add($Entry)
    Write-Host " [ERR] $Entry" -ForegroundColor Red
    if ($script:ErrorLogPath) {
        try { Add-Content -Path $script:ErrorLogPath -Value $Entry -ErrorAction SilentlyContinue } catch {}
    }
}