<#
.SYNOPSIS
    Liest den SCCM/MECM-Deployment-Status der letzten Anwendungen auf einem entfernten Client aus.
.DESCRIPTION
    Das Skript greift ueber die Admin-Freigabe C$ auf die CCM-Logs zu und fragt
    zu Beginn immer nach Benutzername und Passwort.

    Es zeigt standardmaessig die letzten Anwendungen mit Deployment-Aktivitaet an.
    Mit -DebugDetails werden zusaetzliche Pruefschritte ausgegeben, damit man
    erkennen kann, wo Log-Zugriff, App-Erkennung oder Regex-Parsing fehlschlaegt.
.PARAMETER ComputerName
    Name oder IP des Ziel-Clients.
.PARAMETER CCMPath
    Pfad zu CCM auf dem Ziel (Default C:\Windows\CCM).
.PARAMETER Last
    Anzahl der letzten Anwendungen, die angezeigt werden sollen (Default: 5).
.PARAMETER Detailed
    Schaltet zusaetzliche Roh-Logauszuege ein.
.PARAMETER DebugDetails
    Schaltet Debug-Ausgaben zu Zugriff, Logs, Regex und Auswahl ein.
.EXAMPLE
    .\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01
.EXAMPLE
    .\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 -Last 10 -Detailed -DebugDetails
#>

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

    [string]$CCMPath = "C:\\Windows\\CCM",

    [int]$Last = 5,

    [switch]$Detailed,

    [switch]$DebugDetails
)

#region Nachschlagetabellen ---------------------------------------------------

$EvalStates = @{
    0  = 'Kein Status (None)';            1  = 'Verfuegbar (Available)'
    2  = 'Uebermittelt (Submitted)';      3  = 'Erkennung laeuft (Detecting)'
    4  = 'Vor-Download (PreDownload)';    5  = 'Download laeuft (Downloading)'
    6  = 'Wartet auf Installation';        7  = 'Installation laeuft (Installing)'
    8  = 'Neustart ausstehend';            9  = 'Wird ausgefuehrt (Running)'
    10 = 'Wiederholung (Retrying)';        11 = 'Wartet auf Servicefenster'
    12 = 'Wartet auf Benutzersitzung';     13 = 'Wartet auf Benutzer-Abmeldung'
    14 = 'Wartet auf Benutzer-Anmeldung';  15 = 'Wartet (ADU)'
    16 = 'Wartet (ADU)';                   17 = 'Wartet auf Orchestrierung'
    18 = 'Wartet auf Bereitstellung';      19 = 'Wartet auf Verbindung'
    20 = 'Reserviert';                     21 = 'Wartet (Energie/Modus)'
    22 = 'Wartet auf Strom'
}

$InstallStates = @{
    0 = 'Nicht installiert'
    1 = 'Fehler'
    2 = 'Erfolgreich'
    3 = 'In Bearbeitung'
    4 = 'Verfuegbar'
    5 = 'Erfolgreich (Neustart noetig)'
}

$CommonErrors = @{
    0    = 'Erfolg'
    1707 = 'Installation erfolgreich'
    3010 = 'Erfolg, Neustart erforderlich'
    1602 = 'Abbruch durch Benutzer'
    1603 = 'Schwerwiegender Installationsfehler'
    1618 = 'Andere Installation laeuft bereits (MSI-Sperre)'
    1619 = 'MSI-Quellpfad nicht erreichbar'
    1633 = 'Plattform wird nicht unterstuetzt'
    1635 = 'MSI-Paket beschädigt/nicht gefunden'
}

#endregion

#region Hilfsfunktionen -------------------------------------------------------

function ConvertTo-EvalStateText {
    param([int]$Code)
    if ($EvalStates.ContainsKey($Code)) { return $EvalStates[$Code] }
    return "Unbekannter Status ($Code)"
}

function ConvertTo-InstallStateText {
    param([int]$Code)
    if ($InstallStates.ContainsKey($Code)) { return $InstallStates[$Code] }
    return "Unbekannter InstallState ($Code)"
}

function Get-ErrorText {
    param([int]$Code)
    if ($CommonErrors.ContainsKey($Code)) { return $CommonErrors[$Code] }
    $hex = '0x{0:X8}' -f $Code
    return "Code $Code ($hex)"
}

