<#
.SYNOPSIS
    Wertet ein PSProfiler-Ergebnis (Measure-Script, Typ ScriptLineMeasurement) aus.

.DESCRIPTION
    Angepasst an die tatsaechliche PSProfiler-Ausgabe:
      LineNo        [int]       - Zeilennummer
      ExecutionTime [TimeSpan]  - Gesamtzeit dieser Zeile (Summe aller Treffer)
      TimeLine.TimeSpans [List] - Einzelmessungen; deren Anzahl = HitCount
      Line          [string]    - Quellcode-Text der Zeile
      SourceScript  [string]    - Pfad der Quelldatei
      Top           [bool]

    WICHTIG: Measure-Script instrumentiert NUR die uebergebene Datei. Zur Laufzeit
    dot-gesourcte Dateien (Utility/Base/Work/R125) erscheinen NICHT mit eigenen
    Zeilen - ihre Zeit wird der jeweiligen Aufrufzeile im Rahmenskript zugeschlagen.
    Fuer Hotspots INNERHALB von R125 muss R125 separat gemessen werden
    (siehe Measure-R125Direct.ps1).

.PARAMETER XmlPath
    Pfad zur Ergebnis-XML (Export-Clixml).

.PARAMETER Result
    Alternativ das Measure-Script-Ergebnisobjekt direkt.

.PARAMETER Top
    Anzahl Top-Zeilen. Default 25.

.PARAMETER FocusFile
    Optionales Dateinamen-Muster, auf das das Detail eingeschraenkt wird
    (z.B. '*Work-R125.ps1', wenn R125 direkt gemessen wurde).

.EXAMPLE
    ./Analyze-ProfileResult.ps1 -XmlPath ./profile-R125-20260624-195753-run1.xml
#>
[CmdletBinding(DefaultParameterSetName = 'FromXml')]
param(
    [Parameter(ParameterSetName = 'FromXml', Mandatory = $true, Position = 0)]
    [string]$XmlPath,

    [Parameter(ParameterSetName = 'FromObject', Mandatory = $true)]
    [object]$Result,

    [int]$Top = 25,

    [string]$FocusFile = $null,

    # Wird von den gemessenen Zeilennummern abgezogen, um auf die Original-
    # Zeilen der Zieldatei zurueckzurechnen (z.B. wenn ein Mess-Header vorangestellt
    # wurde). Wird automatisch aus der .meta.xml-Sidecar gelesen, falls vorhanden.
    [int]$LineOffset = 0
)

$ErrorActionPreference = "Stop"

if ($PSCmdlet.ParameterSetName -eq 'FromXml') {
    if (-not (Test-Path $XmlPath)) { throw "XML nicht gefunden: $XmlPath" }
    $Result = Import-Clixml -Path $XmlPath

    # Offset automatisch aus Sidecar lesen, falls nicht manuell gesetzt.
    if ($LineOffset -eq 0) {
        $metaPath = $XmlPath -replace '\.xml$', '.meta.xml'
        if (Test-Path $metaPath) {
            try {
                $meta = Import-Clixml -Path $metaPath
                if ($meta.HeaderOffset) {
                    $LineOffset = [int]$meta.HeaderOffset
                    Write-Host ("Sidecar gefunden: Zeilen-Offset {0} (Quelle: {1})" -f $LineOffset, $meta.SourceFile) -ForegroundColor DarkGray
                }
            } catch { }
        }
    }
}

# Flache Liste der ScriptLineMeasurement-Objekte
$rows = @($Result | Where-Object { $_.PSObject.Properties['LineNo'] })
if ($rows.Count -eq 0) {
    throw "Keine ScriptLineMeasurement-Objekte gefunden (Feld 'LineNo' fehlt)."
}

# --- Normalisierung --------------------------------------------------------
# ExecutionTime ist ein TimeSpan -> ms. HitCount = Anzahl Eintraege in
# TimeLine.TimeSpans (Fallback: 1, wenn ExecutionTime > 0).
function Get-HitCount {
    param($row)
    $tl = $row.PSObject.Properties['TimeLine']
    if ($tl -and $tl.Value) {
        $ts = $tl.Value.PSObject.Properties['TimeSpans']
        if ($ts -and $ts.Value) {
            try { return @($ts.Value).Count } catch { }
        }
    }
    if ($row.ExecutionTime -and $row.ExecutionTime.TotalMilliseconds -gt 0) { return 1 }
    return 0
}

$norm = foreach ($row in $rows) {
    $ms = 0.0
    if ($row.ExecutionTime -is [TimeSpan]) { $ms = $row.ExecutionTime.TotalMilliseconds }
    $rawLine = [int]$row.LineNo
    $origLine = $rawLine - $LineOffset
    [pscustomobject]@{
        File   = [string]$row.SourceScript
        Leaf   = if ($row.SourceScript) { Split-Path $row.SourceScript -Leaf } else { '<unknown>' }
        LineNo = $origLine          # auf Original-R125 zurueckgerechnet
        RawLine = $rawLine          # Zeile in der gemessenen (temporaeren) Datei
        IsHeader = ($origLine -le 0)
        Ms     = [double]$ms
        Hits   = [int](Get-HitCount $row)
        Code   = ([string]$row.Line).Trim()
        Top    = [bool]$row.Top
    }
}