function Parse-SCCMLog {
    [CmdletBinding()]
    param([Parameter(Mandatory)][string[]]$Content)
    $re = '^<!\[LOG\[(?<msg>.*?)\]LOG\]!><time="(?<time>[^"]*)" date="(?<date>[^"]*)" component="(?<comp>[^"]*)" context="[^"]*" type="(?<type>\d)" thread="[^"]*" file="[^"]*">'
    $parsed = @()
    foreach ($line in $Content) {
        if ($line -match $re) {
            $parsed += [PSCustomObject]@{
                Time      = "$($Matches.date) $($Matches.time)"
                Component = $Matches.comp
                Type      = [int]$Matches.type
                Message   = $Matches.msg
            }
        }
    }
    return $parsed
}

function Write-DebugLine {
    param([string]$Message)
    if ($DebugDetails) {
        Write-Host "[DEBUG] $Message" -ForegroundColor DarkYellow
    }
}

function Get-RemoteCCMLog {
    [CmdletBinding()]
    param(
        [string]$ComputerName,
        [string]$LogName
    )
    $path = "\\$ComputerName\C$\Windows\CCM\Logs\$LogName"
    Write-DebugLine "Pruefe Log-Pfad: $path"

    if (-not (Test-Path $path)) {
        Write-Warning "Log nicht vorhanden: $path"
        return @()
    }

    $content = @()
    try {
        $net = New-Object -ComObject WScript.Network
        if ($CredUser -and $CredPass) {
            $net.MapNetworkDrive("Z:", "\\$ComputerName\C$", $false, $CredUser, $CredPass)
        }
        else {
            $net.MapNetworkDrive("Z:", "\\$ComputerName\C$", $false)
        }
        $content = Get-Content -Path "Z:\Windows\CCM\Logs\$LogName" -Encoding UTF8 -ErrorAction Stop
        $net.RemoveNetworkDrive("Z:", $true, $false)
    }
    catch {
        Write-Warning "Konnte $LogName nicht ueber C$ laden: $_"
        try {
            $net = New-Object -ComObject WScript.Network
            if ($CredUser -and $CredPass) {
                $net.MapNetworkDrive("Z:", "\\$ComputerName\C$", $false, $CredUser, $CredPass)
            }
            else {
                $net.MapNetworkDrive("Z:", "\\$ComputerName\C$", $false)
            }
            $content = Get-Content -Path "Z:\Windows\CCM\Logs\$LogName" -Encoding UTF8 -ErrorAction Stop
            $net.RemoveNetworkDrive("Z:", $true, $false)
        }
        catch {
            Write-Warning "Log $LogName auch im zweiten Versuch nicht lesbar: $_"
            try { $net.RemoveNetworkDrive("Z:", $true, $false) } catch {}
            return @()
        }
    }

    Write-DebugLine "$LogName geladen: $($content.Count) Zeilen"
    $entries = Parse-SCCMLog -Content $content
    Write-DebugLine "$LogName geparst: $($entries.Count) Eintraege"
    return $entries
}

#endregion

#region Zugangsdaten erfragen -------------------------------------------------

Write-Host "`n=== SCCM Log-Abruf ueber C$ ===" -ForegroundColor Cyan

if (-not [string]::IsNullOrWhiteSpace($env:SCCM_USER) -and -not [string]::IsNullOrWhiteSpace($env:SCCM_PASS)) {
    $credUser = $env:SCCM_USER
    $credPass = $env:SCCM_PASS
    Write-Host "Zugangsdaten aus Umgebungsvariablen SCCM_USER / SCCM_PASS verwendet." -ForegroundColor DarkGray
}
else {
    $credUser = Read-Host "Benutzername fuer $ComputerName (z.B. DOMAENE\Benutzer oder Benutzer)"
    $credPass = Read-Host "Passwort fuer $ComputerName" -AsSecureString
    if ($credPass) {
        $credPass = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($credPass))
    }
    else {
        $credPass = ''
    }
}

$credPassPlain = $credPass

#endregion

#region Logs laden ------------------------------------------------------------

Write-Host "`n=== Lade Logs ===" -ForegroundColor Cyan
$appAll    = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppDiscovery.log'
$appEnfAll = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppEnforce.log'
$casAll    = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'CAS.log'

Write-DebugLine "AppDiscovery: $($appAll.Count) Eintraege"
Write-DebugLine "AppEnforce: $($appEnfAll.Count) Eintraege"
Write-DebugLine "CAS: $($casAll.Count) Eintraege"

#endregion

#region Apps ueber Logs ermitteln ---------------------------------------------

$appsWithChanges = @()
$appNames = @{}

Write-DebugLine "Suche Application-Namen in AppEnforce..."
$sampleAppMatches = 0
foreach ($entry in $appEnfAll) {
    if ($entry.Message -match '(?i)application\s+"([^"]+)"') {
        $sampleAppMatches++
        if ($sampleAppMatches -le 5) {
            Write-DebugLine "Match: $($Matches[1])"
        }
        $name = $Matches[1]
        if (-not $appNames.ContainsKey($name)) {
            $appNames[$name] = $true
            $appsWithChanges += [PSCustomObject]@{
                Name       = $name
                LastChange = $entry.Time
            }
        }
    }
}
Write-DebugLine "AppEnforce-Matches gesamt: $sampleAppMatches"
Write-DebugLine "Eindeutige Anwendungsnamen: $($appNames.Count)"

if ($appsWithChanges.Count -eq 0) {
    Write-Warning "Keine Anwendungen ueber Regex in AppEnforce gefunden."
    Write-Host "`n=== DEBUG: Fallbeispiele AppEnforce ===" -ForegroundColor Yellow
    $appEnfAll | Select-Object -First 20 | ForEach-Object { Write-Host $_.Message }
    Write-Host "=== Ende DEBUG ===`n" -ForegroundColor Yellow
}

$appsWithChanges = $appsWithChanges | Sort-Object -Property LastChange -Descending
$selectedApps   = $appsWithChanges | Select-Object -First $Last

Write-DebugLine "Ausgewaehlte Anwendungen: $($selectedApps.Count)"

#endregion

#region Auswertung -------------------------------------------------------------