# Header-Zeilen (Mess-Geruest) aus der Hotspot-Betrachtung nehmen, sofern ein
# Offset aktiv ist - sie gehoeren nicht zum Funktionskoerper.
if ($LineOffset -gt 0) {
    $norm = $norm | Where-Object { -not $_.IsHeader }
}

if ($FocusFile) {
    $norm = $norm | Where-Object { $_.Leaf -like $FocusFile -or $_.File -like $FocusFile }
}

$totalMs = ($norm | Measure-Object Ms -Sum).Sum

function Fmt-Dur {
    param([double]$ms)
    if ($ms -ge 60000) { return ('{0:n1} min' -f ($ms / 60000)) }
    if ($ms -ge 1000)  { return ('{0:n1} s'   -f ($ms / 1000)) }
    return ('{0:n1} ms' -f $ms)
}

# --- Uebersicht ------------------------------------------------------------
Write-Host ""
Write-Host "================ UEBERSICHT ================" -ForegroundColor Cyan
Write-Host ("Gemessene Zeilen : {0}" -f $norm.Count)
Write-Host ("Gesamtzeit       : {0}  ({1:n0} ms)" -f (Fmt-Dur $totalMs), $totalMs)
$files = @($norm | Select-Object -ExpandProperty Leaf -Unique)
Write-Host ("Quelldateien     : {0}" -f ($files -join ', '))
if ($files.Count -eq 1) {
    Write-Host ""
    Write-Host "HINWEIS: Nur eine Quelldatei instrumentiert. Dot-gesourcte Dateien" -ForegroundColor Yellow
    Write-Host "         (R125 etc.) sind hier NICHT zeilenweise enthalten - ihre Zeit" -ForegroundColor Yellow
    Write-Host "         steckt in den Aufrufzeilen. Fuer R125-interne Hotspots R125" -ForegroundColor Yellow
    Write-Host "         direkt messen (Measure-R125Direct.ps1)." -ForegroundColor Yellow
}

# --- Ranking pro Datei (nur sinnvoll bei mehreren Dateien) -----------------
if ($files.Count -gt 1) {
    Write-Host ""
    Write-Host "================ RANKING PRO DATEI ================" -ForegroundColor Cyan
    $norm | Group-Object Leaf | ForEach-Object {
        $sum = ($_.Group | Measure-Object Ms -Sum).Sum
        [pscustomobject]@{
            Datei   = $_.Name
            Zeit    = Fmt-Dur $sum
            Ms      = [math]::Round($sum, 1)
            Anteil  = ('{0:n1}%' -f ($(if ($totalMs) { 100 * $sum / $totalMs } else { 0 })))
            Zeilen  = $_.Count
        }
    } | Sort-Object Ms -Descending | Format-Table Datei, Zeit, Anteil, Zeilen -AutoSize | Out-Host
}

# --- Top-Hotspots nach Zeit ------------------------------------------------
Write-Host ""
Write-Host "================ TOP $Top NACH ZEIT ================" -ForegroundColor Cyan
$norm | Sort-Object Ms -Descending | Select-Object -First $Top |
    ForEach-Object {
        [pscustomobject]@{
            Datei  = $_.Leaf
            Zeile  = $_.LineNo
            Zeit   = Fmt-Dur $_.Ms
            Anteil = ('{0:n1}%' -f ($(if ($totalMs) { 100 * $_.Ms / $totalMs } else { 0 })))
            Hits   = $_.Hits
            Code   = if ($_.Code.Length -gt 75) { $_.Code.Substring(0, 75) + '...' } else { $_.Code }
        }
    } | Format-Table -AutoSize | Out-Host

# --- Top-Hotspots nach HitCount (Wiederholungs-Hotspots) -------------------
$haveHits = ($norm | Where-Object { $_.Hits -gt 1 } | Measure-Object).Count -gt 0
if ($haveHits) {
    Write-Host ""
    Write-Host "================ TOP $Top NACH HITCOUNT ================" -ForegroundColor Cyan
    Write-Host "(relevant fuer die grosse if/elseif-StartsWith-Kette in R125)" -ForegroundColor DarkGray
    $norm | Sort-Object Hits -Descending | Select-Object -First $Top |
        ForEach-Object {
            [pscustomobject]@{
                Datei = $_.Leaf
                Zeile = $_.LineNo
                Hits  = $_.Hits
                Zeit  = Fmt-Dur $_.Ms
                Code  = if ($_.Code.Length -gt 75) { $_.Code.Substring(0, 75) + '...' } else { $_.Code }
            }
        } | Format-Table -AutoSize | Out-Host
}
else {
    Write-Host ""
    Write-Host "HINWEIS: Alle Zeilen haben HitCount<=1 (TimeSpans nicht pro Treffer" -ForegroundColor DarkGray
    Write-Host "         erfasst). HitCount-Ranking daher uebersprungen." -ForegroundColor DarkGray
}