$results = foreach ($entry in $selectedApps) {
    $ApplicationName = $entry.Name
    $appEnf    = $appEnfAll | Where-Object { $_.Message -like "*$ApplicationName*" }
    $appDisc   = $appAll    | Where-Object { $_.Message -like "*$ApplicationName*" }

    Write-DebugLine "Auswerte: $ApplicationName"
    Write-DebugLine "  AppEnforce-Zeilen: $($appEnf.Count)"
    Write-DebugLine "  AppDiscovery-Zeilen: $($appDisc.Count)"

    # Content
    $contentIds = @()
    foreach ($e in $appEnf) {
        if ($e.Message -match 'content(?:ID| with ID)?[ =:]+([A-Fa-f0-9-]{8,})') { $contentIds += $Matches[1] }
        elseif ($e.Message -match 'ContentId ([A-Fa-f0-9-]{8,})') { $contentIds += $Matches[1] }
    }
    $contentIds = $contentIds | Sort-Object -Unique
    Write-DebugLine "  ContentIds gefunden: $($contentIds.Count)"

    $cachePaths = @()
    foreach ($cid in $contentIds) {
        $casAll | Where-Object { $_.Message -like "*$cid*" -and $_.Message -match 'location = (.+)' } | ForEach-Object {
            if ($_ -match 'location = (.+)') { $cachePaths += $Matches[1].Trim() }
        }
    }
    $cachePaths = $cachePaths | Sort-Object -Unique
    Write-DebugLine "  Cache-Pfade gefunden: $($cachePaths.Count)"

    $arrivedInCache = $cachePaths.Count -gt 0
    $cacheFolder = $cachePaths -join "`n"

    if (-not $arrivedInCache) {
        $cacheParts = $appEnf | Where-Object { $_.Message -match 'CachePath|CacheResult|Download complete|content download complete' } | Select-Object -ExpandProperty Message
        if ($cacheParts) {
            $cacheFolder = ($cacheParts -join "`n")
            $arrivedInCache = $true
            Write-DebugLine "  Cache ueber Keywords erkannt"
        }
    }

    $detectionLines = $appDisc | Where-Object { $_.Message -match 'detection method|Detected|did not detect|not detected' } | Select-Object -ExpandProperty Message
    $detectionResult = if ($detectionLines) {
        if ($detectionLines -match 'did not detect|not detected') { 'NICHT erkannt' } else { 'erkannt / ok' }
    } else { 'kein Detection-Eintrag gefunden' }

    $installStarted   = ($appEnf | Where-Object { $_.Message -match 'Starting Install enforcement|Starting enforcement' }).Count -gt 0
    $installDone      = ($appEnf | Where-Object { $_.Message -match 'Enforcement completed|enforcement completed' }).Count -gt 0
    $installErrors    = $appEnf | Where-Object { $_.Type -in @(2,3) } | Select-Object Time, Message

    $reason = ''
    if ($arrivedInCache) {
        if ($installStarted -and -not $installDone) { $reason = 'Installation laeuft' }
        elseif ($installDone) { $reason = 'Installation abgeschlossen' }
        elseif (-not $installStarted) { $reason = 'Noch nicht mit Installation begonnen' }
        else { $reason = 'Status unklar - Details pruefen' }
    } else {
        $reason = 'Content noch nicht im Cache (Download laeuft oder nicht verteilt)'
    }

    [PSCustomObject]@{
        ComputerName       = $ComputerName
        Application        = $ApplicationName
        InstallState       = if ($installDone) { 'Erfolgreich' } elseif ($installStarted) { 'In Bearbeitung' } else { 'nicht ermittelbar' }
        Angekommen         = $arrivedInCache
        CachePfad          = if ($cacheFolder) { $cacheFolder } else { '(kein Cache-Eintrag)' }
        DetectionErgebnis  = $detectionResult
        InstallStarted     = $installStarted
        InstallAbgeschlossen = $installDone
        LetzteAenderung    = $entry.LastChange
        Grund              = $reason
        InstallFehler      = if ($installErrors) { ($installErrors | ForEach-Object { "$($_.Time): $($_.Message)" }) -join "`n" } else { '(keine)' }
    }
}

#endregion

#region Ausgabe ----------------------------------------------------------------

Write-Host "`n===== SCCM-Deployment-Status: letzte $Last Anwendungen @ $ComputerName =====" -ForegroundColor Cyan
if ($results) {
    foreach ($r in $results) {
        Write-Host ("`nAnwendung        : {0}" -f $r.Application) -ForegroundColor Cyan
        Write-Host ("InstallState     : {0}" -f $r.InstallState)
        Write-Host ("Angekommen/Cache : {0}" -f $(if ($r.Angekommen) { 'JA' } else { 'NEIN' })) -ForegroundColor $(if ($r.Angekommen) { 'Green' } else { 'Yellow' })
        Write-Host ("Cache-Pfad       : {0}" -f $r.CachePfad)
        Write-Host ("Detection        : {0}" -f $r.DetectionErgebnis)
        Write-Host ("Install gestartet: {0}" -f $(if ($r.InstallStarted) { 'JA' } else { 'NEIN' }))
        Write-Host ("Install fertig   : {0}" -f $(if ($r.InstallAbgeschlossen) { 'JA' } else { 'NEIN' }))
        Write-Host ("Letzte Aenderung : {0}" -f $r.LetzteAenderung)
        Write-Host ("Grund/Status     : {0}" -f $r.Grund) -ForegroundColor $(if ($r.Grund -match 'Erfolg|abgeschlossen|erfolgreich|erkannt') { 'Green' } else { 'Yellow' })
        if ($Detailed) {
            Write-Host ("Fehlerdetails    : {0}" -f $r.InstallFehler) -ForegroundColor DarkGray
        }
    }
}
else {
    Write-Host "Es wurden keine Anwendungen zur Anzeige ausgewaehlt." -ForegroundColor Red
    Write-Host "Hinweis: Starte das Script mit -DebugDetails, um zu sehen, wo es haengt." -ForegroundColor Yellow
}
Write-Host ""

# Als Objekte zurueckgeben
$results

#endregion